pretext-pdf 0.5.3 → 0.8.0
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/CHANGELOG.md +462 -361
- package/README.md +749 -568
- package/dist/assets.d.ts +5 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +248 -43
- package/dist/assets.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fonts.d.ts.map +1 -1
- package/dist/fonts.js +67 -8
- package/dist/fonts.js.map +1 -1
- package/dist/index.d.ts +29 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -2
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +28 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +222 -0
- package/dist/markdown.js.map +1 -0
- package/dist/measure-blocks.d.ts.map +1 -1
- package/dist/measure-blocks.js +347 -62
- package/dist/measure-blocks.js.map +1 -1
- package/dist/measure-text.d.ts.map +1 -1
- package/dist/measure-text.js +1 -8
- package/dist/measure-text.js.map +1 -1
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +13 -21
- package/dist/measure.js.map +1 -1
- package/dist/render-blocks.d.ts +4 -1
- package/dist/render-blocks.d.ts.map +1 -1
- package/dist/render-blocks.js +227 -105
- package/dist/render-blocks.js.map +1 -1
- package/dist/render-extras.d.ts.map +1 -1
- package/dist/render-extras.js +72 -71
- package/dist/render-extras.js.map +1 -1
- package/dist/render-utils.d.ts +9 -2
- package/dist/render-utils.d.ts.map +1 -1
- package/dist/render-utils.js +24 -13
- package/dist/render-utils.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +27 -3
- package/dist/render.js.map +1 -1
- package/dist/rich-text.d.ts +0 -4
- package/dist/rich-text.d.ts.map +1 -1
- package/dist/rich-text.js +15 -9
- package/dist/rich-text.js.map +1 -1
- package/dist/templates.d.ts +79 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +201 -0
- package/dist/templates.js.map +1 -0
- package/dist/types.d.ts +139 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +241 -28
- package/dist/validate.js.map +1 -1
- package/package.json +175 -130
package/dist/render-blocks.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { rgb, degrees } from '@cantoo/pdf-lib';
|
|
6
6
|
import { PretextPdfError } from './errors.js';
|
|
7
|
-
import { drawJustifiedLine, addLinkAnnotation, addStickyNoteAnnotation, drawTextDecoration, toPdfY, resolveX, resolveTokens, hexToRgb, drawTabularText, } from './render-utils.js';
|
|
7
|
+
import { drawJustifiedLine, addLinkAnnotation, addStickyNoteAnnotation, drawTextDecoration, toPdfY, resolveX, resolveTokens, hexToRgb, drawTabularText, LINE_HEIGHT_BODY, LINE_HEIGHT_COMPACT, } from './render-utils.js';
|
|
8
8
|
import { buildFontKey } from './measure.js';
|
|
9
9
|
// ─── Text block rendering (paragraph + heading) ───────────────────────────────
|
|
10
10
|
export function renderTextBlock(pdfPage, pagedBlock, geo, fontMap, pdfDoc) {
|
|
@@ -47,10 +47,14 @@ export function renderTextBlock(pdfPage, pagedBlock, geo, fontMap, pdfDoc) {
|
|
|
47
47
|
borderWidth: 0,
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
|
-
// Multi-column layout
|
|
50
|
+
// Multi-column layout — mirrors single-column features (smallCaps, letterSpacing, justify, decoration)
|
|
51
51
|
const columnData = measuredBlock.columnData;
|
|
52
52
|
if (columnData) {
|
|
53
|
-
const {
|
|
53
|
+
const { columnGap, columnWidth, linesPerColumn } = columnData;
|
|
54
|
+
const hasSmallCaps = textElement?.smallCaps === true;
|
|
55
|
+
const mcFontSize = hasSmallCaps ? measuredBlock.fontSize * 0.8 : measuredBlock.fontSize;
|
|
56
|
+
const hasTabular = textElement?.tabularNumbers === true;
|
|
57
|
+
const letterSpacing = (textElement?.letterSpacing ?? 0) > 0 ? textElement.letterSpacing : 0;
|
|
54
58
|
for (let i = 0; i < lines.length; i++) {
|
|
55
59
|
const line = lines[i];
|
|
56
60
|
if (line.text === '')
|
|
@@ -61,22 +65,53 @@ export function renderTextBlock(pdfPage, pagedBlock, geo, fontMap, pdfDoc) {
|
|
|
61
65
|
const absoluteYFromTop = lineYFromTop + geo.margins.top + geo.headerHeight;
|
|
62
66
|
const pdfY = toPdfY(absoluteYFromTop, fontHeight, geo.pageHeight);
|
|
63
67
|
const colX = geo.margins.left + colIdx * (columnWidth + columnGap);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
68
|
+
let trimmedText = line.text.trimEnd();
|
|
69
|
+
if (hasSmallCaps)
|
|
70
|
+
trimmedText = trimmedText.toUpperCase();
|
|
71
|
+
// Last line of each column should not be force-justified (left-align instead)
|
|
72
|
+
const isLastLineInCol = lineInCol === linesPerColumn - 1 || i === lines.length - 1;
|
|
73
|
+
let drawX;
|
|
74
|
+
if (alignRaw === 'justify' && letterSpacing === 0 && !hasTabular) {
|
|
75
|
+
drawJustifiedLine(pdfPage, trimmedText, isLastLineInCol, colX, pdfY, columnWidth, mcFontSize, pdfFont, rgb(r, g, b));
|
|
76
|
+
drawX = colX;
|
|
77
|
+
}
|
|
78
|
+
else if (letterSpacing > 0) {
|
|
79
|
+
const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize) + letterSpacing * (trimmedText.length - 1);
|
|
80
|
+
drawX = resolveX(align, colX, columnWidth, alignWidth);
|
|
81
|
+
let cx = drawX;
|
|
82
|
+
for (const ch of trimmedText) {
|
|
83
|
+
pdfPage.drawText(ch, { x: cx, y: pdfY, size: mcFontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
84
|
+
cx += pdfFont.widthOfTextAtSize(ch, mcFontSize) + letterSpacing;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (hasTabular) {
|
|
88
|
+
const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize);
|
|
89
|
+
drawX = resolveX(align, colX, columnWidth, alignWidth);
|
|
90
|
+
drawTabularText(pdfPage, trimmedText, drawX, pdfY, mcFontSize, pdfFont, rgb(r, g, b));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize);
|
|
94
|
+
drawX = resolveX(align, colX, columnWidth, alignWidth);
|
|
95
|
+
pdfPage.drawText(trimmedText, { x: drawX, y: pdfY, size: mcFontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
96
|
+
}
|
|
97
|
+
// Text decoration (underline, strikethrough)
|
|
98
|
+
if ((element.type === 'paragraph' || element.type === 'heading') && (element.underline || element.strikethrough)) {
|
|
99
|
+
const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize) + (letterSpacing > 0 ? letterSpacing * (trimmedText.length - 1) : 0);
|
|
100
|
+
drawTextDecoration(pdfPage, drawX, lineWidth, pdfY, mcFontSize, pdfFont, [r, g, b], { underline: element.underline ?? false, strikethrough: element.strikethrough ?? false });
|
|
101
|
+
}
|
|
102
|
+
// Hyperlink annotation
|
|
75
103
|
if ((element.type === 'paragraph' || element.type === 'heading') && element.url) {
|
|
76
|
-
const lineWidth = pdfFont.widthOfTextAtSize(trimmedText,
|
|
77
|
-
addLinkAnnotation(pdfDoc, pdfPage,
|
|
104
|
+
const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize) + (letterSpacing > 0 ? letterSpacing * (trimmedText.length - 1) : 0);
|
|
105
|
+
addLinkAnnotation(pdfDoc, pdfPage, drawX, pdfY, lineWidth, mcFontSize, element.url);
|
|
78
106
|
}
|
|
79
107
|
}
|
|
108
|
+
// Sticky note annotation (once per block, not per line)
|
|
109
|
+
if (textElement?.annotation) {
|
|
110
|
+
const ann = textElement.annotation;
|
|
111
|
+
const absY = yFromTop + geo.margins.top + geo.headerHeight;
|
|
112
|
+
const annotPdfY = geo.pageHeight - absY;
|
|
113
|
+
addStickyNoteAnnotation(pdfDoc, pdfPage, geo.margins.left, annotPdfY, ann.contents, ann.author, ann.color, ann.open);
|
|
114
|
+
}
|
|
80
115
|
return; // skip standard single-column path
|
|
81
116
|
}
|
|
82
117
|
// Single-column layout (standard path)
|
|
@@ -238,14 +273,15 @@ export function renderTable(pdfPage, pagedBlock, geo, fontMap) {
|
|
|
238
273
|
// ── Pass 1: Cell backgrounds ──────────────────────────────────────────────
|
|
239
274
|
let rowAbsY = chunkStartAbsY;
|
|
240
275
|
for (const row of chunkRows) {
|
|
241
|
-
const rowPdfY = toPdfY(rowAbsY, row.height, geo.pageHeight);
|
|
242
276
|
let cellX = geo.margins.left;
|
|
243
277
|
for (const cell of row.cells) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
278
|
+
if (!cell.isSpanPlaceholder) {
|
|
279
|
+
const cellRenderHeight = cell.spanHeight ?? row.height;
|
|
280
|
+
const bgColorHex = cell.bgColor ?? (row.isHeader ? headerBgColor : undefined);
|
|
281
|
+
if (bgColorHex) {
|
|
282
|
+
const [r, g, b] = hexToRgb(bgColorHex);
|
|
283
|
+
pdfPage.drawRectangle({ x: cellX, y: toPdfY(rowAbsY, cellRenderHeight, geo.pageHeight), width: cell.mergedWidth, height: cellRenderHeight, color: rgb(r, g, b), borderWidth: 0 });
|
|
284
|
+
}
|
|
249
285
|
}
|
|
250
286
|
cellX += cell.mergedWidth;
|
|
251
287
|
}
|
|
@@ -267,50 +303,56 @@ export function renderTable(pdfPage, pagedBlock, geo, fontMap) {
|
|
|
267
303
|
borderWidth,
|
|
268
304
|
});
|
|
269
305
|
// Internal horizontal lines (row separators, between rows, not at edges)
|
|
306
|
+
// Suppressed after rows that have a spanning cell crossing into the next row
|
|
270
307
|
let lineAbsY = chunkStartAbsY;
|
|
271
308
|
for (let ri = 0; ri < chunkRows.length - 1; ri++) {
|
|
272
309
|
lineAbsY += chunkRows[ri].height;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
start: { x: geo.margins.left, y: linePdfY },
|
|
276
|
-
end: { x: geo.margins.left + totalTableWidth, y: linePdfY },
|
|
277
|
-
thickness: borderWidth,
|
|
278
|
-
color: borderRgb,
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
// Internal vertical lines (column separators, between columns, not at edges)
|
|
282
|
-
// With colspan support: only draw lines at boundaries that are NOT spanned by merged cells
|
|
283
|
-
// Each row may have different active boundaries due to different colspan patterns
|
|
284
|
-
let colBoundaryX = geo.margins.left;
|
|
285
|
-
for (let ci = 0; ci < columnWidths.length; ci++) {
|
|
286
|
-
colBoundaryX += columnWidths[ci];
|
|
287
|
-
// Check if this boundary (between column ci and ci+1) is active in ANY row
|
|
288
|
-
const boundaryIndex = ci; // boundary at index ci is between columns ci and ci+1
|
|
289
|
-
let isActive = false;
|
|
290
|
-
for (const row of chunkRows) {
|
|
291
|
-
if (row.activeBoundaries.includes(boundaryIndex)) {
|
|
292
|
-
isActive = true;
|
|
293
|
-
break;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if (isActive && ci < columnWidths.length - 1) {
|
|
297
|
-
const chunkTopPdfY = geo.pageHeight - chunkStartAbsY;
|
|
298
|
-
const chunkBottomPdfY = geo.pageHeight - (chunkStartAbsY + totalChunkHeight);
|
|
310
|
+
if (!chunkRows[ri].hasRowspan) {
|
|
311
|
+
const linePdfY = geo.pageHeight - lineAbsY;
|
|
299
312
|
pdfPage.drawLine({
|
|
300
|
-
start: { x:
|
|
301
|
-
end: { x:
|
|
313
|
+
start: { x: geo.margins.left, y: linePdfY },
|
|
314
|
+
end: { x: geo.margins.left + totalTableWidth, y: linePdfY },
|
|
302
315
|
thickness: borderWidth,
|
|
303
316
|
color: borderRgb,
|
|
304
317
|
});
|
|
305
318
|
}
|
|
306
319
|
}
|
|
320
|
+
// Internal vertical lines (column separators, between columns, not at edges)
|
|
321
|
+
// Draw per-row segments: a boundary absent from a row's activeBoundaries means a merged cell
|
|
322
|
+
// spans it — a full-chunk line would cut through it. Per-row drawing preserves colspan correctness.
|
|
323
|
+
// Pre-compute X positions once; convert each row's activeBoundaries to a Set for O(1) lookup.
|
|
324
|
+
const colBoundaryXPositions = [];
|
|
325
|
+
let bx = geo.margins.left;
|
|
326
|
+
for (let ci = 0; ci < columnWidths.length - 1; ci++) {
|
|
327
|
+
bx += columnWidths[ci];
|
|
328
|
+
colBoundaryXPositions.push(bx);
|
|
329
|
+
}
|
|
330
|
+
const rowBoundarySets = chunkRows.map(row => new Set(row.activeBoundaries));
|
|
331
|
+
let vertRowAbsY = chunkStartAbsY;
|
|
332
|
+
for (let ri = 0; ri < chunkRows.length; ri++) {
|
|
333
|
+
const row = chunkRows[ri];
|
|
334
|
+
const rowBoundarySet = rowBoundarySets[ri];
|
|
335
|
+
const rowTopPdfY = geo.pageHeight - vertRowAbsY;
|
|
336
|
+
const rowBottomPdfY = geo.pageHeight - (vertRowAbsY + row.height);
|
|
337
|
+
for (let ci = 0; ci < colBoundaryXPositions.length; ci++) {
|
|
338
|
+
if (rowBoundarySet.has(ci)) {
|
|
339
|
+
pdfPage.drawLine({
|
|
340
|
+
start: { x: colBoundaryXPositions[ci], y: rowTopPdfY },
|
|
341
|
+
end: { x: colBoundaryXPositions[ci], y: rowBottomPdfY },
|
|
342
|
+
thickness: borderWidth,
|
|
343
|
+
color: borderRgb,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
vertRowAbsY += row.height;
|
|
348
|
+
}
|
|
307
349
|
}
|
|
308
350
|
// ── Pass 3: Cell text ─────────────────────────────────────────────────────
|
|
309
351
|
rowAbsY = chunkStartAbsY;
|
|
310
352
|
for (const row of chunkRows) {
|
|
311
353
|
let cellX = geo.margins.left;
|
|
312
354
|
for (const cell of row.cells) {
|
|
313
|
-
if (cell.lines.length > 0) {
|
|
355
|
+
if (!cell.isSpanPlaceholder && cell.lines.length > 0) {
|
|
314
356
|
const pdfFont = fontMap.get(cell.fontKey);
|
|
315
357
|
if (!pdfFont) {
|
|
316
358
|
throw new PretextPdfError('FONT_NOT_LOADED', `Table cell font "${cell.fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
|
|
@@ -318,13 +360,16 @@ export function renderTable(pdfPage, pagedBlock, geo, fontMap) {
|
|
|
318
360
|
const fontHeight = pdfFont.heightAtSize(cell.fontSize);
|
|
319
361
|
const [r, g, b] = hexToRgb(cell.color);
|
|
320
362
|
const textAreaX = cellX + cellPaddingH;
|
|
321
|
-
// Use mergedWidth for colspan support
|
|
322
363
|
const textAreaWidth = cell.mergedWidth - 2 * cellPaddingH;
|
|
364
|
+
// For rowspan cells, vertically center within the full spanHeight
|
|
365
|
+
const cellRenderHeight = cell.spanHeight ?? row.height;
|
|
366
|
+
const totalTextHeight = cell.lines.length * cell.lineHeight;
|
|
367
|
+
const verticalOffset = Math.max(0, (cellRenderHeight - totalTextHeight - 2 * cellPaddingV) / 2);
|
|
323
368
|
for (let li = 0; li < cell.lines.length; li++) {
|
|
324
369
|
const line = cell.lines[li];
|
|
325
370
|
if (line.text === '')
|
|
326
371
|
continue;
|
|
327
|
-
const lineYFromPageTop = rowAbsY + cellPaddingV + li * cell.lineHeight;
|
|
372
|
+
const lineYFromPageTop = rowAbsY + cellPaddingV + verticalOffset + li * cell.lineHeight;
|
|
328
373
|
const pdfY = toPdfY(lineYFromPageTop, fontHeight, geo.pageHeight);
|
|
329
374
|
const trimmedText = line.text.trimEnd();
|
|
330
375
|
const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, cell.fontSize);
|
|
@@ -378,26 +423,55 @@ export function renderFloatBlock(pdfPage, pagedBlock, geo, fontMap, imageMap, pd
|
|
|
378
423
|
width: fd.imageRenderWidth,
|
|
379
424
|
height: fd.imageRenderHeight,
|
|
380
425
|
});
|
|
381
|
-
// Draw text lines
|
|
382
|
-
const pdfFont = fontMap.get(fd.textFontKey);
|
|
383
|
-
if (!pdfFont)
|
|
384
|
-
throw new PretextPdfError('FONT_NOT_LOADED', `Float text font key "${fd.textFontKey}" not found in fontMap. This is a bug — font loading should have caught this.`);
|
|
385
|
-
const fontHeight = pdfFont.heightAtSize(fd.textFontSize);
|
|
386
|
-
const [r, g, b] = hexToRgb(fd.textColor);
|
|
426
|
+
// Draw text lines (rich or plain)
|
|
387
427
|
const textBaseX = geo.margins.left + fd.textColX;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
428
|
+
if (fd.richFloatLines && fd.richFloatLines.length > 0) {
|
|
429
|
+
let cumY = 0;
|
|
430
|
+
for (const richLine of fd.richFloatLines) {
|
|
431
|
+
const lineAbsY = baseAbsY + cumY;
|
|
432
|
+
for (const fragment of richLine.fragments) {
|
|
433
|
+
if (!fragment.text || fragment.text.trim() === '')
|
|
434
|
+
continue;
|
|
435
|
+
const pdfFont = fontMap.get(fragment.fontKey);
|
|
436
|
+
if (!pdfFont)
|
|
437
|
+
throw new PretextPdfError('FONT_NOT_LOADED', `Float rich text font "${fragment.fontKey}" not found in fontMap.`);
|
|
438
|
+
const fontHeight = pdfFont.heightAtSize(fragment.fontSize);
|
|
439
|
+
const [r, g, b] = hexToRgb(fragment.color);
|
|
440
|
+
const drawX = textBaseX + fragment.x;
|
|
441
|
+
const pdfY = toPdfY(lineAbsY, fontHeight, geo.pageHeight) + (fragment.yOffset ?? 0);
|
|
442
|
+
const drawText = fragment.text.trimEnd();
|
|
443
|
+
if (fragment.letterSpacing && fragment.letterSpacing > 0) {
|
|
444
|
+
let cx = drawX;
|
|
445
|
+
for (const ch of drawText) {
|
|
446
|
+
pdfPage.drawText(ch, { x: cx, y: pdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
447
|
+
cx += pdfFont.widthOfTextAtSize(ch, fragment.fontSize) + fragment.letterSpacing;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
pdfPage.drawText(drawText, { x: drawX, y: pdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
452
|
+
}
|
|
453
|
+
const fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
|
|
454
|
+
drawTextDecoration(pdfPage, drawX, fragWidth, pdfY, fragment.fontSize, pdfFont, [r, g, b], { underline: fragment.underline ?? false, strikethrough: fragment.strikethrough ?? false });
|
|
455
|
+
if (fragment.url)
|
|
456
|
+
addLinkAnnotation(pdfDoc, pdfPage, drawX, pdfY, fragWidth, fragment.fontSize, fragment.url);
|
|
457
|
+
}
|
|
458
|
+
cumY += richLine.lineHeight;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
const pdfFont = fontMap.get(fd.textFontKey);
|
|
463
|
+
if (!pdfFont)
|
|
464
|
+
throw new PretextPdfError('FONT_NOT_LOADED', `Float text font key "${fd.textFontKey}" not found in fontMap. This is a bug — font loading should have caught this.`);
|
|
465
|
+
const fontHeight = pdfFont.heightAtSize(fd.textFontSize);
|
|
466
|
+
const [r, g, b] = hexToRgb(fd.textColor);
|
|
467
|
+
for (let i = 0; i < fd.textLines.length; i++) {
|
|
468
|
+
const line = fd.textLines[i];
|
|
469
|
+
if (line.text === '')
|
|
470
|
+
continue;
|
|
471
|
+
const lineAbsY = baseAbsY + (i * fd.textLineHeight);
|
|
472
|
+
const pdfY = toPdfY(lineAbsY, fontHeight, geo.pageHeight);
|
|
473
|
+
pdfPage.drawText(line.text.trimEnd(), { x: textBaseX, y: pdfY, size: fd.textFontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
474
|
+
}
|
|
401
475
|
}
|
|
402
476
|
}
|
|
403
477
|
// ─── Float group block rendering ──────────────────────────────────────────────
|
|
@@ -427,7 +501,7 @@ export function renderFloatGroup(pdfPage, pagedBlock, geo, fontMap, imageMap, pd
|
|
|
427
501
|
// Draw plain lines (plain-text fallback for rich-paragraphs)
|
|
428
502
|
for (let i = 0; i < textItem.lines.length; i++) {
|
|
429
503
|
const line = textItem.lines[i];
|
|
430
|
-
if (
|
|
504
|
+
if (line.text === '')
|
|
431
505
|
continue;
|
|
432
506
|
const lineAbsY = baseAbsY + textItem.yOffsetFromTop + (i * textItem.lineHeight);
|
|
433
507
|
const pdfY = toPdfY(lineAbsY, fontHeight, geo.pageHeight);
|
|
@@ -495,17 +569,46 @@ export function renderCodeBlock(pdfPage, pagedBlock, geo, fontMap) {
|
|
|
495
569
|
const fontHeight = pdfFont.heightAtSize(fontSize);
|
|
496
570
|
const [r, g, b] = hexToRgb(textColorHex);
|
|
497
571
|
const textX = geo.margins.left + padding;
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
572
|
+
// Syntax highlighting: tokenize if language is set and highlight.js is available
|
|
573
|
+
const highlightTokens = measuredBlock.codeHighlightTokens;
|
|
574
|
+
if (highlightTokens && highlightTokens.length > 0) {
|
|
575
|
+
// Render with per-token colors
|
|
576
|
+
for (let i = 0; i < lines.length; i++) {
|
|
577
|
+
const lineYFromPageTop = boxAbsY + paddingTop + i * lineHeight;
|
|
578
|
+
const pdfY = toPdfY(lineYFromPageTop, fontHeight, geo.pageHeight);
|
|
579
|
+
const lineTokens = highlightTokens[startLine + i];
|
|
580
|
+
if (!lineTokens)
|
|
581
|
+
continue;
|
|
582
|
+
let curX = textX;
|
|
583
|
+
for (const token of lineTokens) {
|
|
584
|
+
if (!token.text)
|
|
585
|
+
continue;
|
|
586
|
+
const [tr, tg, tb] = hexToRgb(token.color);
|
|
587
|
+
pdfPage.drawText(token.text, {
|
|
588
|
+
x: curX,
|
|
589
|
+
y: pdfY,
|
|
590
|
+
size: fontSize,
|
|
591
|
+
font: pdfFont,
|
|
592
|
+
color: rgb(tr, tg, tb),
|
|
593
|
+
});
|
|
594
|
+
curX += pdfFont.widthOfTextAtSize(token.text, fontSize);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
// Plain text rendering (no highlighting)
|
|
600
|
+
for (let i = 0; i < lines.length; i++) {
|
|
601
|
+
const line = lines[i];
|
|
602
|
+
const lineYFromPageTop = boxAbsY + paddingTop + i * lineHeight;
|
|
603
|
+
const pdfY = toPdfY(lineYFromPageTop, fontHeight, geo.pageHeight);
|
|
604
|
+
pdfPage.drawText(line.text.trimEnd(), {
|
|
605
|
+
x: textX,
|
|
606
|
+
y: pdfY,
|
|
607
|
+
size: fontSize,
|
|
608
|
+
font: pdfFont,
|
|
609
|
+
color: rgb(r, g, b),
|
|
610
|
+
});
|
|
611
|
+
}
|
|
509
612
|
}
|
|
510
613
|
}
|
|
511
614
|
// ─── Blockquote rendering ─────────────────────────────────────────────────────
|
|
@@ -639,7 +742,7 @@ export function renderCallout(pdfPage, pagedBlock, geo, fontMap) {
|
|
|
639
742
|
const boldFontKey = measuredBlock.fontKey.replace(/-400-/, '-700-');
|
|
640
743
|
const titleFont = fontMap.get(boldFontKey) ?? font;
|
|
641
744
|
const [tR, tG, tB] = hexToRgb(titleColor);
|
|
642
|
-
const titlePdfY =
|
|
745
|
+
const titlePdfY = toPdfY(currentAbsY + (fs * LINE_HEIGHT_COMPACT - fs) / 2, fontHeight, geo.pageHeight);
|
|
643
746
|
pdfPage.drawText(titleText, {
|
|
644
747
|
x: geo.margins.left + paddingH,
|
|
645
748
|
y: titlePdfY,
|
|
@@ -657,7 +760,7 @@ export function renderCallout(pdfPage, pagedBlock, geo, fontMap) {
|
|
|
657
760
|
currentAbsY += lh;
|
|
658
761
|
continue;
|
|
659
762
|
}
|
|
660
|
-
const linePdfY =
|
|
763
|
+
const linePdfY = toPdfY(currentAbsY + (lh - fs) / 2, fontHeight, geo.pageHeight);
|
|
661
764
|
pdfPage.drawText(line.text.trimEnd(), {
|
|
662
765
|
x: geo.margins.left + paddingH,
|
|
663
766
|
y: linePdfY,
|
|
@@ -672,6 +775,7 @@ export function renderCallout(pdfPage, pagedBlock, geo, fontMap) {
|
|
|
672
775
|
export function renderRichParagraph(pdfPage, pagedBlock, geo, fontMap, pdfDoc, footnoteNumbering) {
|
|
673
776
|
const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
|
|
674
777
|
const { element, richLines, lineHeight, fontSize } = measuredBlock;
|
|
778
|
+
const tabularNumbers = element.type === 'rich-paragraph' && element.tabularNumbers === true;
|
|
675
779
|
if (!richLines || richLines.length === 0)
|
|
676
780
|
return;
|
|
677
781
|
// Only render the lines on this page chunk
|
|
@@ -734,14 +838,23 @@ export function renderRichParagraph(pdfPage, pagedBlock, geo, fontMap, pdfDoc, f
|
|
|
734
838
|
}
|
|
735
839
|
const fragmentPdfY = basePdfY + (fragment.yOffset ?? 0);
|
|
736
840
|
const drawText = fragment.text.trimEnd();
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
841
|
+
let fragWidth;
|
|
842
|
+
if (fragment.letterSpacing && fragment.letterSpacing > 0) {
|
|
843
|
+
let cx = drawX;
|
|
844
|
+
for (const ch of drawText) {
|
|
845
|
+
pdfPage.drawText(ch, { x: cx, y: fragmentPdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
846
|
+
cx += pdfFont.widthOfTextAtSize(ch, fragment.fontSize) + fragment.letterSpacing;
|
|
847
|
+
}
|
|
848
|
+
fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize) + fragment.letterSpacing * (drawText.length - 1);
|
|
849
|
+
}
|
|
850
|
+
else if (tabularNumbers) {
|
|
851
|
+
drawTabularText(pdfPage, drawText, drawX, fragmentPdfY, fragment.fontSize, pdfFont, rgb(r, g, b));
|
|
852
|
+
fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
pdfPage.drawText(drawText, { x: drawX, y: fragmentPdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
856
|
+
fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
|
|
857
|
+
}
|
|
745
858
|
drawTextDecoration(pdfPage, drawX, fragWidth, fragmentPdfY, fragment.fontSize, pdfFont, [r, g, b], { underline: fragment.underline ?? false, strikethrough: fragment.strikethrough ?? false });
|
|
746
859
|
if (fragment.url) {
|
|
747
860
|
addLinkAnnotation(pdfDoc, pdfPage, drawX, fragmentPdfY, fragWidth, fragment.fontSize, fragment.url);
|
|
@@ -786,14 +899,23 @@ export function renderRichParagraph(pdfPage, pagedBlock, geo, fontMap, pdfDoc, f
|
|
|
786
899
|
}
|
|
787
900
|
const fragmentPdfY = basePdfY + (fragment.yOffset ?? 0);
|
|
788
901
|
const drawText = fragment.text.trimEnd();
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
902
|
+
let fragWidth;
|
|
903
|
+
if (fragment.letterSpacing && fragment.letterSpacing > 0) {
|
|
904
|
+
let cx = drawX;
|
|
905
|
+
for (const ch of drawText) {
|
|
906
|
+
pdfPage.drawText(ch, { x: cx, y: fragmentPdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
907
|
+
cx += pdfFont.widthOfTextAtSize(ch, fragment.fontSize) + fragment.letterSpacing;
|
|
908
|
+
}
|
|
909
|
+
fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize) + fragment.letterSpacing * (drawText.length - 1);
|
|
910
|
+
}
|
|
911
|
+
else if (tabularNumbers) {
|
|
912
|
+
drawTabularText(pdfPage, drawText, drawX, fragmentPdfY, fragment.fontSize, pdfFont, rgb(r, g, b));
|
|
913
|
+
fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
pdfPage.drawText(drawText, { x: drawX, y: fragmentPdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
917
|
+
fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
|
|
918
|
+
}
|
|
797
919
|
drawTextDecoration(pdfPage, drawX, fragWidth, fragmentPdfY, fragment.fontSize, pdfFont, [r, g, b], { underline: fragment.underline ?? false, strikethrough: fragment.strikethrough ?? false });
|
|
798
920
|
if (fragment.url) {
|
|
799
921
|
addLinkAnnotation(pdfDoc, pdfPage, drawX, fragmentPdfY, fragWidth, fragment.fontSize, fragment.url);
|
|
@@ -821,7 +943,7 @@ export function renderFootnoteZone(pdfPage, footnoteItems, zoneHeight, fontMap,
|
|
|
821
943
|
let currentPdfY = separatorY - SEPARATOR_PADDING;
|
|
822
944
|
for (const { def, number } of footnoteItems) {
|
|
823
945
|
const fontSize = def.fontSize ?? Math.max(8, defaultFontSize - 2);
|
|
824
|
-
const lineHeight = fontSize *
|
|
946
|
+
const lineHeight = fontSize * LINE_HEIGHT_BODY;
|
|
825
947
|
const fontFamily = def.fontFamily ?? doc.defaultFont ?? 'Inter';
|
|
826
948
|
const fontKey = buildFontKey(fontFamily, 400, 'normal');
|
|
827
949
|
const pdfFont = fontMap.get(fontKey);
|
|
@@ -841,8 +963,8 @@ export function renderFootnoteZone(pdfPage, footnoteItems, zoneHeight, fontMap,
|
|
|
841
963
|
}
|
|
842
964
|
}
|
|
843
965
|
// ─── Header / Footer rendering ────────────────────────────────────────────────
|
|
844
|
-
export function renderHeaderFooter(pdfPage, spec, pageNumber, totalPages, geo, fontMap, position) {
|
|
845
|
-
const text = resolveTokens(spec.text, pageNumber, totalPages);
|
|
966
|
+
export function renderHeaderFooter(pdfPage, spec, pageNumber, totalPages, geo, fontMap, position, extra) {
|
|
967
|
+
const text = resolveTokens(spec.text, pageNumber, totalPages, extra);
|
|
846
968
|
const fontSize = spec.fontSize ?? 10;
|
|
847
969
|
const align = spec.align ?? 'center';
|
|
848
970
|
const fontKey = `${spec.fontFamily ?? 'Inter'}-${spec.fontWeight ?? 400}-normal`;
|