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 = "html")]
626 Html {
627 mode: Mode,
628 attributes: std::collections::HashMap<String, String>,
629 body: Vec<ParseNode>,
630 #[serde(skip_serializing_if = "Option::is_none")]
631 loc: Option<SourceLocation>,
632 },
633
634 #[serde(rename = "htmlmathml")]
635 HtmlMathMl {
636 mode: Mode,
637 html: Vec<ParseNode>,
638 mathml: Vec<ParseNode>,
639 #[serde(skip_serializing_if = "Option::is_none")]
640 loc: Option<SourceLocation>,
641 },
642
643 #[serde(rename = "includegraphics")]
644 IncludeGraphics {
645 mode: Mode,
646 alt: String,
647 width: Measurement,
648 height: Measurement,
649 totalheight: Measurement,
650 src: String,
651 #[serde(skip_serializing_if = "Option::is_none")]
652 loc: Option<SourceLocation>,
653 },
654
655 #[serde(rename = "cdlabel")]
656 CdLabel {
657 mode: Mode,
658 side: String,
659 label: Box<ParseNode>,
660 #[serde(skip_serializing_if = "Option::is_none")]
661 loc: Option<SourceLocation>,
662 },
663
664 #[serde(rename = "cdlabelparent")]
665 CdLabelParent {
666 mode: Mode,
667 fragment: Box<ParseNode>,
668 #[serde(skip_serializing_if = "Option::is_none")]
669 loc: Option<SourceLocation>,
670 },
671
672 #[serde(rename = "cdArrow")]
673 CdArrow {
674 mode: Mode,
675 direction: String,
677 #[serde(skip_serializing_if = "Option::is_none")]
680 label_above: Option<Box<ParseNode>>,
681 #[serde(skip_serializing_if = "Option::is_none")]
684 label_below: Option<Box<ParseNode>>,
685 #[serde(skip_serializing_if = "Option::is_none")]
686 loc: Option<SourceLocation>,
687 },
688}
689
690fn default_arraystretch() -> f64 {
691 1.0
692}
693
694#[derive(Debug, Clone, Serialize, Deserialize)]
696#[serde(untagged)]
697pub enum ArrayTag {
698 Auto(bool),
699 Explicit(Vec<ParseNode>),
700}
701
702impl ParseNode {
705 pub fn mode(&self) -> Mode {
706 match self {
707 Self::Atom { mode, .. }
708 | Self::MathOrd { mode, .. }
709 | Self::TextOrd { mode, .. }
710 | Self::OpToken { mode, .. }
711 | Self::AccentToken { mode, .. }
712 | Self::SpacingNode { mode, .. }
713 | Self::OrdGroup { mode, .. }
714 | Self::SupSub { mode, .. }
715 | Self::GenFrac { mode, .. }
716 | Self::Sqrt { mode, .. }
717 | Self::Accent { mode, .. }
718 | Self::AccentUnder { mode, .. }
719 | Self::Op { mode, .. }
720 | Self::OperatorName { mode, .. }
721 | Self::Font { mode, .. }
722 | Self::Text { mode, .. }
723 | Self::Color { mode, .. }
724 | Self::ColorToken { mode, .. }
725 | Self::Size { mode, .. }
726 | Self::Styling { mode, .. }
727 | Self::Sizing { mode, .. }
728 | Self::DelimSizing { mode, .. }
729 | Self::LeftRight { mode, .. }
730 | Self::LeftRightRight { mode, .. }
731 | Self::Middle { mode, .. }
732 | Self::Overline { mode, .. }
733 | Self::Underline { mode, .. }
734 | Self::Rule { mode, .. }
735 | Self::Kern { mode, .. }
736 | Self::Phantom { mode, .. }
737 | Self::VPhantom { mode, .. }
738 | Self::Smash { mode, .. }
739 | Self::MClass { mode, .. }
740 | Self::Array { mode, .. }
741 | Self::Environment { mode, .. }
742 | Self::Cr { mode, .. }
743 | Self::Infix { mode, .. }
744 | Self::Internal { mode, .. }
745 | Self::Verb { mode, .. }
746 | Self::Href { mode, .. }
747 | Self::Url { mode, .. }
748 | Self::Raw { mode, .. }
749 | Self::HBox { mode, .. }
750 | Self::HorizBrace { mode, .. }
751 | Self::Enclose { mode, .. }
752 | Self::Lap { mode, .. }
753 | Self::MathChoice { mode, .. }
754 | Self::RaiseBox { mode, .. }
755 | Self::VCenter { mode, .. }
756 | Self::XArrow { mode, .. }
757 | Self::Pmb { mode, .. }
758 | Self::Tag { mode, .. }
759 | Self::Html { mode, .. }
760 | Self::HtmlMathMl { mode, .. }
761 | Self::IncludeGraphics { mode, .. }
762 | Self::CdLabel { mode, .. }
763 | Self::CdLabelParent { mode, .. }
764 | Self::CdArrow { mode, .. } => *mode,
765 }
766 }
767
768 pub fn type_name(&self) -> &'static str {
769 match self {
770 Self::Atom { .. } => "atom",
771 Self::MathOrd { .. } => "mathord",
772 Self::TextOrd { .. } => "textord",
773 Self::OpToken { .. } => "op-token",
774 Self::AccentToken { .. } => "accent-token",
775 Self::SpacingNode { .. } => "spacing",
776 Self::OrdGroup { .. } => "ordgroup",
777 Self::SupSub { .. } => "supsub",
778 Self::GenFrac { .. } => "genfrac",
779 Self::Sqrt { .. } => "sqrt",
780 Self::Accent { .. } => "accent",
781 Self::AccentUnder { .. } => "accentUnder",
782 Self::Op { .. } => "op",
783 Self::OperatorName { .. } => "operatorname",
784 Self::Font { .. } => "font",
785 Self::Text { .. } => "text",
786 Self::Color { .. } => "color",
787 Self::ColorToken { .. } => "color-token",
788 Self::Size { .. } => "size",
789 Self::Styling { .. } => "styling",
790 Self::Sizing { .. } => "sizing",
791 Self::DelimSizing { .. } => "delimsizing",
792 Self::LeftRight { .. } => "leftright",
793 Self::LeftRightRight { .. } => "leftright-right",
794 Self::Middle { .. } => "middle",
795 Self::Overline { .. } => "overline",
796 Self::Underline { .. } => "underline",
797 Self::Rule { .. } => "rule",
798 Self::Kern { .. } => "kern",
799 Self::Phantom { .. } => "phantom",
800 Self::VPhantom { .. } => "vphantom",
801 Self::Smash { .. } => "smash",
802 Self::MClass { .. } => "mclass",
803 Self::Array { .. } => "array",
804 Self::Environment { .. } => "environment",
805 Self::Cr { .. } => "cr",
806 Self::Infix { .. } => "infix",
807 Self::Internal { .. } => "internal",
808 Self::Verb { .. } => "verb",
809 Self::Href { .. } => "href",
810 Self::Url { .. } => "url",
811 Self::Raw { .. } => "raw",
812 Self::HBox { .. } => "hbox",
813 Self::HorizBrace { .. } => "horizBrace",
814 Self::Enclose { .. } => "enclose",
815 Self::Lap { .. } => "lap",
816 Self::MathChoice { .. } => "mathchoice",
817 Self::RaiseBox { .. } => "raisebox",
818 Self::VCenter { .. } => "vcenter",
819 Self::XArrow { .. } => "xArrow",
820 Self::Pmb { .. } => "pmb",
821 Self::Tag { .. } => "tag",
822 Self::Html { .. } => "html",
823 Self::HtmlMathMl { .. } => "htmlmathml",
824 Self::IncludeGraphics { .. } => "includegraphics",
825 Self::CdLabel { .. } => "cdlabel",
826 Self::CdLabelParent { .. } => "cdlabelparent",
827 Self::CdArrow { .. } => "cdArrow",
828 }
829 }
830
831 pub fn is_symbol_node(&self) -> bool {
833 matches!(
834 self,
835 Self::Atom { .. }
836 | Self::MathOrd { .. }
837 | Self::TextOrd { .. }
838 | Self::OpToken { .. }
839 | Self::AccentToken { .. }
840 | Self::SpacingNode { .. }
841 )
842 }
843
844 pub fn symbol_text(&self) -> Option<&str> {
846 match self {
847 Self::Atom { text, .. }
848 | Self::MathOrd { text, .. }
849 | Self::TextOrd { text, .. }
850 | Self::OpToken { text, .. }
851 | Self::AccentToken { text, .. }
852 | Self::SpacingNode { text, .. } => Some(text),
853 _ => None,
854 }
855 }
856
857 pub fn normalize_argument(arg: ParseNode) -> ParseNode {
859 if let ParseNode::OrdGroup { body, .. } = &arg {
860 if body.len() == 1 {
861 return body[0].clone();
862 }
863 }
864 arg
865 }
866
867 pub fn ord_argument(arg: ParseNode) -> Vec<ParseNode> {
869 if let ParseNode::OrdGroup { body, .. } = arg {
870 body
871 } else {
872 vec![arg]
873 }
874 }
875}
876
877#[cfg(test)]
878mod tests {
879 use super::*;
880
881 #[test]
882 fn test_serialize_mathord() {
883 let node = ParseNode::MathOrd {
884 mode: Mode::Math,
885 text: "x".to_string(),
886 loc: None,
887 };
888 let json = serde_json::to_string(&node).unwrap();
889 assert!(json.contains(r#""type":"mathord""#));
890 assert!(json.contains(r#""mode":"math""#));
891 assert!(json.contains(r#""text":"x""#));
892 }
893
894 #[test]
895 fn test_serialize_ordgroup() {
896 let node = ParseNode::OrdGroup {
897 mode: Mode::Math,
898 body: vec![
899 ParseNode::MathOrd {
900 mode: Mode::Math,
901 text: "a".to_string(),
902 loc: None,
903 },
904 ],
905 semisimple: None,
906 loc: None,
907 };
908 let json = serde_json::to_string(&node).unwrap();
909 assert!(json.contains(r#""type":"ordgroup""#));
910 }
911
912 #[test]
913 fn test_serialize_supsub() {
914 let node = ParseNode::SupSub {
915 mode: Mode::Math,
916 base: Some(Box::new(ParseNode::MathOrd {
917 mode: Mode::Math,
918 text: "x".to_string(),
919 loc: None,
920 })),
921 sup: Some(Box::new(ParseNode::TextOrd {
922 mode: Mode::Math,
923 text: "2".to_string(),
924 loc: None,
925 })),
926 sub: None,
927 loc: None,
928 };
929 let json = serde_json::to_string(&node).unwrap();
930 assert!(json.contains(r#""type":"supsub""#));
931 }
932
933 #[test]
934 fn test_serialize_genfrac() {
935 let node = ParseNode::GenFrac {
936 mode: Mode::Math,
937 continued: false,
938 numer: Box::new(ParseNode::MathOrd {
939 mode: Mode::Math,
940 text: "a".to_string(),
941 loc: None,
942 }),
943 denom: Box::new(ParseNode::MathOrd {
944 mode: Mode::Math,
945 text: "b".to_string(),
946 loc: None,
947 }),
948 has_bar_line: true,
949 left_delim: None,
950 right_delim: None,
951 bar_size: None,
952 loc: None,
953 };
954 let json = serde_json::to_string(&node).unwrap();
955 assert!(json.contains(r#""type":"genfrac""#));
956 assert!(json.contains(r#""hasBarLine":true"#));
957 }
958
959 #[test]
960 fn test_serialize_atom() {
961 let node = ParseNode::Atom {
962 mode: Mode::Math,
963 family: AtomFamily::Bin,
964 text: "+".to_string(),
965 loc: None,
966 };
967 let json = serde_json::to_string(&node).unwrap();
968 assert!(json.contains(r#""type":"atom""#));
969 assert!(json.contains(r#""family":"bin""#));
970 }
971
972 #[test]
973 fn test_roundtrip() {
974 let node = ParseNode::MathOrd {
975 mode: Mode::Math,
976 text: "x".to_string(),
977 loc: Some(SourceLocation { start: 0, end: 1 }),
978 };
979 let json = serde_json::to_string(&node).unwrap();
980 let parsed: ParseNode = serde_json::from_str(&json).unwrap();
981 assert_eq!(parsed.type_name(), "mathord");
982 assert_eq!(parsed.symbol_text(), Some("x"));
983 }
984
985 #[test]
986 fn test_mode_accessor() {
987 let node = ParseNode::Atom {
988 mode: Mode::Math,
989 family: AtomFamily::Rel,
990 text: "=".to_string(),
991 loc: None,
992 };
993 assert_eq!(node.mode(), Mode::Math);
994 }
995
996 #[test]
997 fn test_normalize_argument() {
998 let group = ParseNode::OrdGroup {
999 mode: Mode::Math,
1000 body: vec![ParseNode::MathOrd {
1001 mode: Mode::Math,
1002 text: "x".to_string(),
1003 loc: None,
1004 }],
1005 semisimple: None,
1006 loc: None,
1007 };
1008 let normalized = ParseNode::normalize_argument(group);
1009 assert_eq!(normalized.type_name(), "mathord");
1010 }
1011}