Writing an implementation
BMX is a format, so nothing stops you reading it in your own language — and this page is the path, because until now the capability existed and the invitation did not.
There are two ways to extend BMX and they are different jobs:
| You are writing | Go to |
|---|---|
| a layer above BMX — a component system, a framework, a site generator | Building on BMX |
| a reader of BMX — a parser, a renderer, in any language | this page |
If you pick wrong you will read a page that is not for you and conclude the door is shut. It is not: why documents agree ends by saying 1.0 arrives when somebody who did not write the rules builds a tool from them and it passes. You are that person, and this is what to run.
What you are building
Three levels, and only the first is required to say you implement BMX. They are defined normatively
in BOUNDARY.md.
| What it does | Who can | |
|---|---|---|
| the parser | source in, a tree out — or an error with a code | any language |
| level 1 — renders | the parser, plus slot values substituted and escaped on output | any language |
| level 2 — checks | level 1, plus every slot expression verified against a declared interface before the document renders | only a language with types |
Start at the parser. It is what the conformance suite measures, and it is a few hundred lines — no
lexer generator, no parser combinator library, no dependency of any kind. reference/bmx.js is
roughly seven hundred lines of code, with four hundred more explaining itself, and it is written to
be read top to bottom by exactly the person on this page.
Say your level in your README, and do not claim level 2 without a check that fails. That is the
one thing BOUNDARY.md asks of an
implementation.
The suite is data, which is the whole design
The specification’s executable half is a folder of documents paired with exactly what each must produce. Clone the repository and look:
tests/cases/024-block-simple.bmx a document
tests/cases/024-block-simple.json the tree it must produce, exactly
tests/errors/001-unterminated-slot.bmx a document that must be refused
tests/errors/001-unterminated-slot.error the code it must be refused with
More than ninety documents, across both folders. There is no framework to port and no runner to reimplement — the pairs are JSON and text, so conformance costs an afternoon in any language rather than a port. That claim is the reason the format is worth adopting, and it is the one this page exists to stop being a secret.
Read three .json files and you have the tree. They are not a description of the AST; they are
the AST, for real documents, and where they and the prose disagree the files win. The normative
table of node kinds is SPEC.md §5, but
you can write most of a parser from the fixtures alone.
Here is the shape, from the two constructs every document has. This document:
# Receipt {{ order.reference }}
Thank you, {{ customer.name }}.
parses to this, and the fixtures look like it:
{
"type": "document",
"children": [
{
"type": "heading",
"level": 1,
"children": [
{ "type": "text", "value": "Receipt " },
{ "type": "slot", "expression": "order.reference", "offset": 13 }
],
"offset": 0
},
{
"type": "paragraph",
"children": [
{ "type": "text", "value": "Thank you, " },
{ "type": "slot", "expression": "customer.name", "offset": 47 },
{ "type": "text", "value": "." }
],
"offset": 33
}
]
}
Running the suite against your implementation
burxt build tests/harness.bx -o harness
./harness '<your command>'
The command is run once per case with the document’s path appended, and must either
- print the AST as JSON on stdout and exit 0, or
- print an error beginning with its
BMX-Ennncode and exit non-zero.
So most people point it at a two-line wrapper:
./harness 'node reference/bmx.js'
./harness './my-bmx --ast'
./harness 'python3 -m mybmx.cli'
It prints one line per failure, then a count — which grows as cases are added, so treat the numbers here as a shape rather than a target:
024-block-simple.bmx:
expected {"type":"document","children":[{"type":"block","name":"card",...
actual {"type":"document","children":[{"type":"block","name":"card",...
92 cases, 91 passed, 1 failed
The harness is not part of the specification, and it needs a Burxt compiler to build. That is a prerequisite this page did not have when the harness was thirty lines of Python, and it is stated rather than buried: this repository builds its own tooling in the language it serves, and that choice is not a claim on yours.
So if a Burxt toolchain is inconvenient where you are, throw the harness away and write your own. The
loop is: for each .bmx in cases/, run your parser, compare the JSON to the .json beside it — as
parsed values, because key order is not specified; for each .bmx in errors/, expect a non-zero exit and
an error beginning with the code in the .error file. That is the whole contract, it is a page of code in
any language, and the cases are the specification, not the program that walks them. Nothing in BMX
requires you to run our tooling to claim conformance.
The four things an implementation gets wrong
Not guesses. Each one is a defect that shipped here, in a release, and became a fixture.
Offsets are the byte index of the first byte the author wrote. The # of a heading, the - of
a list item, the [ of a link, the first : of a block’s fence. The cheap way to get this right is
to strip leading whitespace in one place — the line builder — so every offset downstream is
already a real source position; the alternative is adding the indent back at twenty call sites,
which is twenty chances to forget one. A slot is the one exception: its offset is the first byte
of the trimmed expression, because the host is handed the expression and must point inside it.
Adjacent text nodes are always merged. An implementation emitting text("a"), text("b") where
another emits text("ab") fails the suite, and rightly — they are the same document.
Stop at the first error. Recovery is a real want and an editor needs it, but recovery that differs between implementations is worse than none, so it is outside conformance for now.
The code is the conformance surface; the message is yours. tests/errors/*.error holds a bare
BMX-E001, and the harness checks only that your output starts with it. Write the best message
your language can — but never one that tells an author to write something the format also refuses,
which is a trap this project has a runner for.
If you render, escaping is not optional
The moment your implementation produces output rather than a tree, one rule binds it, and it is
normative in ESCAPING.md:
A slot’s value is escaped for the output format, at render time, always. There is no configuration that turns it off, and no per-slot syntax that opts out.
There is no {{{ raw }}} and there will not be one. A host that wants to allow trusted markup does
it in the host language, where the waiver is a named call a reviewer can grep. Two further hazards
the rule covers and a first implementation usually misses: a link target is escaped per-context
and its scheme is checked — a document must not reach the output through javascript: — and a
code fence’s info string is metadata, so a host putting it in output escapes it there.
Then ask the question the suite cannot
Passing the suite proves you match what was written down. It cannot prove that what was written down covers everything, because a suite is made of the things somebody thought of.
burxt build tests/agree.bx -o agree
./agree 'node reference/bmx.js' '<your command>'
Both commands are run over every document in tests/cases and tests/errors. Success must produce
the same AST; failure must produce the same code — the message after it is each implementation’s own
words and is not compared. A disagreement here is a bug report against the specification, not
against you: it is exactly where the prose left room. That is how the slot-offset defect in
023-slot-offset-survives-a-stripped-line was found, and planning a second implementation is what
surfaced it.
If you find one, say so. A third implementation disagreeing with these two is the most useful thing that can currently happen to this format.
What you do not have to implement
Stated because an implementer who thinks these are required will conclude the job is bigger than it is:
- Anything inside
{{ … }}. BMX does not define what an expression is — you hand the trimmed bytes and the offset to whatever is rendering, and stop. That is the format’s central boundary. - Lints. The
BMX-Wcodes on when it refuses are a quality-of-life surface, not conformance. No fixture requires them. - Level 2. If your language has no type system there is nothing to check a slot against, and saying so plainly is the honest answer rather than a gap.
- Nested lists, quotes, reactivity, components, a dialect flag. Not absent from your
implementation — absent from the format.
SPEC.md§7 lists every omission with the trigger that would earn it a version.
What you are signing up for, honestly
BMX is 0.12, and inside 0.x a minor version may break a document. Every such change is written
up in VERSIONING.md with the
fixture it edited, and the rule is mechanical: an edited case in tests/cases/ proves a major, so
git diff --diff-filter=M tests/cases tells you whether a release can move a document under you.
Pin a tag and re-run the suite when you bump it; that is the whole maintenance cost.
Two things you should know before you start rather than after. Both implementations here have one author, so their agreement catches drift and regression — it does not prove the spec unambiguous. And there is no npm package and no registry release of the reference parser, on purpose: publishing one is a promise to maintain a release channel, and a format at 0.12 should not make that promise yet. Copy the file.
Which is why your implementation is worth more to this format than another feature is. 1.0 is
not earned by the feature list looking finished. It needs an implementation written by somebody who
did not write this spec, passing the suite, plus a real document set that has tested
SPEC.md §7’s absences against something
other than imagination. Both halves are waiting on somebody who is not us.