Skip to main content

rdx_ast/
lib.rs

1use rkyv::Archive;
2use serde::{Deserialize, Serialize};
3
4/// rkyv wrapper that stores serde_json types as their JSON string representation.
5mod rkyv_json {
6    use rkyv::rancor::Fallible;
7    use rkyv::string::ArchivedString;
8    use rkyv::with::{ArchiveWith, DeserializeWith, SerializeWith};
9    use rkyv::{Archive, Place};
10
11    pub struct AsJsonString;
12
13    impl<T: serde::Serialize> ArchiveWith<T> for AsJsonString {
14        type Archived = ArchivedString;
15        type Resolver = <String as Archive>::Resolver;
16
17        fn resolve_with(field: &T, resolver: Self::Resolver, out: Place<Self::Archived>) {
18            // Safety: serde_json::to_string cannot fail for types that already implement Serialize
19            // (no IO, no map-key errors). An unwrap is appropriate here.
20            let json = serde_json::to_string(field).expect("serde_json::to_string failed");
21            ArchivedString::resolve_from_str(&json, resolver, out);
22        }
23    }
24
25    impl<T: serde::Serialize, S: Fallible<Error: rkyv::rancor::Source> + rkyv::ser::Writer + ?Sized>
26        SerializeWith<T, S> for AsJsonString
27    {
28        fn serialize_with(field: &T, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
29            // Safety: see resolve_with — serialization of valid Serialize types cannot fail.
30            let json = serde_json::to_string(field).expect("serde_json::to_string failed");
31            ArchivedString::serialize_from_str(&json, serializer)
32        }
33    }
34
35    impl<T: serde::de::DeserializeOwned, D: Fallible + ?Sized> DeserializeWith<ArchivedString, T, D>
36        for AsJsonString
37    {
38        fn deserialize_with(archived: &ArchivedString, _: &mut D) -> Result<T, D::Error> {
39            // Safety: the archived string was produced by to_string above, so from_str cannot fail.
40            Ok(serde_json::from_str(archived.as_str()).expect("serde_json::from_str failed"))
41        }
42    }
43}
44
45/// Positional data mapping an AST node back to its source `.rdx` file.
46/// Line and column numbers are 1-indexed. Offsets are 0-indexed byte offsets.
47#[derive(
48    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
49)]
50pub struct Position {
51    pub start: Point,
52    pub end: Point,
53}
54
55#[derive(
56    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
57)]
58pub struct Point {
59    pub line: usize,
60    pub column: usize,
61    pub offset: usize,
62}
63
64/// The root of an RDX document.
65#[derive(
66    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
67)]
68#[rkyv(serialize_bounds(
69    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
70))]
71#[rkyv(deserialize_bounds(
72    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
73))]
74#[rkyv(bytecheck(bounds(
75    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
76)))]
77pub struct Root {
78    #[serde(rename = "type")]
79    pub node_type: RootType,
80    #[rkyv(with = rkyv::with::Map<rkyv_json::AsJsonString>)]
81    pub frontmatter: Option<serde_json::Value>,
82    #[rkyv(omit_bounds)]
83    pub children: Vec<Node>,
84    pub position: Position,
85}
86
87#[derive(
88    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
89)]
90pub enum RootType {
91    #[serde(rename = "root")]
92    Root,
93}
94
95/// A union of all possible RDX nodes.
96#[derive(
97    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
98)]
99#[rkyv(serialize_bounds(
100    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
101))]
102#[rkyv(deserialize_bounds(
103    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
104))]
105#[rkyv(bytecheck(bounds(
106    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
107)))]
108#[serde(tag = "type")]
109pub enum Node {
110    #[serde(rename = "text")]
111    Text(#[rkyv(omit_bounds)] TextNode),
112    #[serde(rename = "code_inline")]
113    CodeInline(#[rkyv(omit_bounds)] CodeInlineNode),
114    #[serde(rename = "code_block")]
115    CodeBlock(#[rkyv(omit_bounds)] CodeBlockNode),
116    #[serde(rename = "paragraph")]
117    Paragraph(#[rkyv(omit_bounds)] StandardBlockNode),
118    #[serde(rename = "heading")]
119    Heading(#[rkyv(omit_bounds)] StandardBlockNode),
120    #[serde(rename = "list")]
121    List(#[rkyv(omit_bounds)] StandardBlockNode),
122    #[serde(rename = "list_item")]
123    ListItem(#[rkyv(omit_bounds)] StandardBlockNode),
124    #[serde(rename = "blockquote")]
125    Blockquote(#[rkyv(omit_bounds)] StandardBlockNode),
126    #[serde(rename = "thematic_break")]
127    ThematicBreak(#[rkyv(omit_bounds)] StandardBlockNode),
128    #[serde(rename = "html")]
129    Html(#[rkyv(omit_bounds)] StandardBlockNode),
130    #[serde(rename = "table")]
131    Table(#[rkyv(omit_bounds)] StandardBlockNode),
132    #[serde(rename = "table_row")]
133    TableRow(#[rkyv(omit_bounds)] StandardBlockNode),
134    #[serde(rename = "table_cell")]
135    TableCell(#[rkyv(omit_bounds)] StandardBlockNode),
136    #[serde(rename = "link")]
137    Link(#[rkyv(omit_bounds)] LinkNode),
138    #[serde(rename = "image")]
139    Image(#[rkyv(omit_bounds)] ImageNode),
140    #[serde(rename = "emphasis")]
141    Emphasis(#[rkyv(omit_bounds)] StandardBlockNode),
142    #[serde(rename = "strong")]
143    Strong(#[rkyv(omit_bounds)] StandardBlockNode),
144    #[serde(rename = "strikethrough")]
145    Strikethrough(#[rkyv(omit_bounds)] StandardBlockNode),
146    #[serde(rename = "definition_list")]
147    DefinitionList(#[rkyv(omit_bounds)] StandardBlockNode),
148    #[serde(rename = "definition_term")]
149    DefinitionTerm(#[rkyv(omit_bounds)] StandardBlockNode),
150    #[serde(rename = "definition_description")]
151    DefinitionDescription(#[rkyv(omit_bounds)] StandardBlockNode),
152    #[serde(rename = "footnote_definition")]
153    FootnoteDefinition(#[rkyv(omit_bounds)] FootnoteNode),
154    #[serde(rename = "footnote_reference")]
155    FootnoteReference(#[rkyv(omit_bounds)] FootnoteNode),
156    #[serde(rename = "math_inline")]
157    MathInline(#[rkyv(omit_bounds)] MathNode),
158    #[serde(rename = "math_display")]
159    MathDisplay(#[rkyv(omit_bounds)] MathDisplayNode),
160    #[serde(rename = "citation")]
161    Citation(#[rkyv(omit_bounds)] CitationNode),
162    #[serde(rename = "cross_ref")]
163    CrossRef(#[rkyv(omit_bounds)] CrossRefNode),
164    #[serde(rename = "component")]
165    Component(#[rkyv(omit_bounds)] ComponentNode),
166    #[serde(rename = "variable")]
167    Variable(#[rkyv(omit_bounds)] VariableNode),
168    #[serde(rename = "error")]
169    Error(#[rkyv(omit_bounds)] ErrorNode),
170}
171
172/// A standard CommonMark block node.
173#[derive(
174    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
175)]
176#[rkyv(serialize_bounds(
177    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
178))]
179#[rkyv(deserialize_bounds(
180    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
181))]
182#[rkyv(bytecheck(bounds(
183    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
184)))]
185pub struct StandardBlockNode {
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub depth: Option<u8>,
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub ordered: Option<bool>,
190    /// For list items: whether a task list checkbox is checked.
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub checked: Option<bool>,
193    /// For headings: an explicit ID attribute (`# Title {#my-id}`).
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub id: Option<String>,
196    #[rkyv(omit_bounds)]
197    pub children: Vec<Node>,
198    pub position: Position,
199}
200
201/// An RDX component node.
202#[derive(
203    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
204)]
205#[rkyv(serialize_bounds(
206    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
207))]
208#[rkyv(deserialize_bounds(
209    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
210))]
211#[rkyv(bytecheck(bounds(
212    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
213)))]
214pub struct ComponentNode {
215    pub name: String,
216    #[serde(rename = "isInline")]
217    pub is_inline: bool,
218    #[rkyv(omit_bounds)]
219    pub attributes: Vec<AttributeNode>,
220    #[rkyv(omit_bounds)]
221    pub children: Vec<Node>,
222    /// Raw source text of the component body (between open/close tags).
223    /// Preserved verbatim for components that need whitespace-sensitive content
224    /// (e.g. CodeBlock). Empty for self-closing components.
225    #[serde(
226        default,
227        rename = "rawContent",
228        skip_serializing_if = "String::is_empty"
229    )]
230    pub raw_content: String,
231    pub position: Position,
232}
233
234/// A single attribute with its own positional data.
235#[derive(
236    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
237)]
238#[rkyv(serialize_bounds(
239    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
240))]
241#[rkyv(deserialize_bounds(
242    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
243))]
244#[rkyv(bytecheck(bounds(
245    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
246)))]
247pub struct AttributeNode {
248    pub name: String,
249    #[rkyv(omit_bounds)]
250    pub value: AttributeValue,
251    pub position: Position,
252}
253
254/// Supported attribute value types.
255#[derive(
256    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
257)]
258#[rkyv(serialize_bounds(
259    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
260))]
261#[rkyv(deserialize_bounds(
262    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
263))]
264#[rkyv(bytecheck(bounds(
265    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
266)))]
267#[serde(untagged)]
268pub enum AttributeValue {
269    Null,
270    Bool(bool),
271    Number(#[rkyv(with = rkyv_json::AsJsonString)] serde_json::Number),
272    String(String),
273    Array(#[rkyv(with = rkyv_json::AsJsonString)] Vec<serde_json::Value>),
274    Object(#[rkyv(with = rkyv_json::AsJsonString)] serde_json::Map<String, serde_json::Value>),
275    Variable(#[rkyv(omit_bounds)] VariableNode),
276}
277
278/// A footnote node (definition or reference).
279#[derive(
280    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
281)]
282#[rkyv(serialize_bounds(
283    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
284))]
285#[rkyv(deserialize_bounds(
286    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
287))]
288#[rkyv(bytecheck(bounds(
289    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
290)))]
291pub struct FootnoteNode {
292    pub label: String,
293    #[rkyv(omit_bounds)]
294    pub children: Vec<Node>,
295    pub position: Position,
296}
297
298/// A link node with URL and optional title.
299#[derive(
300    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
301)]
302#[rkyv(serialize_bounds(
303    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
304))]
305#[rkyv(deserialize_bounds(
306    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
307))]
308#[rkyv(bytecheck(bounds(
309    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
310)))]
311pub struct LinkNode {
312    pub url: String,
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub title: Option<String>,
315    #[rkyv(omit_bounds)]
316    pub children: Vec<Node>,
317    pub position: Position,
318}
319
320/// An image node with URL, optional title, and alt text.
321#[derive(
322    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
323)]
324#[rkyv(serialize_bounds(
325    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
326))]
327#[rkyv(deserialize_bounds(
328    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
329))]
330#[rkyv(bytecheck(bounds(
331    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
332)))]
333pub struct ImageNode {
334    pub url: String,
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub title: Option<String>,
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub alt: Option<String>,
339    #[rkyv(omit_bounds)]
340    pub children: Vec<Node>,
341    pub position: Position,
342}
343
344/// A fenced code block with optional language, meta string, and display metadata.
345#[derive(
346    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
347)]
348pub struct CodeBlockNode {
349    pub value: String,
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub lang: Option<String>,
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub meta: Option<String>,
354    /// Display title or filename (from info string `title="..."`).
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub title: Option<String>,
357    /// Sorted, deduplicated line numbers to emphasize.
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub highlight: Option<Vec<u32>>,
360    /// Whether to display line numbers.
361    #[serde(rename = "showLineNumbers", skip_serializing_if = "Option::is_none")]
362    pub show_line_numbers: Option<bool>,
363    /// Whether to render as a unified diff.
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub diff: Option<bool>,
366    /// Caption text for numbered code listings.
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub caption: Option<String>,
369    pub position: Position,
370}
371
372/// A literal text node.
373#[derive(
374    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
375)]
376pub struct TextNode {
377    pub value: String,
378    pub position: Position,
379}
380
381/// An inline code node with optional language hint.
382#[derive(
383    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
384)]
385pub struct CodeInlineNode {
386    pub value: String,
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub lang: Option<String>,
389    pub position: Position,
390}
391
392/// A variable interpolation node.
393#[derive(
394    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
395)]
396pub struct VariableNode {
397    pub path: String,
398    pub position: Position,
399}
400
401/// An explicit error node for host-level error boundaries.
402#[derive(
403    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
404)]
405pub struct ErrorNode {
406    pub message: String,
407    #[serde(rename = "rawContent")]
408    pub raw_content: String,
409    pub position: Position,
410}
411
412/// A citation reference node containing one or more citation keys.
413#[derive(
414    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
415)]
416pub struct CitationNode {
417    pub keys: Vec<CitationKey>,
418    pub position: Position,
419}
420
421/// A single citation key with optional prefix and locator.
422#[derive(
423    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
424)]
425pub struct CitationKey {
426    pub id: String,
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub prefix: Option<String>,
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub locator: Option<String>,
431}
432
433/// A cross-reference node pointing to a labeled element elsewhere in the document.
434#[derive(
435    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
436)]
437pub struct CrossRefNode {
438    pub target: String,
439    pub position: Position,
440}
441
442/// An inline math expression node containing raw LaTeX and a structured parse tree.
443#[derive(
444    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
445)]
446#[rkyv(serialize_bounds(
447    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
448))]
449#[rkyv(deserialize_bounds(
450    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
451))]
452#[rkyv(bytecheck(bounds(
453    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
454)))]
455pub struct MathNode {
456    pub raw: String,
457    #[rkyv(omit_bounds)]
458    pub tree: MathExpr,
459    pub position: Position,
460}
461
462/// A display math expression node containing raw LaTeX, a structured parse tree, and an optional label.
463#[derive(
464    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
465)]
466#[rkyv(serialize_bounds(
467    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
468))]
469#[rkyv(deserialize_bounds(
470    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
471))]
472#[rkyv(bytecheck(bounds(
473    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
474)))]
475pub struct MathDisplayNode {
476    pub raw: String,
477    #[rkyv(omit_bounds)]
478    pub tree: MathExpr,
479    #[serde(skip_serializing_if = "Option::is_none")]
480    pub label: Option<String>,
481    pub position: Position,
482}
483
484// ---------------------------------------------------------------------------
485// Math expression supporting types
486// ---------------------------------------------------------------------------
487
488/// A mathematical operator symbol with its classification.
489#[derive(
490    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
491)]
492pub struct MathOperator {
493    pub symbol: String,
494    pub kind: OperatorKind,
495}
496
497/// Classification of a mathematical operator.
498#[derive(
499    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
500)]
501#[serde(rename_all = "snake_case")]
502pub enum OperatorKind {
503    Binary,
504    Relation,
505    Prefix,
506    Postfix,
507    Large,
508    Punctuation,
509}
510
511/// A delimiter character used in fenced expressions.
512#[derive(
513    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
514)]
515#[serde(rename_all = "snake_case")]
516pub enum Delimiter {
517    Paren,
518    Bracket,
519    Brace,
520    Angle,
521    Pipe,
522    DoublePipe,
523    Floor,
524    Ceil,
525    None,
526}
527
528/// Style for fraction rendering.
529#[derive(
530    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
531)]
532#[serde(rename_all = "snake_case")]
533pub enum FracStyle {
534    Display,
535    Text,
536    Auto,
537}
538
539/// Limit placement style for big operators.
540#[derive(
541    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
542)]
543#[serde(rename_all = "snake_case")]
544pub enum LimitStyle {
545    DisplayLimits,
546    Limits,
547    NoLimits,
548}
549
550/// Delimiter style for matrix environments.
551#[derive(
552    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
553)]
554#[serde(rename_all = "snake_case")]
555pub enum MatrixDelimiters {
556    Plain,
557    Paren,
558    Bracket,
559    Brace,
560    Pipe,
561    DoublePipe,
562}
563
564/// Column alignment specifier for array environments.
565#[derive(
566    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
567)]
568#[serde(rename_all = "snake_case")]
569pub enum ColumnAlign {
570    Left,
571    Center,
572    Right,
573}
574
575/// Named math spacing amounts.
576#[derive(
577    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
578)]
579#[serde(rename_all = "snake_case")]
580pub enum MathSpace {
581    Thin,
582    Medium,
583    Thick,
584    Quad,
585    QQuad,
586    NegThin,
587    Custom(String),
588}
589
590/// Smash mode indicating which dimension to suppress.
591#[derive(
592    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
593)]
594#[serde(rename_all = "snake_case")]
595pub enum SmashMode {
596    Top,
597    Bottom,
598    Both,
599}
600
601/// Math style (display size) override.
602#[derive(
603    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
604)]
605#[serde(rename_all = "snake_case")]
606pub enum MathStyle {
607    Display,
608    Text,
609    Script,
610    ScriptScript,
611}
612
613/// Math font override.
614#[derive(
615    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
616)]
617#[serde(rename_all = "snake_case")]
618pub enum MathFont {
619    Roman,
620    Bold,
621    Italic,
622    BoldItalic,
623    SansSerif,
624    Monospace,
625    Blackboard,
626    Calligraphic,
627    Fraktur,
628    Script,
629}
630
631/// Accent mark kind.
632#[derive(
633    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
634)]
635#[serde(rename_all = "snake_case")]
636pub enum AccentKind {
637    Hat,
638    Tilde,
639    Vec,
640    Dot,
641    Ddot,
642    Bar,
643    Acute,
644    Grave,
645    Breve,
646    Check,
647    WideHat,
648    WideTilde,
649}
650
651/// A row in an alignment environment (align/alignat).
652#[derive(
653    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
654)]
655#[rkyv(serialize_bounds(
656    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
657))]
658#[rkyv(deserialize_bounds(
659    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
660))]
661#[rkyv(bytecheck(bounds(
662    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
663)))]
664pub struct AlignRow {
665    #[rkyv(omit_bounds)]
666    pub cells: Vec<MathExpr>,
667    #[serde(skip_serializing_if = "Option::is_none")]
668    pub label: Option<String>,
669}
670
671/// A row in a cases environment.
672#[derive(
673    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
674)]
675#[rkyv(serialize_bounds(
676    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
677))]
678#[rkyv(deserialize_bounds(
679    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
680))]
681#[rkyv(bytecheck(bounds(
682    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
683)))]
684pub struct CaseRow {
685    #[rkyv(omit_bounds)]
686    pub expr: MathExpr,
687    #[rkyv(omit_bounds)]
688    pub condition: Option<MathExpr>,
689}
690
691/// Structured math expression tree (Section 2.11).
692///
693/// This is a recursive algebraic type representing the parsed structure of a LaTeX
694/// math expression. `Box<MathExpr>` is used for recursive fields to keep the type
695/// sized.
696#[derive(
697    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
698)]
699#[rkyv(serialize_bounds(
700    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
701))]
702#[rkyv(deserialize_bounds(
703    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
704))]
705#[rkyv(bytecheck(bounds(
706    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
707)))]
708#[serde(tag = "type", rename_all = "snake_case")]
709pub enum MathExpr {
710    // Atoms
711    Ident {
712        value: String,
713    },
714    Number {
715        value: String,
716    },
717    Operator(MathOperator),
718    Text {
719        value: String,
720    },
721
722    // Grouping
723    Row {
724        #[rkyv(omit_bounds)]
725        children: Vec<MathExpr>,
726    },
727    Fenced {
728        open: Delimiter,
729        close: Delimiter,
730        #[rkyv(omit_bounds)]
731        body: Vec<MathExpr>,
732    },
733
734    // Scripts
735    Superscript {
736        #[rkyv(omit_bounds)]
737        base: Box<MathExpr>,
738        #[rkyv(omit_bounds)]
739        script: Box<MathExpr>,
740    },
741    Subscript {
742        #[rkyv(omit_bounds)]
743        base: Box<MathExpr>,
744        #[rkyv(omit_bounds)]
745        script: Box<MathExpr>,
746    },
747    Subsuperscript {
748        #[rkyv(omit_bounds)]
749        base: Box<MathExpr>,
750        #[rkyv(omit_bounds)]
751        sub: Box<MathExpr>,
752        #[rkyv(omit_bounds)]
753        sup: Box<MathExpr>,
754    },
755
756    // Layout
757    Frac {
758        #[rkyv(omit_bounds)]
759        numerator: Box<MathExpr>,
760        #[rkyv(omit_bounds)]
761        denominator: Box<MathExpr>,
762        style: FracStyle,
763    },
764    Sqrt {
765        #[rkyv(omit_bounds)]
766        index: Option<Box<MathExpr>>,
767        #[rkyv(omit_bounds)]
768        body: Box<MathExpr>,
769    },
770
771    // Over/Under
772    Overline {
773        #[rkyv(omit_bounds)]
774        body: Box<MathExpr>,
775    },
776    Underline {
777        #[rkyv(omit_bounds)]
778        body: Box<MathExpr>,
779    },
780    Overbrace {
781        #[rkyv(omit_bounds)]
782        body: Box<MathExpr>,
783        #[rkyv(omit_bounds)]
784        annotation: Option<Box<MathExpr>>,
785    },
786    Underbrace {
787        #[rkyv(omit_bounds)]
788        body: Box<MathExpr>,
789        #[rkyv(omit_bounds)]
790        annotation: Option<Box<MathExpr>>,
791    },
792    Overset {
793        #[rkyv(omit_bounds)]
794        over: Box<MathExpr>,
795        #[rkyv(omit_bounds)]
796        base: Box<MathExpr>,
797    },
798    Underset {
799        #[rkyv(omit_bounds)]
800        under: Box<MathExpr>,
801        #[rkyv(omit_bounds)]
802        base: Box<MathExpr>,
803    },
804    Accent {
805        kind: AccentKind,
806        #[rkyv(omit_bounds)]
807        body: Box<MathExpr>,
808    },
809
810    // Big operators
811    BigOperator {
812        op: MathOperator,
813        limits: LimitStyle,
814        #[rkyv(omit_bounds)]
815        lower: Option<Box<MathExpr>>,
816        #[rkyv(omit_bounds)]
817        upper: Option<Box<MathExpr>>,
818    },
819
820    // Matrices & arrays
821    Matrix {
822        #[rkyv(omit_bounds)]
823        rows: Vec<Vec<MathExpr>>,
824        delimiters: MatrixDelimiters,
825    },
826    Cases {
827        #[rkyv(omit_bounds)]
828        rows: Vec<CaseRow>,
829    },
830    Array {
831        columns: Vec<ColumnAlign>,
832        #[rkyv(omit_bounds)]
833        rows: Vec<Vec<MathExpr>>,
834    },
835
836    // Alignment environments
837    Align {
838        #[rkyv(omit_bounds)]
839        rows: Vec<AlignRow>,
840        numbered: bool,
841    },
842    Gather {
843        #[rkyv(omit_bounds)]
844        rows: Vec<MathExpr>,
845        numbered: bool,
846    },
847
848    // Spacing
849    Space(MathSpace),
850    Phantom {
851        #[rkyv(omit_bounds)]
852        body: Box<MathExpr>,
853    },
854    HPhantom {
855        #[rkyv(omit_bounds)]
856        body: Box<MathExpr>,
857    },
858    VPhantom {
859        #[rkyv(omit_bounds)]
860        body: Box<MathExpr>,
861    },
862    Smash {
863        #[rkyv(omit_bounds)]
864        body: Box<MathExpr>,
865        mode: SmashMode,
866    },
867
868    // Style overrides
869    StyleOverride {
870        style: MathStyle,
871        #[rkyv(omit_bounds)]
872        body: Box<MathExpr>,
873    },
874    FontOverride {
875        font: MathFont,
876        #[rkyv(omit_bounds)]
877        body: Box<MathExpr>,
878    },
879    Color {
880        color: String,
881        #[rkyv(omit_bounds)]
882        body: Box<MathExpr>,
883    },
884
885    // Chemistry
886    Chem {
887        value: String,
888    },
889
890    // Error recovery
891    Error {
892        raw: String,
893        message: String,
894    },
895}
896
897impl Node {
898    /// Returns a mutable reference to this node's children, if it has any.
899    pub fn children_mut(&mut self) -> Option<&mut Vec<Node>> {
900        match self {
901            Node::Paragraph(b)
902            | Node::Heading(b)
903            | Node::List(b)
904            | Node::ListItem(b)
905            | Node::Blockquote(b)
906            | Node::Html(b)
907            | Node::Table(b)
908            | Node::TableRow(b)
909            | Node::TableCell(b)
910            | Node::Emphasis(b)
911            | Node::Strong(b)
912            | Node::Strikethrough(b)
913            | Node::ThematicBreak(b)
914            | Node::DefinitionList(b)
915            | Node::DefinitionTerm(b)
916            | Node::DefinitionDescription(b) => Some(&mut b.children),
917            Node::Link(l) => Some(&mut l.children),
918            Node::Image(i) => Some(&mut i.children),
919            Node::Component(c) => Some(&mut c.children),
920            Node::FootnoteDefinition(n) => Some(&mut n.children),
921            _ => None,
922        }
923    }
924
925    /// Returns a reference to this node's children, if it has any.
926    pub fn children(&self) -> Option<&[Node]> {
927        match self {
928            Node::Paragraph(b)
929            | Node::Heading(b)
930            | Node::List(b)
931            | Node::ListItem(b)
932            | Node::Blockquote(b)
933            | Node::Html(b)
934            | Node::Table(b)
935            | Node::TableRow(b)
936            | Node::TableCell(b)
937            | Node::Emphasis(b)
938            | Node::Strong(b)
939            | Node::Strikethrough(b)
940            | Node::ThematicBreak(b)
941            | Node::DefinitionList(b)
942            | Node::DefinitionTerm(b)
943            | Node::DefinitionDescription(b) => Some(&b.children),
944            Node::Link(l) => Some(&l.children),
945            Node::Image(i) => Some(&i.children),
946            Node::Component(c) => Some(&c.children),
947            Node::FootnoteDefinition(n) => Some(&n.children),
948            _ => None,
949        }
950    }
951}
952
953#[cfg(test)]
954mod tests {
955    use super::*;
956
957    fn pos(line: usize, col: usize, off: usize) -> Point {
958        Point {
959            line,
960            column: col,
961            offset: off,
962        }
963    }
964
965    fn span(sl: usize, sc: usize, so: usize, el: usize, ec: usize, eo: usize) -> Position {
966        Position {
967            start: pos(sl, sc, so),
968            end: pos(el, ec, eo),
969        }
970    }
971
972    #[test]
973    fn root_serializes_type_field() {
974        let root = Root {
975            node_type: RootType::Root,
976            frontmatter: None,
977            children: vec![],
978            position: span(1, 1, 0, 1, 1, 0),
979        };
980        let json = serde_json::to_value(&root).unwrap();
981        assert_eq!(json["type"], "root");
982        assert!(json["frontmatter"].is_null());
983        assert_eq!(json["children"], serde_json::json!([]));
984    }
985
986    #[test]
987    fn component_node_serializes_correctly() {
988        let node = Node::Component(ComponentNode {
989            name: "Badge".into(),
990            is_inline: false,
991            attributes: vec![
992                AttributeNode {
993                    name: "status".into(),
994                    value: AttributeValue::String("beta".into()),
995                    position: span(1, 8, 7, 1, 22, 21),
996                },
997                AttributeNode {
998                    name: "active".into(),
999                    value: AttributeValue::Bool(true),
1000                    position: span(1, 23, 22, 1, 36, 35),
1001                },
1002            ],
1003            children: vec![Node::Text(TextNode {
1004                value: "New Feature".into(),
1005                position: span(1, 37, 36, 1, 48, 47),
1006            })],
1007            raw_content: String::new(),
1008            position: span(1, 1, 0, 1, 55, 54),
1009        });
1010
1011        let json = serde_json::to_value(&node).unwrap();
1012        assert_eq!(json["type"], "component");
1013        assert_eq!(json["name"], "Badge");
1014        assert_eq!(json["isInline"], false);
1015        assert_eq!(json["attributes"][0]["name"], "status");
1016        assert_eq!(json["attributes"][0]["value"], "beta");
1017        assert_eq!(json["attributes"][1]["name"], "active");
1018        assert_eq!(json["attributes"][1]["value"], true);
1019        assert_eq!(json["children"][0]["type"], "text");
1020        assert_eq!(json["children"][0]["value"], "New Feature");
1021    }
1022
1023    #[test]
1024    fn attribute_value_null_serializes_to_null() {
1025        let val = AttributeValue::Null;
1026        let json = serde_json::to_value(&val).unwrap();
1027        assert!(json.is_null());
1028    }
1029
1030    #[test]
1031    fn attribute_value_number() {
1032        let val = AttributeValue::Number(serde_json::Number::from(42));
1033        let json = serde_json::to_value(&val).unwrap();
1034        assert_eq!(json, 42);
1035    }
1036
1037    #[test]
1038    fn attribute_value_json_object() {
1039        let mut map = serde_json::Map::new();
1040        map.insert("type".into(), serde_json::Value::String("bar".into()));
1041        let val = AttributeValue::Object(map);
1042        let json = serde_json::to_value(&val).unwrap();
1043        assert_eq!(json["type"], "bar");
1044    }
1045
1046    #[test]
1047    fn attribute_value_json_array() {
1048        let val = AttributeValue::Array(vec![
1049            serde_json::Value::from(10),
1050            serde_json::Value::from(20),
1051        ]);
1052        let json = serde_json::to_value(&val).unwrap();
1053        assert_eq!(json, serde_json::json!([10, 20]));
1054    }
1055
1056    #[test]
1057    fn variable_node_serializes() {
1058        let node = Node::Variable(VariableNode {
1059            path: "frontmatter.title".into(),
1060            position: span(1, 1, 0, 1, 20, 19),
1061        });
1062        let json = serde_json::to_value(&node).unwrap();
1063        assert_eq!(json["type"], "variable");
1064        assert_eq!(json["path"], "frontmatter.title");
1065    }
1066
1067    #[test]
1068    fn error_node_serializes() {
1069        let node = Node::Error(ErrorNode {
1070            message: "Unclosed tag".into(),
1071            raw_content: "<Notice>".into(),
1072            position: span(1, 1, 0, 1, 9, 8),
1073        });
1074        let json = serde_json::to_value(&node).unwrap();
1075        assert_eq!(json["type"], "error");
1076        assert_eq!(json["message"], "Unclosed tag");
1077        assert_eq!(json["rawContent"], "<Notice>");
1078    }
1079
1080    #[test]
1081    fn standard_block_omits_none_fields() {
1082        let node = Node::Heading(StandardBlockNode {
1083            depth: Some(2),
1084            ordered: None,
1085            checked: None,
1086            id: None,
1087            children: vec![],
1088            position: span(1, 1, 0, 1, 10, 9),
1089        });
1090        let json = serde_json::to_value(&node).unwrap();
1091        assert_eq!(json["depth"], 2);
1092        assert!(json.get("ordered").is_none());
1093        assert!(json.get("checked").is_none());
1094        assert!(json.get("id").is_none());
1095    }
1096
1097    #[test]
1098    fn roundtrip_component_node() {
1099        let original = Node::Component(ComponentNode {
1100            name: "Chart".into(),
1101            is_inline: true,
1102            attributes: vec![],
1103            children: vec![],
1104            raw_content: String::new(),
1105            position: span(1, 1, 0, 1, 10, 9),
1106        });
1107        let serialized = serde_json::to_string(&original).unwrap();
1108        let deserialized: Node = serde_json::from_str(&serialized).unwrap();
1109        assert_eq!(original, deserialized);
1110    }
1111
1112    #[test]
1113    fn code_inline_node_serializes() {
1114        let node = Node::CodeInline(CodeInlineNode {
1115            value: "x + 1".into(),
1116            lang: Some("rust".into()),
1117            position: span(1, 1, 0, 1, 10, 9),
1118        });
1119        let json = serde_json::to_value(&node).unwrap();
1120        assert_eq!(json["type"], "code_inline");
1121        assert_eq!(json["value"], "x + 1");
1122        assert_eq!(json["lang"], "rust");
1123    }
1124
1125    #[test]
1126    fn code_inline_node_omits_lang_when_none() {
1127        let node = Node::CodeInline(CodeInlineNode {
1128            value: "foo()".into(),
1129            lang: None,
1130            position: span(1, 1, 0, 1, 7, 6),
1131        });
1132        let json = serde_json::to_value(&node).unwrap();
1133        assert_eq!(json["type"], "code_inline");
1134        assert!(json.get("lang").is_none());
1135    }
1136
1137    #[test]
1138    fn code_block_node_new_fields_serialize() {
1139        let node = Node::CodeBlock(CodeBlockNode {
1140            value: "fn main() {}".into(),
1141            lang: Some("rust".into()),
1142            meta: None,
1143            title: Some("main.rs".into()),
1144            highlight: Some(vec![1, 3]),
1145            show_line_numbers: Some(true),
1146            diff: None,
1147            caption: None,
1148            position: span(1, 1, 0, 3, 1, 20),
1149        });
1150        let json = serde_json::to_value(&node).unwrap();
1151        assert_eq!(json["type"], "code_block");
1152        assert_eq!(json["title"], "main.rs");
1153        assert_eq!(json["highlight"], serde_json::json!([1, 3]));
1154        assert_eq!(json["showLineNumbers"], true);
1155        assert!(json.get("diff").is_none());
1156        assert!(json.get("caption").is_none());
1157    }
1158
1159    #[test]
1160    fn citation_node_serializes() {
1161        let node = Node::Citation(CitationNode {
1162            keys: vec![
1163                CitationKey {
1164                    id: "smith2024".into(),
1165                    prefix: Some("see ".into()),
1166                    locator: Some("p. 42".into()),
1167                },
1168                CitationKey {
1169                    id: "jones2023".into(),
1170                    prefix: None,
1171                    locator: None,
1172                },
1173            ],
1174            position: span(1, 1, 0, 1, 20, 19),
1175        });
1176        let json = serde_json::to_value(&node).unwrap();
1177        assert_eq!(json["type"], "citation");
1178        assert_eq!(json["keys"][0]["id"], "smith2024");
1179        assert_eq!(json["keys"][0]["prefix"], "see ");
1180        assert_eq!(json["keys"][0]["locator"], "p. 42");
1181        assert_eq!(json["keys"][1]["id"], "jones2023");
1182        assert!(json["keys"][1].get("prefix").is_none());
1183        assert!(json["keys"][1].get("locator").is_none());
1184    }
1185
1186    #[test]
1187    fn cross_ref_node_serializes() {
1188        let node = Node::CrossRef(CrossRefNode {
1189            target: "fig:architecture".into(),
1190            position: span(1, 1, 0, 1, 20, 19),
1191        });
1192        let json = serde_json::to_value(&node).unwrap();
1193        assert_eq!(json["type"], "cross_ref");
1194        assert_eq!(json["target"], "fig:architecture");
1195    }
1196
1197    #[test]
1198    fn math_inline_node_serializes() {
1199        let node = Node::MathInline(MathNode {
1200            raw: "x^2".into(),
1201            tree: MathExpr::Superscript {
1202                base: Box::new(MathExpr::Ident { value: "x".into() }),
1203                script: Box::new(MathExpr::Number { value: "2".into() }),
1204            },
1205            position: span(1, 1, 0, 1, 6, 5),
1206        });
1207        let json = serde_json::to_value(&node).unwrap();
1208        assert_eq!(json["type"], "math_inline");
1209        assert_eq!(json["raw"], "x^2");
1210        assert_eq!(json["tree"]["type"], "superscript");
1211        assert_eq!(json["tree"]["base"]["type"], "ident");
1212        assert_eq!(json["tree"]["base"]["value"], "x");
1213        assert_eq!(json["tree"]["script"]["type"], "number");
1214        assert_eq!(json["tree"]["script"]["value"], "2");
1215    }
1216
1217    #[test]
1218    fn math_display_node_serializes_with_label() {
1219        let node = Node::MathDisplay(MathDisplayNode {
1220            raw: "E = mc^2".into(),
1221            tree: MathExpr::Row {
1222                children: vec![MathExpr::Ident { value: "E".into() }],
1223            },
1224            label: Some("eq:einstein".into()),
1225            position: span(1, 1, 0, 1, 12, 11),
1226        });
1227        let json = serde_json::to_value(&node).unwrap();
1228        assert_eq!(json["type"], "math_display");
1229        assert_eq!(json["raw"], "E = mc^2");
1230        assert_eq!(json["label"], "eq:einstein");
1231        assert_eq!(json["tree"]["type"], "row");
1232    }
1233
1234    #[test]
1235    fn math_display_node_omits_label_when_none() {
1236        let node = Node::MathDisplay(MathDisplayNode {
1237            raw: "x".into(),
1238            tree: MathExpr::Ident { value: "x".into() },
1239            label: None,
1240            position: span(1, 1, 0, 1, 3, 2),
1241        });
1242        let json = serde_json::to_value(&node).unwrap();
1243        assert!(json.get("label").is_none());
1244    }
1245
1246    #[test]
1247    fn definition_list_variants_have_children() {
1248        let dl = Node::DefinitionList(StandardBlockNode {
1249            depth: None,
1250            ordered: None,
1251            checked: None,
1252            id: None,
1253            children: vec![Node::DefinitionTerm(StandardBlockNode {
1254                depth: None,
1255                ordered: None,
1256                checked: None,
1257                id: None,
1258                children: vec![Node::Text(TextNode {
1259                    value: "Term".into(),
1260                    position: span(1, 1, 0, 1, 5, 4),
1261                })],
1262                position: span(1, 1, 0, 1, 5, 4),
1263            })],
1264            position: span(1, 1, 0, 2, 1, 10),
1265        });
1266        let json = serde_json::to_value(&dl).unwrap();
1267        assert_eq!(json["type"], "definition_list");
1268        assert_eq!(json["children"][0]["type"], "definition_term");
1269        assert_eq!(json["children"][0]["children"][0]["value"], "Term");
1270    }
1271
1272    #[test]
1273    fn definition_list_children_accessible() {
1274        let node = Node::DefinitionList(StandardBlockNode {
1275            depth: None,
1276            ordered: None,
1277            checked: None,
1278            id: None,
1279            children: vec![],
1280            position: span(1, 1, 0, 1, 1, 0),
1281        });
1282        assert!(node.children().is_some());
1283    }
1284
1285    #[test]
1286    fn math_frac_roundtrip() {
1287        let expr = MathExpr::Frac {
1288            numerator: Box::new(MathExpr::Number { value: "1".into() }),
1289            denominator: Box::new(MathExpr::Number { value: "2".into() }),
1290            style: FracStyle::Auto,
1291        };
1292        let serialized = serde_json::to_string(&expr).unwrap();
1293        let deserialized: MathExpr = serde_json::from_str(&serialized).unwrap();
1294        assert_eq!(expr, deserialized);
1295    }
1296
1297    #[test]
1298    fn math_error_variant_serializes() {
1299        let expr = MathExpr::Error {
1300            raw: r"\unknown".into(),
1301            message: "Unknown command".into(),
1302        };
1303        let json = serde_json::to_value(&expr).unwrap();
1304        assert_eq!(json["type"], "error");
1305        assert_eq!(json["raw"], r"\unknown");
1306        assert_eq!(json["message"], "Unknown command");
1307    }
1308}