mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-06 15:15:47 +02:00
Don't walk the children of a node having some attached MathML (bug 2025674)
And now Chrome has a MathML Sanitizer implementation (146), the tests related to MathML are updated to reflect that.
This commit is contained in:
parent
777251da85
commit
eff11e88bb
@ -320,37 +320,22 @@ describe("accessibility", () => {
|
|||||||
it("must check that the MathML is correctly inserted", async () => {
|
it("must check that the MathML is correctly inserted", async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
const isSanitizerSupported = await page.evaluate(() => {
|
const mathML = await page.$eval(
|
||||||
try {
|
"span.structTree span[aria-owns='p58R_mc13'] > math",
|
||||||
// eslint-disable-next-line no-undef
|
el => el?.innerHTML ?? ""
|
||||||
return typeof Sanitizer !== "undefined";
|
);
|
||||||
} catch {
|
expect(mathML)
|
||||||
return false;
|
.withContext(`In ${browserName}`)
|
||||||
}
|
.toEqual(
|
||||||
});
|
` <msqrt><msup><mi>x</mi><mn>2</mn></msup></msqrt> <mo>=</mo> <mrow intent="absolute-value($x)"><mo>|</mo><mi arg="x">x</mi><mo>|</mo></mrow> `
|
||||||
if (isSanitizerSupported) {
|
|
||||||
const mathML = await page.$eval(
|
|
||||||
"span.structTree span[aria-owns='p58R_mc13'] > math",
|
|
||||||
el => el?.innerHTML ?? ""
|
|
||||||
);
|
);
|
||||||
expect(mathML)
|
|
||||||
.withContext(`In ${browserName}`)
|
|
||||||
.toEqual(
|
|
||||||
` <msqrt><msup><mi>x</mi><mn>2</mn></msup></msqrt> <mo>=</mo> <mrow intent="absolute-value($x)"><mo>|</mo><mi arg="x">x</mi><mo>|</mo></mrow> `
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check that the math corresponding element is hidden in the text
|
// Check that the math corresponding element is hidden in the text
|
||||||
// layer.
|
// layer.
|
||||||
const ariaHidden = await page.$eval("span#p58R_mc13", el =>
|
const ariaHidden = await page.$eval("span#p58R_mc13", el =>
|
||||||
el.getAttribute("aria-hidden")
|
el.getAttribute("aria-hidden")
|
||||||
);
|
);
|
||||||
expect(ariaHidden).withContext(`In ${browserName}`).toEqual("true");
|
expect(ariaHidden).withContext(`In ${browserName}`).toEqual("true");
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(
|
|
||||||
`Pending in Chrome: Sanitizer API (in ${browserName}) is not supported`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -370,30 +355,15 @@ describe("accessibility", () => {
|
|||||||
it("must check that the MathML is correctly inserted", async () => {
|
it("must check that the MathML is correctly inserted", async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
const isSanitizerSupported = await page.evaluate(() => {
|
const mathML = await page.$eval(
|
||||||
try {
|
"span.structTree span[aria-owns='p21R_mc64']",
|
||||||
// eslint-disable-next-line no-undef
|
el => el?.innerHTML ?? ""
|
||||||
return typeof Sanitizer !== "undefined";
|
);
|
||||||
} catch {
|
expect(mathML)
|
||||||
return false;
|
.withContext(`In ${browserName}`)
|
||||||
}
|
.toEqual(
|
||||||
});
|
'<math display="block"> <msup> <mi>𝑛</mi> <mi>𝑝</mi> </msup> <mo lspace="0.278em" rspace="0.278em">=</mo> <mi>𝑛</mi> <mspace width="1.000em"></mspace> <mi> mod </mi> <mspace width="0.167em"></mspace> <mspace width="0.167em"></mspace> <mi>𝑝</mi> </math>'
|
||||||
if (isSanitizerSupported) {
|
|
||||||
const mathML = await page.$eval(
|
|
||||||
"span.structTree span[aria-owns='p21R_mc64']",
|
|
||||||
el => el?.innerHTML ?? ""
|
|
||||||
);
|
);
|
||||||
expect(mathML)
|
|
||||||
.withContext(`In ${browserName}`)
|
|
||||||
.toEqual(
|
|
||||||
'<math display="block"> <msup> <mi>𝑛</mi> <mi>𝑝</mi> </msup> <mo lspace="0.278em" rspace="0.278em">=</mo> <mi>𝑛</mi> <mspace width="1.000em"></mspace> <mi> mod </mi> <mspace width="0.167em"></mspace> <mspace width="0.167em"></mspace> <mi>𝑝</mi> </math>'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(
|
|
||||||
`Pending in Chrome: Sanitizer API (in ${browserName}) is not supported`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -475,25 +445,11 @@ describe("accessibility", () => {
|
|||||||
it("must check that there's no alt-text on the MathML node", async () => {
|
it("must check that there's no alt-text on the MathML node", async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
const isSanitizerSupported = await page.evaluate(() => {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
return typeof Sanitizer !== "undefined";
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const ariaLabel = await page.$eval(
|
const ariaLabel = await page.$eval(
|
||||||
"span[aria-owns='p3R_mc2']",
|
"span[aria-owns='p3R_mc2']",
|
||||||
el => el.getAttribute("aria-label") || ""
|
el => el.getAttribute("aria-label") || ""
|
||||||
);
|
);
|
||||||
if (isSanitizerSupported) {
|
expect(ariaLabel).withContext(`In ${browserName}`).toEqual("");
|
||||||
expect(ariaLabel).withContext(`In ${browserName}`).toEqual("");
|
|
||||||
} else {
|
|
||||||
expect(ariaLabel)
|
|
||||||
.withContext(`In ${browserName}`)
|
|
||||||
.toEqual("cube root of , x plus y end cube root ");
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -513,14 +469,6 @@ describe("accessibility", () => {
|
|||||||
it("must check that the text in text layer is aria-hidden", async () => {
|
it("must check that the text in text layer is aria-hidden", async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
const isSanitizerSupported = await page.evaluate(() => {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
return typeof Sanitizer !== "undefined";
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const ariaHidden = await page.evaluate(() =>
|
const ariaHidden = await page.evaluate(() =>
|
||||||
Array.from(
|
Array.from(
|
||||||
document.querySelectorAll(".structTree :has(> math)")
|
document.querySelectorAll(".structTree :has(> math)")
|
||||||
@ -530,16 +478,64 @@ describe("accessibility", () => {
|
|||||||
.getAttribute("aria-hidden")
|
.getAttribute("aria-hidden")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (isSanitizerSupported) {
|
expect(ariaHidden)
|
||||||
expect(ariaHidden)
|
.withContext(`In ${browserName}`)
|
||||||
.withContext(`In ${browserName}`)
|
.toEqual(["true", "true", "true"]);
|
||||||
.toEqual(["true", "true", "true"]);
|
})
|
||||||
} else {
|
);
|
||||||
// eslint-disable-next-line no-console
|
});
|
||||||
console.log(
|
});
|
||||||
`Pending in Chrome: Sanitizer API (in ${browserName}) is not supported`
|
|
||||||
|
describe("MathML in AF entry with struct tree children must not be duplicated", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pages = await loadAndWait("bug2025674.pdf", ".textLayer");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("must check that the MathML is not duplicated in the struct tree", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
// The Formula node has both AF MathML and struct tree children.
|
||||||
|
// When AF MathML is present, children must not be walked to avoid
|
||||||
|
// rendering the math content twice in the accessibility tree.
|
||||||
|
const mathCount = await page.evaluate(
|
||||||
|
() => document.querySelectorAll(".structTree math").length
|
||||||
|
);
|
||||||
|
expect(mathCount).withContext(`In ${browserName}`).toBe(1);
|
||||||
|
|
||||||
|
// All text layer elements referenced by the formula subtree must
|
||||||
|
// be aria-hidden so screen readers don't read both the MathML and
|
||||||
|
// the underlying text content.
|
||||||
|
const allHidden = await page.evaluate(() => {
|
||||||
|
const ids = [];
|
||||||
|
for (const el of document.querySelectorAll(
|
||||||
|
".structTree [aria-owns]"
|
||||||
|
)) {
|
||||||
|
if (el.closest("math")) {
|
||||||
|
ids.push(el.getAttribute("aria-owns"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also collect ids from the formula span itself.
|
||||||
|
for (const el of document.querySelectorAll(
|
||||||
|
".structTree span:has(> math)"
|
||||||
|
)) {
|
||||||
|
const owned = el.getAttribute("aria-owns");
|
||||||
|
if (owned) {
|
||||||
|
ids.push(owned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids.every(
|
||||||
|
id =>
|
||||||
|
document.getElementById(id)?.getAttribute("aria-hidden") ===
|
||||||
|
"true"
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
expect(allHidden).withContext(`In ${browserName}`).toBeTrue();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -892,3 +892,4 @@
|
|||||||
!issue20930.pdf
|
!issue20930.pdf
|
||||||
!text_rise_eol_bug.pdf
|
!text_rise_eol_bug.pdf
|
||||||
!hello_world_rotated.pdf
|
!hello_world_rotated.pdf
|
||||||
|
!bug2025674.pdf
|
||||||
|
|||||||
BIN
test/pdfs/bug2025674.pdf
Normal file
BIN
test/pdfs/bug2025674.pdf
Normal file
Binary file not shown.
@ -350,12 +350,25 @@ class StructTreeLayerBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#collectIds(node, ids) {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("id" in node) {
|
||||||
|
ids.push(node.id);
|
||||||
|
}
|
||||||
|
for (const kid of node.children || []) {
|
||||||
|
this.#collectIds(kid, ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#walk(node, parentNodes = []) {
|
#walk(node, parentNodes = []) {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let element;
|
let element;
|
||||||
|
let visitChildren = true;
|
||||||
if ("role" in node) {
|
if ("role" in node) {
|
||||||
const { role } = node;
|
const { role } = node;
|
||||||
if (MathMLElements.has(role)) {
|
if (MathMLElements.has(role)) {
|
||||||
@ -389,18 +402,14 @@ class StructTreeLayerBuilder {
|
|||||||
}
|
}
|
||||||
if (role === "Formula") {
|
if (role === "Formula") {
|
||||||
if (node.mathML && MathMLSanitizer.sanitizer) {
|
if (node.mathML && MathMLSanitizer.sanitizer) {
|
||||||
|
visitChildren = false;
|
||||||
element.setHTML(node.mathML, {
|
element.setHTML(node.mathML, {
|
||||||
sanitizer: MathMLSanitizer.sanitizer,
|
sanitizer: MathMLSanitizer.sanitizer,
|
||||||
});
|
});
|
||||||
// Hide all the corresponding content elements in the text layer in
|
// Hide all the corresponding content elements in the text layer in
|
||||||
// order to avoid screen readers reading both the MathML and the
|
// order to avoid screen readers reading both the MathML and the
|
||||||
// text content.
|
// text content.
|
||||||
for (const { id } of node.children || []) {
|
this.#collectIds(node, (this.#elementsToHideInTextLayer ||= []));
|
||||||
if (!id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
(this.#elementsToHideInTextLayer ||= []).push(id);
|
|
||||||
}
|
|
||||||
// For now, we don't want to keep the alt text if there's valid
|
// For now, we don't want to keep the alt text if there's valid
|
||||||
// MathML (see https://github.com/w3c/mathml-aam/issues/37).
|
// MathML (see https://github.com/w3c/mathml-aam/issues/37).
|
||||||
// TODO: Revisit this decision in the future.
|
// TODO: Revisit this decision in the future.
|
||||||
@ -426,7 +435,7 @@ class StructTreeLayerBuilder {
|
|||||||
// Often there is only one content node so just set the values on the
|
// Often there is only one content node so just set the values on the
|
||||||
// parent node to avoid creating an extra span.
|
// parent node to avoid creating an extra span.
|
||||||
this.#setAttributes(node.children[0], element);
|
this.#setAttributes(node.children[0], element);
|
||||||
} else {
|
} else if (visitChildren) {
|
||||||
parentNodes.push(node);
|
parentNodes.push(node);
|
||||||
for (const kid of node.children) {
|
for (const kid of node.children) {
|
||||||
element.append(this.#walk(kid, parentNodes));
|
element.append(this.#walk(kid, parentNodes));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user