Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

What we learned

Why the server is built the way it is, what we measured, and what we need upstream. See How it works for the short version.

A build and a live parse

The server reads a project two ways, because neither covers everything.

For closed files, the server gets targets, titles and enumerators from the build. An enumerator is the number, like the 1 in Figure 1. The server reads the build page by page from myst start’s content server. We can’t use myst.xref.json instead, because it isn’t rewritten on edits and has no title or enumerator.

Open documents are parsed live with mystmd’s parser and its per-page transforms. Each page is parsed on its own, without the rest of the project. So an open page’s enumerators can differ from the build’s when frontmatter sets numbering.

Directive transforms drop position on the nodes they make (see MyST Author’s data and packaging gaps). For the live parse, mystmd-client’s parseMyst copies the directive’s position onto its children. For built page JSON, src/index-targets.ts falls back to the first descendant that has a position.

References come from regexes

mystmd’s parser finds targets and warnings, but regexes in src/syntax.ts find where each reference is. The parse tree doesn’t give us usable positions:

Replacing the reference regexes

A parser such as myst-parser-unified could replace the reference regexes if it provides:

To swap in a parser, replace refsInText and labelDefinition in src/syntax.ts, not mystmd-client/parse. refAt and optionAt read the cursor’s context in half-typed text, so they stay regex-based.

labelChars leaves out <> on purpose, so a rename doesn’t end {ref}`text <label>` early. labelDefinition adds them back when matching (x)=, since mystmd allows them there.

Conformance

npm run conformance compares the regexes with mystmd’s parse. See Conformance for what it checks, and run it for the current numbers.

We fixed known differences with general rules rather than single cases. For example, the regexes trim spaces around role targets, accept link titles, and count Markdown images as path links.

:label: and :name: values with spaces don’t count as label definitions. When we accepted them, real projects got false labels from frontmatter name: fields and YAML in code blocks. Option text, like a button’s :label:, also read as a label definition. The differences left are in known at the top of scripts/conformance.ts, with the reason for each.

We also ran MYST_CORPUS=<path> npm run conformance by hand on 2026-10-13, on a corpus of public MyST projects:

KindMatchedMissedFalse positive
Label roles72906
{doc} roles100
Path links53204
[](#label) links488217
Label definitions5250149

Most false positives are noise. They come from {myst} example bodies that mystmd keeps as source, from code blocks, and from frontmatter like a kernel’s name: python3. The one real gap is a link written as [](<url>) with parentheses in the URL, which the regexes read as a path link.

The parse-first experiment

The experiment/parse-first branch indexes closed files with the live parser instead of the build. It was never merged. On seven public projects the parse and the build agreed on nearly every target. The first project-wide completion took about a second with the parse, and over a minute with a cold build. The gaps are frontmatter numbering, which the live parse ignores, and mystmd’s own warnings for closed files.

We keep the build because it’s what mystmd actually produces, including what plugins add.

What we need upstream

This server answers mystmd’s LSP and editor support issue. It would be simpler with: