1mod expression;
6mod markdown;
7mod parser;
8mod stream;
9#[cfg(feature = "test-vectors")]
10pub mod vectors;
11
12pub use parser::{parse, parse_bytes};
13pub use stream::{
14 Display, DisplayNode, Revision, StreamParser, Work, PENDING_REFERENCE_CHARS,
15 STREAM_CONTEXT_BYTES, TAG_OPENING_BYTES,
16};
17
18pub const DIALECT: &str = "rich-ui/1";
20pub const PROGRAM_BYTES: usize = 64 * 1024;
21pub const REPLY_BYTES: usize = 256 * 1024;
22pub const AST_NODES: usize = 8192;
23pub const BLOCKS: usize = 2048;
24pub const OUTPUT_SCALARS: usize = 65_536;
25pub const REFERENCES: usize = 64;
26pub const HANDLES: usize = 64;
27pub const DECLARATIONS: usize = 256;
28pub const EXPRESSION_DEPTH: usize = 64;
29pub const TAG_DEPTH: usize = 32;
30pub const MARKDOWN_DEPTH: usize = 16;
31pub const EMPHASIS_DEPTH: usize = 64;
32pub const DIAGNOSTICS: usize = 50;
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum Mode {
63 Reply,
64 Program,
65}
66
67#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
69pub struct Span {
70 pub start: usize,
71 pub end: usize,
72}
73impl Span {
74 pub fn new(start: usize, end: usize) -> Self {
75 Self { start, end }
76 }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
119#[non_exhaustive]
120pub enum DiagnosticCode {
121 Syntax,
123 UnterminatedString,
126 BadEscape,
129 ReservedName,
131 DuplicateName,
133 DeclarationIncomplete,
136 LiteralRequired,
139 LimitDeclarations,
141 DuplicateProp,
143 DuplicateKey,
145 MismatchedClose,
147 UnclosedTag,
149 ReplyForbidden,
151 LimitTags,
153 Nesting,
155 NonAssoc,
157 MixedCoalesce,
159 NumberLiteral,
161 ParseWork,
163 LimitAst,
165 LimitBlocks,
167 LimitText,
169 SourceTooLarge,
171 ReferenceLimit,
174 Encoding,
176}
177
178impl DiagnosticCode {
179 pub const fn as_str(self) -> &'static str {
181 match self {
182 Self::Syntax => "E_SYNTAX",
183 Self::UnterminatedString => "E_UNTERMINATED_STRING",
184 Self::BadEscape => "E_BAD_ESCAPE",
185 Self::ReservedName => "E_RESERVED_NAME",
186 Self::DuplicateName => "E_DUPLICATE_NAME",
187 Self::DeclarationIncomplete => "E_DECLARATION_INCOMPLETE",
188 Self::LiteralRequired => "E_LITERAL_REQUIRED",
189 Self::LimitDeclarations => "E_LIMIT_DECLARATIONS",
190 Self::DuplicateProp => "E_DUPLICATE_PROP",
191 Self::DuplicateKey => "E_DUPLICATE_KEY",
192 Self::MismatchedClose => "E_MISMATCHED_CLOSE",
193 Self::UnclosedTag => "E_UNCLOSED_TAG",
194 Self::ReplyForbidden => "E_REPLY_FORBIDDEN",
195 Self::LimitTags => "E_LIMIT_TAGS",
196 Self::Nesting => "E_NESTING",
197 Self::NonAssoc => "E_NONASSOC",
198 Self::MixedCoalesce => "E_MIXED_COALESCE",
199 Self::NumberLiteral => "E_NUMBER_LITERAL",
200 Self::ParseWork => "E_PARSE_WORK",
201 Self::LimitAst => "E_LIMIT_AST",
202 Self::LimitBlocks => "E_LIMIT_BLOCKS",
203 Self::LimitText => "E_LIMIT_TEXT",
204 Self::SourceTooLarge => "E_SOURCE_TOO_LARGE",
205 Self::ReferenceLimit => "E_REFERENCE_LIMIT",
206 Self::Encoding => "W_ENCODING",
207 }
208 }
209}
210
211impl std::fmt::Display for DiagnosticCode {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 f.write_str(self.as_str())
214 }
215}
216
217#[derive(Clone, Debug, PartialEq, Eq)]
218pub struct Diagnostic {
219 pub code: DiagnosticCode,
220 pub span: Span,
221 pub subject: Option<String>,
229}
230
231#[derive(Clone, Debug, PartialEq, Eq)]
232pub struct Node {
233 pub span: Span,
234 pub kind: Kind,
235 pub children: Vec<Node>,
236}
237
238#[derive(Clone, Debug, PartialEq, Eq)]
239#[non_exhaustive]
240pub enum Kind {
241 Markdown(Markdown),
242 Text(String),
243 Literal(String),
245 Reference {
246 n: String,
247 },
248 Handle {
249 kind: String,
250 path: String,
251 },
252 Tag {
253 name: String,
254 block: bool,
255 props: Vec<Prop>,
256 },
257 Interpolation {
258 block: bool,
259 },
260 Declaration {
261 name: String,
262 state: bool,
263 },
264 Expression(Expression),
265}
266
267#[derive(Clone, Debug, PartialEq, Eq)]
268pub struct Prop {
269 pub name: String,
270 pub span: Span,
271 pub value: Node,
272}
273
274#[derive(Clone, Debug, PartialEq, Eq)]
275#[non_exhaustive]
276pub enum Expression {
277 Number(String),
278 String(String),
279 Boolean(bool),
280 Null,
281 Name(String),
282 State(String),
283 Group,
284 List,
285 Record(Vec<String>),
286 Call(String),
287 Unary(String),
288 Binary(String),
289 Conditional,
290 Member(String),
291 Index,
292}
293
294#[derive(Clone, Debug, PartialEq, Eq)]
295#[non_exhaustive]
296pub enum Markdown {
297 Paragraph,
298 Heading(u8),
299 ThematicBreak,
300 CodeBlock {
301 language: String,
302 text: String,
303 },
304 Code(String),
305 Quote,
306 List {
307 start: Option<u64>,
308 },
309 Item,
310 Task(bool),
311 Table,
312 TableHead,
313 TableRow,
314 TableCell,
315 Emphasis,
316 Strong,
317 Strikethrough,
318 Link {
320 destination: String,
321 title: String,
322 image: bool,
323 },
324 SoftBreak,
325 HardBreak,
326}
327
328#[derive(Clone, Debug, PartialEq, Eq)]
329pub struct Document {
330 pub mode: Mode,
331 pub nodes: Vec<Node>,
332 pub diagnostics: Vec<Diagnostic>,
333 pub suppressed_diagnostics: usize,
334 pub parsed_bytes: usize,
335 pub shortened: bool,
336 pub fallback: bool,
338}
339
340#[derive(Clone, Debug, PartialEq, Eq)]
341pub struct Reference {
342 pub n: String,
344 pub span: Span,
345 pub excess: bool,
349}
350
351#[derive(Clone, Debug, PartialEq, Eq)]
352pub struct ScanResult {
353 pub references: Vec<Reference>,
354 pub diagnostics: Vec<Diagnostic>,
355 pub suppressed_diagnostics: usize,
356 pub parsed_bytes: usize,
357 pub shortened: bool,
358 pub fallback: bool,
359}
360
361impl Document {
362 pub fn references(&self) -> Vec<Reference> {
364 fn visit(node: &Node, refs: &mut Vec<Reference>) {
365 if let Kind::Reference { n } = &node.kind {
366 refs.push(Reference {
367 n: n.clone(),
368 span: node.span,
369 excess: false,
370 });
371 }
372 if let Kind::Tag { props, .. } = &node.kind {
373 for prop in props {
374 visit(&prop.value, refs);
375 }
376 }
377 for child in &node.children {
378 visit(child, refs);
379 }
380 }
381 let mut refs = Vec::new();
382 for node in &self.nodes {
383 visit(node, &mut refs);
384 }
385 refs.sort_by_key(|r| r.span.start);
386 for (i, reference) in refs.iter_mut().enumerate() {
387 reference.excess = i >= REFERENCES;
388 }
389 refs
390 }
391}
392
393pub fn scan_references(source: &str, mode: Mode) -> ScanResult {
394 let doc = parse(source, mode);
395 ScanResult {
396 references: doc.references(),
397 diagnostics: doc.diagnostics,
398 suppressed_diagnostics: doc.suppressed_diagnostics,
399 parsed_bytes: doc.parsed_bytes,
400 shortened: doc.shortened,
401 fallback: doc.fallback,
402 }
403}
404
405pub fn check_token_slot(value: &str) -> Option<Reference> {
407 if value.len() > REPLY_BYTES {
408 return None;
409 }
410 let end = reference_end(value, 0)?;
411 (end == value.len()).then(|| Reference {
412 n: value[5..end - 1].to_owned(),
413 span: Span::new(0, end),
414 excess: false,
415 })
416}
417
418pub(crate) fn reference_end(source: &str, start: usize) -> Option<usize> {
419 let tail = source.get(start..)?;
420 let digits = tail.strip_prefix("$$tc(")?;
421 let count = digits.bytes().take_while(u8::is_ascii_digit).count();
422 if count == 0
423 || (count > 1 && digits.starts_with('0'))
424 || digits.as_bytes().get(count) != Some(&b')')
425 {
426 return None;
427 }
428 Some(start + 6 + count)
429}