diff --git a/src/viewer/navigation/footnote-index.js b/src/viewer/navigation/footnote-index.js index 4df33a0..37b4398 100644 --- a/src/viewer/navigation/footnote-index.js +++ b/src/viewer/navigation/footnote-index.js @@ -334,7 +334,7 @@ export function footnotesForPage(items, viewport, pageNumber, rules = [], refere if (edgePageNumber || entry.x > column.left + (column.right - column.left) * 0.45) continue; const inlineText = entry.text.slice(match[0].length).trim(); const adjacent = entries.filter((other) => other !== entry && other.x > entry.x && - other.y >= entry.y - 2 && other.y - entry.y <= Math.max(3, other.height * 0.65) && other.text.length >= 3 && /[A-Za-z\u00c0-\u02af]/u.test(other.text)) + other.y >= entry.y - 2 && other.y - entry.y <= Math.max(3, other.height * 0.65) && other.text.length >= 3 && /[A-Za-z\u00c0-\u02af0-9]/u.test(other.text)) .sort((a, b) => a.x - b.x)[0]; if (!inlineText && !adjacent) continue; const baseline = inlineText ? entry.y : adjacent.y; @@ -442,7 +442,12 @@ export function footnotesForPage(items, viewport, pageNumber, rules = [], refere const textEntries = entries.filter((entry) => (note.readingY === undefined ? entry.y : readingBaseline(entry)) >= (note.readingY ?? note.baseline) - 2 && (note.hangingMargin === undefined || entry.x >= note.hangingMargin - 3) && (!resumedBody || entry.y < resumedBody.y - 2) && - (!next || (next.readingY === undefined ? entry.y : readingBaseline(entry)) < (next.readingY ?? next.yRatio * viewport.height) - 2) && isPreviewText(entry, note.textHeight) && + (!next || (next.readingY === undefined ? entry.y : readingBaseline(entry)) < (next.readingY ?? next.yRatio * viewport.height) - 2) && + (isPreviewText(entry, note.textHeight) || + // A calculation-only footnote may contain smaller math operator glyphs. + (/^[×xX÷=+−*/-]+$/.test(entry.text) && entries.some((other) => + other !== entry && Math.abs(readingBaseline(other) - readingBaseline(entry)) <= 2 && + isPreviewText(other, note.textHeight)))) && !(/^\d+$/.test(entry.text) && entry.yRatio > 0.92 && !(note.scannedSmallBlock && entries.some(other => other !== entry && /[A-Za-z]/.test(other.text) && Math.abs(readingBaseline(other) - readingBaseline(entry)) < 2)))).sort(readingOrder); diff --git a/tests/footnote-click-copy.test.mjs b/tests/footnote-click-copy.test.mjs index 7d9c8a9..8551842 100644 --- a/tests/footnote-click-copy.test.mjs +++ b/tests/footnote-click-copy.test.mjs @@ -3,6 +3,7 @@ import { readFileSync } from "node:fs"; import test from "node:test"; import vm from "node:vm"; import { footnoteToMarkdown } from "../src/viewer/selection/markdown-copy.js"; +import { footnotesForPage } from "../src/viewer/navigation/footnote-index.js"; const source = readFileSync( new URL("../src/viewer/navigation/footnote-jump.js", import.meta.url), "utf8", @@ -105,3 +106,36 @@ test("clipboard failures do not block footnote navigation", async () => { assert.ok(viewer.events.some(event => event.type === "pdf-viewer-toast" && event.detail.message === "Could not copy footnote 12")); }); + +test("SEC November 2007 TIC footnotes separate arithmetic-only note 13 from note 12", () => { + // Reproduces the geometry of 2007-11.pdf page 173: the note 13 body + // begins with a number, and the multiplication sign uses a smaller glyph. + const height = 789; + const viewport = { width: 612, height, convertToViewportPoint: (x, y) => [x, height - y] }; + const item = (str, x, y, size, width) => ({ + str, width, height: size, transform: [1, 0, 0, size, x, height - y], + }); + const items = [ + item("The reporting burden is estimated", 75, 184, 12, 220), + item("12", 298, 177, 6, 8), + item("from industry discussions", 308, 184, 12, 190), + item("The participants would make disclosures", 75, 270, 12, 275), + item("13", 353, 263, 6, 8), + item("per year.", 362, 270, 12, 80), + item("12", 74, 487, 8, 10), + item("Based on discussions with industry representatives", 110, 490, 12, 320), + item("agreement six times a year, and others would do so seven times a year.", 74, 674, 12, 445), + item("13", 74, 692, 8, 10), + item("6.63", 110, 695, 12, 28), + item("X", 142, 695, 8, 8), + item("800 = 5,304.", 153, 695, 12, 78), + item("13", 303, 747, 12, 13), // printed page number, not a note + ]; + const notes = footnotesForPage(items, viewport, 173, [{ x: 74, y: 367, width: 300 }]); + assert.deepEqual(notes.map(({ number }) => number), [12, 13]); + assert.match(notes[0].text, /others would do so seven times a year\\.$/); + assert.doesNotMatch(notes[0].text, /6\\.63|5,304/); + assert.equal(notes[1].text, "6.63 X 800 = 5,304."); + assert.ok(notes[0].highlightRegions.every(region => region.top + region.height < 687 / height)); + assert.ok(notes[1].highlightRegions.every(region => region.top > 680 / height)); +});