using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Nikse.SubtitleEdit.Core
{
///
/// HTML specific string manipulations.
///
public static class HtmlUtil
{
public static string TagItalic => "i";
public static string TagBold => "b";
public static string TagUnderline => "u";
public static string TagParagraph => "p";
public static string TagFont => "font";
public static string TagCyrillicI => "\u0456"; // Cyrillic Small Letter Byelorussian-Ukrainian i (http://graphemica.com/%D1%96)
private static readonly Regex TagOpenRegex = new Regex(@"<\s*(?:/\s*)?(\w+)[^>]*>", RegexOptions.Compiled);
///
/// Remove all of the specified opening and closing tags from the source HTML string.
///
/// The source string to search for specified HTML tags.
/// The HTML tags to remove.
/// A new string without the specified opening and closing tags.
public static string RemoveOpenCloseTags(string source, params string[] tags)
{
if (string.IsNullOrEmpty(source) || source.IndexOf('<') < 0)
{
return source;
}
// This pattern matches these tag formats:
//
// < tag*>
//
// < /tag*>
// tag*>
// < / tag*>
return TagOpenRegex.Replace(
source,
m => tags.Contains(m.Groups[1].Value, StringComparer.OrdinalIgnoreCase) ? string.Empty : m.Value);
}
///
/// Converts a string to an HTML-encoded string using named character references.
///
/// The string to encode.
/// An encoded string.
public static string EncodeNamed(string source)
{
if (string.IsNullOrEmpty(source))
{
return string.Empty;
}
var encoded = new StringBuilder(source.Length);
foreach (var ch in source)
{
switch (ch)
{
case '<':
encoded.Append("<");
break;
case '>':
encoded.Append(">");
break;
case '"':
encoded.Append(""");
break;
case '&':
encoded.Append("&");
break;
case '\'':
encoded.Append("'");
break;
case ' ':
encoded.Append(" ");
break;
case '–':
encoded.Append("–");
break;
case '—':
encoded.Append("—");
break;
case '¡':
encoded.Append("¡");
break;
case '¿':
encoded.Append("¿");
break;
case '“':
encoded.Append("“");
break;
case '”':
encoded.Append("”");
break;
case '‘':
encoded.Append("‘");
break;
case '’':
encoded.Append("’");
break;
case '«':
encoded.Append("«");
break;
case '»':
encoded.Append("»");
break;
case '¢':
encoded.Append("¢");
break;
case '©':
encoded.Append("©");
break;
case '÷':
encoded.Append("÷");
break;
case 'µ':
encoded.Append("µ");
break;
case '·':
encoded.Append("·");
break;
case '¶':
encoded.Append("¶");
break;
case '±':
encoded.Append("±");
break;
case '€':
encoded.Append("€");
break;
case '£':
encoded.Append("£");
break;
case '®':
encoded.Append("®");
break;
case '§':
encoded.Append("§");
break;
case '™':
encoded.Append("™");
break;
case '¥':
encoded.Append("¥");
break;
case 'á':
encoded.Append("á");
break;
case 'Á':
encoded.Append("Á");
break;
case 'à':
encoded.Append("à");
break;
case 'À':
encoded.Append("À");
break;
case 'â':
encoded.Append("â");
break;
case 'Â':
encoded.Append("Â");
break;
case 'å':
encoded.Append("å");
break;
case 'Å':
encoded.Append("Å");
break;
case 'ã':
encoded.Append("ã");
break;
case 'Ã':
encoded.Append("Ã");
break;
case 'ä':
encoded.Append("ä");
break;
case 'Ä':
encoded.Append("Ä");
break;
case 'æ':
encoded.Append("æ");
break;
case 'Æ':
encoded.Append("Æ");
break;
case 'ç':
encoded.Append("ç");
break;
case 'Ç':
encoded.Append("Ç");
break;
case 'é':
encoded.Append("é");
break;
case 'É':
encoded.Append("É");
break;
case 'è':
encoded.Append("è");
break;
case 'È':
encoded.Append("È");
break;
case 'ê':
encoded.Append("ê");
break;
case 'Ê':
encoded.Append("Ê");
break;
case 'ë':
encoded.Append("ë");
break;
case 'Ë':
encoded.Append("Ë");
break;
case 'í':
encoded.Append("í");
break;
case 'Í':
encoded.Append("Í");
break;
case 'ì':
encoded.Append("ì");
break;
case 'Ì':
encoded.Append("Ì");
break;
case 'î':
encoded.Append("î");
break;
case 'Î':
encoded.Append("Î");
break;
case 'ï':
encoded.Append("ï");
break;
case 'Ï':
encoded.Append("Ï");
break;
case 'ñ':
encoded.Append("ñ");
break;
case 'Ñ':
encoded.Append("Ñ");
break;
case 'ó':
encoded.Append("ó");
break;
case 'Ó':
encoded.Append("Ó");
break;
case 'ò':
encoded.Append("ò");
break;
case 'Ò':
encoded.Append("Ò");
break;
case 'ô':
encoded.Append("ô");
break;
case 'Ô':
encoded.Append("Ô");
break;
case 'ø':
encoded.Append("ø");
break;
case 'Ø':
encoded.Append("Ø");
break;
case 'õ':
encoded.Append("õ");
break;
case 'Õ':
encoded.Append("Õ");
break;
case 'ö':
encoded.Append("ö");
break;
case 'Ö':
encoded.Append("Ö");
break;
case 'ß':
encoded.Append("ß");
break;
case 'ú':
encoded.Append("ú");
break;
case 'Ú':
encoded.Append("Ú");
break;
case 'ù':
encoded.Append("ù");
break;
case 'Ù':
encoded.Append("Ù");
break;
case 'û':
encoded.Append("û");
break;
case 'Û':
encoded.Append("Û");
break;
case 'ü':
encoded.Append("ü");
break;
case 'Ü':
encoded.Append("Ü");
break;
case 'ÿ':
encoded.Append("ÿ");
break;
default:
if (ch > 127)
{
encoded.Append("" + (int)ch + ";");
}
else
{
encoded.Append(ch);
}
break;
}
}
return encoded.ToString();
}
///
/// Converts a string to an HTML-encoded string using numeric character references.
///
/// The string to encode.
/// An encoded string.
public static string EncodeNumeric(string source)
{
if (string.IsNullOrEmpty(source))
{
return string.Empty;
}
var encoded = new StringBuilder(source.Length);
foreach (var ch in source)
{
if (ch == ' ')
{
encoded.Append("");
encoded.Append(160); //
encoded.Append(';');
}
else if (ch > 127 || ch == '<' || ch == '>' || ch == '"' || ch == '&' || ch == '\'')
{
encoded.Append("");
encoded.Append((int)ch);
encoded.Append(';');
}
else
{
encoded.Append(ch);
}
}
return encoded.ToString();
}
public static string RemoveHtmlTags(string input, bool alsoSsaTags = false)
{
if (input == null || input.Length < 3)
{
return input;
}
var s = input;
if (alsoSsaTags)
{
s = Utilities.RemoveSsaTags(s);
}
if (!s.Contains('<'))
{
return s;
}
if (s.Contains("< "))
{
s = FixInvalidItalicTags(s);
}
s = s.Replace("", string.Empty).Replace("", string.Empty);
return RemoveCommonHtmlTags(s);
}
///
/// Optimized method to remove common html tags, like , , , and
///
/// Text to remove html tags from
/// Text stripped from common html tags
private static string RemoveCommonHtmlTags(string s)
{
char[] array = new char[s.Length];
int arrayIndex = 0;
bool inside = false;
for (int i = 0; i < s.Length; i++)
{
char ch = s[i];
if (ch == '<' && i < s.Length - 2)
{
var next = s[i + 1];
var nextNext = s[i + 2];
if (nextNext == '>' &&
(next == 'i' || //
next == 'I' || //
next == 'b' || //
next == 'B' || //
next == 'u' || //
next == 'U')) //
{
inside = true;
continue;
}
if (next == '/' && i < s.Length - 3)
{
var nextNextNext = s[i + 3];
if (nextNextNext == '>' &&
(nextNext == 'i' || //
nextNext == 'I' || //
nextNext == 'b' || //
nextNext == 'B' || //
nextNext == 'u' || //
nextNext == 'U')) //
{
inside = true;
continue;
}
}
if (nextNext == '/' && i < s.Length - 3)
{ // some bad end tags sometimes seen
var nextNextNext = s[i + 3];
if (nextNextNext == '>' &&
(next == 'i' || //
next == 'I' || //
next == 'b' || //
next == 'B' || //
next == 'u' || //
next == 'U')) //
{
inside = true;
continue;
}
}
if ((next == 'f' || next == 'F') && s.Substring(i).StartsWith("", StringComparison.OrdinalIgnoreCase)) //
{
inside = true;
continue;
}
}
if (inside && ch == '>')
{
inside = false;
continue;
}
if (!inside)
{
array[arrayIndex] = ch;
arrayIndex++;
}
}
return new string(array, 0, arrayIndex);
}
public static bool IsUrl(string text)
{
if (string.IsNullOrWhiteSpace(text) || text.Length < 6 || !text.Contains('.') || text.Contains(' '))
{
return false;
}
var allLower = text.ToLowerInvariant();
if (allLower.StartsWith("http://", StringComparison.Ordinal) || allLower.StartsWith("https://", StringComparison.Ordinal) ||
allLower.StartsWith("www.", StringComparison.Ordinal) || allLower.EndsWith(".org", StringComparison.Ordinal) ||
allLower.EndsWith(".com", StringComparison.Ordinal) || allLower.EndsWith(".net", StringComparison.Ordinal))
{
return true;
}
if (allLower.Contains(".org/") || allLower.Contains(".com/") || allLower.Contains(".net/"))
{
return true;
}
return false;
}
public static bool StartsWithUrl(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
var arr = text.Trim().TrimEnd('.').TrimEnd().Split();
if (arr.Length == 0)
{
return false;
}
return IsUrl(arr[0]);
}
private static readonly string[] UppercaseTags = { "", "", "", "", "", "", "" };
public static string FixUpperTags(string input)
{
if (string.IsNullOrEmpty(input) || !input.Contains('<'))
{
return input;
}
var text = input;
var idx = text.IndexOfAny(UppercaseTags, StringComparison.Ordinal);
while (idx >= 0)
{
var endIdx = text.IndexOf('>', idx + 2);
if (endIdx < idx)
{
break;
}
var tag = text.Substring(idx, endIdx - idx).ToLowerInvariant();
text = text.Remove(idx, endIdx - idx).Insert(idx, tag);
idx = text.IndexOfAny(UppercaseTags, StringComparison.Ordinal);
}
return text;
}
public static string FixInvalidItalicTags(string input)
{
var text = input;
const string beginTag = "";
const string endTag = "";
text = text.Replace("< i >", beginTag);
text = text.Replace("< i>", beginTag);
text = text.Replace("", beginTag);
text = text.Replace("< I >", beginTag);
text = text.Replace("< I>", beginTag);
text = text.Replace("", beginTag);
text = text.Replace("< / i >", endTag);
text = text.Replace("< /i>", endTag);
text = text.Replace(" i>", endTag);
text = text.Replace("< /i >", endTag);
text = text.Replace("", endTag);
text = text.Replace(" i >", endTag);
text = text.Replace("< / i>", endTag);
text = text.Replace("", endTag);
text = text.Replace("< / I >", endTag);
text = text.Replace("< /I>", endTag);
text = text.Replace(" I>", endTag);
text = text.Replace("< /I >", endTag);
text = text.Replace("", endTag);
text = text.Replace(" I >", endTag);
text = text.Replace("< / I>", endTag);
text = text.Replace("", beginTag);
text = text.Replace("", endTag);
text = text.Replace(" ", "_@_");
text = text.Replace(" _@_", "_@_");
text = text.Replace(" _@_ ", "_@_");
text = text.Replace("_@_", " ");
if (text.Contains(beginTag))
{
text = text.Replace("", endTag);
text = text.Replace("", endTag);
}
else
{
text = text.Replace("", string.Empty);
text = text.Replace("", string.Empty);
}
text = text.Replace(beginTag + beginTag, beginTag);
text = text.Replace(endTag + endTag, endTag);
int italicBeginTagCount = Utilities.CountTagInText(text, beginTag);
int italicEndTagCount = Utilities.CountTagInText(text, endTag);
int noOfLines = Utilities.GetNumberOfLines(text);
if (italicBeginTagCount + italicEndTagCount > 0)
{
if (italicBeginTagCount == 1 && italicEndTagCount == 1 && text.IndexOf(beginTag, StringComparison.Ordinal) > text.IndexOf(endTag, StringComparison.Ordinal))
{
const string pattern = "___________@";
text = text.Replace(beginTag, pattern);
text = text.Replace(endTag, beginTag);
text = text.Replace(pattern, endTag);
}
if (italicBeginTagCount == 2 && italicEndTagCount == 0)
{
int firstIndex = text.IndexOf(beginTag, StringComparison.Ordinal);
int lastIndex = text.LastIndexOf(beginTag, StringComparison.Ordinal);
int lastIndexWithNewLine = text.LastIndexOf(Environment.NewLine + beginTag, StringComparison.Ordinal) + Environment.NewLine.Length;
if (noOfLines == 2 && lastIndex == lastIndexWithNewLine && firstIndex < 2)
{
text = text.Replace(Environment.NewLine, endTag + Environment.NewLine) + endTag;
}
else
{
text = text.Remove(lastIndex, beginTag.Length).Insert(lastIndex, endTag);
}
}
if (italicBeginTagCount == 1 && italicEndTagCount == 2)
{
int firstIndex = text.IndexOf(endTag, StringComparison.Ordinal);
if (text.StartsWith("--", StringComparison.Ordinal) ||
text.StartsWith("- -", StringComparison.Ordinal) ||
text.StartsWith("- -", StringComparison.Ordinal) ||
text.StartsWith("- -", StringComparison.Ordinal))
{
text = text.Remove(0, 5);
}
else if (firstIndex == 0)
{
text = text.Remove(0, 4);
}
else
{
text = text.Substring(0, firstIndex) + text.Substring(firstIndex + endTag.Length);
}
}
if (italicBeginTagCount == 2 && italicEndTagCount == 1)
{
var lines = text.SplitToLines();
if (lines.Count == 2 && lines[0].StartsWith(beginTag, StringComparison.Ordinal) && lines[0].EndsWith(endTag, StringComparison.Ordinal) &&
lines[1].StartsWith(beginTag, StringComparison.Ordinal))
{
text = text.TrimEnd() + endTag;
}
else
{
int lastIndex = text.LastIndexOf(beginTag, StringComparison.Ordinal);
if (text.Length > lastIndex + endTag.Length)
{
text = text.Substring(0, lastIndex) + text.Substring(lastIndex - 1 + endTag.Length);
}
else
{
text = text.Substring(0, lastIndex - 1) + endTag;
}
}
if (text.StartsWith(beginTag, StringComparison.Ordinal) && text.EndsWith(endTag, StringComparison.Ordinal) && text.Contains(endTag + Environment.NewLine + beginTag))
{
text = text.Replace(endTag + Environment.NewLine + beginTag, Environment.NewLine);
}
}
if (italicBeginTagCount == 1 && italicEndTagCount == 0)
{
int lastIndexWithNewLine = text.LastIndexOf(Environment.NewLine + beginTag, StringComparison.Ordinal) + Environment.NewLine.Length;
int lastIndex = text.LastIndexOf(beginTag, StringComparison.Ordinal);
if (text.StartsWith(beginTag, StringComparison.Ordinal))
{
text += endTag;
}
else if (noOfLines == 2 && lastIndex == lastIndexWithNewLine)
{
text += endTag;
}
else
{
text = text.Replace(beginTag, string.Empty);
}
}
if (italicBeginTagCount == 0 && italicEndTagCount == 1)
{
var cleanText = RemoveOpenCloseTags(text, TagItalic, TagBold, TagUnderline, TagCyrillicI);
bool isFixed = false;
// Foo.
if (text.EndsWith(endTag, StringComparison.Ordinal) && !cleanText.StartsWith('-') && !cleanText.Contains(Environment.NewLine + "-"))
{
text = beginTag + text;
isFixed = true;
}
// - Foo | - Foo.
// - Bar. | - Foo.
if (!isFixed && Utilities.GetNumberOfLines(cleanText) == 2)
{
int newLineIndex = text.IndexOf(Environment.NewLine, StringComparison.Ordinal);
if (newLineIndex > 0)
{
var firstLine = text.Substring(0, newLineIndex).Trim();
var secondLine = text.Substring(newLineIndex + 2).Trim();
if (firstLine.EndsWith(endTag, StringComparison.Ordinal))
{
firstLine = beginTag + firstLine;
isFixed = true;
}
if (secondLine.EndsWith(endTag, StringComparison.Ordinal))
{
secondLine = beginTag + secondLine;
isFixed = true;
}
text = firstLine + Environment.NewLine + secondLine;
}
}
if (!isFixed)
{
text = text.Replace(endTag, string.Empty);
}
}
// - foo.
// - bar.
if (italicBeginTagCount == 0 && italicEndTagCount == 2 && text.Contains(endTag + Environment.NewLine, StringComparison.Ordinal) && text.EndsWith(endTag, StringComparison.Ordinal))
{
text = text.Replace(endTag, string.Empty);
text = beginTag + text + endTag;
}
if (italicBeginTagCount == 0 && italicEndTagCount == 2 && text.StartsWith(endTag, StringComparison.Ordinal) && text.EndsWith(endTag, StringComparison.Ordinal))
{
int firstIndex = text.IndexOf(endTag, StringComparison.Ordinal);
text = text.Remove(firstIndex, endTag.Length).Insert(firstIndex, beginTag);
}
// Foo
// Bar
if (italicBeginTagCount == 2 && italicEndTagCount == 2 && noOfLines == 2)
{
int index = text.IndexOf(Environment.NewLine, StringComparison.Ordinal);
if (index > 0 && text.Length > index + (beginTag.Length + endTag.Length))
{
var firstLine = text.Substring(0, index).Trim();
var secondLine = text.Substring(index + Environment.NewLine.Length).Trim();
if (firstLine.Length > 10 && firstLine.StartsWith("- ", StringComparison.Ordinal) && firstLine.EndsWith(endTag, StringComparison.Ordinal))
{
text = "- " + firstLine.Remove(0, 5) + Environment.NewLine + secondLine;
text = text.Replace("- ", "- ");
index = text.IndexOf(Environment.NewLine, StringComparison.Ordinal);
firstLine = text.Substring(0, index).Trim();
secondLine = text.Substring(index + Environment.NewLine.Length).Trim();
}
if (secondLine.Length > 10 && secondLine.StartsWith("- ", StringComparison.Ordinal) && secondLine.EndsWith(endTag, StringComparison.Ordinal))
{
text = firstLine + Environment.NewLine + "- " + secondLine.Remove(0, 5);
text = text.Replace("- ", "- ");
index = text.IndexOf(Environment.NewLine, StringComparison.Ordinal);
firstLine = text.Substring(0, index).Trim();
secondLine = text.Substring(index + Environment.NewLine.Length).Trim();
}
if (Utilities.StartsAndEndsWithTag(firstLine, beginTag, endTag) && Utilities.StartsAndEndsWithTag(secondLine, beginTag, endTag))
{
text = text.Replace(beginTag, string.Empty).Replace(endTag, string.Empty).Trim();
text = beginTag + text + endTag;
}
}
//FALCONE: I didn't think
it was going to be you,
var colIdx = text.IndexOf(':');
if (colIdx >= 0 && Utilities.CountTagInText(text, beginTag) + Utilities.CountTagInText(text, endTag) == 4 && text.Length > colIdx + 1 && !char.IsDigit(text[colIdx + 1]))
{
var firstLine = text.Substring(0, index);
var secondLine = text.Substring(index).TrimStart();
var secIdxCol = secondLine.IndexOf(':');
if (secIdxCol < 0 || !Utilities.IsBetweenNumbers(secondLine, secIdxCol))
{
var idx = firstLine.IndexOf(':');
if (idx > 1)
{
var pre = text.Substring(0, idx + 1).TrimStart();
text = text.Remove(0, idx + 1);
text = FixInvalidItalicTags(text).Trim();
if (text.StartsWith(" ", StringComparison.OrdinalIgnoreCase))
{
text = Utilities.RemoveSpaceBeforeAfterTag(text, beginTag);
}
text = pre + " " + text;
}
}
}
}
//- You think they're they gone?
//- That can't be.
if (italicBeginTagCount == 3 && italicEndTagCount == 1 && noOfLines == 2)
{
var newLineIdx = text.IndexOf(Environment.NewLine, StringComparison.Ordinal);
var firstLine = text.Substring(0, newLineIdx).Trim();
var secondLine = text.Substring(newLineIdx).Trim();
if ((Utilities.StartsAndEndsWithTag(firstLine, beginTag, beginTag) && Utilities.StartsAndEndsWithTag(secondLine, beginTag, endTag)) ||
(Utilities.StartsAndEndsWithTag(secondLine, beginTag, beginTag) && Utilities.StartsAndEndsWithTag(firstLine, beginTag, endTag)))
{
text = text.Replace(beginTag, string.Empty);
text = text.Replace(endTag, string.Empty);
text = text.Replace(" ", " ").Trim();
text = beginTag + text + endTag;
}
}
if (noOfLines == 3)
{
var lines = text.SplitToLines();
if ((italicBeginTagCount == 3 && italicEndTagCount == 2) || (italicBeginTagCount == 2 && italicEndTagCount == 3))
{
int numberOfItalics = 0;
foreach (var line in lines)
{
if (line.StartsWith(beginTag, StringComparison.Ordinal))
{
numberOfItalics++;
}
if (line.EndsWith(endTag, StringComparison.Ordinal))
{
numberOfItalics++;
}
}
if (numberOfItalics == 5)
{ // fix missing tag
text = "" + text.Replace("", string.Empty).Replace("", string.Empty) + "";
}
}
}
text = text.Replace("", string.Empty);
text = text.Replace(" ", string.Empty);
text = text.Replace(" ", string.Empty);
}
return text;
}
public static string ToggleTag(string input, string tag)
{
var text = input;
if (text.IndexOf("<" + tag + ">", StringComparison.OrdinalIgnoreCase) >= 0 ||
text.IndexOf("" + tag + ">", StringComparison.OrdinalIgnoreCase) >= 0)
{
text = text.Replace("<" + tag + ">", string.Empty);
text = text.Replace("" + tag + ">", string.Empty);
text = text.Replace("<" + tag.ToUpperInvariant() + ">", string.Empty);
text = text.Replace("" + tag.ToUpperInvariant() + ">", string.Empty);
}
else
{
int indexOfEndBracket = text.IndexOf('}');
if (text.StartsWith("{\\", StringComparison.Ordinal) && indexOfEndBracket > 1 && indexOfEndBracket < 6)
{
text = $"{text.Substring(0, indexOfEndBracket + 1)}<{tag}>{text.Remove(0, indexOfEndBracket + 1)}{tag}>";
}
else
{
text = $"<{tag}>{text}{tag}>";
}
}
return text;
}
}
}