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