Skip to main content

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    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        // Size of the buffer is arbitrary for performance guess.
38        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                // Mathematical environments do something different with state compared to things
138                // like left/right and {}.
139                //
140                // - They do not inherit state from their parent, their state is reset to default
141                // upon entering.
142                // - State changes occuring within them are also reset when crossing alignments or
143                // newlines (`&` or `\\`).
144                //
145                // For these reasons, they don't use the `open_tag` method.
146                // TODO: Make `StateChange::Style` be maintained across math_env groups.
147                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                    // Every state appliable to the style of the mrow is reset, i.e., everything
161                    // except font.
162                    *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                    // LaTeX does _nothing_ when a newline is encountered in an eqution, we do the
489                    // same thing.
490                    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"&nbsp;")?;
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"&amp;")?,
608                                '<' => self.writer.write_all(b"&lt;")?,
609                                '>' => self.writer.write_all(b"&gt;")?,
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"&nbsp;")?;
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                    // `BoldSymbol` collapses to `Bold` for digits, since no italic
629                    // Unicode math variant exists for digits.
630                    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            // TexBook p. 153 and 157 for math classes.
720            // TexBook p. 170 for spacing.
721            // TexBook p. 438-443 for type setting rules (especially important for Binary Ops)
722            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        // Safety: this function must only write valid utf-8 to the writer.
904        // How is the writer used?:
905        // - using `write_all` with a utf-8 string.
906        // - using `write!` with a utf-8 string, and the parameters must all be valid utf-8 since
907        //      they are formatted using the `Display` trait.
908        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            // In error recovery scenario, a tag might not be closed.
962            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        // Clean up any remaining unbalanced state rather than panicking.
983        // This can happen with malformed input that confuses the environment tracking.
984        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    /// Map a character to its mathvariant equivalent.
1330    fn map_char(self, c: char) -> char {
1331        char::from_u32(match (self, c) {
1332            // Bold Script mappings
1333            (Font::BoldScript, 'A'..='Z') => c as u32 + 0x1D48F,
1334            (Font::BoldScript, 'a'..='z') => c as u32 + 0x1D489,
1335
1336            // Bold Italic mappings
1337            (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            // Bold mappings
1354            (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            // Fraktur mappings
1371            (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            // Script mappings
1379            (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            // Monospace mappings
1395            (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            // Sans Serif mappings
1400            (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            // Double Struck mappings
1405            (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            // Double Struck Italic mappings (U+2145–U+2149): only D, d, e, i, j exist.
1418            (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            // Italic mappings
1423            (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            // Bold Fraktur mappings
1439            (Font::BoldFraktur, 'A'..='Z') => c as u32 + 0x1D52B,
1440            (Font::BoldFraktur, 'a'..='z') => c as u32 + 0x1D525,
1441
1442            // Sans Serif Bold Italic mappings
1443            (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            // Sans Serif Italic mappings
1460            (Font::SansSerifItalic, 'A'..='Z') => c as u32 + 0x1D5D7,
1461            (Font::SansSerifItalic, 'a'..='z') => c as u32 + 0x1D5C1,
1462
1463            // Bold Sans Serif mappings
1464            (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            // Upright mappings (map to themselves) and unknown mappings
1482            (_, _) => c as u32,
1483        })
1484        .expect("character not in Unicode (developer error)")
1485    }
1486}
1487
1488/// Takes a [`Parser`], or any `Iterator<Item = Result<Event<'_>, E>>` as input, and renders
1489/// the MathML into the given string.
1490///
1491/// [`Parser`]: crate::parser::Parser
1492pub 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    // SAFETY: The MathmlWriter guarantees that all writes to the writer are valid utf-8.
1502    MathmlWriter::new(parser, unsafe { string.as_mut_vec() }, config).write()
1503}
1504
1505/// Takes a [`Parser`], or any `Iterator<Item = Result<Event<'_>, E>>`, as input and renders the
1506/// MathML into the given writer.
1507///
1508/// [`Parser`]: crate::parser::Parser
1509pub 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"&amp;",
1524            b'<' => b"&lt;",
1525            b'>' => b"&gt;",
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}