@boxpdf/html-writer 0.1.10 → 0.1.12
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.cjs +268 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +268 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,59 @@ import {
|
|
|
6
6
|
structurePage,
|
|
7
7
|
tableToRows
|
|
8
8
|
} from "@boxpdf/reader/structure";
|
|
9
|
+
|
|
10
|
+
// src/semantic-inline.ts
|
|
11
|
+
function dominantTextColor(lines) {
|
|
12
|
+
const counts = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const span of lines.flatMap((line) => line.spans)) {
|
|
14
|
+
const color = normalizedColor(span.color) ?? "#000000";
|
|
15
|
+
counts.set(color, (counts.get(color) ?? 0) + Math.max(1, [...span.text].length));
|
|
16
|
+
}
|
|
17
|
+
return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
|
|
18
|
+
}
|
|
19
|
+
function semanticTextHtml(text, lines, defaultColor) {
|
|
20
|
+
const ranges = [];
|
|
21
|
+
let cursor = 0;
|
|
22
|
+
for (const span of lines.flatMap((line) => line.spans)) {
|
|
23
|
+
if (!span.text) continue;
|
|
24
|
+
const start = text.indexOf(span.text, cursor);
|
|
25
|
+
if (start < 0) continue;
|
|
26
|
+
cursor = start + span.text.length;
|
|
27
|
+
const color = normalizedColor(span.color);
|
|
28
|
+
if (color && color !== defaultColor) ranges.push({ start, end: cursor, color });
|
|
29
|
+
}
|
|
30
|
+
const merged = mergeRanges(ranges, text);
|
|
31
|
+
let html = "";
|
|
32
|
+
let offset = 0;
|
|
33
|
+
for (const range of merged) {
|
|
34
|
+
html += escapeHtml(text.slice(offset, range.start));
|
|
35
|
+
html += `<span style="color:${range.color}">${escapeHtml(text.slice(range.start, range.end))}</span>`;
|
|
36
|
+
offset = range.end;
|
|
37
|
+
}
|
|
38
|
+
return html + escapeHtml(text.slice(offset));
|
|
39
|
+
}
|
|
40
|
+
function mergeRanges(ranges, text) {
|
|
41
|
+
const merged = [];
|
|
42
|
+
for (const range of ranges) {
|
|
43
|
+
const previous = merged.at(-1);
|
|
44
|
+
if (previous && previous.color === range.color && /^\s*$/.test(text.slice(previous.end, range.start))) {
|
|
45
|
+
previous.end = range.end;
|
|
46
|
+
} else {
|
|
47
|
+
merged.push({ ...range });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return merged;
|
|
51
|
+
}
|
|
52
|
+
function normalizedColor(value) {
|
|
53
|
+
if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
|
|
54
|
+
const color = value.toLowerCase();
|
|
55
|
+
return color === "#000000" || color === "#000" ? "#000000" : color;
|
|
56
|
+
}
|
|
57
|
+
function escapeHtml(value) {
|
|
58
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/semantic-document.ts
|
|
9
62
|
async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
10
63
|
const stats = {
|
|
11
64
|
pagesProcessed: 0,
|
|
@@ -18,6 +71,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
18
71
|
const seenFurniture = /* @__PURE__ */ new Set();
|
|
19
72
|
const sectionLevels = [];
|
|
20
73
|
let activeTable;
|
|
74
|
+
let headerOpen = false;
|
|
75
|
+
let headerHasParagraph = false;
|
|
76
|
+
let contentStarted = false;
|
|
77
|
+
let employmentOpen = false;
|
|
78
|
+
let pendingParagraph;
|
|
21
79
|
await write('<article class="pdf-semantic-document">');
|
|
22
80
|
const closeTable = async () => {
|
|
23
81
|
if (!activeTable) return;
|
|
@@ -30,14 +88,52 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
30
88
|
sectionLevels.pop();
|
|
31
89
|
}
|
|
32
90
|
};
|
|
91
|
+
const flushPendingParagraph = async () => {
|
|
92
|
+
if (!pendingParagraph) return;
|
|
93
|
+
await write(semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor));
|
|
94
|
+
pendingParagraph = void 0;
|
|
95
|
+
};
|
|
96
|
+
const closeEmployment = async () => {
|
|
97
|
+
if (!employmentOpen) return;
|
|
98
|
+
await write("</section>");
|
|
99
|
+
employmentOpen = false;
|
|
100
|
+
};
|
|
33
101
|
const emitPage = async (page, future) => {
|
|
102
|
+
const defaultColor = dominantTextColor(page.structured.lines);
|
|
34
103
|
const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
|
|
35
104
|
const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
|
|
36
|
-
for (const block of page.structured.blocks) {
|
|
105
|
+
for (const [blockIndex, block] of page.structured.blocks.entries()) {
|
|
106
|
+
const nextBlock = page.structured.blocks[blockIndex + 1];
|
|
37
107
|
if (isRepeatedFurniture(block, page, repeatedFurniture)) {
|
|
38
108
|
stats.suppressedFurniture += 1;
|
|
39
109
|
continue;
|
|
40
110
|
}
|
|
111
|
+
await flushPendingParagraph();
|
|
112
|
+
if (employmentOpen && block.type !== "list") await closeEmployment();
|
|
113
|
+
if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
|
|
114
|
+
await write(`<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor)}</h1>`);
|
|
115
|
+
headerOpen = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (headerOpen) {
|
|
119
|
+
if (block.type === "paragraph") {
|
|
120
|
+
const tag = isContactBlock(block) ? "address" : "p";
|
|
121
|
+
await write(
|
|
122
|
+
`<${tag}>${semanticTextHtml(block.text, block.lines, defaultColor)}</${tag}>`
|
|
123
|
+
);
|
|
124
|
+
headerHasParagraph = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
|
|
128
|
+
await write(
|
|
129
|
+
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
|
|
130
|
+
);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
await write("</header>");
|
|
134
|
+
headerOpen = false;
|
|
135
|
+
contentStarted = true;
|
|
136
|
+
}
|
|
41
137
|
if (block.type === "table") {
|
|
42
138
|
const rows = tableToRows(block.table);
|
|
43
139
|
if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
|
|
@@ -55,16 +151,48 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
55
151
|
activeTable = { table: block.table, header };
|
|
56
152
|
continue;
|
|
57
153
|
}
|
|
154
|
+
if (activeTable && block.type === "definitionList" && isFinancialSummary(block)) {
|
|
155
|
+
const columns = activeTable.table.columns.length;
|
|
156
|
+
await write(
|
|
157
|
+
`<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`
|
|
158
|
+
);
|
|
159
|
+
await closeTable();
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
58
162
|
await closeTable();
|
|
59
163
|
if (block.type === "heading") {
|
|
60
|
-
|
|
164
|
+
const level = contentStarted && block.level === 1 ? 2 : block.level;
|
|
165
|
+
await closeSections(level);
|
|
166
|
+
await write(
|
|
167
|
+
`<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${level}>`
|
|
168
|
+
);
|
|
169
|
+
sectionLevels.push(level);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (block.type === "paragraph") {
|
|
173
|
+
if (isTitledRecord(block)) {
|
|
174
|
+
const [institution, ...details] = block.lines;
|
|
175
|
+
if (institution) await write(`<h3>${escapeHtml2(institution.text)}</h3>`);
|
|
176
|
+
for (const detail of details) await write(`<p>${escapeHtml2(detail.text)}</p>`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (isUnmarkedList(block)) {
|
|
180
|
+
await write(
|
|
181
|
+
`<ul>${block.lines.map((line) => `<li>${escapeHtml2(line.text)}</li>`).join("")}</ul>`
|
|
182
|
+
);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
pendingParagraph = { block, height: page.height, defaultColor };
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (block.type === "employment") {
|
|
61
189
|
await write(
|
|
62
|
-
`<section
|
|
190
|
+
`<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p>`
|
|
63
191
|
);
|
|
64
|
-
|
|
192
|
+
employmentOpen = true;
|
|
65
193
|
continue;
|
|
66
194
|
}
|
|
67
|
-
await write(semanticBlockHtml(block));
|
|
195
|
+
await write(semanticBlockHtml(block, defaultColor));
|
|
68
196
|
}
|
|
69
197
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
70
198
|
};
|
|
@@ -86,11 +214,64 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
86
214
|
const ready = buffer.shift();
|
|
87
215
|
if (ready) await emitPage(ready, buffer);
|
|
88
216
|
}
|
|
217
|
+
if (headerOpen) await write("</header>");
|
|
89
218
|
await closeTable();
|
|
90
|
-
await
|
|
219
|
+
await closeEmployment();
|
|
220
|
+
if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
|
|
221
|
+
await closeSections();
|
|
222
|
+
await write(
|
|
223
|
+
`<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`
|
|
224
|
+
);
|
|
225
|
+
pendingParagraph = void 0;
|
|
226
|
+
} else {
|
|
227
|
+
await flushPendingParagraph();
|
|
228
|
+
await closeSections();
|
|
229
|
+
}
|
|
91
230
|
await write("</article>");
|
|
92
231
|
return stats;
|
|
93
232
|
}
|
|
233
|
+
function isContactBlock(block) {
|
|
234
|
+
const text = block.text;
|
|
235
|
+
const signals = [
|
|
236
|
+
/@/.test(text),
|
|
237
|
+
/\+?\d[\d().\s-]{7,}/.test(text),
|
|
238
|
+
/\bhttps?:\/\//i.test(text),
|
|
239
|
+
/\b\w+\.\w{2,}\b/i.test(text)
|
|
240
|
+
];
|
|
241
|
+
return signals.filter(Boolean).length >= 2;
|
|
242
|
+
}
|
|
243
|
+
function isTitledRecord(block) {
|
|
244
|
+
if (block.lines.length < 2) return false;
|
|
245
|
+
const [first, ...rest] = block.lines;
|
|
246
|
+
if (!first || rest.length === 0) return false;
|
|
247
|
+
const firstSize = Math.max(...first.spans.map((span) => span.fontSize));
|
|
248
|
+
const restSize = Math.max(...rest.flatMap((line) => line.spans.map((span) => span.fontSize)));
|
|
249
|
+
const emphasized = first.spans.some(
|
|
250
|
+
(span) => /(?:bold|semibold|demi)/i.test(span.fontFamily ?? "")
|
|
251
|
+
);
|
|
252
|
+
return emphasized || firstSize >= restSize * 1.08;
|
|
253
|
+
}
|
|
254
|
+
function isUnmarkedList(block) {
|
|
255
|
+
if (block.lines.length < 3) return false;
|
|
256
|
+
const first = block.lines[0];
|
|
257
|
+
if (!first) return false;
|
|
258
|
+
const aligned = block.lines.every(
|
|
259
|
+
(line) => Math.abs(line.bounds.x - first.bounds.x) <= Math.max(8, first.bounds.height)
|
|
260
|
+
);
|
|
261
|
+
const separated = block.lines.slice(1).every((line, index) => {
|
|
262
|
+
const previous = block.lines[index];
|
|
263
|
+
if (!previous) return false;
|
|
264
|
+
const gap = previous.bounds.y - (line.bounds.y + line.bounds.height);
|
|
265
|
+
return gap >= Math.min(previous.bounds.height, line.bounds.height) * 0.55;
|
|
266
|
+
});
|
|
267
|
+
return aligned && separated;
|
|
268
|
+
}
|
|
269
|
+
function isFooterParagraph(block, pageHeight) {
|
|
270
|
+
const inBottomMargin = block.lines.every(
|
|
271
|
+
(line) => line.bounds.y + line.bounds.height <= pageHeight * 0.15
|
|
272
|
+
);
|
|
273
|
+
return inBottomMargin || /^(?:thanks|thank you)\b/i.test(block.text.trim());
|
|
274
|
+
}
|
|
94
275
|
function marginSignatures(page) {
|
|
95
276
|
return page.structured.blocks.flatMap(
|
|
96
277
|
(block) => blockLines(block).filter((line) => isMarginLine(line.bounds.y, line.bounds.height, page.height)).map((line) => furnitureSignature(line.text))
|
|
@@ -120,7 +301,8 @@ function tablesContinue(previous, next, pageWidth) {
|
|
|
120
301
|
function tableHeader(rows) {
|
|
121
302
|
const first = rows[0];
|
|
122
303
|
if (!first) return void 0;
|
|
123
|
-
|
|
304
|
+
const later = rows.slice(1).flat();
|
|
305
|
+
return first.every((value) => /\p{L}/u.test(value) && !isNumericValue(value)) && later.some(isNumericValue) ? first : void 0;
|
|
124
306
|
}
|
|
125
307
|
function sameRow(left, right) {
|
|
126
308
|
return Boolean(
|
|
@@ -129,36 +311,78 @@ function sameRow(left, right) {
|
|
|
129
311
|
}
|
|
130
312
|
function tableRow(row, header) {
|
|
131
313
|
const cell = header ? "th" : "td";
|
|
132
|
-
return `<tr>${row.map((value) => `<${cell}>${
|
|
314
|
+
return `<tr>${row.map((value) => `<${cell}>${escapeHtml2(value)}</${cell}>`).join("")}</tr>`;
|
|
315
|
+
}
|
|
316
|
+
function isFinancialSummary(block) {
|
|
317
|
+
return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
|
|
318
|
+
}
|
|
319
|
+
function isNumericValue(value) {
|
|
320
|
+
return /^(?:\p{Sc}\s*)?[\d.,'’\s]+(?:\s*%)?$/u.test(value.trim());
|
|
321
|
+
}
|
|
322
|
+
function financialSummaryRow(entry, columns) {
|
|
323
|
+
const colspan = columns > 2 ? ` colspan="${columns - 1}"` : "";
|
|
324
|
+
return `<tr><th scope="row"${colspan}>${escapeHtml2(entry.term)}</th><td>${escapeHtml2(entry.description)}</td></tr>`;
|
|
133
325
|
}
|
|
134
|
-
function semanticBlockHtml(block) {
|
|
326
|
+
function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
135
327
|
if (block.type === "heading")
|
|
136
|
-
return `<h${block.level}>${
|
|
137
|
-
if (block.type === "paragraph")
|
|
328
|
+
return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`;
|
|
329
|
+
if (block.type === "paragraph")
|
|
330
|
+
return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
|
|
331
|
+
if (block.type === "preformatted") return `<pre>${escapeHtml2(block.text)}</pre>`;
|
|
138
332
|
if (block.type === "definitionList") {
|
|
139
|
-
|
|
140
|
-
|
|
333
|
+
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()))) {
|
|
334
|
+
return block.entries.map(
|
|
335
|
+
(entry) => `<section><h2>${escapeHtml2(titleCase(entry.term))}</h2><p>${escapeHtml2(entry.description)}</p></section>`
|
|
336
|
+
).join("");
|
|
337
|
+
}
|
|
338
|
+
const list = `<dl>${block.entries.map(
|
|
339
|
+
(entry) => `<div><dt>${escapeHtml2(entry.term)}</dt><dd>${escapeHtml2(entry.description)}</dd></div>`
|
|
141
340
|
).join("")}</dl>`;
|
|
341
|
+
return isFinancialSummary(block) ? `<section>${list}</section>` : list;
|
|
142
342
|
}
|
|
143
343
|
if (block.type === "cardList") {
|
|
144
|
-
return `<
|
|
145
|
-
(item) => `<article><h3>${escapeHtml(item.title)}</h3>${item.details.map((detail) => `<p>${escapeHtml(detail)}</p>`).join("")}</article>`
|
|
146
|
-
).join("")}</div>`;
|
|
344
|
+
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>`;
|
|
147
345
|
}
|
|
148
346
|
if (block.type === "sectionGroup") {
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
347
|
+
return block.items.map(labeledSectionHtml).join("");
|
|
348
|
+
}
|
|
349
|
+
if (block.type === "employment") {
|
|
350
|
+
return `<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p></section>`;
|
|
152
351
|
}
|
|
153
352
|
const tag = block.ordered ? "ol" : "ul";
|
|
154
|
-
return `<${tag}>${block.items.map((item) => `<li>${
|
|
353
|
+
return `<${tag}>${block.items.map((item) => `<li>${escapeHtml2(item.text)}</li>`).join("")}</${tag}>`;
|
|
354
|
+
}
|
|
355
|
+
function cardTableRow(item) {
|
|
356
|
+
const trailing = item.details.at(-1) ?? "";
|
|
357
|
+
const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
|
|
358
|
+
const description = item.details.slice(0, -1).join(" ");
|
|
359
|
+
const detail = description ? `<br><span>${escapeHtml2(description)}</span>` : "";
|
|
360
|
+
const quantity = match?.[1] ?? "";
|
|
361
|
+
const amount = match?.[2] ?? trailing;
|
|
362
|
+
return `<tr><th scope="row">${escapeHtml2(item.title)}${detail}</th><td>${escapeHtml2(quantity)}</td><td>${escapeHtml2(amount)}</td></tr>`;
|
|
363
|
+
}
|
|
364
|
+
function labeledSectionHtml(item) {
|
|
365
|
+
const heading = titleCase(item.label);
|
|
366
|
+
const postal = /\b(?:ship|deliver|mail)(?:ed)?\b/i.test(item.label);
|
|
367
|
+
if (postal) {
|
|
368
|
+
const [name, ...address] = item.content;
|
|
369
|
+
const content = [name ? `<strong>${escapeHtml2(name)}</strong>` : "", ...address.map(escapeHtml2)].filter(Boolean).join("<br>");
|
|
370
|
+
return `<section><h2>${escapeHtml2(heading)}</h2><address>${content}</address></section>`;
|
|
371
|
+
}
|
|
372
|
+
return `<section><h2>${escapeHtml2(heading)}</h2>${item.content.map(
|
|
373
|
+
(content, index) => `<p>${index === 0 ? `<strong>${escapeHtml2(content)}</strong>` : escapeHtml2(content)}</p>`
|
|
374
|
+
).join("")}</section>`;
|
|
155
375
|
}
|
|
156
|
-
function
|
|
376
|
+
function titleCase(value) {
|
|
377
|
+
const normalized = value.trim().toLocaleLowerCase("en");
|
|
378
|
+
return normalized.replace(/^\p{L}/u, (letter) => letter.toLocaleUpperCase("en"));
|
|
379
|
+
}
|
|
380
|
+
function escapeHtml2(value) {
|
|
157
381
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
158
382
|
}
|
|
159
383
|
|
|
160
384
|
// src/index.ts
|
|
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}`;
|
|
385
|
+
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-semantic-document h1,.pdf-page--semantic h1{font-size:1.7em}.pdf-semantic-document h2,.pdf-page--semantic h2{font-size:1.5em}.pdf-semantic-document h3,.pdf-page--semantic h3{font-size:1.35em}.pdf-semantic-document h4,.pdf-page--semantic h4{font-size:1.1em}.pdf-page table{border-collapse:collapse}.pdf-page td{padding:.15rem .4rem;vertical-align:top}`;
|
|
162
386
|
async function writeHtmlDocument(pages, write, options = {}) {
|
|
163
387
|
const includeDocument = options.includeDocument ?? true;
|
|
164
388
|
if (includeDocument) {
|
|
@@ -167,7 +391,7 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
167
391
|
` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
|
|
168
392
|
);
|
|
169
393
|
await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
|
|
170
|
-
await write(`<title>${
|
|
394
|
+
await write(`<title>${escapeHtml3(options.title ?? "PDF document")}</title>`);
|
|
171
395
|
if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
|
|
172
396
|
await write("</head><body>");
|
|
173
397
|
}
|
|
@@ -376,49 +600,58 @@ function positionedSpan(span, fontAliases) {
|
|
|
376
600
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
377
601
|
)
|
|
378
602
|
].join(";");
|
|
379
|
-
return `<span class="pdf-span"${direction} style="${style}">${
|
|
603
|
+
return `<span class="pdf-span"${direction} style="${style}">${escapeHtml3(span.text)}</span>`;
|
|
380
604
|
}
|
|
381
605
|
async function writeFlowPage(page, write) {
|
|
382
606
|
const structured = structurePage2(page);
|
|
607
|
+
const defaultColor = dominantTextColor(structured.lines);
|
|
383
608
|
await write(
|
|
384
609
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
385
610
|
);
|
|
386
611
|
for (const block of structured.blocks) {
|
|
387
612
|
if (block.type === "table") await write(tableToHtml(block.table));
|
|
388
613
|
else if (block.type === "heading") {
|
|
389
|
-
await write(
|
|
614
|
+
await write(
|
|
615
|
+
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
|
|
616
|
+
);
|
|
390
617
|
} else if (block.type === "paragraph") {
|
|
391
618
|
await write(
|
|
392
|
-
`<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${
|
|
619
|
+
`<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`
|
|
393
620
|
);
|
|
621
|
+
} else if (block.type === "preformatted") {
|
|
622
|
+
await write(`<pre>${escapeHtml3(block.text)}</pre>`);
|
|
394
623
|
} else if (block.type === "definitionList") {
|
|
395
624
|
await write("<dl>");
|
|
396
625
|
for (const entry of block.entries) {
|
|
397
626
|
await write(
|
|
398
|
-
`<div><dt>${
|
|
627
|
+
`<div><dt>${escapeHtml3(entry.term)}</dt><dd>${escapeHtml3(entry.description)}</dd></div>`
|
|
399
628
|
);
|
|
400
629
|
}
|
|
401
630
|
await write("</dl>");
|
|
402
631
|
} else if (block.type === "cardList") {
|
|
403
632
|
await write('<div class="pdf-semantic-cards">');
|
|
404
633
|
for (const item of block.items) {
|
|
405
|
-
await write(`<article><h3>${
|
|
406
|
-
for (const detail of item.details) await write(`<p>${
|
|
634
|
+
await write(`<article><h3>${escapeHtml3(item.title)}</h3>`);
|
|
635
|
+
for (const detail of item.details) await write(`<p>${escapeHtml3(detail)}</p>`);
|
|
407
636
|
await write("</article>");
|
|
408
637
|
}
|
|
409
638
|
await write("</div>");
|
|
410
639
|
} else if (block.type === "sectionGroup") {
|
|
411
640
|
await write('<div class="pdf-semantic-sections">');
|
|
412
641
|
for (const item of block.items) {
|
|
413
|
-
await write(`<section><h3>${
|
|
414
|
-
for (const content of item.content) await write(`<p>${
|
|
642
|
+
await write(`<section><h3>${escapeHtml3(item.label)}</h3>`);
|
|
643
|
+
for (const content of item.content) await write(`<p>${escapeHtml3(content)}</p>`);
|
|
415
644
|
await write("</section>");
|
|
416
645
|
}
|
|
417
646
|
await write("</div>");
|
|
647
|
+
} else if (block.type === "employment") {
|
|
648
|
+
await write(
|
|
649
|
+
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`
|
|
650
|
+
);
|
|
418
651
|
} else {
|
|
419
652
|
const tag = block.ordered ? "ol" : "ul";
|
|
420
653
|
await write(`<${tag}>`);
|
|
421
|
-
for (const item of block.items) await write(`<li>${
|
|
654
|
+
for (const item of block.items) await write(`<li>${escapeHtml3(item.text)}</li>`);
|
|
422
655
|
await write(`</${tag}>`);
|
|
423
656
|
}
|
|
424
657
|
}
|
|
@@ -462,7 +695,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
462
695
|
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
463
696
|
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
464
697
|
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number).join(" ")} ${number(anchorX)} ${number(anchorY)})"` : ` x="${number(anchorX)}" y="${number(anchorY)}"`;
|
|
465
|
-
return `<text${direction}${position} font-size="${number(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${
|
|
698
|
+
return `<text${direction}${position} font-size="${number(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml3(span.text)}</text>`;
|
|
466
699
|
}
|
|
467
700
|
function isAdobeCjkFont(fontFamily) {
|
|
468
701
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -579,9 +812,9 @@ function number(value) {
|
|
|
579
812
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
580
813
|
}
|
|
581
814
|
function escapeAttribute(value) {
|
|
582
|
-
return
|
|
815
|
+
return escapeHtml3(value).replaceAll("`", "`");
|
|
583
816
|
}
|
|
584
|
-
function
|
|
817
|
+
function escapeHtml3(value) {
|
|
585
818
|
return [...value].map((character) => {
|
|
586
819
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
587
820
|
if (codePoint === 13) return "\n";
|