Skip to main content

cortexkit_rich_ui_syntax/
lib.rs

1//! A bounded syntax front end for rich UI language v1.
2//!
3//! Reference discovery walks the returned syntax tree; it never scans strings
4//! from data, Markdown destinations, or failed tags a second time.
5mod expression;
6mod markdown;
7mod parser;
8#[cfg(feature = "test-vectors")]
9pub mod vectors;
10
11pub use parser::{parse, parse_bytes, StreamParser};
12
13/// Identifies the rich UI source dialect accepted by this parser.
14pub const DIALECT: &str = "rich-ui/1";
15pub const PROGRAM_BYTES: usize = 64 * 1024;
16pub const REPLY_BYTES: usize = 256 * 1024;
17pub const AST_NODES: usize = 8192;
18pub const BLOCKS: usize = 2048;
19pub const OUTPUT_SCALARS: usize = 65_536;
20pub const REFERENCES: usize = 64;
21pub const HANDLES: usize = 64;
22pub const DECLARATIONS: usize = 256;
23pub const EXPRESSION_DEPTH: usize = 64;
24pub const TAG_DEPTH: usize = 32;
25pub const MARKDOWN_DEPTH: usize = 16;
26pub const EMPHASIS_DEPTH: usize = 64;
27pub const DIAGNOSTICS: usize = 50;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum Mode {
31    Reply,
32    Program,
33}
34
35/// Original UTF-8 byte offsets, including a leading BOM if one was supplied.
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub struct Span {
38    pub start: usize,
39    pub end: usize,
40}
41impl Span {
42    pub fn new(start: usize, end: usize) -> Self {
43        Self { start, end }
44    }
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct Diagnostic {
49    pub code: &'static str,
50    pub span: Span,
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct Node {
55    pub span: Span,
56    pub kind: Kind,
57    pub children: Vec<Node>,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub enum Kind {
62    Markdown(Markdown),
63    Text(String),
64    /// Opaque source, not reparsed as Markdown or expressions.
65    Literal(String),
66    Reference {
67        n: String,
68    },
69    Handle {
70        kind: String,
71        path: String,
72    },
73    Tag {
74        name: String,
75        block: bool,
76        props: Vec<Prop>,
77    },
78    Interpolation {
79        block: bool,
80    },
81    Declaration {
82        name: String,
83        state: bool,
84    },
85    Expression(Expression),
86}
87
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct Prop {
90    pub name: String,
91    pub span: Span,
92    pub value: Node,
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
96pub enum Expression {
97    Number(String),
98    String(String),
99    Boolean(bool),
100    Null,
101    Name(String),
102    State(String),
103    Group,
104    List,
105    Record(Vec<String>),
106    Call(String),
107    Unary(String),
108    Binary(String),
109    Conditional,
110    Member(String),
111    Index,
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
115pub enum Markdown {
116    Paragraph,
117    Heading(u8),
118    ThematicBreak,
119    CodeBlock {
120        language: String,
121        text: String,
122    },
123    Code(String),
124    Quote,
125    List {
126        start: Option<u64>,
127    },
128    Item,
129    Task(bool),
130    Table,
131    TableHead,
132    TableRow,
133    TableCell,
134    Emphasis,
135    Strong,
136    Strikethrough,
137    /// An inert syntax address, not a validated or authorized navigation target.
138    Link {
139        destination: String,
140        title: String,
141        image: bool,
142    },
143    SoftBreak,
144    HardBreak,
145}
146
147#[derive(Clone, Debug, PartialEq, Eq)]
148pub struct Document {
149    pub mode: Mode,
150    pub nodes: Vec<Node>,
151    pub diagnostics: Vec<Diagnostic>,
152    pub suppressed_diagnostics: usize,
153    pub parsed_bytes: usize,
154    pub shortened: bool,
155    /// A whole-source limit selected bounded literal fallback; no refs survive.
156    pub fallback: bool,
157}
158
159#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct Reference {
161    /// Canonical decimal, not a machine integer: the grammar sets no integer cap.
162    pub n: String,
163    pub span: Span,
164    /// True for every occurrence after the first 64 in source order. A caller
165    /// resolving references copies a tool's output only for occurrences where
166    /// this is false.
167    pub excess: bool,
168}
169
170#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct ScanResult {
172    pub references: Vec<Reference>,
173    pub diagnostics: Vec<Diagnostic>,
174    pub suppressed_diagnostics: usize,
175    pub parsed_bytes: usize,
176    pub shortened: bool,
177    pub fallback: bool,
178}
179
180impl Document {
181    /// Lists all reference nodes, including repeated and over-budget occurrences.
182    pub fn references(&self) -> Vec<Reference> {
183        fn visit(node: &Node, refs: &mut Vec<Reference>) {
184            if let Kind::Reference { n } = &node.kind {
185                refs.push(Reference {
186                    n: n.clone(),
187                    span: node.span,
188                    excess: false,
189                });
190            }
191            if let Kind::Tag { props, .. } = &node.kind {
192                for prop in props {
193                    visit(&prop.value, refs);
194                }
195            }
196            for child in &node.children {
197                visit(child, refs);
198            }
199        }
200        let mut refs = Vec::new();
201        for node in &self.nodes {
202            visit(node, &mut refs);
203        }
204        refs.sort_by_key(|r| r.span.start);
205        for (i, reference) in refs.iter_mut().enumerate() {
206            reference.excess = i >= REFERENCES;
207        }
208        refs
209    }
210}
211
212pub fn scan_references(source: &str, mode: Mode) -> ScanResult {
213    let doc = parse(source, mode);
214    ScanResult {
215        references: doc.references(),
216        diagnostics: doc.diagnostics,
217        suppressed_diagnostics: doc.suppressed_diagnostics,
218        parsed_bytes: doc.parsed_bytes,
219        shortened: doc.shortened,
220        fallback: doc.fallback,
221    }
222}
223
224/// Checks a `token` argument slot without trimming, unescaping, or substring search.
225pub fn check_token_slot(value: &str) -> Option<Reference> {
226    if value.len() > REPLY_BYTES {
227        return None;
228    }
229    let end = reference_end(value, 0)?;
230    (end == value.len()).then(|| Reference {
231        n: value[5..end - 1].to_owned(),
232        span: Span::new(0, end),
233        excess: false,
234    })
235}
236
237pub(crate) fn reference_end(source: &str, start: usize) -> Option<usize> {
238    let tail = source.get(start..)?;
239    let digits = tail.strip_prefix("$$tc(")?;
240    let count = digits.bytes().take_while(u8::is_ascii_digit).count();
241    if count == 0
242        || (count > 1 && digits.starts_with('0'))
243        || digits.as_bytes().get(count) != Some(&b')')
244    {
245        return None;
246    }
247    Some(start + 6 + count)
248}