1use crate::diag_catalog::{DIAGNOSTIC_CODES, DIAGNOSTIC_VERBS, DiagnosticCodeInfo};
18use crate::parse::transform::PAGE_KNOWN_PROPS;
19use crate::parse::transform::{ASSET_KNOWN_PROPS, DOCUMENT_KNOWN_PROPS, known_props_for_kind};
20
21pub fn node_kinds() -> &'static [&'static str] {
28 &[
32 "code",
33 "connector",
34 "ellipse",
35 "field",
36 "footnote",
37 "frame",
38 "group",
39 "image",
40 "chart",
41 "instance",
42 "line",
43 "light",
44 "mesh",
45 "pattern",
46 "polygon",
47 "polyline",
48 "rect",
49 "shape",
50 "table",
51 "text",
52 "toc",
53 ]
54}
55
56pub fn node_summary(kind: &str) -> Option<&'static str> {
64 match kind {
65 "rect" => Some("Rectangle with optional fill, stroke, and rounded corners."),
66 "ellipse" => Some("Ellipse or circle with optional fill and stroke."),
67 "line" => Some("Straight line segment between two endpoints."),
68 "text" => Some("Multi-span text block with typography and layout properties."),
69 "code" => Some("Monospace code block with syntax-theme highlighting."),
70 "frame" => Some("Container that clips and positions its children within a fixed box."),
71 "group" => Some("Transparent grouping container for related nodes."),
72 "image" => Some("Raster or SVG image positioned within a bounding box."),
73 "polygon" => Some("Closed polygon defined by an ordered vertex list."),
74 "polyline" => Some("Open polyline defined by an ordered vertex list."),
75 "instance" => Some("Reference to a master component, optionally with overrides."),
76 "field" => Some("Editable variable-data text field bound to a named slot."),
77 "footnote" => Some("Page-level footnote referenced by text span markers."),
78 "toc" => Some("Table-of-contents placeholder resolved to text by the scene compiler."),
79 "table" => Some("Structured data table with columns, rows, and cells."),
80 "shape" => Some("Preset geometric shape with an optional text label."),
81 "connector" => Some("Directed connector line between two anchor points on nodes."),
82 "pattern" => Some("Procedural grid or scatter tiling of one motif node."),
83 "chart" => Some(
84 "Data-visualization chart (bar, line, area, sparkline, pie, donut) with inline series data.",
85 ),
86 "light" => Some("Effect node that emits a soft radial ambient light."),
87 "mesh" => {
88 Some("Effect node that emits a procedural orthographic or perspective grid mesh.")
89 }
90 _ => None,
91 }
92}
93
94pub fn node_example(kind: &str) -> Option<&'static str> {
99 match kind {
100 "light" => Some(
101 "light id=\"bg.glow\" kind=\"ambient\" x=(%)85 y=(%)12 \
102 radius=(token)\"size.glow\" color=(token)\"color.glow\" opacity=0.35",
103 ),
104 "mesh" => Some(
105 "mesh id=\"bg.mesh\" kind=\"perspective\" x=(px)0 y=(px)0 w=(px)1920 h=(px)1080 \
106 rows=7 columns=8 vanishing-x=(px)1260 vanishing-y=(px)-420 extend=(px)160 \
107 stroke=(token)\"color.grid\" stroke-width=(token)\"stroke.hairline\" opacity=0.34",
108 ),
109 _ => None,
110 }
111}
112
113pub struct NodeContentDescriptor {
119 pub description: &'static str,
121 pub example: &'static str,
124}
125
126pub fn node_content(kind: &str) -> Option<NodeContentDescriptor> {
133 match kind {
134 "text" => Some(NodeContentDescriptor {
136 description: "One or more `span` children carry the text runs. \
137 Each span takes a string argument and optional inline style props: \
138 fill, font-weight, italic, underline, strikethrough, highlight, code, link, \
139 vertical-align, footnote-ref. \
140 `highlight` is a per-span background color (token ref or raw color string) \
141 rendered behind the glyph run like a marker-pen highlight. \
142 `code=#true` renders the span in the bundled monospace family with a subtle \
143 background, suitable for inline code. \
144 `link=\"url\"` renders the span underlined in the default link color (unless \
145 `fill` is set); in PDF output the URL becomes a clickable `/Link` annotation \
146 over the span. \
147 `selectable` (node attribute, default `#true`) controls PDF text extraction: \
148 by default the text is emitted as real, selectable / searchable / indexable \
149 text (with a ToUnicode map, so copy and search work and links are clickable); \
150 `selectable=#false` renders the glyphs as filled outlines instead — visually \
151 identical but not selectable, searchable, or extractable. The PNG backend is \
152 unaffected. \
153 The `format` node attribute (values: `markdown` | `plain`) opts into \
154 markdown rendering of the concatenated span text. \
155 When `format=\"markdown\"`, the scene compile pass re-parses the span content \
156 AFTER data-binding substitution and renders both inline marks and block structure. \
157 Inline marks: `**bold**`, `*italic*`, `~~strike~~`, `==highlight==`, \
158 `++underline++`, `` `code` ``, `[label](url)`. \
159 Block structure (one construct per line/paragraph): \
160 `# H1` through `###### H6` (ATX headings), blank line separates paragraphs, \
161 `> text` blockquote, `- item` / `* item` / `+ item` unordered list, \
162 `1. item` ordered list, ` ``` ` fenced code block (optional lang after opening \
163 fence; ends at closing ` ``` `), `---` / `***` / `___` horizontal rule. \
164 The block roles produced (h1..h6, p, blockquote, li, code-block, hr) are the \
165 same names styled by `block role=\"…\"` declarations (see `zenith schema block`). \
166 v1 limitation: in a `chain` flow, code-block backgrounds and `---` rules are \
167 not drawn and blockquote/list indent is not applied — these render fully only \
168 in a single non-chained text box. \
169 Pairs well with a single `data-ref` span to parse external content as markdown \
170 without encoding marks in the document. `format=\"plain\"` or absent = literal \
171 (byte-identical to today's behavior). \
172 The `src` node attribute (`src=\"path/to/file.md\"`) loads the file at the \
173 given path (resolved relative to the document's project directory) and uses its \
174 UTF-8 contents as the node's text content, replacing any inline `span` children \
175 at render time. This keeps the `.zen` file lean for long-form prose. When paired \
176 with `format=\"markdown\"`, the loaded text is parsed as markdown by the \
177 scene compile pass. A missing or unreadable file emits a `text.src_missing` \
178 Error diagnostic (same gate as `asset.missing`). The `src` field is retained \
179 on the node so a future editor can write edits back to the original file. \
180 Threaded text flow (`chain` attribute): all `text` nodes that share the same \
181 `chain=\"id\"` value form one ordered chain (document source order, across pages). \
182 The FIRST member that carries spans or `src` content is the content source; \
183 subsequent members must have EMPTY spans (no `src`, no inline spans) and serve \
184 as overflow boxes. Each member needs explicit `x`/`y`/`w`/`h` geometry. Text \
185 fills box 1, the remainder flows into box 2, etc., across page boundaries. \
186 This is how you resolve a `text.overflow` warning for long-form copy: add \
187 chained continuation boxes (on the same or new pages) until nothing overflows. \
188 Only the first member's font/style drives the whole chain; per-span overrides \
189 on the source are honored. \
190 A `block role=\"…\"` declaration may appear BEFORE span children to set per-role \
191 markdown block style at this text node's scope (highest cascade precedence: \
192 text > page > document). Block decls affect only nodes with `format=\"markdown\"` \
193 and have no effect on plain-text nodes (see `zenith schema block`).",
194 example: concat!(
195 "block role=\"h1\" font-size=(token)\"size.h1\" font-weight=(token)\"weight.bold\"\n",
196 "span \"Hello \"\n",
197 "span \"world\" font-weight=(token)\"weight.bold\" italic=#true",
198 ),
199 }),
200 "shape" => Some(NodeContentDescriptor {
201 description: "Optional `span` children form a text label rendered centered inside the \
202 shape. Use h-align/v-align on the shape node to adjust alignment. \
203 Omit the block entirely for an unlabelled shape.",
204 example: "span \"Approve\"",
205 }),
206 "footnote" => Some(NodeContentDescriptor {
207 description: "One or more `span` children carry the footnote body text, \
208 using the same span model as `text`.",
209 example: "span \"See also Chapter 3.\"",
210 }),
211
212 "polygon" => Some(NodeContentDescriptor {
214 description: "Two or more `point` children define the closed vertex list in order. \
215 Each point carries `x` and `y` as px-literal dimensions.",
216 example: concat!(
217 "point x=(px)0 y=(px)0\n",
218 "point x=(px)100 y=(px)0\n",
219 "point x=(px)50 y=(px)86",
220 ),
221 }),
222 "polyline" => Some(NodeContentDescriptor {
223 description: "Two or more `point` children define the open vertex list in order. \
224 Each point carries `x` and `y` as px-literal dimensions.",
225 example: concat!(
226 "point x=(px)0 y=(px)0\n",
227 "point x=(px)100 y=(px)50\n",
228 "point x=(px)200 y=(px)0",
229 ),
230 }),
231
232 "table" => Some(NodeContentDescriptor {
234 description: "Optional `column` children (each with `width=(px)N`) declare column \
235 widths; then `row` children each containing `cell` children. \
236 Each cell accepts colspan, rowspan, fill, border, h-align, v-align, \
237 and arbitrary renderable child nodes for cell content. \
238 Cell text auto-places: the cell sizes and positions its text into the content box \
239 (padding-inset), wraps to the column width, and aligns via the cell/table \
240 `h-align` (start|center|end) and `v-align` (top|middle|bottom). \
241 Omit `x`/`y`/`w`/`h` on cell text; set them only to override the auto layout. \
242 The table itself requires its own `x`/`y`/`w`/`h`.",
243 example: concat!(
244 "column width=(px)120\n",
245 "column width=(px)80\n",
246 "row {\n",
247 " cell { text { span \"Name\" } }\n",
248 " cell { text { span \"Score\" } }\n",
249 "}",
250 ),
251 }),
252
253 "frame" => Some(NodeContentDescriptor {
255 description: "Arbitrary renderable child nodes (any node kind). \
256 The frame clips its children to its bounding box. \
257 Use layout=\"grid\" with columns/rows attrs for grid layout.",
258 example: "rect id=\"bg\" x=(px)0 y=(px)0 w=(px)400 h=(px)300 fill=(token)\"color.bg\"",
259 }),
260 "group" => Some(NodeContentDescriptor {
261 description: "Arbitrary renderable child nodes (any node kind). \
262 May also include `protected-region id=... x=... y=... w=... h=...` \
263 and `editable-param id=...` metadata children.",
264 example: "rect id=\"box\" x=(px)0 y=(px)0 w=(px)100 h=(px)100",
265 }),
266
267 "chart" => Some(NodeContentDescriptor {
269 description: "Optional `categories` child carries the X-axis category labels as \
270 positional string arguments (one per slot; absent = derive index labels at render). \
271 Optional `label-colors` child carries per-slice value-label colors as positional \
272 PropertyValue arguments (e.g. `(token)\"color.x\"`; one per category in order; \
273 absent = use the chart value-color or the white on-fill default). \
274 Optional `slice-colors` child carries per-slice FILL colors for pie/donut as \
275 positional PropertyValue arguments (e.g. `(token)\"color.x\"`; one per category \
276 in order; absent = use the palette). \
277 Zero or more `series` children carry the numeric data. \
278 Each series node takes its f64 data values as positional arguments \
279 and optional named props: label, color (token ref), label-color (token ref), data-ref. \
280 A `data-ref=\"field\"` binds the whole series to a numeric ARRAY field supplied at \
281 render via `--data` — JSON `{\"field\":[120,185,143]}` or a CSV column named `field` \
282 (one number per category). This is render-time binding, distinct from `zenith merge`, \
283 which substitutes per-row scalar text/image via `role=\"data.<column>\"` and does not \
284 vary chart series per row. \
285 Emit `categories` then `label-colors` then `slice-colors` before any `series` children.",
286 example: concat!(
287 "categories \"Q1\" \"Q2\" \"Q3\" \"Q4\"\n",
288 "label-colors (token)\"color.c1\" (token)\"color.c2\" (token)\"color.c3\" (token)\"color.c4\"\n",
289 "slice-colors (token)\"color.s1\" (token)\"color.s2\" (token)\"color.s3\" (token)\"color.s4\"\n",
290 "series label=\"Revenue\" color=(token)\"color.primary\" label-color=(token)\"color.lbl\" 120.0 200.0 150.0 310.0\n",
291 "series label=\"Costs\" color=(token)\"color.secondary\" 80.0 90.0 100.0 120.0",
292 ),
293 }),
294 "light" | "mesh" => None,
295
296 "pattern" => Some(NodeContentDescriptor {
298 description: "Exactly one required child node — the motif — which is the template \
299 node that gets tiled. Any authorable node kind is valid as the motif.",
300 example: "rect id=\"dot\" x=(px)0 y=(px)0 w=(px)8 h=(px)8 fill=(token)\"color.accent\"",
301 }),
302
303 "instance" => Some(NodeContentDescriptor {
305 description: "Zero or more `override` children apply per-node property overrides \
306 to descendants of the referenced component. Each override targets a node by \
307 `ref=\"id\"` and accepts fill, visible, and optional `span` children to \
308 replace text content.",
309 example: concat!(
310 "override ref=\"headline\" fill=(token)\"color.alt\" {\n",
311 " span \"New headline text\"\n",
312 "}",
313 ),
314 }),
315
316 "code" => Some(NodeContentDescriptor {
318 description: "A single `content` child carries the verbatim source string as its \
319 first positional argument. Newlines and tabs are expressed as \\n and \\t \
320 escape sequences in the string literal.",
321 example: "content \"fn main() {\\n println!(\\\"hello\\\");\\n}\"",
322 }),
323
324 "connector" => Some(NodeContentDescriptor {
326 description: "Optional `span` children form a text label rendered at the \
327 connector's geometric midpoint (the mid-point of the routed polyline). \
328 Use `text-style` on the connector node to apply a style ref to the label. \
329 Omit the block entirely (or author no `span` children) for an unlabelled \
330 connector — the render output is byte-identical when no spans are present.",
331 example: "span \"Yes\"",
332 }),
333
334 "rect" | "ellipse" | "line" | "image" | "field" | "toc" => None,
336
337 _ => None,
339 }
340}
341
342pub fn node_attributes(kind: &str) -> Vec<&'static str> {
354 dedupe_to_kebab(known_props_for_kind(kind))
359}
360
361pub fn page_summary() -> &'static str {
365 "Page declaration — geometry (w/h), margins, bleed, baseline grid, and workflow metadata."
366}
367
368pub fn asset_summary() -> &'static str {
370 "Asset declaration (image/svg/font) — provenance including sha256 and AI-generation fields."
371}
372
373pub fn document_summary() -> &'static str {
375 "Document root — colorspace, pagination, spread gutter, and document-level default margins."
376}
377
378pub fn page_attributes() -> Vec<&'static str> {
386 dedupe_to_kebab(PAGE_KNOWN_PROPS)
387}
388
389pub fn asset_attributes() -> Vec<&'static str> {
394 dedupe_to_kebab(ASSET_KNOWN_PROPS)
395}
396
397pub fn document_attributes() -> Vec<&'static str> {
403 dedupe_to_kebab(DOCUMENT_KNOWN_PROPS)
404}
405
406pub fn attribute_type_for_kind(kind: &str, name: &str) -> &'static str {
434 attribute_type_for_kind_inner(kind, name, "string")
435}
436
437pub fn attribute_type(name: &str) -> &'static str {
447 attribute_type_for_kind_inner("", name, "string")
452}
453
454fn attribute_type_for_kind_inner(kind: &str, name: &str, fallback: &'static str) -> &'static str {
460 match (kind, name) {
469 ("rect" | "ellipse" | "polygon" | "polyline" | "pattern" | "chart", "fill") => {
473 "token ref: color/gradient"
474 }
475 ("text" | "shape" | "code" | "table", "fill") => "token ref: color",
478 (_, "stroke") => "token ref: color",
484 (_, "shadow") => "token ref: shadow",
489 (_, "filter") => "token ref: filter",
490 (_, "mask") => "token ref: mask",
491 (_, "background") => "token ref: color/gradient",
494 (_, "border-top" | "border-bottom" | "border-left" | "border-right" | "stroke-outer") => {
496 "token ref: color"
497 }
498 (_, "border") => "token ref: color",
500 (_, "contrast-bg" | "header-fill") => "token ref: color",
503 ("shape", "kind") => "enum: process|decision|terminator|ellipse",
509 ("pattern", "kind") => "enum: grid|scatter",
510 ("chart", "kind") => "enum: bar|line|area|sparkline|pie|donut",
511 ("light", "kind") => "enum: ambient|glow|key|rim",
512 ("light", "color") => "token ref: color",
513 ("light", "angle") => "dimension: deg",
514 ("mesh", "kind") => "enum: orthographic|perspective",
515 ("mesh", "stroke-width") | ("mesh", "stroke-dash") | ("mesh", "stroke-gap") => {
516 "dimension literal or token ref: dimension"
517 }
518 ("mesh", "x")
519 | ("mesh", "y")
520 | ("mesh", "w")
521 | ("mesh", "h")
522 | ("mesh", "vanishing-x")
523 | ("mesh", "vanishing-y")
524 | ("mesh", "extend") => "dimension literal or token ref: dimension",
525 ("mesh", "rows") | ("mesh", "columns") => "u32 (>0)",
526 ("mesh", "stroke-linecap") => "enum: butt|round|square",
527 ("chart", "legend") => "bool",
529 ("chart", "caption") => "string",
530 ("chart", "axis-min" | "axis-max") => "f64",
531 ("chart", "axis-style") => "string",
532 ("chart", "legend-position") => "enum: right|left|top|bottom",
533 ("chart", "legend-layout") => "enum: wrapped|list",
534 ("chart", "legend-align") => "enum: center|left|right",
535 ("chart", "bar-mode") => "enum: grouped|stacked",
536 ("chart", "orientation") => "enum: vertical|horizontal",
537 ("chart", "point-placement") => "enum: edge|center",
538 ("chart", "value-labels") => "enum: auto|none|top|center",
539 ("chart", "value-color") => "token ref: color",
540 ("chart", "label-color") => "token ref: color",
542 ("", "kind") => "enum: image|svg|font",
545 ("connector", "route") => "enum: straight|orthogonal|avoid",
548 ("frame", "layout") => "enum: absolute|flow|grid",
552 _ => attribute_type_generic(name, fallback),
554 }
555}
556
557fn attribute_type_generic(name: &str, fallback: &'static str) -> &'static str {
562 match name {
563 "id" => "string",
565 "name" => "string",
566 "role" => "string",
567 "style" => "string",
568
569 "x" | "y" | "w" | "h" => "px literal or token ref: dimension",
571 "x1" | "y1" | "x2" | "y2" => "px literal",
572 "rx" | "ry" => "px literal",
573 "rotate" => "px literal",
574 "spacing" => "px literal",
575 "padding-left" | "text-indent" => "px literal",
576 "bullet-gap" => "px literal",
577 "anchor-gap" => "px literal",
578 "blur" => "px literal",
579 "bleed" => "px literal",
580 "spread-gutter" => "px literal",
581 "margin-inner" | "margin-outer" | "margin-top" | "margin-bottom" => "px literal",
582
583 "radius" | "radius-tl" | "radius-tr" | "radius-br" | "radius-bl" => "token ref: dimension",
585 "stroke-width" | "stroke-dash" | "stroke-gap" | "stroke-outer-width" => {
586 "token ref: dimension"
587 }
588 "border-width" => "token ref: dimension",
589 "font-size" | "font-size-min" => "token ref: dimension",
590 "baseline-grid" => "token ref: dimension",
591 "gap" | "cell-padding" | "padding" => "token ref: dimension",
592 "src-x" | "src-y" | "src-w" | "src-h" => "px literal",
594 "object-position-x" | "object-position-y" => "f64 (0.0–1.0)",
596 "clip-radius" => "token ref: dimension",
598
599 "font-family" => "token ref: fontFamily",
601 "font-weight" => "token ref: fontWeight",
602
603 "opacity" | "jitter" | "intensity" => "f64 (0.0–1.0)",
605
606 "seed" | "count" => "i64",
608 "drop-cap-lines" | "widow-orphan" | "tab-width" | "line-numbers" => "i64",
609 "colspan" | "rowspan" | "header-rows" | "columns" | "rows" => "i64",
610 "layer-priority" => "i64",
611
612 "visible" | "locked" | "anchor-parent" | "selectable" => "bool",
614 "hyphenate" | "suppress-first" | "border-collapse" => "bool",
615 "mirror-margins" | "facing-pages" => "bool",
616 "line-jumps" => "bool",
617
618 "anchor" => {
620 "enum: top-left|top-center|top-right|center-left|center|center-right|bottom-left|bottom-center|bottom-right"
621 }
622 "anchor-edge" => "enum: above|below|before|after",
623 "align" => "enum: left|center|right|justify",
624 "overflow" => "enum: clip|visible|scroll",
625 "blend-mode" => "string (enum)",
626 "stroke-alignment" => "enum: inside|center|outside",
627 "stroke-linecap" => "enum: butt|round|square",
628 "fill-rule" => "enum: nonzero|evenodd",
629 "fit" => "enum: fill|contain|cover|none",
630 "clip" => "bool",
631 "parity" => "enum: left|right",
632 "page-parity-start" => "enum: left|right",
633 "page-progression" => "enum: ltr|rtl",
634 "colorspace" => "enum: srgb|display-p3|rec2020",
635 "direction" => "enum: ltr|rtl",
636 "overflow-wrap" => "enum: normal|break-word",
637 "h-align" => "enum: left|center|right",
638 "v-align" => "enum: top|middle|bottom",
639
640 "from" | "to" => "node id",
642 "from-anchor" | "to-anchor" => "string",
643 "marker-start" | "marker-end" => "string",
644
645 "chain" => "string (chain id)",
651 "tab-leader" | "text-exclusion" | "bullet" => "string",
652 "language" => "string",
653 "syntax-theme" => "string",
654
655 "data-ref" => "string",
657 "format" => "enum: currency|percent|number",
658 "precision" => "i64",
659 "locale" => "string",
660
661 "type" => "string",
663 "recto" | "verso" | "target" => "string",
664 "folio-style" | "header-style" | "text-style" => "string",
665 "marker" => "string",
666 "match-role" | "match-style" | "leader" => "string",
667 "flows" => "string",
668
669 "asset" => "asset id",
671
672 "component" => "string",
674
675 "semantic-role" => "string",
677
678 "master" => "string",
680
681 "version" => "string",
683 "doc-id" => "string",
684 "title" => "string",
685
686 "src" => "string",
688 "sha256" => "string",
689 "ai-prompt" => "string",
690 "ai-model" => "string",
691 "ai-provider" => "string",
692 "ai-seed" => "i64",
693 "ai-generation-date" => "string",
694 "ai-license" => "string",
695 "ai-source-rights" => "string",
696 "ai-safety-status" => "string",
697 "ai-reuse-policy" => "string",
698
699 "anchor-zone" | "anchor-sibling" => "string",
701
702 _ => fallback,
703 }
704}
705
706pub fn token_types() -> &'static [&'static str] {
717 &[
718 "color",
719 "dimension",
720 "filter",
721 "fontFamily",
722 "fontWeight",
723 "gradient",
724 "mask",
725 "number",
726 "shadow",
727 ]
728}
729
730pub fn token_type_summary(ty: &str) -> Option<&'static str> {
738 match ty {
739 "color" => Some("sRGB hex, alpha-hex, or CMYK color constant."),
740 "dimension" => Some("Typed measurement with unit: px, pt, pct, or deg."),
741 "filter" => Some("Ordered stack of image filter ops (grayscale, duotone, noise, …)."),
742 "fontFamily" => Some("Named font-family string used for typography."),
743 "fontWeight" => Some("Integer font weight in 100–900 (e.g. 400 = regular, 700 = bold)."),
744 "gradient" => Some("Linear or radial gradient built from ≥2 color-stop child nodes."),
745 "mask" => Some("Spatial coverage mask: a single rect, ellipse, or rounded-rect shape."),
746 "number" => Some("Unitless finite number (e.g. opacity ratio, scale factor)."),
747 "shadow" => Some("Ordered stack of drop-shadow layers, each referencing a color token."),
748 _ => None,
749 }
750}
751
752pub struct TokenTypeDescriptor {
758 pub type_name: &'static str,
760 pub summary: &'static str,
762 pub value_form: &'static str,
765 pub child_nodes: &'static str,
768 pub example: &'static str,
771}
772
773pub fn token_type_descriptor(ty: &str) -> Option<TokenTypeDescriptor> {
779 match ty {
780 "color" => Some(TokenTypeDescriptor {
781 type_name: "color",
782 summary: token_type_summary("color").unwrap_or(""),
783 value_form: r##"String literal: "#rrggbb" (6-digit lowercase hex), "#rrggbbaa" (8-digit), or "cmyk(c,m,y,k)" with each channel 0–100."##,
784 child_nodes: "",
785 example: r##"token id="color.brand.primary" type="color" value="#1a73e8""##,
786 }),
787 "dimension" => Some(TokenTypeDescriptor {
788 type_name: "dimension",
789 summary: token_type_summary("dimension").unwrap_or(""),
790 value_form: "Dimension literal: (px)N, (pt)N, (pct)N, or (deg)N — annotation then bare number, no space. E.g. (px)16, (pt)12, (pct)100, (deg)45.",
791 child_nodes: "",
792 example: r#"token id="dim.radius.card" type="dimension" value=(px)8"#,
793 }),
794 "filter" => Some(TokenTypeDescriptor {
795 type_name: "filter",
796 summary: token_type_summary("filter").unwrap_or(""),
797 value_form: "No inline value. Defined entirely by op child nodes.",
798 child_nodes: "≥1 op child node. Valid op names: grayscale, invert, sepia, saturate, brightness, contrast, hue-rotate (each accept optional amount=N); duotone (requires shadow=(token)\"id\" highlight=(token)\"id\", optional amount=N); noise (accepts seed=N scale=N, optional amount=N).",
799 example: "token id=\"filter.mono\" type=\"filter\" {\n grayscale amount=1.0\n}",
800 }),
801 "fontFamily" => Some(TokenTypeDescriptor {
802 type_name: "fontFamily",
803 summary: token_type_summary("fontFamily").unwrap_or(""),
804 value_form: r#"Non-empty string literal: the font-family name as it appears in the asset block, e.g. "Inter" or "Source Serif 4"."#,
805 child_nodes: "",
806 example: r#"token id="font.body" type="fontFamily" value="Inter""#,
807 }),
808 "fontWeight" => Some(TokenTypeDescriptor {
809 type_name: "fontWeight",
810 summary: token_type_summary("fontWeight").unwrap_or(""),
811 value_form: "Bare integer (NOT a string, NOT a dimension): an integer in 100–900 with no unit annotation. E.g. 400, 700.",
812 child_nodes: "",
813 example: r#"token id="weight.bold" type="fontWeight" value=700"#,
814 }),
815 "gradient" => Some(TokenTypeDescriptor {
816 type_name: "gradient",
817 summary: token_type_summary("gradient").unwrap_or(""),
818 value_form: "No inline value. Defined entirely by stop child nodes plus optional angle/radial props on the token node itself.",
819 child_nodes: "≥2 stop child nodes. Each stop: stop offset=0.0 color=(token)\"color-token-id\". Optional props on the token node: angle=(deg)N (linear, default 90), radial=#true, center-x=0.5 center-y=0.5 radius=1.0.",
820 example: "token id=\"gradient.brand\" type=\"gradient\" angle=(deg)90 {\n stop offset=0.0 color=(token)\"color.brand.primary\"\n stop offset=1.0 color=(token)\"color.brand.secondary\"\n}",
821 }),
822 "mask" => Some(TokenTypeDescriptor {
823 type_name: "mask",
824 summary: token_type_summary("mask").unwrap_or(""),
825 value_form: "No inline value. Defined by exactly one shape child node.",
826 child_nodes: "Exactly 1 shape child: rect, ellipse, or rounded. Each accepts feather=N (Gaussian sigma px, default 0) and invert=#true/#false. rounded also accepts radius=N (corner radius px).",
827 example: "token id=\"mask.card\" type=\"mask\" {\n rounded radius=8 feather=2\n}",
828 }),
829 "number" => Some(TokenTypeDescriptor {
830 type_name: "number",
831 summary: token_type_summary("number").unwrap_or(""),
832 value_form: "Bare finite number with no unit annotation. E.g. 1.0, 0.5, 1.05. NaN and ±inf are invalid.",
833 child_nodes: "",
834 example: r#"token id="number.line-height" type="number" value=1.4"#,
835 }),
836 "shadow" => Some(TokenTypeDescriptor {
837 type_name: "shadow",
838 summary: token_type_summary("shadow").unwrap_or(""),
839 value_form: "No inline value. Defined entirely by layer child nodes.",
840 child_nodes: "≥1 layer child node. Each layer: layer color=(token)\"color-token-id\" dx=(px)N dy=(px)N blur=(px)N. dx/dy can be negative (offsets); blur is clamped to ≥0.",
841 example: "token id=\"shadow.card\" type=\"shadow\" {\n layer color=(token)\"color.shadow\" dx=(px)0 dy=(px)2 blur=(px)8\n}",
842 }),
843 _ => None,
844 }
845}
846
847pub struct VariantDescriptor {
851 pub summary: &'static str,
853 pub block_structure: &'static str,
855 pub variant_node: &'static str,
857 pub override_entry: &'static str,
859 pub override_props: &'static [(&'static str, &'static str, bool)],
861 pub example: &'static str,
863}
864
865pub fn variant_descriptor() -> VariantDescriptor {
871 VariantDescriptor {
872 summary: "Variant system — named page-level derivatives with per-node property overrides.",
873 block_structure: "A `variants { … }` block sits at the document root, as a sibling of \
874 `document` (canonical order: after `provenance`, before `document`) — NOT inside a \
875 page. It contains one or more `variant` entries, each with its own child block of \
876 `override` entries that apply to that variant.",
877 variant_node: "variant id=<id> source=<page-id> w=(px)N h=(px)N { … }\n\
878 \n\
879 • id — unique identifier for this variant (string, required)\n\
880 • source — the page id to base this variant on (page id string, required)\n\
881 • w — override canvas width in pixels, e.g. (px)1920 (dimension, required)\n\
882 • h — override canvas height in pixels, e.g. (px)1080 (dimension, required)\n\
883 \n\
884 The child block of `variant { … }` contains `override` entries (see below).",
885 override_entry: "override node=\"<id>\" …\n\
886 \n\
887 Targets the node whose id equals the `node=` value, and applies one or more \
888 property overrides. The `node` key is the only required field; all visual/geometry \
889 keys are optional and independent (omitted keys retain the source page value).\n\
890 \n\
891 IMPORTANT: the selector key is `node` (the target node's id string), NOT `id`.\n\
892 Wrong: override id=\"hero\" visible=#false\n\
893 Correct: override node=\"hero\" visible=#false",
894 override_props: &[
895 ("node", "string — target node id selector (required)", true),
896 ("visible", "#true or #false", false),
897 ("text", "string — replacement text content", false),
898 ("fill", "token ref or color string", false),
899 ("x", "typed dimension, e.g. (px)100", false),
900 ("y", "typed dimension, e.g. (px)50", false),
901 ("w", "typed dimension, e.g. (px)800", false),
902 ("h", "typed dimension, e.g. (px)600", false),
903 ],
904 example: concat!(
905 "variants {\n",
906 " variant id=\"mobile\" source=\"page.main\" w=(px)390 h=(px)844 {\n",
907 " // hide the desktop-only sidebar\n",
908 " override node=\"sidebar\" visible=#false\n",
909 " // shrink the hero to fit the narrower canvas\n",
910 " override node=\"hero\" x=(px)0 y=(px)0 w=(px)390 h=(px)260\n",
911 " // swap the headline copy\n",
912 " override node=\"headline\" text=\"Mobile headline\"\n",
913 " }\n",
914 "}",
915 ),
916 }
917}
918
919pub fn diagnostics_summary() -> &'static str {
924 "In-file diagnostic policy — allow/deny/warn specific diagnostic codes \
925 (integrity Errors cannot be suppressed)."
926}
927
928pub fn diagnostics_verbs() -> &'static [&'static str] {
933 DIAGNOSTIC_VERBS
934}
935
936pub fn diagnostic_codes() -> &'static [DiagnosticCodeInfo] {
943 DIAGNOSTIC_CODES
944}
945
946fn dedupe_to_kebab(raw: &'static [&'static str]) -> Vec<&'static str> {
956 let mut out: Vec<&'static str> = raw
957 .iter()
958 .map(|&name| {
959 let kebab = name.replace('_', "-");
960 raw.iter().copied().find(|n| *n == kebab).unwrap_or(name)
961 })
962 .collect();
963 out.sort_unstable();
964 out.dedup();
965 out
966}
967
968#[cfg(test)]
971mod tests {
972 use super::*;
973 use crate::ast::Node;
974 use crate::ast::token::TokenType;
975 use crate::parse::KdlSource;
976 use crate::parse::kdl_adapter::KdlAdapter;
977
978 fn node_variant_count_exhaustive(node: &Node) -> usize {
991 match node {
992 Node::Rect(_) => 1,
993 Node::Ellipse(_) => 1,
994 Node::Line(_) => 1,
995 Node::Text(_) => 1,
996 Node::Code(_) => 1,
997 Node::Frame(_) => 1,
998 Node::Group(_) => 1,
999 Node::Image(_) => 1,
1000 Node::Polygon(_) => 1,
1001 Node::Polyline(_) => 1,
1002 Node::Instance(_) => 1,
1003 Node::Field(_) => 1,
1004 Node::Footnote(_) => 1,
1005 Node::Toc(_) => 1,
1006 Node::Table(_) => 1,
1007 Node::Shape(_) => 1,
1008 Node::Connector(_) => 1,
1009 Node::Pattern(_) => 1,
1010 Node::Chart(_) => 1,
1011 Node::Light(_) => 1,
1012 Node::Mesh(_) => 1,
1013 Node::Unknown(_) => 1,
1017 }
1018 }
1019
1020 const TOTAL_NODE_VARIANTS: usize = 22; #[test]
1028 fn node_summary_covers_every_node_kind() {
1029 let expected_authorable = TOTAL_NODE_VARIANTS - 1; assert_eq!(
1033 node_kinds().len(),
1034 expected_authorable,
1035 "node_kinds() has {} entries but the exhaustive Node match covers {} authorable \
1036 variants (plus Unknown). Update node_kinds() and node_summary() when adding a variant.",
1037 node_kinds().len(),
1038 expected_authorable,
1039 );
1040
1041 let _guard: fn(&Node) -> usize = node_variant_count_exhaustive;
1045
1046 for kind in node_kinds() {
1048 assert!(
1049 node_summary(kind).is_some(),
1050 "node_summary(\"{kind}\") returned None — add a one-liner to node_summary()",
1051 );
1052 }
1053 }
1054
1055 #[test]
1064 fn node_content_returns_some_for_content_bearing_kinds() {
1065 let content_kinds = &[
1066 "text",
1067 "shape",
1068 "footnote",
1069 "polygon",
1070 "polyline",
1071 "table",
1072 "frame",
1073 "group",
1074 "pattern",
1075 "chart",
1076 "instance",
1077 "code",
1078 "connector",
1079 ];
1080 for &kind in content_kinds {
1081 let desc = node_content(kind);
1082 assert!(
1083 desc.is_some(),
1084 "node_content(\"{kind}\") returned None — expected Some for a content-bearing kind",
1085 );
1086 let d = desc.unwrap();
1087 assert!(
1088 !d.description.is_empty(),
1089 "node_content(\"{kind}\").description is empty",
1090 );
1091 assert!(
1092 !d.example.is_empty(),
1093 "node_content(\"{kind}\").example is empty",
1094 );
1095 }
1096 }
1097
1098 #[test]
1100 fn node_content_returns_none_for_no_content_kinds() {
1101 let no_content_kinds = &["rect", "ellipse", "line", "image", "field", "toc"];
1102 for &kind in no_content_kinds {
1103 assert!(
1104 node_content(kind).is_none(),
1105 "node_content(\"{kind}\") returned Some — expected None for a leaf-only kind",
1106 );
1107 }
1108 }
1109
1110 #[test]
1111 fn node_attributes_nonempty_for_geometry_kinds() {
1112 let rect_attrs = node_attributes("rect");
1114 assert!(!rect_attrs.is_empty(), "rect attributes must not be empty");
1115 assert!(
1116 rect_attrs.contains(&"fill"),
1117 "rect attributes must contain \"fill\"; got: {:?}",
1118 rect_attrs
1119 );
1120 assert!(
1121 rect_attrs.contains(&"x"),
1122 "rect attributes must contain \"x\"; got: {:?}",
1123 rect_attrs
1124 );
1125 assert!(
1126 rect_attrs.contains(&"w"),
1127 "rect attributes must contain \"w\"; got: {:?}",
1128 rect_attrs
1129 );
1130
1131 let text_attrs = node_attributes("text");
1133 assert!(!text_attrs.is_empty(), "text attributes must not be empty");
1134 assert!(
1135 text_attrs.contains(&"x"),
1136 "text attributes must contain \"x\"; got: {:?}",
1137 text_attrs
1138 );
1139
1140 let pattern_attrs = node_attributes("pattern");
1142 assert!(
1143 !pattern_attrs.is_empty(),
1144 "pattern attributes must not be empty"
1145 );
1146 assert!(
1147 pattern_attrs.contains(&"kind"),
1148 "pattern attributes must contain \"kind\"; got: {:?}",
1149 pattern_attrs
1150 );
1151 assert!(
1152 pattern_attrs.contains(&"spacing"),
1153 "pattern attributes must contain \"spacing\"; got: {:?}",
1154 pattern_attrs
1155 );
1156
1157 let frame_attrs = node_attributes("frame");
1159 assert!(
1160 !frame_attrs.is_empty(),
1161 "frame attributes must not be empty"
1162 );
1163 assert!(
1164 frame_attrs.contains(&"x"),
1165 "frame attributes must contain \"x\"; got: {:?}",
1166 frame_attrs
1167 );
1168 assert!(
1169 frame_attrs.contains(&"w"),
1170 "frame attributes must contain \"w\"; got: {:?}",
1171 frame_attrs
1172 );
1173 }
1174
1175 #[test]
1176 fn node_attributes_empty_for_unknown_kind() {
1177 assert!(
1178 node_attributes("not-a-real-kind").is_empty(),
1179 "unrecognised kinds must return an empty slice"
1180 );
1181 }
1182
1183 #[test]
1189 fn page_attributes_anchor_check() {
1190 let attrs = page_attributes();
1191 assert!(!attrs.is_empty(), "page_attributes() must not be empty");
1192 for anchor in &["w", "h", "line-jumps"] {
1193 assert!(
1194 attrs.contains(anchor),
1195 "page_attributes() must contain \"{anchor}\"; got: {attrs:?}",
1196 );
1197 }
1198 assert!(
1200 !attrs.contains(&"line_jumps"),
1201 "underscore alias \"line_jumps\" must be collapsed; got: {attrs:?}",
1202 );
1203 }
1204
1205 #[test]
1208 fn asset_attributes_anchor_check() {
1209 let attrs = asset_attributes();
1210 assert!(!attrs.is_empty(), "asset_attributes() must not be empty");
1211 for anchor in &["sha256", "ai-prompt", "ai-model", "src", "kind"] {
1212 assert!(
1213 attrs.contains(anchor),
1214 "asset_attributes() must contain \"{anchor}\"; got: {attrs:?}",
1215 );
1216 }
1217 }
1218
1219 #[test]
1222 fn document_attributes_anchor_check() {
1223 let attrs = document_attributes();
1224 assert!(!attrs.is_empty(), "document_attributes() must not be empty");
1225 for anchor in &["title", "colorspace", "doc-id", "spread-gutter"] {
1226 assert!(
1227 attrs.contains(anchor),
1228 "document_attributes() must contain \"{anchor}\"; got: {attrs:?}",
1229 );
1230 }
1231 assert!(
1233 !attrs.contains(&"doc_id"),
1234 "underscore alias \"doc_id\" must be collapsed; got: {attrs:?}",
1235 );
1236 }
1237
1238 #[test]
1256 fn attribute_type_covers_all_known_attrs() {
1257 use std::collections::BTreeMap;
1258 use std::collections::BTreeSet;
1259
1260 let mut attr_to_kind: BTreeMap<&'static str, &'static str> = BTreeMap::new();
1263 for &kind in node_kinds() {
1264 for attr in node_attributes(kind) {
1265 attr_to_kind.entry(attr).or_insert(kind);
1266 }
1267 }
1268
1269 let mut surface_attrs: BTreeSet<&'static str> = BTreeSet::new();
1271 for attr in page_attributes() {
1272 surface_attrs.insert(attr);
1273 }
1274 for attr in asset_attributes() {
1275 surface_attrs.insert(attr);
1276 }
1277 for attr in document_attributes() {
1278 surface_attrs.insert(attr);
1279 }
1280
1281 let mut unmapped: Vec<String> = Vec::new();
1283
1284 for (attr, kind) in &attr_to_kind {
1285 if attribute_type_for_kind_inner(kind, attr, "<unmapped>") == "<unmapped>" {
1286 unmapped.push(format!("{attr} (on {kind})"));
1287 }
1288 }
1289 for attr in &surface_attrs {
1290 if !attr_to_kind.contains_key(attr)
1292 && attribute_type_for_kind_inner("", attr, "<unmapped>") == "<unmapped>"
1293 {
1294 unmapped.push(format!("{attr} (surface)"));
1295 }
1296 }
1297
1298 assert!(
1299 unmapped.is_empty(),
1300 "attribute_type_for_kind_inner() has no entry for {} attribute(s): {:?}\n\
1301 Add an arm to `attribute_type_for_kind_inner` or `attribute_type_generic` \
1302 in zenith-core/src/schema.rs.",
1303 unmapped.len(),
1304 unmapped,
1305 );
1306 }
1307
1308 #[test]
1314 fn fill_type_hint_is_kind_accurate() {
1315 for kind in &["rect", "ellipse", "polygon", "polyline", "pattern"] {
1318 assert_eq!(
1319 attribute_type_for_kind(kind, "fill"),
1320 "token ref: color/gradient",
1321 "fill on {kind} should accept color/gradient (validator uses ColorOrGradient)",
1322 );
1323 }
1324 for kind in &["text", "shape", "code", "table"] {
1327 assert_eq!(
1328 attribute_type_for_kind(kind, "fill"),
1329 "token ref: color",
1330 "fill on {kind} should be color-only (validator uses VisualExpect::Color)",
1331 );
1332 }
1333 }
1334
1335 #[test]
1339 fn stroke_type_hint_is_color_only() {
1340 for kind in &[
1341 "rect",
1342 "ellipse",
1343 "line",
1344 "polygon",
1345 "polyline",
1346 "pattern",
1347 "text",
1348 "shape",
1349 "connector",
1350 ] {
1351 assert_eq!(
1352 attribute_type_for_kind(kind, "stroke"),
1353 "token ref: color",
1354 "stroke on {kind} must be color-only (validator uses VisualExpect::Color)",
1355 );
1356 }
1357 }
1358
1359 #[test]
1363 fn shadow_filter_mask_report_own_token_types() {
1364 for kind in &["rect", "pattern"] {
1365 assert_eq!(
1366 attribute_type_for_kind(kind, "shadow"),
1367 "token ref: shadow",
1368 "shadow on {kind} must reference a shadow token",
1369 );
1370 assert_eq!(
1371 attribute_type_for_kind(kind, "filter"),
1372 "token ref: filter",
1373 "filter on {kind} must reference a filter token",
1374 );
1375 assert_eq!(
1376 attribute_type_for_kind(kind, "mask"),
1377 "token ref: mask",
1378 "mask on {kind} must reference a mask token",
1379 );
1380 }
1381 assert_eq!(attribute_type("shadow"), "token ref: shadow");
1383 assert_eq!(attribute_type("filter"), "token ref: filter");
1384 assert_eq!(attribute_type("mask"), "token ref: mask");
1385 }
1386
1387 #[test]
1388 fn container_effect_attributes_are_discoverable() {
1389 for kind in &["group", "frame"] {
1390 let attrs = node_attributes(kind);
1391 for attr in &["shadow", "filter", "mask", "blur", "blend-mode"] {
1392 assert!(
1393 attrs.contains(attr),
1394 "{kind} schema must include {attr}; attrs: {attrs:?}"
1395 );
1396 }
1397 assert_eq!(attribute_type_for_kind(kind, "shadow"), "token ref: shadow");
1398 assert_eq!(attribute_type_for_kind(kind, "filter"), "token ref: filter");
1399 assert_eq!(attribute_type_for_kind(kind, "mask"), "token ref: mask");
1400 }
1401 }
1402
1403 #[test]
1406 fn background_type_hint_is_color_or_gradient() {
1407 assert_eq!(attribute_type("background"), "token ref: color/gradient");
1408 assert_eq!(
1409 attribute_type_for_kind("", "background"),
1410 "token ref: color/gradient"
1411 );
1412 }
1413
1414 #[test]
1417 fn border_and_stroke_outer_are_color_only() {
1418 for attr in &[
1419 "border-top",
1420 "border-bottom",
1421 "border-left",
1422 "border-right",
1423 "stroke-outer",
1424 ] {
1425 assert_eq!(
1426 attribute_type_for_kind("rect", attr),
1427 "token ref: color",
1428 "{attr} on rect must be color-only (validator uses VisualExpect::Color)",
1429 );
1430 }
1431 }
1432
1433 #[test]
1441 fn kind_type_hint_is_node_aware() {
1442 assert_eq!(
1443 attribute_type_for_kind("shape", "kind"),
1444 "enum: process|decision|terminator|ellipse",
1445 "shape.kind must enumerate the flowchart shape variants",
1446 );
1447 assert_eq!(
1448 attribute_type_for_kind("pattern", "kind"),
1449 "enum: grid|scatter",
1450 "pattern.kind must enumerate the tiling mode variants",
1451 );
1452 assert_ne!(
1454 attribute_type_for_kind("shape", "kind"),
1455 attribute_type_for_kind("pattern", "kind"),
1456 "shape.kind and pattern.kind must NOT return the same hint (they are different enums)",
1457 );
1458 }
1459
1460 #[test]
1464 fn route_type_hint_is_connector_specific() {
1465 assert_eq!(
1466 attribute_type_for_kind("connector", "route"),
1467 "enum: straight|orthogonal|avoid",
1468 "connector.route must enumerate all three routing modes",
1469 );
1470 }
1471
1472 #[test]
1477 fn layout_type_hint_is_frame_specific() {
1478 assert_eq!(
1479 attribute_type_for_kind("frame", "layout"),
1480 "enum: absolute|flow|grid",
1481 "frame.layout must enumerate the three layout modes",
1482 );
1483 }
1484
1485 fn token_type_variant_count_exhaustive(ty: &TokenType) -> usize {
1501 match ty {
1502 TokenType::Color => 1,
1503 TokenType::Dimension => 1,
1504 TokenType::Number => 1,
1505 TokenType::FontFamily => 1,
1506 TokenType::FontWeight => 1,
1507 TokenType::Gradient => 1,
1508 TokenType::Shadow => 1,
1509 TokenType::Filter => 1,
1510 TokenType::Mask => 1,
1511 TokenType::Unknown(_) => 1,
1514 }
1515 }
1516
1517 const TOTAL_TOKEN_TYPE_VARIANTS: usize = 10; #[test]
1522 fn token_type_summary_covers_every_token_type() {
1523 let expected_authorable = TOTAL_TOKEN_TYPE_VARIANTS - 1;
1526 assert_eq!(
1527 token_types().len(),
1528 expected_authorable,
1529 "token_types() has {} entries but the exhaustive TokenType match covers {} authorable \
1530 variants (plus Unknown). Update token_types(), token_type_summary(), and \
1531 token_type_descriptor() when adding a variant.",
1532 token_types().len(),
1533 expected_authorable,
1534 );
1535
1536 let _guard: fn(&TokenType) -> usize = token_type_variant_count_exhaustive;
1540
1541 for ty in token_types() {
1543 assert!(
1544 token_type_summary(ty).is_some(),
1545 "token_type_summary(\"{ty}\") returned None — add a one-liner to token_type_summary()",
1546 );
1547 }
1548 }
1549
1550 #[test]
1551 fn token_type_descriptor_covers_every_token_type() {
1552 for ty in token_types() {
1554 assert!(
1555 token_type_descriptor(ty).is_some(),
1556 "token_type_descriptor(\"{ty}\") returned None — add a descriptor to token_type_descriptor()",
1557 );
1558 }
1559
1560 for ty in token_types() {
1562 let desc = token_type_descriptor(ty).unwrap();
1563 assert_eq!(
1564 desc.type_name, *ty,
1565 "token_type_descriptor(\"{ty}\").type_name is \"{}\", expected \"{ty}\"",
1566 desc.type_name,
1567 );
1568 assert!(
1570 !desc.summary.is_empty(),
1571 "token_type_descriptor(\"{ty}\").summary is empty",
1572 );
1573 assert!(
1575 !desc.value_form.is_empty() || !desc.child_nodes.is_empty(),
1576 "token_type_descriptor(\"{ty}\") has both empty value_form and child_nodes",
1577 );
1578 assert!(
1580 !desc.example.is_empty(),
1581 "token_type_descriptor(\"{ty}\").example is empty",
1582 );
1583 }
1584 }
1585
1586 #[test]
1594 fn token_type_examples_parse_without_syntax_errors() {
1595 for ty in token_types() {
1596 let desc = token_type_descriptor(ty).unwrap();
1597 let doc_src = format!(
1601 "zenith version=1 {{\n\
1602 \x20 tokens format=\"zenith-token-v1\" {{\n\
1603 \x20 {}\n\
1604 \x20 }}\n\
1605 \x20 document id=\"doc\" {{\n\
1606 \x20 page id=\"pg\" w=(px)1 h=(px)1 {{}}\n\
1607 \x20 }}\n\
1608 }}\n",
1609 desc.example,
1610 );
1611 let result = KdlAdapter.parse(doc_src.as_bytes());
1612 assert!(
1613 result.is_ok(),
1614 "token_type_descriptor(\"{ty}\").example failed to parse:\n\
1615 example:\n {}\n\
1616 wrapped doc:\n{doc_src}\n\
1617 parse error: {:?}",
1618 desc.example,
1619 result.err(),
1620 );
1621 }
1622 }
1623}