Skip to content

Render ^x^ superscript via the delimiter rewriter - #183

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

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

Conversation

@vincent-peng

Copy link
Copy Markdown
Contributor

Summary

  • Adds ^x^ superscript as a second DelimiterSpec on the delimiter engine from the stacked highlight PR: x^2^ renders 2 with raised baseline and reduced font size (reuses the superscript attribute handling added by Render a supported subset of inline HTML tags instead of dropping them #181 for <sup>).
  • Intraword superscript works (a^b^c); unclosed or whitespace-containing ranges stay literal (a ^b c^ d, x^2 unchanged).
  • ^ cannot open when preceded by [: LLMs emit [^n]-style references, and pairing across bracketed text would eat [^1][^2] as superscript of 1][. The per-spec openerBlockedAfter set keeps that text literal while a later clean pair ([^1] x^2^) still wraps — and it doesn't reach through foreign interior content, so x[=^y^ still pairs.
  • Cross-spec pairs can't escape their enclosing emit range: ==a ^b== c^ highlights a ^b and leaves c^ literal instead of duplicating text; ^a ==b^ c== stays fully literal because superscript rejects inner whitespace.
  • Conservative-by-design note: ^ uses the same markdown-it flanking rules as ==, so x^(n)^ stays literal (punctuation can't open intraword) — literal output is always a safe failure for streamed text.

Depends on the highlight PR (the delimiter engine and its hardening live there), which in turn depends on #181.

Test plan

  • swiftlint --strict — clean
  • xcodebuild test — full suite green (40 delimiter tests including multi-spec crossing, footnote-pattern, piece-order, and intraword cases)
  • 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>
Extends the delimiter rewriter with a '^' spec producing
{superscript:true}. Inner whitespace is rejected per Pandoc's rule so
x^2 and a^b c^d stay literal. Nested composition relies on the base
branch's attributeWrap merge.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
LLMs emit [^n]-style references; pairing across the bracketed text would
eat [^1][^2] as superscript of '1]['. A per-spec openerBlockedAfter set
stamps ^ non-opening when preceded by '[', leaving such text literal
while later clean pairs still wrap.

Also covers the cross-spec pairing cases from review: a closer outside
its enclosing emit range stays literal, so crossing == and ^ pairs can
no longer emit pieces twice.

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