(* ========================================================================== blueprint DSL — grammar, v1 file extension: .bp --------------------------------------------------------------------------- Two rules carry the whole design: 1. LINE-ORIENTED. Every construct is one line beginning with a keyword. Indentation is decorative and carries NO meaning — an author (or a language model) that mis-indents still produces a valid file. 2. CONTEXT-SCOPED. A declaration line opens a scope; the attribute lines that follow attach to it until the next declaration. Attribute keywords are validated against the open scope, so a misplaced attribute is a precise error rather than silently-dropped data. The single exception to rule 1 is the block scalar (`>` and `|`), where indentation delimits the block. That is the one construct where prose needs to span lines, and it follows YAML's convention so it reads as expected. ========================================================================== *) file = { line } ; line = comment | blank | declaration | attribute ; comment = ws , "#" , { any - newline } , newline ; (* whole-line only *) blank = ws , newline ; (* -- declarations: each opens a scope ------------------------------------- *) declaration = blueprint_decl | stat_decl | group_decl | node_decl | edge_decl | tab_decl | term_decl ; blueprint_decl = ws , "blueprint" , sp , slug , newline ; stat_decl = ws , "stat" , sp , label_text , "=" , value_text , newline ; group_decl = ws , "group" , sp , slug , sp , quoted , newline ; node_decl = ws , node_kind , sp , slug , sp , quoted , { sp , node_modifier } , newline ; node_kind = "entry" | "svc" | "store" | "queue" | "model" | "lib" | "ext" | "job" (* long forms accepted and normalised *) | "entrypoint" | "service" | "library" | "external" ; node_modifier = weight | position | status ; weight = "x" , number ; (* x1.4 — 0.5 .. 2.0 *) position = "@" , integer , "," , integer ; (* @4,-2 — manual layout *) status = "!" , ( "dormant" | "planned" ) ; (* omitted = active *) edge_decl = ws , slug , sp , arrow , sp , slug , [ sp , quoted ] , newline ; arrow = [ "<" ] , "-" , edge_kind , "->" ; edge_kind = "data" | "call" | "event" | "read" | "write" | "spawn" ; tab_decl = ws , "tab" , sp , slug , sp , quoted , newline ; term_decl = ws , "term" , sp , quoted , "=" , value_text , newline | ws , "term" , sp , quoted , sp , block_intro , newline , block ; (* -- attributes: valid only inside a matching scope ----------------------- *) attribute = bp_attr | group_attr | node_attr | edge_attr | tab_attr ; bp_attr = ws , ( "title" | "tagline" | "branch" | "stamp" ) , sp , text_value , newline ; group_attr = ws , "note" , sp , text_value , newline ; node_attr = ws , "is" , sp , text_value , newline (* summary *) | ws , "why" , sp , text_value , newline (* detail *) | ws , "src" , sp , path , { sp , path } , newline | ws , "uses" , sp , word , { sp , word } , newline | ws , "num" , sp , label_text , "=" , value_text , newline ; edge_attr = ws , "as" , sp , slug , newline (* explicit id *) | ws , "carry" , sp , text_value , newline (* payload *) | ws , "why" , sp , text_value , newline | ws , "vol" , sp , number , newline (* 0.0 .. 1.0 *) | ws , "via" , sp , position_pair , { sp , position_pair } , newline ; tab_attr = ws , "h" , sp , text_value , newline | ws , "p" , sp , text_value , newline | ws , "note" , sp , text_value , newline | ws , "code" , sp , block_intro , newline , block | ws , "---" , newline ; (* rule *) (* -- values --------------------------------------------------------------- *) text_value = inline_text | ( block_intro , newline , block ) ; inline_text = { any - newline } ; (* to end of line, then trimmed *) block_intro = ">" (* folded: lines joined with single spaces *) | "|" ; (* literal: newlines preserved verbatim *) (* A block consists of every following line indented STRICTLY MORE than the line carrying the block_intro. It ends at the first line that is not, or at end of file. The common leading indent is stripped. Blank lines inside a block are kept: in a folded block they become a paragraph break, in a literal block they are preserved as-is. *) block = { block_line } ; block_line = deeper_ws , { any - newline } , newline ; quoted = '"' , { ( any - '"' ) | '\"' } , '"' ; slug = lowercase , { lowercase | digit | "_" } ; (* 2..48 chars *) label_text = { any - "=" - newline } ; value_text = { any - newline } ; path = { any - sp - newline } ; word = { any - sp - newline } ; number = [ "-" ] , digit , { digit } , [ "." , digit , { digit } ] ; integer = [ "-" ] , digit , { digit } ; position_pair = integer , "," , integer ; ws = { " " | "\t" } ; sp = " " , { " " | "\t" } ; (* -- inline markup, valid inside any prose value --------------------------- [[display text|node_id]] links a phrase to a node, both directions {{term}} marks a glossary term declared with `term` Everything else is literal. There is no markdown and no HTML. ------------------------------------------------------------------------ *) (* -- scoping rules --------------------------------------------------------- `blueprint` opens the document scope. Must be the first declaration. `group` opens a group. Every node declared after it belongs to it, until the next `group`. This is why a node has no group field. node_decl opens a node scope. edge_decl opens an edge scope. Both endpoints must resolve; forward references are fine, resolution happens after the whole parse. `tab` opens a narrative tab. `h`/`p`/`note`/`code`/`---` append blocks to it in order. `term` is self-contained and does not open a scope. An edge with no `as` is given the id `__`, truncated to 48 chars and suffixed on collision. ------------------------------------------------------------------------ *)