pulldown_latex/
mathml.rs

1//! A simple MathML Core renderer.
2//!
3//! This crate provides a "simple" `mathml` renderer which is available through the
4//! [`push_mathml`] and [`write_mathml`] functions.
5
6use 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        // Size of the buffer is arbitrary for performance guess.
37        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                // Mathematical environments do something different with state compared to things
136                // like left/right and {}.
137                //
138                // - They do not inherit state from their parent, their state is reset to default
139                // upon entering.
140                // - State changes occuring within them are also reset when crossing alignments or
141                // newlines (`&` or `\\`).
142                //
143                // For these reasons, they don't use the `open_tag` method.
144                // TODO: Make `StateChange::Style` be maintained across math_env groups.
145                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                    // Every state appliable to the style of the mrow is reset, i.e., everything
159                    // except font.
160                    *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                    // LaTeX does _nothing_ when a newline is encountered in an eqution, we do the
478                    // same thing.
479                    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"&nbsp;")?;
582                }
583                self.writer.write_all(trimmed.as_bytes())?;
584                if text.ends_with(char::is_whitespace) {
585                    self.writer.write_all(b"&nbsp;")?;
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            // TexBook p. 153 and 157 for math classes.
672            // TexBook p. 170 for spacing.
673            // TexBook p. 438-443 for type setting rules (especially important for Binary Ops)
674            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        // Safety: this function must only write valid utf-8 to the writer.
856        // How is the writer used?:
857        // - using `write_all` with a utf-8 string.
858        // - using `write!` with a utf-8 string, and the parameters must all be valid utf-8 since
859        //      they are formatted using the `Display` trait.
860        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    /// Map a character to its mathvariant equivalent.
1240    fn map_char(self, c: char) -> char {
1241        char::from_u32(match (self, c) {
1242            // Bold Script mappings
1243            (Font::BoldScript, 'A'..='Z') => c as u32 + 0x1D48F,
1244            (Font::BoldScript, 'a'..='z') => c as u32 + 0x1D489,
1245
1246            // Bold Italic mappings
1247            (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            // Bold mappings
1264            (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            // Fraktur mappings
1281            (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            // Script mappings
1289            (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            // Monospace mappings
1305            (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            // Sans Serif mappings
1310            (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            // Double Struck mappings
1315            (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            // Italic mappings
1328            (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            // Bold Fraktur mappings
1344            (Font::BoldFraktur, 'A'..='Z') => c as u32 + 0x1D52B,
1345            (Font::BoldFraktur, 'a'..='z') => c as u32 + 0x1D525,
1346
1347            // Sans Serif Bold Italic mappings
1348            (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            // Sans Serif Italic mappings
1365            (Font::SansSerifItalic, 'A'..='Z') => c as u32 + 0x1D5D7,
1366            (Font::SansSerifItalic, 'a'..='z') => c as u32 + 0x1D5C1,
1367
1368            // Bold Sans Serif mappings
1369            (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            // Upright mappings (map to themselves) and unknown mappings
1387            (_, _) => c as u32,
1388        })
1389        .expect("character not in Unicode (developer error)")
1390    }
1391}
1392
1393/// Takes a [`Parser`], or any `Iterator<Item = Result<Event<'_>, E>>` as input, and renders
1394/// the MathML into the given string.
1395///
1396/// [`Parser`]: crate::parser::Parser
1397pub 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    // SAFETY: The MathmlWriter guarantees that all writes to the writer are valid utf-8.
1407    MathmlWriter::new(parser, unsafe { string.as_mut_vec() }, config).write()
1408}
1409
1410/// Takes a [`Parser`], or any `Iterator<Item = Result<Event<'_>, E>>`, as input and renders the
1411/// MathML into the given writer.
1412///
1413/// [`Parser`]: crate::parser::Parser
1414pub 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}