@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.cjs
CHANGED
|
@@ -29,6 +29,59 @@ var import_structure2 = require("@boxpdf/reader/structure");
|
|
|
29
29
|
|
|
30
30
|
// src/semantic-document.ts
|
|
31
31
|
var import_structure = require("@boxpdf/reader/structure");
|
|
32
|
+
|
|
33
|
+
// src/semantic-inline.ts
|
|
34
|
+
function dominantTextColor(lines) {
|
|
35
|
+
const counts = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const span of lines.flatMap((line) => line.spans)) {
|
|
37
|
+
const color = normalizedColor(span.color) ?? "#000000";
|
|
38
|
+
counts.set(color, (counts.get(color) ?? 0) + Math.max(1, [...span.text].length));
|
|
39
|
+
}
|
|
40
|
+
return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
|
|
41
|
+
}
|
|
42
|
+
function semanticTextHtml(text, lines, defaultColor) {
|
|
43
|
+
const ranges = [];
|
|
44
|
+
let cursor = 0;
|
|
45
|
+
for (const span of lines.flatMap((line) => line.spans)) {
|
|
46
|
+
if (!span.text) continue;
|
|
47
|
+
const start = text.indexOf(span.text, cursor);
|
|
48
|
+
if (start < 0) continue;
|
|
49
|
+
cursor = start + span.text.length;
|
|
50
|
+
const color = normalizedColor(span.color);
|
|
51
|
+
if (color && color !== defaultColor) ranges.push({ start, end: cursor, color });
|
|
52
|
+
}
|
|
53
|
+
const merged = mergeRanges(ranges, text);
|
|
54
|
+
let html = "";
|
|
55
|
+
let offset = 0;
|
|
56
|
+
for (const range of merged) {
|
|
57
|
+
html += escapeHtml(text.slice(offset, range.start));
|
|
58
|
+
html += `<span style="color:${range.color}">${escapeHtml(text.slice(range.start, range.end))}</span>`;
|
|
59
|
+
offset = range.end;
|
|
60
|
+
}
|
|
61
|
+
return html + escapeHtml(text.slice(offset));
|
|
62
|
+
}
|
|
63
|
+
function mergeRanges(ranges, text) {
|
|
64
|
+
const merged = [];
|
|
65
|
+
for (const range of ranges) {
|
|
66
|
+
const previous = merged.at(-1);
|
|
67
|
+
if (previous && previous.color === range.color && /^\s*$/.test(text.slice(previous.end, range.start))) {
|
|
68
|
+
previous.end = range.end;
|
|
69
|
+
} else {
|
|
70
|
+
merged.push({ ...range });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return merged;
|
|
74
|
+
}
|
|
75
|
+
function normalizedColor(value) {
|
|
76
|
+
if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
|
|
77
|
+
const color = value.toLowerCase();
|
|
78
|
+
return color === "#000000" || color === "#000" ? "#000000" : color;
|
|
79
|
+
}
|
|
80
|
+
function escapeHtml(value) {
|
|
81
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/semantic-document.ts
|
|
32
85
|
async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
33
86
|
const stats = {
|
|
34
87
|
pagesProcessed: 0,
|
|
@@ -41,6 +94,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
41
94
|
const seenFurniture = /* @__PURE__ */ new Set();
|
|
42
95
|
const sectionLevels = [];
|
|
43
96
|
let activeTable;
|
|
97
|
+
let headerOpen = false;
|
|
98
|
+
let headerHasParagraph = false;
|
|
99
|
+
let contentStarted = false;
|
|
100
|
+
let employmentOpen = false;
|
|
101
|
+
let pendingParagraph;
|
|
44
102
|
await write('<article class="pdf-semantic-document">');
|
|
45
103
|
const closeTable = async () => {
|
|
46
104
|
if (!activeTable) return;
|
|
@@ -53,14 +111,52 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
53
111
|
sectionLevels.pop();
|
|
54
112
|
}
|
|
55
113
|
};
|
|
114
|
+
const flushPendingParagraph = async () => {
|
|
115
|
+
if (!pendingParagraph) return;
|
|
116
|
+
await write(semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor));
|
|
117
|
+
pendingParagraph = void 0;
|
|
118
|
+
};
|
|
119
|
+
const closeEmployment = async () => {
|
|
120
|
+
if (!employmentOpen) return;
|
|
121
|
+
await write("</section>");
|
|
122
|
+
employmentOpen = false;
|
|
123
|
+
};
|
|
56
124
|
const emitPage = async (page, future) => {
|
|
125
|
+
const defaultColor = dominantTextColor(page.structured.lines);
|
|
57
126
|
const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
|
|
58
127
|
const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
|
|
59
|
-
for (const block of page.structured.blocks) {
|
|
128
|
+
for (const [blockIndex, block] of page.structured.blocks.entries()) {
|
|
129
|
+
const nextBlock = page.structured.blocks[blockIndex + 1];
|
|
60
130
|
if (isRepeatedFurniture(block, page, repeatedFurniture)) {
|
|
61
131
|
stats.suppressedFurniture += 1;
|
|
62
132
|
continue;
|
|
63
133
|
}
|
|
134
|
+
await flushPendingParagraph();
|
|
135
|
+
if (employmentOpen && block.type !== "list") await closeEmployment();
|
|
136
|
+
if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
|
|
137
|
+
await write(`<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor)}</h1>`);
|
|
138
|
+
headerOpen = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (headerOpen) {
|
|
142
|
+
if (block.type === "paragraph") {
|
|
143
|
+
const tag = isContactBlock(block) ? "address" : "p";
|
|
144
|
+
await write(
|
|
145
|
+
`<${tag}>${semanticTextHtml(block.text, block.lines, defaultColor)}</${tag}>`
|
|
146
|
+
);
|
|
147
|
+
headerHasParagraph = true;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
|
|
151
|
+
await write(
|
|
152
|
+
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
|
|
153
|
+
);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
await write("</header>");
|
|
157
|
+
headerOpen = false;
|
|
158
|
+
contentStarted = true;
|
|
159
|
+
}
|
|
64
160
|
if (block.type === "table") {
|
|
65
161
|
const rows = (0, import_structure.tableToRows)(block.table);
|
|
66
162
|
if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
|
|
@@ -78,16 +174,48 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
78
174
|
activeTable = { table: block.table, header };
|
|
79
175
|
continue;
|
|
80
176
|
}
|
|
177
|
+
if (activeTable && block.type === "definitionList" && isFinancialSummary(block)) {
|
|
178
|
+
const columns = activeTable.table.columns.length;
|
|
179
|
+
await write(
|
|
180
|
+
`<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`
|
|
181
|
+
);
|
|
182
|
+
await closeTable();
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
81
185
|
await closeTable();
|
|
82
186
|
if (block.type === "heading") {
|
|
83
|
-
|
|
187
|
+
const level = contentStarted && block.level === 1 ? 2 : block.level;
|
|
188
|
+
await closeSections(level);
|
|
189
|
+
await write(
|
|
190
|
+
`<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${level}>`
|
|
191
|
+
);
|
|
192
|
+
sectionLevels.push(level);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (block.type === "paragraph") {
|
|
196
|
+
if (isTitledRecord(block)) {
|
|
197
|
+
const [institution, ...details] = block.lines;
|
|
198
|
+
if (institution) await write(`<h3>${escapeHtml2(institution.text)}</h3>`);
|
|
199
|
+
for (const detail of details) await write(`<p>${escapeHtml2(detail.text)}</p>`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (isUnmarkedList(block)) {
|
|
203
|
+
await write(
|
|
204
|
+
`<ul>${block.lines.map((line) => `<li>${escapeHtml2(line.text)}</li>`).join("")}</ul>`
|
|
205
|
+
);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
pendingParagraph = { block, height: page.height, defaultColor };
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (block.type === "employment") {
|
|
84
212
|
await write(
|
|
85
|
-
`<section
|
|
213
|
+
`<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p>`
|
|
86
214
|
);
|
|
87
|
-
|
|
215
|
+
employmentOpen = true;
|
|
88
216
|
continue;
|
|
89
217
|
}
|
|
90
|
-
await write(semanticBlockHtml(block));
|
|
218
|
+
await write(semanticBlockHtml(block, defaultColor));
|
|
91
219
|
}
|
|
92
220
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
93
221
|
};
|
|
@@ -109,11 +237,64 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
109
237
|
const ready = buffer.shift();
|
|
110
238
|
if (ready) await emitPage(ready, buffer);
|
|
111
239
|
}
|
|
240
|
+
if (headerOpen) await write("</header>");
|
|
112
241
|
await closeTable();
|
|
113
|
-
await
|
|
242
|
+
await closeEmployment();
|
|
243
|
+
if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
|
|
244
|
+
await closeSections();
|
|
245
|
+
await write(
|
|
246
|
+
`<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`
|
|
247
|
+
);
|
|
248
|
+
pendingParagraph = void 0;
|
|
249
|
+
} else {
|
|
250
|
+
await flushPendingParagraph();
|
|
251
|
+
await closeSections();
|
|
252
|
+
}
|
|
114
253
|
await write("</article>");
|
|
115
254
|
return stats;
|
|
116
255
|
}
|
|
256
|
+
function isContactBlock(block) {
|
|
257
|
+
const text = block.text;
|
|
258
|
+
const signals = [
|
|
259
|
+
/@/.test(text),
|
|
260
|
+
/\+?\d[\d().\s-]{7,}/.test(text),
|
|
261
|
+
/\bhttps?:\/\//i.test(text),
|
|
262
|
+
/\b\w+\.\w{2,}\b/i.test(text)
|
|
263
|
+
];
|
|
264
|
+
return signals.filter(Boolean).length >= 2;
|
|
265
|
+
}
|
|
266
|
+
function isTitledRecord(block) {
|
|
267
|
+
if (block.lines.length < 2) return false;
|
|
268
|
+
const [first, ...rest] = block.lines;
|
|
269
|
+
if (!first || rest.length === 0) return false;
|
|
270
|
+
const firstSize = Math.max(...first.spans.map((span) => span.fontSize));
|
|
271
|
+
const restSize = Math.max(...rest.flatMap((line) => line.spans.map((span) => span.fontSize)));
|
|
272
|
+
const emphasized = first.spans.some(
|
|
273
|
+
(span) => /(?:bold|semibold|demi)/i.test(span.fontFamily ?? "")
|
|
274
|
+
);
|
|
275
|
+
return emphasized || firstSize >= restSize * 1.08;
|
|
276
|
+
}
|
|
277
|
+
function isUnmarkedList(block) {
|
|
278
|
+
if (block.lines.length < 3) return false;
|
|
279
|
+
const first = block.lines[0];
|
|
280
|
+
if (!first) return false;
|
|
281
|
+
const aligned = block.lines.every(
|
|
282
|
+
(line) => Math.abs(line.bounds.x - first.bounds.x) <= Math.max(8, first.bounds.height)
|
|
283
|
+
);
|
|
284
|
+
const separated = block.lines.slice(1).every((line, index) => {
|
|
285
|
+
const previous = block.lines[index];
|
|
286
|
+
if (!previous) return false;
|
|
287
|
+
const gap = previous.bounds.y - (line.bounds.y + line.bounds.height);
|
|
288
|
+
return gap >= Math.min(previous.bounds.height, line.bounds.height) * 0.55;
|
|
289
|
+
});
|
|
290
|
+
return aligned && separated;
|
|
291
|
+
}
|
|
292
|
+
function isFooterParagraph(block, pageHeight) {
|
|
293
|
+
const inBottomMargin = block.lines.every(
|
|
294
|
+
(line) => line.bounds.y + line.bounds.height <= pageHeight * 0.15
|
|
295
|
+
);
|
|
296
|
+
return inBottomMargin || /^(?:thanks|thank you)\b/i.test(block.text.trim());
|
|
297
|
+
}
|
|
117
298
|
function marginSignatures(page) {
|
|
118
299
|
return page.structured.blocks.flatMap(
|
|
119
300
|
(block) => blockLines(block).filter((line) => isMarginLine(line.bounds.y, line.bounds.height, page.height)).map((line) => furnitureSignature(line.text))
|
|
@@ -143,7 +324,8 @@ function tablesContinue(previous, next, pageWidth) {
|
|
|
143
324
|
function tableHeader(rows) {
|
|
144
325
|
const first = rows[0];
|
|
145
326
|
if (!first) return void 0;
|
|
146
|
-
|
|
327
|
+
const later = rows.slice(1).flat();
|
|
328
|
+
return first.every((value) => /\p{L}/u.test(value) && !isNumericValue(value)) && later.some(isNumericValue) ? first : void 0;
|
|
147
329
|
}
|
|
148
330
|
function sameRow(left, right) {
|
|
149
331
|
return Boolean(
|
|
@@ -152,36 +334,78 @@ function sameRow(left, right) {
|
|
|
152
334
|
}
|
|
153
335
|
function tableRow(row, header) {
|
|
154
336
|
const cell = header ? "th" : "td";
|
|
155
|
-
return `<tr>${row.map((value) => `<${cell}>${
|
|
337
|
+
return `<tr>${row.map((value) => `<${cell}>${escapeHtml2(value)}</${cell}>`).join("")}</tr>`;
|
|
338
|
+
}
|
|
339
|
+
function isFinancialSummary(block) {
|
|
340
|
+
return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
|
|
341
|
+
}
|
|
342
|
+
function isNumericValue(value) {
|
|
343
|
+
return /^(?:\p{Sc}\s*)?[\d.,'’\s]+(?:\s*%)?$/u.test(value.trim());
|
|
344
|
+
}
|
|
345
|
+
function financialSummaryRow(entry, columns) {
|
|
346
|
+
const colspan = columns > 2 ? ` colspan="${columns - 1}"` : "";
|
|
347
|
+
return `<tr><th scope="row"${colspan}>${escapeHtml2(entry.term)}</th><td>${escapeHtml2(entry.description)}</td></tr>`;
|
|
156
348
|
}
|
|
157
|
-
function semanticBlockHtml(block) {
|
|
349
|
+
function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
158
350
|
if (block.type === "heading")
|
|
159
|
-
return `<h${block.level}>${
|
|
160
|
-
if (block.type === "paragraph")
|
|
351
|
+
return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`;
|
|
352
|
+
if (block.type === "paragraph")
|
|
353
|
+
return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
|
|
354
|
+
if (block.type === "preformatted") return `<pre>${escapeHtml2(block.text)}</pre>`;
|
|
161
355
|
if (block.type === "definitionList") {
|
|
162
|
-
|
|
163
|
-
|
|
356
|
+
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()))) {
|
|
357
|
+
return block.entries.map(
|
|
358
|
+
(entry) => `<section><h2>${escapeHtml2(titleCase(entry.term))}</h2><p>${escapeHtml2(entry.description)}</p></section>`
|
|
359
|
+
).join("");
|
|
360
|
+
}
|
|
361
|
+
const list = `<dl>${block.entries.map(
|
|
362
|
+
(entry) => `<div><dt>${escapeHtml2(entry.term)}</dt><dd>${escapeHtml2(entry.description)}</dd></div>`
|
|
164
363
|
).join("")}</dl>`;
|
|
364
|
+
return isFinancialSummary(block) ? `<section>${list}</section>` : list;
|
|
165
365
|
}
|
|
166
366
|
if (block.type === "cardList") {
|
|
167
|
-
return `<
|
|
168
|
-
(item) => `<article><h3>${escapeHtml(item.title)}</h3>${item.details.map((detail) => `<p>${escapeHtml(detail)}</p>`).join("")}</article>`
|
|
169
|
-
).join("")}</div>`;
|
|
367
|
+
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>`;
|
|
170
368
|
}
|
|
171
369
|
if (block.type === "sectionGroup") {
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
|
|
370
|
+
return block.items.map(labeledSectionHtml).join("");
|
|
371
|
+
}
|
|
372
|
+
if (block.type === "employment") {
|
|
373
|
+
return `<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p></section>`;
|
|
175
374
|
}
|
|
176
375
|
const tag = block.ordered ? "ol" : "ul";
|
|
177
|
-
return `<${tag}>${block.items.map((item) => `<li>${
|
|
376
|
+
return `<${tag}>${block.items.map((item) => `<li>${escapeHtml2(item.text)}</li>`).join("")}</${tag}>`;
|
|
377
|
+
}
|
|
378
|
+
function cardTableRow(item) {
|
|
379
|
+
const trailing = item.details.at(-1) ?? "";
|
|
380
|
+
const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
|
|
381
|
+
const description = item.details.slice(0, -1).join(" ");
|
|
382
|
+
const detail = description ? `<br><span>${escapeHtml2(description)}</span>` : "";
|
|
383
|
+
const quantity = match?.[1] ?? "";
|
|
384
|
+
const amount = match?.[2] ?? trailing;
|
|
385
|
+
return `<tr><th scope="row">${escapeHtml2(item.title)}${detail}</th><td>${escapeHtml2(quantity)}</td><td>${escapeHtml2(amount)}</td></tr>`;
|
|
386
|
+
}
|
|
387
|
+
function labeledSectionHtml(item) {
|
|
388
|
+
const heading = titleCase(item.label);
|
|
389
|
+
const postal = /\b(?:ship|deliver|mail)(?:ed)?\b/i.test(item.label);
|
|
390
|
+
if (postal) {
|
|
391
|
+
const [name, ...address] = item.content;
|
|
392
|
+
const content = [name ? `<strong>${escapeHtml2(name)}</strong>` : "", ...address.map(escapeHtml2)].filter(Boolean).join("<br>");
|
|
393
|
+
return `<section><h2>${escapeHtml2(heading)}</h2><address>${content}</address></section>`;
|
|
394
|
+
}
|
|
395
|
+
return `<section><h2>${escapeHtml2(heading)}</h2>${item.content.map(
|
|
396
|
+
(content, index) => `<p>${index === 0 ? `<strong>${escapeHtml2(content)}</strong>` : escapeHtml2(content)}</p>`
|
|
397
|
+
).join("")}</section>`;
|
|
178
398
|
}
|
|
179
|
-
function
|
|
399
|
+
function titleCase(value) {
|
|
400
|
+
const normalized = value.trim().toLocaleLowerCase("en");
|
|
401
|
+
return normalized.replace(/^\p{L}/u, (letter) => letter.toLocaleUpperCase("en"));
|
|
402
|
+
}
|
|
403
|
+
function escapeHtml2(value) {
|
|
180
404
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
181
405
|
}
|
|
182
406
|
|
|
183
407
|
// src/index.ts
|
|
184
|
-
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}`;
|
|
408
|
+
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}`;
|
|
185
409
|
async function writeHtmlDocument(pages, write, options = {}) {
|
|
186
410
|
const includeDocument = options.includeDocument ?? true;
|
|
187
411
|
if (includeDocument) {
|
|
@@ -190,7 +414,7 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
190
414
|
` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
|
|
191
415
|
);
|
|
192
416
|
await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
|
|
193
|
-
await write(`<title>${
|
|
417
|
+
await write(`<title>${escapeHtml3(options.title ?? "PDF document")}</title>`);
|
|
194
418
|
if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
|
|
195
419
|
await write("</head><body>");
|
|
196
420
|
}
|
|
@@ -399,49 +623,58 @@ function positionedSpan(span, fontAliases) {
|
|
|
399
623
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
400
624
|
)
|
|
401
625
|
].join(";");
|
|
402
|
-
return `<span class="pdf-span"${direction} style="${style}">${
|
|
626
|
+
return `<span class="pdf-span"${direction} style="${style}">${escapeHtml3(span.text)}</span>`;
|
|
403
627
|
}
|
|
404
628
|
async function writeFlowPage(page, write) {
|
|
405
629
|
const structured = (0, import_structure2.structurePage)(page);
|
|
630
|
+
const defaultColor = dominantTextColor(structured.lines);
|
|
406
631
|
await write(
|
|
407
632
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
408
633
|
);
|
|
409
634
|
for (const block of structured.blocks) {
|
|
410
635
|
if (block.type === "table") await write((0, import_structure2.tableToHtml)(block.table));
|
|
411
636
|
else if (block.type === "heading") {
|
|
412
|
-
await write(
|
|
637
|
+
await write(
|
|
638
|
+
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
|
|
639
|
+
);
|
|
413
640
|
} else if (block.type === "paragraph") {
|
|
414
641
|
await write(
|
|
415
|
-
`<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${
|
|
642
|
+
`<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`
|
|
416
643
|
);
|
|
644
|
+
} else if (block.type === "preformatted") {
|
|
645
|
+
await write(`<pre>${escapeHtml3(block.text)}</pre>`);
|
|
417
646
|
} else if (block.type === "definitionList") {
|
|
418
647
|
await write("<dl>");
|
|
419
648
|
for (const entry of block.entries) {
|
|
420
649
|
await write(
|
|
421
|
-
`<div><dt>${
|
|
650
|
+
`<div><dt>${escapeHtml3(entry.term)}</dt><dd>${escapeHtml3(entry.description)}</dd></div>`
|
|
422
651
|
);
|
|
423
652
|
}
|
|
424
653
|
await write("</dl>");
|
|
425
654
|
} else if (block.type === "cardList") {
|
|
426
655
|
await write('<div class="pdf-semantic-cards">');
|
|
427
656
|
for (const item of block.items) {
|
|
428
|
-
await write(`<article><h3>${
|
|
429
|
-
for (const detail of item.details) await write(`<p>${
|
|
657
|
+
await write(`<article><h3>${escapeHtml3(item.title)}</h3>`);
|
|
658
|
+
for (const detail of item.details) await write(`<p>${escapeHtml3(detail)}</p>`);
|
|
430
659
|
await write("</article>");
|
|
431
660
|
}
|
|
432
661
|
await write("</div>");
|
|
433
662
|
} else if (block.type === "sectionGroup") {
|
|
434
663
|
await write('<div class="pdf-semantic-sections">');
|
|
435
664
|
for (const item of block.items) {
|
|
436
|
-
await write(`<section><h3>${
|
|
437
|
-
for (const content of item.content) await write(`<p>${
|
|
665
|
+
await write(`<section><h3>${escapeHtml3(item.label)}</h3>`);
|
|
666
|
+
for (const content of item.content) await write(`<p>${escapeHtml3(content)}</p>`);
|
|
438
667
|
await write("</section>");
|
|
439
668
|
}
|
|
440
669
|
await write("</div>");
|
|
670
|
+
} else if (block.type === "employment") {
|
|
671
|
+
await write(
|
|
672
|
+
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`
|
|
673
|
+
);
|
|
441
674
|
} else {
|
|
442
675
|
const tag = block.ordered ? "ol" : "ul";
|
|
443
676
|
await write(`<${tag}>`);
|
|
444
|
-
for (const item of block.items) await write(`<li>${
|
|
677
|
+
for (const item of block.items) await write(`<li>${escapeHtml3(item.text)}</li>`);
|
|
445
678
|
await write(`</${tag}>`);
|
|
446
679
|
}
|
|
447
680
|
}
|
|
@@ -485,7 +718,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
485
718
|
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
486
719
|
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
487
720
|
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number).join(" ")} ${number(anchorX)} ${number(anchorY)})"` : ` x="${number(anchorX)}" y="${number(anchorY)}"`;
|
|
488
|
-
return `<text${direction}${position} font-size="${number(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${
|
|
721
|
+
return `<text${direction}${position} font-size="${number(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml3(span.text)}</text>`;
|
|
489
722
|
}
|
|
490
723
|
function isAdobeCjkFont(fontFamily) {
|
|
491
724
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -602,9 +835,9 @@ function number(value) {
|
|
|
602
835
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
603
836
|
}
|
|
604
837
|
function escapeAttribute(value) {
|
|
605
|
-
return
|
|
838
|
+
return escapeHtml3(value).replaceAll("`", "`");
|
|
606
839
|
}
|
|
607
|
-
function
|
|
840
|
+
function escapeHtml3(value) {
|
|
608
841
|
return [...value].map((character) => {
|
|
609
842
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
610
843
|
if (codePoint === 13) return "\n";
|