Skip to content

Add footnote rendering via a preprocessing pass - #127

Open
Ramon Ferreira (ramonfsk) wants to merge 8 commits into
microsoft:mainfrom
ramonfsk:footnote-rendering
Open

Add footnote rendering via a preprocessing pass#127
Ramon Ferreira (ramonfsk) wants to merge 8 commits into
microsoft:mainfrom
ramonfsk:footnote-rendering

Conversation

@ramonfsk

Copy link
Copy Markdown
Contributor

Summary

adds rendering support for GitHub-flavored footnotes ([^1]), previously on the README's "Not yet supported" list. swift-markdown attaches no footnote extension and has no footnote node types, so footnote syntax survived parsing as literal text. following the same approach the library already uses for LaTeX ("a less heavy-weight approach than forking commonmark-gfm and swift-markdown"), a preprocessing pass runs before the parser:

  • a new FootnotePreProcessor collects single-line definitions ([^id]: text), numbers references by order of first appearance (GFM semantics — labels like [^note] render as numbers), and re-emits definitions as an ordered list after a thematic break at the end of the document
  • references become a marked inline code span that Markdown.InlineCode renders as a real superscript (smaller font + baseline offset), mirroring the existing inline-LaTeX marker technique
  • content inside fenced code blocks and inline code spans is never transformed; the footnote pass runs after the LaTeX pass so the same guard protects the code constructs LaTeX emits
  • GFM parity on the edges: references without a definition stay literal, definitions never referenced are dropped
  • display-only in v1 (no tap-to-scroll between reference and note) and single-line definitions only, as discussed in the issue

Closes #126

Validation

  • make lint — passes
  • make test — passes, including the new FootnotePreProcessorTests (10 string-level tests: numbering, named labels, undefined references, orphan definitions, repeated references, fence and inline-code immunity, passthrough) and FootnoteRenderingTests (superscript run + notes section through the full pipeline)
  • testFootnotes snapshot references recorded for iOS and macOS variants and visually reviewed
  • kitchen-sink demo checked in the sample app on the simulator — the footnote reference renders as a superscript and the note appears in the end-of-document section, in both static and streaming modes

OSS readiness

  • No secrets, internal URLs, private identifiers, or product-only service names were added.
  • Public docs, fixtures, or notices were updated if behavior or dependencies changed.
  • Third-party dependency changes (adds, removes, version bumps) are intentional and reviewed. (no dependency changes)
  • Streaming/incomplete markdown behavior remains covered by fixtures or tests.

Screenshots

the images below are the reference snapshots recorded by the new testFootnotes snapshot test (superscript references, a repeated reference sharing its number, and the end-of-document notes section).

iOS (light) iOS (dark)
footnotes on iOS, light mode footnotes on iOS, dark mode
macOS (light) macOS (dark)
footnotes on macOS, light mode footnotes on macOS, dark mode

swift-markdown attaches no footnote extension and has no footnote node
types, so footnote syntax previously survived parsing as literal text.
Following the LaTeX preprocessor precedent, a new FootnotePreProcessor
collects single-line definitions, numbers references by order of first
appearance, replaces them with a marked inline construct rendered as a
real superscript by the inline layer, and re-emits definitions as an
ordered list after a thematic break at the end of the document. Content
inside fenced code blocks and inline code spans is left untouched.

Closes microsoft#126

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds GitHub-flavored footnote rendering to SwiftStreamingMarkdown by introducing a preprocessing step that rewrites [^id] references into renderable inline markers and moves referenced definitions into an end-of-document notes section, aligning behavior with the existing “preprocess-before-parse” approach used for LaTeX.

Changes:

  • Introduces FootnotePreProcessorImpl and wires it into MarkdownParserImpl (after LaTeX preprocessing) to rewrite references/definitions into a thematic-break + ordered-list notes section.
  • Renders footnote references as superscript runs by extending Markdown.InlineCode conversion logic and adding a small cross-platform font resizing helper.
  • Adds unit + rendering + snapshot coverage and updates README + sample fixture to reflect newly supported footnotes.

Reviewed changes

Copilot reviewed 9 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Tests/MarkdownTextTests/MarkdownTextSnapshotTests.swift Adds a snapshot test to validate end-to-end footnote rendering across platforms/variants.
Tests/MarkdownTextTests/FootnoteRenderingTests.swift Adds non-snapshot XCTest coverage for superscript attribute output and notes-section block structure.
Tests/MarkdownTextTests/FootnotePreProcessorTests.swift Adds string-level tests for preprocessing behavior (numbering, skipping code, orphan handling).
Sources/MarkdownText/Utilities/MDFont.swift Adds a cross-platform font resize helper used for superscript styling.
Sources/MarkdownText/Parser/MarkdownParserImpl.swift Integrates footnote preprocessing into the parse pipeline (after LaTeX).
Sources/MarkdownText/Parser/FootnotePreProcessor.swift Implements footnote reference/definition collection, numbering, and output rewriting.
Sources/MarkdownText/Inline/Markdown+InlineConvertible.swift Detects footnote markers in inline code and renders them as superscript text.
README.md Moves footnotes to “supported” list with a brief description of rendering behavior.
Examples/SwiftStreamingMarkdownSample/SwiftStreamingMarkdownSample/Resources/Fixtures/kitchen-sink.md Updates the kitchen-sink fixture section to reflect footnote support and new semantics.

Comment thread Tests/MarkdownTextTests/FootnoteRenderingTests.swift
Comment thread Sources/MarkdownText/Inline/Markdown+InlineConvertible.swift Outdated
Comment thread Sources/MarkdownText/Parser/FootnotePreProcessor.swift Outdated
Comment thread Sources/MarkdownText/Parser/FootnotePreProcessor.swift Outdated
- inline code spans now follow CommonMark backtick-run rules (a run of N
  backticks closes only at the next run of exactly N), so references
  inside double-backtick spans are left untouched
- fenced code tracking records the delimiter character and opening run
  length; closing requires the same character, a run at least as long,
  and nothing after it, so longer fences containing shorter fence lines
  stay protected
- the footnote marker is only rendered as a superscript when its payload
  is a positive integer; user-authored inline code that merely resembles
  a marker keeps normal inline-code rendering
- import CoreGraphics explicitly in FootnoteRenderingTests

Refs microsoft#126

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 15 changed files in this pull request and generated 2 comments.

Comment thread Sources/MarkdownText/Parser/FootnotePreProcessor.swift Outdated
Comment thread Sources/MarkdownText/Parser/FootnotePreProcessor.swift
- definition lines may start with up to three leading spaces (per
  CommonMark, four or more make the line an indented code block, which
  stays untouched)
- CRLF input is normalized to LF before per-line matching so fence
  detection and definition whole-matches see clean lines

Refs microsoft#126

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 15 changed files in this pull request and generated 1 comment.

Comment thread Sources/MarkdownText/Parser/FootnotePreProcessor.swift
Fence detection trimmed all leading whitespace before checking for a
```/~~~ run, so a 4+-space indented code block (CommonMark: indented
code, not a fence) starting with literal backticks could be misread as
a fence opener. With no matching closer, the rest of the document was
then marked as inside a fence, silently stopping footnote rendering
for everything after it.

fenceCandidate(in:) now only treats a line as fence-eligible when it
has at most three leading spaces, mirroring definitionLine's existing
indentation allowance.

Refs microsoft#126

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 15 changed files in this pull request and generated 1 comment.

Comment thread Sources/MarkdownText/Inline/Markdown+InlineConvertible.swift Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 15 changed files in this pull request and generated 1 comment.

Comment thread Sources/MarkdownText/Parser/FootnotePreProcessor.swift
Ramon Ferreira (ramonfsk) and others added 2 commits July 9, 2026 12:01
- fenceCandidate(in:) now disqualifies any line whose leading run
  contains a tab. Per CommonMark, a tab always advances to the next
  four-column tab stop, so a leading tab lands on column 4 regardless
  of how many spaces (0-3) precede it — the same "indented code block,
  never a fence" rule already applied to four or more leading spaces.
  Previously only literal spaces were counted, so a tab-indented line
  containing literal backticks could be misread as a fence opener.
- corrected footnoteReferenceNumber's doc comment: any inline code
  whose payload matches the marker syntax and is a positive integer
  renders as a footnote superscript, regardless of whether the
  preprocessor emitted it or a user authored it verbatim (the same
  trade-off the LaTeX marker syntax already makes) — only a
  non-numeric or malformed payload falls back to normal rendering

Refs microsoft#126

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rendering

# Conflicts:
#	Sources/MarkdownText/Inline/Markdown+InlineConvertible.swift

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 15 changed files in this pull request and generated no new comments.

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.

feature request: render footnotes ([^1])

2 participants