Skip to main content

surf_parse/
types.rs

1use std::collections::{BTreeMap, HashMap};
2
3use serde::{Deserialize, Serialize};
4
5use crate::citation::Reference;
6
7/// A parsed SurfDoc document.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SurfDoc {
10    /// Parsed YAML front matter, if present.
11    pub front_matter: Option<FrontMatter>,
12    /// Ordered sequence of blocks in the document body.
13    pub blocks: Vec<Block>,
14    /// Original source text that was parsed.
15    pub source: String,
16}
17
18/// YAML front matter fields.
19///
20/// Known fields are typed; unknown fields are captured in `extra`.
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
22#[serde(default)]
23pub struct FrontMatter {
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub title: Option<String>,
26
27    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
28    pub doc_type: Option<DocType>,
29
30    /// Publication/citation format for `paper`/`report` doc types
31    /// (`ieee`/`acm`/`article`/`mla`/`apa`/`chicago`). Optional; default None.
32    #[serde(rename = "format", skip_serializing_if = "Option::is_none")]
33    pub format: Option<Format>,
34
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub status: Option<DocStatus>,
37
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub scope: Option<Scope>,
40
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub tags: Option<Vec<String>>,
43
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub created: Option<String>,
46
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub updated: Option<String>,
49
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub author: Option<String>,
52
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub confidence: Option<Confidence>,
55
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub related: Option<Vec<Related>>,
58
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub version: Option<u32>,
61
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub contributors: Option<Vec<String>>,
64
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub description: Option<String>,
67
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub workspace: Option<String>,
70
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub decision: Option<String>,
73
74    /// Any front matter fields not covered by typed fields above.
75    #[serde(flatten)]
76    pub extra: HashMap<String, serde_yaml::Value>,
77}
78
79/// A cross-reference to another document.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct Related {
82    pub path: String,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub relationship: Option<Relationship>,
85}
86
87/// Relationship type for cross-references.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "lowercase")]
90pub enum Relationship {
91    Produces,
92    Consumes,
93    References,
94    Supersedes,
95}
96
97/// SurfDoc document types (front matter `type` field).
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "lowercase")]
100pub enum DocType {
101    Doc,
102    Guide,
103    Conversation,
104    Plan,
105    Agent,
106    Preference,
107    Report,
108    Proposal,
109    Incident,
110    Review,
111    App,
112    Manifest,
113    /// Multi-page website (rendered via the `::site`/`::page` block family).
114    Website,
115    /// Multi-page web document. Synonym of [`DocType::Website`]: drives the
116    /// exact same `::site`/`::page` multipage render path (rendering keys off
117    /// the blocks, not the type), so `type: web` and `type: website` produce
118    /// identical output for the same body.
119    Web,
120    /// Presentation deck (rendered via the `::deck`/`::slide` block family).
121    Deck,
122    /// Alias for [`DocType::Deck`] — `type: slides` in front matter.
123    Slides,
124    /// Presentation. Resolves to the same render profile as
125    /// [`DocType::Deck`]/[`DocType::Slides`] (the slides path).
126    Presentation,
127    /// Scientific/academic paper. Pairs with a [`Format`] (`ieee`/`acm`/
128    /// `article`) to select a paper template at render time.
129    Paper,
130}
131
132/// Publication / citation format for papers and reports (front matter
133/// `format` field).
134///
135/// Optional — when absent the render profile falls back to a sensible default
136/// for the document type (`article` for papers, `mla` for reports). Aliases
137/// accept the common upper/title-case spellings so `format: IEEE` and
138/// `format: ieee` both parse.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "lowercase")]
141pub enum Format {
142    /// IEEE paper format (two-column, numbered citations).
143    #[serde(alias = "IEEE", alias = "Ieee")]
144    Ieee,
145    /// ACM paper format (acmart).
146    #[serde(alias = "ACM", alias = "Acm")]
147    Acm,
148    /// Generic single-column article paper format.
149    #[serde(alias = "Article", alias = "ARTICLE")]
150    Article,
151    /// MLA report format (Works Cited, author-page citations).
152    #[serde(alias = "MLA", alias = "Mla")]
153    Mla,
154    /// APA report format (References, author-date citations).
155    #[serde(alias = "APA", alias = "Apa")]
156    Apa,
157    /// Chicago report format (notes-bibliography or author-date).
158    #[serde(alias = "Chicago", alias = "CHICAGO")]
159    Chicago,
160}
161
162/// A resolved rendering profile: the engine-level decision of *what kind of
163/// artifact* a document produces, derived once from `(DocType, Option<Format>)`
164/// so renderers consult a single small enum instead of re-deriving intent.
165///
166/// This is the Chunk 1 foundation other chunks build on: Chunk 5 keys the
167/// slides path off [`RenderProfile::Presentation`], and Chunk 6 keys the
168/// paper/report templates off [`RenderProfile::Paper`]/[`RenderProfile::Report`]
169/// and the carried [`Format`].
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum RenderProfile {
172    /// Standard single-document rendering (doc, guide, plan, report-less, …).
173    Document,
174    /// Multi-page web rendering (`::site`/`::page`); from `web`/`website`.
175    Web,
176    /// Presentation/slides rendering; from `deck`/`slides`/`presentation`.
177    Presentation,
178    /// Scientific paper with the carried citation/template [`Format`].
179    Paper(Format),
180    /// Academic report with the carried citation/template [`Format`].
181    Report(Format),
182}
183
184/// Resolve a `(DocType, Option<Format>)` pair to a [`RenderProfile`].
185///
186/// Pure and total: every [`DocType`] maps to exactly one profile, and every
187/// [`Format`] reaches a profile (drift-tested). `None` for the doc type
188/// resolves to [`RenderProfile::Document`]. Papers default to
189/// [`Format::Article`] and reports to [`Format::Mla`] when `format` is absent.
190pub fn render_profile(doc_type: Option<DocType>, format: Option<Format>) -> RenderProfile {
191    match doc_type {
192        None => RenderProfile::Document,
193        Some(dt) => match dt {
194            DocType::Website | DocType::Web => RenderProfile::Web,
195            DocType::Deck | DocType::Slides | DocType::Presentation => {
196                RenderProfile::Presentation
197            }
198            DocType::Paper => RenderProfile::Paper(format.unwrap_or(Format::Article)),
199            DocType::Report => RenderProfile::Report(format.unwrap_or(Format::Mla)),
200            DocType::Doc
201            | DocType::Guide
202            | DocType::Conversation
203            | DocType::Plan
204            | DocType::Agent
205            | DocType::Preference
206            | DocType::Proposal
207            | DocType::Incident
208            | DocType::Review
209            | DocType::App
210            | DocType::Manifest => RenderProfile::Document,
211        },
212    }
213}
214
215/// Document lifecycle status.
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(rename_all = "lowercase")]
218pub enum DocStatus {
219    Draft,
220    Active,
221    Closed,
222    Archived,
223}
224
225/// Visibility/access scope.
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(rename_all = "kebab-case")]
228pub enum Scope {
229    Personal,
230    WorkspacePrivate,
231    Workspace,
232    Repo,
233    Public,
234}
235
236/// Confidence level for guides and estimates.
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(rename_all = "lowercase")]
239pub enum Confidence {
240    Low,
241    Medium,
242    High,
243}
244
245/// A parsed block in the document body.
246#[derive(Debug, Clone, Serialize, Deserialize)]
247#[serde(tag = "kind")]
248pub enum Block {
249    /// A block directive that has not yet been typed (Chunk 1 catch-all).
250    Unknown {
251        name: String,
252        attrs: Attrs,
253        content: String,
254        span: Span,
255    },
256    /// Plain markdown content between directives.
257    Markdown {
258        content: String,
259        span: Span,
260    },
261    /// Callout/admonition box.
262    Callout {
263        callout_type: CalloutType,
264        title: Option<String>,
265        content: String,
266        span: Span,
267    },
268    /// Structured data table (CSV/JSON/inline rows).
269    Data {
270        id: Option<String>,
271        format: DataFormat,
272        sortable: bool,
273        headers: Vec<String>,
274        rows: Vec<Vec<String>>,
275        raw_content: String,
276        span: Span,
277    },
278    /// Code block with optional language and file path.
279    Code {
280        lang: Option<String>,
281        file: Option<String>,
282        highlight: Vec<String>,
283        content: String,
284        span: Span,
285    },
286    /// Task list with checkbox items.
287    Tasks {
288        items: Vec<TaskItem>,
289        span: Span,
290    },
291    /// Decision record.
292    Decision {
293        status: DecisionStatus,
294        date: Option<String>,
295        deciders: Vec<String>,
296        content: String,
297        span: Span,
298    },
299    /// Single metric display.
300    Metric {
301        label: String,
302        value: String,
303        trend: Option<Trend>,
304        unit: Option<String>,
305        span: Span,
306    },
307    /// Executive summary block.
308    Summary {
309        content: String,
310        span: Span,
311    },
312    /// A bibliographic reference definition (`::cite`). Renders nothing on its
313    /// own; it registers a [`Reference`] consumed by inline `[@key]` citations
314    /// and the `::bibliography` list. See [`crate::citation`].
315    Cite {
316        /// The parsed reference (carries its own `key`).
317        reference: Reference,
318        span: Span,
319    },
320    /// A rendered reference list (`::bibliography` / `::references`). Collects
321    /// every `::cite`-defined reference and formats them per the active citation
322    /// style (or the optional per-block `style=` override).
323    Bibliography {
324        /// Optional style override; defaults to the document's `format:` (or APA).
325        style: Option<Format>,
326        span: Span,
327    },
328    /// Figure with image source and caption.
329    Figure {
330        src: String,
331        caption: Option<String>,
332        alt: Option<String>,
333        width: Option<String>,
334        span: Span,
335    },
336    /// Native diagram block (`::diagram`) — architecture diagrams + ERDs.
337    /// `content` is the raw DSL source, preserved verbatim for lossless
338    /// round-trip; unknown `diagram_type` values degrade to prose at render.
339    Diagram {
340        /// Raw `type` attr value, lowercased; `""` if absent.
341        diagram_type: String,
342        title: Option<String>,
343        content: String,
344        span: Span,
345    },
346    /// Tabbed content with named panels.
347    Tabs {
348        tabs: Vec<TabPanel>,
349        span: Span,
350    },
351    /// Multi-column layout.
352    Columns {
353        columns: Vec<ColumnContent>,
354        span: Span,
355    },
356    /// Attributed quote with optional source.
357    Quote {
358        content: String,
359        attribution: Option<String>,
360        cite: Option<String>,
361        span: Span,
362    },
363    /// Call-to-action button.
364    Cta {
365        label: String,
366        href: String,
367        primary: bool,
368        icon: Option<String>,
369        span: Span,
370    },
371    /// Navigation bar with links.
372    Nav {
373        items: Vec<NavItem>,
374        logo: Option<String>,
375        /// Labelled link groups for the rich-shell drawer (e.g. Explore /
376        /// Company / Products). Empty for a plain flat nav (the legacy form,
377        /// which uses `items`).
378        #[serde(default)]
379        groups: Vec<NavGroup>,
380        /// Brand wordmark text shown beside the logo (e.g. "CloudSurf").
381        #[serde(default)]
382        brand: Option<String>,
383        /// Append a `®` superscript to the brand wordmark.
384        #[serde(default)]
385        brand_reg: bool,
386        /// Trailing primary call-to-action link (per-page "Get in touch" etc.).
387        #[serde(default)]
388        cta: Option<NavItem>,
389        /// Opt into the rich shell layout: a JS-driven slide-in drawer + scrim,
390        /// grouped/iconned links, brand wordmark, and a head-provided theme
391        /// toggle. When `false` (and no groups/brand), renders the legacy
392        /// checkbox-drawer nav so existing consumers are unchanged.
393        #[serde(default)]
394        drawer: bool,
395        /// Render a stripped topbar only: brand/logo + theme toggle, no
396        /// hamburger, drawer, or scrim. Backward compatible (defaults false).
397        #[serde(default)]
398        minimal: bool,
399        span: Span,
400    },
401    /// Hero image visual.
402    HeroImage {
403        src: String,
404        alt: Option<String>,
405        span: Span,
406    },
407    /// Customer testimonial.
408    Testimonial {
409        content: String,
410        author: Option<String>,
411        role: Option<String>,
412        company: Option<String>,
413        span: Span,
414    },
415    /// Presentation style overrides (key-value pairs).
416    Style {
417        properties: Vec<StyleProperty>,
418        span: Span,
419    },
420    /// FAQ accordion with question/answer pairs.
421    Faq {
422        items: Vec<FaqItem>,
423        span: Span,
424    },
425    /// Pricing comparison table.
426    PricingTable {
427        headers: Vec<String>,
428        rows: Vec<Vec<String>>,
429        span: Span,
430    },
431    /// Site-level configuration (one per document).
432    Site {
433        domain: Option<String>,
434        properties: Vec<StyleProperty>,
435        span: Span,
436    },
437    /// Page/route definition — container block with child blocks.
438    Page {
439        route: String,
440        layout: Option<String>,
441        title: Option<String>,
442        sidebar: bool,
443        /// Raw content for degradation renderers.
444        content: String,
445        /// Parsed child blocks (leaf directives resolved, rest as Markdown).
446        children: Vec<Block>,
447        span: Span,
448    },
449    /// Deck-level configuration (one per document) — the `::deck` block.
450    ///
451    /// Peer of [`Block::Site`]: a leaf config block holding presentation
452    /// properties (`theme`, `aspect`, `transition`, `accent`, `font`). The
453    /// slides themselves are top-level [`Block::Slide`] siblings, exactly as
454    /// `::page` blocks are siblings of `::site`.
455    Deck {
456        properties: Vec<StyleProperty>,
457        span: Span,
458    },
459    /// A single slide — container block with child blocks.
460    ///
461    /// Peer of [`Block::Page`]: its `children` reuse the existing content
462    /// block families (Hero/Features/Stats/Comparison/Quote/Code/…), so a
463    /// slide is an *arrangement* of blocks that already exist, never a new
464    /// content primitive.
465    Slide {
466        layout: Option<SlideLayout>,
467        kicker: Option<String>,
468        notes: Option<String>,
469        /// Raw content for degradation renderers.
470        content: String,
471        children: Vec<Block>,
472        span: Span,
473    },
474    /// Embedded external content (iframe).
475    Embed {
476        src: String,
477        embed_type: Option<EmbedType>,
478        width: Option<String>,
479        height: Option<String>,
480        title: Option<String>,
481        span: Span,
482    },
483    /// Form with arbitrary fields that submits to the inbox.
484    Form {
485        fields: Vec<FormField>,
486        submit_label: Option<String>,
487        /// Form submission target. When `Some`, the rendered `<form>` carries a
488        /// real `action`/`method` so it can POST to a server route.
489        action: Option<String>,
490        /// HTTP method (`post`/`get`). Defaults to `post` when `action` is set.
491        method: Option<String>,
492        /// Emit a hidden `_honey` honeypot field for spam mitigation.
493        honeypot: bool,
494        span: Span,
495    },
496    /// Centered call-to-action band: heading + subtext + optional buttons.
497    Banner {
498        headline: Option<String>,
499        subtitle: Option<String>,
500        buttons: Vec<HeroButton>,
501        /// Optional anchor id for in-page links (e.g. `#contact`).
502        id: Option<String>,
503        content: String,
504        span: Span,
505    },
506    /// Grid of product link-cards, optionally split into labelled groups.
507    ProductGrid {
508        groups: Vec<ProductGroup>,
509        /// `[tiles]` — apple.com-style promo tiles (full-bleed 2-up band,
510        /// centered headline/tagline/CTA over a per-card background) instead
511        /// of the compact emblem link-cards.
512        tiles: bool,
513        span: Span,
514    },
515    /// Card grid for a blog/news/events index.
516    PostGrid {
517        title: Option<String>,
518        subtitle: Option<String>,
519        items: Vec<PostItem>,
520        span: Span,
521    },
522    /// Access-code card: password field + submit button.
523    Gate {
524        title: Option<String>,
525        subtitle: Option<String>,
526        /// Form POST target. Defaults to "".
527        action: String,
528        field_label: Option<String>,
529        submit_label: Option<String>,
530        error: Option<String>,
531        span: Span,
532    },
533    /// Image gallery with optional categories.
534    Gallery {
535        items: Vec<GalleryItem>,
536        columns: Option<u32>,
537        span: Span,
538    },
539    /// Structured footer with sections, copyright, and social links.
540    Footer {
541        sections: Vec<FooterSection>,
542        copyright: Option<String>,
543        social: Vec<SocialLink>,
544        /// Brand wordmark for the footer brand column (e.g. "CloudSurf").
545        #[serde(default)]
546        brand: Option<String>,
547        /// Append a `®` superscript to the footer brand wordmark.
548        #[serde(default)]
549        brand_reg: bool,
550        /// Logo image src for the footer brand column.
551        #[serde(default)]
552        brand_logo: Option<String>,
553        /// Tagline under the brand (e.g. "Innovate • Simplify • Scale").
554        #[serde(default)]
555        tagline: Option<String>,
556        span: Span,
557    },
558    /// Collapsible content section.
559    Details {
560        title: Option<String>,
561        open: bool,
562        content: String,
563        span: Span,
564    },
565    /// Labeled thematic break.
566    Divider {
567        label: Option<String>,
568        span: Span,
569    },
570    /// Full hero section with headline, subtitle, CTA buttons.
571    Hero {
572        headline: Option<String>,
573        subtitle: Option<String>,
574        badge: Option<String>,
575        align: String,
576        image: Option<String>,
577        /// Alt text for `image` (accessibility). `None` → decorative (`alt=""`).
578        image_alt: Option<String>,
579        /// Explicit layout hint: `stacked` (image above headline). When absent,
580        /// the legacy `align`-driven behavior applies (centered → above,
581        /// left → side).
582        layout: Option<String>,
583        /// Drop the hero's card background/shadow so it blends into the page
584        /// (text over the page background instead of a gradient card).
585        transparent: bool,
586        buttons: Vec<HeroButton>,
587        content: String,
588        span: Span,
589    },
590    /// Card grid for features, products, or values.
591    Features {
592        cards: Vec<FeatureCard>,
593        cols: Option<u32>,
594        span: Span,
595    },
596    /// Numbered process/timeline steps.
597    Steps {
598        steps: Vec<StepItem>,
599        span: Span,
600    },
601    /// Row of metric/stat cards.
602    Stats {
603        items: Vec<StatItem>,
604        span: Span,
605    },
606    /// Feature comparison matrix with check/dash rendering.
607    Comparison {
608        headers: Vec<String>,
609        rows: Vec<Vec<String>>,
610        highlight: Option<String>,
611        span: Span,
612    },
613    /// Centered brand/logo display.
614    Logo {
615        src: String,
616        alt: Option<String>,
617        size: Option<u32>,
618        span: Span,
619    },
620    /// Auto-generated table of contents from document headings.
621    Toc {
622        depth: u32,
623        entries: Vec<TocEntry>,
624        span: Span,
625    },
626    /// Before/After problem→solution visualization.
627    BeforeAfter {
628        before_items: Vec<BeforeAfterItem>,
629        after_items: Vec<BeforeAfterItem>,
630        transition: Option<String>,
631        span: Span,
632    },
633    /// Horizontal flow pipeline with arrows between steps.
634    Pipeline {
635        steps: Vec<PipelineStep>,
636        span: Span,
637    },
638    /// Page section container with background control and child blocks.
639    Section {
640        bg: Option<String>,
641        headline: Option<String>,
642        subtitle: Option<String>,
643        content: String,
644        children: Vec<Block>,
645        span: Span,
646    },
647    /// Rich product card with badge, body, features, and CTA.
648    ProductCard {
649        title: String,
650        subtitle: Option<String>,
651        badge: Option<String>,
652        badge_color: Option<String>,
653        body: String,
654        features: Vec<String>,
655        cta_label: Option<String>,
656        cta_href: Option<String>,
657        span: Span,
658    },
659
660    // ----- App description blocks (data-bound) -----
661
662    /// Dynamic data list with filtering and sorting.
663    List {
664        source: String,
665        display: ListDisplay,
666        item_template: String,
667        filters: Vec<ListFilter>,
668        sort: Option<SortSpec>,
669        preload: bool,
670        span: Span,
671    },
672    /// Kanban board with draggable cards.
673    Board {
674        source: String,
675        columns: Vec<String>,
676        card_template: Option<String>,
677        preload: bool,
678        span: Span,
679    },
680    /// CRUD form that submits via HTMX (extends ::form with action).
681    Action {
682        method: HttpMethod,
683        target: String,
684        label: String,
685        fields: Vec<FormField>,
686        confirm: Option<String>,
687        span: Span,
688    },
689    /// Filter controls for data views.
690    FilterBar {
691        target_selector: String,
692        fields: Vec<FilterField>,
693        span: Span,
694    },
695    /// Search input with typeahead results.
696    Search {
697        source: String,
698        placeholder: Option<String>,
699        span: Span,
700    },
701    /// Metrics dashboard with auto-refresh.
702    Dashboard {
703        source: String,
704        refresh: Option<u32>,
705        span: Span,
706    },
707    /// Smart-routed chat input.
708    ChatInput {
709        action: String,
710        placeholder: Option<String>,
711        modes: Vec<String>,
712        span: Span,
713    },
714    /// Real-time content feed (SSE or polling).
715    Feed {
716        source: String,
717        stream: bool,
718        span: Span,
719    },
720    /// Self-contained storefront widget: category-filtered product grid with
721    /// add-to-cart, a live cart (qty steppers + line totals), and a checkout
722    /// form that resolves to an order-confirmation card. Static data-bound (no
723    /// backend; payment links out). Drives the commerce examples (Marketplace,
724    /// Delivery, Local-service, …).
725    Store {
726        title: Option<String>,
727        /// Currency symbol prefixed to prices (e.g. "$"). Defaults to "$".
728        currency: Option<String>,
729        items: Vec<StoreItem>,
730        span: Span,
731    },
732    /// Self-contained appointment/booking widget: optional service selector +
733    /// month calendar driven by per-day availability + slot picker + booking
734    /// confirmation. Static data-bound (no backend); the inlined client script
735    /// renders the calendar and drives selection → confirmation entirely in the
736    /// browser. Drives the scheduling examples (Scheduler, Restaurant-reserve,
737    /// Local-service-book, …).
738    Booking {
739        title: Option<String>,
740        /// Heading shown above the service radios (e.g. "Service", "Treatment").
741        service_label: Option<String>,
742        services: Vec<BookingService>,
743        days: Vec<BookingDay>,
744        span: Span,
745    },
746
747    // ----- Compound widget mount points -----
748
749    /// Code/SurfDoc editor mount point.
750    Editor {
751        source: Option<String>,
752        lang: Option<String>,
753        preview: bool,
754        span: Span,
755    },
756    /// Data visualization mount point.
757    Chart {
758        chart_type: ChartType,
759        source: String,
760        period: Option<String>,
761        /// Optional chart title (from the `title=` attribute).
762        title: Option<String>,
763        /// Inline dataset parsed from the block body. When `Some`, the chart
764        /// is rendered as real deterministic SVG; when `None` it falls back to
765        /// the `source=` live-data mount point / static placeholder.
766        data: Option<ChartData>,
767        span: Span,
768    },
769    /// Resizable side-by-side layout mount point.
770    SplitPane {
771        ratio: String,
772        span: Span,
773    },
774
775    // ----- Infrastructure manifest blocks -----
776
777    /// Top-level app manifest container (like Page — recursively parses children).
778    App {
779        name: String,
780        binary: Option<String>,
781        region: Option<String>,
782        port: Option<u32>,
783        platform: Option<String>,
784        /// App-level auth marker from the `auth=` attribute (e.g.
785        /// `auth=password`). Free-form; distinct from the structured child
786        /// `::auth` block.
787        #[serde(default, skip_serializing_if = "Option::is_none")]
788        auth: Option<String>,
789        content: String,
790        children: Vec<Block>,
791        span: Span,
792    },
793    /// Build configuration (base image, runtime, edition).
794    Build {
795        base: Option<String>,
796        runtime: Option<String>,
797        edition: Option<String>,
798        properties: Vec<StyleProperty>,
799        span: Span,
800    },
801    /// Infrastructure database configuration.
802    InfraDatabase {
803        name: Option<String>,
804        shared_auth: bool,
805        volume_gb: Option<u32>,
806        properties: Vec<StyleProperty>,
807        span: Span,
808    },
809    /// Deployment environment configuration.
810    Deploy {
811        env: Option<String>,
812        app: Option<String>,
813        machines: Option<u32>,
814        memory: Option<u32>,
815        auto_stop: Option<String>,
816        min_machines: Option<u32>,
817        strategy: Option<String>,
818        properties: Vec<StyleProperty>,
819        span: Span,
820    },
821    /// Environment variable group (required/recommended/optional/defaults).
822    InfraEnv {
823        tier: Option<String>,
824        entries: Vec<EnvEntry>,
825        span: Span,
826    },
827    /// Health check configuration.
828    Health {
829        path: Option<String>,
830        method: Option<String>,
831        grace: Option<String>,
832        interval: Option<String>,
833        timeout: Option<String>,
834        span: Span,
835    },
836    /// Concurrency/connection limits.
837    Concurrency {
838        concurrency_type: Option<String>,
839        hard_limit: Option<u32>,
840        soft_limit: Option<u32>,
841        force_https: bool,
842        span: Span,
843    },
844    /// CI/CD pipeline configuration.
845    Cicd {
846        provider: Option<String>,
847        properties: Vec<StyleProperty>,
848        span: Span,
849    },
850    /// Smoke test checks (HTTP method + path + expected status).
851    Smoke {
852        script: Option<String>,
853        checks: Vec<SmokeCheck>,
854        span: Span,
855    },
856    /// Domain entries for the app.
857    Domains {
858        entries: Vec<DomainEntry>,
859        span: Span,
860    },
861    /// Shared crate dependencies.
862    Crates {
863        entries: Vec<CrateEntry>,
864        span: Span,
865    },
866    /// Per-environment deploy URLs.
867    DeployUrls {
868        entries: Vec<StyleProperty>,
869        span: Span,
870    },
871    /// Named volume mounts.
872    Volumes {
873        entries: Vec<VolumeEntry>,
874        span: Span,
875    },
876
877    // ----- App spec blocks (data layer + API) -----
878
879    /// Data model definition with typed fields and constraints.
880    Model {
881        name: String,
882        fields: Vec<ModelField>,
883        span: Span,
884    },
885    /// API route/endpoint definition with optional embedded Rust handler.
886    Route {
887        method: HttpMethod,
888        path: String,
889        auth: Option<String>,
890        returns: Option<String>,
891        body: Option<String>,
892        handler: Option<String>,
893        content: String,
894        span: Span,
895    },
896    /// Authentication configuration.
897    Auth {
898        provider: AuthProvider,
899        session: Option<String>,
900        roles: Vec<String>,
901        default_role: Option<String>,
902        span: Span,
903    },
904    /// Data-to-UI binding connecting a route/model to a UI block.
905    Binding {
906        source: String,
907        target: String,
908        events: Vec<BindingEvent>,
909        span: Span,
910    },
911
912    // ----- App format blocks (schema, deps, config, deploy) -----
913
914    /// Data schema definition with typed fields and constraints.
915    Schema {
916        name: String,
917        fields: Vec<SchemaField>,
918        span: Span,
919    },
920    /// Crate/dependency declarations for an app.
921    Use {
922        crates: Vec<CrateDep>,
923        span: Span,
924    },
925    /// App-level environment variable declarations with descriptions.
926    AppEnv {
927        vars: Vec<EnvVar>,
928        span: Span,
929    },
930    /// App-level deployment configuration.
931    AppDeploy {
932        region: Option<String>,
933        scale: Option<u32>,
934        domain: Option<String>,
935        memory: Option<String>,
936        properties: Vec<(String, String)>,
937        span: Span,
938    },
939
940    /// A compact row component with icon, title, description, and optional link.
941    /// Three states: default (content), loading (skeleton), empty (placeholder).
942    /// `::row[icon=sparkle, href=/wiki/article, state=loading]`
943    Row {
944        icon: String,
945        title: String,
946        description: String,
947        href: Option<String>,
948        state: RowState,
949        span: Span,
950    },
951
952    /// A knowledge/info card with title, subtitle, summary, facts, and optional image.
953    /// `::infocard[intent=who, image=https://...]`
954    InfoCard {
955        intent: String,
956        title: String,
957        subtitle: String,
958        summary: String,
959        image: Option<String>,
960        facts: Vec<[String; 2]>,
961        steps: Vec<String>,
962        state: RowState,
963        span: Span,
964    },
965
966    // ----- Interactive / application blocks -----
967
968    /// Root app container with layout mode.
969    AppShell {
970        layout: String,
971        children: Vec<Block>,
972        span: Span,
973    },
974    /// Collapsible side panel.
975    Sidebar {
976        position: String,
977        collapsible: bool,
978        width: Option<u32>,
979        children: Vec<Block>,
980        span: Span,
981    },
982    /// Resizable bottom/side panel.
983    Panel {
984        position: String,
985        resizable: bool,
986        height: Option<u32>,
987        desktop_only: bool,
988        children: Vec<Block>,
989        span: Span,
990    },
991    /// Tab strip navigation.
992    TabBar {
993        active: Option<String>,
994        items: Vec<TabBarItem>,
995        span: Span,
996    },
997    /// Tab-associated content pane.
998    TabContent {
999        tab: String,
1000        children: Vec<Block>,
1001        span: Span,
1002    },
1003    /// Horizontal toolbar with buttons, separators, badges, etc.
1004    Toolbar {
1005        items: Vec<ToolbarItem>,
1006        span: Span,
1007    },
1008    /// Slide-out drawer panel.
1009    Drawer {
1010        name: String,
1011        position: String,
1012        width: Option<u32>,
1013        trigger: Option<String>,
1014        children: Vec<Block>,
1015        span: Span,
1016    },
1017    /// Dialog overlay.
1018    Modal {
1019        name: String,
1020        title: Option<String>,
1021        children: Vec<Block>,
1022        span: Span,
1023    },
1024    /// Searchable command picker.
1025    CommandPalette {
1026        trigger: Option<String>,
1027        items: Vec<CommandItem>,
1028        span: Span,
1029    },
1030    /// Syntax-highlighted code editor.
1031    CodeEditor {
1032        lang: Option<String>,
1033        source: Option<String>,
1034        line_numbers: bool,
1035        content: String,
1036        span: Span,
1037    },
1038    /// Visual block editor mount point.
1039    BlockEditor {
1040        source: Option<String>,
1041        span: Span,
1042    },
1043    /// Shell/terminal panel.
1044    Terminal {
1045        shell: Option<String>,
1046        cwd: Option<String>,
1047        span: Span,
1048    },
1049    /// File/navigation tree.
1050    NavTree {
1051        source: Option<String>,
1052        on_select: Option<String>,
1053        on_rename: Option<String>,
1054        on_delete: Option<String>,
1055        span: Span,
1056    },
1057    /// Status badge pill.
1058    Badge {
1059        value: String,
1060        color: Option<String>,
1061        span: Span,
1062    },
1063    /// Clickable suggestion chips.
1064    SuggestionChips {
1065        source: Option<String>,
1066        max: Option<u32>,
1067        dismissible: bool,
1068        span: Span,
1069    },
1070    /// Chat conversation thread display.
1071    ChatThread {
1072        source: Option<String>,
1073        on_action: Option<String>,
1074        span: Span,
1075    },
1076    /// Simple chat message input (distinct from app-bound ChatInput).
1077    ChatInputSimple {
1078        placeholder: Option<String>,
1079        action: Option<String>,
1080        span: Span,
1081    },
1082    /// Step/progress indicator.
1083    Progress {
1084        source: Option<String>,
1085        steps: Vec<ProgressStep>,
1086        span: Span,
1087    },
1088    /// Live log output stream.
1089    LogStream {
1090        source: Option<String>,
1091        tail: Option<u32>,
1092        span: Span,
1093    },
1094    /// Error/warning problem list.
1095    ProblemList {
1096        source: Option<String>,
1097        span: Span,
1098    },
1099}
1100
1101/// State for Row and InfoCard blocks.
1102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1103#[serde(rename_all = "lowercase")]
1104pub enum RowState {
1105    Default,
1106    Loading,
1107    Empty,
1108}
1109
1110/// A tab item within a `TabBar` block.
1111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1112pub struct TabBarItem {
1113    pub id: String,
1114    pub label: String,
1115}
1116
1117/// An item within a `Toolbar` block.
1118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1119pub enum ToolbarItem {
1120    Button {
1121        label: Option<String>,
1122        action: Option<String>,
1123        icon: Option<String>,
1124        style: Option<String>,
1125        disabled: bool,
1126    },
1127    Separator,
1128    Spacer,
1129    Badge {
1130        value: String,
1131        color: Option<String>,
1132    },
1133    Dropdown {
1134        label: String,
1135        options: Option<String>,
1136        action: Option<String>,
1137    },
1138    Text {
1139        value: String,
1140        editable: bool,
1141        action: Option<String>,
1142    },
1143}
1144
1145/// A command within a `CommandPalette` block.
1146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1147pub struct CommandItem {
1148    pub label: String,
1149    pub description: Option<String>,
1150    pub action: Option<String>,
1151    pub icon: Option<String>,
1152    pub group: Option<String>,
1153}
1154
1155/// A step within a `Progress` block.
1156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1157pub struct ProgressStep {
1158    pub label: String,
1159    /// One of: "done", "active", "pending".
1160    pub status: String,
1161}
1162
1163/// Callout/admonition type.
1164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1165#[serde(rename_all = "lowercase")]
1166pub enum CalloutType {
1167    Info,
1168    Warning,
1169    Danger,
1170    Tip,
1171    Note,
1172    Success,
1173    Context,
1174}
1175
1176/// Data block format.
1177#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1178#[serde(rename_all = "lowercase")]
1179pub enum DataFormat {
1180    Table,
1181    Csv,
1182    Json,
1183}
1184
1185/// A single task item within a `Tasks` block.
1186#[derive(Debug, Clone, Serialize, Deserialize)]
1187pub struct TaskItem {
1188    pub done: bool,
1189    pub text: String,
1190    pub assignee: Option<String>,
1191}
1192
1193/// Decision record status.
1194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1195#[serde(rename_all = "lowercase")]
1196pub enum DecisionStatus {
1197    Proposed,
1198    Accepted,
1199    Rejected,
1200    Superseded,
1201}
1202
1203/// Metric trend direction.
1204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1205#[serde(rename_all = "lowercase")]
1206pub enum Trend {
1207    Up,
1208    Down,
1209    Flat,
1210}
1211
1212/// A single tab panel within a `Tabs` block.
1213#[derive(Debug, Clone, Serialize, Deserialize)]
1214pub struct TabPanel {
1215    pub label: String,
1216    pub content: String,
1217}
1218
1219/// A single column in a `Columns` block.
1220#[derive(Debug, Clone, Serialize, Deserialize)]
1221pub struct ColumnContent {
1222    pub content: String,
1223}
1224
1225/// A key-value style override within a `Style` block.
1226#[derive(Debug, Clone, Serialize, Deserialize)]
1227pub struct StyleProperty {
1228    pub key: String,
1229    pub value: String,
1230}
1231
1232/// A question/answer pair within a `Faq` block.
1233#[derive(Debug, Clone, Serialize, Deserialize)]
1234pub struct FaqItem {
1235    pub question: String,
1236    pub answer: String,
1237}
1238
1239/// A navigation link within a `Nav` block.
1240#[derive(Debug, Clone, Serialize, Deserialize)]
1241pub struct NavItem {
1242    pub label: String,
1243    pub href: String,
1244    pub icon: Option<String>,
1245    /// Image emblem src (product link-cards). Renders before the label, in
1246    /// place of an `icon`. Optional — links render text-only when absent.
1247    #[serde(default)]
1248    pub image: Option<String>,
1249    /// Open in a new tab (`target="_blank" rel="noopener"`). Used for external
1250    /// product links in the rich shell nav.
1251    #[serde(default)]
1252    pub external: bool,
1253}
1254
1255/// A labelled group of navigation links within a `Nav` block (rich shell).
1256///
1257/// Mirrors [`ProductGroup`]: the grouped form parses its rows inside the nav
1258/// block's own grammar (`## Heading` introduces a group), never via nested
1259/// `::` blocks. Ungrouped navs leave `groups` empty and use `Nav.items`.
1260#[derive(Debug, Clone, Serialize, Deserialize)]
1261pub struct NavGroup {
1262    /// Group heading (e.g. "Explore"). `None` for an unlabelled lead group.
1263    pub label: Option<String>,
1264    pub items: Vec<NavItem>,
1265}
1266
1267/// Type of embedded content.
1268#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1269#[serde(rename_all = "lowercase")]
1270pub enum EmbedType {
1271    Map,
1272    Video,
1273    Audio,
1274    Generic,
1275}
1276
1277/// Layout of a single slide in a `::deck`.
1278///
1279/// A fixed set of presentation arrangements — the escape hatch for anything
1280/// outside this set is a per-slide `::style` block, not a new layout. This
1281/// keeps slides expressive without trying to be Figma.
1282#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1283#[serde(rename_all = "lowercase")]
1284pub enum SlideLayout {
1285    /// Title/opening slide with large headline.
1286    Cover,
1287    /// Title-only slide.
1288    Title,
1289    /// Section divider.
1290    Section,
1291    /// Default body slide — bulleted content.
1292    #[default]
1293    Bullets,
1294    /// Card grid (maps to `::features`).
1295    Cards,
1296    /// Side-by-side comparison (maps to `::comparison`).
1297    Compare,
1298    /// Single big statistic (maps to `::stats`).
1299    Stat,
1300    /// Pull-quote slide (maps to `::quote`).
1301    Quote,
1302    /// Live-demo / terminal slide.
1303    Demo,
1304    /// Full-bleed image.
1305    Image,
1306    /// Two-column split.
1307    Two,
1308    /// Code-focused slide (monospace, full-bleed code block).
1309    Code,
1310    /// Empty canvas — content controls itself.
1311    Blank,
1312}
1313
1314impl SlideLayout {
1315    /// Parse a layout name from a `::slide` `layout:` attribute.
1316    pub fn from_name(s: &str) -> Option<SlideLayout> {
1317        match s.trim().to_ascii_lowercase().as_str() {
1318            "cover" => Some(SlideLayout::Cover),
1319            "title" => Some(SlideLayout::Title),
1320            "section" => Some(SlideLayout::Section),
1321            "bullets" | "default" => Some(SlideLayout::Bullets),
1322            "cards" => Some(SlideLayout::Cards),
1323            "compare" | "comparison" => Some(SlideLayout::Compare),
1324            "stat" | "stats" => Some(SlideLayout::Stat),
1325            "quote" => Some(SlideLayout::Quote),
1326            "demo" => Some(SlideLayout::Demo),
1327            "image" => Some(SlideLayout::Image),
1328            "two" | "split" | "two-column" | "two-col" | "twocolumn" => Some(SlideLayout::Two),
1329            "code" => Some(SlideLayout::Code),
1330            "blank" => Some(SlideLayout::Blank),
1331            _ => None,
1332        }
1333    }
1334
1335    /// The CSS class suffix for this layout (e.g. `cover` → `slide cover`).
1336    pub fn css_class(self) -> &'static str {
1337        match self {
1338            SlideLayout::Cover => "cover",
1339            SlideLayout::Title => "title",
1340            SlideLayout::Section => "section",
1341            SlideLayout::Bullets => "bullets",
1342            SlideLayout::Cards => "cards",
1343            SlideLayout::Compare => "compare",
1344            SlideLayout::Stat => "stat",
1345            SlideLayout::Quote => "quote",
1346            SlideLayout::Demo => "demo",
1347            SlideLayout::Image => "image",
1348            SlideLayout::Two => "two",
1349            SlideLayout::Code => "code",
1350            SlideLayout::Blank => "blank",
1351        }
1352    }
1353}
1354
1355/// A single field in a `Form` block.
1356#[derive(Debug, Clone, Serialize, Deserialize)]
1357pub struct FormField {
1358    pub label: String,
1359    pub name: String,
1360    pub field_type: FormFieldType,
1361    pub required: bool,
1362    pub placeholder: Option<String>,
1363    pub options: Vec<String>,
1364}
1365
1366/// Form field input types.
1367#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1368#[serde(rename_all = "lowercase")]
1369pub enum FormFieldType {
1370    Text,
1371    Email,
1372    Tel,
1373    Date,
1374    Number,
1375    Password,
1376    Select,
1377    Textarea,
1378}
1379
1380/// A single item in a `Gallery` block.
1381#[derive(Debug, Clone, Serialize, Deserialize)]
1382pub struct GalleryItem {
1383    pub src: String,
1384    pub caption: Option<String>,
1385    pub alt: Option<String>,
1386    pub category: Option<String>,
1387}
1388
1389/// A section within a `Footer` block.
1390#[derive(Debug, Clone, Serialize, Deserialize)]
1391pub struct FooterSection {
1392    pub heading: String,
1393    pub links: Vec<NavItem>,
1394}
1395
1396/// A social media link within a `Footer` block.
1397#[derive(Debug, Clone, Serialize, Deserialize)]
1398pub struct SocialLink {
1399    pub platform: String,
1400    pub href: String,
1401}
1402
1403/// A button within a `Hero` or `Banner` block.
1404#[derive(Debug, Clone, Serialize, Deserialize)]
1405pub struct HeroButton {
1406    pub label: String,
1407    pub href: String,
1408    pub primary: bool,
1409    /// `{external}` — the button opens in a new tab (`target="_blank" rel="noopener"`).
1410    /// Combines with primary as `{primary external}`.
1411    #[serde(default)]
1412    pub external: bool,
1413}
1414
1415/// A labelled group of product link-cards within a `ProductGrid`.
1416#[derive(Debug, Clone, Serialize, Deserialize)]
1417pub struct ProductGroup {
1418    /// Group heading (e.g. "Platforms"). Empty when the grid is ungrouped.
1419    pub label: Option<String>,
1420    pub items: Vec<ProductItem>,
1421    /// `{cols=N}` on the heading line (tiles mode): max columns for this
1422    /// group's row, clamped 1–3. None → the 2-col default.
1423    #[serde(default, skip_serializing_if = "Option::is_none")]
1424    pub cols: Option<u8>,
1425}
1426
1427/// A single product link-card: emblem + name + tagline + link.
1428#[derive(Debug, Clone, Serialize, Deserialize)]
1429pub struct ProductItem {
1430    pub name: String,
1431    pub href: String,
1432    /// Emblem image src. Optional — cards render without an image when absent.
1433    pub emblem: Option<String>,
1434    pub tagline: Option<String>,
1435    /// Primary CTA override (`[Label](href)`, first of two trailing link
1436    /// fields): replaces the default "Learn more" pill (tiles mode).
1437    #[serde(default, skip_serializing_if = "Option::is_none")]
1438    pub cta1_label: Option<String>,
1439    #[serde(default, skip_serializing_if = "Option::is_none")]
1440    pub cta1_href: Option<String>,
1441    /// Secondary CTA (`[Label](href)` trailing pipe field): renders as the
1442    /// second, outline pill in the tile's CTA row (tiles mode).
1443    #[serde(default, skip_serializing_if = "Option::is_none")]
1444    pub cta2_label: Option<String>,
1445    #[serde(default, skip_serializing_if = "Option::is_none")]
1446    pub cta2_href: Option<String>,
1447    /// Tile background spec (5th pipe field, raw author value):
1448    /// `image:<src>` | `color:<css-color>` | `gradient:<css-gradient>` |
1449    /// `transparent` (default when absent). A trailing ` dark` token flags a
1450    /// dark background → the tile renders light text.
1451    #[serde(default, skip_serializing_if = "Option::is_none")]
1452    pub bg: Option<String>,
1453}
1454
1455/// A card within a `PostGrid` block: a blog/news/events index entry.
1456#[derive(Debug, Clone, Serialize, Deserialize)]
1457pub struct PostItem {
1458    pub title: String,
1459    pub href: String,
1460    /// Meta line above the title (e.g. "Category · Date").
1461    pub meta: Option<String>,
1462    /// One-line excerpt under the title.
1463    pub excerpt: Option<String>,
1464    /// Optional lead image src.
1465    pub image: Option<String>,
1466    /// Open in a new tab (renders `target="_blank" rel="noopener"`).
1467    pub external: bool,
1468}
1469
1470/// A card within a `Features` block.
1471#[derive(Debug, Clone, Serialize, Deserialize)]
1472pub struct FeatureCard {
1473    pub title: String,
1474    pub icon: Option<String>,
1475    pub body: String,
1476    pub link_label: Option<String>,
1477    pub link_href: Option<String>,
1478}
1479
1480/// A step within a `Steps` block.
1481#[derive(Debug, Clone, Serialize, Deserialize)]
1482pub struct StepItem {
1483    pub title: String,
1484    pub time: Option<String>,
1485    pub body: String,
1486}
1487
1488/// A stat within a `Stats` block.
1489#[derive(Debug, Clone, Serialize, Deserialize)]
1490pub struct StatItem {
1491    pub value: String,
1492    pub label: String,
1493    pub color: Option<String>,
1494}
1495
1496/// A TOC entry within a `Toc` block.
1497#[derive(Debug, Clone, Serialize, Deserialize)]
1498pub struct TocEntry {
1499    pub text: String,
1500    pub id: String,
1501    pub level: u32,
1502}
1503
1504/// An item within a `BeforeAfter` block.
1505#[derive(Debug, Clone, Serialize, Deserialize)]
1506pub struct BeforeAfterItem {
1507    pub label: String,
1508    pub detail: String,
1509}
1510
1511/// A step within a `Pipeline` block.
1512#[derive(Debug, Clone, Serialize, Deserialize)]
1513pub struct PipelineStep {
1514    pub label: String,
1515    pub description: Option<String>,
1516}
1517
1518// ----- Infrastructure manifest supporting types -----
1519
1520/// An environment variable entry within an `InfraEnv` block.
1521#[derive(Debug, Clone, Serialize, Deserialize)]
1522pub struct EnvEntry {
1523    pub name: String,
1524    pub default_value: Option<String>,
1525}
1526
1527/// A smoke test check: HTTP method, path, expected status code.
1528#[derive(Debug, Clone, Serialize, Deserialize)]
1529pub struct SmokeCheck {
1530    pub method: String,
1531    pub path: String,
1532    pub expected: u16,
1533}
1534
1535/// A domain entry with optional description.
1536#[derive(Debug, Clone, Serialize, Deserialize)]
1537pub struct DomainEntry {
1538    pub domain: String,
1539    pub description: Option<String>,
1540}
1541
1542/// A shared crate dependency entry.
1543#[derive(Debug, Clone, Serialize, Deserialize)]
1544pub struct CrateEntry {
1545    pub name: String,
1546    pub source: Option<String>,
1547    pub features: Option<String>,
1548}
1549
1550/// A named volume mount.
1551#[derive(Debug, Clone, Serialize, Deserialize)]
1552pub struct VolumeEntry {
1553    pub name: String,
1554    pub mount: String,
1555}
1556
1557// ----- App description language supporting types -----
1558
1559/// Display style for a `List` block.
1560#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1561#[serde(rename_all = "lowercase")]
1562pub enum ListDisplay {
1563    Card,
1564    Table,
1565    Compact,
1566}
1567
1568/// A filter declared inside a `List` block.
1569#[derive(Debug, Clone, Serialize, Deserialize)]
1570pub struct ListFilter {
1571    pub field: String,
1572}
1573
1574/// Sort specification: field name + direction.
1575#[derive(Debug, Clone, Serialize, Deserialize)]
1576pub struct SortSpec {
1577    pub field: String,
1578    pub descending: bool,
1579}
1580
1581/// HTTP method for `Action` blocks.
1582#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1583#[serde(rename_all = "lowercase")]
1584pub enum HttpMethod {
1585    Get,
1586    Post,
1587    Put,
1588    Patch,
1589    Delete,
1590}
1591
1592/// A filter field in a `FilterBar` block.
1593#[derive(Debug, Clone, Serialize, Deserialize)]
1594pub struct FilterField {
1595    pub label: String,
1596    pub name: String,
1597    pub options: Vec<String>,
1598}
1599
1600/// A product line in a `Store` block.
1601#[derive(Debug, Clone, Serialize, Deserialize)]
1602pub struct StoreItem {
1603    pub name: String,
1604    /// Numeric price as authored (e.g. "48", "12.50"); the widget parses it.
1605    pub price: String,
1606    /// One-line description shown under the name. Optional.
1607    pub blurb: Option<String>,
1608    /// Corner badge (e.g. "Bestseller", "New"). Optional.
1609    pub badge: Option<String>,
1610    /// Category for the filter chips. `None` groups under "All".
1611    pub category: Option<String>,
1612}
1613
1614/// A bookable service in a `Booking` block (e.g. "60-min Strategy Call").
1615#[derive(Debug, Clone, Serialize, Deserialize)]
1616pub struct BookingService {
1617    pub name: String,
1618    /// Free-text duration (e.g. "60 min"). Optional.
1619    pub duration: Option<String>,
1620    /// Free-text price (e.g. "$120", "Free"). Optional.
1621    pub price: Option<String>,
1622}
1623
1624/// One day's availability in a `Booking` block. An empty `slots` (or the
1625/// literal `full`) marks the day as unavailable.
1626#[derive(Debug, Clone, Serialize, Deserialize)]
1627pub struct BookingDay {
1628    /// ISO date `YYYY-MM-DD`.
1629    pub date: String,
1630    /// Selectable time-slot labels (e.g. "9:00 AM"); empty = unavailable.
1631    pub slots: Vec<String>,
1632}
1633
1634/// Chart visualization type.
1635#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1636#[serde(rename_all = "lowercase")]
1637pub enum ChartType {
1638    Line,
1639    Bar,
1640    Pie,
1641    Area,
1642    /// Numeric x/y point cloud (one or more series).
1643    Scatter,
1644    /// Pie with an inner radius (ring).
1645    Donut,
1646    /// Bars stacked per category across series.
1647    StackedBar,
1648    /// Polygon per series over N labelled axes.
1649    Radar,
1650}
1651
1652/// One named numeric series of a [`ChartData`] dataset. `values` is aligned
1653/// position-for-position with the owning dataset's `categories`.
1654#[derive(Debug, Clone, Serialize, Deserialize)]
1655pub struct ChartSeries {
1656    /// Series name (shown in the legend).
1657    pub name: String,
1658    /// y-values, one per category. Missing/non-numeric cells parse to `0.0`.
1659    pub values: Vec<f64>,
1660}
1661
1662/// Inline chart dataset parsed from a `::chart` block body — a shared category
1663/// axis plus one or more named series. When a `Chart` block has `data: None`
1664/// it falls back to the live-data mount point (`source=`) / static placeholder.
1665///
1666/// For cartesian charts (line/area/bar/stacked-bar) `categories` are x-axis
1667/// labels; for scatter they are the numeric x-values (kept as strings); for
1668/// radar they are the axis labels; for pie/donut they are the slice labels and
1669/// only the first series is used.
1670#[derive(Debug, Clone, Serialize, Deserialize)]
1671pub struct ChartData {
1672    /// Category / x-axis / slice / axis labels (one per data point).
1673    pub categories: Vec<String>,
1674    /// One or more named series of values aligned to `categories`.
1675    pub series: Vec<ChartSeries>,
1676}
1677
1678// ----- App spec supporting types -----
1679
1680/// A field within a `Model` block.
1681#[derive(Debug, Clone, Serialize, Deserialize)]
1682pub struct ModelField {
1683    pub name: String,
1684    pub field_type: ModelFieldType,
1685    pub constraints: Vec<FieldConstraint>,
1686}
1687
1688/// Data types for model fields.
1689#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1690#[serde(rename_all = "lowercase")]
1691pub enum ModelFieldType {
1692    Uuid,
1693    String,
1694    Int,
1695    Float,
1696    Bool,
1697    Datetime,
1698    Text,
1699    Json,
1700    /// Monetary value stored as i64 cents (e.g. 1999 = $19.99).
1701    Money,
1702    /// Image URL/path — stored as String, triggers upload codegen.
1703    Image,
1704    /// Email address — stored as String, auto-capped at 254 chars per RFC 5321.
1705    Email,
1706    /// URL — stored as String, auto-capped at 2048 chars.
1707    Url,
1708    /// Enum with named variants.
1709    Enum(Vec<String>),
1710    /// Foreign key reference to another model.
1711    Ref(String),
1712}
1713
1714/// Constraints on a model field.
1715#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1716#[serde(rename_all = "lowercase")]
1717pub enum FieldConstraint {
1718    Primary,
1719    Auto,
1720    Required,
1721    Optional,
1722    Unique,
1723    Max(u32),
1724    Min(u32),
1725    Default(String),
1726    /// Database index hint for query performance.
1727    Index,
1728}
1729
1730/// Authentication provider type.
1731#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1732#[serde(rename_all = "lowercase")]
1733pub enum AuthProvider {
1734    Email,
1735    OAuth,
1736    ApiKey,
1737    Token,
1738}
1739
1740/// An event in a `Binding` block (on_create, on_update, etc.).
1741#[derive(Debug, Clone, Serialize, Deserialize)]
1742pub struct BindingEvent {
1743    pub event: String,
1744    pub action: String,
1745}
1746
1747// ----- App format supporting types -----
1748
1749/// A field within a `Schema` block.
1750#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1751pub struct SchemaField {
1752    pub name: String,
1753    pub field_type: ModelFieldType,
1754    pub constraints: Vec<FieldConstraint>,
1755}
1756
1757/// A crate dependency within a `Use` block.
1758#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1759pub struct CrateDep {
1760    pub name: String,
1761    pub version: Option<String>,
1762    pub features: Vec<String>,
1763}
1764
1765/// An environment variable declaration within an `AppEnv` block.
1766#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1767pub struct EnvVar {
1768    pub name: String,
1769    pub description: Option<String>,
1770    pub required: bool,
1771}
1772
1773/// Inline extension found within text content.
1774#[derive(Debug, Clone, Serialize, Deserialize)]
1775pub enum InlineExt {
1776    Evidence {
1777        tier: Option<u8>,
1778        source: Option<String>,
1779        text: String,
1780    },
1781    Status {
1782        value: String,
1783    },
1784}
1785
1786/// Ordered map of attribute key-value pairs.
1787pub type Attrs = BTreeMap<String, AttrValue>;
1788
1789/// A value inside a block directive attribute.
1790#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1791#[serde(untagged)]
1792pub enum AttrValue {
1793    String(String),
1794    Number(f64),
1795    Bool(bool),
1796    Null,
1797}
1798
1799/// Source location of a block in the original document.
1800#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1801pub struct Span {
1802    /// 1-based starting line number.
1803    pub start_line: usize,
1804    /// 1-based ending line number (inclusive).
1805    pub end_line: usize,
1806    /// 0-based byte offset of the first character.
1807    pub start_offset: usize,
1808    /// 0-based byte offset past the last character.
1809    pub end_offset: usize,
1810}
1811
1812impl Span {
1813    /// A zero-valued span for programmatically constructed blocks that have no
1814    /// source location.
1815    pub const SYNTHETIC: Span = Span {
1816        start_line: 0,
1817        end_line: 0,
1818        start_offset: 0,
1819        end_offset: 0,
1820    };
1821}
1822
1823#[cfg(test)]
1824mod doc_type_format_tests {
1825    use super::*;
1826
1827    fn parse_fm(yaml: &str) -> FrontMatter {
1828        serde_yaml::from_str::<FrontMatter>(yaml).expect("front matter should parse")
1829    }
1830
1831    // ----- L1/L2: new DocType variants deserialize from `type:` -----
1832
1833    #[test]
1834    fn new_doc_types_deserialize() {
1835        assert_eq!(parse_fm("type: presentation").doc_type, Some(DocType::Presentation));
1836        assert_eq!(parse_fm("type: web").doc_type, Some(DocType::Web));
1837        assert_eq!(parse_fm("type: website").doc_type, Some(DocType::Website));
1838        assert_eq!(parse_fm("type: paper").doc_type, Some(DocType::Paper));
1839        assert_eq!(parse_fm("type: report").doc_type, Some(DocType::Report));
1840    }
1841
1842    #[test]
1843    fn existing_doc_types_unchanged() {
1844        assert_eq!(parse_fm("type: deck").doc_type, Some(DocType::Deck));
1845        assert_eq!(parse_fm("type: slides").doc_type, Some(DocType::Slides));
1846        assert_eq!(parse_fm("type: doc").doc_type, Some(DocType::Doc));
1847    }
1848
1849    // ----- L1/L2: Format values + aliases + missing -----
1850
1851    #[test]
1852    fn format_values_deserialize() {
1853        for (s, want) in [
1854            ("ieee", Format::Ieee),
1855            ("acm", Format::Acm),
1856            ("article", Format::Article),
1857            ("mla", Format::Mla),
1858            ("apa", Format::Apa),
1859            ("chicago", Format::Chicago),
1860        ] {
1861            assert_eq!(parse_fm(&format!("format: {s}")).format, Some(want));
1862        }
1863    }
1864
1865    #[test]
1866    fn format_aliases_are_case_insensitive() {
1867        assert_eq!(parse_fm("format: IEEE").format, Some(Format::Ieee));
1868        assert_eq!(parse_fm("format: ACM").format, Some(Format::Acm));
1869        assert_eq!(parse_fm("format: MLA").format, Some(Format::Mla));
1870        assert_eq!(parse_fm("format: APA").format, Some(Format::Apa));
1871        assert_eq!(parse_fm("format: Chicago").format, Some(Format::Chicago));
1872        assert_eq!(parse_fm("format: Article").format, Some(Format::Article));
1873    }
1874
1875    #[test]
1876    fn format_missing_defaults_to_none() {
1877        assert_eq!(parse_fm("type: paper").format, None);
1878        assert_eq!(parse_fm("title: Hello").format, None);
1879    }
1880
1881    // ----- L3 drift: every Format reaches a RenderProfile -----
1882
1883    #[test]
1884    fn every_format_maps_to_a_render_profile() {
1885        for f in [
1886            Format::Ieee,
1887            Format::Acm,
1888            Format::Article,
1889            Format::Mla,
1890            Format::Apa,
1891            Format::Chicago,
1892        ] {
1893            // Papers carry the format through.
1894            assert_eq!(
1895                render_profile(Some(DocType::Paper), Some(f)),
1896                RenderProfile::Paper(f)
1897            );
1898            // Reports carry the format through.
1899            assert_eq!(
1900                render_profile(Some(DocType::Report), Some(f)),
1901                RenderProfile::Report(f)
1902            );
1903        }
1904    }
1905
1906    #[test]
1907    fn render_profile_mapping_is_total_and_stable() {
1908        // None → Document.
1909        assert_eq!(render_profile(None, None), RenderProfile::Document);
1910        // Web == Website profile.
1911        assert_eq!(render_profile(Some(DocType::Web), None), RenderProfile::Web);
1912        assert_eq!(render_profile(Some(DocType::Website), None), RenderProfile::Web);
1913        // Deck / Slides / Presentation → Presentation.
1914        assert_eq!(
1915            render_profile(Some(DocType::Deck), None),
1916            RenderProfile::Presentation
1917        );
1918        assert_eq!(
1919            render_profile(Some(DocType::Slides), None),
1920            RenderProfile::Presentation
1921        );
1922        assert_eq!(
1923            render_profile(Some(DocType::Presentation), None),
1924            RenderProfile::Presentation
1925        );
1926        // Defaults when format omitted.
1927        assert_eq!(
1928            render_profile(Some(DocType::Paper), None),
1929            RenderProfile::Paper(Format::Article)
1930        );
1931        assert_eq!(
1932            render_profile(Some(DocType::Report), None),
1933            RenderProfile::Report(Format::Mla)
1934        );
1935        // Ordinary docs → Document.
1936        for dt in [DocType::Doc, DocType::Guide, DocType::Plan, DocType::App] {
1937            assert_eq!(render_profile(Some(dt), None), RenderProfile::Document);
1938        }
1939    }
1940}