A quantity was written in an inline code span, as in `0.002 m^2/kg`, so
the caret was drawn literally and the span looked like code. A hundred and
ninety-five of them are now inline formulas, and with them the powers of
ten, the Greek letters, the expressions with an operator in them, and the
symbols with a subscript. Single letters went the same way, except the
three that name a classification in the format, which are data rather
than mathematics and stay monospaced.
Drawing a code block as a formula was removed rather than refined. The
guess was wrong in practice: a doc comment in the library carries a Rust
example in a fence with no language on it, and the example holds an equals
sign, so the page drew a program as a formula and reported the underscores
in its identifiers as script markers. A code block is now drawn as a code
block and nothing else, and the reader that guessed is gone, with five
hundred lines of parser and twenty-three tests that tested it.
The check on the formulas was comparing the source with itself. The source
is carried beside the MathML for a reader to copy, and both the Rust check
and the audit were reading the whole span, so a formula that lost every
number still passed. Both now take the MathML alone, and the audit counts
only the text of a leaf element, because a browser drops text that has no
element around it and counting that text made the check pass whatever the
renderer produced. With the check made real it found the program drawn as
a formula, which is what led to the guess being removed.
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.