A hands-on guide to authoring RMDoc-DSL fixtures in YAML or JavaScript (goja), rendering them to debug PNGs, and using them to systematically debug rendering issues.
RMDoc-DSL is a small, versioned DSL that lets you describe “reMarkable-like” documents in a human-readable way. The immediate payoff is debug velocity: you can generate controlled test fixtures (like “ellipse at y=1500”) without hand-authoring binary .rm files, and without the ambiguity of reusing a screenshot from a different fixture.
In the short term, RMDoc-DSL is about producing programmatic PNG debug renders quickly (grid overlays, predictable coordinate space, stable outputs). In the long term, it’s also a foundation for generating real .rmdoc notebooks and for building interoperability (“create documents from other applications”) without baking format details into every tool.
This page shows:
If you want the spec itself (the canonical schema and design notes), see:
remarquee help rmdsl-spec
(That document lives in the RMQ-0006 ticket; this page focuses on usage.)
RMDoc-DSL sits between “author intent” and “renderer output”.
+--------------------+
| YAML or JS Case |
| (RMDoc-DSL v0) |
+---------+----------+
|
v
+--------------------+
| Loader/Validator |
| (defaults, ids, |
| schema sanity) |
+---------+----------+
|
v
+--------------------+
| Programmatic PNG |
| Renderer (no PDF) |
| (grid + overlays) |
+--------------------+
Key design principle: fixture identity is everything. We only ever compare outputs if they come from the same tuple:
.yaml or .js).rmdoc and device screenshot of the same fixtureYou need:
remarquee repo checkoutgo run from the repo root)RMDoc-DSL rendering currently uses a ticket script:
ttmp/2025/12/24/RMQ-0006--rmdoc-rendering-testing-validation-golden-runner/scripts/18-rmdsl-render-to-png/main.goThis script produces programmatic PNGs (no PDF renderer involved).
By default, RMDoc-DSL uses rm_screen_v6 coordinate space:
Think of it as:
Y=0 (top)
|
| X=-702 X=0 X=+702
| |------------|------------|
v
Y=1872 (bottom)
This matters because a lot of “ellipse is off” debates are actually:
The DSL tries to make the coordinate model explicit so we can debug systematically.
YAML is ideal for:
The ticket includes:
ttmp/.../cases/01-ellipse-at-bottom.yamlIt describes:
y: 1500Run this from the remarquee/ repo root:
go run ./ttmp/2025/12/24/RMQ-0006--rmdoc-rendering-testing-validation-golden-runner/scripts/18-rmdsl-render-to-png/main.go \
--in ./ttmp/2025/12/24/RMQ-0006--rmdoc-rendering-testing-validation-golden-runner/cases/01-ellipse-at-bottom.yaml \
--out /home/manuel/workspaces/2025-12-14/build-remarquee-tool/remarquee/rendering/rmq-0006-ellipse
Expected output:
--out directory named like ellipse-at-bottom-p1.png (the exact name depends on the document/page IDs).YAML is intentionally static. JavaScript is for when you want:
The JS runtime is goja embedded in Go. This is not Node.js. It’s a controlled sandbox intended for case generation.
A JS case file must provide an entry point in one of these forms:
function main(params) { ... return dslObject; }exports.default = function(params) { ... }module.exports = function(params) { ... }It must return a plain object matching the DSL schema:
{ rm_dsl: "v0", document: { ... } }rm builder APIThe runtime injects a global rm namespace that lets you build the DSL object fluently.
At a high level, you can think of it as:
rm.doc(name)
-> .page(id).canvas(space,w,h)
-> .layer(name)
-> add items (ellipse/rect/stroke)
-> .done() // returns plain object
The ticket includes:
ttmp/.../cases/02-ellipse-at-bottom.jsIt uses the rm builder:
function main(params) {
return rm.doc("ellipse-at-bottom-js")
.notebook().v6()
.page("p1").canvas(rm.space.rm_screen_v6, 1404, 1872)
.layer("shapes")
.ellipse({ x: 0, y: 1500 }, 240, 140).stroke(rm.tool.fineliner_2, rm.color.black, 1)
.rect({ x: 280, y: 1050, w: 320, h: 320 }).rotateDeg(15).stroke(rm.tool.fineliner_2, rm.color.black, 1)
.stroke(rm.tool.fineliner_2, rm.color.red, 1).polyline([{ x: -650, y: 50 }, { x: -450, y: 50 }])
.stroke(rm.tool.fineliner_2, rm.color.green, 1).polyline([{ x: -650, y: 1820 }, { x: -450, y: 1820 }])
.done();
}
go run ./ttmp/2025/12/24/RMQ-0006--rmdoc-rendering-testing-validation-golden-runner/scripts/18-rmdsl-render-to-png/main.go \
--in ./ttmp/2025/12/24/RMQ-0006--rmdoc-rendering-testing-validation-golden-runner/cases/02-ellipse-at-bottom.js \
--out /home/manuel/workspaces/2025-12-14/build-remarquee-tool/remarquee/rendering/rmq-0006-ellipse
Expected output (example):
ellipse-at-bottom-js-p1.pngrm.include()JS cases can load helper code using:
rm.include("./lib/helpers.js");
Rules:
This section is intentionally opinionated. It’s the workflow we want to standardize on.
Use a sweep case to quickly check whether your renderer has:
Pseudo-approach:
for y in [200, 600, 1000, 1400, 1700]:
render ellipse centered at (0, y)
render marker at y=50 and y=1820
compare outputs between renderers
If the ellipse moves consistently with y in the PNGs, your coordinate mapping is likely correct. If it “compresses” or “drifts”, you likely have a scaling bug.
If you’re using the device as ground truth, you must ensure you’re looking at the same fixture.
Checklist:
.rmdoc bytes are on deviceRMDoc-DSL helps here because you can generate “obvious markers”:
That typically suggests:
RMDoc-DSL can isolate this quickly:
The renderer script currently expects --out to already exist.
Reason: it’s intentionally conservative and avoids side effects in scripts unless explicitly requested.
Your .js file must define one of:
function main(params) { ... }exports.default = function(params) { ... }module.exports = function(params) { ... }The loader injects rm, but only when the input is a .js case.
If you’re executing the JS with a different runtime (or copying code into another tool), you won’t have the builder.
RMDoc-DSL can now be compiled into a real V6 .rmdoc notebook with strokes, glyph highlights, and typed text.
This is the missing bridge for device validation: generate the same fixture bytes,
upload them, and confirm the notebook is editable.
Run from the remarquee/ repo root:
go run ./cmd/remarquee rmdsl compile \
./ttmp/2025/12/24/RMQ-0006--rmdoc-rendering-testing-validation-golden-runner/cases/03-ellipse-sweep.js \
--out ./rendering/rmq-0009-ellipse/ellipse-sweep.rmdoc
Notes:
ellipse, rect) are lowered to polyline strokes.There is a convenience script in the RMQ-0009 ticket:
ttmp/2026/01/10/RMQ-0009--compile-rmdoc-dsl-to-rmdoc/scripts/01-compile-ellipse-sweep-rmdoc.shThe core implementation lives in:
pkg/rmdsl/
LoadFromFile) and normalization (Normalize)js.go) providing:
rm builder preluderm.include()The initial consumer is:
ttmp/.../scripts/18-rmdsl-render-to-png/main.go