fix(reader): retain cross-block headings and native click targets
Reproduce multiline inline closing tags in the following HTML block. Preserve complete token spans, place Contents tooltips away from rows and locate the observed exact picker edit through owned enumeration. Co-Authored-By: Codex <noreply@openai.com> Signed-off-by: James Maes <jmaes@dmdbrands.com>
This commit is contained in:
6 files changed
+119
-3
No files matched your search
@@ -2,6 +2,35 @@
|
||||
|
||||
Updated: 2026-10-11
|
||||
|
||||
## Multiline token boundary and native input regressions corrected locally - 2026-10-11
|
||||
|
||||
Exactb5e519e / CI38184378762 focused19/22 and native98/100 fail with zero skips.
|
||||
Focused failures: raw-inline complete-header folding, source-navigation physical
|
||||
click/alignment, and native Open picker filename lookup. Full native reproduces
|
||||
the first two; new General/Contents presentation case passes in focused/full.
|
||||
Retain current-focused-b5e519e.log/current-native-b5e519e.log and actual focused
|
||||
TRX/screenshots under artifacts/reader-regressions-38184378762. Installed job
|
||||
114603470178 is still live at this observation; preserve it before any push.
|
||||
|
||||
An exact core regression reproduces the raw opening-tag/newline failure: Markdig
|
||||
moves a line-start closing tag into a following HtmlBlock. Existing actual HTML
|
||||
tokens now retain complete inline titles across consecutive raw blocks, ignoring
|
||||
comment/attribute lookalikes and stopping at a new heading/Markdown boundary.
|
||||
All598 core tests pass with100% measured line coverage, including CRLF, comments,
|
||||
multiline closing syntax and unrelated blocks. Release/format pass.
|
||||
|
||||
Owned UI evidence shows the new full-title tooltip covering a subsequent Contents
|
||||
click. Place the native tooltip to the right and make it non-hit-testable; retain
|
||||
all physical click/alignment assertions. The exact native filename control1148/
|
||||
Edit appears in owned enumeration despite the provider condition lookup timing
|
||||
out; use that same ID/type in owned enumeration, without broader controls or a
|
||||
longer deadline. These native corrections require fresh hosted acceptance.
|
||||
|
||||
A dedicated native Printing page/shared options editor is prepared separately;
|
||||
its new apply/restore/dialog-cancel/sibling-persistence case compiles/discovers,
|
||||
not executed. Keep the goal active; final signing/review/chooser/design acceptance
|
||||
and production publication remain open. No local input or preview changes.
|
||||
|
||||
## Registration and simultaneous editions verified - 2026-10-11
|
||||
|
||||
Exact53755ab / CI38182128299 is terminal success: focused21/21, native99/99,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Development Tasks
|
||||
|
||||
- [x] Reproduce exact raw inline/HtmlBlock closing-boundary failure and correct token provenance;598 core/100% measured lines pass.
|
||||
- [x] Prepare native tooltip placement/non-hit-testing and exact owned picker enumeration corrections from retained b5e519e evidence.
|
||||
- [ ] Verify corrected raw fold, physical source/Contents clicks and picker cancellation on the next exact hosted head; retain every assertion/deadline.
|
||||
|
||||
- [x] Exact53755ab: published99/99, installed stable/dev98/98 each; actual five-extension registration before/after update and owned uninstall/default preservation pass.
|
||||
- [x] Exact53755ab: simultaneous stable/dev profiles/processes, isolated actual updates and stable uninstall preserving dev pass; retain coexisting-channels.json and TRXs.
|
||||
|
||||
|
||||
@@ -108,12 +108,71 @@ internal sealed class DocumentSourceMap
|
||||
}
|
||||
if(opening is not null)
|
||||
{
|
||||
AnnotateInlineHeading(opening, opening.Span.End, offset);
|
||||
AnnotateInlineHeading(opening, InlineHeadingEnd(leaf, opening), offset);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>Consecutive raw HTML blocks can finish an inline title; a new heading or Markdown block prevents borrowing later syntax.</summary>
|
||||
private static int InlineHeadingEnd(LeafBlock leaf, HtmlInline opening)
|
||||
{
|
||||
ContainerBlock parent = leaf.Parent!;
|
||||
for(int index = parent.IndexOf(leaf) + 1; index < parent.Count && parent[index] is HtmlBlock block; index++)
|
||||
{
|
||||
(int? end, bool boundary) = HeadingBoundary(block);
|
||||
if(end is { } close)
|
||||
{
|
||||
return close;
|
||||
}
|
||||
if(boundary)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return opening.Span.End;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>Actual HTML tokens distinguish a finishing tag from a new heading, excluding comments and quoted-attribute lookalikes.</summary>
|
||||
private static (int? End, bool Boundary) HeadingBoundary(HtmlBlock block)
|
||||
{
|
||||
StringLine[] lines = block.Lines.Lines;
|
||||
string html = string.Join('\n', lines.Take(block.Lines.Count).Select(line => line.Slice.ToString()));
|
||||
int? close = null;
|
||||
bool seenTag = false;
|
||||
var parser = new HtmlParser(new HtmlParserOptions
|
||||
{
|
||||
OnToken = (token, range) =>
|
||||
{
|
||||
if(!seenTag && token.Type is HtmlTokenType.StartTag or HtmlTokenType.EndTag
|
||||
&& token.Name.Length == 2 && token.Name[0] == 'h' && token.Name[1] is >= '1' and <= '6')
|
||||
{
|
||||
seenTag = true;
|
||||
if(token.Type == HtmlTokenType.EndTag)
|
||||
{
|
||||
close = range.End.Index - 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
using AngleSharp.Html.Dom.IHtmlDocument document = parser.ParseDocument(html);
|
||||
if(close is not { } position)
|
||||
{
|
||||
return (null, seenTag);
|
||||
}
|
||||
int index = 0;
|
||||
while(position >= lines[index].Slice.Length)
|
||||
{
|
||||
position -= lines[index].Slice.Length + 1;
|
||||
index++;
|
||||
}
|
||||
return (lines[index].Slice.Start + position, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>Retains original syntax through the parsed closing line while inserting only the private identity in the opening tag.</summary>
|
||||
private void AnnotateInlineHeading(HtmlInline opening, int end, int offset)
|
||||
{
|
||||
|
||||
@@ -99,7 +99,7 @@ public sealed partial class MainWindow
|
||||
row.Children.Add(label);
|
||||
var item = new ListViewItem { Content = row, Tag = heading, HorizontalContentAlignment = HorizontalAlignment.Stretch };
|
||||
Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(item, heading.Title);
|
||||
ToolTipService.SetToolTip(item, new TextBlock { Text = heading.Title, TextWrapping = TextWrapping.Wrap, MaxWidth = 320 });
|
||||
ToolTipService.SetToolTip(item, new ToolTip { Content = new TextBlock { Text = heading.Title, TextWrapping = TextWrapping.Wrap, MaxWidth = 320 }, Placement = PlacementMode.Right, IsHitTestVisible = false });
|
||||
Microsoft.UI.Xaml.Automation.AutomationProperties.SetAutomationId(item, "OutlineHeading:" + heading.Index.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
OutlineList.Items.Add(item);
|
||||
if(heading == selected)
|
||||
|
||||
@@ -119,6 +119,9 @@ public sealed class DocumentSourceTextTests
|
||||
/// <summary>Inline raw headings may span Markdown lines; folding preserves their complete authored title and closing tag.</summary>
|
||||
[Theory]
|
||||
[InlineData("<h2 title='inline'>Raw <em>inline</em>\nheading</h2>")]
|
||||
[InlineData("<H2\n title='metadata'>Raw child\n</H2>")]
|
||||
[InlineData("<H2\r\n title='metadata'>Raw child\r\n</H2>")]
|
||||
[InlineData("<H2\n title='metadata'>Raw child\n<!-- fake </h2> -->\n</H2\n>")]
|
||||
[InlineData("<h2>Raw `</h2><h3>fake</h3>`<!-- </h2> -->\nheading</h2>")]
|
||||
public void SetCollapsed_RawInlineMultilineHeading_PreservesCompleteHeader(string header)
|
||||
{
|
||||
@@ -135,6 +138,25 @@ public sealed class DocumentSourceTextTests
|
||||
|
||||
|
||||
|
||||
/// <summary>A following HTML block that starts a new heading or only contains comments cannot supply a false closing range.</summary>
|
||||
[Theory]
|
||||
[InlineData("<div><h3>Child</h3></div>", 4)]
|
||||
[InlineData("<!-- fake </h2> -->", 3)]
|
||||
public void SetCollapsed_RawInlineHeadingFollowedByBlock_DoesNotBorrowClosingTag(string block, int headings)
|
||||
{
|
||||
string original = "# Root\n\nIntro <h2>Header\n\n" + block + "\n\nHidden body\n\n# Last\nTail";
|
||||
var source = new DocumentSourceText(original, new DocumentOutline(original));
|
||||
Assert.Equal(headings, source.Headings.Count);
|
||||
Assert.True(source.SetCollapsed([1]));
|
||||
Assert.Contains("Intro <h2>Header\r", source.Text, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Hidden body", source.Text, StringComparison.Ordinal);
|
||||
Assert.Contains("# Last\rTail", source.Text, StringComparison.Ordinal);
|
||||
Assert.True(source.SetCollapsed([]));
|
||||
Assert.Equal(original, source.GetCopyText(0, source.Text.Length));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>Folding accepts only bounded canonical snapshots with matching source anchors.</summary>
|
||||
[Fact]
|
||||
public void Create_InvalidSnapshot_RejectsInput()
|
||||
|
||||
@@ -227,7 +227,9 @@ public partial class AppTests
|
||||
{
|
||||
try
|
||||
{
|
||||
return session.WaitFor(() => session.FindInApp(cf => cf.ByAutomationId("1148").And(cf.ByControlType(ControlType.Edit))), "the real Windows multi-file picker filename field");
|
||||
return session.WaitFor(() => session.TopLevelWindows().SelectMany(window => window.FindAllDescendants())
|
||||
.FirstOrDefault(element => string.Equals(element.Properties.AutomationId.ValueOrDefault, "1148", StringComparison.Ordinal)
|
||||
&& element.Properties.ControlType.ValueOrDefault == ControlType.Edit), "the real Windows multi-file picker filename field");
|
||||
}
|
||||
catch(TimeoutException exception)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user