diff --git a/src/docs/mathml.rs b/src/docs/mathml.rs index 2de2bce..c111a0d 100644 --- a/src/docs/mathml.rs +++ b/src/docs/mathml.rs @@ -524,32 +524,35 @@ fn parse_symbol(symbol: char, tokens: &[Token], cursor: &mut usize, _depth: &mut Node::Text(text) } // An opening bracket stretches to the height of what it contains. + // + // A bracketed group is a base like any other, so it is given its + // scripts by the same call at the end of this function. Returning + // the group from here instead left `(a + b)^2` with the caret set as + // an ordinary symbol and the exponent beside it, and inside a + // fraction the exponent escaped the fraction altogether. '(' => { let inner = parse_row(tokens, cursor, Some(&[',', ')'])); if matches!(tokens.get(*cursor), Some(Token::Symbol(')'))) { *cursor += 1; } - return vec![Node::Row(vec![ + Node::Row(vec![ Node::Text("(".to_string()), Node::Row(inner), Node::Text(")".to_string()), - ])]; + ]) } '[' => { let inner = parse_row(tokens, cursor, Some(&[',', ']'])); if matches!(tokens.get(*cursor), Some(Token::Symbol(']'))) { *cursor += 1; } - return vec![Node::Row(vec![ + Node::Row(vec![ Node::Text("[".to_string()), Node::Row(inner), Node::Text("]".to_string()), - ])]; - } - '{' => { - let inner = parse_row(tokens, cursor, Some(&['}'])); - return vec![Node::Row(inner)]; + ]) } + '{' => Node::Row(parse_row(tokens, cursor, Some(&['}']))), '_' | '^' => Node::Text(symbol.to_string()), // The spaces are kept between words and dropped elsewhere, which is // what keeps `a b` apart without leaving gaps after a bracket. @@ -629,6 +632,18 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec }; vec![Node::Text(symbol.to_string())] } + // A vertical bar names an absolute value or a norm. Written as a + // command it is a delimiter like any other, and was being shown as + // the letters of its own name. + "lvert" | "rvert" | "vert" => { + vec![attach_scripts(Node::Text("|".to_string()), tokens, cursor)] + } + "lVert" | "rVert" | "Vert" => { + vec![attach_scripts(Node::Text("‖".to_string()), tokens, cursor)] + } + "lbrace" | "rbrace" => { + vec![attach_scripts(Node::Text("{".to_string()), tokens, cursor)] + } "bmod" => vec![Node::Text(" mod ".to_string())], "pmod" => { let argument = parse_argument(tokens, cursor); @@ -686,12 +701,15 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec // `\text{...}` holds words as they are written. Read as variables // they would be set one letter at a time, so `km` became `k m`. "text" | "mathrm" | "operatorname" | "mbox" => { - vec![Node::Verbatim(parse_raw_text(tokens, cursor))] + let words = Node::Verbatim(parse_raw_text(tokens, cursor)); + vec![attach_scripts(words, tokens, cursor)] } - // A style command keeps its argument set as mathematics. + // A style command keeps its argument set as mathematics. The group + // it holds is a base, so it takes the scripts that follow it: the + // `_central` of `\mathbf{a}_{\text{central}}` belongs to the `a`. "mathsf" | "mathtt" | "mathbf" | "mathit" | "mathcal" | "mathfrak" => { let inner = parse_argument(tokens, cursor); - vec![Node::Row(vec![inner])] + vec![attach_scripts(Node::Row(vec![inner]), tokens, cursor)] } // An unknown command is shown as written, so that a formula that // uses one still reads rather than losing part of itself. diff --git a/tests/docs_mathml.rs b/tests/docs_mathml.rs index c16b908..4842fce 100644 --- a/tests/docs_mathml.rs +++ b/tests/docs_mathml.rs @@ -296,3 +296,38 @@ fn a_row_holds_elements_rather_than_text() { ); } } + +#[test] +fn a_bracketed_group_takes_the_power_that_follows_it() { + // The bracket branches returned before the scripts were attached, so the + // caret was set as an ordinary symbol and the exponent sat beside the + // group: `(a + b)^2` showed its own caret. + for latex in [ + r"(a + b)^2", + r"\left( \frac{x_{ke}}{n_0} \right)^{2/3}", + r"\left[ a \right]^2", + r"\{a\}_2", + ] { + let html = render_plain_of(latex); + assert!( + html.contains("") || html.contains(""), + "{latex} kept its script marker: {html}" + ); + assert!( + !html.contains("^") && !html.contains("_"), + "{latex} left a script marker in the output: {html}" + ); + } +} + +/// The MathML of a formula written in LaTeX. +fn render_plain_of(latex: &str) -> String { + apsis_book::docs::mathml::render(latex, false) + .split("display=\"inline\">") + .nth(1) + .unwrap_or("") + .split("") + .next() + .unwrap_or("") + .to_string() +} diff --git a/tests/formula_rendering.rs b/tests/formula_rendering.rs index 3f93674..1565798 100644 --- a/tests/formula_rendering.rs +++ b/tests/formula_rendering.rs @@ -173,6 +173,17 @@ fn every_formula_shows_every_character_it_was_written_with() { .chars() .filter(|c| c.is_alphanumeric()) .collect(); + // A caret and an underscore are notation: they say what to + // raise or lower, and neither is drawn. One that reaches the + // output is a script that was never attached, which leaves a + // formula that has every character in it and is still wrong. + let body = &html[start..end]; + for marker in ["^", "_"] { + assert!( + !body.contains(marker), + "{id} 渲染结果里留下了记法符号 {marker}:{body}" + ); + } let missing: String = written.chars().filter(|c| !shown.contains(*c)).collect(); if !missing.is_empty() { failures.push(format!( diff --git a/tmp/error.png b/tmp/error.png new file mode 100644 index 0000000..5b9a8c2 Binary files /dev/null and b/tmp/error.png differ diff --git a/tmp/fixed.png b/tmp/fixed.png new file mode 100644 index 0000000..2a40db4 Binary files /dev/null and b/tmp/fixed.png differ diff --git a/tmp/right.png b/tmp/right.png new file mode 100644 index 0000000..ded2a4a Binary files /dev/null and b/tmp/right.png differ