All twenty test files and the tool handlers. The tests that share the
paragraph explaining why they live in tests/ now share its translation
too, so the same four lines are written once per file rather than
paraphrased.
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 was written as a fenced code block, which is what a plain text
file allows and is not what Markdown has for mathematics. A code block is
code: the reader of the source sees a formula presented as code, and the
renderer had been taught to guess which blocks held equations, which is a
guess that is wrong whenever a block holds something else.
Every formula now stands between dollar signs, which is the notation every
Markdown mathematics renderer reads, and the guess is gone from the
renderer. A hundred and four blocks became two hundred and four formulas
in the two languages.
Three things that were in a code block were not formulas and are now what
they are. A paragraph of geometry was prose. A table of the exponent
notation is a table, because its two columns are a written form and what
it means. Two blocks are left as they were: the layer diagram, which is a
diagram, and a sample element set, which has to keep its column alignment.
The renderer learned what the notation needs and did not have: matrix
environments, with an ampersand between the columns of a row and a double
backslash between the rows; the floor and ceiling marks; the modulo
operator; a mark above an argument, as in the time derivative of Omega and
a barred coefficient; the text command, whose argument is words and was
being set one letter at a time; and the spacing commands.
The check on the formulas covered the concept documents and not the
interface documentation, so every formula there was unexamined. It covers
both now, and it treats a command name and an environment name as
notation, which is what they are: neither is drawn as the letters of its
own name.
A doc comment names an item by its full path, as in
`NoiseTemperature::from_noise_figure_db`. A path has no space in it, so
the column could shrink and the word could not, and the interface page of
a crate scrolled sideways on a phone. The body of a document now breaks a
word rather than overflow.
The audit that renders every route now covers the whole interface: 744
combinations, being ninety-three routes in both languages and both themes
at two widths, and it passes. It reports four things, which are the four
ways a page has failed here before: a page that does not load, a page
with almost no text, a page that scrolls sideways, and a formula that
loses a letter or a digit.
Two mistakes of the audit itself are fixed rather than worked around.
It awaited a `Runtime.evaluate` that called `location.reload`. Such a
call destroys its own execution context, so the reply never arrives, the
promise never settles and the run hangs until the environment kills it.
The preferences are now set once per language on a page that stays put,
and each route is reached by an ordinary navigation.
It gave the browser a function whose comment contained a backtick. The
backtick closed the template literal that held the function, so the script
did not parse at all.
The formula comparison now treats a command name as notation, as it
already did for the operators: \Delta draws a Greek letter and \sqrt a
radical sign, so neither is drawn as the letters of its own name.
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.
`apsis-rs` now writes each doc comment in English and then in Chinese, in
the one comment beside the code. The extractor splits the two halves as it
reads them and writes both to the catalogue, so the interface
documentation carries both languages and no translation file is maintained
beside it.
This replaces `content/api.zh.json`, which held the Chinese keyed by item
name. That arrangement was unsound: a name is not unique. `new` is
defined by fifty-nine crates and does not mean the same thing in each, so
a translation filed under the bare name could be shown against the wrong
documentation. Splitting the comment removes the question, because the
translation is attached to the item rather than looked up by a name.
The coverage went from 434 of 1202 items to all of them, and the crate
summaries from 15 of 40 to all of them. A test now asserts that no item in
the committed catalogue is left without Chinese, so a crate that is not
translated fails the build rather than reading as English to a Chinese
reader.
Two defects surfaced while doing this.
The JSON codec decoded one string character by validating the whole
remaining buffer as UTF-8. That is correct and quadratic, and it repeated
for every character of the document: the catalogue took twenty-nine
seconds to parse once it grew, and the service could not start. It now
validates only the sequence at hand, which is six milliseconds.
The module documentation was joined into one line before it was split,
which destroyed both the paragraph structure and the boundary between the
languages, so the English half came out empty and the Chinese half held
the English text.
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.