Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ date when the release is tagged.

### Fixed

- **Dependency explorer.** A bare `\ref` in a summary shows as typed rather
than as `???`; a pass that adds to a page no longer reads MathJax's output,
so no hidden duplicate formula lands in its assistive copy; the rail's and
details' rows keep their whole focus rings; and a typeset distribution label
stays together (#468).
- **Claims.** `autoform dashboard` runs its claim listing's Git in the
dashboard's process group again, so Ctrl+C or a signal to that group stops
Git along with the dashboard (#447). On macOS and Windows, a listing shows
Expand Down
6 changes: 4 additions & 2 deletions autoform_cli/dag_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,7 @@ def viewer_script() -> str:
.bp-dag-filter-popover legend { margin-bottom: 6px; }
.bp-dag-filter-popover legend, .bp-dag-viewer.bp-dag-viewer .bp-dag-navigation-group h2 { color: var(--dag-muted); font-size: 11px; font-weight: 600; letter-spacing: .06em; line-height: 1.3; text-transform: uppercase; }
.bp-dag-viewer mjx-container { text-transform: none; }
.bp-dag-viewer.bp-dag-viewer :is(ul, ol):has(mjx-container) { overflow-x: visible; }
.bp-dag-check { display: flex; align-items: center; gap: 7px; min-height: 32px; padding: 3px 5px; border-radius: var(--dag-radius-row); }
.bp-dag-check:hover { background: var(--dag-panel); }
.bp-dag-check input { accent-color: var(--dag-link); }
Expand Down Expand Up @@ -1140,9 +1141,10 @@ def viewer_script() -> str:
var section = element("section", "bp-dag-section"), list = element("div", "bp-dag-distribution");
section.appendChild(element("h3", "", title));
rows.forEach(function (row) {
var line = element("div", "bp-dag-distribution-row"), name = element("span", "bp-dag-distribution-label " + TEX);
var line = element("div", "bp-dag-distribution-row"), name = element("span", "bp-dag-distribution-label");
if (row[2]) name.appendChild(swatch(row[2]));
name.appendChild(document.createTextNode(row[0])); line.appendChild(name);
// The label is flex, so the text goes in a span of its own and stays one item.
name.appendChild(element("span", TEX, row[0])); line.appendChild(name);
line.appendChild(element("span", "", String(row[1]))); list.appendChild(line);
});
section.appendChild(list); refs.inspectorInner.appendChild(section);
Expand Down
14 changes: 11 additions & 3 deletions autoform_cli/mathjax.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ def _script(macros: dict[str, object]) -> str:
"macros": {**dict(sorted(macros.items())), **{name: "\\notallowed" for name in NEUTRALISED}},
"inlineMath": [["$", "$"], ["\\(", "\\)"]],
"displayMath": [["$$", "$$"], ["\\[", "\\]"]],
# A reference outside a formula is not one: in an article every
# formula is delimited, and the explorer's marked prose shows a
# bare \ref as typed rather than as a missing label's ???.
"processRefs": False,
},
# Each card formula is one of these, as render writes it.
"card": {
Expand Down Expand Up @@ -250,13 +254,17 @@ def _script(macros: dict[str, object]) -> str:
var adaptor = startup.adaptor;
var formulas = adaptor.getElements(["." + SETTINGS.formulas], root);
var controls = formulas.filter(function (formula) { return inControl(adaptor, formula); });
var steps = [[SETTINGS.page, {processHtmlClass: SETTINGS.nothing},
// A pass that adds to the page reads the formulas and cards typeset
// before it too; MathJax's output, and the hidden copy of each formula it
// keeps for assistive technology, are not read again.
var page = {processHtmlClass: SETTINGS.nothing, skipHtmlTags: {"[+]": ["mjx-container"]}};
var steps = [[SETTINGS.page, copy(page),
formulas.filter(function (formula) { return controls.indexOf(formula) < 0; })]];
if (controls.length) {
steps.push([SETTINGS.page, {processHtmlClass: SETTINGS.nothing, enableMenu: false, renderActions: {explorable: []}}, controls]);
steps.push([SETTINGS.page, Object.assign(copy(page), {enableMenu: false, renderActions: {explorable: []}}), controls]);
}
adaptor.getElements(["." + SETTINGS.cards], root).forEach(function (card) {
steps.push([SETTINGS.card, {ignoreHtmlClass: SETTINGS.cards, processHtmlClass: SETTINGS.cardFormulas}, [card]]);
steps.push([SETTINGS.card, Object.assign(copy(page), {ignoreHtmlClass: SETTINGS.cards, processHtmlClass: SETTINGS.cardFormulas}), [card]]);
});
// What the renderer kept of the last page, which every renderer can
// forget, though only CHTML has clearCache. A pass that adds to the page
Expand Down
5 changes: 4 additions & 1 deletion tests/test_dag_viewer_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2225,8 +2225,11 @@ def with_tex(route: object) -> None:
playwright.expect(page.locator(f"{control} mjx-container").first).to_be_attached()
assert page.locator(f"{control} [tabindex]").count() == 0, control
assert page.locator('.bp-dag-navigation-title mjx-container[tabindex="0"]').count() == 1
# Map cards are not typeset, and MathJax's own document, which MathJax.typesetPromise reads, holds nothing.
# Map cards and the head's trail, which repeats the rail's, show titles as typed, and MathJax's own
# document, which MathJax.typesetPromise reads, holds nothing.
assert page.locator(".bp-dag-node mjx-container").count() == 0
assert page.locator(".bp-dag-breadcrumb mjx-container").count() == 0
assert "Roadmap over $\\mathbb{N}$" in page.locator(".bp-dag-breadcrumb").inner_text()
assert page.evaluate("Array.from(MathJax.startup.document.math).length") == 0


Expand Down
9 changes: 9 additions & 0 deletions tests/test_graph_view_rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,15 @@ def test_explorer_host_is_escaped_responsive_and_accessible() -> None:
assert script.count("typeset();") == 3
# A formula keeps its letters' case under the rail's group headings, which are set in capitals.
assert ".bp-dag-viewer mjx-container { text-transform: none; }" in dag_viewer._STYLE
# The site clips a list holding a formula; the explorer's lists sit in panels that scroll, and clipping
# them cut the sides of their rows' focus rings.
assert ".bp-dag-viewer.bp-dag-viewer :is(ul, ol):has(mjx-container) { overflow-x: visible; }" in dag_viewer._STYLE
# A distribution label is flex, so its text is one span; the handle is marked only for a node's title;
# search rows and the head's trail show titles as typed.
assert 'name.appendChild(element("span", TEX, row[0]));' in script
assert "refs.sheetHandle.classList.toggle(TEX, !!state.selected);" in script
assert 'choose.appendChild(element("span", "bp-dag-result-title", match.node.title));' in script
assert 'crumbs.appendChild(trailList(trail.length ? trail : [{label: data.title || "Dependencies"}]));' in script
assert 'refs.announcement.setAttribute("aria-live", "polite")' in script
assert "control.dataset.autoformNodeId = node.id" in script
assert "new ResizeObserver(resize)" in script
Expand Down
116 changes: 113 additions & 3 deletions tests/test_site_mathjax_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ def mathjax_package(*, node: bool = True) -> Path:
# the next page as Material's instant navigation shows it, "new" or "old":
# whether mkdocs.yml also lists the bundle, after the script, and "after" when
# a project script listed after it assigns the configuration Material's
# documentation gives, and "explorer" when the next page is instead what the
# documentation gives, or "version" when the bundle is another MathJax release,
# and "explorer" when the next page is instead what the
# dependency explorer draws into the first, after which it asks for a pass.
# The bundle's menu needs a browser, so each document gets one with what the
# script uses of it.
Expand Down Expand Up @@ -89,9 +90,15 @@ def mathjax_package(*, node: bool = True) -> Path:
let docs = [];
let first = null;
MathJax.startup.ready = () => {
if (override === "version") MathJax.version = "3.2.1";
const original = MathJax._.mathjax.mathjax.document;
MathJax._.mathjax.mathjax.document = function (root, options) {
// What the script asked of the document, before MathJax drops what Node lacks, such as the menu.
const requested = JSON.parse(JSON.stringify({
enableMenu: options.enableMenu, renderActions: options.renderActions, skipHtmlTags: options.skipHtmlTags,
}));
const doc = original.call(this, root, options);
doc.requested = requested;
doc.menu = new Menu();
if (first === null) first = doc;
else docs.push(doc);
Expand Down Expand Up @@ -125,6 +132,7 @@ class Menu {}
return docs.map((doc) => ({
card: cardOf(doc),
packages: doc.inputJax[0].parseOptions.options.packages.slice(),
requested: doc.requested,
math: Array.from(doc.math).map((item) => ({tex: item.math, mml: MathJax.startup.toMML(item.root)})),
}));
}
Expand Down Expand Up @@ -248,6 +256,88 @@ def test_the_rendered_configuration_typesets_each_card_alone(tmp_path: Path, pro
assert _LEQ in second[0]["math"][0]["mml"] and _LEQ in second[1]["math"][0]["mml"]


def test_another_mathjax_release_leaves_every_formula_as_typed(tmp_path: Path) -> None:
"""The script is written for one MathJax release. Under another, which a
site that lists a bundle of its own can load, it typesets nothing and says
why."""

report = _node_report(tmp_path, _node_script(tmp_path), "new", override="version")

assert report["errors"] == [
"autoform: this page loaded MathJax 3.2.1, but javascripts/mathjax.js was written for 3.2.2; formulas "
"are left as typed. Load MathJax only through javascripts/mathjax.js in mkdocs.yml."
]
assert report["passes"] == []


# A MathJax that mkdocs.yml lists before javascripts/mathjax.js: loaded when
# the script runs, and started afterwards with its own configuration. Reports
# how many formulas it typeset. argv: the script, or "none" for a page without
# it, and the page.
_LOADED_FIRST_HARNESS = r"""
"use strict";
const fs = require("fs");
const path = require("path");
const [scriptPath, pagePath] = process.argv.slice(2);
const report = {errors: [], math: null};
global.window = globalThis;
console.error = (...args) => report.errors.push(args.map(String).join(" "));
const main = require(path.join(process.env.AUTOFORM_MATHJAX_DIR, "es5", "node-main.js"));
const loaded = window.MathJax;
global.document = {readyState: "complete", head: {appendChild: () => {}}, createElement: () => ({}),
addEventListener: () => {}};
if (scriptPath !== "none") (0, eval)(fs.readFileSync(scriptPath, "utf8"));
delete global.document;
loaded.config.startup.document = fs.readFileSync(pagePath, "utf8");
loaded.config.loader.load.push("input/tex", "output/chtml");
main.init({}).then(() => {
report.math = Array.from(loaded.startup.document.math).length;
}).catch((error) => {
report.errors.push("harness: " + error.stack);
}).finally(() => {
process.stdout.write(JSON.stringify(report));
});
"""


def test_a_mathjax_listed_before_the_script_typesets_nothing(tmp_path: Path) -> None:
"""A bundle listed in mkdocs.yml before javascripts/mathjax.js starts
without the site's configuration and would typeset every formula on the
page with its own, a card's too. The script stops it instead, and says
why."""

files = {
"harness.js": _LOADED_FIRST_HARNESS,
"mathjax.js": _node_script(tmp_path),
"page.html": '<html><body><p><span class="arithmatex">\\(a \\leq b\\)</span> \\(c\\)</p></body></html>',
}
for name, text in files.items():
(tmp_path / name).write_text(text, encoding="utf-8")

def run(script: str) -> dict:
done = subprocess.run(
[NODE, "harness.js", script, "page.html"],
cwd=tmp_path,
env=dict(os.environ, AUTOFORM_MATHJAX_DIR=str(mathjax_package())),
capture_output=True,
text=True,
timeout=120,
check=False,
)
assert done.returncode == 0, done.stderr
return json.loads(done.stdout)

assert run("mathjax.js") == {
"errors": [
"autoform: MathJax was loaded before javascripts/mathjax.js; formulas are left as typed. Load MathJax "
"only through javascripts/mathjax.js in mkdocs.yml."
],
"math": 0,
}
# The same MathJax without the script typesets both formulas.
assert run("none") == {"errors": [], "math": 2}


def test_no_class_makes_an_input_read_more_of_a_card(tmp_path: Path) -> None:
"""A document made outside MathJax's startup reads an element of class
mathjax_process wherever it is unless told otherwise, inside a card too."""
Expand Down Expand Up @@ -309,10 +399,22 @@ def test_the_explorer_s_titles_and_summaries_are_typeset_as_articles_are(tmp_pat
'<a class="bp-dag-navigation-link" href="#base"><span class="arithmatex">Order of $g^k$</span></a>'
'<button class="bp-dag-sheet-handle arithmatex">Handle $h_1$</button>'
'<button class="bp-dag-relation-button arithmatex">Relation $r^2$</button>'
'<p class="bp-dag-summary arithmatex">By \\ref{thm:a}, which is no formula, $q$ holds.</p>'
"</div></aside></body></html>"
)
# An article formula whose hidden assistive copy holds a pair of dollar
# signs, and a card formula whose copy holds an environment.
first = _FIRST_PAGE.replace(
'<p id="a1">', '<p id="a1"><span class="arithmatex">\\(\\text{costs \\$5 and \\$6}\\)</span> '
).replace(
'<div class="bp-readback-body"><span class="bp-card-formula">\\(e',
'<div class="bp-readback-body"><span class="bp-card-formula">\\(\\text{see \\begin{cases}a\\end{cases}}\\)</span> '
'<span class="bp-card-formula">\\(e',
)

report = _node_report(tmp_path, _node_script(tmp_path, json.dumps(_MACROS)), "new", second=drawn, then="explorer")
report = _node_report(
tmp_path, _node_script(tmp_path, json.dumps(_MACROS)), "new", first=first, second=drawn, then="explorer"
)

assert report["errors"] == []
assert report["early"] == "undefined"
Expand All @@ -325,10 +427,18 @@ def test_the_explorer_s_titles_and_summaries_are_typeset_as_articles_are(tmp_pat
# control inside the control.
page, link = explorer[:2]
assert page["card"] is None and page["packages"] == _PAGE_PACKAGES
assert [math["tex"] for math in page["math"]] == ["\\mathbb{Z}/n", "C_n \\cong \\RR", "\\varphi(n)"]
# A bare \\ref is no formula, and MathJax's output, the assistive copy
# included, is not read again.
assert [math["tex"] for math in page["math"]] == ["\\mathbb{Z}/n", "C_n \\cong \\RR", "\\varphi(n)", "q"]
assert "merror" not in page["math"][1]["mml"] and 'mathcolor="red"' not in page["math"][1]["mml"]
assert link["card"] is None and link["packages"] == _PAGE_PACKAGES
assert sorted(math["tex"] for math in link["math"]) == ["g^k", "h_1", "r^2", "t_0"]
# The control's document has neither MathJax's menu nor its accessibility explorer.
skip = {"[+]": ["mjx-container"]}
assert page["requested"] == {"skipHtmlTags": skip}
assert link["requested"] == {"enableMenu": False, "renderActions": {"explorable": []}, "skipHtmlTags": skip}
# The cards were typeset by the first pass, their hidden copies included.
assert [document["card"] for document in explorer[2:]] == ["c1", "c2"]
assert all(document["math"] == [] for document in explorer[2:])
# MathJax's own document, which a call to MathJax.typesetPromise would use, holds nothing.
assert report["startup"] == 0
Expand Down
Loading