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.
A formula is written as plain text and translated to MathML. Text left
bare inside a row is not drawn by the browser, so a token without an
element disappears: every multi-digit number did. `a = floor((14 - month)
/ 12)` came out as `a = floor ( ( - month ) )`, and `year + 4800` as
`year + `. The markup looked reasonable at a glance, and the numbers were
simply gone.
The classification now covers every kind of text and has no bare output.
A number of any length is `mn`; one letter of any script is an identifier,
so Greek stays a variable rather than becoming upright text; an operator,
including one written with its spacing, is `mo`; a space is an explicit
`mspace`; and anything else is `mtext` rather than nothing.
A test walks every document in both languages and compares the characters
the page would show against the characters the formula was written with.
It found three more formulas dropping a sign, and it now guards the whole
set: ninety-six formula blocks, checked on every run rather than by
looking at a page. Only letters and digits are compared, because the
other characters in a formula source are notation — `^`, `_`, `*`,
`/`, braces, and the word `sqrt`, which is drawn as a radical sign.
The service is a library with a thin binary over it, which is what makes
the move possible: an integration test is a separate crate and reaches
only what the library makes public, so the modules now live in
`src/lib.rs` and `main.rs` is the entry point. `src/` holds no test
code, which is the shape `apsis-rs` already has.
Eleven items are internal to a module and a test reaches them anyway.
Those stay `pub(crate)` where they are defined and are offered through
`src/testkit.rs`, which is the one place a caller is expected to look for
them. A wrapper is also the only way to offer a private thing outward:
Rust does not let visibility be widened twice, so a `pub(crate)` item
cannot be re-exported as `pub`. That keeps the seams in one file instead
of scattering them through the modules under test.
The 146 module tests and the 23 end to end tests all still run, which is
the number this move had to preserve: the 146 are spread over eighteen
files in `tests/`, one per module that had them.