xmd renders executable Markdown. You write a normal Markdown document, mark some code blocks as runnable, and xmd turns it into HTML — optionally running those code blocks and inserting their output, the way a Jupyter notebook or a Quarto document does.
Python, R, raw HTML, and Observable JS work out of the box, and xmd is built so you can add your own block types and ways of running code.
A document, hello.xmd:
---
title: Hello
---
# Hello
The time, computed when this page is built:
```{python}
from datetime import datetime
print(datetime.now())
```
Render it to HTML and run the code:
xmd render hello.xmd --runYou get an HTML page with the heading, the prose, the Python source, and the
printed time below it. Drop --run and you get the same page with the code shown
but not executed.
Not yet on npm — use one of the routes below.
As a global xmd command (the usual case — runs through Node):
# from a checkout of this repo
npm install # installs dependencies and builds the project
npm link # puts `xmd` on your PATHAn .xmd file is Markdown plus a few additions.
Frontmatter — optional YAML metadata between --- fences at the very top:
---
title: My Document
author: Jane
---
Cells — a fenced code block whose info string is a language in braces is a
cell: something xmd can render and (with --run) execute.
```{python}
print("hello")
```
Cells take options on lines beginning with #|:
```{python}
#| echo: false # hide the source; show only the output
#| eval: false # show the source; do not run it
print("hello")
```
Built-in cell languages: python, r, html (emitted as raw HTML),
and ojs (Observable JS, which runs in the browser).
Inline code — `{python} 6 * 7` inside a sentence runs the expression and
drops its value into the text, e.g. "the answer is 42".
Callouts and columns — ::: "directive" blocks become HTML containers:
:::callout-note
A note box.
:::
::::columns
:::column
left
:::
:::column
right
:::
::::
Anything else is ordinary Markdown (headings, lists, links, GFM, etc.).
By default, rendering does not run anything — cells are just displayed. To
execute them, add --run (CLI) or use renderAsync (library). When you do, xmd
runs the cells top to bottom, captures what each one prints, and inserts that
output beneath the cell. A cell that produces a figure (e.g. matplotlib) shows the
image.
How code runs is controlled by a runtime, which you set in your config (next section). Three things you can control:
- Which interpreter. By default xmd calls
python3,Rscript, etc. from yourPATH. You can point a language at a specific interpreter instead — a virtualenv,conda run, a custom path. - Whether cells share state. By default each cell runs in a fresh process, so a variable set in one cell isn't visible in the next. A stateful runtime keeps one process alive per language, so cells share variables — like a notebook.
- Different backends per language. You can run, say, Python in a container and shell scripts locally.
With no configuration, --run uses a fresh-process local runtime and the
interpreters on your PATH.
To customize xmd, create a file named xmd.config.ts in the directory you run
xmd from (your project root). The CLI looks for it there automatically — there
is no flag to pass. (xmd.config.mjs and xmd.config.js also work.)
It exports a default object with two optional fields, handlers and runtimes:
// xmd.config.ts
import { createStatefulSubprocessAdapter } from "@xmd/engine";
export default {
handlers: [], // custom block types — see "Adding a block type"
runtimes: [ // how code runs
createStatefulSubprocessAdapter({
commands: { python: "./.venv/bin/python" }, // use this project's virtualenv
}),
],
};With that file present, xmd render doc.xmd --run runs Python cells with your
venv and keeps state across cells.
A handler owns one {name} block type and turns it into HTML. The built-in
python/r/html/ojs cells are just handlers that ship pre-registered; you add
your own the same way.
A minimal handler that adds a {badge} block:
import type { BlockHandler } from "@xmd/engine";
const badge: BlockHandler = {
name: "badge", // handles blocks tagged {badge}
toHtml: (ctx) => `<span class="badge">${ctx.content}</span>`,
};ctx gives the handler the block's content (its text), options (the parsed
#| values), and the document's frontmatter.
Register it by adding it to handlers in your xmd.config.ts:
export default { handlers: [badge] };Now blocks tagged {badge} render through your handler. (If your handler's name
matches a built-in one, yours takes over and xmd logs a warning.)
A handler can also execute its block by implementing build (for cells) or
inline (for inline code) — that's how the Python handler runs Python. The
built-in pythonHandler source is the reference example.
A runtime adapter knows how to execute one or more languages. The built-in adapters run local processes; you can write your own — to run code in a container, on a remote service, in the browser — by implementing this shape:
interface RuntimeAdapter {
name: string;
supports(language: string): boolean; // which languages it handles
exec(req: { language: string; code: string }): Promise<{
ok: boolean; stdout: string; stderr: string;
}>; // run one cell, return its output
createSession?(language: string): RuntimeSession; // optional: keep state across cells
}Add adapters under runtimes in xmd.config.ts. When several are registered, each
language goes to the first adapter whose supports() returns true, so you can
mix backends per language.
import { createBaselineEngine, createLocalSubprocessAdapter } from "@xmd/engine";
const engine = createBaselineEngine(); // python/r/html/ojs pre-registered
// engine.registerBlock(badge); // add your own handler
// engine.registerRuntime(myAdapter); // add your own runtime
const src = "# Hi\n\n```{python}\nprint(2 + 2)\n```";
const shown = engine.render(src); // parse + render, no execution
const ran = await engine.renderAsync(src, { // execute cells, insert output
runtime: createLocalSubprocessAdapter(),
});
console.log(ran.html); // the rendered HTML string
console.log(ran.frontmatter); // the parsed frontmatter objectUse render when you only need the document rendered; use renderAsync (with a
runtime) when you want cells executed.
xmd render <file.xmd> # render to HTML (no execution)
xmd render <file.xmd> --run # also execute cells and insert their output
xmd render <file.xmd> --frontmatter # print the parsed frontmatter as JSON
xmd render <file.xmd> --mode=NAME # pass an opaque "mode" string to handlersThe CLI registers the built-in handlers and loads xmd.config.ts from the current
directory. --mode is only relevant if you write handlers that branch on it;
otherwise ignore it.
Engine
| Export | What it is |
|---|---|
createBaselineEngine(): XmdEngine |
An engine with the built-in python/r/html/ojs handlers registered. |
new XmdEngine() |
An empty engine — no handlers registered. |
engine.render(src, { mode? }) |
Parse and render to { html, frontmatter }. Does not execute code. |
engine.renderAsync(src, { runtime?, runtimes?, mode? }) |
Like render, but executes cells through the given runtime(s). Returns a Promise. |
engine.registerBlock(handler) |
Add (or override) a block-type handler. Returns the engine. |
engine.registerRuntime(adapter) |
Add a runtime adapter, routed by language. Returns the engine. |
Built-in handlers (to compose your own engine)
| Export | What it is |
|---|---|
pythonHandler, rHandler |
Executable cells; honor echo/eval/output and inline code. |
htmlHandler |
Emits the block's content as raw HTML. |
ojsHandler |
Emits an Observable JS mount (runs in the browser). |
makeLanguageCell(name, language, inlineWrap?) |
Build a python-style cell handler for any language. |
Runtimes
| Export | What it is |
|---|---|
createLocalSubprocessAdapter(opts?) |
Runs each cell as a fresh subprocess (stateless). opts: commands (per-language interpreter, string or argv array), cwd, env. |
createStatefulSubprocessAdapter(opts?) |
Keeps a process alive per language so cells share state (python/node). Same opts. |
localSubprocessAdapter |
A ready-made default local adapter (PATH interpreters). |
Types (for TypeScript users): BlockHandler, BlockContext, RuntimeAdapter,
RuntimeSession, ExecRequest, ExecResult, RenderResult, RenderOptions.
Their exact shapes are in the bundled .d.ts files.
npm run build # compile TypeScript to dist/ (+ type declarations)
npm test # run the test suite (node:test)
npm run build:binary # compile a standalone `xmd` executable (needs bun)See examples/ for runnable documents, including a custom-handler example.
rcells needRscriptinstalled on the host.ojsruns in the browser, so it emits a client-side mount rather than executing during the build.
MIT