Skip to content

IMSC 1.1: keep trailing <br/> forced line breaks - #16005

Merged
niksedk merged 1 commit into
SubtitleEdit:mainfrom
chiragjangiir:fix/imsc-trailing-br
Oct 13, 2026
Merged

niksedk merged 1 commit into
SubtitleEdit:mainfrom
chiragjangiir:fix/imsc-trailing-br

Conversation

@chiragjangiir

Copy link
Copy Markdown
Contributor

Summary

TimedTextImsc11 dropped trailing forced line breaks on import: a cue written as C<br/><br/><br/> loaded as C. In TTML <br/> is a forced break with layout meaning (often paired with region extent / tts:displayAlign), so the cue that the file describes is not the cue the reader produced. This is the remaining half of #15289 — the extension-routing half was fixed in #15570.

Root cause

LoadSubtitle built each cue as:

var text = assStyle + ReadParagraph(node, headIndex).TrimEnd();

ReadParagraph() maps every <br/> to AppendLine(), so the trailing breaks were present in the string and then the blanket TrimEnd() removed them along with source formatting whitespace.

Reproduced on unmodified main with a reader-level test:

Assert.Equal() Failure: Strings differ
Expected: "C\n\n\n"
Actual:   "C"

The writer already serializes text faithfully (string.Join("<br/>", text.SplitToLines())), so the loss was reader-only: load → save at format level was not lossless.

Solution

Track the end of the last forced break while reading a paragraph, and trim only the text behind it:

  • everything before that point already ends in a line break and is content, so it is left untouched — trailing <br/> breaks now survive;
  • the tail is still source formatting and is trimmed exactly as before;
  • a paragraph with no <br/> is trimmed exactly as before (raw.TrimEnd()), so behaviour for ordinary files is unchanged;
  • nested <span> content is still read raw and trimmed only in the finished paragraph, so a space inside the last span still survives (Italic_Trailing_Space_Preserved).

No changes to timing, paragraph start/end values, or the writer.

Testing

Local: Apple Silicon macOS, .NET SDK 10.0.401.

Command Result
dotnet test tests/libse/LibSETests.csproj --filter "FullyQualifiedName~TimedTextImsc11Test" before fix: 2 failed (Trailing_Forced_Line_Breaks_Are_Preserved, LoadSaveLoad_Keeps_Trailing_Forced_Line_Breaks), 7 passed; after fix: 9/9 passed
dotnet build SubtitleEdit.sln --no-restore exit 0, 0 errors, 0 warnings
dotnet test SubtitleEdit.sln --no-build (same as .github/workflows/tests.yml) LibSETests 2588/2588 passed, SeConvTests 586 passed / 2 skipped, LibUiLogicTests 1215 passed, UITests 7380 passed / 6 failed

New regression tests:

  1. Trailing_Forced_Line_Breaks_Are_Preserved — the reported case (A<br/><br/><br/>B and C<br/><br/><br/>).
  2. Trailing_Source_Whitespace_Is_Still_Trimmed — guards the whitespace half of the old TrimEnd() (inline trailing space, indented paragraph).
  3. LoadSaveLoad_Keeps_Trailing_Forced_Line_Breaks — load → ToText → load is now lossless.

git diff --check is clean; only src/libse/SubtitleFormats/TimedTextImsc11.cs and tests/libse/SubtitleFormats/TimedTextImsc11Test.cs are changed.

Limitations

  • The 6 UITests failures are pre-existing on unmodified main on this machine: I re-ran exactly those tests with my changes stashed and the same 6 failed (SyntaxHighlightingTextPresenterCanaryTests ×3, RtlSplitRunHitTestTests, AssaTagRtlAvaloniaCanaryTests, TableViewScrollBarTroughPagingTests). They are headless-Avalonia rendering/style canaries unrelated to subtitle formats. CI runs on ubuntu-latest; the real CI result is the authority for that suite.
  • GUI round trip still trims trailing line breaks — intentionally. SubtitleLineViewModel.ToParagraph() trims trailing whitespace on every save so a trailing Enter typed in the edit box can never reach a saved file (the cross-format invariant from Remove text for HI reports a trailing empty line as a change #13389). Distinguishing a loaded break from a typed one would need state the text box does not have, so I did not touch it. This PR makes the reader (and everything built on Subtitle — headless conversion, batch, plugins, the text shown in the editor) faithful; saving an untouched file from the GUI still applies the existing trimming rule.
  • Tested on macOS only (no Windows machine available); no GUI/manual verification was performed — coverage is at the SubtitleFormat API level, which is the project's supported way to test readers.

The reader built each cue as ReadParagraph(...).TrimEnd(), which also
removed the newlines a trailing <br/> adds - so "C<br/><br/><br/>" loaded
as "C" and the forced breaks were lost on import (SubtitleEdit#15289).

Trim only the text behind the last forced break instead: everything before
it already ends in a line break and is content, while the tail is still
source formatting. A paragraph without a <br/> trims exactly as before, and
nested spans are still read raw, so a space inside the last span survives.

Part of SubtitleEdit#15289

Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@niksedk
niksedk merged commit b4628d7 into SubtitleEdit:main Oct 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants