diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md
index aeb4230..a5dac5d 100644
--- a/docs/SESSION-STATE.md
+++ b/docs/SESSION-STATE.md
@@ -2,6 +2,30 @@
Updated: 2026-10-11
+## Raw multiline heading fold reproduced and corrected - 2026-10-11
+
+Exact764c7ac / CI38179931820 is terminal: focused20/21, native98/99,
+published98/99, zero skips. All failures are the newly added raw-HTML case at
+its complete-header fold assertion. Published failure prevents the new installed
+registration lifecycle readbacks from running; do not claim those passed.
+Retain current-focused-764c7ac.log/current-native-764c7ac.log and downloaded TRXs
+under artifacts/reader-regressions-38179931820 and
+artifacts/current-installed-evidence-38179931820.
+
+The parser mapped a raw heading's end to its opening line, hiding remaining
+multiline attributes/title/closing syntax when folded. Three actual failing core
+regressions reproduced this; existing AngleSharp token spans now retain the
+complete header, respecting quoted attributes, comments and nested title markup.
+All34 source mapping/folding cases and all589 core tests pass with100% measured
+line coverage. Release build has zero warnings/errors and format verification
+passes. The unchanged raw-HTML runtime case and full distributed suites must
+verify the correction on the next pushed head.
+
+No production publication, signing verification, OS default selection or local
+desktop input. Preserve owner preview55472. Actual chooser/Explorer, concurrent
+stable/dev isolation, full fidelity audit, genuine maintainer review and final
+signed release artifacts remain required.
+
## All distributed source cases pass; next release acceptance checks - 2026-10-11
Exact9994eda CI38176727468 is terminal success: focused20/20, native98/98,
@@ -10,7 +34,7 @@ Actual AppKit apply/restart advances stable0.1.0→0.4.0 and devbeta.1→beta.2;
post-upgrade channel/versions and target DLL hashes are verified by the harness.
Retain actual TRXs and before/after JSON under artifacts/current-installed-evidence-38176727468.
These fixtures are unsigned and sequential; they do not prove production signing
-or simultaneous stable/dev coexistence. Signed cleanupc89ce5c remains local.
+or simultaneous stable/dev coexistence. Signed cleanupc89ce5c was pushed as parent of764c7ac.
New read-only harness checks require all five Open With/Default Apps extensions
at real initial install and after upgrade, the same owned ProgID, complete owned
diff --git a/docs/TODO.md b/docs/TODO.md
index 60577ea..9345044 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -3,7 +3,9 @@
- [x] Exact6374611: native97/98 and published97/98, focused19/20, zero skips; actual sustained colors/hover/focus and outline pass.
- [x] Exact9994eda: offscreen native geometry correction passes focused20/20 and full native98/98; original focus/both-direction glyph/wheel assertions unchanged.
- [x] Exact9994eda: published98/98 and installed stable/dev97/97 each, real update/apply/restart and target hashes pass; zero skips.
-- [ ] Verify the clean1.0 milestone candidate, actual install/update/uninstall registration readbacks and new raw-HTML runtime case on the next exact head.
+- [x] Exact764c7ac: retain focused20/21, native98/99 and published98/99; only the new raw multiline-heading fold fails, zero skips. Installed lifecycle phase is not reached.
+- [x] Reproduce multiline raw-header loss in core tests and correct parser token end spans;589 core/100% measured lines, zero-warning Release/format pass.
+- [ ] Verify the correction with unchanged raw-HTML runtime assertions and actual install/update/uninstall registration readbacks on the next exact head.
- [x] Generate and publish the GUI-only AppKit application with the approved identity.
- [x] Configure/read back repository protections and establish the initial hosted CI baseline.
diff --git a/docs/standards-readiness.md b/docs/standards-readiness.md
index 7c70643..a13feb5 100644
--- a/docs/standards-readiness.md
+++ b/docs/standards-readiness.md
@@ -10,7 +10,7 @@ The full port and public release are not complete; do not add a conformance badg
| Required inventory | `K22-REPO-42` remains pending: signed inventory proposal6b4fd17 is in standards draftPR36; its checks pass. App/starter are Product with adoption-in-progress status. Review/merge is required before claiming registration in main. |
| Shared community defaults | `K22-REPO-10` warns: no owner `.github` repository. App governance/security/contributing files exist; shared owner defaults remain optional setup work. |
| Workflow reviews | `K22-CI-02/03/31` require review of PR/push tests, documented command alignment and full release gate reuse. Current callers visibly run PR/push(main) CI and the release reuses CI/security. No human review acknowledgement is inferred. |
-| Test evidence | Exact9994eda passes586 core tests/100% measured lines, focused20/20 and full native98/98, zero skips. CI38176727468 is terminal: published98/98 and installed stable/dev97/97 each, zero skips; actual apply/restart passes both. Diagnostic cleanup needs fresh exact-head acceptance. |
+| Test evidence | Exact9994eda passes586 core tests/100% measured lines, focused20/20 and full native98/98, zero skips. CI38176727468 is terminal: published98/98 and installed stable/dev97/97 each, zero skips; actual apply/restart passes both. Diagnostic-free764c7ac passes98/99 native/published and20/21 focused: the new multiline raw-header fold fails. Local correction passes589 core/100% lines; fresh native/installed registration acceptance remains required. |
| Release | Stable/dev synthetic installed updates call AppKit apply/restart successfully. Actual verified Azure identity/profile/signer permissions and exact signed distributed artifacts remain deferred. Owner authorized stable1.0/dev publication after all requirements pass; signing and review requirements remain unmet. |
Full acceptance remains in `docs/PRODUCT.md` and `docs/TODO.md`; maintainer review,
diff --git a/src/MarkdownViewr.Windows.Core/Documents/DocumentSourceMap.cs b/src/MarkdownViewr.Windows.Core/Documents/DocumentSourceMap.cs
index d3a84c2..98a5f86 100644
--- a/src/MarkdownViewr.Windows.Core/Documents/DocumentSourceMap.cs
+++ b/src/MarkdownViewr.Windows.Core/Documents/DocumentSourceMap.cs
@@ -73,7 +73,7 @@ internal sealed class DocumentSourceMap
else if(node is HtmlInline inline && IsHeadingTag(inline.Tag))
{
int start = offset + inline.Span.Start;
- inline.Tag = inline.Tag.Insert(3, Attribute(AddPosition(start, EndOfLine(start))));
+ inline.Tag = inline.Tag.Insert(3, Attribute(AddPosition(start, EndOfLine(offset + inline.Span.End))));
}
}
return Markdown.ToHtml(document, pipeline);
@@ -110,12 +110,41 @@ internal sealed class DocumentSourceMap
+ /// Tokenizer spans preserve complete raw headings across lines without mistaking quoted attributes or comments for closing syntax.
+ private static HtmlParser CreateSourceParser(Dictionary headingEnds)
+ {
+ int pendingHeading = -1;
+ return new HtmlParser(new HtmlParserOptions
+ {
+ IsKeepingSourceReferences = true,
+ OnToken = (token, range) =>
+ {
+ if(token.Name.Length == 2 && token.Name[0] == 'h' && token.Name[1] is >= '1' and <= '6')
+ {
+ if(token.Type == HtmlTokenType.StartTag)
+ {
+ pendingHeading = range.Start.Index;
+ headingEnds.Add(pendingHeading, range.End.Index - 1);
+ }
+ else if(token.Type == HtmlTokenType.EndTag && pendingHeading >= 0)
+ {
+ headingEnds[pendingHeading] = range.End.Index - 1;
+ pendingHeading = -1;
+ }
+ }
+ },
+ });
+ }
+
+
+
/// Source slices retain quote/list indentation offsets while emitted raw HTML omits those Markdown prefixes.
private void AnnotateHtmlBlock(HtmlBlock block, int offset)
{
StringLine[] lines = block.Lines.Lines;
string html = string.Join('\n', lines.Take(block.Lines.Count).Select(line => line.Slice.ToString()));
- using AngleSharp.Html.Dom.IHtmlDocument document = new HtmlParser(new HtmlParserOptions { IsKeepingSourceReferences = true }).ParseDocument(html);
+ var headingEnds = new Dictionary();
+ using AngleSharp.Html.Dom.IHtmlDocument document = CreateSourceParser(headingEnds).ParseDocument(html);
int[] lineStarts = new int[block.Lines.Count];
int length = 0;
for(int index = 0; index < lineStarts.Length; index++)
@@ -139,7 +168,11 @@ internal sealed class DocumentSourceMap
changes = [];
inserts.Add(index, changes);
}
- changes.Add((column + 3, Attribute(AddPosition(start, EndOfLine(start)))));
+ int endPosition = headingEnds[position];
+ int endIndex = Array.BinarySearch(lineStarts, endPosition);
+ endIndex = endIndex >= 0 ? endIndex : ~endIndex - 1;
+ int end = offset + lines[endIndex].Slice.Start + endPosition - lineStarts[endIndex];
+ changes.Add((column + 3, Attribute(AddPosition(start, EndOfLine(end)))));
}
foreach((int index, List<(int Position, string Attribute)> changes) in inserts)
{
diff --git a/tests/MarkdownViewr.Windows.Core.Tests/DocumentSourceTextTests.cs b/tests/MarkdownViewr.Windows.Core.Tests/DocumentSourceTextTests.cs
index 574d994..0b23fe7 100644
--- a/tests/MarkdownViewr.Windows.Core.Tests/DocumentSourceTextTests.cs
+++ b/tests/MarkdownViewr.Windows.Core.Tests/DocumentSourceTextTests.cs
@@ -93,6 +93,29 @@ public sealed class DocumentSourceTextTests
+ /// A folded multiline HTML heading retains all authored attributes, nested title markup and closing syntax.
+ [Theory]
+ [InlineData("\n")]
+ [InlineData("\r\n")]
+ [InlineData("\r")]
+ public void SetCollapsed_RawMultilineHeading_PreservesCompleteHeader(string newline)
+ {
+ string header = string.Join(newline, "", "Raw child", "", "
");
+ string original = "# Root" + newline
+ + header + newline + "Hidden body" + newline + "First peer
Second peer
" + newline + "Peer body" + newline + newline + "# Last" + newline + "Tail";
+ var source = new DocumentSourceText(original, new DocumentOutline(original));
+ Assert.True(source.SetCollapsed([1]));
+ Assert.Contains(header.ReplaceLineEndings("\r"), source.Text, StringComparison.Ordinal);
+ Assert.DoesNotContain("Hidden body", source.Text, StringComparison.Ordinal);
+ Assert.DoesNotContain("First peer", source.Text, StringComparison.Ordinal);
+ Assert.Contains("# Last\rTail", source.Text, StringComparison.Ordinal);
+ Assert.Equal(new[] { 0, 1, 4 }, source.Headings.Select(heading => heading.Index));
+ Assert.True(source.SetCollapsed([]));
+ Assert.Equal(original, source.GetCopyText(0, source.Text.Length));
+ }
+
+
+
/// Folding accepts only bounded canonical snapshots with matching source anchors.
[Fact]
public void Create_InvalidSnapshot_RejectsInput()