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
            | blank

Three 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. C2 both names and locates a cell. #id decouples

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 claim

Two kinds of term, and the difference matters:

termmeaningfollows the node?
#ida memberyes — the box moves with it
*refa positional claimno — 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 -> E2

The 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 symbol

OVERLAY — leave room for telemetry

OVERLAY
OVERLAY TR

Says 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 BR

PAGE — decomposition

PAGE main "Overview"
C2 COMPUTE "EC2" #ec2 @app-tier

PAGE app-tier "Application Tier"
B2 COMPUTE "TASK" #task

Scope 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                     removes

A 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 out

Reuses 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 alias

Both 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 library

Resolution 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.**

codemeaning
unterminated-quotea string never closed; terminated at end of line
unparsedthe line matched no statement form
unknown-tokena token in a placement that is not an id, drill or symbol
unknown-id#id names nothing on this page
bad-refa malformed cell reference
bad-terma malformed container term
empty-container[] with no terms
duplicate-idthe same #id twice within one frame
no-column-extenta container whose terms constrain no column
no-row-extenta container whose terms constrain no row
bad-symbola symbol token that cannot be resolved
duplicate-aliasthe same ICON name bound twice
duplicate-pagethe same PAGE id twice
unknown-page@page or a request names a page that is not declared
cross-pagean 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