Skip to content

Render ==highlight== via delimiter pairing rewriter - #182

Draft
Vincent Peng (vincent-peng) wants to merge 8 commits into
microsoft:mainfrom
vincent-peng:highlight-delimiter
Draft

Vincent Peng (vincent-peng) wants to merge 8 commits into
microsoft:mainfrom
vincent-peng:highlight-delimiter

Conversation

@vincent-peng

Copy link
Copy Markdown
Contributor

Summary

  • Adds InlineDelimiterRewriter, a post-parse rewriter that pairs repeated-character delimiter runs inside paragraphs/headings/table cells and wraps the matched range in InlineAttributes, using the same attribute channel the inline HTML rewriter introduced.
  • Ships the first delimiter spec: ==highlight== renders as highlighted text (background color via the existing highlight attribute).
  • Pairing follows markdown-it-mark semantics: full left/right-flanking rules (whitespace and punctuation on each side of a run), closer-preference stack pairing, and the adjacent-opener jump rule so ====x==== nests instead of collapsing.
  • Unpaired markers, empty pairs (a====b), and markers inside links/code spans stay literal and in order — the empty/rejected fallback paths emit pieces verbatim rather than reordering boundary remainders.
  • Link/Image children pass through a wrap unstyled but alive, so ==a [x](u) b== keeps a live link and ImageBlockRewriter still sees paragraph-level images.
  • Nested InlineAttributes compose: ==a <sup>b</sup> c== keeps both the highlight and the superscript by merging attribute keys into split runs (a wrap can't contain a nested attributes node).
  • A paired closer is bounded by its enclosing emit range, so cross-spec pairs can't escape and emit pieces twice (matters for the stacked superscript spec).

The engine is spec-driven — a DelimiterSpec table declares each marker, its attribute key, inner-whitespace policy, and opener-blocking predecessor characters — so follow-on delimiters (superscript ^x^ is a stacked PR on top of this one) are a table entry plus tests.

Depends on #181 (uses InlineAttributes + the attributed-string converter introduced there).

Test plan

  • swiftlint --strict — clean
  • xcodebuild test — full suite green (26 dedicated delimiter tests: pairing, punctuation flanking, run nesting, empty pairs, link/image pass-through, nested attribute composition, constrained containers)
  • make build-sample — builds

Generated with Devin

Inline raw-HTML nodes (InlineHTML) were previously dropped during inline
conversion because they don't conform to InlineConvertible. This adds an
AST-level InlineHTMLRewriter that maps a supported formatting subset onto
equivalent Markdown nodes or InlineAttributes carriers before conversion:

- <br> -> line break, <wbr> -> zero-width space
- <b>/<strong> -> strong, <i>/<em> -> emphasis
- <s>/<del>/<strike> -> strikethrough
- <u>/<ins>, <mark>, <sub>/<sup> -> InlineAttributes, now rendered by a
  new InlineConvertible conformance (underline, highlight, baseline offset)
- <code>/<kbd>/<samp>/<tt> -> inline code
- <a href> -> link

HTML comments are dropped. Unmatched, unsupported, or malformed tags keep
their source as literal text so documents degrade gracefully. An open tag
with no matching close applies to the rest of the inline container, which
keeps mid-stream documents sensible while a tag is still arriving.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Follow-ups from review:

- Bound tag-matching recursion at 64 pairs of nesting; deeper input emits
  literally instead of recursing quadratically / overflowing the stack.
- Convert HTMLBlock nodes (standalone-line tags, cmark block type 7) to
  literal paragraphs so their text is never silently dropped.
- Replace the href regex with a quote-aware scanner: matches only on a
  whitespace boundary (no data-href/xlink:href false positives), skips
  quoted attribute values, and accepts case-insensitive HREF.
- Decode common named/numeric entities in href values (&amp;, &quot;, …).
- Flatten InlineAttributes inside <a> to their children so the link still
  forms; split underline/highlight/sub/sup runs around non-recurring
  children (Link, Image) instead of going fully literal.
- Use InlineCode.code when flattening nested inline code inside <code>.
- Accept the full cmark whitespace set inside tags; map </br> to a line
  break like browsers do.
- Require a key boundary in the InlineAttributes enabled-key regex.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- visitHTMLBlock now drops comment/PI/declaration blocks instead of
  surfacing them as visible text, matching inline comment handling.
- Images inside <b>/<i>/<s> wrappers split the run so Image nodes reach
  paragraph level where ImageBlockRewriter can hoist them — previously
  an image inside a bold tag was silently dropped at conversion.
- Link/Image/InlineAttributes children are filtered back to
  RecurringInlineMarkup (text-preserved fallback) so rewritten content
  stays within swift-markdown's structural rules.
- Depth-cap fallback path now drops comments consistently.
- enabledKey regex requires a word boundary after 'true'; README lists
  <strike> and clarifies comment handling.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Adds InlineDelimiterRewriter, a spec-driven post-parse pass that pairs
'==' runs in Text nodes using markdown-it-mark flanking rules and wraps
matched ranges in InlineAttributes (already rendered with a highlight
background). Pairing spans sibling inlines so '==**b**==' works, nested
pairs fold into the outer highlight, and unpaired markers stay literal
— important for streamed documents where a closer may still be
arriving. Link/Image/InlineAttributes containers are left alone to
preserve their RecurringInlineMarkup invariants.

Stacked on the inline-HTML branch for the InlineAttributes renderer.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A nested InlineAttributes child is not RecurringInlineMarkup, so the
first version degraded it to plain text and lost the inner style — e.g.
'==a <sup>b</sup>==' highlighted everything but dropped the
superscript. attributeWrap now splits the run around nested attribute
nodes and re-wraps each under the union of both key sets.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Bound a marker's close index by the enclosing emit range so cross-spec
  pairs can't consume pieces the outer level emits again.
- Empty-inner pairs (a====b) emit literal markers instead of vanishing.
- attributeWrap keeps Link/Image nodes alive between styled runs instead
  of degrading them to plain text.
- Skip the inner whitespace scan for specs that allow it; dedupe merged
  attribute keys on exact entries.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- applyFlanking now uses the full left/right-flanking rules (punctuation
  on either side of a run), so a==(b)==c and x==hi!==y stay literal and
  x==a)==b== picks the same pair the reference renderer does.
- pairedCloses skips an immediately adjacent opener, mirroring
  markdown-it's jump rule; ====x==== nests instead of each run's markers
  collapsing onto each other.
- emit's boundary-remainder strip now only hoists remainders matching
  the spec's own marker char, and the empty/rejected literal paths emit
  in piece order instead of reordering remainders across the markers.
- Corrected the stale doc comment: backslash-escaped \== stays literal
  because cmark emits the escape as a separate Text node.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ImageBlockRewriter can only hoist a paragraph-level Image. An Image or
SymbolLink inside a wrapped range deeper in (e.g. inside **==..==**)
would be silently dropped at conversion — degrade it to its text
fallback so the alt text joins the styled run instead of vanishing.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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.

1 participant