Repository navigation
~~strike~~ no longer exports as bold + strike (#1373) - #1382
Conversation
ReadInlinesInto decided an emphasis span was strong from `DelimiterCount >= 2`. Markdig's EmphasisExtras — on here via UseAdvancedExtensions — folds SIX constructs into the one EmphasisInline node and tells them apart by the delimiter CHARACTER, not the count. `~~` is doubled, so strikethrough satisfied "strong" too and the run was emitted bold AND struck, in the PDF print (<strong><s>…</s></strong>) and in Word (<w:b/> beside <w:strike/>) alike. The same slip mis-styled four more, all of them invisible until you look: ++ins++ -> bold (doubled delimiter) ==mark== -> bold (doubled delimiter) ~sub~ -> struck (shares '~' with strikethrough) ^sup^ -> italic (fell through to "not strong, not strike") Emphasis is now keyed on DelimiterChar. Subscript, superscript, inserted and marked have no representation in the document model (bold/italic/strike/code), so they render as their plain text — content kept, invented emphasis dropped. Giving them styles of their own is a model change, not this fix. Verified on the ARTEFACT, not just the model — the complaint is about a word on a page. Fail-before, read out of the produced files: PDF the struck word's glyphs were drawn with DAAAAA+Arial-BoldMT DOCX the struck run carried <w:b/> and by rasterising both prints and looking at them: before, "Retired" is bold and struck while sub/sup/ins/mark each wear a style; after, "Retired" is struck at normal weight, real bold is still bold, bold-containing-strike keeps both, and the four unrepresentable flavours print as plain text. Both artefact tests carry a genuinely bold word in the SAME document as their control, so "not bold" is a finding rather than an absence, and the DOCX assertions read run properties out first instead of through `?.` — a short-circuit there would have made them unfailable. MeshWeaver.Markdown.Export.Test 113/113 (8 red before, 0 after), the export e2e suites 16/16, MeshWeaver.Documentation.Test 20/20, -c Release -warnaserror clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes Markdown export emphasis handling so strikethrough (~~) no longer incorrectly renders as bold + strike in PDF/DOCX by keying emphasis styling off Markdig’s EmphasisInline.DelimiterChar (not DelimiterCount). This aligns the export model with Markdig’s “EmphasisExtras” semantics and prevents other doubled-delimiter constructs (++, ==) from being misinterpreted as bold.
Changes:
- Update
DocumentBuilder.ReadInlinesIntoto derive bold/italic/strike fromDelimiterCharvia a dedicatedEmphasisStylehelper. - Extend AST-level tests to cover strikethrough plus the other emphasis-extra delimiters and nesting (
***,**...~~...**). - Add artifact-level regression tests that assert styling in produced DOCX/PDF outputs; add a What’s New entry documenting the fix and behavioral change (unsupported emphasis types now render as plain text).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/MeshWeaver.Markdown.Export/Ast/DocumentBuilder.cs |
Fixes emphasis mapping by switching from count-based inference to delimiter-character-based styling (and centralizes the logic). |
test/MeshWeaver.Markdown.Export.Test/DocumentBuilderTests.cs |
Adds model-level coverage for ~~, ++, ==, ~, ^, and nesting behaviors to prevent regressions. |
test/MeshWeaver.Markdown.Export.Test/StrikethroughRenderTests.cs |
Adds end-to-end assertions on generated DOCX run properties and PDF font usage to validate the user-visible output. |
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-13-crossed-out-text-is-no-longer-also-bold.md |
Documents the fix and clarifies how unsupported emphasis constructs are now rendered in exports. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
CI red on an unrelated live-model test — recorded so it is not re-derivedRun 31667783996 (shard 0), It cannot be this diff. The assertion counts how many intermediate streaming writes a live It belongs to an existing population, not to this branch. Scanning the last 10 failed Deliberately not re-run. Re-running a red test to see whether it goes green hides races Local verification on the merged tree is unchanged: |
…h-not-bold # Conflicts: # test/MeshWeaver.Markdown.Export.Test/DocumentBuilderTests.cs
…w requires A semantic conflict the 3-way merge could not see. #1372 gave PdfDocumentRenderer an IoPoolRegistry parameter for the two-pass contents-page-number print; this branch's artefact test predates it and constructed the renderer with the browser alone, so the merged tree failed to compile (CS7036) while every text hunk merged clean. The test already owns a pools field for HeadlessChromiumPdfRenderer, so it passes that one — the same shape RendererOutputTests and PdfHeaderLogoTests use. The artefact assertion itself is untouched and still real: Chrome resolves on this host, so Pdf_prints_the_word_struck_but_not_bold printed and read the fonts back out rather than taking the no-browser branch. MeshWeaver.Markdown.Export.Test 135/135, MeshWeaver.Documentation.Test 20/20, -c Release -warnaserror clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #1373.
The cause
DocumentBuilder.ReadInlinesIntodecided a span was strong fromDelimiterCount >= 2.Markdig's
EmphasisExtras— enabled here byUseAdvancedExtensions— folds six differentconstructs into the one
EmphasisInlinenode and tells them apart by the delimiter character,never the count. Probed against the export's own pipeline:
DelimiterCharDelimiterCount**b**/__b__*/_*i*/_i_*/_~~s~~~~sub~~^sup^^++ins+++==mark===~~is doubled, so strikethrough satisfied "strong" as well and the run was emitted bold andstruck. The same slip mis-styled four more nobody had noticed:
++ins++→ bold,==mark==→ bold (doubled delimiter)~sub~→ struck through (shares~with strikethrough)^sup^→ italic (fell through to "not strong, not strike")Emphasis is now keyed on
DelimiterChar. Not post-processed, not special-cased for~~: thecharacter was always the discriminator and the code was reading the wrong field.
The four with nowhere to go
Subscript, superscript, inserted and marked have no representation in the document model, which
carries bold / italic / strike / code and nothing else. They render as their plain text — every
word kept, no emphasis invented. That is a deliberate change from today's behaviour, where they wear
a style at random; giving them styles of their own means extending the model and both renderers,
which is a feature and not this fix.
Verified on the artefact
The complaint is that a word came out bold on a page, so a model assertion cannot close it alone.
Fail-before, read straight out of the produced files:
DAAAAA+Arial-BoldMT. Chromium embedded thebold face for it; that is the browser's own answer to "did you print this bold?".
<w:b/>.And by rasterising both prints and looking at them:
~~Retired~~~sub~^sup^++ins++==mark==Real
**bold**is still bold,*italic*still italic,***both***still both, and**bold with ~~strike~~ inside**keeps both on the inner phrase — the fix removes a phantom, not astyle.
Both artefact tests carry a genuinely bold word in the same document as their control, so
"not bold" is a finding rather than an absence: if the pipeline had stopped producing bold at all,
the control fails first. The DOCX test reads run properties out and asserts them non-null before
inspecting children — a
?.chain there would short-circuit on a run without properties and makethe assertion unfailable. (
-warnaserrorcaught the null-deref that led me to it.)Verification
MeshWeaver.Markdown.Export.Test113/113 — 8 red before the fix (both artefact tests + 6AST cases), 0 after.
MeshWeaver.Hosting.Monolith.Test16/16 ·MeshWeaver.Documentation.Test20/20.
-c Release -warnaserrorclean onMeshWeaver.Markdown.Exportand the test project.The email/HTML export is unaffected and always was: it renders markdown through Markdig's own HTML
renderer rather than the document model, so
~~has always produced<del>there. That matches theissue's scope — PDF and DOCX only.
Overlap with #1372
Both touch
Ast/DocumentBuilder.cs, but not the same code: #1372 changes theWalkBlockssignature and
AnchorFromInlines; this changes theEmphasisInlinecase inReadInlinesIntoandadds a private helper before
ExtractPlainText. The nearest hunks are ~11 lines apart in theoriginal file, so a 3-way merge takes both. Either order works — no sequencing needed.
🤖 Generated with Claude Code