Platen
The published grammar of the diagram notation.
This is a reference, not a rationale. Where a rule exists for a reason that is not obvious from the syntax, the reason is stated in one line and no further. Section references of the form §N point at sections of this document.
The same reference as plain text: /platen.txt. The fence tag is diagram.
1. Shape
Flat, line-oriented. No nesting, no braces, no indentation significance. One statement per line; blank lines and stanzas are preserved by every edit.
document := statement*
statement := title | attribution | note | code | page | keyframe | hide
| frame | zoom | icons | icon | placement | container | edge
| blankThree statement forms carry the drawing — placement, container, edge — and nine keywords configure the document around them.
Three rules that constrain everything downstream
- Position is identity.
C2both names and locates a cell.#iddecouples
them, and exists so that moving something is a one-line diff.
- The author places; the engine only routes. There is no auto-layout and
there must never be one. Composition carries meaning.
- Declaration order is semantic. Two containers over the same range nest by
order of declaration; overlapping containers resolve their level by it. Reordering two lines changes the drawing.
2. Lexical
ref := /[A-Z]+[0-9]+/ column letters, row number, both 1-based
range := ref (':' ref)? A1 or C1:E1
id := /[A-Za-z0-9_-]+/ after '#'
page-id := /[A-Za-z][A-Za-z0-9_-]*/
string := '"' … '"' may span lines — see §9
duration := /[0-9]+ms/
number := /[0-9]+(\.[0-9]+)?/A1 is column A, row 1. Columns run A…Z, AA, AB…. Both axes are 1-based as written and 0-based internally; a published span is 0-based, a diagnostic's line is 1-based. (Noted because it is the one inconsistency in the system.)
Comments do not exist. A line that parses as nothing is a diagnostic, not a comment. Blank lines are the only inert line.
$NAME — a value supplied with the request
Inside a quoted string, $NAME is replaced by a value the caller supplies. Names match /[A-Za-z][A-Za-z0-9_]*/ and are case-insensitive, so $SHARE and $share are one name; uppercase is the convention.
TITLE "PAYMENTS — $ENV"
NOTE TR "$SHARE"It is substituted only inside a string, and only into text an author wrote: node, container and edge labels, TITLE, ATTRIBUTION and NOTE. NOTE TR $SHARE without the quotes is a parse error. Ids, cell references, page names, symbol names and keyframe labels are never substituted — they are the language's own vocabulary rather than prose, so a value can caption a diagram and can never restructure one.
A name with no value supplied is left standing in the drawing and reported as unknown-var, so the gap is visible rather than silent.
$$ is a literal dollar, and is the only escape. There are no expressions, no defaults and no conditionals: a richer form would be a template language, which belongs outside a layout engine.
A value is always text. It cannot open a string, close one, or introduce a statement, however it is spelled — substitution happens after the document has been parsed, so what a value can affect is what a label can affect.
3. Placement — a node
placement := range symbol? string? ('#' ident)? ('@' ident)?The range comes first. Everything after it is order-free — the parser dispatches on the first character (# id, @ drill-down) or on whether the token resolves as a symbol.
C2 an empty cell, claimed
C2 "EC2" a text cell
C2 COMPUTE a symbol, no label
C2 COMPUTE "EC2" the common form
C2 COMPUTE "EC2" #ec2 named, so it can be referenced
C1:E1 "A WIDE BLOCK OF PROSE" a span across three columns
E2 archive "→ APP TIER" @app-tier drills down to another page- A span (
C1:E1) occupies every cell in the rectangle. - The label is the only quoted string; it wraps to the cell's width,
measured in advance units — a Latin letter is one, a CJK ideograph or an emoji is two — and breaking between ideographs where a script writes no spaces. Arabic, Hebrew, Devanagari and Thai are not supported: their advance is not a function of character count at all, and no monospace font changes that.
- **
#id** is optional. Unidentified nodes are keyed positionally, which is
why an editor promotes to an id the moment a move would otherwise have to rewrite the edges too.
- **
@page** marks a drill-down. The affordance that draws it is
derived from this, never declared.
Id uniqueness is per frame, not per document. A repeat inside one frame is duplicate-id. A later KEYFRAME naming an existing id re-places it, which is how animation works and is not a duplicate. Ids are shared across pages, so the same id on two pages in the same frame is a duplicate.
4. Container
container := '[' termlist symbol? string? ']'
termlist := term (',' term)*
term := '#' ident | '*'? ref[#web, #db] members, keyed by what they hold
[#web, #db "PUBLIC SUBNET"] labelled
[#ec2 SHIELD "WEB VPC 10.0.X.X"] with a symbol on the label
[B1:E3] sugar for [*B1, *E3]
[#web, *F] a member plus a column claimTwo kinds of term, and the difference matters:
| term | meaning | follows the node? |
|---|---|---|
#id | a member | yes — the box moves with it |
*ref | a positional claim | no — it holds a cell |
Members floor the range, stars ceiling it. A star accepts a cell (*C2), a whole column (*E) or a whole row (*3); a column or row contributes only to its own axis, so *E widens without stretching.
A container needs both axes. [*A, *C] is no-row-extent and is refused.
Nesting is by containment of range, not by syntax — and where two ranges are equal or partially overlap, by declaration order.
5. Edge
edge := endpoint ('->' | '<->') endpoint string?
endpoint := ref | '#' ident#web -> #db
#web <-> #db "SIGNED URL"
C2 -> E2The engine routes; the author never draws a path. An endpoint may be positional or an id, and a positional endpoint is rewritten automatically if that node is later moved by the editor.
An edge may not cross a page. Endpoints on different pages are cross-page — a diagnostic, not a feature. Continuation is a connector node the author places, because the language rests on the author placing.
6. Document keywords
title := 'TITLE' string
attribution := 'ATTRIBUTION' string
note := 'NOTE' ('TL'|'TR'|'BL'|'BR') string
code := 'CODE' ('TL'|'TR'|'BL'|'BR') string?
overlay := 'OVERLAY' ('TL'|'TR'|'BL'|'BR')?
page := 'PAGE' ident string?
frame := 'FRAME' ('MIN' | 'MAX')
keyframe := 'KEYFRAME' string duration?
hide := 'HIDE' '#' ident
zoom := 'ZOOM' (termlist | 'ALL') ('PAD' number)?
icons := 'ICONS' name
icon := 'ICON' name symbolOVERLAY — leave room for telemetry
OVERLAY
OVERLAY TRSays that this diagram may have telemetry painted onto it, and reserves a small square at the named corner of every cell so that arriving telemetry never moves anything. TR by default: a label sits at the bottom of its cell and a symbol in the middle, so the top corners are the empty ones.
It carries no values. What is painted arrives with the request that renders the diagram, keyed by #id — an overlay changes every minute, and a diagram that changed with it would have a new version, a new address and a meaningless diff every time a service went amber.
A document that does not say OVERLAY is drawn exactly as it was before the statement existed. Nothing is reserved and nothing can be painted.
Give an overlaid element an #id. A cell's position is not a stable name — an inserted column renumbers it — and telemetry that follows a position rather than an element repaints the wrong thing after any edit.
CODE — a scannable mark in a corner
CODE BR
CODE BR "DGM.INK/K7M2Q4X"Reserves a square in one of the four corners and draws the string in it as a scannable code. The string is **optional and defaults to $SHARE**, which is the common case: the address of a diagram is not something an author types, and $SHARE is supplied with the request like any other value (§2).
A document may have one. A second CODE replaces the first — one sheet, one address, and no way to say which of two marks is the one to scan.
The mark carries a URL, so the string is made into one. A payload with no :// in it is given HTTPS://, and the whole payload is upper-cased. Write DGM.INK/K7M2Q4X and the mark holds HTTPS://DGM.INK/K7M2Q4X; a phone opens it, where a payload with no scheme is text and a phone searches for it. A scheme you write yourself is kept. The case does not matter to a reader — a scheme and a host are case-insensitive, and a share code is uppercase already — and it matters to the mark, which has no lowercase to encode with.
The corner it takes is otherwise empty, so the reserve costs the sheet only a little height. A NOTE in the same corner keeps its distance from it automatically; nothing overlaps.
Printing the address in words as well is an ordinary note, and the two are independent — CODE is the mark, NOTE is the text:
NOTE BL "$SHARE"
CODE BRPAGE — decomposition
PAGE main "Overview"
C2 COMPUTE "EC2" #ec2 @app-tier
PAGE app-tier "Application Tier"
B2 COMPUTE "TASK" #taskScope is flat: a statement belongs to the most recently declared page, and a document with no PAGE has one called main. Each page lays out independently with its own sheet and its own display list, so C2 on one page is a different cell from C2 on another — while #id is shared across the whole document.
Nothing may cross a page: neither an edge (§5) nor a container reaching off one.
KEYFRAME / HIDE — animation
A1 "ONE" #a frame 0 is everything before the first KEYFRAME
KEYFRAME "Scale out" 1200ms
B1 "TWO" #b adds
C1 #a re-places #a
HIDE #b removesA keyframe is a delta, and HIDE is the only statement that subtracts. Frame n is frame n−1 with keyframe n applied. A duration is optional; without one the frame waits for the presenter.
References resolve per frame — which node #a names depends on the frame that last placed it.
FRAME — how frames are sized against each other
MAX (default) lays every frame into one stage big enough for all of them, so the sheet never changes and a host that centres its canvas never moves the drawing. MIN gives each frame its own extent.
ZOOM — what part to look at
ZOOM #ec2 one node
ZOOM #ec2, #s3 PAD 1 two, with a cell of room around them
ZOOM C2:E4 a range
ZOOM *2 a whole tier
ZOOM ALL back outReuses the container's term list, so every form a container accepts a zoom accepts. A zoom is sticky — it holds until another replaces it, which is why ZOOM ALL exists. An axis no term constrains is derived from the canvas ratio rather than being an error. The view is grown into the ratio and never cropped: everything a zoom names is visible when it settles.
ICONS / ICON — symbol naming
ICONS aws bare names resolve against this pack
ICON db aws:databases/amazon-rds-multi-az a document-local aliasBoth are purely syntactic — they rewrite a name into a ref and never ask a pack what it contains, so a quarterly artwork refresh cannot silently redraw a diagram. An alias resolves one hop only.
7. Symbols
symbol := keyword | ref | qualified | name
keyword := /[A-Z][A-Z0-9_]*/ COMPUTE, PERSON, LOAD_BALANCER
ref := /[a-z-]+:[a-z0-9-]+\/[a-z0-9-]+/ aws:databases/rds
qualified := /[a-z-]+:[a-z0-9-]+/ aws:rds → default category
name := /[a-z][a-z0-9-]*/ database → the core libraryResolution order, first match wins:
1. a document alias from ICON — the author's own word, bound at parse time 2. a keyword alias from the built-in registry 3. a full ref, used as written 4. **pack:name, expanded with the pack's default category 5. a bare name**, resolved against ICONS or the core pack
A bare name is the core library, and that is the shape the language wants: A1 database "DB" reads better than a prefix, and a prefix earns its place only when it disambiguates. Keywords remain as aliases.
An unresolvable symbol draws an empty box of the correct size — never an error, never a broken layout.
8. Diagnostics
Every diagnostic carries a stable machine-readable code, a 1-based line, a human msg and sometimes a hint. **A generation loop branches on code, never on msg.**
| code | meaning |
|---|---|
unterminated-quote | a string never closed; terminated at end of line |
unparsed | the line matched no statement form |
unknown-token | a token in a placement that is not an id, drill or symbol |
unknown-id | #id names nothing on this page |
bad-ref | a malformed cell reference |
bad-term | a malformed container term |
empty-container | [] with no terms |
duplicate-id | the same #id twice within one frame |
no-column-extent | a container whose terms constrain no column |
no-row-extent | a container whose terms constrain no row |
bad-symbol | a symbol token that cannot be resolved |
duplicate-alias | the same ICON name bound twice |
duplicate-page | the same PAGE id twice |
unknown-page | @page or a request names a page that is not declared |
cross-page | an edge or container reaching off its page |
unknown-var | $NAME in a string, with no value supplied for it |
A diagnostic never withholds the drawing. The engine returns what it could draw and what was wrong, together. This is load-bearing: no single keystroke may reduce the drawing to nothing, or an editor loses the diagram every time a quote is half-typed — and a model iterating on its own output needs the same guarantee for the same reason.
9. Two parsing rules worth knowing
Strings may span lines, but not statements. The tokenizer joins lines until quotes balance — otherwise typing one " would swallow the rest of the document. Two mitigations, both required: scanning stops if a line looks like a new statement, and an unbalanced quote at end of scan terminates implicitly and reports unterminated-quote.
Whitespace is insignificant except as a separator. Leading and trailing space is trimmed; runs of space between tokens are one separator.
10. A complete example
TITLE "SAMPLE DIAGRAM"
NOTE TR "DIAGRAM.INK/JPM/DWOJ2BFD"
CODE BR "DGM.INK/DWOJ2BFD"
A1 "LOREM IPSUM DOLOR SIT AMET"
A2 PERSON "CUSTOMER [ UPLOAD ]" #up
A3 PERSON "CUSTOMER [ DOWNLOAD ]" #down
B1 LOGO_AWS
B2 LOAD_BALANCER "APPLICATION LOAD BALANCER" #alb
C1:E1 "A WIDE BLOCK OF PROSE ACROSS THREE COLUMNS"
C2 COMPUTE "EC2" #ec2
C3 archive "S3" #s3
D2 database "RDS" #rds
D3 DISK "BACK UP" #bk1
E2 DISK "BACK UP" #bk2
[B1:E3]
[#ec2 SHIELD "WEB VPC 10.0.X.X"]
[#rds, #bk2 "NO DIRECT INTERNET ACCESS"]
[#rds, #bk2 SHIELD "DB VPC 10.1.X.X"]
#up -> #alb
#alb -> #ec2
#ec2 -> #rds
#ec2 -> #s3
#down <-> #s3 "SIGNED URL"
ATTRIBUTION "CODIFIED BY JP MCMULLAN"11. Notes for anyone indexing this language
A declaration is a tuple, so a structural projection is close to trivial — but four things are easy to miss:
1. **A placement can carry @page, and its token order after the range is free. Extract by prefix, not by position. 2. A range is a rectangle, not a cell.** C1:E1 occupies three cells, and containment queries must use the extent rather than the origin. 3. Container terms are of two kinds, and only #id members follow their node. A projection that treats *C2 as a membership edge will be wrong the moment anything moves. 4. Ids are unique per frame, not per document. A document with keyframes has the same #id on several lines legitimately, each re-placing it. An index keyed on id alone will collapse them.
Containment is positional and therefore cheap: a cell is inside a container when its address falls within the container's resolved range — two integer comparisons, computed once at index time, no tree required.
Diagnostic index
Every code the engine can emit. A generation loop branches on code, never on msg.
- unterminated-quote
- a string never closed; terminated at end of line
- unparsed
- the line matched no statement form
- unknown-token
- a token in a placement that is not an id, drill or symbol
- unknown-id
- `#id` names nothing on this page
- bad-ref
- a malformed cell reference
- bad-term
- a malformed container term
- empty-container
- `[]` with no terms
- duplicate-id
- the same `#id` twice within one frame
- no-column-extent
- a container whose terms constrain no column
- no-row-extent
- a container whose terms constrain no row
- bad-symbol
- a symbol token that cannot be resolved
- duplicate-alias
- the same `ICON` name bound twice
- duplicate-page
- the same `PAGE` id twice
- unknown-page
- `@page` or a request names a page that is not declared
- cross-page
- an edge or container reaching off its page
- unknown-var
- `$NAME` in a string, with no value supplied for it