1use ratex_lexer::token::SourceLocation;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum Mode {
8 Math,
9 Text,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum StyleStr {
16 Display,
17 Text,
18 Script,
19 Scriptscript,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum AtomFamily {
26 Bin,
27 Close,
28 Inner,
29 Open,
30 Punct,
31 Rel,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct Measurement {
37 pub number: f64,
38 pub unit: String,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct AlignSpec {
44 #[serde(rename = "type")]
45 pub align_type: AlignType,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub align: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub pregap: Option<f64>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub postgap: Option<f64>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(rename_all = "lowercase")]
56pub enum AlignType {
57 Align,
58 Separator,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(tag = "type")]
67pub enum ParseNode {
68 #[serde(rename = "atom")]
72 Atom {
73 mode: Mode,
74 family: AtomFamily,
75 text: String,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 loc: Option<SourceLocation>,
78 },
79
80 #[serde(rename = "mathord")]
81 MathOrd {
82 mode: Mode,
83 text: String,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 loc: Option<SourceLocation>,
86 },
87
88 #[serde(rename = "textord")]
89 TextOrd {
90 mode: Mode,
91 text: String,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 loc: Option<SourceLocation>,
94 },
95
96 #[serde(rename = "op-token")]
97 OpToken {
98 mode: Mode,
99 text: String,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 loc: Option<SourceLocation>,
102 },
103
104 #[serde(rename = "accent-token")]
105 AccentToken {
106 mode: Mode,
107 text: String,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 loc: Option<SourceLocation>,
110 },
111
112 #[serde(rename = "spacing")]
113 SpacingNode {
114 mode: Mode,
115 text: String,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 loc: Option<SourceLocation>,
118 },
119
120 #[serde(rename = "ordgroup")]
124 OrdGroup {
125 mode: Mode,
126 body: Vec<ParseNode>,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 semisimple: Option<bool>,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 loc: Option<SourceLocation>,
131 },
132
133 #[serde(rename = "supsub")]
134 SupSub {
135 mode: Mode,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 base: Option<Box<ParseNode>>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 sup: Option<Box<ParseNode>>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 sub: Option<Box<ParseNode>>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 loc: Option<SourceLocation>,
144 },
145
146 #[serde(rename = "genfrac")]
150 GenFrac {
151 mode: Mode,
152 continued: bool,
153 numer: Box<ParseNode>,
154 denom: Box<ParseNode>,
155 #[serde(rename = "hasBarLine")]
156 has_bar_line: bool,
157 #[serde(rename = "leftDelim")]
158 left_delim: Option<String>,
159 #[serde(rename = "rightDelim")]
160 right_delim: Option<String>,
161 #[serde(rename = "barSize")]
162 bar_size: Option<Measurement>,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 loc: Option<SourceLocation>,
165 },
166
167 #[serde(rename = "sqrt")]
168 Sqrt {
169 mode: Mode,
170 body: Box<ParseNode>,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 index: Option<Box<ParseNode>>,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 loc: Option<SourceLocation>,
175 },
176
177 #[serde(rename = "accent")]
178 Accent {
179 mode: Mode,
180 label: String,
181 #[serde(rename = "isStretchy")]
182 #[serde(skip_serializing_if = "Option::is_none")]
183 is_stretchy: Option<bool>,
184 #[serde(rename = "isShifty")]
185 #[serde(skip_serializing_if = "Option::is_none")]
186 is_shifty: Option<bool>,
187 base: Box<ParseNode>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 loc: Option<SourceLocation>,
190 },
191
192 #[serde(rename = "accentUnder")]
193 AccentUnder {
194 mode: Mode,
195 label: String,
196 #[serde(rename = "isStretchy")]
197 #[serde(skip_serializing_if = "Option::is_none")]
198 is_stretchy: Option<bool>,
199 #[serde(rename = "isShifty")]
200 #[serde(skip_serializing_if = "Option::is_none")]
201 is_shifty: Option<bool>,
202 base: Box<ParseNode>,
203 #[serde(skip_serializing_if = "Option::is_none")]
204 loc: Option<SourceLocation>,
205 },
206
207 #[serde(rename = "op")]
208 Op {
209 mode: Mode,
210 limits: bool,
211 #[serde(rename = "alwaysHandleSupSub")]
212 #[serde(skip_serializing_if = "Option::is_none")]
213 always_handle_sup_sub: Option<bool>,
214 #[serde(rename = "suppressBaseShift")]
215 #[serde(skip_serializing_if = "Option::is_none")]
216 suppress_base_shift: Option<bool>,
217 #[serde(rename = "parentIsSupSub")]
218 parent_is_sup_sub: bool,
219 symbol: bool,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 name: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 body: Option<Vec<ParseNode>>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 loc: Option<SourceLocation>,
226 },
227
228 #[serde(rename = "operatorname")]
229 OperatorName {
230 mode: Mode,
231 body: Vec<ParseNode>,
232 #[serde(rename = "alwaysHandleSupSub")]
233 always_handle_sup_sub: bool,
234 limits: bool,
235 #[serde(rename = "parentIsSupSub")]
236 parent_is_sup_sub: bool,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 loc: Option<SourceLocation>,
239 },
240
241 #[serde(rename = "font")]
242 Font {
243 mode: Mode,
244 font: String,
245 body: Box<ParseNode>,
246 #[serde(skip_serializing_if = "Option::is_none")]
247 loc: Option<SourceLocation>,
248 },
249
250 #[serde(rename = "text")]
251 Text {
252 mode: Mode,
253 body: Vec<ParseNode>,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 font: Option<String>,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 loc: Option<SourceLocation>,
258 },
259
260 #[serde(rename = "color")]
261 Color {
262 mode: Mode,
263 color: String,
264 body: Vec<ParseNode>,
265 #[serde(skip_serializing_if = "Option::is_none")]
266 loc: Option<SourceLocation>,
267 },
268
269 #[serde(rename = "color-token")]
270 ColorToken {
271 mode: Mode,
272 color: String,
273 #[serde(skip_serializing_if = "Option::is_none")]
274 loc: Option<SourceLocation>,
275 },
276
277 #[serde(rename = "size")]
278 Size {
279 mode: Mode,
280 value: Measurement,
281 #[serde(rename = "isBlank")]
282 is_blank: bool,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 loc: Option<SourceLocation>,
285 },
286
287 #[serde(rename = "styling")]
288 Styling {
289 mode: Mode,
290 style: StyleStr,
291 body: Vec<ParseNode>,
292 #[serde(skip_serializing_if = "Option::is_none")]
293 loc: Option<SourceLocation>,
294 },
295
296 #[serde(rename = "sizing")]
297 Sizing {
298 mode: Mode,
299 size: u8,
300 body: Vec<ParseNode>,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 loc: Option<SourceLocation>,
303 },
304
305 #[serde(rename = "delimsizing")]
306 DelimSizing {
307 mode: Mode,
308 size: u8,
309 mclass: String,
310 delim: String,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 loc: Option<SourceLocation>,
313 },
314
315 #[serde(rename = "leftright")]
316 LeftRight {
317 mode: Mode,
318 body: Vec<ParseNode>,
319 left: String,
320 right: String,
321 #[serde(rename = "rightColor")]
322 #[serde(skip_serializing_if = "Option::is_none")]
323 right_color: Option<String>,
324 #[serde(skip_serializing_if = "Option::is_none")]
325 loc: Option<SourceLocation>,
326 },
327
328 #[serde(rename = "leftright-right")]
329 LeftRightRight {
330 mode: Mode,
331 delim: String,
332 #[serde(skip_serializing_if = "Option::is_none")]
333 color: Option<String>,
334 #[serde(skip_serializing_if = "Option::is_none")]
335 loc: Option<SourceLocation>,
336 },
337
338 #[serde(rename = "middle")]
339 Middle {
340 mode: Mode,
341 delim: String,
342 #[serde(skip_serializing_if = "Option::is_none")]
343 loc: Option<SourceLocation>,
344 },
345
346 #[serde(rename = "overline")]
347 Overline {
348 mode: Mode,
349 body: Box<ParseNode>,
350 #[serde(skip_serializing_if = "Option::is_none")]
351 loc: Option<SourceLocation>,
352 },
353
354 #[serde(rename = "underline")]
355 Underline {
356 mode: Mode,
357 body: Box<ParseNode>,
358 #[serde(skip_serializing_if = "Option::is_none")]
359 loc: Option<SourceLocation>,
360 },
361
362 #[serde(rename = "rule")]
363 Rule {
364 mode: Mode,
365 #[serde(skip_serializing_if = "Option::is_none")]
366 shift: Option<Measurement>,
367 width: Measurement,
368 height: Measurement,
369 #[serde(skip_serializing_if = "Option::is_none")]
370 loc: Option<SourceLocation>,
371 },
372
373 #[serde(rename = "kern")]
374 Kern {
375 mode: Mode,
376 dimension: Measurement,
377 #[serde(skip_serializing_if = "Option::is_none")]
378 loc: Option<SourceLocation>,
379 },
380
381 #[serde(rename = "phantom")]
382 Phantom {
383 mode: Mode,
384 body: Vec<ParseNode>,
385 #[serde(skip_serializing_if = "Option::is_none")]
386 loc: Option<SourceLocation>,
387 },
388
389 #[serde(rename = "vphantom")]
390 VPhantom {
391 mode: Mode,
392 body: Box<ParseNode>,
393 #[serde(skip_serializing_if = "Option::is_none")]
394 loc: Option<SourceLocation>,
395 },
396
397 #[serde(rename = "smash")]
398 Smash {
399 mode: Mode,
400 body: Box<ParseNode>,
401 #[serde(rename = "smashHeight")]
402 smash_height: bool,
403 #[serde(rename = "smashDepth")]
404 smash_depth: bool,
405 #[serde(skip_serializing_if = "Option::is_none")]
406 loc: Option<SourceLocation>,
407 },
408
409 #[serde(rename = "mclass")]
410 MClass {
411 mode: Mode,
412 mclass: String,
413 body: Vec<ParseNode>,
414 #[serde(rename = "isCharacterBox")]
415 is_character_box: bool,
416 #[serde(skip_serializing_if = "Option::is_none")]
417 loc: Option<SourceLocation>,
418 },
419
420 #[serde(rename = "array")]
421 Array {
422 mode: Mode,
423 body: Vec<Vec<ParseNode>>,
424 #[serde(rename = "rowGaps")]
425 row_gaps: Vec<Option<Measurement>>,
426 #[serde(rename = "hLinesBeforeRow")]
427 hlines_before_row: Vec<Vec<bool>>,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 cols: Option<Vec<AlignSpec>>,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 #[serde(rename = "colSeparationType")]
432 col_separation_type: Option<String>,
433 #[serde(skip_serializing_if = "Option::is_none")]
434 #[serde(rename = "hskipBeforeAndAfter")]
435 hskip_before_and_after: Option<bool>,
436 #[serde(skip_serializing_if = "Option::is_none")]
437 #[serde(rename = "addJot")]
438 add_jot: Option<bool>,
439 #[serde(default = "default_arraystretch")]
440 arraystretch: f64,
441 #[serde(skip_serializing_if = "Option::is_none")]
442 tags: Option<Vec<ArrayTag>>,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 leqno: Option<bool>,
445 #[serde(skip_serializing_if = "Option::is_none")]
446 #[serde(rename = "isCD")]
447 is_cd: Option<bool>,
448 #[serde(skip_serializing_if = "Option::is_none")]
449 loc: Option<SourceLocation>,
450 },
451
452 #[serde(rename = "environment")]
453 Environment {
454 mode: Mode,
455 name: String,
456 #[serde(rename = "nameGroup")]
457 name_group: Box<ParseNode>,
458 #[serde(skip_serializing_if = "Option::is_none")]
459 loc: Option<SourceLocation>,
460 },
461
462 #[serde(rename = "cr")]
463 Cr {
464 mode: Mode,
465 #[serde(rename = "newLine")]
466 new_line: bool,
467 #[serde(skip_serializing_if = "Option::is_none")]
468 size: Option<Measurement>,
469 #[serde(skip_serializing_if = "Option::is_none")]
470 loc: Option<SourceLocation>,
471 },
472
473 #[serde(rename = "infix")]
474 Infix {
475 mode: Mode,
476 #[serde(rename = "replaceWith")]
477 replace_with: String,
478 #[serde(skip_serializing_if = "Option::is_none")]
479 size: Option<Measurement>,
480 #[serde(skip_serializing_if = "Option::is_none")]
481 loc: Option<SourceLocation>,
482 },
483
484 #[serde(rename = "internal")]
485 Internal {
486 mode: Mode,
487 #[serde(skip_serializing_if = "Option::is_none")]
488 loc: Option<SourceLocation>,
489 },
490
491 #[serde(rename = "verb")]
492 Verb {
493 mode: Mode,
494 body: String,
495 star: bool,
496 #[serde(skip_serializing_if = "Option::is_none")]
497 loc: Option<SourceLocation>,
498 },
499
500 #[serde(rename = "href")]
501 Href {
502 mode: Mode,
503 href: String,
504 body: Vec<ParseNode>,
505 #[serde(skip_serializing_if = "Option::is_none")]
506 loc: Option<SourceLocation>,
507 },
508
509 #[serde(rename = "url")]
510 Url {
511 mode: Mode,
512 url: String,
513 #[serde(skip_serializing_if = "Option::is_none")]
514 loc: Option<SourceLocation>,
515 },
516
517 #[serde(rename = "raw")]
518 Raw {
519 mode: Mode,
520 string: String,
521 #[serde(skip_serializing_if = "Option::is_none")]
522 loc: Option<SourceLocation>,
523 },
524
525 #[serde(rename = "hbox")]
526 HBox {
527 mode: Mode,
528 body: Vec<ParseNode>,
529 #[serde(skip_serializing_if = "Option::is_none")]
530 loc: Option<SourceLocation>,
531 },
532
533 #[serde(rename = "horizBrace")]
534 HorizBrace {
535 mode: Mode,
536 label: String,
537 #[serde(rename = "isOver")]
538 is_over: bool,
539 base: Box<ParseNode>,
540 #[serde(skip_serializing_if = "Option::is_none")]
541 loc: Option<SourceLocation>,
542 },
543
544 #[serde(rename = "enclose")]
545 Enclose {
546 mode: Mode,
547 label: String,
548 #[serde(rename = "backgroundColor")]
549 #[serde(skip_serializing_if = "Option::is_none")]
550 background_color: Option<String>,
551 #[serde(rename = "borderColor")]
552 #[serde(skip_serializing_if = "Option::is_none")]
553 border_color: Option<String>,
554 body: Box<ParseNode>,
555 #[serde(skip_serializing_if = "Option::is_none")]
556 loc: Option<SourceLocation>,
557 },
558
559 #[serde(rename = "lap")]
560 Lap {
561 mode: Mode,
562 alignment: String,
563 body: Box<ParseNode>,
564 #[serde(skip_serializing_if = "Option::is_none")]
565 loc: Option<SourceLocation>,
566 },
567
568 #[serde(rename = "mathchoice")]
569 MathChoice {
570 mode: Mode,
571 display: Vec<ParseNode>,
572 text: Vec<ParseNode>,
573 script: Vec<ParseNode>,
574 scriptscript: Vec<ParseNode>,
575 #[serde(skip_serializing_if = "Option::is_none")]
576 loc: Option<SourceLocation>,
577 },
578
579 #[serde(rename = "raisebox")]
580 RaiseBox {
581 mode: Mode,
582 dy: Measurement,
583 body: Box<ParseNode>,
584 #[serde(skip_serializing_if = "Option::is_none")]
585 loc: Option<SourceLocation>,
586 },
587
588 #[serde(rename = "vcenter")]
589 VCenter {
590 mode: Mode,
591 body: Box<ParseNode>,
592 #[serde(skip_serializing_if = "Option::is_none")]
593 loc: Option<SourceLocation>,
594 },
595
596 #[serde(rename = "xArrow")]
597 XArrow {
598 mode: Mode,
599 label: String,
600 body: Box<ParseNode>,
601 #[serde(skip_serializing_if = "Option::is_none")]
602 below: Option<Box<ParseNode>>,
603 #[serde(skip_serializing_if = "Option::is_none")]
604 loc: Option<SourceLocation>,
605 },
606
607 #[serde(rename = "pmb")]
608 Pmb {
609 mode: Mode,
610 mclass: String,
611 body: Vec<ParseNode>,
612 #[serde(skip_serializing_if = "Option::is_none")]
613 loc: Option<SourceLocation>,
614 },
615
616 #[serde(rename = "tag")]
617 Tag {
618 mode: Mode,
619 body: Vec<ParseNode>,
620 tag: Vec<ParseNode>,
621 #[serde(skip_serializing_if = "Option::is_none")]
622 loc: Option<SourceLocation>,
623 },
624
625 #[serde(rename = "nonumber")]
626 NoNumber {
627 mode: Mode,
628 #[serde(skip_serializing_if = "Option::is_none")]
629 loc: Option<SourceLocation>,
630 },
631
632 #[serde(rename = "html")]
633 Html {
634 mode: Mode,
635 attributes: std::collections::HashMap<String, String>,
636 body: Vec<ParseNode>,
637 #[serde(skip_serializing_if = "Option::is_none")]
638 loc: Option<SourceLocation>,
639 },
640
641 #[serde(rename = "htmlmathml")]
642 HtmlMathMl {
643 mode: Mode,
644 html: Vec<ParseNode>,
645 mathml: Vec<ParseNode>,
646 #[serde(skip_serializing_if = "Option::is_none")]
647 loc: Option<SourceLocation>,
648 },
649
650 #[serde(rename = "includegraphics")]
651 IncludeGraphics {
652 mode: Mode,
653 alt: String,
654 width: Measurement,
655 height: Measurement,
656 totalheight: Measurement,
657 src: String,
658 #[serde(skip_serializing_if = "Option::is_none")]
659 loc: Option<SourceLocation>,
660 },
661
662 #[serde(rename = "cdlabel")]
663 CdLabel {
664 mode: Mode,
665 side: String,
666 label: Box<ParseNode>,
667 #[serde(skip_serializing_if = "Option::is_none")]
668 loc: Option<SourceLocation>,
669 },
670
671 #[serde(rename = "cdlabelparent")]
672 CdLabelParent {
673 mode: Mode,
674 fragment: Box<ParseNode>,
675 #[serde(skip_serializing_if = "Option::is_none")]
676 loc: Option<SourceLocation>,
677 },
678
679 #[serde(rename = "cdArrow")]
680 CdArrow {
681 mode: Mode,
682 direction: String,
684 #[serde(skip_serializing_if = "Option::is_none")]
687 label_above: Option<Box<ParseNode>>,
688 #[serde(skip_serializing_if = "Option::is_none")]
691 label_below: Option<Box<ParseNode>>,
692 #[serde(skip_serializing_if = "Option::is_none")]
693 loc: Option<SourceLocation>,
694 },
695}
696
697fn default_arraystretch() -> f64 {
698 1.0
699}
700
701#[derive(Debug, Clone, Serialize, Deserialize)]
703#[serde(untagged)]
704pub enum ArrayTag {
705 Auto(bool),
706 Explicit(Vec<ParseNode>),
707}
708
709impl ParseNode {
712 pub fn mode(&self) -> Mode {
713 match self {
714 Self::Atom { mode, .. }
715 | Self::MathOrd { mode, .. }
716 | Self::TextOrd { mode, .. }
717 | Self::OpToken { mode, .. }
718 | Self::AccentToken { mode, .. }
719 | Self::SpacingNode { mode, .. }
720 | Self::OrdGroup { mode, .. }
721 | Self::SupSub { mode, .. }
722 | Self::GenFrac { mode, .. }
723 | Self::Sqrt { mode, .. }
724 | Self::Accent { mode, .. }
725 | Self::AccentUnder { mode, .. }
726 | Self::Op { mode, .. }
727 | Self::OperatorName { mode, .. }
728 | Self::Font { mode, .. }
729 | Self::Text { mode, .. }
730 | Self::Color { mode, .. }
731 | Self::ColorToken { mode, .. }
732 | Self::Size { mode, .. }
733 | Self::Styling { mode, .. }
734 | Self::Sizing { mode, .. }
735 | Self::DelimSizing { mode, .. }
736 | Self::LeftRight { mode, .. }
737 | Self::LeftRightRight { mode, .. }
738 | Self::Middle { mode, .. }
739 | Self::Overline { mode, .. }
740 | Self::Underline { mode, .. }
741 | Self::Rule { mode, .. }
742 | Self::Kern { mode, .. }
743 | Self::Phantom { mode, .. }
744 | Self::VPhantom { mode, .. }
745 | Self::Smash { mode, .. }
746 | Self::MClass { mode, .. }
747 | Self::Array { mode, .. }
748 | Self::Environment { mode, .. }
749 | Self::Cr { mode, .. }
750 | Self::Infix { mode, .. }
751 | Self::Internal { mode, .. }
752 | Self::Verb { mode, .. }
753 | Self::Href { mode, .. }
754 | Self::Url { mode, .. }
755 | Self::Raw { mode, .. }
756 | Self::HBox { mode, .. }
757 | Self::HorizBrace { mode, .. }
758 | Self::Enclose { mode, .. }
759 | Self::Lap { mode, .. }
760 | Self::MathChoice { mode, .. }
761 | Self::RaiseBox { mode, .. }
762 | Self::VCenter { mode, .. }
763 | Self::XArrow { mode, .. }
764 | Self::Pmb { mode, .. }
765 | Self::Tag { mode, .. }
766 | Self::NoNumber { mode, .. }
767 | Self::Html { mode, .. }
768 | Self::HtmlMathMl { mode, .. }
769 | Self::IncludeGraphics { mode, .. }
770 | Self::CdLabel { mode, .. }
771 | Self::CdLabelParent { mode, .. }
772 | Self::CdArrow { mode, .. } => *mode,
773 }
774 }
775
776 pub fn type_name(&self) -> &'static str {
777 match self {
778 Self::Atom { .. } => "atom",
779 Self::MathOrd { .. } => "mathord",
780 Self::TextOrd { .. } => "textord",
781 Self::OpToken { .. } => "op-token",
782 Self::AccentToken { .. } => "accent-token",
783 Self::SpacingNode { .. } => "spacing",
784 Self::OrdGroup { .. } => "ordgroup",
785 Self::SupSub { .. } => "supsub",
786 Self::GenFrac { .. } => "genfrac",
787 Self::Sqrt { .. } => "sqrt",
788 Self::Accent { .. } => "accent",
789 Self::AccentUnder { .. } => "accentUnder",
790 Self::Op { .. } => "op",
791 Self::OperatorName { .. } => "operatorname",
792 Self::Font { .. } => "font",
793 Self::Text { .. } => "text",
794 Self::Color { .. } => "color",
795 Self::ColorToken { .. } => "color-token",
796 Self::Size { .. } => "size",
797 Self::Styling { .. } => "styling",
798 Self::Sizing { .. } => "sizing",
799 Self::DelimSizing { .. } => "delimsizing",
800 Self::LeftRight { .. } => "leftright",
801 Self::LeftRightRight { .. } => "leftright-right",
802 Self::Middle { .. } => "middle",
803 Self::Overline { .. } => "overline",
804 Self::Underline { .. } => "underline",
805 Self::Rule { .. } => "rule",
806 Self::Kern { .. } => "kern",
807 Self::Phantom { .. } => "phantom",
808 Self::VPhantom { .. } => "vphantom",
809 Self::Smash { .. } => "smash",
810 Self::MClass { .. } => "mclass",
811 Self::Array { .. } => "array",
812 Self::Environment { .. } => "environment",
813 Self::Cr { .. } => "cr",
814 Self::Infix { .. } => "infix",
815 Self::Internal { .. } => "internal",
816 Self::Verb { .. } => "verb",
817 Self::Href { .. } => "href",
818 Self::Url { .. } => "url",
819 Self::Raw { .. } => "raw",
820 Self::HBox { .. } => "hbox",
821 Self::HorizBrace { .. } => "horizBrace",
822 Self::Enclose { .. } => "enclose",
823 Self::Lap { .. } => "lap",
824 Self::MathChoice { .. } => "mathchoice",
825 Self::RaiseBox { .. } => "raisebox",
826 Self::VCenter { .. } => "vcenter",
827 Self::XArrow { .. } => "xArrow",
828 Self::Pmb { .. } => "pmb",
829 Self::Tag { .. } => "tag",
830 Self::NoNumber { .. } => "nonumber",
831 Self::Html { .. } => "html",
832 Self::HtmlMathMl { .. } => "htmlmathml",
833 Self::IncludeGraphics { .. } => "includegraphics",
834 Self::CdLabel { .. } => "cdlabel",
835 Self::CdLabelParent { .. } => "cdlabelparent",
836 Self::CdArrow { .. } => "cdArrow",
837 }
838 }
839
840 pub fn is_symbol_node(&self) -> bool {
842 matches!(
843 self,
844 Self::Atom { .. }
845 | Self::MathOrd { .. }
846 | Self::TextOrd { .. }
847 | Self::OpToken { .. }
848 | Self::AccentToken { .. }
849 | Self::SpacingNode { .. }
850 )
851 }
852
853 pub fn symbol_text(&self) -> Option<&str> {
855 match self {
856 Self::Atom { text, .. }
857 | Self::MathOrd { text, .. }
858 | Self::TextOrd { text, .. }
859 | Self::OpToken { text, .. }
860 | Self::AccentToken { text, .. }
861 | Self::SpacingNode { text, .. } => Some(text),
862 _ => None,
863 }
864 }
865
866 pub fn normalize_argument(arg: ParseNode) -> ParseNode {
868 if let ParseNode::OrdGroup { body, .. } = &arg {
869 if body.len() == 1 {
870 return body[0].clone();
871 }
872 }
873 arg
874 }
875
876 pub fn ord_argument(arg: ParseNode) -> Vec<ParseNode> {
878 if let ParseNode::OrdGroup { body, .. } = arg {
879 body
880 } else {
881 vec![arg]
882 }
883 }
884}
885
886#[cfg(test)]
887mod tests {
888 use super::*;
889
890 #[test]
891 fn test_serialize_mathord() {
892 let node = ParseNode::MathOrd {
893 mode: Mode::Math,
894 text: "x".to_string(),
895 loc: None,
896 };
897 let json = serde_json::to_string(&node).unwrap();
898 assert!(json.contains(r#""type":"mathord""#));
899 assert!(json.contains(r#""mode":"math""#));
900 assert!(json.contains(r#""text":"x""#));
901 }
902
903 #[test]
904 fn test_serialize_ordgroup() {
905 let node = ParseNode::OrdGroup {
906 mode: Mode::Math,
907 body: vec![
908 ParseNode::MathOrd {
909 mode: Mode::Math,
910 text: "a".to_string(),
911 loc: None,
912 },
913 ],
914 semisimple: None,
915 loc: None,
916 };
917 let json = serde_json::to_string(&node).unwrap();
918 assert!(json.contains(r#""type":"ordgroup""#));
919 }
920
921 #[test]
922 fn test_serialize_supsub() {
923 let node = ParseNode::SupSub {
924 mode: Mode::Math,
925 base: Some(Box::new(ParseNode::MathOrd {
926 mode: Mode::Math,
927 text: "x".to_string(),
928 loc: None,
929 })),
930 sup: Some(Box::new(ParseNode::TextOrd {
931 mode: Mode::Math,
932 text: "2".to_string(),
933 loc: None,
934 })),
935 sub: None,
936 loc: None,
937 };
938 let json = serde_json::to_string(&node).unwrap();
939 assert!(json.contains(r#""type":"supsub""#));
940 }
941
942 #[test]
943 fn test_serialize_genfrac() {
944 let node = ParseNode::GenFrac {
945 mode: Mode::Math,
946 continued: false,
947 numer: Box::new(ParseNode::MathOrd {
948 mode: Mode::Math,
949 text: "a".to_string(),
950 loc: None,
951 }),
952 denom: Box::new(ParseNode::MathOrd {
953 mode: Mode::Math,
954 text: "b".to_string(),
955 loc: None,
956 }),
957 has_bar_line: true,
958 left_delim: None,
959 right_delim: None,
960 bar_size: None,
961 loc: None,
962 };
963 let json = serde_json::to_string(&node).unwrap();
964 assert!(json.contains(r#""type":"genfrac""#));
965 assert!(json.contains(r#""hasBarLine":true"#));
966 }
967
968 #[test]
969 fn test_serialize_atom() {
970 let node = ParseNode::Atom {
971 mode: Mode::Math,
972 family: AtomFamily::Bin,
973 text: "+".to_string(),
974 loc: None,
975 };
976 let json = serde_json::to_string(&node).unwrap();
977 assert!(json.contains(r#""type":"atom""#));
978 assert!(json.contains(r#""family":"bin""#));
979 }
980
981 #[test]
982 fn test_roundtrip() {
983 let node = ParseNode::MathOrd {
984 mode: Mode::Math,
985 text: "x".to_string(),
986 loc: Some(SourceLocation { start: 0, end: 1 }),
987 };
988 let json = serde_json::to_string(&node).unwrap();
989 let parsed: ParseNode = serde_json::from_str(&json).unwrap();
990 assert_eq!(parsed.type_name(), "mathord");
991 assert_eq!(parsed.symbol_text(), Some("x"));
992 }
993
994 #[test]
995 fn test_mode_accessor() {
996 let node = ParseNode::Atom {
997 mode: Mode::Math,
998 family: AtomFamily::Rel,
999 text: "=".to_string(),
1000 loc: None,
1001 };
1002 assert_eq!(node.mode(), Mode::Math);
1003 }
1004
1005 #[test]
1006 fn test_normalize_argument() {
1007 let group = ParseNode::OrdGroup {
1008 mode: Mode::Math,
1009 body: vec![ParseNode::MathOrd {
1010 mode: Mode::Math,
1011 text: "x".to_string(),
1012 loc: None,
1013 }],
1014 semisimple: None,
1015 loc: None,
1016 };
1017 let normalized = ParseNode::normalize_argument(group);
1018 assert_eq!(normalized.type_name(), "mathord");
1019 }
1020}