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/validate.js
CHANGED
|
@@ -35,6 +35,25 @@ import { resolvePageDimensions } from './page-sizes.js';
|
|
|
35
35
|
const RTL_REGEX = /[\u0590-\u08FF\uFB1D-\uFB4F\uFB50-\uFDFF\uFE70-\uFEFF\u{10800}-\u{10CFF}\u{10D00}-\u{10D3F}\u{10E80}-\u{10EFF}\u{10F30}-\u{10FFF}\u{1E800}-\u{1E95F}\u{1EC70}-\u{1ECBF}\u{1EE00}-\u{1EEFF}]/u;
|
|
36
36
|
/** Valid 6-digit hex color */
|
|
37
37
|
const HEX_COLOR_REGEX = /^#[0-9A-Fa-f]{6}$/;
|
|
38
|
+
/** Allowed URL schemes for hyperlinks — blocks javascript:, data:, vbscript: */
|
|
39
|
+
const SAFE_URL_SCHEME = /^(https?|mailto|ftp|#)/i;
|
|
40
|
+
/** BCP47 language tag pattern for hyphenation.language — prevents dynamic-import path injection */
|
|
41
|
+
const LANGUAGE_TAG_REGEX = /^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{2,8})*$/;
|
|
42
|
+
/** Validate a hyperlink URL — throws VALIDATION_ERROR for unsafe schemes */
|
|
43
|
+
function validateUrl(url, prefix) {
|
|
44
|
+
if (!SAFE_URL_SCHEME.test(url)) {
|
|
45
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix}: URL scheme not allowed — only http, https, mailto, ftp, and anchor (#) links are permitted. Got: "${url.slice(0, 60)}"`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Validate a metadata string field — rejects control chars and enforces length */
|
|
49
|
+
function validateMetadataString(value, fieldName) {
|
|
50
|
+
if (value.length > 1000) {
|
|
51
|
+
throw new PretextPdfError('VALIDATION_ERROR', `metadata.${fieldName} must not exceed 1000 characters`);
|
|
52
|
+
}
|
|
53
|
+
if (/[\x00\r\n]/.test(value)) {
|
|
54
|
+
throw new PretextPdfError('VALIDATION_ERROR', `metadata.${fieldName} must not contain null bytes or newline characters`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
38
57
|
/** Valid column width: positive number OR '2*', '*', '1.5*' format */
|
|
39
58
|
const STAR_WIDTH_REGEX = /^(\d*\.?\d+)?\*$/;
|
|
40
59
|
/** Families always available without explicit doc.fonts entry */
|
|
@@ -183,8 +202,8 @@ export function validate(doc) {
|
|
|
183
202
|
if (wm.opacity !== undefined && (typeof wm.opacity !== 'number' || wm.opacity < 0 || wm.opacity > 1 || !isFinite(wm.opacity))) {
|
|
184
203
|
throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark.opacity must be a number 0.0–1.0');
|
|
185
204
|
}
|
|
186
|
-
if (wm.fontSize !== undefined && (typeof wm.fontSize !== 'number' || wm.fontSize <= 0 || !isFinite(wm.fontSize))) {
|
|
187
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark.fontSize must be a positive finite number');
|
|
205
|
+
if (wm.fontSize !== undefined && (typeof wm.fontSize !== 'number' || wm.fontSize <= 0 || wm.fontSize > 500 || !isFinite(wm.fontSize))) {
|
|
206
|
+
throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark.fontSize must be a positive finite number and <= 500');
|
|
188
207
|
}
|
|
189
208
|
if (wm.fontWeight !== undefined && ![400, 700].includes(wm.fontWeight)) {
|
|
190
209
|
throw new PretextPdfError('VALIDATION_ERROR', "doc.watermark.fontWeight must be 400 or 700");
|
|
@@ -207,6 +226,9 @@ export function validate(doc) {
|
|
|
207
226
|
if (enc.userPassword !== undefined && typeof enc.userPassword !== 'string') {
|
|
208
227
|
throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.userPassword must be a string if provided');
|
|
209
228
|
}
|
|
229
|
+
if (enc.userPassword !== undefined && enc.userPassword === '') {
|
|
230
|
+
throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.userPassword must not be an empty string — an empty password provides no access control. Omit userPassword for permissions-only encryption.');
|
|
231
|
+
}
|
|
210
232
|
if (enc.ownerPassword !== undefined && typeof enc.ownerPassword !== 'string') {
|
|
211
233
|
throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.ownerPassword must be a string if provided');
|
|
212
234
|
}
|
|
@@ -273,6 +295,9 @@ export function validate(doc) {
|
|
|
273
295
|
if (!h.language || typeof h.language !== 'string') {
|
|
274
296
|
throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.language is required (e.g. "en-us")');
|
|
275
297
|
}
|
|
298
|
+
if (!LANGUAGE_TAG_REGEX.test(h.language)) {
|
|
299
|
+
throw new PretextPdfError('VALIDATION_ERROR', `doc.hyphenation.language must be a BCP47 tag like "en-us" or "de" (letters and hyphens only). Got: "${h.language}"`);
|
|
300
|
+
}
|
|
276
301
|
if (h.minWordLength !== undefined && (h.minWordLength < 2 || h.minWordLength > 20)) {
|
|
277
302
|
throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.minWordLength must be 2–20');
|
|
278
303
|
}
|
|
@@ -289,9 +314,17 @@ export function validate(doc) {
|
|
|
289
314
|
if (m.language !== undefined && (typeof m.language !== 'string' || m.language.trim() === '')) {
|
|
290
315
|
throw new PretextPdfError('VALIDATION_ERROR', 'metadata.language must be a non-empty string (BCP47 tag e.g. "en-US")');
|
|
291
316
|
}
|
|
317
|
+
if (m.language !== undefined && typeof m.language === 'string')
|
|
318
|
+
validateMetadataString(m.language, 'language');
|
|
292
319
|
if (m.producer !== undefined && (typeof m.producer !== 'string' || m.producer.trim() === '')) {
|
|
293
320
|
throw new PretextPdfError('VALIDATION_ERROR', 'metadata.producer must be a non-empty string');
|
|
294
321
|
}
|
|
322
|
+
// Validate free-text fields for injection chars and length
|
|
323
|
+
for (const field of ['title', 'author', 'subject', 'keywords', 'creator', 'producer']) {
|
|
324
|
+
const val = m[field];
|
|
325
|
+
if (val !== undefined && typeof val === 'string')
|
|
326
|
+
validateMetadataString(val, field);
|
|
327
|
+
}
|
|
295
328
|
}
|
|
296
329
|
// validate each content element
|
|
297
330
|
const loadedFamilies = new Set([
|
|
@@ -362,6 +395,9 @@ function validateFontReferences(doc, loadedFamilies) {
|
|
|
362
395
|
loadedVariants.add(`${f.family}-${f.weight ?? 400}-${f.style ?? 'normal'}`);
|
|
363
396
|
}
|
|
364
397
|
const requireFamily = (family, context) => {
|
|
398
|
+
if (!/^[a-zA-Z0-9 ._+\-]+$/.test(family)) {
|
|
399
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${context}: font family name "${family}" contains invalid characters. Use only letters, digits, spaces, hyphens, and underscores.`);
|
|
400
|
+
}
|
|
365
401
|
if (!loadedFamilies.has(family)) {
|
|
366
402
|
throw new PretextPdfError('FONT_NOT_LOADED', `${context}: font family '${family}' is not loaded. Add { family: '${family}', src: '/path/to.ttf' } to doc.fonts, or remove the fontFamily reference to use the default ('${defaultFamily}').`);
|
|
367
403
|
}
|
|
@@ -517,6 +553,8 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
517
553
|
if (el.url !== undefined && (typeof el.url !== 'string' || el.url.trim() === '')) {
|
|
518
554
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'url' must be a non-empty string if provided`);
|
|
519
555
|
}
|
|
556
|
+
if (el.url !== undefined && typeof el.url === 'string')
|
|
557
|
+
validateUrl(el.url, `${prefix} (paragraph) url`);
|
|
520
558
|
if (el.letterSpacing !== undefined && (typeof el.letterSpacing !== 'number' || el.letterSpacing < 0 || !isFinite(el.letterSpacing) || el.letterSpacing > 200)) {
|
|
521
559
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'letterSpacing' must be a non-negative finite number and <= 200`);
|
|
522
560
|
}
|
|
@@ -524,6 +562,15 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
524
562
|
if (!el.annotation.contents || el.annotation.contents.trim() === '') {
|
|
525
563
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): annotation.contents is required and must be non-empty`);
|
|
526
564
|
}
|
|
565
|
+
if (el.annotation.contents.length > 5000) {
|
|
566
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): annotation.contents must be 5000 characters or fewer`);
|
|
567
|
+
}
|
|
568
|
+
if (el.annotation.color !== undefined && !/^#[0-9a-fA-F]{6}$/.test(el.annotation.color)) {
|
|
569
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): annotation.color must be a 6-digit hex color (e.g. "#FFFF00"). Got: "${el.annotation.color}"`);
|
|
570
|
+
}
|
|
571
|
+
if (el.annotation.author !== undefined && el.annotation.author.length > 100) {
|
|
572
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): annotation.author must be 100 characters or fewer`);
|
|
573
|
+
}
|
|
527
574
|
}
|
|
528
575
|
break;
|
|
529
576
|
}
|
|
@@ -570,6 +617,8 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
570
617
|
if (el.url !== undefined && (typeof el.url !== 'string' || el.url.trim() === '')) {
|
|
571
618
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'url' must be a non-empty string if provided`);
|
|
572
619
|
}
|
|
620
|
+
if (el.url !== undefined && typeof el.url === 'string')
|
|
621
|
+
validateUrl(el.url, `${prefix} (heading) url`);
|
|
573
622
|
if (el.anchor !== undefined && (typeof el.anchor !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(el.anchor))) {
|
|
574
623
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'anchor' must be alphanumeric with hyphens/underscores only. Got: '${el.anchor}'`);
|
|
575
624
|
}
|
|
@@ -580,12 +629,21 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
580
629
|
if (!el.annotation.contents || el.annotation.contents.trim() === '') {
|
|
581
630
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): annotation.contents is required and must be non-empty`);
|
|
582
631
|
}
|
|
632
|
+
if (el.annotation.contents.length > 5000) {
|
|
633
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): annotation.contents must be 5000 characters or fewer`);
|
|
634
|
+
}
|
|
635
|
+
if (el.annotation.color !== undefined && !/^#[0-9a-fA-F]{6}$/.test(el.annotation.color)) {
|
|
636
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): annotation.color must be a 6-digit hex color (e.g. "#FFFF00"). Got: "${el.annotation.color}"`);
|
|
637
|
+
}
|
|
638
|
+
if (el.annotation.author !== undefined && el.annotation.author.length > 100) {
|
|
639
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): annotation.author must be 100 characters or fewer`);
|
|
640
|
+
}
|
|
583
641
|
}
|
|
584
642
|
break;
|
|
585
643
|
}
|
|
586
644
|
case 'spacer': {
|
|
587
|
-
if (typeof el.height !== 'number' || el.height < 0 || !isFinite(el.height)) {
|
|
588
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (spacer): 'height' must be a non-negative finite number`);
|
|
645
|
+
if (typeof el.height !== 'number' || el.height < 0 || el.height > 14400 || !isFinite(el.height)) {
|
|
646
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (spacer): 'height' must be a non-negative finite number and <= 14400pt (200 inches)`);
|
|
589
647
|
}
|
|
590
648
|
break;
|
|
591
649
|
}
|
|
@@ -629,12 +687,36 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
629
687
|
if (headerRowCount >= el.rows.length) {
|
|
630
688
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): table must have at least 1 non-header body row. headerRows=${headerRowCount}, total rows=${el.rows.length}`);
|
|
631
689
|
}
|
|
690
|
+
// Build span occupancy grid for colspan validation (rowspan cells occupy future rows)
|
|
691
|
+
const spanOccupied = new Set();
|
|
692
|
+
for (let ri = 0; ri < el.rows.length; ri++) {
|
|
693
|
+
const row = el.rows[ri];
|
|
694
|
+
let ci = 0;
|
|
695
|
+
for (const cell of row.cells) {
|
|
696
|
+
while (spanOccupied.has(`${ri},${ci}`))
|
|
697
|
+
ci++;
|
|
698
|
+
const cs = cell.colspan ?? 1;
|
|
699
|
+
const rs = cell.rowspan ?? 1;
|
|
700
|
+
for (let r2 = ri + 1; r2 < ri + rs; r2++) {
|
|
701
|
+
for (let c2 = ci; c2 < ci + cs; c2++) {
|
|
702
|
+
spanOccupied.add(`${r2},${c2}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
ci += cs;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
632
708
|
for (let ri = 0; ri < el.rows.length; ri++) {
|
|
633
709
|
const row = el.rows[ri];
|
|
634
710
|
if (!Array.isArray(row.cells)) {
|
|
635
711
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells must be an array`);
|
|
636
712
|
}
|
|
637
|
-
//
|
|
713
|
+
// Count how many columns in this row are occupied by rowspan cells from above
|
|
714
|
+
let occupiedCols = 0;
|
|
715
|
+
for (let ci = 0; ci < colCount; ci++) {
|
|
716
|
+
if (spanOccupied.has(`${ri},${ci}`))
|
|
717
|
+
occupiedCols++;
|
|
718
|
+
}
|
|
719
|
+
// Validate colspan sum equals colCount minus occupied columns
|
|
638
720
|
let colspanSum = 0;
|
|
639
721
|
for (let cellI = 0; cellI < row.cells.length; cellI++) {
|
|
640
722
|
const cell = row.cells[cellI];
|
|
@@ -643,9 +725,13 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
643
725
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].colspan must be a positive integer`);
|
|
644
726
|
}
|
|
645
727
|
colspanSum += cs;
|
|
728
|
+
const rs = cell.rowspan ?? 1;
|
|
729
|
+
if (typeof rs !== 'number' || rs < 1 || !Number.isInteger(rs)) {
|
|
730
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].rowspan must be a positive integer`);
|
|
731
|
+
}
|
|
646
732
|
}
|
|
647
|
-
if (colspanSum !== colCount) {
|
|
648
|
-
throw new PretextPdfError('COLSPAN_OVERFLOW', `${prefix} (table): rows[${ri}] colspan sum is ${colspanSum} but
|
|
733
|
+
if (colspanSum !== colCount - occupiedCols) {
|
|
734
|
+
throw new PretextPdfError('COLSPAN_OVERFLOW', `${prefix} (table): rows[${ri}] colspan sum is ${colspanSum} but expected ${colCount - occupiedCols} (${colCount} columns minus ${occupiedCols} occupied by rowspan). Sum of explicit cell colspans must cover only unoccupied columns.`);
|
|
649
735
|
}
|
|
650
736
|
for (let cellI = 0; cellI < row.cells.length; cellI++) {
|
|
651
737
|
const cell = row.cells[cellI];
|
|
@@ -667,8 +753,17 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
667
753
|
if (cell.bgColor !== undefined && !HEX_COLOR_REGEX.test(cell.bgColor)) {
|
|
668
754
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].bgColor must be a 6-digit hex string`);
|
|
669
755
|
}
|
|
756
|
+
if (cell.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(cell.dir)) {
|
|
757
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].dir must be 'ltr', 'rtl', or 'auto'`);
|
|
758
|
+
}
|
|
759
|
+
if (cell.align !== undefined && !['left', 'center', 'right'].includes(cell.align)) {
|
|
760
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].align must be 'left', 'center', or 'right'`);
|
|
761
|
+
}
|
|
670
762
|
}
|
|
671
763
|
}
|
|
764
|
+
if (el.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(el.dir)) {
|
|
765
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'dir' must be 'ltr', 'rtl', or 'auto'`);
|
|
766
|
+
}
|
|
672
767
|
if (el.borderColor !== undefined && !HEX_COLOR_REGEX.test(el.borderColor)) {
|
|
673
768
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'borderColor' must be a 6-digit hex string`);
|
|
674
769
|
}
|
|
@@ -678,14 +773,14 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
678
773
|
if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
|
|
679
774
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'fontSize' must be a positive finite number`);
|
|
680
775
|
}
|
|
681
|
-
if (el.borderWidth !== undefined && (typeof el.borderWidth !== 'number' || el.borderWidth < 0)) {
|
|
682
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'borderWidth' must be a non-negative number`);
|
|
776
|
+
if (el.borderWidth !== undefined && (typeof el.borderWidth !== 'number' || el.borderWidth < 0 || el.borderWidth > 50)) {
|
|
777
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'borderWidth' must be a non-negative number <= 50`);
|
|
683
778
|
}
|
|
684
|
-
if (el.cellPaddingH !== undefined && (typeof el.cellPaddingH !== 'number' || el.cellPaddingH < 0)) {
|
|
685
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'cellPaddingH' must be a non-negative number`);
|
|
779
|
+
if (el.cellPaddingH !== undefined && (typeof el.cellPaddingH !== 'number' || el.cellPaddingH < 0 || el.cellPaddingH > 200)) {
|
|
780
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'cellPaddingH' must be a non-negative number <= 200`);
|
|
686
781
|
}
|
|
687
|
-
if (el.cellPaddingV !== undefined && (typeof el.cellPaddingV !== 'number' || el.cellPaddingV < 0)) {
|
|
688
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'cellPaddingV' must be a non-negative number`);
|
|
782
|
+
if (el.cellPaddingV !== undefined && (typeof el.cellPaddingV !== 'number' || el.cellPaddingV < 0 || el.cellPaddingV > 200)) {
|
|
783
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'cellPaddingV' must be a non-negative number <= 200`);
|
|
689
784
|
}
|
|
690
785
|
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
691
786
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'spaceAfter' must be a non-negative finite number`);
|
|
@@ -699,6 +794,9 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
699
794
|
if (!el.src || (typeof el.src !== 'string' && !(el.src instanceof Uint8Array))) {
|
|
700
795
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'src' must be a non-empty string path or Uint8Array`);
|
|
701
796
|
}
|
|
797
|
+
if (typeof el.src === 'string' && (el.src.startsWith('\\\\') || el.src.startsWith('//'))) {
|
|
798
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'src' must not be a UNC/network path`);
|
|
799
|
+
}
|
|
702
800
|
const fmt = el.format ?? 'auto';
|
|
703
801
|
if (fmt !== 'png' && fmt !== 'jpg' && fmt !== 'auto') {
|
|
704
802
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'format' must be 'png', 'jpg', or 'auto'. Got: '${String(el.format)}'`);
|
|
@@ -722,12 +820,18 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
722
820
|
if (el.float !== undefined && el.float !== 'left' && el.float !== 'right') {
|
|
723
821
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'float' must be 'left' or 'right'`);
|
|
724
822
|
}
|
|
725
|
-
if (el.float !== undefined && (!el.floatText || el.floatText.trim() === '')) {
|
|
726
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatText' is required when 'float' is set`);
|
|
823
|
+
if (el.float !== undefined && (!el.floatText || el.floatText.trim() === '') && (!el.floatSpans || el.floatSpans.length === 0)) {
|
|
824
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatText' or 'floatSpans' is required when 'float' is set`);
|
|
825
|
+
}
|
|
826
|
+
if (el.floatText !== undefined && el.floatSpans !== undefined) {
|
|
827
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatText' and 'floatSpans' are mutually exclusive — use one or the other`);
|
|
727
828
|
}
|
|
728
829
|
if (el.floatText !== undefined && el.float === undefined) {
|
|
729
830
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatText' has no effect without 'float'`);
|
|
730
831
|
}
|
|
832
|
+
if (el.floatSpans !== undefined && el.float === undefined) {
|
|
833
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatSpans' has no effect without 'float'`);
|
|
834
|
+
}
|
|
731
835
|
if (el.floatWidth !== undefined && (typeof el.floatWidth !== 'number' || el.floatWidth <= 0 || !isFinite(el.floatWidth))) {
|
|
732
836
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatWidth' must be a positive finite number`);
|
|
733
837
|
}
|
|
@@ -748,8 +852,15 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
748
852
|
if (hasSvg && !el.svg.trim().startsWith('<')) {
|
|
749
853
|
throw new PretextPdfError('SVG_INVALID_MARKUP', `${prefix} (svg): 'svg' must be valid SVG markup (must start with '<')`);
|
|
750
854
|
}
|
|
751
|
-
if (hasSrc
|
|
752
|
-
|
|
855
|
+
if (hasSrc) {
|
|
856
|
+
const src = el.src;
|
|
857
|
+
const isUNC = src.startsWith('\\\\') || src.startsWith('//');
|
|
858
|
+
if (isUNC) {
|
|
859
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'src' must not be a UNC/network path`);
|
|
860
|
+
}
|
|
861
|
+
if (!src.startsWith('/') && !src.startsWith('https://') && !/^[A-Za-z]:[/\\]/.test(src)) {
|
|
862
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'src' must be an absolute file path or an https:// URL`);
|
|
863
|
+
}
|
|
753
864
|
}
|
|
754
865
|
if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
|
|
755
866
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'width' must be a positive finite number`);
|
|
@@ -768,6 +879,87 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
768
879
|
}
|
|
769
880
|
break;
|
|
770
881
|
}
|
|
882
|
+
case 'qr-code': {
|
|
883
|
+
if (typeof el.data !== 'string' || el.data.trim().length === 0) {
|
|
884
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'data' must be a non-empty string`);
|
|
885
|
+
}
|
|
886
|
+
if (el.data.length > 2953) {
|
|
887
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'data' exceeds maximum QR capacity of 2953 characters (got ${el.data.length})`);
|
|
888
|
+
}
|
|
889
|
+
if (el.errorCorrectionLevel !== undefined && !['L', 'M', 'Q', 'H'].includes(el.errorCorrectionLevel)) {
|
|
890
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'errorCorrectionLevel' must be 'L', 'M', 'Q', or 'H'`);
|
|
891
|
+
}
|
|
892
|
+
if (el.size !== undefined && (typeof el.size !== 'number' || el.size <= 0 || !isFinite(el.size))) {
|
|
893
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'size' must be a positive finite number`);
|
|
894
|
+
}
|
|
895
|
+
if (el.foreground !== undefined && !HEX_COLOR_REGEX.test(el.foreground)) {
|
|
896
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'foreground' must be a 6-digit hex string like '#000000'`);
|
|
897
|
+
}
|
|
898
|
+
if (el.background !== undefined && !HEX_COLOR_REGEX.test(el.background)) {
|
|
899
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'background' must be a 6-digit hex string like '#ffffff'`);
|
|
900
|
+
}
|
|
901
|
+
if (el.margin !== undefined && (typeof el.margin !== 'number' || el.margin < 0 || !isFinite(el.margin))) {
|
|
902
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'margin' must be a non-negative finite number`);
|
|
903
|
+
}
|
|
904
|
+
if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
|
|
905
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'align' must be 'left', 'center', or 'right'`);
|
|
906
|
+
}
|
|
907
|
+
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
908
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'spaceAfter' must be a non-negative finite number`);
|
|
909
|
+
}
|
|
910
|
+
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
911
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'spaceBefore' must be a non-negative finite number`);
|
|
912
|
+
}
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
case 'barcode': {
|
|
916
|
+
if (typeof el.symbology !== 'string' || el.symbology.trim().length === 0) {
|
|
917
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'symbology' must be a non-empty string (e.g. 'ean13', 'code128')`);
|
|
918
|
+
}
|
|
919
|
+
if (typeof el.data !== 'string' || el.data.trim().length === 0) {
|
|
920
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'data' must be a non-empty string`);
|
|
921
|
+
}
|
|
922
|
+
if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
|
|
923
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'width' must be a positive finite number`);
|
|
924
|
+
}
|
|
925
|
+
if (el.height !== undefined && (typeof el.height !== 'number' || el.height <= 0 || !isFinite(el.height))) {
|
|
926
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'height' must be a positive finite number`);
|
|
927
|
+
}
|
|
928
|
+
if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
|
|
929
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'align' must be 'left', 'center', or 'right'`);
|
|
930
|
+
}
|
|
931
|
+
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
932
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'spaceAfter' must be a non-negative finite number`);
|
|
933
|
+
}
|
|
934
|
+
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
935
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'spaceBefore' must be a non-negative finite number`);
|
|
936
|
+
}
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
case 'chart': {
|
|
940
|
+
if (el.spec === null || typeof el.spec !== 'object' || Array.isArray(el.spec)) {
|
|
941
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'spec' must be a plain vega-lite specification object`);
|
|
942
|
+
}
|
|
943
|
+
if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
|
|
944
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'width' must be a positive finite number`);
|
|
945
|
+
}
|
|
946
|
+
if (el.height !== undefined && (typeof el.height !== 'number' || el.height <= 0 || !isFinite(el.height))) {
|
|
947
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'height' must be a positive finite number`);
|
|
948
|
+
}
|
|
949
|
+
if (el.caption !== undefined && typeof el.caption !== 'string') {
|
|
950
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'caption' must be a string`);
|
|
951
|
+
}
|
|
952
|
+
if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
|
|
953
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'align' must be 'left', 'center', or 'right'`);
|
|
954
|
+
}
|
|
955
|
+
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
956
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'spaceAfter' must be a non-negative finite number`);
|
|
957
|
+
}
|
|
958
|
+
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
959
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'spaceBefore' must be a non-negative finite number`);
|
|
960
|
+
}
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
771
963
|
case 'list': {
|
|
772
964
|
if (el.style !== 'ordered' && el.style !== 'unordered') {
|
|
773
965
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'style' must be 'ordered' or 'unordered'`);
|
|
@@ -797,8 +989,8 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
797
989
|
if (typeof nested.text !== 'string' || nested.text.trim() === '') {
|
|
798
990
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].items[${ni}].text must be a non-empty string`);
|
|
799
991
|
}
|
|
800
|
-
if (nested.items
|
|
801
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].items[${ni}]
|
|
992
|
+
if (nested.items !== undefined) {
|
|
993
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].items[${ni}].items is not allowed — maximum nesting depth is 2 levels`);
|
|
802
994
|
}
|
|
803
995
|
}
|
|
804
996
|
}
|
|
@@ -866,15 +1058,22 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
866
1058
|
if (span.url !== undefined && (typeof span.url !== 'string' || span.url.trim() === '')) {
|
|
867
1059
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].url must be a non-empty string if provided`);
|
|
868
1060
|
}
|
|
1061
|
+
if (span.url !== undefined && typeof span.url === 'string')
|
|
1062
|
+
validateUrl(span.url, `${prefix} (rich-paragraph) spans[${si}].url`);
|
|
869
1063
|
if (span.href !== undefined && (typeof span.href !== 'string' || span.href.trim() === '')) {
|
|
870
1064
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].href must be a non-empty string if provided`);
|
|
871
1065
|
}
|
|
1066
|
+
if (span.href !== undefined && typeof span.href === 'string')
|
|
1067
|
+
validateUrl(span.href, `${prefix} (rich-paragraph) spans[${si}].href`);
|
|
872
1068
|
if (span.url !== undefined && span.href !== undefined) {
|
|
873
1069
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}] cannot have both 'url' and 'href' — use one or the other`);
|
|
874
1070
|
}
|
|
875
1071
|
if (span.verticalAlign !== undefined && span.verticalAlign !== 'superscript' && span.verticalAlign !== 'subscript') {
|
|
876
1072
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].verticalAlign must be "superscript" or "subscript"`);
|
|
877
1073
|
}
|
|
1074
|
+
if (span.letterSpacing !== undefined && (typeof span.letterSpacing !== 'number' || span.letterSpacing < 0 || !isFinite(span.letterSpacing) || span.letterSpacing > 200)) {
|
|
1075
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].letterSpacing must be a non-negative finite number and <= 200`);
|
|
1076
|
+
}
|
|
878
1077
|
}
|
|
879
1078
|
if (el.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(el.dir)) {
|
|
880
1079
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'dir' must be 'ltr', 'rtl', or 'auto'`);
|
|
@@ -936,6 +1135,19 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
936
1135
|
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
937
1136
|
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'spaceBefore' must be a non-negative finite number`);
|
|
938
1137
|
}
|
|
1138
|
+
if (el.language !== undefined && typeof el.language !== 'string') {
|
|
1139
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'language' must be a string`);
|
|
1140
|
+
}
|
|
1141
|
+
if (el.highlightTheme !== undefined) {
|
|
1142
|
+
if (typeof el.highlightTheme !== 'object' || el.highlightTheme === null) {
|
|
1143
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'highlightTheme' must be an object`);
|
|
1144
|
+
}
|
|
1145
|
+
for (const [k, v] of Object.entries(el.highlightTheme)) {
|
|
1146
|
+
if (v !== undefined && !HEX_COLOR_REGEX.test(v)) {
|
|
1147
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): highlightTheme.${k} must be a 6-digit hex string`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
939
1151
|
break;
|
|
940
1152
|
}
|
|
941
1153
|
case 'blockquote': {
|
|
@@ -1058,26 +1270,27 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
1058
1270
|
break;
|
|
1059
1271
|
}
|
|
1060
1272
|
case 'form-field': {
|
|
1273
|
+
const ff = el;
|
|
1061
1274
|
const fieldTypes = ['text', 'checkbox', 'radio', 'dropdown', 'button'];
|
|
1062
|
-
if (!fieldTypes.includes(
|
|
1275
|
+
if (!fieldTypes.includes(ff.fieldType)) {
|
|
1063
1276
|
throw new PretextPdfError('VALIDATION_ERROR', `[${index}] form-field.fieldType must be one of: ${fieldTypes.join(', ')}`);
|
|
1064
1277
|
}
|
|
1065
|
-
if (!
|
|
1278
|
+
if (!ff.name || ff.name.trim() === '') {
|
|
1066
1279
|
throw new PretextPdfError('VALIDATION_ERROR', `[${index}] form-field.name is required and must be a non-empty string`);
|
|
1067
1280
|
}
|
|
1068
|
-
if ((
|
|
1069
|
-
throw new PretextPdfError('VALIDATION_ERROR', `[${index}] form-field of type "${
|
|
1281
|
+
if ((ff.fieldType === 'radio' || ff.fieldType === 'dropdown') && (!ff.options || ff.options.length === 0)) {
|
|
1282
|
+
throw new PretextPdfError('VALIDATION_ERROR', `[${index}] form-field of type "${ff.fieldType}" requires a non-empty options array`);
|
|
1070
1283
|
}
|
|
1071
|
-
if (
|
|
1284
|
+
if (ff.width !== undefined && (typeof ff.width !== 'number' || ff.width <= 0)) {
|
|
1072
1285
|
throw new PretextPdfError('VALIDATION_ERROR', `[${index}] form-field.width must be a positive number`);
|
|
1073
1286
|
}
|
|
1074
|
-
if (
|
|
1287
|
+
if (ff.height !== undefined && (typeof ff.height !== 'number' || ff.height <= 0)) {
|
|
1075
1288
|
throw new PretextPdfError('VALIDATION_ERROR', `[${index}] form-field.height must be a positive number`);
|
|
1076
1289
|
}
|
|
1077
|
-
if (
|
|
1290
|
+
if (ff.borderColor !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(ff.borderColor)) {
|
|
1078
1291
|
throw new PretextPdfError('VALIDATION_ERROR', `[${index}] form-field.borderColor must be a 6-digit hex color`);
|
|
1079
1292
|
}
|
|
1080
|
-
if (
|
|
1293
|
+
if (ff.backgroundColor !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(ff.backgroundColor)) {
|
|
1081
1294
|
throw new PretextPdfError('VALIDATION_ERROR', `[${index}] form-field.backgroundColor must be a 6-digit hex color`);
|
|
1082
1295
|
}
|
|
1083
1296
|
break;
|
|
@@ -1147,7 +1360,7 @@ function validateElement(el, index, loadedFamilies) {
|
|
|
1147
1360
|
}
|
|
1148
1361
|
default: {
|
|
1149
1362
|
const type = el.type;
|
|
1150
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix}: unknown element type '${String(type)}'. Valid types: 'paragraph', 'heading', 'spacer', 'table', 'image', 'svg', 'list', 'hr', 'page-break', 'code', 'rich-paragraph', 'blockquote', 'toc', 'comment', 'form-field', 'footnote-def'`);
|
|
1363
|
+
throw new PretextPdfError('VALIDATION_ERROR', `${prefix}: unknown element type '${String(type)}'. Valid types: 'paragraph', 'heading', 'spacer', 'table', 'image', 'svg', 'list', 'hr', 'page-break', 'code', 'rich-paragraph', 'blockquote', 'callout', 'toc', 'comment', 'form-field', 'footnote-def', 'float-group'`);
|
|
1151
1364
|
}
|
|
1152
1365
|
}
|
|
1153
1366
|
}
|