@boxpdf/html-writer 0.1.11 → 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 +113 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +113 -45
- 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,
|
|
@@ -37,7 +90,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
37
90
|
};
|
|
38
91
|
const flushPendingParagraph = async () => {
|
|
39
92
|
if (!pendingParagraph) return;
|
|
40
|
-
await write(semanticBlockHtml(pendingParagraph.block));
|
|
93
|
+
await write(semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor));
|
|
41
94
|
pendingParagraph = void 0;
|
|
42
95
|
};
|
|
43
96
|
const closeEmployment = async () => {
|
|
@@ -46,9 +99,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
46
99
|
employmentOpen = false;
|
|
47
100
|
};
|
|
48
101
|
const emitPage = async (page, future) => {
|
|
102
|
+
const defaultColor = dominantTextColor(page.structured.lines);
|
|
49
103
|
const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
|
|
50
104
|
const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
|
|
51
|
-
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];
|
|
52
107
|
if (isRepeatedFurniture(block, page, repeatedFurniture)) {
|
|
53
108
|
stats.suppressedFurniture += 1;
|
|
54
109
|
continue;
|
|
@@ -56,19 +111,23 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
56
111
|
await flushPendingParagraph();
|
|
57
112
|
if (employmentOpen && block.type !== "list") await closeEmployment();
|
|
58
113
|
if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
|
|
59
|
-
await write(`<header><h1>${
|
|
114
|
+
await write(`<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor)}</h1>`);
|
|
60
115
|
headerOpen = true;
|
|
61
116
|
continue;
|
|
62
117
|
}
|
|
63
118
|
if (headerOpen) {
|
|
64
119
|
if (block.type === "paragraph") {
|
|
65
120
|
const tag = isContactBlock(block) ? "address" : "p";
|
|
66
|
-
await write(
|
|
121
|
+
await write(
|
|
122
|
+
`<${tag}>${semanticTextHtml(block.text, block.lines, defaultColor)}</${tag}>`
|
|
123
|
+
);
|
|
67
124
|
headerHasParagraph = true;
|
|
68
125
|
continue;
|
|
69
126
|
}
|
|
70
|
-
if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#"))) {
|
|
71
|
-
await write(
|
|
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
|
+
);
|
|
72
131
|
continue;
|
|
73
132
|
}
|
|
74
133
|
await write("</header>");
|
|
@@ -105,7 +164,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
105
164
|
const level = contentStarted && block.level === 1 ? 2 : block.level;
|
|
106
165
|
await closeSections(level);
|
|
107
166
|
await write(
|
|
108
|
-
`<section data-level="${level}"><h${level}>${
|
|
167
|
+
`<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${level}>`
|
|
109
168
|
);
|
|
110
169
|
sectionLevels.push(level);
|
|
111
170
|
continue;
|
|
@@ -113,27 +172,27 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
113
172
|
if (block.type === "paragraph") {
|
|
114
173
|
if (isTitledRecord(block)) {
|
|
115
174
|
const [institution, ...details] = block.lines;
|
|
116
|
-
if (institution) await write(`<h3>${
|
|
117
|
-
for (const detail of details) await write(`<p>${
|
|
175
|
+
if (institution) await write(`<h3>${escapeHtml2(institution.text)}</h3>`);
|
|
176
|
+
for (const detail of details) await write(`<p>${escapeHtml2(detail.text)}</p>`);
|
|
118
177
|
continue;
|
|
119
178
|
}
|
|
120
179
|
if (isUnmarkedList(block)) {
|
|
121
180
|
await write(
|
|
122
|
-
`<ul>${block.lines.map((line) => `<li>${
|
|
181
|
+
`<ul>${block.lines.map((line) => `<li>${escapeHtml2(line.text)}</li>`).join("")}</ul>`
|
|
123
182
|
);
|
|
124
183
|
continue;
|
|
125
184
|
}
|
|
126
|
-
pendingParagraph = { block, height: page.height };
|
|
185
|
+
pendingParagraph = { block, height: page.height, defaultColor };
|
|
127
186
|
continue;
|
|
128
187
|
}
|
|
129
188
|
if (block.type === "employment") {
|
|
130
189
|
await write(
|
|
131
|
-
`<section><h3>${
|
|
190
|
+
`<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p>`
|
|
132
191
|
);
|
|
133
192
|
employmentOpen = true;
|
|
134
193
|
continue;
|
|
135
194
|
}
|
|
136
|
-
await write(semanticBlockHtml(block));
|
|
195
|
+
await write(semanticBlockHtml(block, defaultColor));
|
|
137
196
|
}
|
|
138
197
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
139
198
|
};
|
|
@@ -160,7 +219,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
160
219
|
await closeEmployment();
|
|
161
220
|
if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
|
|
162
221
|
await closeSections();
|
|
163
|
-
await write(
|
|
222
|
+
await write(
|
|
223
|
+
`<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`
|
|
224
|
+
);
|
|
164
225
|
pendingParagraph = void 0;
|
|
165
226
|
} else {
|
|
166
227
|
await flushPendingParagraph();
|
|
@@ -250,7 +311,7 @@ function sameRow(left, right) {
|
|
|
250
311
|
}
|
|
251
312
|
function tableRow(row, header) {
|
|
252
313
|
const cell = header ? "th" : "td";
|
|
253
|
-
return `<tr>${row.map((value) => `<${cell}>${
|
|
314
|
+
return `<tr>${row.map((value) => `<${cell}>${escapeHtml2(value)}</${cell}>`).join("")}</tr>`;
|
|
254
315
|
}
|
|
255
316
|
function isFinancialSummary(block) {
|
|
256
317
|
return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
|
|
@@ -260,20 +321,22 @@ function isNumericValue(value) {
|
|
|
260
321
|
}
|
|
261
322
|
function financialSummaryRow(entry, columns) {
|
|
262
323
|
const colspan = columns > 2 ? ` colspan="${columns - 1}"` : "";
|
|
263
|
-
return `<tr><th scope="row"${colspan}>${
|
|
324
|
+
return `<tr><th scope="row"${colspan}>${escapeHtml2(entry.term)}</th><td>${escapeHtml2(entry.description)}</td></tr>`;
|
|
264
325
|
}
|
|
265
|
-
function semanticBlockHtml(block) {
|
|
326
|
+
function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
266
327
|
if (block.type === "heading")
|
|
267
|
-
return `<h${block.level}>${
|
|
268
|
-
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>`;
|
|
269
332
|
if (block.type === "definitionList") {
|
|
270
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()))) {
|
|
271
334
|
return block.entries.map(
|
|
272
|
-
(entry) => `<section><h2>${
|
|
335
|
+
(entry) => `<section><h2>${escapeHtml2(titleCase(entry.term))}</h2><p>${escapeHtml2(entry.description)}</p></section>`
|
|
273
336
|
).join("");
|
|
274
337
|
}
|
|
275
338
|
const list = `<dl>${block.entries.map(
|
|
276
|
-
(entry) => `<div><dt>${
|
|
339
|
+
(entry) => `<div><dt>${escapeHtml2(entry.term)}</dt><dd>${escapeHtml2(entry.description)}</dd></div>`
|
|
277
340
|
).join("")}</dl>`;
|
|
278
341
|
return isFinancialSummary(block) ? `<section>${list}</section>` : list;
|
|
279
342
|
}
|
|
@@ -284,42 +347,42 @@ function semanticBlockHtml(block) {
|
|
|
284
347
|
return block.items.map(labeledSectionHtml).join("");
|
|
285
348
|
}
|
|
286
349
|
if (block.type === "employment") {
|
|
287
|
-
return `<section><h3>${
|
|
350
|
+
return `<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p></section>`;
|
|
288
351
|
}
|
|
289
352
|
const tag = block.ordered ? "ol" : "ul";
|
|
290
|
-
return `<${tag}>${block.items.map((item) => `<li>${
|
|
353
|
+
return `<${tag}>${block.items.map((item) => `<li>${escapeHtml2(item.text)}</li>`).join("")}</${tag}>`;
|
|
291
354
|
}
|
|
292
355
|
function cardTableRow(item) {
|
|
293
356
|
const trailing = item.details.at(-1) ?? "";
|
|
294
357
|
const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
|
|
295
358
|
const description = item.details.slice(0, -1).join(" ");
|
|
296
|
-
const detail = description ? `<br><span>${
|
|
359
|
+
const detail = description ? `<br><span>${escapeHtml2(description)}</span>` : "";
|
|
297
360
|
const quantity = match?.[1] ?? "";
|
|
298
361
|
const amount = match?.[2] ?? trailing;
|
|
299
|
-
return `<tr><th scope="row">${
|
|
362
|
+
return `<tr><th scope="row">${escapeHtml2(item.title)}${detail}</th><td>${escapeHtml2(quantity)}</td><td>${escapeHtml2(amount)}</td></tr>`;
|
|
300
363
|
}
|
|
301
364
|
function labeledSectionHtml(item) {
|
|
302
365
|
const heading = titleCase(item.label);
|
|
303
366
|
const postal = /\b(?:ship|deliver|mail)(?:ed)?\b/i.test(item.label);
|
|
304
367
|
if (postal) {
|
|
305
368
|
const [name, ...address] = item.content;
|
|
306
|
-
const content = [name ? `<strong>${
|
|
307
|
-
return `<section><h2>${
|
|
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>`;
|
|
308
371
|
}
|
|
309
|
-
return `<section><h2>${
|
|
310
|
-
(content, index) => `<p>${index === 0 ? `<strong>${
|
|
372
|
+
return `<section><h2>${escapeHtml2(heading)}</h2>${item.content.map(
|
|
373
|
+
(content, index) => `<p>${index === 0 ? `<strong>${escapeHtml2(content)}</strong>` : escapeHtml2(content)}</p>`
|
|
311
374
|
).join("")}</section>`;
|
|
312
375
|
}
|
|
313
376
|
function titleCase(value) {
|
|
314
377
|
const normalized = value.trim().toLocaleLowerCase("en");
|
|
315
378
|
return normalized.replace(/^\p{L}/u, (letter) => letter.toLocaleUpperCase("en"));
|
|
316
379
|
}
|
|
317
|
-
function
|
|
380
|
+
function escapeHtml2(value) {
|
|
318
381
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
319
382
|
}
|
|
320
383
|
|
|
321
384
|
// src/index.ts
|
|
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}`;
|
|
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}`;
|
|
323
386
|
async function writeHtmlDocument(pages, write, options = {}) {
|
|
324
387
|
const includeDocument = options.includeDocument ?? true;
|
|
325
388
|
if (includeDocument) {
|
|
@@ -328,7 +391,7 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
328
391
|
` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
|
|
329
392
|
);
|
|
330
393
|
await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
|
|
331
|
-
await write(`<title>${
|
|
394
|
+
await write(`<title>${escapeHtml3(options.title ?? "PDF document")}</title>`);
|
|
332
395
|
if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
|
|
333
396
|
await write("</head><body>");
|
|
334
397
|
}
|
|
@@ -537,53 +600,58 @@ function positionedSpan(span, fontAliases) {
|
|
|
537
600
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
538
601
|
)
|
|
539
602
|
].join(";");
|
|
540
|
-
return `<span class="pdf-span"${direction} style="${style}">${
|
|
603
|
+
return `<span class="pdf-span"${direction} style="${style}">${escapeHtml3(span.text)}</span>`;
|
|
541
604
|
}
|
|
542
605
|
async function writeFlowPage(page, write) {
|
|
543
606
|
const structured = structurePage2(page);
|
|
607
|
+
const defaultColor = dominantTextColor(structured.lines);
|
|
544
608
|
await write(
|
|
545
609
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
546
610
|
);
|
|
547
611
|
for (const block of structured.blocks) {
|
|
548
612
|
if (block.type === "table") await write(tableToHtml(block.table));
|
|
549
613
|
else if (block.type === "heading") {
|
|
550
|
-
await write(
|
|
614
|
+
await write(
|
|
615
|
+
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
|
|
616
|
+
);
|
|
551
617
|
} else if (block.type === "paragraph") {
|
|
552
618
|
await write(
|
|
553
|
-
`<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${
|
|
619
|
+
`<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`
|
|
554
620
|
);
|
|
621
|
+
} else if (block.type === "preformatted") {
|
|
622
|
+
await write(`<pre>${escapeHtml3(block.text)}</pre>`);
|
|
555
623
|
} else if (block.type === "definitionList") {
|
|
556
624
|
await write("<dl>");
|
|
557
625
|
for (const entry of block.entries) {
|
|
558
626
|
await write(
|
|
559
|
-
`<div><dt>${
|
|
627
|
+
`<div><dt>${escapeHtml3(entry.term)}</dt><dd>${escapeHtml3(entry.description)}</dd></div>`
|
|
560
628
|
);
|
|
561
629
|
}
|
|
562
630
|
await write("</dl>");
|
|
563
631
|
} else if (block.type === "cardList") {
|
|
564
632
|
await write('<div class="pdf-semantic-cards">');
|
|
565
633
|
for (const item of block.items) {
|
|
566
|
-
await write(`<article><h3>${
|
|
567
|
-
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>`);
|
|
568
636
|
await write("</article>");
|
|
569
637
|
}
|
|
570
638
|
await write("</div>");
|
|
571
639
|
} else if (block.type === "sectionGroup") {
|
|
572
640
|
await write('<div class="pdf-semantic-sections">');
|
|
573
641
|
for (const item of block.items) {
|
|
574
|
-
await write(`<section><h3>${
|
|
575
|
-
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>`);
|
|
576
644
|
await write("</section>");
|
|
577
645
|
}
|
|
578
646
|
await write("</div>");
|
|
579
647
|
} else if (block.type === "employment") {
|
|
580
648
|
await write(
|
|
581
|
-
`<section><h3>${
|
|
649
|
+
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`
|
|
582
650
|
);
|
|
583
651
|
} else {
|
|
584
652
|
const tag = block.ordered ? "ol" : "ul";
|
|
585
653
|
await write(`<${tag}>`);
|
|
586
|
-
for (const item of block.items) await write(`<li>${
|
|
654
|
+
for (const item of block.items) await write(`<li>${escapeHtml3(item.text)}</li>`);
|
|
587
655
|
await write(`</${tag}>`);
|
|
588
656
|
}
|
|
589
657
|
}
|
|
@@ -627,7 +695,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
627
695
|
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
628
696
|
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
629
697
|
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number).join(" ")} ${number(anchorX)} ${number(anchorY)})"` : ` x="${number(anchorX)}" y="${number(anchorY)}"`;
|
|
630
|
-
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>`;
|
|
631
699
|
}
|
|
632
700
|
function isAdobeCjkFont(fontFamily) {
|
|
633
701
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -744,9 +812,9 @@ function number(value) {
|
|
|
744
812
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
745
813
|
}
|
|
746
814
|
function escapeAttribute(value) {
|
|
747
|
-
return
|
|
815
|
+
return escapeHtml3(value).replaceAll("`", "`");
|
|
748
816
|
}
|
|
749
|
-
function
|
|
817
|
+
function escapeHtml3(value) {
|
|
750
818
|
return [...value].map((character) => {
|
|
751
819
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
752
820
|
if (codePoint === 13) return "\n";
|