@boxpdf/html-writer 0.1.14 → 0.1.16

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 CHANGED
@@ -81,6 +81,52 @@ function escapeHtml(value) {
81
81
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
82
82
  }
83
83
 
84
+ // src/visual-font.ts
85
+ function visualFontAliases(pageNumber, fonts) {
86
+ return new Map(
87
+ fonts.filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${pageNumber}-${font.id}`])
88
+ );
89
+ }
90
+ function visualFontFace(font, aliases) {
91
+ if (font.format !== "truetype") return "";
92
+ const alias = aliases.get(font.id);
93
+ if (!alias) return "";
94
+ const styles2 = visualFontStyles(font.family, alias).filter(
95
+ (style) => !style.startsWith("font-family:")
96
+ );
97
+ return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base64(font.data)}) format("truetype");${styles2.join(";")}}`;
98
+ }
99
+ function visualFontStyles(fontFamily, alias) {
100
+ const normalized = fontFamily?.toLowerCase() ?? "";
101
+ const styles2 = [];
102
+ let fallback;
103
+ if (/courier|mono|nimbusmono|^cmtt/.test(normalized)) {
104
+ fallback = "Courier New,Courier,monospace";
105
+ } else if (/times|minion|serif|baskerville|georgia|nimbusrom|guardian.*egyp|^cm[rs]y?\d/.test(normalized)) {
106
+ fallback = "Times New Roman,Times,serif";
107
+ } else if (/helvetica|arial|sans|nimbussan|calibre|myriad|panton|^tte|^mstt/.test(normalized)) {
108
+ fallback = "Arial,Helvetica,sans-serif";
109
+ }
110
+ if (alias || fallback) styles2.push(`font-family:${[alias, fallback].filter(Boolean).join(",")}`);
111
+ if (/bold|black|semibold|demi|medi|^tte/.test(normalized)) styles2.push("font-weight:700");
112
+ if (/italic|oblique|slant|ital(?:$|[_-])/.test(normalized)) styles2.push("font-style:italic");
113
+ return styles2;
114
+ }
115
+ function base64(bytes) {
116
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
117
+ let output = "";
118
+ for (let index = 0; index < bytes.length; index += 3) {
119
+ const first = bytes[index] ?? 0;
120
+ const second = bytes[index + 1] ?? 0;
121
+ const third = bytes[index + 2] ?? 0;
122
+ output += alphabet[first >> 2];
123
+ output += alphabet[(first & 3) << 4 | second >> 4];
124
+ output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
125
+ output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
126
+ }
127
+ return output;
128
+ }
129
+
84
130
  // src/semantic-media.ts
85
131
  function semanticMedia(page) {
86
132
  const output = (page.images ?? []).map((image) => rasterMedia(image));
@@ -106,12 +152,51 @@ function vectorMedia(page) {
106
152
  ...fills.map(fillBounds)
107
153
  ]);
108
154
  if (!bounds || bounds.width <= 0 || bounds.height <= 0) return void 0;
109
- const content = fills.map(vectorFill).join("") + paths.map((path) => vectorPath(path)).join("");
155
+ const aliases = visualFontAliases(page.number, page.fonts ?? []);
156
+ const visualCodeFonts = new Set(
157
+ (page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
158
+ );
159
+ const visualSpans = page.visualSpans ?? page.spans;
160
+ const overlay = visualSpans.filter(
161
+ (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
162
+ );
163
+ const consumedSpans = page.spans.filter(
164
+ (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
165
+ );
166
+ const fontIds = new Set(overlay.map((span) => span.fontAssetId));
167
+ const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
110
168
  return {
111
169
  bounds,
112
- 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>`
170
+ 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>`,
171
+ ...consumedSpans.length > 0 ? { consumedSpans } : {}
113
172
  };
114
173
  }
174
+ function withoutSemanticMediaSpans(page, media) {
175
+ const consumed = new Set(media.flatMap((item) => item.consumedSpans ?? []));
176
+ return consumed.size > 0 ? { ...page, spans: page.spans.filter((span) => !consumed.has(span)) } : page;
177
+ }
178
+ function vectorText(span, pageHeight, aliases) {
179
+ if (span.renderingMode === 3 || span.renderingMode === 7) return "";
180
+ const styles2 = [
181
+ cssColor(span.color) ? `fill:${span.color}` : "",
182
+ unitInterval(span.fillOpacity) ? `fill-opacity:${number(span.fillOpacity)}` : "",
183
+ ...visualFontStyles(
184
+ span.fontFamily,
185
+ span.fontAssetId ? aliases.get(span.fontAssetId) : void 0
186
+ )
187
+ ].filter(Boolean).join(";");
188
+ const anchorY = pageHeight - span.bounds.y;
189
+ const transform = span.transform;
190
+ 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)}"`;
191
+ const extent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
192
+ const length = extent > 0 ? ` textLength="${number(extent)}" lengthAdjust="spacingAndGlyphs"` : "";
193
+ return `<text${position} font-size="${number(span.fontSize)}"${length}${styles2 ? ` style="${styles2}"` : ""}>${escapeHtml2(span.text)}</text>`;
194
+ }
195
+ function centerInside(inner, outer) {
196
+ const x = inner.x + inner.width / 2;
197
+ const y = inner.y + inner.height / 2;
198
+ return x >= outer.x && x <= outer.x + outer.width && y >= outer.y && y <= outer.y + outer.height;
199
+ }
115
200
  function vectorFill(fill) {
116
201
  const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
117
202
  const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
@@ -197,20 +282,6 @@ function rgbBmp(image) {
197
282
  }
198
283
  return output;
199
284
  }
200
- function base64(bytes) {
201
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
202
- let output = "";
203
- for (let index = 0; index < bytes.length; index += 3) {
204
- const first = bytes[index] ?? 0;
205
- const second = bytes[index + 1] ?? 0;
206
- const third = bytes[index + 2] ?? 0;
207
- output += alphabet[first >> 2];
208
- output += alphabet[(first & 3) << 4 | second >> 4];
209
- output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
210
- output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
211
- }
212
- return output;
213
- }
214
285
  function safePath(value) {
215
286
  return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
216
287
  }
@@ -226,6 +297,9 @@ function unitInterval(value) {
226
297
  function number(value) {
227
298
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
228
299
  }
300
+ function escapeHtml2(value) {
301
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
302
+ }
229
303
 
230
304
  // src/semantic-document.ts
231
305
  async function writeSemanticDocument(pages, write, lookaheadPages) {
@@ -352,13 +426,13 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
352
426
  if (block.type === "paragraph") {
353
427
  if (isTitledRecord(block)) {
354
428
  const [institution, ...details] = block.lines;
355
- if (institution) await write(`<h3>${escapeHtml2(institution.text)}</h3>`);
356
- for (const detail of details) await write(`<p>${escapeHtml2(detail.text)}</p>`);
429
+ if (institution) await write(`<h3>${escapeHtml3(institution.text)}</h3>`);
430
+ for (const detail of details) await write(`<p>${escapeHtml3(detail.text)}</p>`);
357
431
  continue;
358
432
  }
359
433
  if (isUnmarkedList(block)) {
360
434
  await write(
361
- `<ul>${block.lines.map((line) => `<li>${escapeHtml2(line.text)}</li>`).join("")}</ul>`
435
+ `<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`
362
436
  );
363
437
  continue;
364
438
  }
@@ -367,7 +441,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
367
441
  }
368
442
  if (block.type === "employment") {
369
443
  await write(
370
- `<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p>`
444
+ `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`
371
445
  );
372
446
  employmentOpen = true;
373
447
  continue;
@@ -384,8 +458,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
384
458
  for (const signature of marginSignatures(page)) seenFurniture.add(signature);
385
459
  };
386
460
  for await (const page of pages) {
387
- const structured = (0, import_structure.structurePage)(page);
388
- buffer.push({ width: page.width, height: page.height, structured, media: semanticMedia(page) });
461
+ const media = semanticMedia(page);
462
+ const structured = (0, import_structure.structurePage)(withoutSemanticMediaSpans(page, media));
463
+ buffer.push({ width: page.width, height: page.height, structured, media });
389
464
  stats.pagesProcessed += 1;
390
465
  stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
391
466
  stats.peakBufferedLines = Math.max(
@@ -498,7 +573,7 @@ function sameRow(left, right) {
498
573
  }
499
574
  function tableRow(row, header) {
500
575
  const cell = header ? "th" : "td";
501
- return `<tr>${row.map((value) => `<${cell}>${escapeHtml2(value)}</${cell}>`).join("")}</tr>`;
576
+ return `<tr>${row.map((value) => `<${cell}>${escapeHtml3(value)}</${cell}>`).join("")}</tr>`;
502
577
  }
503
578
  function isFinancialSummary(block) {
504
579
  return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
@@ -508,22 +583,22 @@ function isNumericValue(value) {
508
583
  }
509
584
  function financialSummaryRow(entry, columns) {
510
585
  const colspan = columns > 2 ? ` colspan="${columns - 1}"` : "";
511
- return `<tr><th scope="row"${colspan}>${escapeHtml2(entry.term)}</th><td>${escapeHtml2(entry.description)}</td></tr>`;
586
+ return `<tr><th scope="row"${colspan}>${escapeHtml3(entry.term)}</th><td>${escapeHtml3(entry.description)}</td></tr>`;
512
587
  }
513
588
  function semanticBlockHtml(block, defaultColor = "#000000") {
514
589
  if (block.type === "heading")
515
590
  return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`;
516
591
  if (block.type === "paragraph")
517
592
  return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
518
- if (block.type === "preformatted") return `<pre>${escapeHtml2(block.text)}</pre>`;
593
+ if (block.type === "preformatted") return `<pre>${escapeHtml3(block.text)}</pre>`;
519
594
  if (block.type === "definitionList") {
520
595
  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()))) {
521
596
  return block.entries.map(
522
- (entry) => `<section><h2>${escapeHtml2(titleCase(entry.term))}</h2><p>${escapeHtml2(entry.description)}</p></section>`
597
+ (entry) => `<section><h2>${escapeHtml3(titleCase(entry.term))}</h2><p>${escapeHtml3(entry.description)}</p></section>`
523
598
  ).join("");
524
599
  }
525
600
  const list = `<dl>${block.entries.map(
526
- (entry) => `<div><dt>${escapeHtml2(entry.term)}</dt><dd>${escapeHtml2(entry.description)}</dd></div>`
601
+ (entry) => `<div><dt>${escapeHtml3(entry.term)}</dt><dd>${escapeHtml3(entry.description)}</dd></div>`
527
602
  ).join("")}</dl>`;
528
603
  return isFinancialSummary(block) ? `<section>${list}</section>` : list;
529
604
  }
@@ -534,10 +609,10 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
534
609
  return block.items.map(labeledSectionHtml).join("");
535
610
  }
536
611
  if (block.type === "employment") {
537
- return `<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p></section>`;
612
+ return `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`;
538
613
  }
539
614
  const tag = block.ordered ? "ol" : "ul";
540
- return `<${tag}>${block.items.map((item) => `<li>${escapeHtml2(item.text)}</li>`).join("")}</${tag}>`;
615
+ return `<${tag}>${block.items.map((item) => `<li>${escapeHtml3(item.text)}</li>`).join("")}</${tag}>`;
541
616
  }
542
617
  function semanticBlockY(block) {
543
618
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
@@ -547,28 +622,28 @@ function cardTableRow(item) {
547
622
  const trailing = item.details.at(-1) ?? "";
548
623
  const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
549
624
  const description = item.details.slice(0, -1).join(" ");
550
- const detail = description ? `<br><span>${escapeHtml2(description)}</span>` : "";
625
+ const detail = description ? `<br><span>${escapeHtml3(description)}</span>` : "";
551
626
  const quantity = match?.[1] ?? "";
552
627
  const amount = match?.[2] ?? trailing;
553
- return `<tr><th scope="row">${escapeHtml2(item.title)}${detail}</th><td>${escapeHtml2(quantity)}</td><td>${escapeHtml2(amount)}</td></tr>`;
628
+ return `<tr><th scope="row">${escapeHtml3(item.title)}${detail}</th><td>${escapeHtml3(quantity)}</td><td>${escapeHtml3(amount)}</td></tr>`;
554
629
  }
555
630
  function labeledSectionHtml(item) {
556
631
  const heading = titleCase(item.label);
557
632
  const postal = /\b(?:ship|deliver|mail)(?:ed)?\b/i.test(item.label);
558
633
  if (postal) {
559
634
  const [name, ...address] = item.content;
560
- const content = [name ? `<strong>${escapeHtml2(name)}</strong>` : "", ...address.map(escapeHtml2)].filter(Boolean).join("<br>");
561
- return `<section><h2>${escapeHtml2(heading)}</h2><address>${content}</address></section>`;
635
+ const content = [name ? `<strong>${escapeHtml3(name)}</strong>` : "", ...address.map(escapeHtml3)].filter(Boolean).join("<br>");
636
+ return `<section><h2>${escapeHtml3(heading)}</h2><address>${content}</address></section>`;
562
637
  }
563
- return `<section><h2>${escapeHtml2(heading)}</h2>${item.content.map(
564
- (content, index) => `<p>${index === 0 ? `<strong>${escapeHtml2(content)}</strong>` : escapeHtml2(content)}</p>`
638
+ return `<section><h2>${escapeHtml3(heading)}</h2>${item.content.map(
639
+ (content, index) => `<p>${index === 0 ? `<strong>${escapeHtml3(content)}</strong>` : escapeHtml3(content)}</p>`
565
640
  ).join("")}</section>`;
566
641
  }
567
642
  function titleCase(value) {
568
643
  const normalized = value.trim().toLocaleLowerCase("en");
569
644
  return normalized.replace(/^\p{L}/u, (letter) => letter.toLocaleUpperCase("en"));
570
645
  }
571
- function escapeHtml2(value) {
646
+ function escapeHtml3(value) {
572
647
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
573
648
  }
574
649
 
@@ -582,7 +657,7 @@ async function writeHtmlDocument(pages, write, options = {}) {
582
657
  ` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
583
658
  );
584
659
  await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
585
- await write(`<title>${escapeHtml3(options.title ?? "PDF document")}</title>`);
660
+ await write(`<title>${escapeHtml4(options.title ?? "PDF document")}</title>`);
586
661
  if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
587
662
  await write("</head><body>");
588
663
  }
@@ -628,14 +703,14 @@ async function writePositionedPage(page, write, options) {
628
703
  await write(
629
704
  `<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">`
630
705
  );
631
- const fontAliases = new Map(
632
- (page.fonts ?? []).filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${page.number}-${font.id}`])
633
- );
706
+ const fontAliases = visualFontAliases(page.number, page.fonts ?? []);
634
707
  const type3Fonts = new Map(
635
708
  (page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
636
709
  );
637
710
  if ((options.includeStyles ?? true) && page.fonts?.length) {
638
- await write(`<style>${page.fonts.map((font) => fontFace(font, fontAliases)).join("")}</style>`);
711
+ await write(
712
+ `<style>${page.fonts.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
713
+ );
639
714
  }
640
715
  await write(
641
716
  `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number2(page.width)}pt;height:${number2(page.height)}pt${rotationTransform(page)}">`
@@ -709,7 +784,7 @@ function visualImage(image, pageHeight, pageNumber, imageIndex) {
709
784
  const opacity = isUnitInterval(image.opacity) ? ` opacity="${number2(image.opacity)}"` : "";
710
785
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
711
786
  const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
712
- let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base642(data)}"${opacity}/>`;
787
+ let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
713
788
  for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
714
789
  output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
715
790
  }
@@ -786,17 +861,17 @@ function positionedSpan(span, fontAliases) {
786
861
  `font-size:${number2(span.fontSize)}pt`,
787
862
  ...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
788
863
  ...isUnitInterval(span.fillOpacity) ? [`opacity:${number2(span.fillOpacity)}`] : [],
789
- ...fontStyles(
864
+ ...visualFontStyles(
790
865
  span.fontFamily,
791
866
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
792
867
  )
793
868
  ].join(";");
794
- return `<span class="pdf-span"${direction} style="${style}">${escapeHtml3(span.text)}</span>`;
869
+ return `<span class="pdf-span"${direction} style="${style}">${escapeHtml4(span.text)}</span>`;
795
870
  }
796
871
  async function writeFlowPage(page, write) {
797
- const structured = (0, import_structure2.structurePage)(page);
798
- const defaultColor = dominantTextColor(structured.lines);
799
872
  const media = semanticMedia(page);
873
+ const structured = (0, import_structure2.structurePage)(withoutSemanticMediaSpans(page, media));
874
+ const defaultColor = dominantTextColor(structured.lines);
800
875
  let mediaIndex = 0;
801
876
  await write(
802
877
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
@@ -817,39 +892,39 @@ async function writeFlowPage(page, write) {
817
892
  `<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`
818
893
  );
819
894
  } else if (block.type === "preformatted") {
820
- await write(`<pre>${escapeHtml3(block.text)}</pre>`);
895
+ await write(`<pre>${escapeHtml4(block.text)}</pre>`);
821
896
  } else if (block.type === "definitionList") {
822
897
  await write("<dl>");
823
898
  for (const entry of block.entries) {
824
899
  await write(
825
- `<div><dt>${escapeHtml3(entry.term)}</dt><dd>${escapeHtml3(entry.description)}</dd></div>`
900
+ `<div><dt>${escapeHtml4(entry.term)}</dt><dd>${escapeHtml4(entry.description)}</dd></div>`
826
901
  );
827
902
  }
828
903
  await write("</dl>");
829
904
  } else if (block.type === "cardList") {
830
905
  await write('<div class="pdf-semantic-cards">');
831
906
  for (const item of block.items) {
832
- await write(`<article><h3>${escapeHtml3(item.title)}</h3>`);
833
- for (const detail of item.details) await write(`<p>${escapeHtml3(detail)}</p>`);
907
+ await write(`<article><h3>${escapeHtml4(item.title)}</h3>`);
908
+ for (const detail of item.details) await write(`<p>${escapeHtml4(detail)}</p>`);
834
909
  await write("</article>");
835
910
  }
836
911
  await write("</div>");
837
912
  } else if (block.type === "sectionGroup") {
838
913
  await write('<div class="pdf-semantic-sections">');
839
914
  for (const item of block.items) {
840
- await write(`<section><h3>${escapeHtml3(item.label)}</h3>`);
841
- for (const content of item.content) await write(`<p>${escapeHtml3(content)}</p>`);
915
+ await write(`<section><h3>${escapeHtml4(item.label)}</h3>`);
916
+ for (const content of item.content) await write(`<p>${escapeHtml4(content)}</p>`);
842
917
  await write("</section>");
843
918
  }
844
919
  await write("</div>");
845
920
  } else if (block.type === "employment") {
846
921
  await write(
847
- `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`
922
+ `<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`
848
923
  );
849
924
  } else {
850
925
  const tag = block.ordered ? "ol" : "ul";
851
926
  await write(`<${tag}>`);
852
- for (const item of block.items) await write(`<li>${escapeHtml3(item.text)}</li>`);
927
+ for (const item of block.items) await write(`<li>${escapeHtml4(item.text)}</li>`);
853
928
  await write(`</${tag}>`);
854
929
  }
855
930
  }
@@ -867,7 +942,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
867
942
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
868
943
  if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
869
944
  const direction = directionAttribute([span]);
870
- const font = fontStyles(
945
+ const font = visualFontStyles(
871
946
  span.fontFamily,
872
947
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
873
948
  ).join(";");
@@ -901,7 +976,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
901
976
  const anchorX = span.bounds.x + basisX * rtlOffset;
902
977
  const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
903
978
  const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
904
- return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml3(span.text)}</text>`;
979
+ return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
905
980
  }
906
981
  function isAdobeCjkFont(fontFamily) {
907
982
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -919,7 +994,7 @@ function visualType3Text(span, font, pageHeight) {
919
994
  let content = "";
920
995
  for (const glyph of sequence) {
921
996
  if (!glyph) continue;
922
- content += `<g transform="translate(${number2(offset)} 0)">${type3Glyph(glyph)}</g>`;
997
+ content += `<g transform="translate(${number2(offset)} 0)">${type3Glyph(glyph, span.color)}</g>`;
923
998
  offset += glyph.advance;
924
999
  }
925
1000
  return `<g transform="${outer}"><g transform="scale(${number2(xScale)} ${number2(-span.fontSize)})">${content}</g></g>`;
@@ -930,18 +1005,19 @@ function isHebrewPaintOrder(span) {
930
1005
  function usesSpacingAdjustment(span) {
931
1006
  return !span.fontAssetId && /arial/i.test(span.fontFamily ?? "");
932
1007
  }
933
- function type3Glyph(glyph) {
1008
+ function type3Glyph(glyph, textColor) {
934
1009
  let output = "";
935
1010
  for (const fill of glyph.fills ?? []) {
936
- if (!isCssHexColor(fill.color)) continue;
1011
+ const color = glyph.usesTextColor && isCssHexColor(textColor) ? textColor : fill.color;
1012
+ if (!isCssHexColor(color)) continue;
937
1013
  const points = fill.points.map(([x, y]) => `${number2(x)},${number2(y)}`).join(" ");
938
1014
  const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
939
- output += `<polygon points="${points}" fill="${fill.color}"${opacity}/>`;
1015
+ output += `<polygon points="${points}" fill="${color}"${opacity}/>`;
940
1016
  }
941
1017
  for (const path of glyph.paths ?? []) {
942
1018
  if (!isSvgPath(path.d)) continue;
943
- const fill = isCssHexColor(path.fill) ? path.fill : "none";
944
- const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
1019
+ const fill = glyph.usesTextColor && isCssHexColor(textColor) ? textColor : isCssHexColor(path.fill) ? path.fill : "none";
1020
+ const stroke = glyph.usesTextColor && isCssHexColor(textColor) && path.stroke ? textColor : isCssHexColor(path.stroke) ? path.stroke : "none";
945
1021
  const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
946
1022
  output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
947
1023
  }
@@ -956,24 +1032,6 @@ function isUnitInterval(value) {
956
1032
  function isSvgPath(value) {
957
1033
  return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
958
1034
  }
959
- function fontStyles(fontFamily, alias) {
960
- const normalized = fontFamily?.toLowerCase() ?? "";
961
- const styles2 = [];
962
- let fallback;
963
- if (/courier|mono|nimbusmono|^cmtt/.test(normalized)) {
964
- fallback = "Courier New,Courier,monospace";
965
- } else if (/times|minion|serif|baskerville|georgia|nimbusrom|guardian.*egyp|^cm[rs]y?\d/.test(normalized)) {
966
- fallback = "Times New Roman,Times,serif";
967
- } else if (/helvetica|arial|sans|nimbussan|calibre|myriad|panton|^tte/.test(normalized)) {
968
- fallback = "Arial,Helvetica,sans-serif";
969
- } else if (/^mstt/.test(normalized)) {
970
- fallback = "Arial,Helvetica,sans-serif";
971
- }
972
- if (alias || fallback) styles2.push(`font-family:${[alias, fallback].filter(Boolean).join(",")}`);
973
- if (/bold|black|semibold|demi|medi|^tte/.test(normalized)) styles2.push("font-weight:700");
974
- if (/italic|oblique|slant|ital(?:$|[_-])/.test(normalized)) styles2.push("font-style:italic");
975
- return styles2;
976
- }
977
1035
  function isMonospace(fontFamily) {
978
1036
  return /courier|mono/i.test(fontFamily ?? "");
979
1037
  }
@@ -985,29 +1043,6 @@ function hasNonIdentityTransform(transform) {
985
1043
  const identity = [1, 0, 0, 1];
986
1044
  return transform.some((value, index) => Math.abs(value - (identity[index] ?? 0)) > 1e-6);
987
1045
  }
988
- function fontFace(font, aliases) {
989
- if (font.format !== "truetype") return "";
990
- const alias = aliases.get(font.id);
991
- if (!alias) return "";
992
- const styles2 = fontStyles(font.family, alias).filter(
993
- (style) => !style.startsWith("font-family:")
994
- );
995
- return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base642(font.data)}) format("truetype");${styles2.join(";")}}`;
996
- }
997
- function base642(bytes) {
998
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
999
- let output = "";
1000
- for (let index = 0; index < bytes.length; index += 3) {
1001
- const first = bytes[index] ?? 0;
1002
- const second = bytes[index + 1] ?? 0;
1003
- const third = bytes[index + 2] ?? 0;
1004
- output += alphabet[first >> 2];
1005
- output += alphabet[(first & 3) << 4 | second >> 4];
1006
- output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
1007
- output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
1008
- }
1009
- return output;
1010
- }
1011
1046
  function directionAttribute(spans) {
1012
1047
  const rtl = spans.filter((span) => span.direction === "rtl").length;
1013
1048
  const vertical = spans.filter((span) => span.direction === "ttb").length;
@@ -1018,9 +1053,9 @@ function number2(value) {
1018
1053
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
1019
1054
  }
1020
1055
  function escapeAttribute(value) {
1021
- return escapeHtml3(value).replaceAll("`", "&#96;");
1056
+ return escapeHtml4(value).replaceAll("`", "&#96;");
1022
1057
  }
1023
- function escapeHtml3(value) {
1058
+ function escapeHtml4(value) {
1024
1059
  return [...value].map((character) => {
1025
1060
  const codePoint = character.codePointAt(0) ?? 0;
1026
1061
  if (codePoint === 13) return "\n";