@boxpdf/html-writer 0.1.14 → 0.1.15

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
@@ -58,6 +58,52 @@ function escapeHtml(value) {
58
58
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
59
59
  }
60
60
 
61
+ // src/visual-font.ts
62
+ function visualFontAliases(pageNumber, fonts) {
63
+ return new Map(
64
+ fonts.filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${pageNumber}-${font.id}`])
65
+ );
66
+ }
67
+ function visualFontFace(font, aliases) {
68
+ if (font.format !== "truetype") return "";
69
+ const alias = aliases.get(font.id);
70
+ if (!alias) return "";
71
+ const styles2 = visualFontStyles(font.family, alias).filter(
72
+ (style) => !style.startsWith("font-family:")
73
+ );
74
+ return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base64(font.data)}) format("truetype");${styles2.join(";")}}`;
75
+ }
76
+ function visualFontStyles(fontFamily, alias) {
77
+ const normalized = fontFamily?.toLowerCase() ?? "";
78
+ const styles2 = [];
79
+ let fallback;
80
+ if (/courier|mono|nimbusmono|^cmtt/.test(normalized)) {
81
+ fallback = "Courier New,Courier,monospace";
82
+ } else if (/times|minion|serif|baskerville|georgia|nimbusrom|guardian.*egyp|^cm[rs]y?\d/.test(normalized)) {
83
+ fallback = "Times New Roman,Times,serif";
84
+ } else if (/helvetica|arial|sans|nimbussan|calibre|myriad|panton|^tte|^mstt/.test(normalized)) {
85
+ fallback = "Arial,Helvetica,sans-serif";
86
+ }
87
+ if (alias || fallback) styles2.push(`font-family:${[alias, fallback].filter(Boolean).join(",")}`);
88
+ if (/bold|black|semibold|demi|medi|^tte/.test(normalized)) styles2.push("font-weight:700");
89
+ if (/italic|oblique|slant|ital(?:$|[_-])/.test(normalized)) styles2.push("font-style:italic");
90
+ return styles2;
91
+ }
92
+ function base64(bytes) {
93
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
94
+ let output = "";
95
+ for (let index = 0; index < bytes.length; index += 3) {
96
+ const first = bytes[index] ?? 0;
97
+ const second = bytes[index + 1] ?? 0;
98
+ const third = bytes[index + 2] ?? 0;
99
+ output += alphabet[first >> 2];
100
+ output += alphabet[(first & 3) << 4 | second >> 4];
101
+ output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
102
+ output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
103
+ }
104
+ return output;
105
+ }
106
+
61
107
  // src/semantic-media.ts
62
108
  function semanticMedia(page) {
63
109
  const output = (page.images ?? []).map((image) => rasterMedia(image));
@@ -83,12 +129,51 @@ function vectorMedia(page) {
83
129
  ...fills.map(fillBounds)
84
130
  ]);
85
131
  if (!bounds || bounds.width <= 0 || bounds.height <= 0) return void 0;
86
- const content = fills.map(vectorFill).join("") + paths.map((path) => vectorPath(path)).join("");
132
+ const aliases = visualFontAliases(page.number, page.fonts ?? []);
133
+ const visualCodeFonts = new Set(
134
+ (page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
135
+ );
136
+ const visualSpans = page.visualSpans ?? page.spans;
137
+ const overlay = visualSpans.filter(
138
+ (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
139
+ );
140
+ const consumedSpans = page.spans.filter(
141
+ (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
142
+ );
143
+ const fontIds = new Set(overlay.map((span) => span.fontAssetId));
144
+ const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
87
145
  return {
88
146
  bounds,
89
- html: `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number(bounds.x)} ${number(page.height - bounds.y - bounds.height)} ${number(bounds.width)} ${number(bounds.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true"><g transform="translate(0 ${number(page.height)}) scale(1 -1)">${content}</g></svg>`
147
+ html: `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number(bounds.x)} ${number(page.height - bounds.y - bounds.height)} ${number(bounds.width)} ${number(bounds.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true">${fontFaces ? `<style>${fontFaces}</style>` : ""}<g transform="translate(0 ${number(page.height)}) scale(1 -1)">${fills.map(vectorFill).join("") + paths.map((path) => vectorPath(path)).join("")}</g>${overlay.map((span) => vectorText(span, page.height, aliases)).join("")}</svg>`,
148
+ ...consumedSpans.length > 0 ? { consumedSpans } : {}
90
149
  };
91
150
  }
151
+ function withoutSemanticMediaSpans(page, media) {
152
+ const consumed = new Set(media.flatMap((item) => item.consumedSpans ?? []));
153
+ return consumed.size > 0 ? { ...page, spans: page.spans.filter((span) => !consumed.has(span)) } : page;
154
+ }
155
+ function vectorText(span, pageHeight, aliases) {
156
+ if (span.renderingMode === 3 || span.renderingMode === 7) return "";
157
+ const styles2 = [
158
+ cssColor(span.color) ? `fill:${span.color}` : "",
159
+ unitInterval(span.fillOpacity) ? `fill-opacity:${number(span.fillOpacity)}` : "",
160
+ ...visualFontStyles(
161
+ span.fontFamily,
162
+ span.fontAssetId ? aliases.get(span.fontAssetId) : void 0
163
+ )
164
+ ].filter(Boolean).join(";");
165
+ const anchorY = pageHeight - span.bounds.y;
166
+ const transform = span.transform;
167
+ const position = transform ? ` x="0" y="0" transform="matrix(${transform.map(number).join(" ")} ${number(span.bounds.x)} ${number(anchorY)})"` : ` x="${number(span.bounds.x)}" y="${number(anchorY)}"`;
168
+ const extent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
169
+ const length = extent > 0 ? ` textLength="${number(extent)}" lengthAdjust="spacingAndGlyphs"` : "";
170
+ return `<text${position} font-size="${number(span.fontSize)}"${length}${styles2 ? ` style="${styles2}"` : ""}>${escapeHtml2(span.text)}</text>`;
171
+ }
172
+ function centerInside(inner, outer) {
173
+ const x = inner.x + inner.width / 2;
174
+ const y = inner.y + inner.height / 2;
175
+ return x >= outer.x && x <= outer.x + outer.width && y >= outer.y && y <= outer.y + outer.height;
176
+ }
92
177
  function vectorFill(fill) {
93
178
  const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
94
179
  const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
@@ -174,20 +259,6 @@ function rgbBmp(image) {
174
259
  }
175
260
  return output;
176
261
  }
177
- function base64(bytes) {
178
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
179
- let output = "";
180
- for (let index = 0; index < bytes.length; index += 3) {
181
- const first = bytes[index] ?? 0;
182
- const second = bytes[index + 1] ?? 0;
183
- const third = bytes[index + 2] ?? 0;
184
- output += alphabet[first >> 2];
185
- output += alphabet[(first & 3) << 4 | second >> 4];
186
- output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
187
- output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
188
- }
189
- return output;
190
- }
191
262
  function safePath(value) {
192
263
  return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
193
264
  }
@@ -203,6 +274,9 @@ function unitInterval(value) {
203
274
  function number(value) {
204
275
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
205
276
  }
277
+ function escapeHtml2(value) {
278
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
279
+ }
206
280
 
207
281
  // src/semantic-document.ts
208
282
  async function writeSemanticDocument(pages, write, lookaheadPages) {
@@ -329,13 +403,13 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
329
403
  if (block.type === "paragraph") {
330
404
  if (isTitledRecord(block)) {
331
405
  const [institution, ...details] = block.lines;
332
- if (institution) await write(`<h3>${escapeHtml2(institution.text)}</h3>`);
333
- for (const detail of details) await write(`<p>${escapeHtml2(detail.text)}</p>`);
406
+ if (institution) await write(`<h3>${escapeHtml3(institution.text)}</h3>`);
407
+ for (const detail of details) await write(`<p>${escapeHtml3(detail.text)}</p>`);
334
408
  continue;
335
409
  }
336
410
  if (isUnmarkedList(block)) {
337
411
  await write(
338
- `<ul>${block.lines.map((line) => `<li>${escapeHtml2(line.text)}</li>`).join("")}</ul>`
412
+ `<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`
339
413
  );
340
414
  continue;
341
415
  }
@@ -344,7 +418,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
344
418
  }
345
419
  if (block.type === "employment") {
346
420
  await write(
347
- `<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p>`
421
+ `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`
348
422
  );
349
423
  employmentOpen = true;
350
424
  continue;
@@ -361,8 +435,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
361
435
  for (const signature of marginSignatures(page)) seenFurniture.add(signature);
362
436
  };
363
437
  for await (const page of pages) {
364
- const structured = structurePage(page);
365
- buffer.push({ width: page.width, height: page.height, structured, media: semanticMedia(page) });
438
+ const media = semanticMedia(page);
439
+ const structured = structurePage(withoutSemanticMediaSpans(page, media));
440
+ buffer.push({ width: page.width, height: page.height, structured, media });
366
441
  stats.pagesProcessed += 1;
367
442
  stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
368
443
  stats.peakBufferedLines = Math.max(
@@ -475,7 +550,7 @@ function sameRow(left, right) {
475
550
  }
476
551
  function tableRow(row, header) {
477
552
  const cell = header ? "th" : "td";
478
- return `<tr>${row.map((value) => `<${cell}>${escapeHtml2(value)}</${cell}>`).join("")}</tr>`;
553
+ return `<tr>${row.map((value) => `<${cell}>${escapeHtml3(value)}</${cell}>`).join("")}</tr>`;
479
554
  }
480
555
  function isFinancialSummary(block) {
481
556
  return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
@@ -485,22 +560,22 @@ function isNumericValue(value) {
485
560
  }
486
561
  function financialSummaryRow(entry, columns) {
487
562
  const colspan = columns > 2 ? ` colspan="${columns - 1}"` : "";
488
- return `<tr><th scope="row"${colspan}>${escapeHtml2(entry.term)}</th><td>${escapeHtml2(entry.description)}</td></tr>`;
563
+ return `<tr><th scope="row"${colspan}>${escapeHtml3(entry.term)}</th><td>${escapeHtml3(entry.description)}</td></tr>`;
489
564
  }
490
565
  function semanticBlockHtml(block, defaultColor = "#000000") {
491
566
  if (block.type === "heading")
492
567
  return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`;
493
568
  if (block.type === "paragraph")
494
569
  return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
495
- if (block.type === "preformatted") return `<pre>${escapeHtml2(block.text)}</pre>`;
570
+ if (block.type === "preformatted") return `<pre>${escapeHtml3(block.text)}</pre>`;
496
571
  if (block.type === "definitionList") {
497
572
  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()))) {
498
573
  return block.entries.map(
499
- (entry) => `<section><h2>${escapeHtml2(titleCase(entry.term))}</h2><p>${escapeHtml2(entry.description)}</p></section>`
574
+ (entry) => `<section><h2>${escapeHtml3(titleCase(entry.term))}</h2><p>${escapeHtml3(entry.description)}</p></section>`
500
575
  ).join("");
501
576
  }
502
577
  const list = `<dl>${block.entries.map(
503
- (entry) => `<div><dt>${escapeHtml2(entry.term)}</dt><dd>${escapeHtml2(entry.description)}</dd></div>`
578
+ (entry) => `<div><dt>${escapeHtml3(entry.term)}</dt><dd>${escapeHtml3(entry.description)}</dd></div>`
504
579
  ).join("")}</dl>`;
505
580
  return isFinancialSummary(block) ? `<section>${list}</section>` : list;
506
581
  }
@@ -511,10 +586,10 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
511
586
  return block.items.map(labeledSectionHtml).join("");
512
587
  }
513
588
  if (block.type === "employment") {
514
- return `<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p></section>`;
589
+ return `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`;
515
590
  }
516
591
  const tag = block.ordered ? "ol" : "ul";
517
- return `<${tag}>${block.items.map((item) => `<li>${escapeHtml2(item.text)}</li>`).join("")}</${tag}>`;
592
+ return `<${tag}>${block.items.map((item) => `<li>${escapeHtml3(item.text)}</li>`).join("")}</${tag}>`;
518
593
  }
519
594
  function semanticBlockY(block) {
520
595
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
@@ -524,28 +599,28 @@ function cardTableRow(item) {
524
599
  const trailing = item.details.at(-1) ?? "";
525
600
  const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
526
601
  const description = item.details.slice(0, -1).join(" ");
527
- const detail = description ? `<br><span>${escapeHtml2(description)}</span>` : "";
602
+ const detail = description ? `<br><span>${escapeHtml3(description)}</span>` : "";
528
603
  const quantity = match?.[1] ?? "";
529
604
  const amount = match?.[2] ?? trailing;
530
- return `<tr><th scope="row">${escapeHtml2(item.title)}${detail}</th><td>${escapeHtml2(quantity)}</td><td>${escapeHtml2(amount)}</td></tr>`;
605
+ return `<tr><th scope="row">${escapeHtml3(item.title)}${detail}</th><td>${escapeHtml3(quantity)}</td><td>${escapeHtml3(amount)}</td></tr>`;
531
606
  }
532
607
  function labeledSectionHtml(item) {
533
608
  const heading = titleCase(item.label);
534
609
  const postal = /\b(?:ship|deliver|mail)(?:ed)?\b/i.test(item.label);
535
610
  if (postal) {
536
611
  const [name, ...address] = item.content;
537
- const content = [name ? `<strong>${escapeHtml2(name)}</strong>` : "", ...address.map(escapeHtml2)].filter(Boolean).join("<br>");
538
- return `<section><h2>${escapeHtml2(heading)}</h2><address>${content}</address></section>`;
612
+ const content = [name ? `<strong>${escapeHtml3(name)}</strong>` : "", ...address.map(escapeHtml3)].filter(Boolean).join("<br>");
613
+ return `<section><h2>${escapeHtml3(heading)}</h2><address>${content}</address></section>`;
539
614
  }
540
- return `<section><h2>${escapeHtml2(heading)}</h2>${item.content.map(
541
- (content, index) => `<p>${index === 0 ? `<strong>${escapeHtml2(content)}</strong>` : escapeHtml2(content)}</p>`
615
+ return `<section><h2>${escapeHtml3(heading)}</h2>${item.content.map(
616
+ (content, index) => `<p>${index === 0 ? `<strong>${escapeHtml3(content)}</strong>` : escapeHtml3(content)}</p>`
542
617
  ).join("")}</section>`;
543
618
  }
544
619
  function titleCase(value) {
545
620
  const normalized = value.trim().toLocaleLowerCase("en");
546
621
  return normalized.replace(/^\p{L}/u, (letter) => letter.toLocaleUpperCase("en"));
547
622
  }
548
- function escapeHtml2(value) {
623
+ function escapeHtml3(value) {
549
624
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
550
625
  }
551
626
 
@@ -559,7 +634,7 @@ async function writeHtmlDocument(pages, write, options = {}) {
559
634
  ` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
560
635
  );
561
636
  await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
562
- await write(`<title>${escapeHtml3(options.title ?? "PDF document")}</title>`);
637
+ await write(`<title>${escapeHtml4(options.title ?? "PDF document")}</title>`);
563
638
  if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
564
639
  await write("</head><body>");
565
640
  }
@@ -605,14 +680,14 @@ async function writePositionedPage(page, write, options) {
605
680
  await write(
606
681
  `<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number2(displayWidth)}pt;height:${number2(displayHeight)}pt">`
607
682
  );
608
- const fontAliases = new Map(
609
- (page.fonts ?? []).filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${page.number}-${font.id}`])
610
- );
683
+ const fontAliases = visualFontAliases(page.number, page.fonts ?? []);
611
684
  const type3Fonts = new Map(
612
685
  (page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
613
686
  );
614
687
  if ((options.includeStyles ?? true) && page.fonts?.length) {
615
- await write(`<style>${page.fonts.map((font) => fontFace(font, fontAliases)).join("")}</style>`);
688
+ await write(
689
+ `<style>${page.fonts.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
690
+ );
616
691
  }
617
692
  await write(
618
693
  `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number2(page.width)}pt;height:${number2(page.height)}pt${rotationTransform(page)}">`
@@ -686,7 +761,7 @@ function visualImage(image, pageHeight, pageNumber, imageIndex) {
686
761
  const opacity = isUnitInterval(image.opacity) ? ` opacity="${number2(image.opacity)}"` : "";
687
762
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
688
763
  const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
689
- let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base642(data)}"${opacity}/>`;
764
+ let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
690
765
  for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
691
766
  output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
692
767
  }
@@ -763,17 +838,17 @@ function positionedSpan(span, fontAliases) {
763
838
  `font-size:${number2(span.fontSize)}pt`,
764
839
  ...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
765
840
  ...isUnitInterval(span.fillOpacity) ? [`opacity:${number2(span.fillOpacity)}`] : [],
766
- ...fontStyles(
841
+ ...visualFontStyles(
767
842
  span.fontFamily,
768
843
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
769
844
  )
770
845
  ].join(";");
771
- return `<span class="pdf-span"${direction} style="${style}">${escapeHtml3(span.text)}</span>`;
846
+ return `<span class="pdf-span"${direction} style="${style}">${escapeHtml4(span.text)}</span>`;
772
847
  }
773
848
  async function writeFlowPage(page, write) {
774
- const structured = structurePage2(page);
775
- const defaultColor = dominantTextColor(structured.lines);
776
849
  const media = semanticMedia(page);
850
+ const structured = structurePage2(withoutSemanticMediaSpans(page, media));
851
+ const defaultColor = dominantTextColor(structured.lines);
777
852
  let mediaIndex = 0;
778
853
  await write(
779
854
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
@@ -794,39 +869,39 @@ async function writeFlowPage(page, write) {
794
869
  `<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`
795
870
  );
796
871
  } else if (block.type === "preformatted") {
797
- await write(`<pre>${escapeHtml3(block.text)}</pre>`);
872
+ await write(`<pre>${escapeHtml4(block.text)}</pre>`);
798
873
  } else if (block.type === "definitionList") {
799
874
  await write("<dl>");
800
875
  for (const entry of block.entries) {
801
876
  await write(
802
- `<div><dt>${escapeHtml3(entry.term)}</dt><dd>${escapeHtml3(entry.description)}</dd></div>`
877
+ `<div><dt>${escapeHtml4(entry.term)}</dt><dd>${escapeHtml4(entry.description)}</dd></div>`
803
878
  );
804
879
  }
805
880
  await write("</dl>");
806
881
  } else if (block.type === "cardList") {
807
882
  await write('<div class="pdf-semantic-cards">');
808
883
  for (const item of block.items) {
809
- await write(`<article><h3>${escapeHtml3(item.title)}</h3>`);
810
- for (const detail of item.details) await write(`<p>${escapeHtml3(detail)}</p>`);
884
+ await write(`<article><h3>${escapeHtml4(item.title)}</h3>`);
885
+ for (const detail of item.details) await write(`<p>${escapeHtml4(detail)}</p>`);
811
886
  await write("</article>");
812
887
  }
813
888
  await write("</div>");
814
889
  } else if (block.type === "sectionGroup") {
815
890
  await write('<div class="pdf-semantic-sections">');
816
891
  for (const item of block.items) {
817
- await write(`<section><h3>${escapeHtml3(item.label)}</h3>`);
818
- for (const content of item.content) await write(`<p>${escapeHtml3(content)}</p>`);
892
+ await write(`<section><h3>${escapeHtml4(item.label)}</h3>`);
893
+ for (const content of item.content) await write(`<p>${escapeHtml4(content)}</p>`);
819
894
  await write("</section>");
820
895
  }
821
896
  await write("</div>");
822
897
  } else if (block.type === "employment") {
823
898
  await write(
824
- `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`
899
+ `<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`
825
900
  );
826
901
  } else {
827
902
  const tag = block.ordered ? "ol" : "ul";
828
903
  await write(`<${tag}>`);
829
- for (const item of block.items) await write(`<li>${escapeHtml3(item.text)}</li>`);
904
+ for (const item of block.items) await write(`<li>${escapeHtml4(item.text)}</li>`);
830
905
  await write(`</${tag}>`);
831
906
  }
832
907
  }
@@ -844,7 +919,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
844
919
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
845
920
  if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
846
921
  const direction = directionAttribute([span]);
847
- const font = fontStyles(
922
+ const font = visualFontStyles(
848
923
  span.fontFamily,
849
924
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
850
925
  ).join(";");
@@ -878,7 +953,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
878
953
  const anchorX = span.bounds.x + basisX * rtlOffset;
879
954
  const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
880
955
  const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
881
- return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml3(span.text)}</text>`;
956
+ return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
882
957
  }
883
958
  function isAdobeCjkFont(fontFamily) {
884
959
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -933,24 +1008,6 @@ function isUnitInterval(value) {
933
1008
  function isSvgPath(value) {
934
1009
  return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
935
1010
  }
936
- function fontStyles(fontFamily, alias) {
937
- const normalized = fontFamily?.toLowerCase() ?? "";
938
- const styles2 = [];
939
- let fallback;
940
- if (/courier|mono|nimbusmono|^cmtt/.test(normalized)) {
941
- fallback = "Courier New,Courier,monospace";
942
- } else if (/times|minion|serif|baskerville|georgia|nimbusrom|guardian.*egyp|^cm[rs]y?\d/.test(normalized)) {
943
- fallback = "Times New Roman,Times,serif";
944
- } else if (/helvetica|arial|sans|nimbussan|calibre|myriad|panton|^tte/.test(normalized)) {
945
- fallback = "Arial,Helvetica,sans-serif";
946
- } else if (/^mstt/.test(normalized)) {
947
- fallback = "Arial,Helvetica,sans-serif";
948
- }
949
- if (alias || fallback) styles2.push(`font-family:${[alias, fallback].filter(Boolean).join(",")}`);
950
- if (/bold|black|semibold|demi|medi|^tte/.test(normalized)) styles2.push("font-weight:700");
951
- if (/italic|oblique|slant|ital(?:$|[_-])/.test(normalized)) styles2.push("font-style:italic");
952
- return styles2;
953
- }
954
1011
  function isMonospace(fontFamily) {
955
1012
  return /courier|mono/i.test(fontFamily ?? "");
956
1013
  }
@@ -962,29 +1019,6 @@ function hasNonIdentityTransform(transform) {
962
1019
  const identity = [1, 0, 0, 1];
963
1020
  return transform.some((value, index) => Math.abs(value - (identity[index] ?? 0)) > 1e-6);
964
1021
  }
965
- function fontFace(font, aliases) {
966
- if (font.format !== "truetype") return "";
967
- const alias = aliases.get(font.id);
968
- if (!alias) return "";
969
- const styles2 = fontStyles(font.family, alias).filter(
970
- (style) => !style.startsWith("font-family:")
971
- );
972
- return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base642(font.data)}) format("truetype");${styles2.join(";")}}`;
973
- }
974
- function base642(bytes) {
975
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
976
- let output = "";
977
- for (let index = 0; index < bytes.length; index += 3) {
978
- const first = bytes[index] ?? 0;
979
- const second = bytes[index + 1] ?? 0;
980
- const third = bytes[index + 2] ?? 0;
981
- output += alphabet[first >> 2];
982
- output += alphabet[(first & 3) << 4 | second >> 4];
983
- output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
984
- output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
985
- }
986
- return output;
987
- }
988
1022
  function directionAttribute(spans) {
989
1023
  const rtl = spans.filter((span) => span.direction === "rtl").length;
990
1024
  const vertical = spans.filter((span) => span.direction === "ttb").length;
@@ -995,9 +1029,9 @@ function number2(value) {
995
1029
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
996
1030
  }
997
1031
  function escapeAttribute(value) {
998
- return escapeHtml3(value).replaceAll("`", "&#96;");
1032
+ return escapeHtml4(value).replaceAll("`", "&#96;");
999
1033
  }
1000
- function escapeHtml3(value) {
1034
+ function escapeHtml4(value) {
1001
1035
  return [...value].map((character) => {
1002
1036
  const codePoint = character.codePointAt(0) ?? 0;
1003
1037
  if (codePoint === 13) return "\n";