Add footnote rendering via a preprocessing pass - #127
Open
Ramon Ferreira (ramonfsk) wants to merge 8 commits into
Open
Add footnote rendering via a preprocessing pass#127Ramon Ferreira (ramonfsk) wants to merge 8 commits into
Ramon Ferreira (ramonfsk) wants to merge 8 commits into
Conversation
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>
There was a problem hiding this comment.
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
FootnotePreProcessorImpland wires it intoMarkdownParserImpl(after LaTeX preprocessing) to rewrite references/definitions into a thematic-break + ordered-list notes section. - Renders footnote references as superscript runs by extending
Markdown.InlineCodeconversion 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. |
- 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>
- 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>
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>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
- 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
# Conflicts: # README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:FootnotePreProcessorcollects 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 documentMarkdown.InlineCoderenders as a real superscript (smaller font + baseline offset), mirroring the existing inline-LaTeX marker techniqueCloses #126
Validation
make lint— passesmake test— passes, including the newFootnotePreProcessorTests(10 string-level tests: numbering, named labels, undefined references, orphan definitions, repeated references, fence and inline-code immunity, passthrough) andFootnoteRenderingTests(superscript run + notes section through the full pipeline)testFootnotessnapshot references recorded for iOS and macOS variants and visually reviewedOSS readiness
Screenshots
the images below are the reference snapshots recorded by the new
testFootnotessnapshot test (superscript references, a repeated reference sharing its number, and the end-of-document notes section).