@boxpdf/html-writer 0.1.15 → 0.1.17

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
@@ -39,7 +39,7 @@ function dominantTextColor(lines) {
39
39
  }
40
40
  return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
41
41
  }
42
- function semanticTextHtml(text, lines, defaultColor) {
42
+ function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
43
43
  const ranges = [];
44
44
  let cursor = 0;
45
45
  for (const span of lines.flatMap((line) => line.spans)) {
@@ -48,14 +48,25 @@ function semanticTextHtml(text, lines, defaultColor) {
48
48
  if (start < 0) continue;
49
49
  cursor = start + span.text.length;
50
50
  const color = normalizedColor(span.color);
51
- if (color && color !== defaultColor) ranges.push({ start, end: cursor, color });
51
+ const bold = preserveWeight && /(?:bold|semibold|demi|medium|medi)/i.test(span.fontFamily ?? "");
52
+ const italic = /(?:italic|oblique|slanted|slant|ital)/i.test(span.fontFamily ?? "");
53
+ const nondefaultColor = color && color !== defaultColor ? color : void 0;
54
+ if (nondefaultColor || bold || italic) {
55
+ ranges.push({
56
+ start,
57
+ end: cursor,
58
+ ...nondefaultColor ? { color: nondefaultColor } : {},
59
+ bold,
60
+ italic
61
+ });
62
+ }
52
63
  }
53
64
  const merged = mergeRanges(ranges, text);
54
65
  let html = "";
55
66
  let offset = 0;
56
67
  for (const range of merged) {
57
68
  html += escapeHtml(text.slice(offset, range.start));
58
- html += `<span style="color:${range.color}">${escapeHtml(text.slice(range.start, range.end))}</span>`;
69
+ html += styledHtml(text.slice(range.start, range.end), range);
59
70
  offset = range.end;
60
71
  }
61
72
  return html + escapeHtml(text.slice(offset));
@@ -64,7 +75,7 @@ function mergeRanges(ranges, text) {
64
75
  const merged = [];
65
76
  for (const range of ranges) {
66
77
  const previous = merged.at(-1);
67
- if (previous && previous.color === range.color && /^\s*$/.test(text.slice(previous.end, range.start))) {
78
+ if (previous && previous.color === range.color && previous.bold === range.bold && previous.italic === range.italic && /^\s*$/.test(text.slice(previous.end, range.start))) {
68
79
  previous.end = range.end;
69
80
  } else {
70
81
  merged.push({ ...range });
@@ -72,6 +83,13 @@ function mergeRanges(ranges, text) {
72
83
  }
73
84
  return merged;
74
85
  }
86
+ function styledHtml(value, range) {
87
+ let html = escapeHtml(value);
88
+ if (range.color) html = `<span style="color:${range.color}">${html}</span>`;
89
+ if (range.italic) html = `<em>${html}</em>`;
90
+ if (range.bold) html = `<strong>${html}</strong>`;
91
+ return html;
92
+ }
75
93
  function normalizedColor(value) {
76
94
  if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
77
95
  const color = value.toLowerCase();
@@ -365,7 +383,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
365
383
  await flushPendingParagraph();
366
384
  if (employmentOpen && block.type !== "list") await closeEmployment();
367
385
  if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
368
- await write(`<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor)}</h1>`);
386
+ await write(
387
+ `<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h1>`
388
+ );
369
389
  headerOpen = true;
370
390
  continue;
371
391
  }
@@ -380,7 +400,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
380
400
  }
381
401
  if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
382
402
  await write(
383
- `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
403
+ `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
384
404
  );
385
405
  continue;
386
406
  }
@@ -418,7 +438,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
418
438
  const level = contentStarted && block.level === 1 ? 2 : block.level;
419
439
  await closeSections(level);
420
440
  await write(
421
- `<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${level}>`
441
+ `<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${level}>`
422
442
  );
423
443
  sectionLevels.push(level);
424
444
  continue;
@@ -587,7 +607,7 @@ function financialSummaryRow(entry, columns) {
587
607
  }
588
608
  function semanticBlockHtml(block, defaultColor = "#000000") {
589
609
  if (block.type === "heading")
590
- return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`;
610
+ return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`;
591
611
  if (block.type === "paragraph")
592
612
  return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
593
613
  if (block.type === "preformatted") return `<pre>${escapeHtml3(block.text)}</pre>`;
@@ -612,7 +632,7 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
612
632
  return `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`;
613
633
  }
614
634
  const tag = block.ordered ? "ol" : "ul";
615
- return `<${tag}>${block.items.map((item) => `<li>${escapeHtml3(item.text)}</li>`).join("")}</${tag}>`;
635
+ return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
616
636
  }
617
637
  function semanticBlockY(block) {
618
638
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
@@ -885,7 +905,7 @@ async function writeFlowPage(page, write) {
885
905
  if (block.type === "table") await write((0, import_structure2.tableToHtml)(block.table));
886
906
  else if (block.type === "heading") {
887
907
  await write(
888
- `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
908
+ `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
889
909
  );
890
910
  } else if (block.type === "paragraph") {
891
911
  await write(
@@ -924,7 +944,9 @@ async function writeFlowPage(page, write) {
924
944
  } else {
925
945
  const tag = block.ordered ? "ol" : "ul";
926
946
  await write(`<${tag}>`);
927
- for (const item of block.items) await write(`<li>${escapeHtml4(item.text)}</li>`);
947
+ for (const item of block.items) {
948
+ await write(`<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`);
949
+ }
928
950
  await write(`</${tag}>`);
929
951
  }
930
952
  }
@@ -994,7 +1016,7 @@ function visualType3Text(span, font, pageHeight) {
994
1016
  let content = "";
995
1017
  for (const glyph of sequence) {
996
1018
  if (!glyph) continue;
997
- content += `<g transform="translate(${number2(offset)} 0)">${type3Glyph(glyph)}</g>`;
1019
+ content += `<g transform="translate(${number2(offset)} 0)">${type3Glyph(glyph, span.color)}</g>`;
998
1020
  offset += glyph.advance;
999
1021
  }
1000
1022
  return `<g transform="${outer}"><g transform="scale(${number2(xScale)} ${number2(-span.fontSize)})">${content}</g></g>`;
@@ -1005,18 +1027,19 @@ function isHebrewPaintOrder(span) {
1005
1027
  function usesSpacingAdjustment(span) {
1006
1028
  return !span.fontAssetId && /arial/i.test(span.fontFamily ?? "");
1007
1029
  }
1008
- function type3Glyph(glyph) {
1030
+ function type3Glyph(glyph, textColor) {
1009
1031
  let output = "";
1010
1032
  for (const fill of glyph.fills ?? []) {
1011
- if (!isCssHexColor(fill.color)) continue;
1033
+ const color = glyph.usesTextColor && isCssHexColor(textColor) ? textColor : fill.color;
1034
+ if (!isCssHexColor(color)) continue;
1012
1035
  const points = fill.points.map(([x, y]) => `${number2(x)},${number2(y)}`).join(" ");
1013
1036
  const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
1014
- output += `<polygon points="${points}" fill="${fill.color}"${opacity}/>`;
1037
+ output += `<polygon points="${points}" fill="${color}"${opacity}/>`;
1015
1038
  }
1016
1039
  for (const path of glyph.paths ?? []) {
1017
1040
  if (!isSvgPath(path.d)) continue;
1018
- const fill = isCssHexColor(path.fill) ? path.fill : "none";
1019
- const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
1041
+ const fill = glyph.usesTextColor && isCssHexColor(textColor) ? textColor : isCssHexColor(path.fill) ? path.fill : "none";
1042
+ const stroke = glyph.usesTextColor && isCssHexColor(textColor) && path.stroke ? textColor : isCssHexColor(path.stroke) ? path.stroke : "none";
1020
1043
  const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
1021
1044
  output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
1022
1045
  }