Skip to content

Fix AbbreviationExtension corrupting emphasis/bold/italic resolution (#935)#936

Merged
xoofx merged 2 commits intoxoofx:mainfrom
Kryptos-FR:fix/935-abbreviation-emphasis
Apr 20, 2026
Merged

Fix AbbreviationExtension corrupting emphasis/bold/italic resolution (#935)#936
xoofx merged 2 commits intoxoofx:mainfrom
Kryptos-FR:fix/935-abbreviation-emphasis

Conversation

@Kryptos-FR
Copy link
Copy Markdown
Contributor

@Kryptos-FR Kryptos-FR commented Apr 18, 2026

Closes #935

Summary

AbbreviationParser subscribed to LiteralInlineParser.PostMatch to perform abbreviation substitution during inline parsing, before emphasis delimiters were resolved. This caused two bugs:

  • Abbreviations before emphasis on the same line left **/* as literal text instead of resolving them.
  • Abbreviations inside emphasis (e.g. **HTML is great**) were silently dropped due to an off-by-one in the word-boundary guard (i != 0 instead of i != content.Start).

The fix moves substitution to document.ProcessInlinesEnd, so it runs after the full inline tree — including emphasis and links — has been resolved. Abbreviations are inserted as direct siblings into each parent container rather than wrapped in an intermediate ContainerInline.

Testing

Three new spec examples cover the two bugs and their combination. All 3 764 existing tests pass.

Performance

Benchmarks run on .NET 10 / Intel Core i3-10110U. "Before" = upstream/main (old PostMatch approach); "after" = this PR.

Scenario Before (mean) After (mean) Δ time Before alloc After alloc Δ alloc
2 defs, 10 paragraphs 9.743 µs 8.260 µs −15 % 8.19 KB 8.14 KB −1 %
2 defs, 200 paragraphs 157.1 µs 141.4 µs −10 % 124.0 KB 119.6 KB −4 %
10 defs, 10 paragraphs 14.41 µs 13.41 µs −7 % 16.81 KB 16.34 KB −3 %
10 defs, 200 paragraphs 243.9 µs 222.8 µs −9 % 247.7 KB 234.7 KB −5 %
Defs present, no matches, 200 paragraphs 126.8 µs 125.1 µs −1 % (noise) 78.7 KB 82.9 KB +5 %

Key gains come from eliminating processor.GetSourcePosition() (a binary search per match, now replaced by O(1) arithmetic) and removing the ContainerInline wrapper allocation per matched literal. The slight allocation uptick in the no-matches case is from the Stack<Block> used by document.Descendants<LeafBlock>(), which fires even when nothing is substituted; this is within noise for real documents.

…asis corruption (xoofx#935)

The PostMatch hook approach (running mid-parse) produced a ContainerInline
with IsClosed=false that disrupted FindLastContainer(), causing emphasis
delimiters to be appended as children of the abbreviation container instead
of as root siblings. It also had an off-by-one (i != 0 vs i != content.Start)
that caused abbreviations to be missed when the literal started inside emphasis.

Replace with a document.ProcessInlinesEnd handler that walks the fully resolved
inline tree (after EmphasisInlineParser and LinkInlineParser have run), inserting
AbbreviationInline/LiteralInline siblings directly into parent containers without
any wrapper ContainerInline.

Also update TestSourcePosition.TestAbbreviations to reflect the new tree shape
(no container wrapper; empty leading literals at word boundaries are pruned).

Fixes xoofx#935
@xoofx xoofx merged commit 5365879 into xoofx:main Apr 20, 2026
3 checks passed
@xoofx
Copy link
Copy Markdown
Owner

xoofx commented Apr 20, 2026

Thanks for the fix!

@xoofx xoofx added the bug label Apr 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AbbreviationExtension corrupts emphasis (bold/italic) resolution on the same line

2 participants