Classify links by source syntax, and expose the label position - #10
Merged
Merged
Conversation
`isAutolink()` guessed at GFM bare autolinks by comparing the label against the destination, so any inline link whose label was a suffix of its destination — `[harbor](#harbor)`, exactly what heading anchors look like — got the bare-autolink +1 start adjustment and lost its opening `[`. Classify from the source syntax instead (`CMNode.linkSyntax`), which also fixes two neighbours of the same heuristic: - angle autolinks (`<https://example.com>`) are reported correctly by cmark and were being shifted right, dropping `<` and swallowing `>`; - a bare autolink's own child text started a character before its parent, and the text run in front of it still ended inside the URL. Adds `Link.kind` (`LinkKind.inline` / `.reference` / `.autolink`, from the source syntax) and `Link.labelPosition` / `Image.labelPosition` — the span between the brackets, `nil` for an autolink or an empty label — so callers can style the label separately from the link syntax. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The manifest declared `.upToNextMajor(from: "0.7.1")`, which already allows anything below 1.0.0 — so SPM was resolving 0.8.0 regardless, and the tests that characterise cmark's position reporting were written against it. This only makes the declared floor match what was already being built. `CI=true swift test`: 116 passed, 1 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #10 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 1 1
Lines 137 133 -4
=========================================
- Hits 137 133 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
The bug
isAutolink()guessed at GFM bare autolinks by comparing the label against the destination:So any inline link whose label is a suffix of its destination was mistaken for a bare autolink, got the bare-autolink
+1start adjustment, and lost its opening[. Verified against the parser:[harbor](#harbor)is exactly the shape a heading anchor takes, which is why this is worth fixing rather than working around.The fix
CMNode.linkSyntax(in:using:)classifies a.linkfrom the source bytes at its own position —[+)→ inline,[+]→ reference,<+>→ angle autolink, else bare GFM autolink. Comparison is done in the UTF-8 view, because these indices come from UTF-8 offset arithmetic and are not guaranteed to sit on aCharacterboundary.One cause, three symptoms. Two neighbours of the same heuristic fall out:
<https://example.com>) are reported correctly by cmark, brackets included — they were being shifted right by the same adjustment, dropping<and swallowing>. The fix is subtractive: don't touch them.testAutoLinkBracesRangeonly passed because its input starts at offset 0, where thestartOffset > text.startIndexguard happened to block the adjustment..textnow routes throughadjustedPositiontoo; both clamps are one-directional, so a text node that is already right is untouched.New API
LinkKind—.inline/.reference/.autolink, from the source syntax.Link.kind,Link.labelPosition,Image.labelPosition— the span between the brackets, a sub-range ofposition,nilfor an autolink or an empty label such as[](url).Both new fields are defaulted, so the existing
Link(url:title:children:position:)call shape still compiles.This is what lets a consumer style a link's label separately from its syntax — painting the words a reader reads in the prose colour and leaving only the brackets and destination link-coloured.
Also here
chore(deps)raises the swift-cmark floor to 0.8.0. The manifest said.upToNextMajor(from: "0.7.1"), which already permits anything below 1.0.0, so SPM was resolving 0.8.0 regardless — this only makes the declared floor match what was already being built. No migration.Tests
Tests/MarkdownSyntaxTests/LinkLabelPositionTests.swift— 18 cases: all three repros above, plus everykind/labelPositionrow (inline, reference, shortcut, both autolink forms, empty label, nested emphasis in a label, image alt text).CI=true swift test→ 116 passed, 1 skipped.Known gap, not fixed here
A bare autolink at the start of a paragraph gets an invalid position from cmark (start column 0 →
position(in:)fails early, offsetsnil), so it gets no highlighting at all. Fixing it means touchingposition(in:)itself. The new classifier falls back to the old shape test in that un-inspectable case, sokindis still.autolinkrather than silently.inline.🤖 Generated with Claude Code