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