@boxpdf/html-writer 0.1.9 → 0.1.11

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.js CHANGED
@@ -1,5 +1,324 @@
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
+ let headerOpen = false;
22
+ let headerHasParagraph = false;
23
+ let contentStarted = false;
24
+ let employmentOpen = false;
25
+ let pendingParagraph;
26
+ await write('<article class="pdf-semantic-document">');
27
+ const closeTable = async () => {
28
+ if (!activeTable) return;
29
+ await write("</table>");
30
+ activeTable = void 0;
31
+ };
32
+ const closeSections = async (minimumLevel = 0) => {
33
+ while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
34
+ await write("</section>");
35
+ sectionLevels.pop();
36
+ }
37
+ };
38
+ const flushPendingParagraph = async () => {
39
+ if (!pendingParagraph) return;
40
+ await write(semanticBlockHtml(pendingParagraph.block));
41
+ pendingParagraph = void 0;
42
+ };
43
+ const closeEmployment = async () => {
44
+ if (!employmentOpen) return;
45
+ await write("</section>");
46
+ employmentOpen = false;
47
+ };
48
+ const emitPage = async (page, future) => {
49
+ const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
50
+ const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
51
+ for (const block of page.structured.blocks) {
52
+ if (isRepeatedFurniture(block, page, repeatedFurniture)) {
53
+ stats.suppressedFurniture += 1;
54
+ continue;
55
+ }
56
+ await flushPendingParagraph();
57
+ if (employmentOpen && block.type !== "list") await closeEmployment();
58
+ if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
59
+ await write(`<header><h1>${escapeHtml(block.text)}</h1>`);
60
+ headerOpen = true;
61
+ continue;
62
+ }
63
+ if (headerOpen) {
64
+ if (block.type === "paragraph") {
65
+ const tag = isContactBlock(block) ? "address" : "p";
66
+ await write(`<${tag}>${escapeHtml(block.text)}</${tag}>`);
67
+ headerHasParagraph = true;
68
+ continue;
69
+ }
70
+ if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#"))) {
71
+ await write(`<h${block.level}>${escapeHtml(block.text)}</h${block.level}>`);
72
+ continue;
73
+ }
74
+ await write("</header>");
75
+ headerOpen = false;
76
+ contentStarted = true;
77
+ }
78
+ if (block.type === "table") {
79
+ const rows = tableToRows(block.table);
80
+ if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
81
+ const continuationRows = sameRow(activeTable.header, rows[0]) ? rows.slice(1) : rows;
82
+ for (const row of continuationRows) await write(tableRow(row, false));
83
+ activeTable.table = block.table;
84
+ stats.mergedTables += 1;
85
+ continue;
86
+ }
87
+ await closeTable();
88
+ const header = tableHeader(rows);
89
+ await write("<table>");
90
+ for (const [index, row] of rows.entries())
91
+ await write(tableRow(row, Boolean(header && index === 0)));
92
+ activeTable = { table: block.table, header };
93
+ continue;
94
+ }
95
+ if (activeTable && block.type === "definitionList" && isFinancialSummary(block)) {
96
+ const columns = activeTable.table.columns.length;
97
+ await write(
98
+ `<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`
99
+ );
100
+ await closeTable();
101
+ continue;
102
+ }
103
+ await closeTable();
104
+ if (block.type === "heading") {
105
+ const level = contentStarted && block.level === 1 ? 2 : block.level;
106
+ await closeSections(level);
107
+ await write(
108
+ `<section data-level="${level}"><h${level}>${escapeHtml(block.text)}</h${level}>`
109
+ );
110
+ sectionLevels.push(level);
111
+ continue;
112
+ }
113
+ if (block.type === "paragraph") {
114
+ if (isTitledRecord(block)) {
115
+ const [institution, ...details] = block.lines;
116
+ if (institution) await write(`<h3>${escapeHtml(institution.text)}</h3>`);
117
+ for (const detail of details) await write(`<p>${escapeHtml(detail.text)}</p>`);
118
+ continue;
119
+ }
120
+ if (isUnmarkedList(block)) {
121
+ await write(
122
+ `<ul>${block.lines.map((line) => `<li>${escapeHtml(line.text)}</li>`).join("")}</ul>`
123
+ );
124
+ continue;
125
+ }
126
+ pendingParagraph = { block, height: page.height };
127
+ continue;
128
+ }
129
+ if (block.type === "employment") {
130
+ await write(
131
+ `<section><h3>${escapeHtml(block.role)}</h3><p>${escapeHtml(block.organization)}</p><p>${escapeHtml(block.date)}</p>`
132
+ );
133
+ employmentOpen = true;
134
+ continue;
135
+ }
136
+ await write(semanticBlockHtml(block));
137
+ }
138
+ for (const signature of marginSignatures(page)) seenFurniture.add(signature);
139
+ };
140
+ for await (const page of pages) {
141
+ const structured = structurePage(page);
142
+ buffer.push({ width: page.width, height: page.height, structured });
143
+ stats.pagesProcessed += 1;
144
+ stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
145
+ stats.peakBufferedLines = Math.max(
146
+ stats.peakBufferedLines,
147
+ buffer.reduce((total, item) => total + item.structured.lines.length, 0)
148
+ );
149
+ if (buffer.length >= lookaheadPages) {
150
+ const ready = buffer.shift();
151
+ if (ready) await emitPage(ready, buffer);
152
+ }
153
+ }
154
+ while (buffer.length > 0) {
155
+ const ready = buffer.shift();
156
+ if (ready) await emitPage(ready, buffer);
157
+ }
158
+ if (headerOpen) await write("</header>");
159
+ await closeTable();
160
+ await closeEmployment();
161
+ if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
162
+ await closeSections();
163
+ await write(`<footer>${semanticBlockHtml(pendingParagraph.block)}</footer>`);
164
+ pendingParagraph = void 0;
165
+ } else {
166
+ await flushPendingParagraph();
167
+ await closeSections();
168
+ }
169
+ await write("</article>");
170
+ return stats;
171
+ }
172
+ function isContactBlock(block) {
173
+ const text = block.text;
174
+ const signals = [
175
+ /@/.test(text),
176
+ /\+?\d[\d().\s-]{7,}/.test(text),
177
+ /\bhttps?:\/\//i.test(text),
178
+ /\b\w+\.\w{2,}\b/i.test(text)
179
+ ];
180
+ return signals.filter(Boolean).length >= 2;
181
+ }
182
+ function isTitledRecord(block) {
183
+ if (block.lines.length < 2) return false;
184
+ const [first, ...rest] = block.lines;
185
+ if (!first || rest.length === 0) return false;
186
+ const firstSize = Math.max(...first.spans.map((span) => span.fontSize));
187
+ const restSize = Math.max(...rest.flatMap((line) => line.spans.map((span) => span.fontSize)));
188
+ const emphasized = first.spans.some(
189
+ (span) => /(?:bold|semibold|demi)/i.test(span.fontFamily ?? "")
190
+ );
191
+ return emphasized || firstSize >= restSize * 1.08;
192
+ }
193
+ function isUnmarkedList(block) {
194
+ if (block.lines.length < 3) return false;
195
+ const first = block.lines[0];
196
+ if (!first) return false;
197
+ const aligned = block.lines.every(
198
+ (line) => Math.abs(line.bounds.x - first.bounds.x) <= Math.max(8, first.bounds.height)
199
+ );
200
+ const separated = block.lines.slice(1).every((line, index) => {
201
+ const previous = block.lines[index];
202
+ if (!previous) return false;
203
+ const gap = previous.bounds.y - (line.bounds.y + line.bounds.height);
204
+ return gap >= Math.min(previous.bounds.height, line.bounds.height) * 0.55;
205
+ });
206
+ return aligned && separated;
207
+ }
208
+ function isFooterParagraph(block, pageHeight) {
209
+ const inBottomMargin = block.lines.every(
210
+ (line) => line.bounds.y + line.bounds.height <= pageHeight * 0.15
211
+ );
212
+ return inBottomMargin || /^(?:thanks|thank you)\b/i.test(block.text.trim());
213
+ }
214
+ function marginSignatures(page) {
215
+ return page.structured.blocks.flatMap(
216
+ (block) => blockLines(block).filter((line) => isMarginLine(line.bounds.y, line.bounds.height, page.height)).map((line) => furnitureSignature(line.text))
217
+ );
218
+ }
219
+ function isRepeatedFurniture(block, page, futureFurniture) {
220
+ const lines = blockLines(block);
221
+ return lines.length > 0 && lines.every(
222
+ (line) => isMarginLine(line.bounds.y, line.bounds.height, page.height) && futureFurniture.has(furnitureSignature(line.text))
223
+ );
224
+ }
225
+ function blockLines(block) {
226
+ return block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
227
+ }
228
+ function isMarginLine(y, height, pageHeight) {
229
+ return y + height <= pageHeight * 0.12 || y >= pageHeight * 0.88;
230
+ }
231
+ function furnitureSignature(value) {
232
+ return value.toLocaleLowerCase("en").replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
233
+ }
234
+ function tablesContinue(previous, next, pageWidth) {
235
+ if (previous.columns.length !== next.columns.length || previous.columns.length < 2) return false;
236
+ return previous.columns.every(
237
+ (column, index) => Math.abs(column - (next.columns[index] ?? Number.POSITIVE_INFINITY)) / pageWidth <= 0.04
238
+ );
239
+ }
240
+ function tableHeader(rows) {
241
+ const first = rows[0];
242
+ if (!first) return void 0;
243
+ const later = rows.slice(1).flat();
244
+ return first.every((value) => /\p{L}/u.test(value) && !isNumericValue(value)) && later.some(isNumericValue) ? first : void 0;
245
+ }
246
+ function sameRow(left, right) {
247
+ return Boolean(
248
+ left && right && left.length === right.length && left.every((value, index) => value === right[index])
249
+ );
250
+ }
251
+ function tableRow(row, header) {
252
+ const cell = header ? "th" : "td";
253
+ return `<tr>${row.map((value) => `<${cell}>${escapeHtml(value)}</${cell}>`).join("")}</tr>`;
254
+ }
255
+ function isFinancialSummary(block) {
256
+ return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
257
+ }
258
+ function isNumericValue(value) {
259
+ return /^(?:\p{Sc}\s*)?[\d.,'’\s]+(?:\s*%)?$/u.test(value.trim());
260
+ }
261
+ function financialSummaryRow(entry, columns) {
262
+ const colspan = columns > 2 ? ` colspan="${columns - 1}"` : "";
263
+ return `<tr><th scope="row"${colspan}>${escapeHtml(entry.term)}</th><td>${escapeHtml(entry.description)}</td></tr>`;
264
+ }
265
+ function semanticBlockHtml(block) {
266
+ if (block.type === "heading")
267
+ return `<h${block.level}>${escapeHtml(block.text)}</h${block.level}>`;
268
+ if (block.type === "paragraph") return `<p>${escapeHtml(block.text)}</p>`;
269
+ if (block.type === "definitionList") {
270
+ if (block.entries.length <= 3 && block.entries.some((entry) => entry.description.trim().split(/\s+/).length >= 5) && block.entries.every((entry) => /^[A-Z][A-Z\s/-]*$/.test(entry.term.trim()))) {
271
+ return block.entries.map(
272
+ (entry) => `<section><h2>${escapeHtml(titleCase(entry.term))}</h2><p>${escapeHtml(entry.description)}</p></section>`
273
+ ).join("");
274
+ }
275
+ const list = `<dl>${block.entries.map(
276
+ (entry) => `<div><dt>${escapeHtml(entry.term)}</dt><dd>${escapeHtml(entry.description)}</dd></div>`
277
+ ).join("")}</dl>`;
278
+ return isFinancialSummary(block) ? `<section>${list}</section>` : list;
279
+ }
280
+ if (block.type === "cardList") {
281
+ return `<section><h2>Items ordered</h2><table><thead><tr><th scope="col">Item</th><th scope="col">Quantity</th><th scope="col">Amount</th></tr></thead><tbody>${block.items.map(cardTableRow).join("")}</tbody></table></section>`;
282
+ }
283
+ if (block.type === "sectionGroup") {
284
+ return block.items.map(labeledSectionHtml).join("");
285
+ }
286
+ if (block.type === "employment") {
287
+ return `<section><h3>${escapeHtml(block.role)}</h3><p>${escapeHtml(block.organization)}</p><p>${escapeHtml(block.date)}</p></section>`;
288
+ }
289
+ const tag = block.ordered ? "ol" : "ul";
290
+ return `<${tag}>${block.items.map((item) => `<li>${escapeHtml(item.text)}</li>`).join("")}</${tag}>`;
291
+ }
292
+ function cardTableRow(item) {
293
+ const trailing = item.details.at(-1) ?? "";
294
+ const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
295
+ const description = item.details.slice(0, -1).join(" ");
296
+ const detail = description ? `<br><span>${escapeHtml(description)}</span>` : "";
297
+ const quantity = match?.[1] ?? "";
298
+ const amount = match?.[2] ?? trailing;
299
+ return `<tr><th scope="row">${escapeHtml(item.title)}${detail}</th><td>${escapeHtml(quantity)}</td><td>${escapeHtml(amount)}</td></tr>`;
300
+ }
301
+ function labeledSectionHtml(item) {
302
+ const heading = titleCase(item.label);
303
+ const postal = /\b(?:ship|deliver|mail)(?:ed)?\b/i.test(item.label);
304
+ if (postal) {
305
+ const [name, ...address] = item.content;
306
+ const content = [name ? `<strong>${escapeHtml(name)}</strong>` : "", ...address.map(escapeHtml)].filter(Boolean).join("<br>");
307
+ return `<section><h2>${escapeHtml(heading)}</h2><address>${content}</address></section>`;
308
+ }
309
+ return `<section><h2>${escapeHtml(heading)}</h2>${item.content.map(
310
+ (content, index) => `<p>${index === 0 ? `<strong>${escapeHtml(content)}</strong>` : escapeHtml(content)}</p>`
311
+ ).join("")}</section>`;
312
+ }
313
+ function titleCase(value) {
314
+ const normalized = value.trim().toLocaleLowerCase("en");
315
+ return normalized.replace(/^\p{L}/u, (letter) => letter.toLocaleUpperCase("en"));
316
+ }
317
+ function escapeHtml(value) {
318
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
319
+ }
320
+
321
+ // src/index.ts
3
322
  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
323
  async function writeHtmlDocument(pages, write, options = {}) {
5
324
  const includeDocument = options.includeDocument ?? true;
@@ -9,15 +328,28 @@ async function writeHtmlDocument(pages, write, options = {}) {
9
328
  ` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
10
329
  );
11
330
  await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
12
- await write(`<title>${escapeHtml(options.title ?? "PDF document")}</title>`);
331
+ await write(`<title>${escapeHtml2(options.title ?? "PDF document")}</title>`);
13
332
  if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
14
333
  await write("</head><body>");
15
334
  }
16
335
  await write('<main class="pdf-document">');
17
- for await (const page of pages) await writePage(page, write, options);
336
+ if (resolveProfile(options) === "semantic") {
337
+ const lookahead = semanticLookahead(options.semanticLookaheadPages);
338
+ const stats = await writeSemanticDocument(pages, write, lookahead);
339
+ options.onSemanticStats?.(stats);
340
+ } else {
341
+ for await (const page of pages) await writePage(page, write, options);
342
+ }
18
343
  await write("</main>");
19
344
  if (includeDocument) await write("</body></html>");
20
345
  }
346
+ function semanticLookahead(value) {
347
+ const lookahead = value ?? 4;
348
+ if (!Number.isSafeInteger(lookahead) || lookahead < 1 || lookahead > 16) {
349
+ throw new RangeError("semanticLookaheadPages must be an integer between 1 and 16");
350
+ }
351
+ return lookahead;
352
+ }
21
353
  async function writePage(page, write, options = {}) {
22
354
  if (resolveProfile(options) === "semantic") await writeFlowPage(page, write);
23
355
  else await writePositionedPage(page, write, options);
@@ -57,8 +389,12 @@ async function writePositionedPage(page, write, options) {
57
389
  await write(
58
390
  `<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
391
  );
392
+ const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
393
+ if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
60
394
  if (reflectedOverlay) {
61
- for (const image of page.images ?? []) await write(visualImage(image, page.height));
395
+ for (const [index, image] of (page.images ?? []).entries()) {
396
+ await write(visualImage(image, page.height, page.number, index));
397
+ }
62
398
  }
63
399
  for (const fill of page.fills ?? []) {
64
400
  const points = fill.points.map(([x, y]) => `${number(x)},${number(page.height - y)}`).join(" ");
@@ -69,7 +405,7 @@ async function writePositionedPage(page, write, options) {
69
405
  }
70
406
  if (page.paths?.length) {
71
407
  await write(`<g transform="translate(0 ${number(page.height)}) scale(1 -1)">`);
72
- for (const path of page.paths) {
408
+ for (const [pathIndex, path] of page.paths.entries()) {
73
409
  if (!isSvgPath(path.d)) continue;
74
410
  const fill = isCssHexColor(path.fill) ? path.fill : "none";
75
411
  const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
@@ -81,14 +417,18 @@ async function writePositionedPage(page, write, options) {
81
417
  const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number(path.strokeDashoffset ?? 0)}"` : "";
82
418
  const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
83
419
  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
- );
420
+ let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
421
+ for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
422
+ output = `<g clip-path="url(#${pathClipId(page.number, pathIndex, index)})">${output}</g>`;
423
+ }
424
+ await write(output);
87
425
  }
88
426
  await write("</g>");
89
427
  }
90
428
  if (!reflectedOverlay) {
91
- for (const image of page.images ?? []) await write(visualImage(image, page.height));
429
+ for (const [index, image] of (page.images ?? []).entries()) {
430
+ await write(visualImage(image, page.height, page.number, index));
431
+ }
92
432
  }
93
433
  for (const span of visualSpans) {
94
434
  if (!usesPositionedSpan(span)) {
@@ -109,13 +449,41 @@ function usesReflectedVisualOverlay(page, spans) {
109
449
  (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
450
  );
111
451
  }
112
- function visualImage(image, pageHeight) {
452
+ function visualImage(image, pageHeight, pageNumber, imageIndex) {
113
453
  const [a, b, c, d, e, f] = image.transform;
114
454
  const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number).join(" ");
115
455
  const opacity = isUnitInterval(image.opacity) ? ` opacity="${number(image.opacity)}"` : "";
116
456
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
117
457
  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}/>`;
458
+ let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
459
+ for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
460
+ output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
461
+ }
462
+ return output;
463
+ }
464
+ function imageClipDefinitions(images, pageNumber, pageHeight) {
465
+ return images.flatMap(
466
+ (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
467
+ if (!isSvgPath(clip.d)) return "";
468
+ const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
469
+ return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
470
+ })
471
+ ).join("");
472
+ }
473
+ function imageClipId(pageNumber, imageIndex, clipIndex) {
474
+ return `boxpdf-clip-${pageNumber}-${imageIndex}-${clipIndex}`;
475
+ }
476
+ function pathClipDefinitions(paths, pageNumber) {
477
+ return paths.flatMap(
478
+ (path, pathIndex) => (path.clips ?? []).map((clip, clipIndex) => {
479
+ if (!isSvgPath(clip.d)) return "";
480
+ const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
481
+ return `<clipPath id="${pathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${fillRule}/></clipPath>`;
482
+ })
483
+ ).join("");
484
+ }
485
+ function pathClipId(pageNumber, pathIndex, clipIndex) {
486
+ return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
119
487
  }
120
488
  function rgbBmp(image) {
121
489
  const stride = Math.ceil(image.width * 3 / 4) * 4;
@@ -169,28 +537,55 @@ function positionedSpan(span, fontAliases) {
169
537
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
170
538
  )
171
539
  ].join(";");
172
- return `<span class="pdf-span"${direction} style="${style}">${escapeHtml(span.text)}</span>`;
540
+ return `<span class="pdf-span"${direction} style="${style}">${escapeHtml2(span.text)}</span>`;
173
541
  }
174
542
  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();
543
+ const structured = structurePage2(page);
178
544
  await write(
179
545
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
180
546
  );
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);
547
+ for (const block of structured.blocks) {
548
+ if (block.type === "table") await write(tableToHtml(block.table));
549
+ else if (block.type === "heading") {
550
+ await write(`<h${block.level}>${escapeHtml2(block.text)}</h${block.level}>`);
551
+ } else if (block.type === "paragraph") {
552
+ await write(
553
+ `<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${escapeHtml2(block.text)}</p>`
554
+ );
555
+ } else if (block.type === "definitionList") {
556
+ await write("<dl>");
557
+ for (const entry of block.entries) {
558
+ await write(
559
+ `<div><dt>${escapeHtml2(entry.term)}</dt><dd>${escapeHtml2(entry.description)}</dd></div>`
560
+ );
187
561
  }
188
- continue;
562
+ await write("</dl>");
563
+ } else if (block.type === "cardList") {
564
+ await write('<div class="pdf-semantic-cards">');
565
+ for (const item of block.items) {
566
+ await write(`<article><h3>${escapeHtml2(item.title)}</h3>`);
567
+ for (const detail of item.details) await write(`<p>${escapeHtml2(detail)}</p>`);
568
+ await write("</article>");
569
+ }
570
+ await write("</div>");
571
+ } else if (block.type === "sectionGroup") {
572
+ await write('<div class="pdf-semantic-sections">');
573
+ for (const item of block.items) {
574
+ await write(`<section><h3>${escapeHtml2(item.label)}</h3>`);
575
+ for (const content of item.content) await write(`<p>${escapeHtml2(content)}</p>`);
576
+ await write("</section>");
577
+ }
578
+ await write("</div>");
579
+ } else if (block.type === "employment") {
580
+ await write(
581
+ `<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p></section>`
582
+ );
583
+ } else {
584
+ const tag = block.ordered ? "ol" : "ul";
585
+ await write(`<${tag}>`);
586
+ for (const item of block.items) await write(`<li>${escapeHtml2(item.text)}</li>`);
587
+ await write(`</${tag}>`);
189
588
  }
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
589
  }
195
590
  await write("</section>");
196
591
  }
@@ -232,7 +627,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
232
627
  const anchorX = span.bounds.x + basisX * rtlOffset;
233
628
  const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
234
629
  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>`;
630
+ return `<text${direction}${position} font-size="${number(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml2(span.text)}</text>`;
236
631
  }
237
632
  function isAdobeCjkFont(fontFamily) {
238
633
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -345,16 +740,13 @@ function directionAttribute(spans) {
345
740
  if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
346
741
  return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
347
742
  }
348
- function containsY(table, y) {
349
- return y >= table.bounds.y && y <= table.bounds.y + table.bounds.height;
350
- }
351
743
  function number(value) {
352
744
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
353
745
  }
354
746
  function escapeAttribute(value) {
355
- return escapeHtml(value).replaceAll("`", "&#96;");
747
+ return escapeHtml2(value).replaceAll("`", "&#96;");
356
748
  }
357
- function escapeHtml(value) {
749
+ function escapeHtml2(value) {
358
750
  return [...value].map((character) => {
359
751
  const codePoint = character.codePointAt(0) ?? 0;
360
752
  if (codePoint === 13) return "\n";