Skip to content

Classify links by source syntax, and expose the label position - #10

Merged
hebertialmeida merged 2 commits into
masterfrom
fix/link-label-positions
Sep 10, 2026
Merged

hebertialmeida merged 2 commits into
masterfrom
fix/link-label-positions

Conversation

@hebertialmeida

Copy link
Copy Markdown
Owner

The bug

isAutolink() guessed at GFM bare autolinks by comparing the label against the destination:

childText == linkURLString || linkURLString.hasSuffix(childText)

So any inline link whose label is a suffix of its destination was mistaken for a bare autolink, got the bare-autolink +1 start adjustment, and lost its opening [. Verified against the parser:

go [harbor](#harbor) now               → Link reported "harbor](#harbor)"        ← the "[" belonged to no node
go [chapter-two](#chapter-two) now     → Link reported "chapter-two](#chapter-two)"
go [example.com](https://example.com)  → Link reported "example.com](https://example.com)"
go [the harbor](#harbor) now           → Link reported "[the harbor](#harbor)"   ← correct, not a suffix

[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 .link from 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 a Character boundary.

One cause, three symptoms. Two neighbours of the same heuristic fall out:

  • Angle autolinks (<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. testAutoLinkBracesRange only passed because its input starts at offset 0, where the startOffset > text.startIndex guard happened to block the adjustment.
  • 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. .text now routes through adjustedPosition too; 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 of position, nil for 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 every kind / labelPosition row (inline, reference, shortcut, both autolink forms, empty label, nested emphasis in a label, image alt text).

CI=true swift test116 passed, 1 skipped.

swift test without CI=true crashes in ParserInlineTests/testInvalidLink (Index out of range). Pre-existing on master — that test carries XCTSkipIf(isCI).

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, offsets nil), so it gets no highlighting at all. Fixing it means touching position(in:) itself. The new classifier falls back to the old shape test in that un-inspectable case, so kind is still .autolink rather than silently .inline.

🤖 Generated with Claude Code

hebertialmeida and others added 2 commits September 9, 2026 23:33
`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

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (8cb12b3) to head (a4e066e).
⚠️ Report is 3 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hebertialmeida
hebertialmeida merged commit d5693bf into master Sep 10, 2026
3 checks passed
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