Building on BMX
For anyone — person or agent — writing a layer above BMX: a component system, a framework, a static site generator, an editor integration. star-burxt is the first of these, and this page is the contract it codes against.
Read this before extending anything. Most of what a framework wants to add to BMX belongs in the host language instead, and the difference is not a matter of taste — it is what keeps the format implementable by somebody who is not you.
The one rule
BMX describes structure. Everything that happens at runtime belongs to the host.
That is BOUNDARY.md in a sentence.
Applied concretely:
| You want | Where it goes |
|---|---|
| Conditionals, loops | the host. A view is a function; it already has if and while |
| Reactivity, state, stores | the host. BMX has no runtime and will not grow one |
| Components, composition | the host. Two views compose by one calling the other |
| Event handlers | the host. A document has no way to name a function, deliberately |
| A new block or inline type | the format, via a spec change and conformance cases — and only if a real document needed it |
| A new expression syntax | nowhere. The slot’s contents are the host’s language, entire |
If your feature needs the format to change, you are almost certainly holding it wrong. The test: could a language with no type system still implement this? If not, it is not the format’s.
What you can rely on
These are guaranteed, verified by the conformance suite, and will not change inside 0.x without a major version:
Every slot value is escaped, always. There is no raw syntax and there will not be one. A layer above BMX cannot accidentally emit an unescaped value through a document, because the document has no way to express one.
A malformed document is an error, never partial output. Parsing answers a result or a code — it never answers a half-built tree. So a generated document that got truncated fails loudly rather than rendering nearly right.
Slot offsets point into the author’s source. offset is the byte index of the first byte of
the trimmed expression. This is what lets you report an error at a position in the .bmx file the
author opened rather than in whatever you generated. It is mandatory, and it is tested.
Adjacent text nodes are merged. a b is one text node, always. Two implementations that
disagree about that disagree about the document.
A code block’s content is never parsed. No slots, no inline markup. That is what makes it possible to document BMX in BMX — and what lets you put a framework’s own syntax in a fenced block without the format touching it.
What you must implement yourself
The format requires these of a host and does not provide them:
Refuse dangerous link schemes. [click](javascript:steal()) is a working attack that no
character escaping addresses, because the danger is the scheme. The allowed set is yours; emitting
an arbitrary scheme unexamined is not conforming. Burxt’s answer is bmx_target_allowed, which
permits http, https, mailto and relative targets.
Decide what an expression means. BMX hands you text and an offset. Whether order.total
resolves, type-checks, or is allowed to touch the filesystem is entirely yours.
Decide what a missing value does. BMX has no opinion. Burxt’s answer is to refuse — a slot with no binding is an error, never an empty string, because the empty string is how a page ships with a missing total nobody sees. A framework that renders blank here has chosen to, and should say so.
The Burxt surface
If you are building on Burxt specifically, this is what exists today. Signatures are exact;
lib/bmx.bx and lib/html.bx are ordinary Burxt you can read.
Parsing
function bmx_parse(source: String) -> Result<[Block], String>
function bmx_json(blocks: [Block]) -> Json
Block is Heading · Paragraph · Quote · List · Code; inline Bmx is Text ·
Emphasis · Strong · CodeSpan · Link · Slot. Walk them with match.
Rendering — level 1
pure function bmx_bind(name: String, value: String) -> Binding
function bmx_html(blocks: [Block], bindings: [Binding]) -> Result<Html, String>
function bmx_to_html(source: String, bindings: [Binding]) -> Result<String, String>
Generating — level 2
function bmx_emit_burxt(blocks: [Block], source_name: String, name: String,
parameters: String, clauses: [String]) -> Result<String, String>
Turns a document into a pure function … -> Html whose slots are ordinary expressions. The
signature comes from the caller, not from the document — BMX has no front matter, and a
generator inventing one would be adding to the format from the host side.
The output tree
pure function html_text(value: String) -> Html // escaped on render
pure function html_raw(trusted: String) -> Html // the waiver, spelled out
pure function html_element(tag: String, attrs: [Attr], children: [Html]) -> Html
pure function html_attr(name: String, value: String) -> Attr
pure function html_render(node: Html) -> String
This is where a framework attaches. bmx_html gives you an Html tree, not a string — so you
can wrap it, walk it, or splice your own elements around it before rendering. Building the tree in
Burxt and rendering once is how you add anything BMX does not have.
Two things the tree refuses, both by contract on the constructor: a tag or attribute name that is not a name, and a void element carrying children. Both are holes escaping does not cover.
Everything above is pure. That is load-bearing rather than decoration: a view built from
these can be pure, which is what makes burxt effects --allow "" a confirmation by construction
rather than a hope — and it is what lets a view compile to a WebAssembly island that provably
touches nothing.
What does not exist yet
Stated plainly because a document that lets you infer capabilities you do not have is worse than one that says nothing.
No reactivity, no DOM updates, no event handling, no stores, no lifecycle. star-burxt is the plan for these and they are unbuilt. What exists today is: a document becomes a typed function, that function produces HTML, and that HTML can be served over CGI or compiled to WebAssembly and called from JavaScript.
No component composition beyond calling a function. Which may be enough — a view is a function, so composing views is ordinary code — but there is no slot-filling, no children, no props system.
No hot reload, no dev server, no bundler integration.
No nesting in documents — no nested lists or quotes. If a framework needs them, that is a format change with conformance cases, and a real document that needs it is what earns it.
If you extend the format anyway
Sometimes the answer really is a format change. Then:
- A case in
tests/is the proposal. Where the spec and the tests disagree, the tests win, so the case is the specification of your feature. - Adding a case is a minor; editing one is a major. The suite is the semver —
git diff --diff-filter=M tests/decides it mechanically rather than by judgement. - Error codes are permanent. Once assigned, a code means that thing forever; retire it rather than reuse it, because a host may be keyed on it.
- Both implementations must agree.
python3 tests/agree.py 'node reference/bmx.js' '<yours>'asks the question the suite cannot: do two implementations reach the same answer where nothing was written down? That is where a spec’s ambiguities live.