1//! Equations as OneNote stores them: a linear text where U+FDD0 opens an inline object,
2//! U+FDEE separates its arguments and U+FDEF closes it, with the object's kind on the run
3//! data of the opening character. `Math::parse` builds the tree and `mathml` renders it the
4//! way OneNote's own export does, verified against native fixtures for every kind the
5//! equation editor produced: scripts, fractions, fences, n-ary operators with any limits,
6//! radicals, limit objects, accents, overbars, boxes, matrices and equation arrays.
7
8use super::text::Paragraph;
9use crate::document::MathObject;
10
11#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
12pub enum Math {
13 Identifier(char),
14 /// Upright letters, as the editor stores a function name such as `lim`.
15 Function(String),
16 Number(String),
17 Operator(char),
18 Object {
19 kind: u32,
20 symbols: Vec<char>,
21 /// Column count of a matrix (arguments are its cells, row by row) or an equation
22 /// array (arguments are its rows).
23 columns: Option<u8>,
24 arguments: Vec<Vec<Math>>,
25 },
26}
27
28const OBJECT_START: char = '\u{fdd0}';
29const ARGUMENT_SEPARATOR: char = '\u{fdee}';
30const OBJECT_END: char = '\u{fdef}';
31
32impl Math {
33 /// Whether a paragraph's text is stored as an equation.
34 pub fn is_equation(paragraph: &Paragraph) -> bool {
35 paragraph.text().contains(OBJECT_START)
36 || paragraph
37 .spans()
38 .iter()
39 .any(|span| span.format.math == Some(true))
40 }
41
42 /// The equation a paragraph's text holds, as a sequence of nodes.
43 pub fn parse(paragraph: &Paragraph) -> Result<Vec<Math>, crate::Error> {
44 let invalid = |message| crate::Error { offset: 0, message };
45 let object_at = |offset: usize| -> Option<&MathObject> {
46 paragraph
47 .format_at(u32::try_from(offset).ok()?)
48 .ok()
49 .and_then(|f| f.math_object.as_ref())
50 };
51 struct Frame {
52 kind: u32,
53 symbols: Vec<char>,
54 columns: Option<u8>,
55 arguments: Vec<Vec<Math>>,
56 }
57 let mut frames: Vec<Frame> = Vec::new();
58 let mut sequences: Vec<Vec<Math>> = vec![Vec::new()];
59 let mut number = String::new();
60 let mut word = String::new();
61 let mut offset = 0;
62 for character in paragraph.text().chars() {
63 let here = offset;
64 offset += character.len_utf16();
65 if character.is_ascii_digit() {
66 number.push(character);
67 continue;
68 }
69 if !number.is_empty() {
70 let number = std::mem::take(&mut number);
71 sequences.last_mut().unwrap().push(Math::Number(number));
72 }
73 if character.is_ascii_alphabetic() {
74 word.push(character);
75 continue;
76 }
77 if !word.is_empty() {
78 let word = std::mem::take(&mut word);
79 sequences.last_mut().unwrap().push(Math::Function(word));
80 }
81 match character {
82 OBJECT_START => {
83 // The span lookup treats a boundary offset as the span ending there, so the
84 // opening character's own span is the one containing the offset after it.
85 let object = object_at(here + 1)
86 .ok_or_else(|| invalid("An equation object has no run data"))?;
87 frames.push(Frame {
88 kind: object.kind,
89 symbols: object.symbols.clone(),
90 columns: object.columns,
91 arguments: Vec::new(),
92 });
93 sequences.push(Vec::new());
94 }
95 ARGUMENT_SEPARATOR => {
96 let argument = sequences.pop().unwrap();
97 frames
98 .last_mut()
99 .ok_or_else(|| invalid("An equation argument separator has no object"))?
100 .arguments
101 .push(argument);
102 sequences.push(Vec::new());
103 }
104 OBJECT_END => {
105 let argument = sequences.pop().unwrap();
106 let mut frame = frames
107 .pop()
108 .ok_or_else(|| invalid("An equation object ends before it starts"))?;
109 frame.arguments.push(argument);
110 sequences.last_mut().unwrap().push(Math::Object {
111 kind: frame.kind,
112 symbols: frame.symbols,
113 columns: frame.columns,
114 arguments: frame.arguments,
115 });
116 }
117 c if c.is_whitespace() => sequences.last_mut().unwrap().push(Math::Operator(' ')),
118 c if c.is_alphabetic() => sequences
119 .last_mut()
120 .unwrap()
121 .push(Math::Identifier(plain(c))),
122 c => sequences.last_mut().unwrap().push(Math::Operator(c)),
123 }
124 }
125 if !number.is_empty() {
126 sequences.last_mut().unwrap().push(Math::Number(number));
127 }
128 if !word.is_empty() {
129 sequences.last_mut().unwrap().push(Math::Function(word));
130 }
131 if !frames.is_empty() {
132 return Err(invalid("An equation object never ends"));
133 }
134 Ok(sequences.pop().unwrap())
135 }
136
137 /// MathML for a node sequence, without the `math` wrapper, in OneNote's export form
138 /// (`mml:` prefixed elements, one `mi` per letter).
139 pub fn mathml(nodes: &[Math]) -> String {
140 let mut out = String::new();
141 for node in nodes {
142 node.write(&mut out);
143 }
144 out
145 }
146
147 fn write(&self, out: &mut String) {
148 match self {
149 Math::Identifier(c) => tag(out, "mi", &c.to_string()),
150 Math::Function(name) => tag(out, "mi", name),
151 Math::Number(n) => tag(out, "mn", n),
152 // A parenthesis an object does not pair, as in linear text, is no fence.
153 Math::Operator(c @ ('(' | ')')) => {
154 out.push_str(&format!("<mml:mo fence=\"false\">{c}</mml:mo>"));
155 }
156 Math::Operator(c) => tag(out, "mo", &c.to_string()),
157 Math::Object {
158 kind,
159 symbols,
160 columns,
161 arguments,
162 } => {
163 // A lone letter, number or operator stands bare; anything else is a row.
164 let argument = |out: &mut String, index: usize| match arguments
165 .get(index)
166 .map(Vec::as_slice)
167 {
168 Some([single])
169 if !matches!(single, Math::Object { .. } | Math::Function(_)) =>
170 {
171 single.write(out)
172 }
173 Some(many) => {
174 out.push_str("<mml:mrow>");
175 for node in many {
176 node.write(out);
177 }
178 out.push_str("</mml:mrow>");
179 }
180 None => out.push_str("<mml:mrow/>"),
181 };
182 let wrapped = |out: &mut String, element: &str, order: &[usize]| {
183 out.push_str(&format!("<mml:{element}>"));
184 for index in order {
185 argument(out, *index);
186 }
187 out.push_str(&format!("</mml:{element}>"));
188 };
189 match (kind, arguments.len()) {
190 (11, 1) => wrapped(out, "mpadded", &[0]),
191 (12, 1) => {
192 out.push_str("<mml:menclose notation=\"box\">");
193 argument(out, 0);
194 out.push_str("</mml:menclose>");
195 }
196 // Matrix cells row by row; an equation array's rows carry `&` alignment
197 // marks, exported as an alignment group at the row start and a mark at each.
198 (20, _) | (15, _) => {
199 let width = if *kind == 20 {
200 usize::from(columns.unwrap_or(1).max(1))
201 } else {
202 1
203 };
204 out.push_str("<mml:mtable>");
205 for (r, row) in arguments.chunks(width).enumerate() {
206 out.push_str("<mml:mtr>");
207 for (c, cell) in row.iter().enumerate() {
208 out.push_str("<mml:mtd>");
209 if *kind == 15 {
210 out.push_str("<mml:maligngroup/>");
211 for node in cell {
212 match node {
213 Math::Operator('&') => {
214 out.push_str("<mml:malignmark/>")
215 }
216 node => node.write(out),
217 }
218 }
219 } else {
220 argument(out, r * width + c);
221 }
222 out.push_str("</mml:mtd>");
223 }
224 out.push_str("</mml:mtr>");
225 }
226 out.push_str("</mml:mtable>");
227 }
228 (31, 2) => wrapped(out, "msup", &[0, 1]),
229 (29, 2) => wrapped(out, "msub", &[0, 1]),
230 (30, 3) => wrapped(out, "msubsup", &[0, 1, 2]),
231 (16, 2) | (26, 2) => wrapped(out, "mfrac", &[0, 1]),
232 (25, 2) if !arguments[0].is_empty() => wrapped(out, "mroot", &[1, 0]),
233 (25, _) => wrapped(out, "msqrt", &[arguments.len() - 1]),
234 (19, 2) => wrapped(out, "munder", &[0, 1]),
235 (33, 2) => wrapped(out, "mover", &[0, 1]),
236 // Parentheses are the default fences; other pairs name themselves.
237 (13, _) => {
238 let (open, close) = (symbols.first(), symbols.get(1));
239 if open == Some(&'(') && close == Some(&')') {
240 wrapped(out, "mfenced", &[0]);
241 } else {
242 out.push_str("<mml:mfenced");
243 if let Some(open) = open {
244 out.push_str(&format!(" open=\"{}\"", escaped(*open)));
245 }
246 if let Some(close) = close {
247 out.push_str(&format!(" close=\"{}\"", escaped(*close)));
248 }
249 out.push('>');
250 argument(out, 0);
251 out.push_str("</mml:mfenced>");
252 }
253 }
254 (10, 1) | (23, 1) => {
255 let accent = *kind == 10;
256 out.push_str(&format!("<mml:mover accent=\"{accent}\">"));
257 argument(out, 0);
258 if accent {
259 tag(
260 out,
261 "mo",
262 &spacing(symbols.first().copied().unwrap_or('^')).to_string(),
263 );
264 } else {
265 out.push_str("<mml:mo stretchy=\"true\">");
266 out.push(symbols.first().copied().unwrap_or('¯'));
267 out.push_str("</mml:mo>");
268 }
269 out.push_str("</mml:mover>");
270 }
271 // Lower limit, upper limit, body; integrals take their limits as scripts,
272 // other operators above and below, and an empty limit leaves its side out.
273 (21, 3) => {
274 let operator = symbols.first().copied().unwrap_or('∑');
275 let scripts = ('\u{222b}'..='\u{2233}').contains(&operator);
276 let (lower, upper) = (!arguments[0].is_empty(), !arguments[1].is_empty());
277 let element = match (lower, upper, scripts) {
278 (true, true, true) => "msubsup",
279 (true, true, false) => "munderover",
280 (true, false, true) => "msub",
281 (true, false, false) => "munder",
282 (false, true, true) => "msup",
283 (false, true, false) => "mover",
284 (false, false, _) => "mrow",
285 };
286 out.push_str(&format!(
287 "<mml:{element}><mml:mo stretchy=\"false\">{operator}</mml:mo>"
288 ));
289 if lower {
290 argument(out, 0);
291 }
292 if upper {
293 argument(out, 1);
294 }
295 out.push_str(&format!("</mml:{element}>"));
296 out.push_str("<mml:mrow>");
297 for node in &arguments[2] {
298 node.write(out);
299 }
300 out.push_str("</mml:mrow>");
301 }
302 _ => {
303 out.push_str("<mml:mrow>");
304 for symbol in symbols {
305 tag(out, "mo", &symbol.to_string());
306 }
307 for index in 0..arguments.len() {
308 argument(out, index);
309 }
310 out.push_str("</mml:mrow>");
311 }
312 }
313 }
314 }
315 }
316}
317
318fn escaped(c: char) -> String {
319 match c {
320 '<' => "&lt;".into(),
321 '&' => "&amp;".into(),
322 '"' => "&quot;".into(),
323 c => c.to_string(),
324 }
325}
326
327/// The spacing character OneNote exports for a combining accent.
328fn spacing(c: char) -> char {
329 match c {
330 '\u{302}' => '^',
331 '\u{303}' => '~',
332 '\u{308}' => '¨',
333 c => c,
334 }
335}
336
337fn tag(out: &mut String, element: &str, content: &str) {
338 out.push_str(&format!("<mml:{element}>"));
339 for c in content.chars() {
340 match c {
341 '<' => out.push_str("&lt;"),
342 '&' => out.push_str("&amp;"),
343 '>' => out.push_str("&gt;"),
344 ' ' => out.push_str("&nbsp;"),
345 c => out.push(c),
346 }
347 }
348 out.push_str(&format!("</mml:{element}>"));
349}
350
351/// OneNote exports mathematical italic Latin letters as their plain letters and leaves every
352/// other alphabet (Greek, double-struck, …) as stored.
353pub(crate) fn plain(c: char) -> char {
354 match u32::from(c) {
355 code @ 0x1d434..=0x1d44d => char::from_u32(code - 0x1d434 + u32::from('A')).unwrap(),
356 code @ 0x1d44e..=0x1d467 => char::from_u32(code - 0x1d44e + u32::from('a')).unwrap(),
357 0x210e => 'h',
358 _ => c,
359 }
360}
361
362impl Math {
363 /// The paragraph OneNote stores for `nodes`: linear text with object controls, every run
364 /// formatted the way the equation editor formats it (Cambria Math, italic, the math flags
365 /// and the math language over `base`; function names upright), and the run data each object
366 /// carries.
367 /// How the equation editor formats math typed over `base`, before it builds up: as a
368 /// run between objects.
369 pub fn format(base: &crate::document::Format) -> crate::document::Format {
370 let mut format = style(base);
371 format.math_object = Some(MathObject {
372 kind: PLAIN_RUN,
373 arguments: None,
374 columns: None,
375 symbols: Vec::new(),
376 });
377 format
378 }
379
380 pub fn paragraph(nodes: &[Math], base: &crate::document::Format) -> Paragraph {
381 let style = style(base);
382 let mut runs = Vec::new();
383 write_sequence(nodes, None, &mut runs);
384 Paragraph::from_runs(runs.into_iter().map(|run| {
385 let mut format = style.clone();
386 if run.upright {
387 format.italic = Some(false);
388 }
389 format.math_object = Some(run.object);
390 (run.text, format)
391 }))
392 }
393}
394
395/// The object kind OneNote gives runs between objects.
396const PLAIN_RUN: u32 = 0x9000_0000;
397
398/// How the equation editor formats math over `base`.
399fn style(base: &crate::document::Format) -> crate::document::Format {
400 let mut style = base.clone();
401 style.italic = Some(true);
402 style.hidden = Some(false);
403 style.hyperlink = Some(false);
404 style.hyperlink_label = None;
405 style.math = Some(true);
406 style.embedded_object = Some(true);
407 style.font = Some("Cambria Math".into());
408 style.language = Some(0x1007f);
409 style
410}
411
412struct Run {
413 text: String,
414 object: MathObject,
415 upright: bool,
416 /// Text alone, which the control ending its argument may join.
417 leaf: bool,
418}
419
420/// Runs for `nodes`, an argument of an object of kind `within` or the equation itself. Text
421/// in an argument carries its object's kind, as the equation editor stores it.
422fn write_sequence(nodes: &[Math], within: Option<u32>, runs: &mut Vec<Run>) {
423 let object = || MathObject {
424 kind: within.unwrap_or(PLAIN_RUN),
425 arguments: None,
426 columns: None,
427 symbols: Vec::new(),
428 };
429 let mut leaf = String::new();
430 let flush = |leaf: &mut String, runs: &mut Vec<Run>, upright: bool| {
431 if !leaf.is_empty() {
432 runs.push(Run {
433 text: std::mem::take(leaf),
434 object: object(),
435 upright,
436 leaf: true,
437 });
438 }
439 };
440 for node in nodes {
441 match node {
442 Math::Identifier(c) => leaf.push(italic(*c)),
443 Math::Function(name) => {
444 flush(&mut leaf, runs, false);
445 leaf.push_str(name);
446 flush(&mut leaf, runs, true);
447 }
448 Math::Number(n) => leaf.push_str(n),
449 Math::Operator(c) => leaf.push(*c),
450 Math::Object {
451 kind,
452 symbols,
453 columns,
454 arguments,
455 } => {
456 flush(&mut leaf, runs, false);
457 runs.push(Run {
458 text: OBJECT_START.to_string(),
459 object: MathObject {
460 kind: *kind,
461 arguments: Some(arguments.len() as u32),
462 columns: *columns,
463 symbols: symbols.clone(),
464 },
465 upright: false,
466 leaf: false,
467 });
468 for (index, argument) in arguments.iter().enumerate() {
469 let before = runs.len();
470 write_sequence(argument, Some(*kind), runs);
471 let extended = runs.len() > before;
472 let control = if index + 1 == arguments.len() {
473 OBJECT_END
474 } else {
475 ARGUMENT_SEPARATOR
476 };
477 let object = MathObject {
478 kind: *kind,
479 arguments: (index > 0).then_some(index as u32),
480 columns: None,
481 symbols: Vec::new(),
482 };
483 match runs.last_mut() {
484 Some(run) if extended && run.leaf && !run.upright => {
485 run.text.push(control);
486 run.object = object;
487 run.leaf = false;
488 }
489 _ => runs.push(Run {
490 text: control.to_string(),
491 object,
492 upright: false,
493 leaf: false,
494 }),
495 }
496 }
497 }
498 }
499 }
500 flush(&mut leaf, runs, false);
501}
502
503/// The equation editor stores Latin letters and lowercase Greek as mathematical italics.
504pub(crate) fn italic(c: char) -> char {
505 match c {
506 'h' => '\u{210e}',
507 'A'..='Z' => char::from_u32(0x1d434 + u32::from(c) - u32::from('A')).unwrap(),
508 'a'..='z' => char::from_u32(0x1d44e + u32::from(c) - u32::from('a')).unwrap(),
509 'α'..='ω' => char::from_u32(0x1d6fc + u32::from(c) - u32::from('α')).unwrap(),
510 c => c,
511 }
512}