features / F03-links / design.md

F03 design

Written against spec.md in this folder, from origin/cursor/spec-links-rendering-editing-7df9. That branch is not merged. Every requirement in that spec is behavior already in the tree. Version pins and backlinks computed at merge time are out of this spec; a target that contains @ stays an unresolved link.

Modules

ModuleRole
lib/markdown/links.tsresolveDoc and hrefFor
lib/markdown/plugins.tsremarkWiki turns wiki syntax into a link or an embed placeholder
lib/markdown/outline.tsextractHeadings and extractSection
components/markdown-parts.tsxEmbed depth, the stop message, and the link renderer
components/markdown-view.tsx, components/static-markdown.tsxPreview and the server render share those plugins
lib/workspace/relink.tsRewrites targets after a rename or a move
lib/workspace/moves.tsCalls the relinker when a page or a folder moves
lib/workspace/tree.tsbacklinks
components/workspace.tsxPasses the backlink list into the reading column
components/missing-page.tsxThe missing-URL screen
lib/workspace/paths.tshrefOf and humanize

Data shapes

type DocRef = { path: string; title: string };

type WikiMatch = {
  target: string;
  heading?: string;
  label?: string;
  embed: boolean;
};

type EmbedFrame = {
  path: string;
  heading: string;
  depth: number;
  trail: string[];
};

resolveDoc trims the target, strips one trailing .md without regard to case, and strips leading slashes. It then matches that text to a page path with .md removed. If no path matches, it matches the last segment to a file name and returns the page only when exactly one page has that name. Zero matches and two or more matches are unresolved. A target that contains @ does not match a path or a unique file name, so it renders as unresolved text. That is the current fallback for a pin. This feature does not store a pin as its own field.

A resolved link goes to /{project}/{path} with .md removed, plus a # fragment when a heading was written. The visible text is the label if one was written, otherwise the heading text, otherwise the page title. Unresolved text is the label, the heading, or the target, in that order, in red, and it is not an anchor.

An embed is a paragraph whose only text is ![[target]] or ![[target#Heading]]. The same syntax with a label, or inside a sentence, is a wiki link, not an embed. A resolved embed shows a source link reading {title} or {title} / {Heading}, then the page body or the headed section, and omits the embedded page's property row. A missing embed replaces the paragraph with Missing page: {target} in red.

extractSection keeps the named heading and the lines after it until the next heading of the same or higher level. The heading match ignores case. If no heading matches, the embed uses the page body, and the source link still carries the fragment for the text that was written.

State

There is no link store. Resolution reads the project's page list. The embed trail is an argument on the render, empty on the open page. MAX_EMBED_DEPTH in markdown-parts.tsx is 3. A fourth embed, or a path already on the trail, shows Embed stops here: {path} is already shown above. and does not render that body.

"Linked from" is derived on each render by backlinks. The open page is left out. An empty list has no heading. Preview and editing both render the page body, so the list is on both. Source mode has no preview pane.

Contracts

Heading ids come from github-slugger, in document order, skipping headings inside fenced code. A repeated heading text gets -1, then -2. A wiki fragment is slugified on its own, so it matches the first heading with that text.

Colors and the embed radius are in specs/contracts/tokens.md: the wiki link colors, the broken-link treatment, and the 18px block radius on an embed. This feature adds no shortcut. The [[ picker is F09 and F20.