fix(links): make Copy Reference output a working link - #380
Merged
PathGao merged 1 commit intoAug 2, 2026
Merged
Conversation
Copy Reference is the app's own context-menu action; all three call sites emit `[[Note#Heading]]`. Pasted back into Markpad it rendered as literal text, because the wikilink pattern required `#` to follow `[[` immediately - only the same-document anchor form existed, and there was no branch at all for a file target. `[[path#heading]]` and `[[path#heading|alias]]` are now rewritten to an ordinary relative markdown link. The href shape is dictated by the frontend path that has to accept it (`getMarkdownLinkTarget` -> `decodeLinkPath` -> `scrollToAnchor`), so no frontend change is needed: `.md` is appended to a bare note name because the frontend only claims links whose pre-`#` path has a markdown extension, and the destination is percent-encoded because the frontend decodes it. Deliberately limited to forms that contain a `#` in the target half. Obsidian's bare `[[Note]]` is a feature, not this defect, and claiming every `[[...]]` would swallow bracketed citation numbering (`[[1]]`) and pre-empt CommonMark reference links - a regression, where leaving bare wikilinks inert is merely the status quo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 problem
Right-click a heading → Copy Reference → paste. All three call sites of that action emit
[[Note#Heading]], and Markpad renders it as literal text. The app's own output is dead inside the app.The wikilink pattern required
#to follow[[immediately, so only the same-document anchor form ([[#Heading]]) existed. There was no branch at all for a target with a file name —process_internal_embedsonly handles the![[…]]embed form.The fix
[[path#heading]]and[[path#heading|alias]]are rewritten to an ordinary relative markdown link, in the same Rust preprocessing pass that already handles[[#heading]].The href shape is dictated by the frontend that has to accept it, not chosen freely — I read the click path first (
handleLinkClick→getMarkdownLinkTarget→openRelativeMarkdownTarget→scrollToAnchorWhenReady) and emitted only what it already claims, so no frontend change is needed:getMarkdownLinkTargetclaims a link only if the pre-#path has a markdown extension.mdis appended to a bare note name; the Rust extension list mirrorsMARKDOWN_LINK_EXTENSIONSdecodeLinkPath=decodeURIComponent?are percent-encoded,%→%25first so a literal percent round-tripsscrollToAnchorstrips a leading^[[Note#^abc]]and[[Note#abc]]both land on the<a id="abc">thatBLOCK_ID_REemitsNon-markdown targets (
[[report.pdf#Intro]]) stay literal: the frontend would not claim them, and the click would fall through toopenUrl()with a path resolved against the webview origin — which opens nothing. Literal text is the honest result.Scope: only forms with a
#in the targetThis is deliberate and is the main thing to review.
Obsidian's bare
[[Note]]link is a feature, not this defect. Claiming every[[…]]would also swallow:[[1]] Author, Title.— bracketed citation numbering, common in READMEs and notes[[foo]]where[foo]: https://…is defined — pre-empting CommonMark's reference linkBoth are regressions in documents that render correctly today. Leaving bare wikilinks inert is merely the status quo — it was already inert before this change. The asymmetry decides it: "did not get better" is not the same cost as "got worse."
A
#in the alias half ([[Notes|see #1]]) does not qualify either; the closure re-checks the target half after the pattern's cheap prefilter.Alignment with Obsidian
Checked before settling the syntax. Obsidian documents exactly these forms —
[[Note#Heading]],[[Note#Heading|Alias]],[[Note#^blockid]]— and renders an un-aliased heading link asNote > Heading, which this matches, so a reference pasted between the two apps reads the same.Deliberate divergence: Obsidian resolves extension-less note names against a vault index. Markpad has no index, so the target is resolved relative to the current document — the same resolution a plain
[text](../other.md)link already gets.Tests
12 new Rust tests + a 6-case TypeScript contract pin.
master, untouchedmaster+ these tests only[[…]]) + these testsThe other 4 Rust tests pass on
masterby design — they are regression guards (same-document form unchanged, embeds untouched, non-markdown targets literal,[[1]](url)untouched), not reproductions, and are counted separately rather than folded into the red number.Two pre-existing tests were re-pointed at
#-bearing inputs so they still test what their names claim: under the new scope[[log.txt]]and[[report.pdf]]would have passed on the#rule rather than on the extension rejection they exist to check.The TypeScript file is a contract pin, not a reproduction — only 1 of its 6 cases can go red on
master, since the frontend is unchanged. It asserts that every href the Rust rewriter emits is actually claimed bygetMarkdownLinkTargetwith the right path and anchor, and that the two extension lists still mirror each other. Labelled as such in its header.Known boundaries (documented in code, not fixed here)
[[file#H1#H2]]: everything after the first#is one heading name. The same limitation already exists in the same-document form; flagging it rather than silently redefining it.-1,-2, …; a wikilink can only ever address the first. The preprocessor cannot see the target document's heading sequence at all — a hard boundary, already noted upstream inheading_anchor_id.[[#Heading|]](pipe present, alias blank) now falls back to the heading as display text instead of staying literal. Preserving the old behaviour would require adding a special case, and dead text is a poor outcome for someone who just deleted an alias.🤖 Generated with Claude Code