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.

Building a MyST authoring environment to identify things that MyST core could improve

2i2c
Project Jupyter

Something that has bugged me for a long time is that MyST doesn’t have a lot of support for authors to write MyST documents more effectively. MyST documents have a ton of rich metadata inside them, but it has mostly been used for making the reader experience more interesting, rather than the author experience.

For MyST to become a bigger part of the research or education workflow, I think it needs to have a cleaner author UX - at least, for some of the minimal features that writing in other languages often has. Over the last few months I’ve been playing around with various ways to do this, and this is a blog post to share what I’ve built and learned.

tl;dr: I’ve built a few new tools in the MyST ecosystem in order to improve the authoring experience and drive the creation of a few building blocks and improvements in core MyST along the way! Here are the main things I worked on:

Here’s a list of the tools I built along the way:

TitleDescription
myst-authorAn opinionated, Obsidian-style editing interface for MyST built on CodeMirror 6 (used as a web editor, or as a JupyterLab or VS Code extension).
mystmd-lspA language server protocol (LSP) implementation for MyST that uses the mystmd server to expose labels, references, etc. in addition to basic syntax.
mystmd-clientA small helper library to manage the lifecycle of a mystmd process (used by the LSP and myst-author, I hope this can be deleted once some upstream improvements are made!)
myst-syntaxA syntax grammar that can be re-used for highlighting across code editors.
myst-parser-unifiedA remark-based parser for MyST - this is a proof of concept to understand the extra ground we'd have to cover to bring this into core.

myst-author is the editor, and it uses myst-syntax for highlighting and mystmd-lsp for autocomplete. Both myst-author and mystmd-lsp use mystmd-client to run mystmd in the background. myst-parser-unified is a standalone experiment that isn’t used in the stack above, but addresses some problems I kept running into (especially with the LSP).

Building an end-user product to figure out what building blocks needed to exist first

Building opinionated end-user tools can be a useful way to uncover the underlying APIs and logic improvements that need to be made in core technologies in order to enable the end-user thing you’re trying to build. This is particularly interesting with LLMs, because they will often still build the thing you ask for even though it is not possible to do so the “right” way. The result tends to be spaghetti code that hacks around the limitations of the core library, but this is a useful signal for a place to make improvements!

So, I followed a development process that looked roughly like this[1]:

myst-author: An opinionated interactive authoring interface for MyST documents

The main tool that I built is called myst-author.

Animated demo of myst-author switching a rendered block to its MyST source and back.

Clicking a rendered block in myst-author shows its MyST source, typing edits it, and clicking away renders it again.

I’ve been using Obsidian a lot lately, and find that it’s a great hybrid between WYSIWYG and source code writing. It means I can use diffing and merging tools more easily when writing MyST documents, but I still get the interactivity and immediate feedback from rendering things straight away.

myst-author comes with VS Code and JupyterLab extensions, and you can try it out on Binder or Codespaces here:

Launch on Binder Open in GitHub Codespaces

It does some interesting hacky things to get around the fact that the mystmd engine doesn’t easily run on JupyterHub or Binder. Rather than running the MyST theme server, it starts mystmd in headless mode (which builds and serves your content without the website theme). It then renders the AST in the browser with the MyST react components. This means that we can do all the necessary re-writing of assets, etc. at render-time, so it will work even if you’re not being served from the URL root.

It uses CodeMirror 6 for the editing and rendering window, which means that it can be run in a bunch of different places, or potentially as part of a bigger editing application etc. This made it a little bit more complicated for things like VS Code, but felt like a good tradeoff for the generalizability.

What I’d like to do with this repository: Who knows! I’m going to keep using it myself and seeing how I can make it more useful. It feels too opinionated to be in the core MyST organization, but maybe it could live as a standalone editor in myst-contrib? Or maybe it’s best to continue existing as a driving proof of concept for more development. I still haven’t decided.

mystmd-lsp: A language server protocol implementation that uses mystmd

I wanted interactive auto-complete and drop-down hints for myst-author, but I also wanted to go beyond just “give me the basic directive syntax”. I knew that mystmd has a ton of information about the objects in a MyST project contained inside it, so this tool is meant to use all of that information.

mystmd-lsp spawns its own mystmd process in the background. It builds the project for whatever folder you’re in, and then exposes that project’s labels, references, etc. for an editor to use via the Language Server Protocol.

This means that you can type MyST short-hand for references like @ or [](# and mystmd-lsp will show you a list of all the labels that you can reference in the current MyST project (it even includes external references too!). It also autocompletes directives and roles from the project’s plugins, though it has to dig them out of mystmd’s cache to do so.

Animated demo of mystmd-lsp autocompleting a reference and a citation in VS Code.

mystmd-lsp in VS Code: autocompleting a {numref} reference and a citation, then hovering the reference.

It comes with a VS Code extension, and works with any editor that has an LSP client (npm install -g mystmd-lsp). You can try it out in VS Code on Codespaces, or in JupyterLab (via jupyterlab-lsp) on Binder here:

Launch on Binder Open in GitHub Codespaces

I needed a way for my LSP server, and for myst-author, to control the lifecycle of mystmd. However, mystmd doesn’t expose the right APIs to do this naturally. It’s built as a CLI, so there’s no lightweight exported way to launch myst start --headless, talk to its content server, or parse a single page with the same settings that myst build uses. So mystmd-client is just a little helper that holds the logic for controlling start/stop/etc of the myst server so I can re-use it in a few places.

What I’d like to do with this repository: This feels like a good candidate for being in the core myst engine, since it’s best to have a source of truth for this kind of thing. mystmd-client feels like it should just be an upstream improvement in the MyST engine.

myst-syntax: A grammar for MyST syntax highlighting

I realized that the core “grammar rules” for things like syntax highlighting were more generically useful, so that got its own repository as well! These are essentially a bunch of regex style patterns that tell an editor how to chunk up the symbols on the screen (e.g. :::{note}) into discrete units and color them appropriately. This is generic stuff that can be re-used across many kinds of editors, and doesn’t depend on a “server” to understand.

It comes in three flavors:

myst-author and mystmd-lsp both use it instead of shipping their own highlighting. You can try out the extensions on Binder or Codespaces here:

Launch on Binder Open in GitHub Codespaces

What I’d like to do with this repository: Same as mystmd-lsp, this feels like a good candidate for being in the core myst engine.

myst-parser-unified: A native unifiedjs parser for MyST syntax

I kept hitting challenges associated with MyST’s current markdown parser: markdown-it. Right now, mystmd parses each document with markdown-it, then converts the document to a unifiedjs-style AST. markdown-it doesn’t give you column information as it is parsing, only line information. This is a big problem for things like an LSP, which needs to know where in the line you’re typing so that it knows what to autocomplete!

UnifiedJS has its own native parser ecosystem, built on a tool called remark. In principle it should be simpler to just parse MyST markdown directly with remark, rather than to use markdown-it and then convert it over to remark AST. So this tool is an experiment at making that happen. It’s not actually used in the stack above, mostly because I wanted to keep them dependent on mystmd and not totally side-step the core engine. But it seems doable to move this into the core MyST engine at some point.

Note: This one I haven’t used as much, but I think of it more like a proof of concept that should drive improvements to the spec, and might be a helpful starting point for a remark parser. If you want to play with it, you can install it with npm install github:choldgraf/myst-parser-unified and use it as a remark plugin.

At a first pass, I ran the parser against the examples in the MyST conformance tests in myst-spec. Because these tests have some carry-over from markdown-it in them, it was hard to know whether some tests were failing for the wrong reasons (mystmd’s own parser only matches 53 of the 90 MyST examples!). So instead of using the tests’ expected output, I compared against the tree that mystmd’s own parser builds for each example. I also built a little “converter” to transform the remark AST into the idiosyncratic AST that mystmd produces (we should really fix that!). This drove a few useful cycles of iteration to get the parsing to behave properly. I split this up into commonmark tests vs. MyST Markdown tests to get a better feel for this.

Here’s where things stood in October 2026 (the parser docs have the latest numbers):

Examples matching mystmd’s parserMyST (of 90)CommonMark (of 651)
Raw parser output65398
After converting to mystmd’s AST83625

The remaining MyST failures aren’t about parsing MyST syntax. They’re about two features markdown-it adds on top of parsing: “smart quotes” (turning " into curly quotes) and “linkify” (turning bare URLs like www.example.com into links). The unified ecosystem has its own plugins for both, but with slightly different rules. So if we switch, we’ll need to choose whether to port markdown-it’s exact rules so nobody’s output changes, or use the unified plugins and accept small differences in some documents.

It wasn’t too much work to get here, so I’m hopeful that with some more thoughtful iteration we could get the parser to a place where we can just drop it into the myst engine. That said, there might be some trade-offs in that decision e.g., the unified parser is about 2x slower. In practice that’s ~0.4 ms vs. ~0.2 ms for a typical page, or ~1 s vs. ~0.5 s across ~600 real-world MyST files. That’s small next to the rest of a build, but still worth discussing.

What I’d like to do with this repository: I’d also love for this to serve as inspiration for an upstream contribution into the MyST engine itself! We’ve been talking about a unified-native parser for a long time, and if we improved our conformance tests I don’t think that it should be too difficult. The conformance tests themselves haven’t been updated in a while! They are missing some core syntax features that have been added over the past year or two, and need a few more tests to make it easier for others to write their own parsers etc.

Where we could improve things

Here are the upstream improvements that would let us remove some complexity and hacks from these tools.

mystmd

myst-theme

Other projects

Overall what did I learn

This was a really interesting exercise! It uncovered a bunch of stuff that probably wouldn’t have been discovered until somebody had tried to do something specific, and it also resulted in some hopefully useful technology. I tried to create issues for any of the clear stuff that was actionable, and will (hopefully) have time to work through those or work with others in the community that want to tackle them. Here are a few things that stood out to me:

Footnotes
  1. I used Claude heavily in this loop, because there’s no way I’d be able to iterate on MVPs without it. I don’t consider this code to be “production ready”, though some of it might be useful! I’ve licensed it all CC0 in case anybody wants to make use of the code however they like.