1use crate::atom::AtomClass;
9use crate::node::{
10 AccentKind, Delim, FragmentKind, Limits, MathFont, Node, NodeKind, PhantomKind, SpaceKind,
11 Span, StackKind, TextStyle,
12};
13use crate::style::Style;
14
15const MATHML_NS: &str = "http://www.w3.org/1998/Math/MathML";
16
17const MATHML_BYTES_PER_TEX_BYTE: usize = 8;
24
25const MATHML_CAPACITY_FLOOR: usize = 160;
29
30#[must_use]
35pub fn to_mathml(node: &Node, display: bool) -> String {
36 to_mathml_with_capacity(node, display, 0)
37}
38
39#[must_use]
46pub fn to_mathml_with_capacity(node: &Node, display: bool, tex_len: usize) -> String {
47 let cap = MATHML_CAPACITY_FLOOR + tex_len.saturating_mul(MATHML_BYTES_PER_TEX_BYTE);
48 let mut w = Writer::with_capacity(cap);
49 let display_val = if display { "block" } else { "inline" };
50 w.open("math", &[("xmlns", MATHML_NS), ("display", display_val)]);
51 let style = if display { Style::Display } else { Style::Text };
52 match &node.kind {
53 NodeKind::List(items) => emit_run(&mut w, items, style, None, None),
54 _ => emit_node(&mut w, node, style),
55 }
56 w.close("math");
57 w.buf
58}
59
60pub fn to_mathml_element(node: &Node) -> String {
64 let mut w = Writer::with_capacity(0);
65 emit_node(&mut w, node, Style::Display);
66 w.buf
67}
68
69pub fn mathml_well_formed(xml: &str) -> Result<(), String> {
72 check_well_formed(xml)
73}
74
75struct Writer {
76 buf: String,
77}
78
79impl Writer {
80 fn with_capacity(cap: usize) -> Self {
81 Self {
82 buf: String::with_capacity(cap),
83 }
84 }
85
86 #[inline(always)]
87 fn open(&mut self, tag: &str, attrs: &[(&str, &str)]) {
88 self.buf.push('<');
89 self.buf.push_str(tag);
90 for &(name, value) in attrs {
91 self.buf.push(' ');
92 self.buf.push_str(name);
93 self.buf.push_str("=\"");
94 push_escaped(&mut self.buf, value, true);
95 self.buf.push('"');
96 }
97 self.buf.push('>');
98 }
99
100 #[inline(always)]
101 fn close(&mut self, tag: &str) {
102 self.buf.push('<');
103 self.buf.push('/');
104 self.buf.push_str(tag);
105 self.buf.push('>');
106 }
107
108 #[inline(always)]
109 fn text(&mut self, s: &str) {
110 push_escaped(&mut self.buf, s, false);
111 }
112
113 #[inline(always)]
114 fn char_text(&mut self, ch: char) {
115 match ch {
116 '&' => self.buf.push_str("&"),
117 '<' => self.buf.push_str("<"),
118 '>' => self.buf.push_str(">"),
119 _ => self.buf.push(ch),
120 }
121 }
122}
123
124#[inline(always)]
125fn push_escaped(buf: &mut String, s: &str, attr: bool) {
126 if !s
127 .as_bytes()
128 .iter()
129 .any(|&b| b == b'&' || b == b'<' || b == b'>' || (attr && b == b'"'))
130 {
131 buf.push_str(s);
132 return;
133 }
134 let bytes = s.as_bytes();
135 let mut clean_start = 0;
136 for (i, &b) in bytes.iter().enumerate() {
137 let esc = match b {
138 b'&' => "&",
139 b'<' => "<",
140 b'>' => ">",
141 b'"' if attr => """,
142 _ => continue,
143 };
144 if clean_start < i {
145 buf.push_str(&s[clean_start..i]);
146 }
147 buf.push_str(esc);
148 clean_start = i + 1;
149 }
150 if clean_start < s.len() {
151 buf.push_str(&s[clean_start..]);
152 }
153}
154
155fn emit_node(w: &mut Writer, node: &Node, style: Style) {
156 match &node.kind {
157 NodeKind::List(items) => {
158 w.open("mrow", &[]);
159 emit_run(w, items, style, None, None);
160 w.close("mrow");
161 }
162 NodeKind::Symbol { ch, class } => emit_symbol(w, *ch, *class),
163 NodeKind::BigOp { ch, .. } => {
164 w.open("mo", &[("movablelimits", "true")]);
165 w.char_text(*ch);
166 w.close("mo");
167 }
168 NodeKind::OpName { name, .. } => {
169 w.open("mi", &[("mathvariant", "normal")]);
170 w.text(name);
171 w.close("mi");
172 }
173 NodeKind::Scripts {
174 base,
175 sub,
176 sup,
177 primes,
178 } => emit_scripts(
179 w,
180 base.as_deref(),
181 sub.as_deref(),
182 sup.as_deref(),
183 primes,
184 style,
185 ),
186 NodeKind::Frac { num, den, spec } => {
187 emit_frac(w, num, den, spec.bar, spec.delims, spec.forced_style, style)
188 }
189 NodeKind::Radical { index, radicand } => emit_radical(w, index.as_deref(), radicand, style),
190 NodeKind::Accent { accent, base } => emit_accent(w, *accent, base, style),
191 NodeKind::LeftRight { left, right, body } => emit_left_right(w, left, right, body, style),
192 NodeKind::SizedDelim { delim, .. } => {
193 if let Some(ch) = delim.ch {
194 w.open("mo", &[]);
195 w.char_text(ch);
196 w.close("mo");
197 }
198 }
199 NodeKind::Text { body } => emit_mtext_nodes(w, body),
200 NodeKind::TextRun { text, .. } => {
201 w.open("mtext", &[]);
202 w.text(text);
203 w.close("mtext");
204 }
205 NodeKind::TextStyled { style: ts, body } => emit_text_styled(w, *ts, body),
206 NodeKind::MathIsland { body, display } => {
207 let inner_style = if *display {
208 Style::Display
209 } else {
210 Style::Text
211 };
212 w.open("mrow", &[]);
213 emit_run(w, body, inner_style, None, None);
214 w.close("mrow");
215 }
216 NodeKind::StyleChange(_)
217 | NodeKind::AlignChange(_)
218 | NodeKind::SizeChange(_)
219 | NodeKind::ColorChange(_)
220 | NodeKind::LineSpacing(_) => {
221 }
223 NodeKind::MathFont { font, body } => {
224 w.open("mstyle", &[("mathvariant", math_font_variant(*font))]);
225 emit_node(w, body, style);
226 w.close("mstyle");
227 }
228 NodeKind::Phantom { kind, body } => emit_phantom(w, *kind, body, style),
229 NodeKind::Stack {
230 kind,
231 annotation,
232 base,
233 } => emit_stack(w, *kind, annotation, base, style),
234 NodeKind::XArrow {
235 mapsto,
236 above,
237 below,
238 } => emit_xarrow(w, *mapsto, above, below.as_deref(), style),
239 NodeKind::Space(kind) => emit_space(w, *kind),
240 NodeKind::Tie => {
241 w.open("mtext", &[]);
242 w.buf.push('\u{00A0}');
243 w.close("mtext");
244 }
245 NodeKind::Linebreak => {
246 w.open("mspace", &[("linebreak", "newline")]);
247 w.close("mspace");
248 }
249 NodeKind::AlignTab => {}
250 NodeKind::AlignBlock { lines, .. } => emit_align_block(w, lines, style),
251 NodeKind::Environment { name, spec, rows } => {
252 emit_environment(w, name, spec.as_deref(), rows, style)
253 }
254 NodeKind::Fragment(kind) => emit_fragment(w, kind),
255 }
256}
257
258fn emit_run(w: &mut Writer, items: &[Node], style: Style, color: Option<&str>, size: Option<f64>) {
259 let mut items = items;
263 let mut style = style;
264 let mut color = color;
265 let mut size = size;
266 while !items.is_empty() {
267 let marker_at = items.iter().position(|n| is_remainder_marker(&n.kind));
268 match marker_at {
269 None => {
270 emit_styled_siblings(w, items, style, color, size);
271 return;
272 }
273 Some(0) => {
274 let Some((first, rest)) = items.split_first() else {
275 return;
276 };
277 let (next_style, next_color, next_size) =
278 apply_marker(&first.kind, style, color, size);
279 style = next_style;
280 color = next_color;
281 size = next_size;
282 items = rest;
283 }
284 Some(k) => {
285 emit_styled_siblings(w, &items[..k], style, color, size);
286 items = &items[k..];
287 }
288 }
289 }
290}
291
292fn is_remainder_marker(kind: &NodeKind) -> bool {
293 matches!(
294 kind,
295 NodeKind::StyleChange(_)
296 | NodeKind::AlignChange(_)
297 | NodeKind::SizeChange(_)
298 | NodeKind::ColorChange(_)
299 | NodeKind::LineSpacing(_)
300 )
301}
302
303fn apply_marker<'a>(
304 kind: &'a NodeKind,
305 style: Style,
306 color: Option<&'a str>,
307 size: Option<f64>,
308) -> (Style, Option<&'a str>, Option<f64>) {
309 match kind {
310 NodeKind::StyleChange(s) => (*s, color, size),
311 NodeKind::ColorChange(c) => (style, Some(c.as_str()), size),
312 NodeKind::SizeChange(f) => (style, color, Some(*f)),
313 NodeKind::AlignChange(_) | NodeKind::LineSpacing(_) => (style, color, size),
314 _ => (style, color, size),
315 }
316}
317
318fn emit_styled_siblings(
319 w: &mut Writer,
320 items: &[Node],
321 style: Style,
322 color: Option<&str>,
323 size: Option<f64>,
324) {
325 if items.is_empty() {
326 return;
327 }
328 let wrap = color.is_some() || size.is_some() || style_needs_mstyle(style);
329 if wrap {
330 let ds = if matches!(style, Style::Display) {
331 "true"
332 } else {
333 "false"
334 };
335 let sl = match style {
336 Style::Display | Style::Text => "0",
337 Style::Script => "1",
338 Style::ScriptScript => "2",
339 };
340 let mut pct_buf = [0u8; PERCENT_BUF_LEN];
346 let size_val = size.map(|f| write_percent(&mut pct_buf, f));
347 let mut attrs: [(&str, &str); MSTYLE_MAX_ATTRS] = [("", ""); MSTYLE_MAX_ATTRS];
348 let mut n_attrs = 0usize;
349 if style_needs_mstyle(style) {
350 attrs[n_attrs] = ("displaystyle", ds);
351 n_attrs += 1;
352 attrs[n_attrs] = ("scriptlevel", sl);
353 n_attrs += 1;
354 }
355 if let Some(c) = color {
356 attrs[n_attrs] = ("mathcolor", c);
357 n_attrs += 1;
358 }
359 if let Some(s) = size_val {
360 attrs[n_attrs] = ("mathsize", s);
361 n_attrs += 1;
362 }
363 w.open("mstyle", &attrs[..n_attrs]);
364 for n in items {
365 emit_node(w, n, style);
366 }
367 w.close("mstyle");
368 } else {
369 for n in items {
370 emit_node(w, n, style);
371 }
372 }
373}
374
375fn style_needs_mstyle(style: Style) -> bool {
376 !matches!(style, Style::Display | Style::Text)
377}
378
379const MSTYLE_MAX_ATTRS: usize = 4;
382
383const PERCENT_BUF_LEN: usize = 12;
387
388fn write_percent(buf: &mut [u8; PERCENT_BUF_LEN], factor: f64) -> &str {
394 let pct = (factor * 100.0).round();
395 let n = if pct.is_finite() {
396 pct.clamp(-10_000.0, 10_000.0) as i32
397 } else {
398 100
399 };
400 let mut len = 0usize;
401 if n < 0 {
402 buf[len] = b'-';
403 len += 1;
404 }
405 let mut mag = n.unsigned_abs();
406 let mut digits = [0u8; 10];
407 let mut nd = 0usize;
408 loop {
409 digits[nd] = b'0' + (mag % 10) as u8;
410 nd += 1;
411 mag /= 10;
412 if mag == 0 {
413 break;
414 }
415 }
416 while nd > 0 {
417 nd -= 1;
418 buf[len] = digits[nd];
419 len += 1;
420 }
421 buf[len] = b'%';
422 len += 1;
423 std::str::from_utf8(&buf[..len]).unwrap_or_default()
424}
425
426fn emit_symbol(w: &mut Writer, ch: char, class: AtomClass) {
427 let tag = symbol_tag(ch, class);
428 w.open(tag, &[]);
429 w.char_text(ch);
430 w.close(tag);
431}
432
433fn symbol_tag(ch: char, class: AtomClass) -> &'static str {
434 match class {
435 AtomClass::Ord if ch.is_ascii_digit() => "mn",
436 AtomClass::Ord => "mi",
437 AtomClass::Op
438 | AtomClass::Bin
439 | AtomClass::Rel
440 | AtomClass::Open
441 | AtomClass::Close
442 | AtomClass::Punct
443 | AtomClass::Inner => "mo",
444 }
445}
446
447fn emit_scripts(
448 w: &mut Writer,
449 base: Option<&Node>,
450 sub: Option<&Node>,
451 sup: Option<&Node>,
452 primes: &[Span],
453 style: Style,
454) {
455 let limits = scripts_as_limits(base, style);
456 let has_primes = !primes.is_empty();
457 let has_sub = sub.is_some();
458 let has_sup = sup.is_some() || has_primes;
459 if !has_sub && !has_sup {
460 match base {
461 Some(b) => emit_node(w, b, style),
462 None => {
463 w.open("mrow", &[]);
464 w.close("mrow");
465 }
466 }
467 return;
468 }
469 let tag = if limits {
470 match (has_sub, has_sup) {
471 (true, true) => "munderover",
472 (true, false) => "munder",
473 (false, true) => "mover",
474 (false, false) => "mrow",
475 }
476 } else {
477 match (has_sub, has_sup) {
478 (true, true) => "msubsup",
479 (true, false) => "msub",
480 (false, true) => "msup",
481 (false, false) => "mrow",
482 }
483 };
484 w.open(tag, &[]);
485 match base {
486 Some(b) => emit_node(w, b, style),
487 None => {
488 w.open("mrow", &[]);
489 w.close("mrow");
490 }
491 }
492 if has_sub {
493 if let Some(s) = sub {
494 emit_node(w, s, style);
495 }
496 }
497 if has_sup {
498 emit_superscript(w, sup, primes, style);
499 }
500 w.close(tag);
501}
502
503fn emit_superscript(w: &mut Writer, sup: Option<&Node>, primes: &[Span], style: Style) {
504 if primes.is_empty() {
505 if let Some(s) = sup {
506 emit_node(w, s, style);
507 }
508 return;
509 }
510 if sup.is_none() && primes.len() == 1 {
511 w.open("mo", &[]);
512 w.buf.push('′');
513 w.close("mo");
514 return;
515 }
516 w.open("mrow", &[]);
517 for _ in primes {
518 w.open("mo", &[]);
519 w.buf.push('′');
520 w.close("mo");
521 }
522 if let Some(s) = sup {
523 emit_node(w, s, style);
524 }
525 w.close("mrow");
526}
527
528#[inline(always)]
529fn scripts_as_limits(base: Option<&Node>, style: Style) -> bool {
530 let Some(node) = base else {
531 return false;
532 };
533 match &node.kind {
534 NodeKind::BigOp {
535 limits, integral, ..
536 } => match limits {
537 Limits::Limits => true,
538 Limits::NoLimits => false,
539 Limits::Default => !*integral && matches!(style, Style::Display),
540 },
541 NodeKind::OpName { limits, .. } => *limits && matches!(style, Style::Display),
542 _ => false,
543 }
544}
545
546fn emit_frac(
547 w: &mut Writer,
548 num: &Node,
549 den: &Node,
550 bar: bool,
551 delims: Option<(char, char)>,
552 forced_style: Option<Style>,
553 style: Style,
554) {
555 let wrap_style = forced_style;
556 if let Some(st) = wrap_style {
557 let ds = if matches!(st, Style::Display) {
558 "true"
559 } else {
560 "false"
561 };
562 w.open("mstyle", &[("displaystyle", ds)]);
563 emit_frac_body(w, num, den, bar, delims, style);
564 w.close("mstyle");
565 } else {
566 emit_frac_body(w, num, den, bar, delims, style);
567 }
568}
569
570fn emit_frac_body(
571 w: &mut Writer,
572 num: &Node,
573 den: &Node,
574 bar: bool,
575 delims: Option<(char, char)>,
576 style: Style,
577) {
578 if let Some((left, right)) = delims {
579 w.open("mrow", &[]);
580 emit_fence(w, left);
581 emit_mfrac(w, num, den, bar, style);
582 emit_fence(w, right);
583 w.close("mrow");
584 } else {
585 emit_mfrac(w, num, den, bar, style);
586 }
587}
588
589fn emit_mfrac(w: &mut Writer, num: &Node, den: &Node, bar: bool, style: Style) {
590 if bar {
591 w.open("mfrac", &[]);
592 } else {
593 w.open("mfrac", &[("linethickness", "0")]);
594 }
595 emit_node(w, num, style);
596 emit_node(w, den, style);
597 w.close("mfrac");
598}
599
600fn emit_fence(w: &mut Writer, ch: char) {
601 w.open("mo", &[("fence", "true"), ("stretchy", "true")]);
602 w.char_text(ch);
603 w.close("mo");
604}
605
606fn emit_radical(w: &mut Writer, index: Option<&Node>, radicand: &Node, style: Style) {
607 if let Some(ix) = index {
608 w.open("mroot", &[]);
609 emit_node(w, radicand, style);
610 emit_node(w, ix, style);
611 w.close("mroot");
612 } else {
613 w.open("msqrt", &[]);
614 emit_node(w, radicand, style);
615 w.close("msqrt");
616 }
617}
618
619fn emit_accent(w: &mut Writer, accent: AccentKind, base: &Node, style: Style) {
620 let tag = if accent.is_over() { "mover" } else { "munder" };
621 let stretchy = matches!(
622 accent,
623 AccentKind::WideHat
624 | AccentKind::WideTilde
625 | AccentKind::OverLine
626 | AccentKind::UnderLine
627 | AccentKind::OverBrace
628 | AccentKind::UnderBrace
629 | AccentKind::OverRightArrow
630 | AccentKind::OverLeftArrow
631 );
632 w.open(tag, &[]);
633 emit_node(w, base, style);
634 if stretchy {
635 w.open("mo", &[("stretchy", "true")]);
636 } else {
637 w.open("mo", &[]);
638 }
639 w.text(accent_char(accent));
640 w.close("mo");
641 w.close(tag);
642}
643
644#[inline(always)]
645fn accent_char(kind: AccentKind) -> &'static str {
646 match kind {
647 AccentKind::Hat | AccentKind::WideHat => "\u{02C6}",
648 AccentKind::Check => "\u{02C7}",
649 AccentKind::Tilde | AccentKind::WideTilde => "\u{02DC}",
650 AccentKind::Acute => "\u{00B4}",
651 AccentKind::Grave => "`",
652 AccentKind::Dot => "\u{02D9}",
653 AccentKind::Ddot => "\u{00A8}",
654 AccentKind::Breve => "\u{02D8}",
655 AccentKind::Bar => "\u{00AF}",
656 AccentKind::Vec | AccentKind::OverRightArrow => "\u{2192}",
657 AccentKind::Dddot => "\u{20DB}",
658 AccentKind::Ddddot => "\u{20DC}",
659 AccentKind::Ring => "\u{02DA}",
660 AccentKind::OverLine => "\u{203E}",
661 AccentKind::UnderLine => "_",
662 AccentKind::OverBrace => "\u{23DE}",
663 AccentKind::UnderBrace => "\u{23DF}",
664 AccentKind::OverLeftArrow => "\u{2190}",
665 }
666}
667
668fn emit_left_right(w: &mut Writer, left: &Delim, right: &Delim, body: &[Node], style: Style) {
669 w.open("mrow", &[]);
670 if let Some(ch) = left.ch {
671 emit_fence(w, ch);
672 }
673 emit_run(w, body, style, None, None);
674 if let Some(ch) = right.ch {
675 emit_fence(w, ch);
676 }
677 w.close("mrow");
678}
679
680fn emit_mtext_nodes(w: &mut Writer, body: &[Node]) {
681 w.open("mtext", &[]);
682 collect_text(w, body);
683 w.close("mtext");
684}
685
686fn collect_text(w: &mut Writer, body: &[Node]) {
687 for n in body {
688 match &n.kind {
689 NodeKind::TextRun { text, .. } => w.text(text),
690 NodeKind::Symbol { ch, .. } => w.char_text(*ch),
691 NodeKind::List(items) | NodeKind::Text { body: items } => collect_text(w, items),
692 NodeKind::TextStyled { body, .. } => collect_text(w, body),
693 NodeKind::Space(_) => w.buf.push(' '),
694 NodeKind::Tie => w.buf.push('\u{00A0}'),
695 _ => {}
696 }
697 }
698}
699
700fn emit_text_styled(w: &mut Writer, ts: TextStyle, body: &[Node]) {
701 let variant = match ts {
702 TextStyle::Bold => "bold",
703 TextStyle::Emph => "italic",
704 TextStyle::Underline => "normal",
705 };
706 w.open("mtext", &[("mathvariant", variant)]);
707 collect_text(w, body);
708 w.close("mtext");
709}
710
711fn math_font_variant(font: MathFont) -> &'static str {
712 match font {
713 MathFont::Blackboard => "double-struck",
714 MathFont::Calligraphic => "script",
715 MathFont::Roman => "normal",
716 MathFont::Bold => "bold",
717 MathFont::BoldItalic => "bold-italic",
718 MathFont::SansSerif => "sans-serif",
719 MathFont::Typewriter => "monospace",
720 MathFont::Italic => "italic",
721 }
722}
723
724fn emit_phantom(w: &mut Writer, kind: PhantomKind, body: &Node, style: Style) {
725 match kind {
726 PhantomKind::Full => {
727 w.open("mphantom", &[]);
728 emit_node(w, body, style);
729 w.close("mphantom");
730 }
731 PhantomKind::Horizontal => {
732 w.open("mpadded", &[("height", "0"), ("depth", "0")]);
733 w.open("mphantom", &[]);
734 emit_node(w, body, style);
735 w.close("mphantom");
736 w.close("mpadded");
737 }
738 PhantomKind::Vertical => {
739 w.open("mpadded", &[("width", "0")]);
740 w.open("mphantom", &[]);
741 emit_node(w, body, style);
742 w.close("mphantom");
743 w.close("mpadded");
744 }
745 }
746}
747
748fn emit_stack(w: &mut Writer, kind: StackKind, annotation: &Node, base: &Node, style: Style) {
749 let tag = match kind {
750 StackKind::Stackrel | StackKind::Overset => "mover",
751 StackKind::Underset => "munder",
752 };
753 w.open(tag, &[]);
754 emit_node(w, base, style);
755 emit_node(w, annotation, style);
756 w.close(tag);
757}
758
759fn emit_xarrow(w: &mut Writer, mapsto: bool, above: &Node, below: Option<&Node>, style: Style) {
760 let arrow = if mapsto { "\u{21A6}" } else { "\u{2192}" };
761 let tag = if below.is_some() {
762 "munderover"
763 } else {
764 "mover"
765 };
766 w.open(tag, &[]);
767 w.open("mo", &[("stretchy", "true")]);
768 w.text(arrow);
769 w.close("mo");
770 if let Some(b) = below {
771 emit_node(w, b, style);
772 }
773 emit_node(w, above, style);
774 w.close(tag);
775}
776
777fn emit_space(w: &mut Writer, kind: SpaceKind) {
778 let width = em_from_mu(kind.mu());
779 w.open("mspace", &[("width", &width)]);
780 w.close("mspace");
781}
782
783fn em_from_mu(mu: i32) -> String {
784 let sign = if mu < 0 { -1 } else { 1 };
786 let milli = if mu == 0 {
787 0
788 } else {
789 (mu * 1000 + 9 * sign) / 18
790 };
791 let mut s = String::new();
792 if milli < 0 {
793 s.push('-');
794 }
795 let abs = milli.unsigned_abs();
796 let whole = abs / 1000;
797 let frac = abs % 1000;
798 s.push_str(&whole.to_string());
799 if frac != 0 {
800 s.push('.');
801 if frac < 100 {
802 s.push('0');
803 }
804 if frac < 10 {
805 s.push('0');
806 }
807 s.push_str(&frac.to_string());
808 while s.ends_with('0') && s.contains('.') {
809 s.pop();
810 }
811 if s.ends_with('.') {
812 s.pop();
813 }
814 }
815 s.push_str("em");
816 s
817}
818
819fn emit_environment(
820 w: &mut Writer,
821 name: &str,
822 spec: Option<&str>,
823 rows: &[Vec<Node>],
824 style: Style,
825) {
826 let fences = env_fences(name);
827 let columnalign = env_columnalign(name, spec, column_count(rows));
828 if let Some((left, right)) = fences {
829 w.open("mrow", &[]);
830 if let Some(ch) = left {
831 emit_fence(w, ch);
832 }
833 emit_table(w, rows, columnalign.as_deref(), style);
834 if let Some(ch) = right {
835 emit_fence(w, ch);
836 }
837 w.close("mrow");
838 } else {
839 emit_table(w, rows, columnalign.as_deref(), style);
840 }
841}
842
843#[inline(always)]
844fn env_fences(name: &str) -> Option<(Option<char>, Option<char>)> {
845 match name {
846 "pmatrix" => Some((Some('('), Some(')'))),
847 "bmatrix" => Some((Some('['), Some(']'))),
848 "Bmatrix" => Some((Some('{'), Some('}'))),
849 "vmatrix" => Some((Some('|'), Some('|'))),
850 "Vmatrix" => Some((Some('\u{2016}'), Some('\u{2016}'))),
851 "cases" => Some((Some('{'), None)),
852 _ => None,
853 }
854}
855
856fn column_count(rows: &[Vec<Node>]) -> usize {
857 rows.iter().map(Vec::len).max().unwrap_or(0)
858}
859
860fn env_columnalign(name: &str, spec: Option<&str>, cols: usize) -> Option<String> {
861 if let Some(spec) = spec {
862 let mut parts = Vec::new();
863 for ch in spec.chars() {
864 match ch {
865 'l' => parts.push("left"),
866 'r' => parts.push("right"),
867 'c' => parts.push("center"),
868 _ => {}
869 }
870 }
871 if !parts.is_empty() {
872 return Some(parts.join(" "));
873 }
874 }
875 match name {
876 "align" | "align*" | "aligned" => {
877 if cols == 0 {
878 return None;
879 }
880 let mut parts = Vec::with_capacity(cols);
881 for i in 0..cols {
882 parts.push(if i % 2 == 0 { "right" } else { "left" });
883 }
884 Some(parts.join(" "))
885 }
886 "cases" => Some("left left".to_owned()),
887 _ => None,
888 }
889}
890
891fn emit_align_block(w: &mut Writer, lines: &[Node], style: Style) {
892 w.open("mtable", &[]);
893 for line in lines {
894 w.open("mtr", &[]);
895 w.open("mtd", &[]);
896 emit_node(w, line, style);
897 w.close("mtd");
898 w.close("mtr");
899 }
900 w.close("mtable");
901}
902
903fn emit_table(w: &mut Writer, rows: &[Vec<Node>], columnalign: Option<&str>, style: Style) {
904 if let Some(align) = columnalign {
905 w.open("mtable", &[("columnalign", align)]);
906 } else {
907 w.open("mtable", &[]);
908 }
909 let width = column_count(rows);
910 for row in rows {
911 w.open("mtr", &[]);
912 for i in 0..width {
913 w.open("mtd", &[]);
914 if let Some(cell) = row.get(i) {
915 emit_node(w, cell, style);
916 }
917 w.close("mtd");
918 }
919 w.close("mtr");
920 }
921 w.close("mtable");
922}
923
924fn emit_fragment(w: &mut Writer, kind: &FragmentKind) {
925 match kind {
926 FragmentKind::UnmatchedClose | FragmentKind::RedundantMathShift => {}
927 FragmentKind::StrayRight(delim) => {
928 if let Some(ch) = delim.ch {
929 emit_fence(w, ch);
930 }
931 }
932 }
933}
934
935fn check_well_formed(xml: &str) -> Result<(), String> {
936 let bytes = xml.as_bytes();
937 let mut i = 0;
938 let mut stack: Vec<(String, usize)> = Vec::new();
939 while i < bytes.len() {
940 if bytes[i] == b'<' {
941 let start = i;
942 i += 1;
943 if i >= bytes.len() {
944 return Err("truncated tag".to_owned());
945 }
946 if bytes[i] == b'/' {
947 i += 1;
948 let name = read_name(bytes, &mut i)?;
949 skip_ws(bytes, &mut i);
950 if bytes.get(i).copied() != Some(b'>') {
951 return Err(format!("malformed close tag at {start}"));
952 }
953 i += 1;
954 match stack.pop() {
955 Some((open, _)) if open == name => {}
956 Some((open, at)) => {
957 return Err(format!(
958 "close </{name}> at {start} does not match <{open}> opened at {at}"
959 ));
960 }
961 None => return Err(format!("unmatched close </{name}> at {start}")),
962 }
963 } else {
964 let name = read_name(bytes, &mut i)?;
965 read_attrs(bytes, &mut i)?;
966 if bytes.get(i).copied() == Some(b'/') {
967 return Err(format!(
968 "self-closing tag <{name}/> at {start} is forbidden"
969 ));
970 }
971 if bytes.get(i).copied() != Some(b'>') {
972 return Err(format!("unterminated open tag <{name}> at {start}"));
973 }
974 i += 1;
975 stack.push((name, start));
976 }
977 } else {
978 if bytes[i] == b'&' {
980 i += 1;
981 consume_entity(bytes, &mut i)?;
982 } else {
983 i += 1;
984 }
985 }
986 }
987 if let Some((open, at)) = stack.last() {
988 return Err(format!("unclosed <{open}> opened at {at}"));
989 }
990 Ok(())
991}
992
993fn read_name(bytes: &[u8], i: &mut usize) -> Result<String, String> {
994 let start = *i;
995 if *i >= bytes.len() || !bytes[*i].is_ascii_alphabetic() {
996 return Err(format!("expected tag name at {start}"));
997 }
998 *i += 1;
999 while *i < bytes.len() && (bytes[*i].is_ascii_alphanumeric() || bytes[*i] == b'-') {
1000 *i += 1;
1001 }
1002 let name = core::str::from_utf8(&bytes[start..*i]).map_err(|_| "non-utf8 tag name")?;
1003 Ok(name.to_owned())
1004}
1005
1006fn skip_ws(bytes: &[u8], i: &mut usize) {
1007 while *i < bytes.len() && bytes[*i].is_ascii_whitespace() {
1008 *i += 1;
1009 }
1010}
1011
1012fn read_attrs(bytes: &[u8], i: &mut usize) -> Result<(), String> {
1013 loop {
1014 skip_ws(bytes, i);
1015 if *i >= bytes.len() {
1016 return Err("truncated attributes".to_owned());
1017 }
1018 match bytes[*i] {
1019 b'>' | b'/' => return Ok(()),
1020 b'a'..=b'z' | b'A'..=b'Z' => {
1021 let _ = read_name(bytes, i)?;
1022 skip_ws(bytes, i);
1023 if bytes.get(*i).copied() != Some(b'=') {
1024 return Err("attribute missing '='".to_owned());
1025 }
1026 *i += 1;
1027 skip_ws(bytes, i);
1028 if bytes.get(*i).copied() != Some(b'"') {
1029 return Err("attribute value must be double-quoted".to_owned());
1030 }
1031 *i += 1;
1032 while *i < bytes.len() && bytes[*i] != b'"' {
1033 if bytes[*i] == b'&' {
1034 *i += 1;
1035 consume_entity(bytes, i)?;
1036 } else if bytes[*i] == b'<' {
1037 return Err("raw '<' in attribute".to_owned());
1038 } else {
1039 *i += 1;
1040 }
1041 }
1042 if bytes.get(*i).copied() != Some(b'"') {
1043 return Err("unterminated attribute value".to_owned());
1044 }
1045 *i += 1;
1046 }
1047 _ => return Err(format!("unexpected byte 0x{:02x} in tag", bytes[*i])),
1048 }
1049 }
1050}
1051
1052fn consume_entity(bytes: &[u8], i: &mut usize) -> Result<(), String> {
1053 let start = *i;
1054 if bytes.get(*i).copied() == Some(b'#') {
1055 *i += 1;
1056 let hex = bytes.get(*i).copied() == Some(b'x') || bytes.get(*i).copied() == Some(b'X');
1057 if hex {
1058 *i += 1;
1059 }
1060 let digit_start = *i;
1061 while *i < bytes.len() {
1062 let b = bytes[*i];
1063 let ok = if hex {
1064 b.is_ascii_hexdigit()
1065 } else {
1066 b.is_ascii_digit()
1067 };
1068 if !ok {
1069 break;
1070 }
1071 *i += 1;
1072 }
1073 if *i == digit_start {
1074 return Err("empty numeric entity".to_owned());
1075 }
1076 } else {
1077 while *i < bytes.len() && bytes[*i].is_ascii_alphabetic() {
1078 *i += 1;
1079 }
1080 if *i == start {
1081 return Err("bare '&'".to_owned());
1082 }
1083 }
1084 if bytes.get(*i).copied() != Some(b';') {
1085 return Err("entity missing ';'".to_owned());
1086 }
1087 *i += 1;
1088 Ok(())
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use super::{PERCENT_BUF_LEN, em_from_mu, write_percent};
1094
1095 #[test]
1096 fn em_from_mu_thousandths() {
1097 assert_eq!(em_from_mu(3), "0.167em");
1098 assert_eq!(em_from_mu(18), "1em");
1099 assert_eq!(em_from_mu(-3), "-0.167em");
1100 assert_eq!(em_from_mu(0), "0em");
1101 }
1102
1103 fn percent_size_ref(factor: f64) -> String {
1106 let pct = (factor * 100.0).round();
1107 let n = if pct.is_finite() {
1108 pct.clamp(-10_000.0, 10_000.0) as i32
1109 } else {
1110 100
1111 };
1112 format!("{n}%")
1113 }
1114
1115 #[test]
1116 fn percent_digits_match_format_reference() {
1117 let mut buf = [0u8; PERCENT_BUF_LEN];
1118 let mut factors: Vec<f64> = Vec::new();
1122 let mut f = -300.0;
1123 while f <= 300.0 {
1124 factors.push(f);
1125 f += 0.0173;
1126 }
1127 factors.extend([
1128 -0.0,
1129 0.0,
1130 0.004_999,
1131 0.005,
1132 0.494_999,
1133 0.495,
1134 0.994_999,
1135 0.995,
1136 1.0,
1137 9.995,
1138 10.0,
1139 99.995,
1140 100.0,
1141 1e3,
1142 1e5,
1143 f64::NAN,
1144 f64::INFINITY,
1145 f64::NEG_INFINITY,
1146 f64::MIN,
1147 f64::MAX,
1148 ]);
1149 for f in factors {
1150 assert_eq!(
1151 write_percent(&mut buf, f),
1152 percent_size_ref(f),
1153 "factor {f}"
1154 );
1155 }
1156 for f in [-100.0, 100.0, -100.0 - 1e-9, 100.0 + 1e-9, 0.0, 10.0, -10.0] {
1158 assert_eq!(
1159 write_percent(&mut buf, f),
1160 percent_size_ref(f),
1161 "clamp corner {f}"
1162 );
1163 }
1164 assert_eq!(write_percent(&mut buf, 0.0), "0%");
1166 assert_eq!(write_percent(&mut buf, 0.5), "50%");
1167 assert_eq!(write_percent(&mut buf, 1.0), "100%");
1168 assert_eq!(write_percent(&mut buf, 2.074), "207%");
1169 assert_eq!(write_percent(&mut buf, 100.0), "10000%");
1170 assert_eq!(write_percent(&mut buf, -0.8), "-80%");
1171 assert_eq!(write_percent(&mut buf, f64::NAN), "100%");
1172 }
1173}