Skip to content
Merged
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
112 changes: 111 additions & 1 deletion docs/web/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,116 @@ const cookieConsentHead = [
},
];

/** @typedef {{ type?: string, tagName?: string, properties?: Record<string, unknown>, children?: HastNode[] }} HastNode */

/**
* Adds accessible semantics to the SVG output generated by MathJax.
*
* MathJax's SVG output is visually understandable but otherwise exposes its
* drawing primitives to assistive technology. The wrapper and SVG get an
* accessible math role and label, while the SVG internals are hidden so the
* equation is announced once rather than as a collection of vector nodes.
*
* @returns {(tree: HastNode) => void}
*/
function rehypeLabelMathJaxSvg() {
return (tree) => {
/**
* Selects the most useful available label for a rendered equation.
*
* @param {HastNode | undefined} container
* @returns {string}
*/
const getMathLabelFromContainer = (container) => {
const properties = container?.properties ?? {};
const candidates = [
properties["data-tex"],
properties["data-latex"],
properties["aria-label"],
];

for (const candidate of candidates) {
if (typeof candidate === "string" && candidate.trim().length > 0) {
return candidate.trim();
}
}

return "Mathematical expression";
};

/**
* Hides visual-only SVG nodes from assistive technology.
*
* @param {HastNode | undefined} node
* @returns {void}
*/
const hideDecorativeSvgNodes = (node) => {
if (!node || typeof node !== "object") {
return;
}

if (node.type === "element") {
const decorativeTags = new Set(["defs", "path", "use", "g", "rect"]);
if (typeof node.tagName === "string" && decorativeTags.has(node.tagName)) {
const properties = node.properties ?? {};
properties["aria-hidden"] = "true";
node.properties = properties;
}
}

if (Array.isArray(node.children)) {
for (const child of node.children) {
hideDecorativeSvgNodes(child);
}
}
};

/**
* Applies accessible semantics to MathJax containers and their SVGs.
*
* @param {HastNode | undefined} node
* @returns {void}
*/
const visit = (node) => {
if (!node || typeof node !== "object") {
return;
}

if (node.type === "element" && node.tagName === "mjx-container") {
const containerProperties = node.properties ?? {};
containerProperties.role = "math";
node.properties = containerProperties;

const mathLabel = getMathLabelFromContainer(node);
const children = Array.isArray(node.children) ? node.children : [];

for (const child of children) {
if (child?.type === "element" && child.tagName === "svg") {
const svgProperties = child.properties ?? {};
svgProperties.role = "math";
svgProperties.focusable = "false";

if (!svgProperties["aria-label"] && !svgProperties["aria-labelledby"]) {
svgProperties["aria-label"] = mathLabel;
}

child.properties = svgProperties;
hideDecorativeSvgNodes(child);
}
}
}

if (Array.isArray(node.children)) {
for (const child of node.children) {
visit(child);
}
}
};

visit(tree);
};
}

// https://astro.build/config
export default defineConfig({
site: "https://docs.lowcarboncontracts.uk",
Expand Down Expand Up @@ -105,7 +215,7 @@ export default defineConfig({
},
markdown: {
remarkPlugins: [remarkMath],
rehypePlugins: [rehypeMathjax],
rehypePlugins: [rehypeMathjax, rehypeLabelMathJaxSvg],
},
integrations: [
starlight({
Expand Down
Loading