# Platen — the diagram notation 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. ## 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. ```text 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 ``` ```text [#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: | 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 -> 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 ```text 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 ```text 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 ```text 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`.** | 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