Skip to content

Latest commit

 

History

History
422 lines (306 loc) · 13.6 KB

File metadata and controls

422 lines (306 loc) · 13.6 KB

Wyreframe Syntax v2.3 Reference

The canonical reference for Wyreframe ASCII wireframe syntax, version 2.3.0. This document covers the wireframe syntax only; the Interaction DSL is parsed separately and is not part of v2.3.

Parse this syntax with the V2 parser:

import { parse } from 'wyreframe/parser/v2';
const result = parse(source);

Table of Contents

  1. Block Types
  2. Core Elements
  3. ID System
  4. String Literals
  5. Emoji Shortcodes
  6. Prop Placeholders
  7. Implicit Layout
  8. Parsing Priority
  9. Unicode and Whitespace
  10. Errors and Warnings

Block Types

Every wireframe file consists of one or more top-level blocks. A block starts with a @scene: or @component: declaration. Blocks cannot be nested — a nested declaration is an error (NestedBlockDeclaration), and a file with no declaration at all is an error (MissingBlockDeclaration).

Scene

@scene: login          # required — unique slug
@title: Login Page     # optional — page title
@device: mobile        # optional — mobile | tablet | desktop
@transition: fade      # optional — default transition effect

Component

@component: user-card  # required — unique slug
@props: name, avatar?  # optional — comma-separated; `?` suffix = optional prop

Duplicate prop names emit DuplicatePropName (last declaration wins).


Core Elements

Container

A box drawn with + (corners), - (horizontal borders), and | (walls):

+--Header-----------+      <- top border, optional name
|  "Welcome"        |      <- body rows: children of the container
|  [ Start ]        |
+-------------------+      <- bottom border closes the container
  • The text embedded in the top border (+--name--+) becomes the container's name. A plain border (+-----+) yields a name-less container.
  • Containers nest recursively; a nested container must fit completely within its parent's walls.
  • A missing bottom border raises UnclosedContainer (with recovery).
  • Wall/corner alignment is tolerant: drift within containerColumnTolerance (default ±1 col) parses with a warning (MisalignedContainerWall, MisalignedContainerCorner); border width mismatch within containerWidthTolerance (default ±2) warns with InconsistentContainerWidth.
  • Nesting deeper than maxDepth (default 10) raises MaxDepthExceeded; the offending container parses as empty and is marked containsErrorRecovery: true.

Text

Any content that matches no other pattern is plain text (the lowest-priority fallback):

|  Welcome back!    |

Alignment (Left / Center / Right) is inferred from the text's position within its container (see centerSymmetryThreshold, rightAlignThreshold heuristics).

Button

[ text ] — square brackets with spaces around the label:

[ Login ]
[ Sign Up ]
[  ]          <- empty button (2+ spaces)
  • The label is stored as text; an id is auto-generated by slugifying the text ([ Sign Up ]sign-up).
  • Patterns that also match Select/Input/Checkbox are not buttons (see Parsing Priority).

Link

< text > — angle brackets:

< Forgot password >
< Back to home >

The label is stored as text with an auto-slugged id.

Input

[__placeholder__] — double underscores inside brackets:

[__email__]
[__password______________]
[____________]                <- empty placeholder
[__my__var__]                 <- placeholder: "my__var" (first `__` to last `__`)

A pattern that opens with [__ but never closes with __] raises UnclosedInput (recovery is row-bounded — it never crosses lines).

Select

[v: placeholder] — dropdown:

[v: Choose a country]

The text after v: becomes placeholder; the id is auto-slugged from it.

Checkbox

A 3-character bracket marker followed by a label:

[x] Remember me     <- checked ([x], [X], [v], [V])
[ ] Subscribe       <- unchecked (exactly one space)

A checkbox without a label emits MissingCheckboxLabel.

Radio

(*) / ( ) followed by a label:

(*) Credit card
( ) Bank transfer
( ) Crypto

Radios are auto-grouped when they are:

  • vertically consecutive (within radioMaxBlankRows, default 0 blank rows, and radioVerticalColumnTolerance, default ±1 col), or
  • side by side on the same row (within radioHorizontalGap, default 6 cols), or
  • inside the same container.

A radio without a label emits MissingRadioLabel. Multiple selected radios in one group emit MultipleRadiosSelected.

Divider

Horizontal rule made of dashes or equals signs (3+ recommended, dividerMinRun default 3):

---                  <- normal divider
===                  <- bold divider
--- Section ---      <- labeled divider
=== Payment ===      <- labeled bold divider
---#section-1---     <- divider with ID
-#top-               <- short form with ID

A divider mixing a label and an ID (--- text #id ---) emits MixedDividerLabelId and is treated as text.


ID System

Containers can be given an explicit ID for later reference:

Format 1 — ID in the top border:

+--#login-form--+
|  [ Submit ]   |
+---------------+

Short forms +#id+ and +-#id-+ are also accepted. Hyphenated IDs (#login-form) are supported; a trailing border dash is never consumed into the ID.

Format 2 — standalone ID line:

+---------------+
| #login-form   |
|  [ Submit ]   |
+---------------+

Rules:

  • A Format 2 line must contain only the ID — | #id text | raises InvalidIdFormat.
  • If both formats appear, Format 1 wins and the Format 2 line is treated as plain text.
  • Two or more #id patterns in one container raise MultipleIdDeclarations.
  • Duplicate container IDs across the block emit DuplicateContainerId.

String Literals

"text" — double-quoted strings disable inner syntax parsing:

"Click [ here ] for details"     <- [ here ] is plain text, not a button
"He said \"hello\""              <- escaped quote
"C:\\path\\to\\file"             <- escaped backslash
"Costs \$5"                      <- escaped dollar (prevents prop interpolation)
"Hello ${name}"                  <- prop interpolation (inside @component)
  • Strings may span multiple lines; the line break is preserved (multiline: true).
  • A " without a closing quote raises UnclosedString.

Emoji Shortcodes

:name: converts to a Unicode emoji. 14 built-in shortcodes:

Shortcode Emoji Shortcode Emoji
:check: :home: 🏠
:cross: :mail:
:warning: :bell: 🔔
:info: :lock: 🔒
:heart: :bow: 🙇
:star: :search: 🔍
:user: 👤 :settings:
  • Unknown shortcodes emit UnknownEmoji and render as plain text.
  • Custom shortcodes can be supplied per parse via parseOptions.emojiRegistry.

Prop Placeholders

${prop} inserts a component prop value. Valid only inside @component blocks:

@component: greeting
@props: name, title?

+---------------------------+
|  "Hello, ${name}!"        |
|  ${title?}                |      <- optional prop
|  ${role:Member}           |      <- default value "Member"
+---------------------------+
Form Meaning
${prop} Required prop
${prop?} Optional prop (required: false)
${prop:default} Optional with default value (colons allowed in the default)
  • ${prop} inside a @scene block emits PropOutsideComponent and is preserved as literal text.
  • A placeholder referencing a name not in @props emits UnknownPropReference.

Implicit Layout

No layout keywords exist — arrangement is inferred from position:

  • Elements whose first non-whitespace token starts on the same visual row form a Row.
  • Elements on different rows stack as a Column.
  • A multiline container's start row is its top-border (+--) row.
  • Spacing between elements never affects direction — it only informs the optional distribution field.
+--------------------------------+
|  [ Cancel ]      [ Confirm ]   |   <- Row
|                                |
|  "Terms apply"                 |   <- Column item below the row
+--------------------------------+

Layout output (layoutInfo) addresses children by index range into parent.children — child nodes are never duplicated.


Parsing Priority

At each source position, element parsers are probed in descending priority; the first match wins:

Priority Element Pattern
115 String "..." (incl. multiline)
110 Container ID +--#id--+, | #id |
105 PropPlaceholder ${...}
100 Emoji :name:
95 Select [v: ...]
90 Input [__...__]
85 Radio (*), ( )
80 Checkbox [x], [ ] (3 chars)
70 Button [ ... ]
60 Link < ... >
50 Divider labeled bold === text ===
48 Divider labeled --- text ---
45 Divider ID -#id-
40 Divider ---, ===
10 Container +--+
1 Text fallback

The bracket family ([ ]) disambiguates as Select > Input > Checkbox > Button. Input that is one token away from a known pattern triggers a near-miss warning (LooksLikeButton, LooksLikeInput, LooksLikeCheckbox, LooksLikeRadio).


Unicode and Whitespace

  • Input is UTF-8; LF, CRLF, and standalone CR line endings are supported.
  • Columns are visual columns: East Asian Wide characters and emoji grapheme clusters count as 2, combining marks as 0. ZWJ sequences and surrogate pairs are never split.
  • Tabs expand per parseOptions.tabSize (default 4).
  • Bidi/RTL text is out of scope for v2.3 (treated as LTR for column counting).
  • All positions are 0-based row / col / offset (converted to 1-based only for human display).

Errors and Warnings

Errors

Code Message
InvalidIdFormat Invalid ID format - ID line must contain only #id
MultipleIdDeclarations Multiple ID declarations in container
UnclosedInput Unclosed Input boundary - missing '__]'
UnclosedString Unclosed string literal - missing '"'
UnclosedContainer Unclosed container - missing bottom border
MissingBlockDeclaration Missing block declaration - add @scene: or @component:
NestedBlockDeclaration @scene/@component cannot be nested inside another block
MaxDepthExceeded Container nesting exceeded maxDepth

Every error carries code, message, a location (start/end positions), and a recoverable flag. In non-strict mode the parser recovers and keeps parsing; strict: true promotes all errors to fatal and halts at the first one.

Warnings

Spec-mandated:

Code Trigger
PropOutsideComponent ${prop} in a @scene block
UnknownEmoji(name) Unregistered :shortcode:
MixedDividerLabelId --- text #id ---
MissingCheckboxLabel [x] with no label
MissingRadioLabel (*) with no label

Heuristic-driven (each carries a ruleId for traceability):

Code ruleId
MisalignedContainerCorner container.cornerAlignment
MisalignedContainerWall container.wallAlignment
InconsistentContainerWidth container.widthConsistency
RadioGroupAmbiguous radioGrouping.container
LooksLikeButton / LooksLikeInput / LooksLikeCheckbox / LooksLikeRadio nearMissPatterns

Cross-cutting (Validator):

Code Trigger
DuplicatePropName(name) Same prop declared twice (last wins)
DuplicateContainerId(id) Same container id used twice
UnknownPropReference(name) ${name} not in @props
MultipleRadiosSelected(group) More than one (*) in a group

Heuristic Defaults

All tolerances are overridable via parseOptions.heuristics:

Heuristic Default Purpose
containerColumnTolerance 1 ± cols for wall/corner alignment
containerWidthTolerance 2 ± cols for top/bottom border width match
radioHorizontalGap 6 Max col gap for same-row radio grouping
radioVerticalColumnTolerance 1 ± cols for vertical radio grouping
radioMaxBlankRows 0 Blank rows allowed inside a vertical radio group
centerSymmetryThreshold 0.15 Symmetry ratio for center-align detection
rightAlignThreshold 0.10 Right-margin ratio for right-align detection
dividerMinRun 3 Minimum dash/equals run for a divider
nearMissTokenDistance 1 Token edit distance for near-miss warnings

Differences from V1 Syntax

Concept V1 (legacy) v2.3
Input field #id [__placeholder__]
Link "text" < text >
String literal "text"
Emphasis/title 'text' — (use Text/String)
Select [v: placeholder]
Radio (*) / ( )
Component + props @component: / @props: / ${prop}
Emoji shortcodes :name:
Container ID +--#id--+ / | #id |

V1 syntax is still consumed by the V1 parser and renderer (wyreframe main export). See api.md.


Spec version: 2.3.0 Parser: wyreframe/parser/v2 (implementation: "rescript-v2")