1use std::{
7 collections::VecDeque,
8 io::{self, Write},
9};
10
11use crate::{
12 config::{DisplayMode, RenderConfig},
13 event::{
14 ArrayColumn, ColorChange, ColorTarget, ColumnAlignment, Content, DelimiterType,
15 EnvironmentFlow, Event, Font, Grouping, Line, ScriptPosition, ScriptType, StateChange,
16 Style, Visual,
17 },
18};
19
20struct MathmlWriter<'a, I: Iterator, W> {
21 input: ManyPeek<I>,
22 writer: W,
23 config: RenderConfig<'a>,
24 env_stack: Vec<Environment>,
25 state_stack: Vec<State>,
26 previous_atom: Option<Atom>,
27}
28
29impl<'a, I, W, E> MathmlWriter<'a, I, W>
30where
31 I: Iterator<Item = Result<Event<'a>, E>>,
32 W: io::Write,
33 E: std::error::Error,
34{
35 fn new(input: I, writer: W, config: RenderConfig<'a>) -> Self {
36 let mut state_stack = Vec::with_capacity(16);
38 state_stack.push(State {
39 font: None,
40 text_color: None,
41 border_color: None,
42 background_color: None,
43 style: None,
44 });
45 let env_stack = Vec::with_capacity(16);
46 Self {
47 input: ManyPeek::new(input),
48 writer,
49 config,
50 env_stack,
51 state_stack,
52 previous_atom: None,
53 }
54 }
55
56 fn open_tag(&mut self, tag: &str, classes: Option<&str>) -> io::Result<()> {
57 let State {
58 text_color,
59 border_color,
60 background_color,
61 style,
62 font: _,
63 } = *self.state();
64 write!(self.writer, "<{}", tag)?;
65 if let Some(style) = style {
66 if !matches!(
67 self.env_stack.last(),
68 Some(
69 Environment::Script {
70 ty: ScriptType::Subscript | ScriptType::Superscript,
71 count: 0,
72 ..
73 } | Environment::Script {
74 ty: ScriptType::SubSuperscript,
75 count: 0 | 1,
76 ..
77 }
78 )
79 ) {
80 let args = match style {
81 Style::Display => (true, 0),
82 Style::Text => (false, 0),
83 Style::Script => (false, 1),
84 Style::ScriptScript => (false, 2),
85 };
86 write!(
87 self.writer,
88 " displaystyle=\"{}\" scriptlevel=\"{}\"",
89 args.0, args.1
90 )?;
91 }
92 }
93
94 let prefix = |style_written: &mut bool| {
95 if !*style_written {
96 *style_written = true;
97 " style=\""
98 } else {
99 "; "
100 }
101 };
102
103 let mut style_written = false;
104 if let Some((r, g, b)) = text_color {
105 write!(self.writer, " style=\"color: rgb({r} {g} {b})")?;
106 style_written = true;
107 }
108 if let Some((r, g, b)) = border_color {
109 write!(
110 self.writer,
111 "{}border: 0.06em solid rgb({r} {g} {b})",
112 prefix(&mut style_written)
113 )?;
114 }
115 if let Some((r, g, b)) = background_color {
116 write!(
117 self.writer,
118 "{}background-color: rgb({r} {g} {b})",
119 prefix(&mut style_written)
120 )?;
121 }
122 if style_written {
123 self.writer.write_all(b"\"")?;
124 }
125 if let Some(classes) = classes {
126 write!(self.writer, " class=\"{}\"", classes)?;
127 }
128 Ok(())
129 }
130
131 fn write_event(&mut self, event: Result<Event<'a>, E>) -> io::Result<()> {
132 match event {
133 Ok(Event::Content(content)) => self.write_content(content, false),
134 Ok(Event::Begin(grouping)) => {
135 self.previous_atom = None;
146 if grouping.is_math_env() {
147 self.state_stack.push(State::default())
148 } else {
149 let last_state = *self.state();
150 self.state_stack.push(last_state);
151 while let Some(Ok(Event::StateChange(state_change))) = self.input.peek_first() {
152 let state_change = *state_change;
153 self.handle_state_change(state_change);
154 self.input.next();
155 }
156 self.open_tag("mrow", None)?;
157 self.writer.write_all(b">")?;
158 *self.state_stack.last_mut().expect("state stack is empty") = State {
161 font: self.state().font,
162 ..State::default()
163 };
164 }
165
166 macro_rules! env_horizontal_lines {
167 () => {
168 if let Some(Ok(Event::EnvironmentFlow(EnvironmentFlow::StartLines {
169 lines,
170 }))) = self.input.peek_first()
171 {
172 env_horizontal_lines(&mut self.writer, lines)?;
173 self.input.next();
174 }
175 };
176 }
177
178 let env_group = match grouping {
179 Grouping::Normal => EnvGrouping::Normal,
180 Grouping::LeftRight(opening, closing) => {
181 if let Some(delim) = opening {
182 self.open_tag("mo", None)?;
183 self.writer.write_all(b" stretchy=\"true\">")?;
184 let mut buf = [0u8; 4];
185 self.writer
186 .write_all(delim.encode_utf8(&mut buf).as_bytes())?;
187 self.writer.write_all(b"</mo>")?;
188 }
189 self.previous_atom = Some(Atom::Open);
190 EnvGrouping::LeftRight { closing }
191 }
192 Grouping::Align { eq_numbers } => {
193 self.writer
194 .write_all(b"<mtable class=\"menv-alignlike menv-align")?;
195 if eq_numbers {
196 self.writer.write_all(b" menv-with-eqn")?;
197 }
198 self.writer.write_all(b"\"><mtr")?;
199 env_horizontal_lines!();
200 self.writer.write_all(b"><mtd>")?;
201 EnvGrouping::Align
202 }
203 Grouping::Matrix { alignment } => {
204 self.writer.write_all(b"<mtable class=\"menv-arraylike")?;
205 self.writer.write_all(match alignment {
206 ColumnAlignment::Left => b" menv-cells-left\"",
207 ColumnAlignment::Center => b"\"",
208 ColumnAlignment::Right => b" menv-cells-right\"",
209 })?;
210 self.writer.write_all(b"><mtr")?;
211 env_horizontal_lines!();
212 self.writer.write_all(b"><mtd>")?;
213 EnvGrouping::Matrix
214 }
215 Grouping::Cases { left } => {
216 self.writer.write_all(b"<mrow>")?;
217 if left {
218 self.writer.write_all(b"<mo stretchy=\"true\">{</mo>")?;
219 }
220 self.writer
221 .write_all(b"<mtable class=\"menv-cells-left menv-cases\"><mtr")?;
222 env_horizontal_lines!();
223 self.writer.write_all(b"><mtd>")?;
224 EnvGrouping::Cases {
225 left,
226 used_align: false,
227 }
228 }
229 Grouping::Array(cols) => {
230 self.writer
231 .write_all(b"<mtable class=\"menv-arraylike\"><mtr")?;
232 env_horizontal_lines!();
233 self.writer.write_all(b">")?;
234 let index = array_newline(&mut self.writer, &cols)?;
235 EnvGrouping::Array {
236 cols,
237 cols_index: index,
238 }
239 }
240 Grouping::Aligned => {
241 self.writer
242 .write_all(b"<mtable class=\"menv-alignlike menv-align\"><mtr")?;
243 env_horizontal_lines!();
244 self.writer.write_all(b"><mtd>")?;
245 EnvGrouping::Align
246 }
247 Grouping::SubArray { alignment } => {
248 self.writer.write_all(b"<mtable")?;
249 match alignment {
250 crate::event::ColumnAlignment::Left => {
251 self.writer.write_all(b" class=\"menv-cells-left\"")?
252 }
253 crate::event::ColumnAlignment::Center => (),
254 crate::event::ColumnAlignment::Right => {
255 self.writer.write_all(b" class=\"menv-cells-right\"")?
256 }
257 }
258 self.writer.write_all(b"><mtr")?;
259 env_horizontal_lines!();
260 self.writer.write_all(b"><mtd>")?;
261 EnvGrouping::SubArray
262 }
263 Grouping::Alignat { pairs, eq_numbers } => {
264 self.writer.write_all(b"<mtable class=\"menv-alignlike")?;
265 if eq_numbers {
266 self.writer.write_all(b" menv-with-eqn")?;
267 }
268 self.writer.write_all(b"\"><mtr")?;
269 env_horizontal_lines!();
270 self.writer.write_all(b"><mtd>")?;
271 EnvGrouping::Alignat {
272 pairs,
273 columns_used: 0,
274 }
275 }
276 Grouping::Alignedat { pairs } => {
277 self.writer.write_all(b"<mtable class=\"menv-alignlike\"")?;
278 self.writer.write_all(b"><mtr")?;
279 env_horizontal_lines!();
280 self.writer.write_all(b"><mtd>")?;
281 EnvGrouping::Alignat {
282 pairs,
283 columns_used: 0,
284 }
285 }
286 Grouping::Gather { eq_numbers } => {
287 self.writer.write_all(b"<mtable")?;
288 if eq_numbers {
289 self.writer.write_all(b" class=\"menv-with-eqn\"")?;
290 }
291 self.writer.write_all(b"><mtr")?;
292 env_horizontal_lines!();
293 self.writer.write_all(b"><mtd>")?;
294 EnvGrouping::Gather
295 }
296 Grouping::Gathered => {
297 self.writer.write_all(b"<mtable><mtr")?;
298 env_horizontal_lines!();
299 self.writer.write_all(b"><mtd>")?;
300 EnvGrouping::Gather
301 }
302 Grouping::Multline => {
303 self.writer
304 .write_all(b"<mtable class=\"menv-multline\"><mtr")?;
305 env_horizontal_lines!();
306 self.writer.write_all(b"><mtd>")?;
307 EnvGrouping::Multline
308 }
309 Grouping::Split => {
310 self.writer
311 .write_all(b"<mtable class=\"menv-alignlike\"><mtr")?;
312 env_horizontal_lines!();
313 self.writer.write_all(b"><mtd>")?;
314 EnvGrouping::Split { used_align: false }
315 }
316 Grouping::Equation { eq_numbers } => {
317 self.writer.write_all(b"<mtable")?;
318 if eq_numbers {
319 self.writer.write_all(b" class=\"menv-with-eqn\"")?;
320 }
321 self.writer.write_all(b"><mtr><mtd>")?;
322 EnvGrouping::Equation
323 }
324 };
325 self.env_stack.push(Environment::from(env_group));
326 Ok(())
327 }
328 Ok(Event::End) => {
329 let env = self
330 .env_stack
331 .pop()
332 .expect("cannot pop an environment in group end");
333 let Environment::Group(grouping) = env else {
334 panic!("unexpected environment in group end");
335 };
336 self.state_stack
337 .pop()
338 .expect("cannot pop a state in group end");
339 self.previous_atom = Some(Atom::Inner);
340 match grouping {
341 EnvGrouping::Normal => self.writer.write_all(b"</mrow>"),
342 EnvGrouping::LeftRight { closing } => {
343 if let Some(delim) = closing {
344 self.open_tag("mo", None)?;
345 self.writer.write_all(b" stretchy=\"true\">")?;
346 let mut buf = [0u8; 4];
347 self.writer
348 .write_all(delim.encode_utf8(&mut buf).as_bytes())?;
349 self.writer.write_all(b"</mo>")?;
350 }
351 self.previous_atom = Some(Atom::Close);
352 self.writer.write_all(b"</mrow>")
353 }
354 EnvGrouping::Matrix
355 | EnvGrouping::Align
356 | EnvGrouping::SubArray
357 | EnvGrouping::Gather
358 | EnvGrouping::Multline
359 | EnvGrouping::Equation
360 | EnvGrouping::Split { .. }
361 | EnvGrouping::Alignat { .. } => {
362 self.writer.write_all(b"</mtd></mtr></mtable>")
363 }
364 EnvGrouping::Array { cols, cols_index } => {
365 self.writer.write_all(b"</mtd>")?;
366 cols[cols_index..]
367 .iter()
368 .map_while(|col| match col {
369 ArrayColumn::Separator(line) => Some(line),
370 _ => None,
371 })
372 .try_for_each(|line| {
373 self.writer.write_all(match line {
374 Line::Solid => {
375 b"<mtd class=\"menv-right-solid menv-border-only\"></mtd>"
376 }
377 Line::Dashed => {
378 b"<mtd class=\"menv-right-dashed menv-border-only\"></mtd>"
379 }
380 })
381 })?;
382 self.writer.write_all(b"</mtr></mtable>")
383 }
384 EnvGrouping::Cases { left, .. } => {
385 self.writer.write_all(b"</mtd></mtr></mtable>")?;
386 if !left {
387 self.writer.write_all(b"<mo stretchy=\"true\">}</mo>")?;
388 }
389 self.writer.write_all(b"</mrow>")
390 }
391 }
392 }
393 Ok(Event::Visual(visual)) => {
394 if visual == Visual::Negation {
395 match self.input.peek_first() {
396 Some(Ok(Event::Content(
397 content @ Content::Ordinary { .. }
398 | content @ Content::Relation { .. }
399 | content @ Content::BinaryOp { .. }
400 | content @ Content::LargeOp { .. }
401 | content @ Content::Delimiter { .. }
402 | content @ Content::Punctuation(_),
403 ))) => {
404 let content = *content;
405 self.write_content(content, true)?;
406 self.input.next();
407 }
408 _ => {
409 self.open_tag("mrow", Some("mop-negated"))?;
410 self.writer.write_all(b">")?;
411 self.env_stack.push(Environment::from(visual));
412 }
413 }
414 return Ok(());
415 }
416
417 let env = Environment::from(visual);
418 self.env_stack.push(env);
419 self.open_tag(visual_tag(visual), None)?;
420 if let Visual::Fraction(Some(dim)) = visual {
421 write!(self.writer, " linethickness=\"{}\"", dim)?;
422 }
423
424 self.writer.write_all(b">")
425 }
426
427 Ok(Event::Script { ty, position }) => {
428 let state = self.state();
429 let above_below = match position {
430 ScriptPosition::Right => false,
431 ScriptPosition::AboveBelow => true,
432 ScriptPosition::Movable => {
433 state.style == Some(Style::Display)
434 || (state.style.is_none()
435 && self.config.display_mode == DisplayMode::Block)
436 }
437 };
438 let env = Environment::from((ty, above_below));
439 self.env_stack.push(env);
440 self.open_tag(script_tag(ty, above_below), None)?;
441 self.writer.write_all(b">")
442 }
443
444 Ok(Event::Space { width, height }) => {
445 if let Some(width) = width {
446 write!(self.writer, "<mspace width=\"{}\"", width)?;
447 if width.value < 0. {
448 write!(self.writer, " style=\"margin-left: {}\"", width)?;
449 }
450 }
451 if let Some(height) = height {
452 write!(self.writer, " height=\"{}\"", height)?;
453 }
454 self.writer.write_all(b" />")
455 }
456 Ok(Event::StateChange(state_change)) => {
457 self.handle_state_change(state_change);
458 Ok(())
459 }
460 Ok(Event::EnvironmentFlow(EnvironmentFlow::NewLine {
461 spacing,
462 horizontal_lines,
463 })) => {
464 *self
465 .state_stack
466 .last_mut()
467 .expect("state stack should not be empty") = State::default();
468 self.previous_atom = None;
469
470 if let Some(Environment::Group(EnvGrouping::Array { cols, cols_index })) =
471 self.env_stack.last()
472 {
473 array_close_line(&mut self.writer, &cols[*cols_index..])?;
474 } else if let Some(Environment::Group(EnvGrouping::Equation { .. })) =
475 self.env_stack.last()
476 {
477 return Ok(());
480 } else {
481 self.writer.write_all(b"</mtd></mtr><mtr")?;
482 }
483
484 if let Some(spacing) = spacing {
485 write!(self.writer, " style=\"height: {}\">", spacing)?;
486 if let Some(Environment::Group(EnvGrouping::Array { cols, cols_index })) =
487 self.env_stack.last_mut()
488 {
489 let mut index = array_newline(&mut self.writer, cols)?;
490 while index < *cols_index {
491 array_align(&mut self.writer, cols, &mut index)?;
492 }
493 array_close_line(&mut self.writer, &cols[index..])?;
494 } else {
495 self.writer
496 .write_all(b"<mtd class=\"menv-nonumber\"></mtd></mtr><mtr")?;
497 }
498 }
499 env_horizontal_lines(&mut self.writer, &horizontal_lines)?;
500
501 match self.env_stack.last_mut() {
502 Some(Environment::Group(
503 EnvGrouping::Cases { used_align, .. } | EnvGrouping::Split { used_align },
504 )) => {
505 *used_align = false;
506 self.writer.write_all(b"><mtd>")
507 }
508 Some(Environment::Group(
509 EnvGrouping::Matrix
510 | EnvGrouping::Align
511 | EnvGrouping::Gather
512 | EnvGrouping::SubArray
513 | EnvGrouping::Multline,
514 )) => self.writer.write_all(b"><mtd>"),
515 Some(Environment::Group(EnvGrouping::Array { cols, cols_index })) => {
516 self.writer.write_all(b">")?;
517 let new_index = array_newline(&mut self.writer, cols)?;
518 *cols_index = new_index;
519 Ok(())
520 }
521 Some(Environment::Group(EnvGrouping::Alignat { columns_used, .. })) => {
522 *columns_used = 0;
523 self.writer.write_all(b"><mtd>")
524 }
525
526 _ => panic!("newline not allowed in current environment"),
527 }
528 }
529 Ok(Event::EnvironmentFlow(EnvironmentFlow::Alignment)) => {
530 *self.state_stack.last_mut().expect("state stack is empty") = State::default();
531 self.previous_atom = None;
532 match self.env_stack.last_mut() {
533 Some(Environment::Group(
534 EnvGrouping::Cases {
535 used_align: false, ..
536 }
537 | EnvGrouping::Split { used_align: false },
538 )) => self.writer.write_all(b"</mtd><mtd>"),
539 Some(Environment::Group(EnvGrouping::Align | EnvGrouping::Matrix)) => {
540 self.writer.write_all(b"</mtd><mtd>")
541 }
542 Some(Environment::Group(EnvGrouping::Alignat {
543 pairs,
544 columns_used,
545 })) if *columns_used / 2 <= *pairs => {
546 *columns_used += 1;
547 self.writer.write_all(b"</mtd><mtd>")
548 }
549 Some(Environment::Group(EnvGrouping::Array { cols, cols_index })) => {
550 array_align(&mut self.writer, cols, cols_index)
551 }
552 _ => panic!("alignment not allowed in current environment"),
553 }
554 }
555
556 Ok(Event::EnvironmentFlow(EnvironmentFlow::StartLines { .. })) => {
557 panic!("unexpected StartLines event found")
558 }
559
560 Err(e) => {
561 let error_color = self.config.error_color;
562 write!(
563 self.writer,
564 "<merror style=\"border-color: #{:x}{:x}{:x}\"><mtext>",
565 error_color.0, error_color.1, error_color.2
566 )?;
567 self.writer.write_all(e.to_string().as_bytes())?;
568 self.writer.write_all(b"</mtext></merror>")
569 }
570 }
571 }
572
573 fn write_content(&mut self, content: Content<'a>, negate: bool) -> io::Result<()> {
574 let mut buf = [0u8; 4];
575 match content {
576 Content::Text(text) => {
577 self.open_tag("mtext", None)?;
578 self.writer.write_all(b">")?;
579 let trimmed = text.trim();
580 if text.starts_with(char::is_whitespace) {
581 self.writer.write_all(b" ")?;
582 }
583 self.writer.write_all(trimmed.as_bytes())?;
584 if text.ends_with(char::is_whitespace) {
585 self.writer.write_all(b" ")?;
586 }
587 self.set_previous_atom(Atom::Ord);
588 self.writer.write_all(b"</mtext>")
589 }
590 Content::Number(number) => {
591 self.open_tag("mn", None)?;
592 self.writer.write_all(b">")?;
593 let buf = &mut [0u8; 4];
594 number.chars().try_for_each(|c| {
595 let content = self.state().font.map_or(c, |v| v.map_char(c));
596 let bytes = content.encode_utf8(buf);
597 self.writer.write_all(bytes.as_bytes())
598 })?;
599 self.set_previous_atom(Atom::Ord);
600 self.writer.write_all(b"</mn>")
601 }
602 Content::Function(str) => {
603 if matches!(
604 self.previous_atom,
605 Some(Atom::Inner | Atom::Close | Atom::Ord)
606 ) {
607 self.writer
608 .write_all("<mspace width=\"0.1667em\" />".as_bytes())?;
609 }
610
611 self.open_tag("mi", None)?;
612 self.writer.write_all(if str.chars().count() == 1 {
613 b" mathvariant=\"normal\">"
614 } else {
615 b">"
616 })?;
617 self.writer.write_all(str.as_bytes())?;
618 self.set_previous_atom(Atom::Op);
619 self.writer.write_all(b"</mi>")?;
620
621 if let Some(Environment::Script { fn_application, .. }) = self.env_stack.last_mut()
622 {
623 *fn_application = true;
624 } else if let Some(atom) = self.next_atom() {
625 self.writer.write_all("<mo>\u{2061}</mo>".as_bytes())?;
626 if !matches!(atom, Atom::Open | Atom::Punct | Atom::Close) {
627 self.writer
628 .write_all("<mspace width=\"0.1667em\" />".as_bytes())?;
629 }
630 };
631
632 Ok(())
633 }
634 Content::Ordinary { content, stretchy } => {
635 if stretchy {
636 self.writer.write_all(b"<mo stretchy=\"true\">")?;
637 self.writer
638 .write_all(content.encode_utf8(&mut buf).as_bytes())?;
639 self.writer.write_all(b"</mo>")
640 } else {
641 self.open_tag("mi", None)?;
642
643 let content = match (
644 self.state().font,
645 self.config.math_style.should_be_upright(content),
646 ) {
647 (Some(Font::UpRight), _) | (None, true) => {
648 self.writer.write_all(b" mathvariant=\"normal\">")?;
649 content
650 }
651 (Some(font), _) => {
652 self.writer.write_all(b">")?;
653 font.map_char(content)
654 }
655 _ => {
656 self.writer.write_all(b">")?;
657 content
658 }
659 };
660
661 let buf = &mut [0u8; 4];
662 let bytes = content.encode_utf8(buf);
663 self.writer.write_all(bytes.as_bytes())?;
664 if negate {
665 self.writer.write_all("\u{0338}".as_bytes())?;
666 }
667 self.set_previous_atom(Atom::Ord);
668 self.writer.write_all(b"</mi>")
669 }
670 }
671 Content::BinaryOp { content, small } => {
675 let tag = if matches!(
676 self.previous_atom,
677 Some(Atom::Inner | Atom::Close | Atom::Ord)
678 ) && !matches!(
679 self.env_stack.last(),
680 Some(
681 Environment::Script { .. }
682 | Environment::Visual {
683 ty: Visual::Root | Visual::Fraction(_) | Visual::SquareRoot,
684 ..
685 }
686 )
687 ) && matches!(
688 self.next_atom(),
689 Some(Atom::Inner | Atom::Bin | Atom::Op | Atom::Ord | Atom::Open)
690 ) {
691 self.set_previous_atom(Atom::Bin);
692 "mo"
693 } else {
694 self.set_previous_atom(Atom::Ord);
695 "mi"
696 };
697
698 self.open_tag(tag, small.then_some("small"))?;
699 self.writer.write_all(b">")?;
700 self.writer
701 .write_all(content.encode_utf8(&mut buf).as_bytes())?;
702 if negate {
703 self.writer.write_all("\u{0338}".as_bytes())?;
704 }
705 write!(self.writer, "</{}>", tag)
706 }
707 Content::Relation { content, small } => {
708 let mut buf = [0; 8];
709 self.open_tag("mo", small.then_some("small"))?;
710 self.writer.write_all(b">")?;
711 self.writer
712 .write_all(content.encode_utf8_to_buf(&mut buf))?;
713 if negate {
714 self.writer.write_all("\u{0338}".as_bytes())?;
715 }
716 self.set_previous_atom(Atom::Rel);
717 self.writer.write_all(b"</mo>")
718 }
719
720 Content::LargeOp { content, small } => {
721 self.open_tag("mo", None)?;
722 if small {
723 self.writer.write_all(b" largeop=\"false\"")?;
724 }
725 self.writer.write_all(b" movablelimits=\"false\">")?;
726 self.writer
727 .write_all(content.encode_utf8(&mut buf).as_bytes())?;
728 if negate {
729 self.writer.write_all("\u{0338}".as_bytes())?;
730 }
731 self.set_previous_atom(Atom::Op);
732 self.writer.write_all(b"</mo>")
733 }
734 Content::Delimiter { content, size, ty } => {
735 self.open_tag("mo", None)?;
736 write!(
737 self.writer,
738 " symmetric=\"{0}\" stretchy=\"{0}\"",
739 ty == DelimiterType::Fence || size.is_some()
740 )?;
741 if let Some(size) = size {
742 write!(
743 self.writer,
744 "minsize=\"{0}em\" maxsize=\"{0}em\"",
745 size.to_em()
746 )?;
747 }
748
749 self.writer.write_all(b">")?;
750 self.writer
751 .write_all(content.encode_utf8(&mut buf).as_bytes())?;
752 if negate {
753 self.writer.write_all("\u{0338}".as_bytes())?;
754 }
755 self.set_previous_atom(match ty {
756 DelimiterType::Open => Atom::Open,
757 DelimiterType::Fence => Atom::Punct,
758 DelimiterType::Close => Atom::Close,
759 });
760 self.writer.write_all(b"</mo>")
761 }
762 Content::Punctuation(content) => {
763 self.open_tag("mo", None)?;
764 self.writer.write_all(b">")?;
765 self.writer
766 .write_all(content.encode_utf8(&mut buf).as_bytes())?;
767 if negate {
768 self.writer.write_all("\u{0338}".as_bytes())?;
769 }
770 self.set_previous_atom(Atom::Punct);
771 self.writer.write_all(b"</mo>")
772 }
773 }
774 }
775
776 fn handle_state_change(&mut self, state_change: StateChange) {
777 let state = self.state_stack.last_mut().expect("state stack is empty");
778 match state_change {
779 StateChange::Font(font) => state.font = font,
780 StateChange::Color(ColorChange { color, target }) => match target {
781 ColorTarget::Text => state.text_color = Some(color),
782 ColorTarget::Border => state.border_color = Some(color),
783 ColorTarget::Background => state.background_color = Some(color),
784 },
785 StateChange::Style(style) => state.style = Some(style),
786 }
787 }
788
789 fn set_previous_atom(&mut self, atom: Atom) {
790 if !matches!(
791 self.env_stack.last(),
792 Some(
793 Environment::Visual {
794 ty: Visual::Root | Visual::Fraction(_),
795 count: 0
796 } | Environment::Script {
797 ty: ScriptType::Subscript | ScriptType::Superscript,
798 count: 0,
799 ..
800 } | Environment::Script {
801 ty: ScriptType::SubSuperscript,
802 count: 0 | 1,
803 ..
804 }
805 )
806 ) {
807 self.previous_atom = Some(atom);
808 }
809 }
810
811 fn next_atom(&mut self) -> Option<Atom> {
812 let mut index = 0;
813 loop {
814 let next = match self.input.peeked_nth(index) {
815 None => self.input.peek_next()?,
816 Some(next) => {
817 index += 1;
818 next
819 }
820 };
821
822 break match next {
823 Ok(
824 Event::StateChange(_)
825 | Event::Space { .. }
826 | Event::Visual(Visual::Negation)
827 | Event::Script { .. },
828 ) => continue,
829 Ok(Event::End | Event::EnvironmentFlow(_)) | Err(_) => None,
830 Ok(Event::Visual(_) | Event::Begin(_)) => Some(Atom::Inner),
831 Ok(Event::Content(content)) => match content {
832 Content::BinaryOp { .. } => Some(Atom::Bin),
833 Content::LargeOp { .. } => Some(Atom::Op),
834 Content::Relation { .. } => Some(Atom::Rel),
835 Content::Delimiter {
836 ty: DelimiterType::Open,
837 ..
838 } => Some(Atom::Open),
839 Content::Delimiter {
840 ty: DelimiterType::Close,
841 ..
842 } => Some(Atom::Close),
843 Content::Punctuation(_) => Some(Atom::Punct),
844 _ => Some(Atom::Ord),
845 },
846 };
847 }
848 }
849
850 fn state(&self) -> &State {
851 self.state_stack.last().expect("state stack is empty")
852 }
853
854 fn write(mut self) -> io::Result<()> {
855 write!(
861 self.writer,
862 "<math display=\"{}\"",
863 self.config.display_mode
864 )?;
865 if self.config.xml {
866 self.writer
867 .write_all(b" xmlns=\"http://www.w3.org/1998/Math/MathML\"")?;
868 }
869 self.writer.write_all(b">")?;
870 if self.config.annotation.is_some() {
871 self.writer.write_all(b"<semantics><mrow>")?;
872 }
873
874 while let Some(event) = self.input.next() {
875 self.write_event(event)?;
876
877 while let Some((tag, count, fn_application)) =
878 self.env_stack.last_mut().and_then(|env| match env {
879 Environment::Group(_) => None,
880 Environment::Visual { ty, count } => Some((visual_tag(*ty), count, None)),
881 Environment::Script {
882 ty,
883 above_below,
884 count,
885 fn_application,
886 } => Some((script_tag(*ty, *above_below), count, Some(*fn_application))),
887 })
888 {
889 if *count != 0 {
890 *count -= 1;
891 break;
892 }
893 self.writer.write_all(b"</")?;
894 self.writer.write_all(tag.as_bytes())?;
895 self.writer.write_all(b">")?;
896 self.set_previous_atom(Atom::Inner);
897 self.env_stack.pop();
898
899 if fn_application.unwrap_or(false) {
900 if let Some(atom) = self.next_atom() {
901 self.writer.write_all("<mo>\u{2061}</mo>".as_bytes())?;
902
903 if !matches!(atom, Atom::Open | Atom::Punct | Atom::Close) {
904 self.writer
905 .write_all("<mspace width=\"0.1667em\" />".as_bytes())?;
906 }
907 }
908 }
909 }
910 }
911
912 if !self.env_stack.is_empty() || self.state_stack.len() != 1 {
913 panic!("unbalanced environment stack or state stack");
914 }
915
916 if let Some(annotation) = self.config.annotation {
917 self.writer.write_all(b"</mrow>")?;
918 write!(
919 self.writer,
920 "<annotation encoding=\"application/x-tex\">{}</annotation>",
921 annotation
922 )?;
923 self.writer.write_all(b"</semantics>")?;
924 }
925 self.writer.write_all(b"</math>")
926 }
927}
928
929fn array_newline<W: Write>(writer: &mut W, cols: &[ArrayColumn]) -> io::Result<usize> {
930 let mut index = 0;
931 writer.write_all(b"<mtd")?;
932 cols.windows(2)
933 .map_while(|window| match window[..2] {
934 [ArrayColumn::Separator(line), ArrayColumn::Separator(_)] => Some(line),
935 _ => None,
936 })
937 .try_for_each(|line| {
938 index += 1;
939 writer.write_all(match line {
940 Line::Solid => b" class=\"menv-left-solid menv-border-only\"></mtd><mtd",
941 Line::Dashed => b" class=\"menv-left-dashed menv-border-only\"></mtd><mtd",
942 })
943 })?;
944
945 let to_append: &[u8] = match (cols.get(index), cols.get(index + 1)) {
946 (Some(ArrayColumn::Separator(line)), Some(ArrayColumn::Column(col))) => {
947 writer.write_all(match (line, col) {
948 (Line::Solid, ColumnAlignment::Left) => b" class=\"menv-left-solid cell-left",
949 (Line::Solid, ColumnAlignment::Center) => b" class=\"menv-left-solid",
950 (Line::Solid, ColumnAlignment::Right) => b" class=\"menv-left-solid cell-right",
951 (Line::Dashed, ColumnAlignment::Left) => b" class=\"menv-left-dashed cell-left",
952 (Line::Dashed, ColumnAlignment::Center) => b" class=\"menv-left-dashed",
953 (Line::Dashed, ColumnAlignment::Right) => b" class=\"menv-left-dashed cell-right",
954 })?;
955 index += 2;
956
957 if let Some(ArrayColumn::Separator(line)) = cols.get(index) {
958 index += 1;
959 match line {
960 Line::Solid => b" menv-right-solid\">",
961 Line::Dashed => b" menv-right-dashed\">",
962 }
963 } else {
964 b"\">"
965 }
966 }
967 (Some(ArrayColumn::Column(col)), Some(ArrayColumn::Separator(line))) => {
968 index += 2;
969 match (col, line) {
970 (ColumnAlignment::Left, Line::Solid) => b" class=\"cell-left menv-right-solid\">",
971 (ColumnAlignment::Left, Line::Dashed) => b" class=\"cell-left menv-right-dashed\">",
972 (ColumnAlignment::Center, Line::Solid) => b" class=\"menv-right-solid\">",
973 (ColumnAlignment::Center, Line::Dashed) => b" class=\"menv-right-dashed\">",
974 (ColumnAlignment::Right, Line::Solid) => b" class=\"cell-right menv-right-solid\">",
975 (ColumnAlignment::Right, Line::Dashed) => {
976 b" class=\"cell-right menv-right-dashed\">"
977 }
978 }
979 }
980 (Some(ArrayColumn::Column(col)), _) => {
981 index += 1;
982 match col {
983 ColumnAlignment::Left => b" class=\"cell-left\">",
984 ColumnAlignment::Center => b">",
985 ColumnAlignment::Right => b" class=\"cell-right\">",
986 }
987 }
988 (None, None) => b">",
989 _ => unreachable!(),
990 };
991 writer.write_all(to_append)?;
992
993 Ok(index)
994}
995
996fn array_align<W: Write>(
997 writer: &mut W,
998 cols: &[ArrayColumn],
999 cols_index: &mut usize,
1000) -> io::Result<()> {
1001 writer.write_all(b"</mtd><mtd")?;
1002 cols[*cols_index..]
1003 .iter()
1004 .map_while(|col| match col {
1005 ArrayColumn::Separator(line) => Some(line),
1006 _ => None,
1007 })
1008 .try_for_each(|line| {
1009 *cols_index += 1;
1010 writer.write_all(match line {
1011 Line::Solid => b" class=\"menv-right-solid menv-border-only\"></mtd><mtd",
1012 Line::Dashed => b" class=\"menv-right-dashed menv-border-only\"></mtd><mtd",
1013 })
1014 })?;
1015
1016 let to_append: &[u8] = match (cols[*cols_index], cols.get(*cols_index + 1)) {
1017 (ArrayColumn::Column(col), Some(ArrayColumn::Separator(line))) => {
1018 *cols_index += 2;
1019 match (col, line) {
1020 (ColumnAlignment::Left, Line::Solid) => b" class=\"cell-left menv-right-solid\">",
1021 (ColumnAlignment::Left, Line::Dashed) => b" class=\"cell-left menv-right-dashed\">",
1022 (ColumnAlignment::Center, Line::Solid) => b" class=\"menv-right-solid\">",
1023 (ColumnAlignment::Center, Line::Dashed) => b" class=\"menv-right-dashed\">",
1024 (ColumnAlignment::Right, Line::Solid) => b" class=\"cell-right menv-right-solid\">",
1025 (ColumnAlignment::Right, Line::Dashed) => {
1026 b" class=\"cell-right menv-right-dashed\">"
1027 }
1028 }
1029 }
1030 (ArrayColumn::Column(col), _) => {
1031 *cols_index += 1;
1032 match col {
1033 ColumnAlignment::Left => b" class=\"cell-left\">",
1034 ColumnAlignment::Center => b">",
1035 ColumnAlignment::Right => b" class=\"cell-right\">",
1036 }
1037 }
1038 (ArrayColumn::Separator(_), _) => unreachable!(),
1039 };
1040 writer.write_all(to_append)
1041}
1042
1043fn array_close_line<W: Write>(writer: &mut W, rest_cols: &[ArrayColumn]) -> io::Result<()> {
1044 writer.write_all(b"</mtd>")?;
1045 rest_cols
1046 .iter()
1047 .map_while(|col| match col {
1048 ArrayColumn::Separator(line) => Some(line),
1049 _ => None,
1050 })
1051 .try_for_each(|line| {
1052 writer.write_all(match line {
1053 Line::Solid => b"<mtd class=\"menv-right-solid menv-border-only\"></mtd>",
1054 Line::Dashed => b"<mtd class=\"menv-right-dashed menv-border-only\"></mtd>",
1055 })
1056 })?;
1057 writer.write_all(b"</mtr><mtr")
1058}
1059
1060fn env_horizontal_lines<W: Write>(writer: &mut W, lines: &[Line]) -> io::Result<()> {
1061 let mut iter = lines.iter();
1062 if let Some(last_line) = iter.next_back() {
1063 iter.try_for_each(|line| {
1064 writer.write_all(match line {
1065 Line::Solid => {
1066 b" class=\"menv-hline\"><mtd class=\"menv-nonumber\"></mtd></mtr><mtr"
1067 }
1068 Line::Dashed => {
1069 b" class=\"menv-hdashline\"><mtd class=\"menv-nonumber\"></mtd></mtr><mtr"
1070 }
1071 })
1072 })?;
1073 writer.write_all(match last_line {
1074 Line::Solid => b" class=\"menv-hline\"",
1075 Line::Dashed => b" class=\"menv-hdashline\"",
1076 })?;
1077 };
1078 Ok(())
1079}
1080
1081enum Atom {
1082 Bin,
1083 Op,
1084 Rel,
1085 Open,
1086 Close,
1087 Punct,
1088 Ord,
1089 Inner,
1090}
1091
1092#[derive(Debug, Clone, PartialEq)]
1093enum EnvGrouping {
1094 Normal,
1095 LeftRight {
1096 closing: Option<char>,
1097 },
1098 Array {
1099 cols: Box<[ArrayColumn]>,
1100 cols_index: usize,
1101 },
1102 Matrix,
1103 Cases {
1104 used_align: bool,
1105 left: bool,
1106 },
1107 Align,
1108 Alignat {
1109 pairs: u16,
1110 columns_used: u16,
1111 },
1112 SubArray,
1113 Gather,
1114 Multline,
1115 Split {
1116 used_align: bool,
1117 },
1118 Equation,
1119}
1120
1121#[derive(Debug, Clone, PartialEq)]
1122enum Environment {
1123 Group(EnvGrouping),
1124 Visual {
1125 ty: Visual,
1126 count: u8,
1127 },
1128 Script {
1129 ty: ScriptType,
1130 above_below: bool,
1131 count: u8,
1132 fn_application: bool,
1133 },
1134}
1135
1136impl From<EnvGrouping> for Environment {
1137 fn from(v: EnvGrouping) -> Self {
1138 Self::Group(v)
1139 }
1140}
1141
1142impl From<(ScriptType, bool)> for Environment {
1143 fn from((ty, above_below): (ScriptType, bool)) -> Self {
1144 let count = match ty {
1145 ScriptType::Subscript => 2,
1146 ScriptType::Superscript => 2,
1147 ScriptType::SubSuperscript => 3,
1148 };
1149 Self::Script {
1150 ty,
1151 above_below,
1152 count,
1153 fn_application: false,
1154 }
1155 }
1156}
1157
1158impl From<Visual> for Environment {
1159 fn from(v: Visual) -> Self {
1160 let count = match v {
1161 Visual::SquareRoot => 1,
1162 Visual::Root => 2,
1163 Visual::Fraction(_) => 2,
1164 Visual::Negation => 1,
1165 };
1166 Self::Visual { ty: v, count }
1167 }
1168}
1169
1170fn script_tag(ty: ScriptType, above_below: bool) -> &'static str {
1171 match (ty, above_below) {
1172 (ScriptType::Subscript, false) => "msub",
1173 (ScriptType::Superscript, false) => "msup",
1174 (ScriptType::SubSuperscript, false) => "msubsup",
1175 (ScriptType::Subscript, true) => "munder",
1176 (ScriptType::Superscript, true) => "mover",
1177 (ScriptType::SubSuperscript, true) => "munderover",
1178 }
1179}
1180
1181fn visual_tag(visual: Visual) -> &'static str {
1182 match visual {
1183 Visual::Root => "mroot",
1184 Visual::Fraction(_) => "mfrac",
1185 Visual::SquareRoot => "msqrt",
1186 Visual::Negation => "mrow",
1187 }
1188}
1189
1190#[derive(Debug, Clone, Copy, Default)]
1191struct State {
1192 font: Option<Font>,
1193 text_color: Option<(u8, u8, u8)>,
1194 border_color: Option<(u8, u8, u8)>,
1195 background_color: Option<(u8, u8, u8)>,
1196 style: Option<Style>,
1197}
1198
1199struct ManyPeek<I: Iterator> {
1200 iter: I,
1201 peeked: VecDeque<I::Item>,
1202}
1203
1204impl<I: Iterator> ManyPeek<I> {
1205 fn new(iter: I) -> Self {
1206 Self {
1207 iter,
1208 peeked: VecDeque::new(),
1209 }
1210 }
1211
1212 fn peek_next(&mut self) -> Option<&I::Item> {
1213 self.peeked.push_back(self.iter.next()?);
1214 self.peeked.back()
1215 }
1216
1217 fn peeked_nth(&self, n: usize) -> Option<&I::Item> {
1218 self.peeked.get(n)
1219 }
1220
1221 fn peek_first(&mut self) -> Option<&I::Item> {
1222 if self.peeked.is_empty() {
1223 self.peek_next()
1224 } else {
1225 self.peeked.front()
1226 }
1227 }
1228}
1229
1230impl<I: Iterator> Iterator for ManyPeek<I> {
1231 type Item = I::Item;
1232
1233 fn next(&mut self) -> Option<Self::Item> {
1234 self.peeked.pop_front().or_else(|| self.iter.next())
1235 }
1236}
1237
1238impl Font {
1239 fn map_char(self, c: char) -> char {
1241 char::from_u32(match (self, c) {
1242 (Font::BoldScript, 'A'..='Z') => c as u32 + 0x1D48F,
1244 (Font::BoldScript, 'a'..='z') => c as u32 + 0x1D489,
1245
1246 (Font::BoldItalic, 'A'..='Z') => c as u32 + 0x1D427,
1248 (Font::BoldItalic, 'a'..='z') => c as u32 + 0x1D421,
1249 (Font::BoldItalic, '\u{0391}'..='\u{03A1}' | '\u{03A3}'..='\u{03A9}') => {
1250 c as u32 + 0x1D38B
1251 }
1252 (Font::BoldItalic, '\u{03F4}') => c as u32 + 0x1D339,
1253 (Font::BoldItalic, '\u{2207}') => c as u32 + 0x1B52E,
1254 (Font::BoldItalic, '\u{03B1}'..='\u{03C9}') => c as u32 + 0x1D385,
1255 (Font::BoldItalic, '\u{2202}') => c as u32 + 0x1B54D,
1256 (Font::BoldItalic, '\u{03F5}') => c as u32 + 0x1D35B,
1257 (Font::BoldItalic, '\u{03D1}') => c as u32 + 0x1D380,
1258 (Font::BoldItalic, '\u{03F0}') => c as u32 + 0x1D362,
1259 (Font::BoldItalic, '\u{03D5}') => c as u32 + 0x1D37E,
1260 (Font::BoldItalic, '\u{03F1}') => c as u32 + 0x1D363,
1261 (Font::BoldItalic, '\u{03D6}') => c as u32 + 0x1D37F,
1262
1263 (Font::Bold, 'A'..='Z') => c as u32 + 0x1D3BF,
1265 (Font::Bold, 'a'..='z') => c as u32 + 0x1D3B9,
1266 (Font::Bold, '\u{0391}'..='\u{03A1}' | '\u{03A3}'..='\u{03A9}') => c as u32 + 0x1D317,
1267 (Font::Bold, '\u{03F4}') => c as u32 + 0x1D2C5,
1268 (Font::Bold, '\u{2207}') => c as u32 + 0x1B4BA,
1269 (Font::Bold, '\u{03B1}'..='\u{03C9}') => c as u32 + 0x1D311,
1270 (Font::Bold, '\u{2202}') => c as u32 + 0x1B4D9,
1271 (Font::Bold, '\u{03F5}') => c as u32 + 0x1D2E7,
1272 (Font::Bold, '\u{03D1}') => c as u32 + 0x1D30C,
1273 (Font::Bold, '\u{03F0}') => c as u32 + 0x1D2EE,
1274 (Font::Bold, '\u{03D5}') => c as u32 + 0x1D30A,
1275 (Font::Bold, '\u{03F1}') => c as u32 + 0x1D2EF,
1276 (Font::Bold, '\u{03D6}') => c as u32 + 0x1D30B,
1277 (Font::Bold, '\u{03DC}' | '\u{03DD}') => c as u32 + 0x1D7CA,
1278 (Font::Bold, '0'..='9') => c as u32 + 0x1D79E,
1279
1280 (Font::Fraktur, 'A' | 'B' | 'D'..='G' | 'J'..='Q' | 'S'..='Y') => c as u32 + 0x1D4C3,
1282 (Font::Fraktur, 'C') => c as u32 + 0x20EA,
1283 (Font::Fraktur, 'H' | 'I') => c as u32 + 0x20C4,
1284 (Font::Fraktur, 'R') => c as u32 + 0x20CA,
1285 (Font::Fraktur, 'Z') => c as u32 + 0x20CE,
1286 (Font::Fraktur, 'a'..='z') => c as u32 + 0x1D4BD,
1287
1288 (Font::Script, 'A' | 'C' | 'D' | 'G' | 'J' | 'K' | 'N'..='Q' | 'S'..='Z') => {
1290 c as u32 + 0x1D45B
1291 }
1292 (Font::Script, 'B') => c as u32 + 0x20EA,
1293 (Font::Script, 'E' | 'F') => c as u32 + 0x20EB,
1294 (Font::Script, 'H') => c as u32 + 0x20C3,
1295 (Font::Script, 'I') => c as u32 + 0x20C7,
1296 (Font::Script, 'L') => c as u32 + 0x20C6,
1297 (Font::Script, 'M') => c as u32 + 0x20E6,
1298 (Font::Script, 'R') => c as u32 + 0x20C9,
1299 (Font::Script, 'a'..='d' | 'f' | 'h'..='n' | 'p'..='z') => c as u32 + 0x1D455,
1300 (Font::Script, 'e') => c as u32 + 0x20CA,
1301 (Font::Script, 'g') => c as u32 + 0x20A3,
1302 (Font::Script, 'o') => c as u32 + 0x20C5,
1303
1304 (Font::Monospace, 'A'..='Z') => c as u32 + 0x1D62F,
1306 (Font::Monospace, 'a'..='z') => c as u32 + 0x1D629,
1307 (Font::Monospace, '0'..='9') => c as u32 + 0x1D7C6,
1308
1309 (Font::SansSerif, 'A'..='Z') => c as u32 + 0x1D55F,
1311 (Font::SansSerif, 'a'..='z') => c as u32 + 0x1D559,
1312 (Font::SansSerif, '0'..='9') => c as u32 + 0x1D7B2,
1313
1314 (Font::DoubleStruck, 'A' | 'B' | 'D'..='G' | 'I'..='M' | 'O' | 'S'..='Y') => {
1316 c as u32 + 0x1D4F7
1317 }
1318 (Font::DoubleStruck, 'C') => c as u32 + 0x20BF,
1319 (Font::DoubleStruck, 'H') => c as u32 + 0x20C5,
1320 (Font::DoubleStruck, 'N') => c as u32 + 0x20C7,
1321 (Font::DoubleStruck, 'P' | 'Q') => c as u32 + 0x20C9,
1322 (Font::DoubleStruck, 'R') => c as u32 + 0x20CB,
1323 (Font::DoubleStruck, 'Z') => c as u32 + 0x20CA,
1324 (Font::DoubleStruck, 'a'..='z') => c as u32 + 0x1D4F1,
1325 (Font::DoubleStruck, '0'..='9') => c as u32 + 0x1D7A8,
1326
1327 (Font::Italic, 'A'..='Z') => c as u32 + 0x1D3F3,
1329 (Font::Italic, 'a'..='g' | 'i'..='z') => c as u32 + 0x1D3ED,
1330 (Font::Italic, 'h') => c as u32 + 0x20A6,
1331 (Font::Italic, '\u{0391}'..='\u{03A1}' | '\u{03A3}'..='\u{03A9}') => c as u32 + 0x1D351,
1332 (Font::Italic, '\u{03F4}') => c as u32 + 0x1D2FF,
1333 (Font::Italic, '\u{2207}') => c as u32 + 0x1B4F4,
1334 (Font::Italic, '\u{03B1}'..='\u{03C9}') => c as u32 + 0x1D34B,
1335 (Font::Italic, '\u{2202}') => c as u32 + 0x1B513,
1336 (Font::Italic, '\u{03F5}') => c as u32 + 0x1D321,
1337 (Font::Italic, '\u{03D1}') => c as u32 + 0x1D346,
1338 (Font::Italic, '\u{03F0}') => c as u32 + 0x1D328,
1339 (Font::Italic, '\u{03D5}') => c as u32 + 0x1D344,
1340 (Font::Italic, '\u{03F1}') => c as u32 + 0x1D329,
1341 (Font::Italic, '\u{03D6}') => c as u32 + 0x1D345,
1342
1343 (Font::BoldFraktur, 'A'..='Z') => c as u32 + 0x1D52B,
1345 (Font::BoldFraktur, 'a'..='z') => c as u32 + 0x1D525,
1346
1347 (Font::SansSerifBoldItalic, 'A'..='Z') => c as u32 + 0x1D5FB,
1349 (Font::SansSerifBoldItalic, 'a'..='z') => c as u32 + 0x1D5F5,
1350 (Font::SansSerifBoldItalic, '\u{0391}'..='\u{03A1}' | '\u{03A3}'..='\u{03A9}') => {
1351 c as u32 + 0x1D3FF
1352 }
1353 (Font::SansSerifBoldItalic, '\u{03F4}') => c as u32 + 0x1D3AD,
1354 (Font::SansSerifBoldItalic, '\u{2207}') => c as u32 + 0x1B5A2,
1355 (Font::SansSerifBoldItalic, '\u{03B1}'..='\u{03C9}') => c as u32 + 0x1D3F9,
1356 (Font::SansSerifBoldItalic, '\u{2202}') => c as u32 + 0x1B5C1,
1357 (Font::SansSerifBoldItalic, '\u{03F5}') => c as u32 + 0x1D3CF,
1358 (Font::SansSerifBoldItalic, '\u{03D1}') => c as u32 + 0x1D3F4,
1359 (Font::SansSerifBoldItalic, '\u{03F0}') => c as u32 + 0x1D3D6,
1360 (Font::SansSerifBoldItalic, '\u{03D5}') => c as u32 + 0x1D3F2,
1361 (Font::SansSerifBoldItalic, '\u{03F1}') => c as u32 + 0x1D3D7,
1362 (Font::SansSerifBoldItalic, '\u{03D6}') => c as u32 + 0x1D3F3,
1363
1364 (Font::SansSerifItalic, 'A'..='Z') => c as u32 + 0x1D5D7,
1366 (Font::SansSerifItalic, 'a'..='z') => c as u32 + 0x1D5C1,
1367
1368 (Font::BoldSansSerif, 'A'..='Z') => c as u32 + 0x1D593,
1370 (Font::BoldSansSerif, 'a'..='z') => c as u32 + 0x1D58D,
1371 (Font::BoldSansSerif, '\u{0391}'..='\u{03A1}' | '\u{03A3}'..='\u{03A9}') => {
1372 c as u32 + 0x1D3C5
1373 }
1374 (Font::BoldSansSerif, '\u{03F4}') => c as u32 + 0x1D373,
1375 (Font::BoldSansSerif, '\u{2207}') => c as u32 + 0x1B568,
1376 (Font::BoldSansSerif, '\u{03B1}'..='\u{03C9}') => c as u32 + 0x1D3BF,
1377 (Font::BoldSansSerif, '\u{2202}') => c as u32 + 0x1B587,
1378 (Font::BoldSansSerif, '\u{03F5}') => c as u32 + 0x1D395,
1379 (Font::BoldSansSerif, '\u{03D1}') => c as u32 + 0x1D3BA,
1380 (Font::BoldSansSerif, '\u{03F0}') => c as u32 + 0x1D39C,
1381 (Font::BoldSansSerif, '\u{03D5}') => c as u32 + 0x1D3B8,
1382 (Font::BoldSansSerif, '\u{03F1}') => c as u32 + 0x1D39D,
1383 (Font::BoldSansSerif, '\u{03D6}') => c as u32 + 0x1D3B9,
1384 (Font::BoldSansSerif, '0'..='9') => c as u32 + 0x1D7BC,
1385
1386 (_, _) => c as u32,
1388 })
1389 .expect("character not in Unicode (developer error)")
1390 }
1391}
1392
1393pub fn push_mathml<'a, I, E>(
1398 string: &mut String,
1399 parser: I,
1400 config: RenderConfig<'a>,
1401) -> io::Result<()>
1402where
1403 I: Iterator<Item = Result<Event<'a>, E>>,
1404 E: std::error::Error,
1405{
1406 MathmlWriter::new(parser, unsafe { string.as_mut_vec() }, config).write()
1408}
1409
1410pub fn write_mathml<'a, I, W, E>(writer: W, parser: I, config: RenderConfig<'a>) -> io::Result<()>
1415where
1416 I: Iterator<Item = Result<Event<'a>, E>>,
1417 W: io::Write,
1418 E: std::error::Error,
1419{
1420 MathmlWriter::new(parser, writer, config).write()
1421}