@boxpdf/html-writer 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,13 @@
1
1
  import { ExtractedPage } from '@boxpdf/reader';
2
2
 
3
+ interface SemanticDocumentStats {
4
+ pagesProcessed: number;
5
+ peakBufferedPages: number;
6
+ peakBufferedLines: number;
7
+ mergedTables: number;
8
+ suppressedFurniture: number;
9
+ }
10
+
3
11
  type HtmlLayout = "positioned" | "flow";
4
12
  type HtmlProfile = "visual" | "semantic";
5
13
  type HtmlWrite = (chunk: string) => void | Promise<void>;
@@ -12,9 +20,13 @@ interface HtmlWriterOptions {
12
20
  language?: string;
13
21
  includeDocument?: boolean;
14
22
  includeStyles?: boolean;
23
+ /** Maximum extracted page models retained for document-level semantic decisions. */
24
+ semanticLookaheadPages?: number;
25
+ /** Receives bounded-buffer and document-inference statistics after semantic output completes. */
26
+ onSemanticStats?: (stats: Readonly<SemanticDocumentStats>) => void;
15
27
  }
16
28
  declare function writeHtmlDocument(pages: AsyncIterable<ExtractedPage> | Iterable<ExtractedPage>, write: HtmlWrite, options?: HtmlWriterOptions): Promise<void>;
17
29
  declare function writePage(page: ExtractedPage, write: HtmlWrite, options?: HtmlWriterOptions): Promise<void>;
18
30
  declare function pageToHtml(page: ExtractedPage, options?: HtmlWriterOptions): Promise<string>;
19
31
 
20
- export { type HtmlLayout, type HtmlProfile, type HtmlWrite, type HtmlWriterOptions, pageToHtml, writeHtmlDocument, writePage };
32
+ export { type HtmlLayout, type HtmlProfile, type HtmlWrite, type HtmlWriterOptions, type SemanticDocumentStats, pageToHtml, writeHtmlDocument, writePage };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import { ExtractedPage } from '@boxpdf/reader';
2
2
 
3
+ interface SemanticDocumentStats {
4
+ pagesProcessed: number;
5
+ peakBufferedPages: number;
6
+ peakBufferedLines: number;
7
+ mergedTables: number;
8
+ suppressedFurniture: number;
9
+ }
10
+
3
11
  type HtmlLayout = "positioned" | "flow";
4
12
  type HtmlProfile = "visual" | "semantic";
5
13
  type HtmlWrite = (chunk: string) => void | Promise<void>;
@@ -12,9 +20,13 @@ interface HtmlWriterOptions {
12
20
  language?: string;
13
21
  includeDocument?: boolean;
14
22
  includeStyles?: boolean;
23
+ /** Maximum extracted page models retained for document-level semantic decisions. */
24
+ semanticLookaheadPages?: number;
25
+ /** Receives bounded-buffer and document-inference statistics after semantic output completes. */
26
+ onSemanticStats?: (stats: Readonly<SemanticDocumentStats>) => void;
15
27
  }
16
28
  declare function writeHtmlDocument(pages: AsyncIterable<ExtractedPage> | Iterable<ExtractedPage>, write: HtmlWrite, options?: HtmlWriterOptions): Promise<void>;
17
29
  declare function writePage(page: ExtractedPage, write: HtmlWrite, options?: HtmlWriterOptions): Promise<void>;
18
30
  declare function pageToHtml(page: ExtractedPage, options?: HtmlWriterOptions): Promise<string>;
19
31
 
20
- export { type HtmlLayout, type HtmlProfile, type HtmlWrite, type HtmlWriterOptions, pageToHtml, writeHtmlDocument, writePage };
32
+ export { type HtmlLayout, type HtmlProfile, type HtmlWrite, type HtmlWriterOptions, type SemanticDocumentStats, pageToHtml, writeHtmlDocument, writePage };
package/dist/index.js CHANGED
@@ -1,5 +1,163 @@
1
1
  // src/index.ts
2
- import { structurePage, tableToHtml } from "@boxpdf/reader/structure";
2
+ import { structurePage as structurePage2, tableToHtml } from "@boxpdf/reader/structure";
3
+
4
+ // src/semantic-document.ts
5
+ import {
6
+ structurePage,
7
+ tableToRows
8
+ } from "@boxpdf/reader/structure";
9
+ async function writeSemanticDocument(pages, write, lookaheadPages) {
10
+ const stats = {
11
+ pagesProcessed: 0,
12
+ peakBufferedPages: 0,
13
+ peakBufferedLines: 0,
14
+ mergedTables: 0,
15
+ suppressedFurniture: 0
16
+ };
17
+ const buffer = [];
18
+ const seenFurniture = /* @__PURE__ */ new Set();
19
+ const sectionLevels = [];
20
+ let activeTable;
21
+ await write('<article class="pdf-semantic-document">');
22
+ const closeTable = async () => {
23
+ if (!activeTable) return;
24
+ await write("</table>");
25
+ activeTable = void 0;
26
+ };
27
+ const closeSections = async (minimumLevel = 0) => {
28
+ while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
29
+ await write("</section>");
30
+ sectionLevels.pop();
31
+ }
32
+ };
33
+ const emitPage = async (page, future) => {
34
+ const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
35
+ const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
36
+ for (const block of page.structured.blocks) {
37
+ if (isRepeatedFurniture(block, page, repeatedFurniture)) {
38
+ stats.suppressedFurniture += 1;
39
+ continue;
40
+ }
41
+ if (block.type === "table") {
42
+ const rows = tableToRows(block.table);
43
+ if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
44
+ const continuationRows = sameRow(activeTable.header, rows[0]) ? rows.slice(1) : rows;
45
+ for (const row of continuationRows) await write(tableRow(row, false));
46
+ activeTable.table = block.table;
47
+ stats.mergedTables += 1;
48
+ continue;
49
+ }
50
+ await closeTable();
51
+ const header = tableHeader(rows);
52
+ await write("<table>");
53
+ for (const [index, row] of rows.entries())
54
+ await write(tableRow(row, Boolean(header && index === 0)));
55
+ activeTable = { table: block.table, header };
56
+ continue;
57
+ }
58
+ await closeTable();
59
+ if (block.type === "heading") {
60
+ await closeSections(block.level);
61
+ await write(
62
+ `<section data-level="${block.level}"><h${block.level}>${escapeHtml(block.text)}</h${block.level}>`
63
+ );
64
+ sectionLevels.push(block.level);
65
+ continue;
66
+ }
67
+ await write(semanticBlockHtml(block));
68
+ }
69
+ for (const signature of marginSignatures(page)) seenFurniture.add(signature);
70
+ };
71
+ for await (const page of pages) {
72
+ const structured = structurePage(page);
73
+ buffer.push({ width: page.width, height: page.height, structured });
74
+ stats.pagesProcessed += 1;
75
+ stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
76
+ stats.peakBufferedLines = Math.max(
77
+ stats.peakBufferedLines,
78
+ buffer.reduce((total, item) => total + item.structured.lines.length, 0)
79
+ );
80
+ if (buffer.length >= lookaheadPages) {
81
+ const ready = buffer.shift();
82
+ if (ready) await emitPage(ready, buffer);
83
+ }
84
+ }
85
+ while (buffer.length > 0) {
86
+ const ready = buffer.shift();
87
+ if (ready) await emitPage(ready, buffer);
88
+ }
89
+ await closeTable();
90
+ await closeSections();
91
+ await write("</article>");
92
+ return stats;
93
+ }
94
+ function marginSignatures(page) {
95
+ return page.structured.blocks.flatMap(
96
+ (block) => blockLines(block).filter((line) => isMarginLine(line.bounds.y, line.bounds.height, page.height)).map((line) => furnitureSignature(line.text))
97
+ );
98
+ }
99
+ function isRepeatedFurniture(block, page, futureFurniture) {
100
+ const lines = blockLines(block);
101
+ return lines.length > 0 && lines.every(
102
+ (line) => isMarginLine(line.bounds.y, line.bounds.height, page.height) && futureFurniture.has(furnitureSignature(line.text))
103
+ );
104
+ }
105
+ function blockLines(block) {
106
+ return block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
107
+ }
108
+ function isMarginLine(y, height, pageHeight) {
109
+ return y + height <= pageHeight * 0.12 || y >= pageHeight * 0.88;
110
+ }
111
+ function furnitureSignature(value) {
112
+ return value.toLocaleLowerCase("en").replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
113
+ }
114
+ function tablesContinue(previous, next, pageWidth) {
115
+ if (previous.columns.length !== next.columns.length || previous.columns.length < 2) return false;
116
+ return previous.columns.every(
117
+ (column, index) => Math.abs(column - (next.columns[index] ?? Number.POSITIVE_INFINITY)) / pageWidth <= 0.04
118
+ );
119
+ }
120
+ function tableHeader(rows) {
121
+ const first = rows[0];
122
+ if (!first) return void 0;
123
+ return first.some((value) => /^(?:item|description|feature|qty|unit|amount|total)$/i.test(value)) ? first : void 0;
124
+ }
125
+ function sameRow(left, right) {
126
+ return Boolean(
127
+ left && right && left.length === right.length && left.every((value, index) => value === right[index])
128
+ );
129
+ }
130
+ function tableRow(row, header) {
131
+ const cell = header ? "th" : "td";
132
+ return `<tr>${row.map((value) => `<${cell}>${escapeHtml(value)}</${cell}>`).join("")}</tr>`;
133
+ }
134
+ function semanticBlockHtml(block) {
135
+ if (block.type === "heading")
136
+ return `<h${block.level}>${escapeHtml(block.text)}</h${block.level}>`;
137
+ if (block.type === "paragraph") return `<p>${escapeHtml(block.text)}</p>`;
138
+ if (block.type === "definitionList") {
139
+ return `<dl>${block.entries.map(
140
+ (entry) => `<div><dt>${escapeHtml(entry.term)}</dt><dd>${escapeHtml(entry.description)}</dd></div>`
141
+ ).join("")}</dl>`;
142
+ }
143
+ if (block.type === "cardList") {
144
+ return `<div class="pdf-semantic-cards">${block.items.map(
145
+ (item) => `<article><h3>${escapeHtml(item.title)}</h3>${item.details.map((detail) => `<p>${escapeHtml(detail)}</p>`).join("")}</article>`
146
+ ).join("")}</div>`;
147
+ }
148
+ if (block.type === "sectionGroup") {
149
+ return `<div class="pdf-semantic-sections">${block.items.map(
150
+ (item) => `<section><h3>${escapeHtml(item.label)}</h3>${item.content.map((content) => `<p>${escapeHtml(content)}</p>`).join("")}</section>`
151
+ ).join("")}</div>`;
152
+ }
153
+ const tag = block.ordered ? "ol" : "ul";
154
+ return `<${tag}>${block.items.map((item) => `<li>${escapeHtml(item.text)}</li>`).join("")}</${tag}>`;
155
+ }
156
+ function escapeHtml(value) {
157
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
158
+ }
159
+
160
+ // src/index.ts
3
161
  var styles = `.pdf-document{margin:0 auto}.pdf-page{box-sizing:border-box;margin:1rem auto;background:#fff;color:#000}.pdf-page--visual,.pdf-page--positioned{position:relative;overflow:hidden}.pdf-page-content{position:absolute;transform-origin:0 0}.pdf-span{position:absolute;white-space:pre;transform-origin:left bottom;unicode-bidi:isolate}.pdf-span[data-direction=ttb]{writing-mode:vertical-rl}.pdf-page--semantic,.pdf-page--flow{max-width:60rem;padding:1rem}.pdf-page--semantic p,.pdf-page--flow p{white-space:pre-wrap;unicode-bidi:plaintext}.pdf-page table{border-collapse:collapse}.pdf-page td{padding:.15rem .4rem;vertical-align:top}`;
4
162
  async function writeHtmlDocument(pages, write, options = {}) {
5
163
  const includeDocument = options.includeDocument ?? true;
@@ -9,15 +167,28 @@ async function writeHtmlDocument(pages, write, options = {}) {
9
167
  ` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
10
168
  );
11
169
  await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
12
- await write(`<title>${escapeHtml(options.title ?? "PDF document")}</title>`);
170
+ await write(`<title>${escapeHtml2(options.title ?? "PDF document")}</title>`);
13
171
  if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
14
172
  await write("</head><body>");
15
173
  }
16
174
  await write('<main class="pdf-document">');
17
- for await (const page of pages) await writePage(page, write, options);
175
+ if (resolveProfile(options) === "semantic") {
176
+ const lookahead = semanticLookahead(options.semanticLookaheadPages);
177
+ const stats = await writeSemanticDocument(pages, write, lookahead);
178
+ options.onSemanticStats?.(stats);
179
+ } else {
180
+ for await (const page of pages) await writePage(page, write, options);
181
+ }
18
182
  await write("</main>");
19
183
  if (includeDocument) await write("</body></html>");
20
184
  }
185
+ function semanticLookahead(value) {
186
+ const lookahead = value ?? 4;
187
+ if (!Number.isSafeInteger(lookahead) || lookahead < 1 || lookahead > 16) {
188
+ throw new RangeError("semanticLookaheadPages must be an integer between 1 and 16");
189
+ }
190
+ return lookahead;
191
+ }
21
192
  async function writePage(page, write, options = {}) {
22
193
  if (resolveProfile(options) === "semantic") await writeFlowPage(page, write);
23
194
  else await writePositionedPage(page, write, options);
@@ -57,8 +228,12 @@ async function writePositionedPage(page, write, options) {
57
228
  await write(
58
229
  `<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number(page.width)}pt" height="${number(page.height)}pt" viewBox="0 0 ${number(page.width)} ${number(page.height)}">`
59
230
  );
231
+ const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
232
+ if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
60
233
  if (reflectedOverlay) {
61
- for (const image of page.images ?? []) await write(visualImage(image, page.height));
234
+ for (const [index, image] of (page.images ?? []).entries()) {
235
+ await write(visualImage(image, page.height, page.number, index));
236
+ }
62
237
  }
63
238
  for (const fill of page.fills ?? []) {
64
239
  const points = fill.points.map(([x, y]) => `${number(x)},${number(page.height - y)}`).join(" ");
@@ -69,7 +244,7 @@ async function writePositionedPage(page, write, options) {
69
244
  }
70
245
  if (page.paths?.length) {
71
246
  await write(`<g transform="translate(0 ${number(page.height)}) scale(1 -1)">`);
72
- for (const path of page.paths) {
247
+ for (const [pathIndex, path] of page.paths.entries()) {
73
248
  if (!isSvgPath(path.d)) continue;
74
249
  const fill = isCssHexColor(path.fill) ? path.fill : "none";
75
250
  const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
@@ -81,14 +256,18 @@ async function writePositionedPage(page, write, options) {
81
256
  const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number(path.strokeDashoffset ?? 0)}"` : "";
82
257
  const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
83
258
  const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
84
- await write(
85
- `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`
86
- );
259
+ let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
260
+ for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
261
+ output = `<g clip-path="url(#${pathClipId(page.number, pathIndex, index)})">${output}</g>`;
262
+ }
263
+ await write(output);
87
264
  }
88
265
  await write("</g>");
89
266
  }
90
267
  if (!reflectedOverlay) {
91
- for (const image of page.images ?? []) await write(visualImage(image, page.height));
268
+ for (const [index, image] of (page.images ?? []).entries()) {
269
+ await write(visualImage(image, page.height, page.number, index));
270
+ }
92
271
  }
93
272
  for (const span of visualSpans) {
94
273
  if (!usesPositionedSpan(span)) {
@@ -109,13 +288,41 @@ function usesReflectedVisualOverlay(page, spans) {
109
288
  (span) => span.transform !== void 0 && Math.abs(span.transform[0] + 1) < 1e-6 && Math.abs(span.transform[1]) < 1e-6 && Math.abs(span.transform[2]) < 1e-6 && Math.abs(span.transform[3] - 1) < 1e-6
110
289
  );
111
290
  }
112
- function visualImage(image, pageHeight) {
291
+ function visualImage(image, pageHeight, pageNumber, imageIndex) {
113
292
  const [a, b, c, d, e, f] = image.transform;
114
293
  const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number).join(" ");
115
294
  const opacity = isUnitInterval(image.opacity) ? ` opacity="${number(image.opacity)}"` : "";
116
295
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
117
296
  const data = image.format === "jpeg" ? image.data : rgbBmp(image);
118
- return `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
297
+ let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
298
+ for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
299
+ output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
300
+ }
301
+ return output;
302
+ }
303
+ function imageClipDefinitions(images, pageNumber, pageHeight) {
304
+ return images.flatMap(
305
+ (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
306
+ if (!isSvgPath(clip.d)) return "";
307
+ const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
308
+ return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
309
+ })
310
+ ).join("");
311
+ }
312
+ function imageClipId(pageNumber, imageIndex, clipIndex) {
313
+ return `boxpdf-clip-${pageNumber}-${imageIndex}-${clipIndex}`;
314
+ }
315
+ function pathClipDefinitions(paths, pageNumber) {
316
+ return paths.flatMap(
317
+ (path, pathIndex) => (path.clips ?? []).map((clip, clipIndex) => {
318
+ if (!isSvgPath(clip.d)) return "";
319
+ const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
320
+ return `<clipPath id="${pathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${fillRule}/></clipPath>`;
321
+ })
322
+ ).join("");
323
+ }
324
+ function pathClipId(pageNumber, pathIndex, clipIndex) {
325
+ return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
119
326
  }
120
327
  function rgbBmp(image) {
121
328
  const stride = Math.ceil(image.width * 3 / 4) * 4;
@@ -169,28 +376,51 @@ function positionedSpan(span, fontAliases) {
169
376
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
170
377
  )
171
378
  ].join(";");
172
- return `<span class="pdf-span"${direction} style="${style}">${escapeHtml(span.text)}</span>`;
379
+ return `<span class="pdf-span"${direction} style="${style}">${escapeHtml2(span.text)}</span>`;
173
380
  }
174
381
  async function writeFlowPage(page, write) {
175
- const structured = structurePage(page);
176
- const tables = [...structured.tables].sort((left, right) => right.bounds.y - left.bounds.y);
177
- const emittedTables = /* @__PURE__ */ new Set();
382
+ const structured = structurePage2(page);
178
383
  await write(
179
384
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
180
385
  );
181
- for (const line of structured.lines) {
182
- const table = tables.find((candidate) => containsY(candidate, line.bounds.y));
183
- if (table) {
184
- if (!emittedTables.has(table)) {
185
- await write(tableToHtml(table));
186
- emittedTables.add(table);
386
+ for (const block of structured.blocks) {
387
+ if (block.type === "table") await write(tableToHtml(block.table));
388
+ else if (block.type === "heading") {
389
+ await write(`<h${block.level}>${escapeHtml2(block.text)}</h${block.level}>`);
390
+ } else if (block.type === "paragraph") {
391
+ await write(
392
+ `<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${escapeHtml2(block.text)}</p>`
393
+ );
394
+ } else if (block.type === "definitionList") {
395
+ await write("<dl>");
396
+ for (const entry of block.entries) {
397
+ await write(
398
+ `<div><dt>${escapeHtml2(entry.term)}</dt><dd>${escapeHtml2(entry.description)}</dd></div>`
399
+ );
187
400
  }
188
- continue;
401
+ await write("</dl>");
402
+ } else if (block.type === "cardList") {
403
+ await write('<div class="pdf-semantic-cards">');
404
+ for (const item of block.items) {
405
+ await write(`<article><h3>${escapeHtml2(item.title)}</h3>`);
406
+ for (const detail of item.details) await write(`<p>${escapeHtml2(detail)}</p>`);
407
+ await write("</article>");
408
+ }
409
+ await write("</div>");
410
+ } else if (block.type === "sectionGroup") {
411
+ await write('<div class="pdf-semantic-sections">');
412
+ for (const item of block.items) {
413
+ await write(`<section><h3>${escapeHtml2(item.label)}</h3>`);
414
+ for (const content of item.content) await write(`<p>${escapeHtml2(content)}</p>`);
415
+ await write("</section>");
416
+ }
417
+ await write("</div>");
418
+ } else {
419
+ const tag = block.ordered ? "ol" : "ul";
420
+ await write(`<${tag}>`);
421
+ for (const item of block.items) await write(`<li>${escapeHtml2(item.text)}</li>`);
422
+ await write(`</${tag}>`);
189
423
  }
190
- await write(`<p${directionAttribute(line.spans)}>${escapeHtml(line.text)}</p>`);
191
- }
192
- for (const table of tables) {
193
- if (!emittedTables.has(table)) await write(tableToHtml(table));
194
424
  }
195
425
  await write("</section>");
196
426
  }
@@ -232,7 +462,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
232
462
  const anchorX = span.bounds.x + basisX * rtlOffset;
233
463
  const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
234
464
  const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number).join(" ")} ${number(anchorX)} ${number(anchorY)})"` : ` x="${number(anchorX)}" y="${number(anchorY)}"`;
235
- return `<text${direction}${position} font-size="${number(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml(span.text)}</text>`;
465
+ return `<text${direction}${position} font-size="${number(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml2(span.text)}</text>`;
236
466
  }
237
467
  function isAdobeCjkFont(fontFamily) {
238
468
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -345,16 +575,13 @@ function directionAttribute(spans) {
345
575
  if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
346
576
  return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
347
577
  }
348
- function containsY(table, y) {
349
- return y >= table.bounds.y && y <= table.bounds.y + table.bounds.height;
350
- }
351
578
  function number(value) {
352
579
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
353
580
  }
354
581
  function escapeAttribute(value) {
355
- return escapeHtml(value).replaceAll("`", "&#96;");
582
+ return escapeHtml2(value).replaceAll("`", "&#96;");
356
583
  }
357
- function escapeHtml(value) {
584
+ function escapeHtml2(value) {
358
585
  return [...value].map((character) => {
359
586
  const codePoint = character.codePointAt(0) ?? 0;
360
587
  if (codePoint === 13) return "\n";