| 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 | |
| 8 | use super::text::Paragraph; |
| 9 | use crate::document::MathObject; |
| 10 | |
| 11 | #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] |
| 12 | pub 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 | |
| 28 | const OBJECT_START: char = '\u{fdd0}'; |
| 29 | const ARGUMENT_SEPARATOR: char = '\u{fdee}'; |
| 30 | const OBJECT_END: char = '\u{fdef}'; |
| 31 | |
| 32 | impl 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 | |
| 318 | fn 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. |
| 328 | fn spacing(c: char) -> char { |
| 329 | match c { |
| 330 | '\u{302}' => '^', |
| 331 | '\u{303}' => '~', |
| 332 | '\u{308}' => '¨', |
| 333 | c => c, |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | fn 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. |
| 353 | pub(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 | |
| 362 | impl 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. |
| 396 | const PLAIN_RUN: u32 = 0x9000_0000; |
| 397 | |
| 398 | /// How the equation editor formats math over `base`. |
| 399 | fn 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 | |
| 412 | struct 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. |
| 422 | fn 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. |
| 504 | pub(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 | } |