Attach the script to the group it belongs to
A bracketed group did not take the power that followed it. The branches
for `(`, `[` and `{` returned from the reader before the call that
attaches superscripts and subscripts, so `(a + b)^2` was set with the
caret as an ordinary symbol and the 2 beside it, and
`\left( x \right)^{2/3}` came out as `(x)^2/3` with the caret visible.
Inside a fraction it was worse: `\frac{1}{\left(1 - e^2\right)^{3/2}}`
put the `3/2` after the fraction, so the exponent hung outside it. A
bracketed group is a base like any other and is now given its scripts by
the same call as everything else.
Two more of the same kind were found while fixing it. A style command
such as `\mathbf` dropped the subscript of `\mathbf{a}_{...}`, and the
vertical bars were not known at all: `\lvert` and `\rvert` were shown as
the letters of their own names, which also lost the power after them.
The check on the formulas compared the characters and so missed all of
this, because every character was present and in the wrong place. It now
also asserts what the notation means: a caret and an underscore say what
to raise or lower and are never drawn, so one that reaches the output is
a script that was never attached. That single assertion found the two
further cases on its first run.
This commit is contained in:
parent
ec78ac009b
commit
ec92981257
|
|
@ -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<Node>
|
|||
};
|
||||
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<Node>
|
|||
// `\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.
|
||||
|
|
|
|||
|
|
@ -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("<msup>") || html.contains("<msub>"),
|
||||
"{latex} kept its script marker: {html}"
|
||||
);
|
||||
assert!(
|
||||
!html.contains("<mo>^</mo>") && !html.contains("<mo>_</mo>"),
|
||||
"{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("</math>")
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ["<mo>^</mo>", "<mo>_</mo>"] {
|
||||
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!(
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Loading…
Reference in New Issue