diff --git a/packages/pdf/src/pdf.ts b/packages/pdf/src/pdf.ts index b20737e..3fd9c48 100644 --- a/packages/pdf/src/pdf.ts +++ b/packages/pdf/src/pdf.ts @@ -143,7 +143,9 @@ async function convertUniqueImages( function finishPdf(extracted: ExtractedPdf, imageBlocks: MarkdownBlock[]): string { const markdown = itemsToMarkdown( - mergeNumericFragments(mergeScriptItems(dedupeOverlappingItems(extracted.items))), + mergeNumericFragments( + mergeStackedMath(mergeScriptItems(dedupeOverlappingItems(extracted.items))), + ), extracted.strokeLines, extracted.pageRects, imageBlocks, @@ -2939,6 +2941,105 @@ function dedupeOverlappingItems(items: TextItem[]): TextItem[] { const SUP = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹']; const SUB = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉']; +const SUP_CHARS: Record = { + '0': '⁰', + '1': '¹', + '2': '²', + '3': '³', + '4': '⁴', + '5': '⁵', + '6': '⁶', + '7': '⁷', + '8': '⁸', + '9': '⁹', + '+': '⁺', + '-': '⁻', + '−': '⁻', + '=': '⁼', + '(': '⁽', + ')': '⁾', + a: 'ᵃ', + b: 'ᵇ', + c: 'ᶜ', + d: 'ᵈ', + e: 'ᵉ', + f: 'ᶠ', + g: 'ᵍ', + h: 'ʰ', + i: 'ⁱ', + j: 'ʲ', + k: 'ᵏ', + l: 'ˡ', + m: 'ᵐ', + n: 'ⁿ', + o: 'ᵒ', + p: 'ᵖ', + r: 'ʳ', + s: 'ˢ', + t: 'ᵗ', + u: 'ᵘ', + v: 'ᵛ', + w: 'ʷ', + x: 'ˣ', + y: 'ʸ', + z: 'ᶻ', + A: 'ᴬ', + B: 'ᴮ', + D: 'ᴰ', + E: 'ᴱ', + G: 'ᴳ', + H: 'ᴴ', + I: 'ᴵ', + J: 'ᴶ', + K: 'ᴷ', + L: 'ᴸ', + M: 'ᴹ', + N: 'ᴺ', + O: 'ᴼ', + P: 'ᴾ', + R: 'ᴿ', + T: 'ᵀ', + U: 'ᵁ', + V: 'ⱽ', + W: 'ᵂ', +}; + +const SUB_CHARS: Record = { + '0': '₀', + '1': '₁', + '2': '₂', + '3': '₃', + '4': '₄', + '5': '₅', + '6': '₆', + '7': '₇', + '8': '₈', + '9': '₉', + '+': '₊', + '-': '₋', + '−': '₋', + '=': '₌', + '(': '₍', + ')': '₎', + a: 'ₐ', + e: 'ₑ', + h: 'ₕ', + i: 'ᵢ', + j: 'ⱼ', + k: 'ₖ', + l: 'ₗ', + m: 'ₘ', + n: 'ₙ', + o: 'ₒ', + p: 'ₚ', + r: 'ᵣ', + s: 'ₛ', + t: 'ₜ', + u: 'ᵤ', + v: 'ᵥ', + x: 'ₓ', +}; + function scriptDigits(text: string): string | undefined { const t = text.trim(); if (t.length === 0 || t.length > 4) return undefined; @@ -2955,18 +3056,47 @@ function scriptDigits(text: string): string | undefined { return out; } +function isScriptCandidate(text: string): boolean { + const t = text.trim(); + if (t.length === 0 || t.length > 4) return false; + if (scriptDigits(t) !== undefined) return true; + if (/^[+\-−+=()]$/.test(t)) return true; + return /^[\p{L}]$/u.test(t); +} + +function toScript(text: string, sub: boolean): string { + const map = sub ? SUB_CHARS : SUP_CHARS; + const digits = scriptDigits(text); + let out = ''; + for (const c of digits ?? text.trim()) out += map[c] ?? c; + return out; +} + function canAttachScript(parent: TextItem, item: TextItem): boolean { if (parent.page !== item.page) return false; if (item.fontSize <= 0) return false; - if (scriptDigits(item.text) === undefined) return false; - if (parent.fontSize < item.fontSize * 1.15) return false; + if (!isScriptCandidate(item.text)) return false; + const t = item.text.trim(); + const digits = scriptDigits(t) !== undefined; const last = [...parent.text.trimEnd()].at(-1); if (last === undefined) return false; - if (!/[\p{L}\p{N}\p{P}]/u.test(last)) return false; + const ratio = item.fontSize > 0 ? parent.fontSize / item.fontSize : 1; + if (ratio < 1.2) return false; + if (!digits && ratio > 1.9) return false; + const scripted = + Object.values(SUP_CHARS).includes(last) || + Object.values(SUB_CHARS).includes(last) || + SUP.includes(last) || + SUB.includes(last); + if (!scripted && !/[\p{L}\p{N}\p{P}]/u.test(last)) return false; const fs = Math.max(parent.fontSize, 8); const gap = item.x - (parent.x + parent.width); - if (gap >= fs * 0.35 || gap <= -fs * 0.5) return false; - if (Math.abs(item.y - parent.y) > Math.max(fs * 0.8, 8)) return false; + const gapMax = digits ? fs * 0.35 : fs * 0.4; + if (gap >= gapMax || gap <= -fs * 0.5) return false; + const yOff = Math.abs(item.y - parent.y); + const yMax = digits ? Math.max(fs * 0.8, 8) : fs * 0.55; + if (yOff > yMax) return false; + if (yOff < fs * 0.12 && !digits) return false; return true; } @@ -3019,16 +3149,16 @@ function mergeScriptItems(items: TextItem[]): TextItem[] { const merged: TextItem[] = []; for (const item of pageItems) { let attached = false; - if (scriptDigits(item.text) !== undefined) { + if (isScriptCandidate(item.text)) { for (let i = merged.length - 1; i >= 0; i -= 1) { const parent = merged[i]!; if (parent.x > item.x) continue; if (!canAttachScript(parent, item)) continue; - const digits = scriptDigits(item.text)!; const lowered = item.y < parent.y - parent.fontSize * 0.12; - const table = lowered ? SUB : SUP; - parent.text += [...digits].map((c) => table[Number(c)]!).join(''); + parent.text += toScript(item.text, lowered); parent.width = Math.max(parent.width, item.x + item.width - parent.x); + const span = Math.abs(item.y - parent.y) + item.height; + parent.height = Math.max(parent.height, item.height, span); attached = true; break; } @@ -3040,6 +3170,73 @@ function mergeScriptItems(items: TextItem[]): TextItem[] { return result; } +function isFractionNumerator(text: string): boolean { + const t = text.trim(); + if (t.length === 0 || t.length > 6) return false; + if (/[∂∑∫√∞]/.test(t)) return true; + return t === 'd'; +} + +function isFractionDenominator(text: string): boolean { + const t = text.trim(); + if (t.length === 0 || t.length > 6) return false; + if (/[∂∑∫√∞]/.test(t)) return true; + return /^(?:dt|dx|dy|dq)$/.test(t); +} + +function mergeStackedMath(items: TextItem[]): TextItem[] { + if (items.length < 2) return items; + const sorted = items.slice().sort((a, b) => a.page - b.page || b.y - a.y || a.x - b.x); + const used = new Set(); + const out: TextItem[] = []; + for (let i = 0; i < sorted.length; i += 1) { + if (used.has(i)) continue; + const top = sorted[i]!; + if (!isFractionNumerator(top.text)) { + out.push(top); + continue; + } + const fsTop = Math.max(top.fontSize, 8); + let best = -1; + let bestScore = 0; + for (let j = i + 1; j < sorted.length; j += 1) { + if (used.has(j)) continue; + const bot = sorted[j]!; + if (bot.page !== top.page) break; + if (!isFractionDenominator(bot.text)) continue; + const gap = top.y - bot.y; + const fs = Math.max(fsTop, bot.fontSize, 8); + if (gap < fs * 0.45 || gap > fs * 1.7) continue; + const overlap = + Math.min(top.x + Math.max(top.width, 1), bot.x + Math.max(bot.width, 1)) - + Math.max(top.x, bot.x); + const minW = Math.min(Math.max(top.width, 4), Math.max(bot.width, 4)); + if (overlap < minW * 0.35) continue; + const score = overlap / Math.max(gap, 1); + if (score > bestScore) { + bestScore = score; + best = j; + } + } + if (best < 0) { + out.push(top); + continue; + } + const bot = sorted[best]!; + used.add(best); + const x = Math.min(top.x, bot.x); + out.push({ + ...top, + text: `${top.text.trim()}/${bot.text.trim()}`, + x, + y: (top.y + bot.y) / 2, + width: Math.max(top.x + Math.max(top.width, 0), bot.x + Math.max(bot.width, 0)) - x, + height: Math.max(top.height, top.y - bot.y + bot.height), + }); + } + return out; +} + // --------------------------------------------------------------------------- // Line grouping + markdown // --------------------------------------------------------------------------- @@ -3133,6 +3330,26 @@ function formatLine(items: TextItem[], bold: boolean, italic: boolean, deco: boo return result; } +function isEquationNumberLine(text: string): boolean { + return /^\(?\d+(?:\.\d+)*[a-z]?\)?$/.test(text.trim()); +} + +function isMathHeading(text: string): boolean { + const t = text.trim(); + if (isEquationNumberLine(t)) return true; + const ops = t.match(/[=≠≈≤≥±∂∑∫√∞×·−]/g)?.length ?? 0; + const words = t.split(/\s+/).filter((w) => /\p{L}{3,}/u.test(w)).length; + return ops >= 1 && words <= 1; +} + +function isFalseIsolatedHeading(text: string): boolean { + const t = text.trim(); + if (/^[\p{Ll}]/u.test(t)) return true; + if (/[:,]$/.test(t)) return true; + if (/\band$/i.test(t)) return true; + return isMathHeading(t); +} + function isListItem(text: string): boolean { const t = text.trimStart(); if ( @@ -3321,6 +3538,7 @@ function itemsToMarkdown( const wc = plain.split(/\s+/).filter(Boolean).length; if (wc < 1 || wc > 6 || plain.length <= 3) continue; if (line[0]!.fontSize < base * 0.95) continue; + if (isFalseIsolatedHeading(plain)) continue; const prev = lines[i - 1]; const next = lines[i + 1]; const before = !prev || prev[0]!.page !== line[0]!.page || prev[0]!.y - line[0]!.y > paraTh; @@ -3330,6 +3548,7 @@ function itemsToMarkdown( const headerLevel = (i: number, line: TextItem[], plain: string): number | undefined => { if (plain.length <= 3 || plain.split(/\s+/).filter(Boolean).length > 15) return undefined; + if (isMathHeading(plain) || /^[\p{Ll}]/u.test(plain)) return undefined; const fs = line[0]!.fontSize; const ratio = fs / base; if (ratio >= 1.2) { diff --git a/packages/pdf/src/tables.ts b/packages/pdf/src/tables.ts index 1ca3d6d..ce2aa89 100644 --- a/packages/pdf/src/tables.ts +++ b/packages/pdf/src/tables.ts @@ -688,7 +688,8 @@ function acceptBorderless( looksLikeChartValues(grid, run) || looksLikeNumericScatter(grid) || looksLikeLabeledTicks(grid) || - looksLikeHeaderlessJunk(grid) + looksLikeHeaderlessJunk(grid) || + looksLikeMathLayout(grid) ) { return false; } @@ -778,6 +779,24 @@ function looksLikeLabeledTicks(grid: string[][]): boolean { return percentAt >= 0 && labelAt >= 0 && percentAt < labelAt; } +function looksLikeMathLayout(grid: string[][]): boolean { + const cells = grid.flat().filter((c) => c.length > 0); + if (cells.length < 3) return false; + const eqNums = cells.filter((c) => /\(\d+(?:\.\d+)*[a-z]?\)/.test(c)).length; + const ops = cells.filter((c) => /[=≠≈≤≥∂∑∫√∞×·]/.test(c)).length; + const mathGlyphs = cells.filter((c) => /[∂∑∫√∞]/.test(c)).length; + const short = cells.filter((c) => [...c].length <= 10).length; + const tiny = cells.filter((c) => [...c].length <= 3).length; + const prose = cells.filter( + (c) => (wordCount(c) >= 3 || /\p{L}{5,}/u.test(c)) && !/[=∂∑∫]/.test(c), + ).length; + if (prose >= 2) return false; + if (eqNums >= 1 && (ops >= 1 || cells.some((c) => /[±−]/.test(c)))) return true; + if (mathGlyphs >= 2) return true; + if (ops >= 2 && short / cells.length >= 0.65 && grid.length >= 3) return true; + return ops >= 1 && tiny >= 4 && short / cells.length >= 0.65; +} + function looksLikeHeaderlessJunk(grid: string[][]): boolean { if (grid.length !== 2 || grid[0]!.length < 3) return false; const r0 = grid[0]!.filter((c) => c.length > 0); diff --git a/packages/pdf/test/math.test.ts b/packages/pdf/test/math.test.ts new file mode 100644 index 0000000..073fb94 --- /dev/null +++ b/packages/pdf/test/math.test.ts @@ -0,0 +1,180 @@ +import { describe, expect, it } from 'vitest'; +import { toMarkdownFromPdf } from '../src/pdf.js'; + +function pagePdf(content: string, mediaBox = [0, 0, 300, 200], encoding = ''): Uint8Array { + const objects = [ + '1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n', + '2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n', + `3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [${mediaBox.join(' ')}] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\nendobj\n`, + `4 0 obj\n<< /Length ${content.length} >>\nstream\n${content}endstream\nendobj\n`, + `5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica${encoding} >>\nendobj\n`, + ]; + let body = '%PDF-1.4\n'; + const offsets = [0]; + for (const obj of objects) { + offsets.push(body.length); + body += obj; + } + const xrefAt = body.length; + let xref = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; + for (let i = 1; i <= objects.length; i += 1) { + xref += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`; + } + body += `${xref}trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefAt}\n%%EOF\n`; + return new TextEncoder().encode(body); +} + +describe('PDF math layout', () => { + it('keeps a stacked fraction as text instead of a table', () => { + const md = toMarkdownFromPdf( + pagePdf( + `BT +/F1 12 Tf +1 0 0 1 20 180 Tm (A mechanical system is described by a Lagrangian.) Tj +1 0 0 1 20 160 Tm (Traditionally the Lagrange equations are written) Tj +/F1 10 Tf +1 0 0 1 40 118 Tm (d) Tj +1 0 0 1 58 118 Tm (L) Tj +1 0 0 1 90 118 Tm (L) Tj +1 0 0 1 74 110 Tm (-) Tj +1 0 0 1 108 108 Tm (=) Tj +1 0 0 1 120 108 Tm (0.) Tj +1 0 0 1 38 104 Tm (dt) Tj +1 0 0 1 56 104 Tm (q) Tj +1 0 0 1 88 104 Tm (q) Tj +/F1 12 Tf +1 0 0 1 20 70 Tm (What could this expression possibly mean?) Tj +1 0 0 1 20 50 Tm (Let us try to write a program that implements them.) Tj +ET +`, + [0, 0, 300, 200], + ), + ); + expect(md).not.toMatch(/\|---\|/); + expect(md.replace(/\s+/g, '')).toContain('d/dt'); + expect(md).toContain('What could this expression possibly mean?'); + }); + + it('attaches a letter exponent to its base', () => { + const md = toMarkdownFromPdf( + pagePdf( + `BT +/F1 12 Tf +1 0 0 1 40 80 Tm (h) Tj +/F1 8 Tf +1 0 0 1 48 86 Tm (p) Tj +/F1 12 Tf +1 0 0 1 56 80 Tm (+) Tj +1 0 0 1 68 80 Tm (O) Tj +ET +`, + ), + ); + expect(md).toContain('hᵖ'); + expect(md).not.toMatch(/\|---\|/); + }); + + it('remaps a superior digit to subscript when the run sits below the baseline', () => { + const md = toMarkdownFromPdf( + pagePdf( + `BT +/F1 12 Tf +1 0 0 1 40 80 Tm (H) Tj +/F1 8 Tf +1 0 0 1 48 74 Tm (\\262) Tj +ET +`, + [0, 0, 300, 200], + ' /Encoding /WinAnsiEncoding', + ), + ); + expect(md).toContain('H₂'); + expect(md).not.toContain('H²'); + }); + + it('attaches p+1 as a superscript run', () => { + const md = toMarkdownFromPdf( + pagePdf( + `BT +/F1 12 Tf +1 0 0 1 40 80 Tm (h) Tj +/F1 8 Tf +1 0 0 1 48 86 Tm (p) Tj +1 0 0 1 54 86 Tm (+) Tj +1 0 0 1 60 86 Tm (1) Tj +ET +`, + ), + ); + expect(md.replace(/\s+/g, '')).toContain('hᵖ⁺¹'); + }); + + it('does not emit an equation number as a heading', () => { + const md = toMarkdownFromPdf( + pagePdf( + `BT +/F1 12 Tf +1 0 0 1 20 160 Tm (As an example the forward-difference method is considered.) Tj +1 0 0 1 40 110 Tm (f) Tj +1 0 0 1 48 110 Tm (x) Tj +1 0 0 1 90 80 Tm (\\(3.18\\)) Tj +1 0 0 1 20 40 Tm (The next paragraph continues after the number.) Tj +ET +`, + [0, 0, 300, 200], + ), + ); + expect(md).not.toMatch(/^#{1,6} \(?3\.18\)?/m); + expect(md).toContain('(3.18)'); + }); + + it('does not emit a lowercase lead-in as a heading', () => { + const md = toMarkdownFromPdf( + pagePdf( + `BT +/F1 12 Tf +1 0 0 1 20 160 Tm (The derivative can be written one way) Tj +1 0 0 1 20 90 Tm (or inversely) Tj +1 0 0 1 20 40 Tm (as a function of the original path.) Tj +ET +`, + [0, 0, 300, 200], + ), + ); + expect(md).not.toMatch(/^#{1,6} or inversely/m); + expect(md).toContain('or inversely'); + }); + + it('keeps a same-size hyphen as a hyphen, not a superscript', () => { + const md = toMarkdownFromPdf( + pagePdf( + `BT +/F1 12 Tf +1 0 0 1 20 80 Tm (unam) Tj +1 0 0 1 50 80 Tm (-) Tj +1 0 0 1 58 80 Tm (biguously) Tj +ET +`, + ), + ); + expect(md).not.toContain('⁻'); + expect(md).toMatch(/unam-?\s*biguously/); + }); + + it('does not emit a colon lead-in as a heading', () => { + const md = toMarkdownFromPdf( + pagePdf( + `BT +/F1 12 Tf +1 0 0 1 20 160 Tm (Rearranging Equation) Tj +1 0 0 1 20 90 Tm (Using Gamma we can write:) Tj +1 0 0 1 20 40 Tm (the Lagrange equation in functional form.) Tj +ET +`, + [0, 0, 300, 200], + ), + ); + expect(md).not.toMatch(/^#{1,6} Using Gamma we can write:/m); + expect(md).toContain('Using Gamma we can write:'); + }); +}); diff --git a/packages/pdf/test/tables.test.ts b/packages/pdf/test/tables.test.ts index 32c6b21..73d8b1e 100644 --- a/packages/pdf/test/tables.test.ts +++ b/packages/pdf/test/tables.test.ts @@ -500,6 +500,76 @@ describe('ruled table detection', () => { expect(t.markdown).not.toMatch(/0.*1.*\//); } }); + + it('keeps a compact numeric table with minus and plus-minus cells', () => { + const items = [ + { text: 'Yr', x: 20, y: 80, width: 14, page: 1 }, + { text: 'Δ', x: 80, y: 80, width: 12, page: 1 }, + { text: 'SD', x: 140, y: 80, width: 14, page: 1 }, + { text: '19', x: 20, y: 60, width: 14, page: 1 }, + { text: '−2.1', x: 80, y: 60, width: 22, page: 1 }, + { text: '1.2±0.3', x: 140, y: 60, width: 36, page: 1 }, + { text: '20', x: 20, y: 40, width: 14, page: 1 }, + { text: '−', x: 80, y: 40, width: 8, page: 1 }, + { text: '1.4±0.2', x: 140, y: 40, width: 36, page: 1 }, + { text: '21', x: 20, y: 20, width: 14, page: 1 }, + { text: '0.4', x: 80, y: 20, width: 16, page: 1 }, + { text: '1.1±0.1', x: 140, y: 20, width: 36, page: 1 }, + ]; + const tables = detectTables(items, [], []); + expect(tables).toHaveLength(1); + expect(tables[0]!.markdown).toContain('|Yr|Δ|SD|'); + expect(tables[0]!.markdown).toContain('|−2.1|1.2±0.3|'); + expect(tables[0]!.markdown).toContain('|−|1.4±0.2|'); + }); + + it('does not treat a stacked Lagrange formula as a table', () => { + const items = [ + { text: 'd', x: 147, y: 470, width: 6, page: 1, fontSize: 10 }, + { text: '∂L', x: 157, y: 470, width: 14, page: 1, fontSize: 10 }, + { text: '∂L', x: 187, y: 470, width: 14, page: 1, fontSize: 10 }, + { text: '−', x: 175, y: 460, width: 8, page: 1, fontSize: 10 }, + { text: '=', x: 205, y: 458, width: 8, page: 1, fontSize: 10 }, + { text: '0.', x: 216, y: 458, width: 10, page: 1, fontSize: 10 }, + { text: 'dt', x: 145, y: 455, width: 10, page: 1, fontSize: 10 }, + { text: '∂', x: 158, y: 455, width: 6, page: 1, fontSize: 10 }, + { text: 'q̇', x: 165, y: 455, width: 6, page: 1, fontSize: 10 }, + { text: '∂q', x: 188, y: 455, width: 12, page: 1, fontSize: 10 }, + ]; + expect(detectTables(items, [], [])).toHaveLength(0); + }); + + it('does not treat numbered display equations as a table', () => { + const items = [ + { text: 'M', x: 227, y: 462, width: 9, page: 1, fontSize: 10 }, + { text: '−', x: 238, y: 461, width: 8, page: 1, fontSize: 10 }, + { text: 'Q(h)', x: 249, y: 462, width: 22, page: 1, fontSize: 10 }, + { text: '=', x: 278, y: 461, width: 8, page: 1, fontSize: 10 }, + { text: 'c', x: 289, y: 462, width: 5, page: 1, fontSize: 8 }, + { text: 'p', x: 294, y: 465, width: 4, page: 1, fontSize: 7 }, + { text: '(3.15a)', x: 484, y: 462, width: 30, page: 1, fontSize: 10 }, + { text: 'M', x: 227, y: 479, width: 9, page: 1, fontSize: 10 }, + { text: '−', x: 238, y: 478, width: 8, page: 1, fontSize: 10 }, + { text: 'Q(2h)', x: 249, y: 479, width: 26, page: 1, fontSize: 10 }, + { text: '=', x: 278, y: 478, width: 8, page: 1, fontSize: 10 }, + { text: '(3.15b)', x: 484, y: 479, width: 30, page: 1, fontSize: 10 }, + ]; + expect(detectTables(items, [], [])).toHaveLength(0); + }); + + it('does not treat numbered minus-only equations as a table', () => { + const items = [ + { text: 'U', x: 40, y: 80, width: 8, page: 1, fontSize: 10 }, + { text: '−', x: 80, y: 80, width: 8, page: 1, fontSize: 10 }, + { text: 'V', x: 120, y: 80, width: 8, page: 1, fontSize: 10 }, + { text: '(2.1a)', x: 180, y: 80, width: 28, page: 1, fontSize: 10 }, + { text: 'A', x: 40, y: 60, width: 8, page: 1, fontSize: 10 }, + { text: '±', x: 80, y: 60, width: 8, page: 1, fontSize: 10 }, + { text: 'B', x: 120, y: 60, width: 8, page: 1, fontSize: 10 }, + { text: '(2.1b)', x: 180, y: 60, width: 28, page: 1, fontSize: 10 }, + ]; + expect(detectTables(items, [], [])).toHaveLength(0); + }); }); function filledPathGridPdf(): Uint8Array { diff --git a/test/snapshots/snapshots__pdf__text.pdf.snap b/test/snapshots/snapshots__pdf__text.pdf.snap index 2be7a22..2aeadc6 100644 --- a/test/snapshots/snapshots__pdf__text.pdf.snap +++ b/test/snapshots/snapshots__pdf__text.pdf.snap @@ -25,7 +25,7 @@ V.Roman five •Bullet one •Bullet two ◦Nested bullet ## Notes and special text -Music clef 𝄞 appears before this footnote¹ reference. i An endnote follows here. Persian with ZWNJ: مهاوخیم. Family emoji: 👨👩👧. Markdown specials: *stars* _under_ [bracket] `tick` #hash 1. dotted | pipe. +Music clef 𝄞 appears before this footnote¹ reference. An endnote follows hereⁱ. Persian with ZWNJ: مهاوخیم. Family emoji: 👨👩👧. Markdown specials: *stars* _under_ [bracket] `tick` #hash 1. dotted | pipe. ## Links and anchors