become tags; its color/font are kept as paragraph effects
- var italic = paragraphStyle.Italic && !inherited.Italic;
- var bold = paragraphStyle.Bold && !inherited.Bold;
- var underline = paragraphStyle.Underline && !inherited.Underline;
- pText.Append(italic ? "" : string.Empty).Append(bold ? "" : string.Empty).Append(underline ? "" : string.Empty);
- ReadParagraphChildren(pText, node, styles, headIndex, paragraphStyle);
- pText.Append(underline ? "" : string.Empty).Append(bold ? "" : string.Empty).Append(italic ? "" : string.Empty);
+ // italic/bold/underline in effect on the (own or from body/div) become tags;
+ // its color/font are kept as paragraph effects
+ var openTags = new List();
+ var start = pText.Length;
+ SetTtmlTextTags(pText, openTags, paragraphStyle.Italic, paragraphStyle.Bold, paragraphStyle.Underline, null);
+ ReadParagraphChildren(pText, node, styles, headIndex, paragraphStyle, openTags);
+ CloseTtmlTags(pText, openTags, 0);
+ RemoveEmptyTtmlTags(pText, start);
}
- private static void ReadParagraphChildren(StringBuilder pText, XmlNode node, List styles, TtmlHeadIndex headIndex, TtmlTextStyle parentStyle)
+ private static string GetCloseTag(string openTag)
+ {
+ return openTag.StartsWith("" : "" + openTag.Substring(1);
+ }
+
+ private static void CloseTtmlTags(StringBuilder pText, List openTags, int count)
+ {
+ while (openTags.Count > count)
+ {
+ pText.Append(GetCloseTag(openTags[openTags.Count - 1]));
+ openTags.RemoveAt(openTags.Count - 1);
+ }
+ }
+
+ ///
+ /// Makes italic/bold/underline match the given state and opens the optional font tag. A style that
+ /// turns off closes its tag (and the tags opened inside it, which are reopened).
+ ///
+ private static void SetTtmlTextTags(StringBuilder pText, List openTags, bool italic, bool bold, bool underline, string fontTag)
+ {
+ var keepCount = openTags.Count;
+ for (var i = 0; i < openTags.Count; i++)
+ {
+ var tag = openTags[i];
+ if ((tag == "" && !italic) || (tag == "" && !bold) || (tag == "" && !underline))
+ {
+ keepCount = i;
+ break;
+ }
+ }
+
+ var reopen = new List();
+ for (var i = keepCount; i < openTags.Count; i++)
+ {
+ var tag = openTags[i];
+ if (!((tag == "" && !italic) || (tag == "" && !bold) || (tag == "" && !underline)))
+ {
+ reopen.Add(tag);
+ }
+ }
+
+ CloseTtmlTags(pText, openTags, keepCount);
+
+ if (italic && !reopen.Contains("") && !openTags.Contains(""))
+ {
+ reopen.Add("");
+ }
+
+ if (bold && !reopen.Contains("") && !openTags.Contains(""))
+ {
+ reopen.Add("");
+ }
+
+ if (underline && !reopen.Contains("") && !openTags.Contains(""))
+ {
+ reopen.Add("");
+ }
+
+ if (fontTag != null)
+ {
+ reopen.Add(fontTag);
+ }
+
+ foreach (var tag in reopen)
+ {
+ pText.Append(tag);
+ openTags.Add(tag);
+ }
+ }
+
+ private static readonly Regex EmptyFontTagRegex = new Regex("]*>", RegexOptions.Compiled);
+
+ private static void RemoveEmptyTtmlTags(StringBuilder pText, int start)
+ {
+ var text = pText.ToString(start, pText.Length - start);
+ string old;
+ do
+ {
+ old = text;
+ text = EmptyFontTagRegex.Replace(text.Replace("", string.Empty).Replace("", string.Empty).Replace("", string.Empty), string.Empty);
+ } while (text != old);
+
+ pText.Length = start;
+ pText.Append(text);
+ }
+
+ private static void ReadParagraphChildren(StringBuilder pText, XmlNode node, List styles, TtmlHeadIndex headIndex, TtmlTextStyle parentStyle, List openTags)
{
foreach (XmlNode child in node.ChildNodes)
{
@@ -1374,68 +1462,39 @@ private static void ReadParagraphChildren(StringBuilder pText, XmlNode node, Lis
var spanStyle = parentStyle.Clone();
ApplyTtmlTextStyle(spanStyle, child, styles, headIndex);
- var isItalic = spanStyle.Italic && !parentStyle.Italic;
- var isBold = spanStyle.Bold && !parentStyle.Bold;
- var isUnderlined = spanStyle.Underline && !parentStyle.Underline;
var fontFamily = !string.IsNullOrEmpty(spanStyle.FontFamily) && spanStyle.FontFamily != parentStyle.FontFamily &&
!(parentStyle.FontFamily == null && TtmlGenericFontFamilies.Contains(spanStyle.FontFamily))
? spanStyle.FontFamily
: null;
var color = !string.IsNullOrEmpty(spanStyle.Color) && !IsSameTtmlColor(spanStyle.Color, parentStyle.Color) ? spanStyle.Color : null;
-
- // Applying styles
- if (isItalic)
- {
- pText.Append("");
- }
-
- if (isBold)
- {
- pText.Append("");
- }
-
- if (isUnderlined)
- {
- pText.Append("");
- }
-
+ string fontTag = null;
if (!string.IsNullOrEmpty(fontFamily) || !string.IsNullOrEmpty(color))
{
- pText.Append("");
+ fontTag = "";
}
- ReadParagraphChildren(pText, child, styles, headIndex, spanStyle);
-
- if (!string.IsNullOrEmpty(fontFamily) || !string.IsNullOrEmpty(color))
- {
- pText.Append("");
- }
+ // Applying styles - tags turned off by the span are closed and reopened after it
+ var parentTags = new List(openTags);
+ SetTtmlTextTags(pText, openTags, spanStyle.Italic, spanStyle.Bold, spanStyle.Underline, fontTag);
+ var spanTags = new List(openTags);
- if (isUnderlined)
- {
- pText.Append("");
- }
+ ReadParagraphChildren(pText, child, styles, headIndex, spanStyle, openTags);
- if (isBold)
+ // restore the parent's tags, reusing the ones still open
+ var common = 0;
+ while (common < parentTags.Count && common < spanTags.Count && parentTags[common] == spanTags[common])
{
- pText.Append("");
+ common++;
}
- if (isItalic)
+ CloseTtmlTags(pText, openTags, common);
+ for (var i = common; i < parentTags.Count; i++)
{
- pText.Append("");
+ pText.Append(parentTags[i]);
+ openTags.Add(parentTags[i]);
}
}
}
diff --git a/src/libse/SubtitleFormats/TimedTextImsc11.cs b/src/libse/SubtitleFormats/TimedTextImsc11.cs
index 63cbad402ea..77396d900cf 100644
--- a/src/libse/SubtitleFormats/TimedTextImsc11.cs
+++ b/src/libse/SubtitleFormats/TimedTextImsc11.cs
@@ -626,7 +626,7 @@ public override void LoadSubtitle(Subtitle subtitle, List lines, string
assStyle = GetAssStyleFromRegion(region.InnerText, headIndex);
}
- var text = assStyle + ReadParagraph(node, headIndex).TrimEnd();
+ var text = assStyle + ReadParagraph(node, headIndex);
// Keep the region name: the alignment tag above only snaps to the screen thirds,
// the video preview positions the line from the region box itself.
@@ -637,9 +637,30 @@ public override void LoadSubtitle(Subtitle subtitle, List lines, string
subtitle.Renumber();
}
+ ///
+ /// Reads one TTML p element into subtitle text. Trailing source whitespace is
+ /// trimmed here, but the line breaks a trailing
adds are content: in TTML they are
+ /// forced breaks with layout meaning, so dropping them changes the cue (#15289). Nested
+ /// spans are read raw - only the finished paragraph is trimmed, so a space inside the
+ /// last span still survives.
+ ///
private static string ReadParagraph(XmlNode node, TtmlHeadIndex headIndex)
+ {
+ var raw = BuildParagraph(node, headIndex, out var forcedBreakEnd);
+ if (forcedBreakEnd < 0)
+ {
+ return raw.TrimEnd();
+ }
+
+ // Everything before the last forced break already ends in a line break, so only the
+ // text behind it can be source formatting - that tail is trimmed as before.
+ return raw[..forcedBreakEnd] + raw[forcedBreakEnd..].TrimEnd();
+ }
+
+ private static string BuildParagraph(XmlNode node, TtmlHeadIndex headIndex, out int forcedBreakEnd)
{
var pText = new StringBuilder();
+ forcedBreakEnd = -1;
foreach (XmlNode child in node.ChildNodes)
{
if (child.NodeType == XmlNodeType.Text)
@@ -649,6 +670,7 @@ private static string ReadParagraph(XmlNode node, TtmlHeadIndex headIndex)
else if (child.Name == "br" || child.Name == "tt:br")
{
pText.AppendLine();
+ forcedBreakEnd = pText.Length;
}
else if (child.Name == "#significant-whitespace" || child.Name == "tt:#significant-whitespace")
{
@@ -787,7 +809,13 @@ private static string ReadParagraph(XmlNode node, TtmlHeadIndex headIndex)
pText.Append(">");
}
- pText.Append(ReadParagraph(child, headIndex));
+ var contentStart = pText.Length;
+ var content = BuildParagraph(child, headIndex, out var nestedBreakEnd);
+ pText.Append(content);
+ if (nestedBreakEnd >= 0)
+ {
+ forcedBreakEnd = contentStart + nestedBreakEnd;
+ }
if (!string.IsNullOrEmpty(fontFamily) || !string.IsNullOrEmpty(color))
{
diff --git a/src/libse/VobSub/IfoParser.cs b/src/libse/VobSub/IfoParser.cs
index e59e7338508..fa601455c4d 100644
--- a/src/libse/VobSub/IfoParser.cs
+++ b/src/libse/VobSub/IfoParser.cs
@@ -336,6 +336,8 @@ public static string GetIfoFileName(string vobFileName)
/// A file in named ignoring case (a DVD
/// copied to a case sensitive file system may be all lowercase: video_ts/vts_01_1.vob), or null.
///