pretext-pdf 0.1.1

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +242 -0
  2. package/LICENSE +21 -0
  3. package/README.md +402 -0
  4. package/dist/assets.d.ts +14 -0
  5. package/dist/assets.d.ts.map +1 -0
  6. package/dist/assets.js +182 -0
  7. package/dist/assets.js.map +1 -0
  8. package/dist/builder.d.ts +53 -0
  9. package/dist/builder.d.ts.map +1 -0
  10. package/dist/builder.js +129 -0
  11. package/dist/builder.js.map +1 -0
  12. package/dist/errors.d.ts +7 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +13 -0
  15. package/dist/errors.js.map +1 -0
  16. package/dist/fonts.d.ts +21 -0
  17. package/dist/fonts.d.ts.map +1 -0
  18. package/dist/fonts.js +310 -0
  19. package/dist/fonts.js.map +1 -0
  20. package/dist/index.d.ts +29 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +154 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/measure.d.ts +53 -0
  25. package/dist/measure.d.ts.map +1 -0
  26. package/dist/measure.js +1029 -0
  27. package/dist/measure.js.map +1 -0
  28. package/dist/node-polyfill.d.ts +7 -0
  29. package/dist/node-polyfill.d.ts.map +1 -0
  30. package/dist/node-polyfill.js +82 -0
  31. package/dist/node-polyfill.js.map +1 -0
  32. package/dist/page-sizes.d.ts +13 -0
  33. package/dist/page-sizes.d.ts.map +1 -0
  34. package/dist/page-sizes.js +24 -0
  35. package/dist/page-sizes.js.map +1 -0
  36. package/dist/paginate.d.ts +15 -0
  37. package/dist/paginate.d.ts.map +1 -0
  38. package/dist/paginate.js +395 -0
  39. package/dist/paginate.js.map +1 -0
  40. package/dist/render.d.ts +12 -0
  41. package/dist/render.d.ts.map +1 -0
  42. package/dist/render.js +1028 -0
  43. package/dist/render.js.map +1 -0
  44. package/dist/rich-text.d.ts +14 -0
  45. package/dist/rich-text.d.ts.map +1 -0
  46. package/dist/rich-text.js +183 -0
  47. package/dist/rich-text.js.map +1 -0
  48. package/dist/types.d.ts +697 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +2 -0
  51. package/dist/types.js.map +1 -0
  52. package/dist/validate.d.ts +3 -0
  53. package/dist/validate.d.ts.map +1 -0
  54. package/dist/validate.js +786 -0
  55. package/dist/validate.js.map +1 -0
  56. package/package.json +79 -0
@@ -0,0 +1,786 @@
1
+ import { PretextPdfError } from './errors.js';
2
+ import { resolvePageDimensions } from './page-sizes.js';
3
+ /** RTL Unicode ranges: Arabic, Hebrew, Thaana, Syriac */
4
+ const RTL_REGEX = /[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F]/;
5
+ /** Valid 6-digit hex color */
6
+ const HEX_COLOR_REGEX = /^#[0-9A-Fa-f]{6}$/;
7
+ /** Valid column width: positive number OR '2*', '*', '1.5*' format */
8
+ const STAR_WIDTH_REGEX = /^(\d*\.?\d+)?\*$/;
9
+ /** Families always available without explicit doc.fonts entry */
10
+ const BUNDLED_FAMILIES = new Set(['Inter']);
11
+ /** Font variants (family-weight-style) always available without explicit doc.fonts entry */
12
+ const BUNDLED_VARIANTS = new Set(['Inter-400-normal', 'Inter-700-normal']);
13
+ export function validate(doc) {
14
+ // content must be a non-empty array
15
+ if (!Array.isArray(doc.content) || doc.content.length === 0) {
16
+ throw new PretextPdfError('VALIDATION_ERROR', 'document.content must be a non-empty array');
17
+ }
18
+ // memory guard
19
+ if (doc.content.length > 10_000) {
20
+ throw new PretextPdfError('VALIDATION_ERROR', `document.content has ${doc.content.length} elements. Maximum is 10,000. For large documents, split into multiple render() calls.`);
21
+ }
22
+ // page size
23
+ if (Array.isArray(doc.pageSize)) {
24
+ const [w, h] = doc.pageSize;
25
+ if (typeof w !== 'number' || typeof h !== 'number' ||
26
+ !isFinite(w) || !isFinite(h) ||
27
+ w <= 0 || h <= 0) {
28
+ throw new PretextPdfError('VALIDATION_ERROR', 'pageSize array must be [width, height] with two positive finite numbers in pt');
29
+ }
30
+ }
31
+ // margins can't make content area zero/negative
32
+ if (doc.margins) {
33
+ const m = doc.margins;
34
+ const [pageW, pageH] = resolvePageDimensions(doc.pageSize);
35
+ const left = m.left ?? 72;
36
+ const right = m.right ?? 72;
37
+ const top = m.top ?? 72;
38
+ const bottom = m.bottom ?? 72;
39
+ if (pageW - left - right <= 0) {
40
+ throw new PretextPdfError('PAGE_TOO_SMALL', `Left+right margins (${left}+${right}) exceed page width (${pageW}pt). Content area would be zero or negative.`);
41
+ }
42
+ if (pageH - top - bottom <= 0) {
43
+ throw new PretextPdfError('PAGE_TOO_SMALL', `Top+bottom margins (${top}+${bottom}) exceed page height (${pageH}pt). Content area would be zero or negative.`);
44
+ }
45
+ }
46
+ // font specs
47
+ if (doc.fonts) {
48
+ for (const font of doc.fonts) {
49
+ validateFontSpec(font);
50
+ }
51
+ }
52
+ // header / footer
53
+ for (const [spec, label] of [[doc.header, 'doc.header'], [doc.footer, 'doc.footer']]) {
54
+ if (!spec)
55
+ continue;
56
+ if (typeof spec.text !== 'string') {
57
+ throw new PretextPdfError('VALIDATION_ERROR', `${label}.text must be a string`);
58
+ }
59
+ if (spec.fontSize !== undefined && (typeof spec.fontSize !== 'number' || spec.fontSize <= 0 || !isFinite(spec.fontSize))) {
60
+ throw new PretextPdfError('VALIDATION_ERROR', `${label}.fontSize must be a positive finite number`);
61
+ }
62
+ if (spec.align !== undefined && !['left', 'center', 'right'].includes(spec.align)) {
63
+ throw new PretextPdfError('VALIDATION_ERROR', `${label}.align must be 'left', 'center', or 'right'`);
64
+ }
65
+ if (spec.fontWeight !== undefined && ![400, 700].includes(spec.fontWeight)) {
66
+ throw new PretextPdfError('VALIDATION_ERROR', `${label}.fontWeight must be 400 or 700`);
67
+ }
68
+ if (spec.color !== undefined && !HEX_COLOR_REGEX.test(spec.color)) {
69
+ throw new PretextPdfError('VALIDATION_ERROR', `${label}.color must be a 6-digit hex string like '#666666'. Got: '${spec.color}'`);
70
+ }
71
+ }
72
+ // watermark
73
+ if (doc.watermark) {
74
+ const wm = doc.watermark;
75
+ if (!wm.text && !wm.image) {
76
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark requires either .text or .image');
77
+ }
78
+ if (wm.opacity !== undefined && (typeof wm.opacity !== 'number' || wm.opacity < 0 || wm.opacity > 1 || !isFinite(wm.opacity))) {
79
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark.opacity must be a number 0.0–1.0');
80
+ }
81
+ if (wm.fontSize !== undefined && (typeof wm.fontSize !== 'number' || wm.fontSize <= 0 || !isFinite(wm.fontSize))) {
82
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark.fontSize must be a positive finite number');
83
+ }
84
+ if (wm.fontWeight !== undefined && ![400, 700].includes(wm.fontWeight)) {
85
+ throw new PretextPdfError('VALIDATION_ERROR', "doc.watermark.fontWeight must be 400 or 700");
86
+ }
87
+ if (wm.color !== undefined && !HEX_COLOR_REGEX.test(wm.color)) {
88
+ throw new PretextPdfError('VALIDATION_ERROR', `doc.watermark.color must be a 6-digit hex string. Got: '${wm.color}'`);
89
+ }
90
+ if (wm.rotation !== undefined) {
91
+ if (typeof wm.rotation !== 'number' || !isFinite(wm.rotation)) {
92
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark.rotation must be a finite number');
93
+ }
94
+ if (wm.rotation < -360 || wm.rotation > 360) {
95
+ throw new PretextPdfError('WATERMARK_ROTATION_OUT_OF_RANGE', 'doc.watermark.rotation must be between -360 and 360 degrees');
96
+ }
97
+ }
98
+ }
99
+ // encryption
100
+ if (doc.encryption) {
101
+ const enc = doc.encryption;
102
+ if (enc.userPassword !== undefined && typeof enc.userPassword !== 'string') {
103
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.userPassword must be a string if provided');
104
+ }
105
+ if (enc.ownerPassword !== undefined && typeof enc.ownerPassword !== 'string') {
106
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.ownerPassword must be a string if provided');
107
+ }
108
+ if (enc.ownerPassword !== undefined && enc.ownerPassword === '') {
109
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.ownerPassword must not be an empty string');
110
+ }
111
+ // permissions sub-fields are booleans — TypeScript enforces the type, no runtime check needed
112
+ }
113
+ // bookmarks
114
+ if (doc.bookmarks !== undefined && doc.bookmarks !== false) {
115
+ const bm = doc.bookmarks;
116
+ if (bm.minLevel !== undefined && ![1, 2, 3, 4].includes(bm.minLevel)) {
117
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.bookmarks.minLevel must be 1, 2, 3, or 4');
118
+ }
119
+ if (bm.maxLevel !== undefined && ![1, 2, 3, 4].includes(bm.maxLevel)) {
120
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.bookmarks.maxLevel must be 1, 2, 3, or 4');
121
+ }
122
+ if (bm.minLevel !== undefined && bm.maxLevel !== undefined && bm.minLevel > bm.maxLevel) {
123
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.bookmarks.minLevel must be ≤ maxLevel');
124
+ }
125
+ }
126
+ // hyphenation
127
+ if (doc.hyphenation) {
128
+ const h = doc.hyphenation;
129
+ if (!h.language || typeof h.language !== 'string') {
130
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.language is required (e.g. "en-us")');
131
+ }
132
+ if (h.minWordLength !== undefined && (h.minWordLength < 2 || h.minWordLength > 20)) {
133
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.minWordLength must be 2–20');
134
+ }
135
+ if (h.leftMin !== undefined && (h.leftMin < 1 || h.leftMin > 5)) {
136
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.leftMin must be 1–5');
137
+ }
138
+ if (h.rightMin !== undefined && (h.rightMin < 1 || h.rightMin > 5)) {
139
+ throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.rightMin must be 1–5');
140
+ }
141
+ }
142
+ // validate each content element
143
+ const loadedFamilies = new Set([
144
+ ...BUNDLED_FAMILIES,
145
+ ...(doc.fonts ?? []).map(f => f.family),
146
+ ]);
147
+ for (let i = 0; i < doc.content.length; i++) {
148
+ validateElement(doc.content[i], i, loadedFamilies);
149
+ }
150
+ // validate all font references are loadable
151
+ validateFontReferences(doc, loadedFamilies);
152
+ }
153
+ /**
154
+ * Validate that every font family referenced anywhere in the document
155
+ * is either bundled (Inter) or present in doc.fonts.
156
+ * Catches problems early instead of silently falling back or dropping content.
157
+ */
158
+ function validateFontReferences(doc, loadedFamilies) {
159
+ const defaultFamily = doc.defaultFont ?? 'Inter';
160
+ // Build a variant-level set for italic checks: "Family-weight-style"
161
+ const loadedVariants = new Set(BUNDLED_VARIANTS);
162
+ for (const f of doc.fonts ?? []) {
163
+ loadedVariants.add(`${f.family}-${f.weight ?? 400}-${f.style ?? 'normal'}`);
164
+ }
165
+ const requireFamily = (family, context) => {
166
+ if (!loadedFamilies.has(family)) {
167
+ 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}').`);
168
+ }
169
+ };
170
+ const requireVariant = (family, weight, style, context) => {
171
+ const key = `${family}-${weight}-${style}`;
172
+ if (!loadedVariants.has(key)) {
173
+ if (style === 'italic') {
174
+ throw new PretextPdfError('ITALIC_FONT_NOT_LOADED', `${context}: fontStyle 'italic' requires an italic font variant. Add { family: '${family}', weight: ${weight}, style: 'italic', src: '/path/to-italic.ttf' } to doc.fonts.`);
175
+ }
176
+ throw new PretextPdfError('FONT_NOT_LOADED', `${context}: font variant '${key}' is not loaded. Add a matching FontSpec to doc.fonts.`);
177
+ }
178
+ };
179
+ // 1. defaultFont must be loadable
180
+ requireFamily(defaultFamily, `doc.defaultFont '${defaultFamily}'`);
181
+ // 2. header / footer fontFamily + fontWeight variant
182
+ for (const [spec, label] of [[doc.header, 'doc.header'], [doc.footer, 'doc.footer']]) {
183
+ if (!spec)
184
+ continue;
185
+ if (spec.fontFamily)
186
+ requireFamily(spec.fontFamily, `${label}.fontFamily`);
187
+ if ((spec.fontWeight ?? 400) === 700) {
188
+ const family = spec.fontFamily ?? defaultFamily;
189
+ requireVariant(family, 700, 'normal', `${label} fontWeight:700`);
190
+ }
191
+ }
192
+ // 2b. watermark fontFamily (text watermark only)
193
+ if (doc.watermark?.text) {
194
+ if (doc.watermark.fontFamily)
195
+ requireFamily(doc.watermark.fontFamily, 'doc.watermark.fontFamily');
196
+ if ((doc.watermark.fontWeight ?? 400) === 700) {
197
+ const family = doc.watermark.fontFamily ?? defaultFamily;
198
+ requireVariant(family, 700, 'normal', 'doc.watermark fontWeight:700');
199
+ }
200
+ }
201
+ // 3. content elements
202
+ for (let i = 0; i < doc.content.length; i++) {
203
+ const el = doc.content[i];
204
+ const prefix = `content[${i}]`;
205
+ if (el.type === 'paragraph') {
206
+ if (el.fontFamily)
207
+ requireFamily(el.fontFamily, `${prefix} (paragraph).fontFamily`);
208
+ if ((el.fontWeight ?? 400) === 700) {
209
+ const family = el.fontFamily ?? defaultFamily;
210
+ requireVariant(family, 700, 'normal', `${prefix} (paragraph) fontWeight:700`);
211
+ }
212
+ }
213
+ if (el.type === 'heading') {
214
+ if (el.fontFamily)
215
+ requireFamily(el.fontFamily, `${prefix} (heading).fontFamily`);
216
+ const family = el.fontFamily ?? defaultFamily;
217
+ const weight = el.fontWeight ?? 700;
218
+ requireVariant(family, weight, 'normal', `${prefix} (heading) fontWeight:${weight}`);
219
+ }
220
+ if (el.type === 'list') {
221
+ for (let ii = 0; ii < el.items.length; ii++) {
222
+ const item = el.items[ii];
223
+ if ((item.fontWeight ?? 400) === 700) {
224
+ requireVariant(defaultFamily, 700, 'normal', `${prefix} (list) items[${ii}] fontWeight:700`);
225
+ }
226
+ }
227
+ }
228
+ if (el.type === 'table') {
229
+ for (let ri = 0; ri < el.rows.length; ri++) {
230
+ for (let ci = 0; ci < el.rows[ri].cells.length; ci++) {
231
+ const cell = el.rows[ri].cells[ci];
232
+ if (cell.fontFamily)
233
+ requireFamily(cell.fontFamily, `${prefix} (table) rows[${ri}].cells[${ci}].fontFamily`);
234
+ if ((cell.fontWeight ?? 400) === 700) {
235
+ const family = cell.fontFamily ?? defaultFamily;
236
+ requireVariant(family, 700, 'normal', `${prefix} (table) rows[${ri}].cells[${ci}] fontWeight:700`);
237
+ }
238
+ }
239
+ }
240
+ }
241
+ if (el.type === 'rich-paragraph') {
242
+ for (let si = 0; si < el.spans.length; si++) {
243
+ const span = el.spans[si];
244
+ const spanFamily = span.fontFamily ?? defaultFamily;
245
+ const spanWeight = span.fontWeight ?? 400;
246
+ const spanStyle = span.fontStyle ?? 'normal';
247
+ if (span.fontFamily)
248
+ requireFamily(span.fontFamily, `${prefix} (rich-paragraph) spans[${si}].fontFamily`);
249
+ if (spanStyle === 'italic') {
250
+ requireVariant(spanFamily, spanWeight, 'italic', `${prefix} (rich-paragraph) spans[${si}]`);
251
+ }
252
+ }
253
+ }
254
+ if (el.type === 'blockquote') {
255
+ if (el.fontFamily)
256
+ requireFamily(el.fontFamily, `${prefix} (blockquote).fontFamily`);
257
+ const family = el.fontFamily ?? defaultFamily;
258
+ const weight = el.fontWeight ?? 400;
259
+ const style = el.fontStyle ?? 'normal';
260
+ if (style === 'italic') {
261
+ requireVariant(family, weight, 'italic', `${prefix} (blockquote) fontStyle:italic`);
262
+ }
263
+ else if (weight === 700) {
264
+ requireVariant(family, 700, 'normal', `${prefix} (blockquote) fontWeight:700`);
265
+ }
266
+ }
267
+ // code.fontFamily already validated against loadedFamilies in validateElement
268
+ }
269
+ }
270
+ function validateElement(el, index, loadedFamilies) {
271
+ const prefix = `content[${index}]`;
272
+ if (!el || typeof el !== 'object' || !('type' in el)) {
273
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix}: each element must have a 'type' field`);
274
+ }
275
+ switch (el.type) {
276
+ case 'paragraph': {
277
+ if (typeof el.text !== 'string') {
278
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'text' must be a string`);
279
+ }
280
+ // NEW: Validate dir field (Phase 7F)
281
+ if (el.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(el.dir)) {
282
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'dir' must be 'ltr', 'rtl', or 'auto'`);
283
+ }
284
+ if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
285
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'color' must be a 6-digit hex string like '#ff0000'. Got: '${el.color}'`);
286
+ }
287
+ if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
288
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'fontSize' must be a positive finite number`);
289
+ }
290
+ if (el.lineHeight !== undefined && typeof el.lineHeight === 'number') {
291
+ // Compare against explicit fontSize if set, or default (12pt) if not
292
+ const effectiveFontSize = el.fontSize ?? 12;
293
+ if (el.lineHeight < effectiveFontSize) {
294
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): lineHeight (${el.lineHeight}) is less than fontSize (${effectiveFontSize}). Lines would overlap. Set lineHeight >= fontSize.`);
295
+ }
296
+ }
297
+ if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
298
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'bgColor' must be a 6-digit hex string like '#f0f0f0'. Got: '${el.bgColor}'`);
299
+ }
300
+ if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
301
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'spaceAfter' must be a non-negative finite number`);
302
+ }
303
+ if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
304
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'spaceBefore' must be a non-negative finite number`);
305
+ }
306
+ if (el.columns !== undefined && (typeof el.columns !== 'number' || el.columns < 1 || !Number.isInteger(el.columns) || el.columns > 6)) {
307
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'columns' must be a positive integer between 1 and 6`);
308
+ }
309
+ if (el.columnGap !== undefined && (typeof el.columnGap !== 'number' || el.columnGap < 0 || !isFinite(el.columnGap))) {
310
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'columnGap' must be a non-negative finite number`);
311
+ }
312
+ if (el.align !== undefined && !['left', 'center', 'right', 'justify'].includes(el.align)) {
313
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'align' must be 'left', 'center', 'right', or 'justify'`);
314
+ }
315
+ if (el.url !== undefined && (typeof el.url !== 'string' || el.url.trim() === '')) {
316
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'url' must be a non-empty string if provided`);
317
+ }
318
+ break;
319
+ }
320
+ case 'heading': {
321
+ if (typeof el.text !== 'string') {
322
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'text' must be a string`);
323
+ }
324
+ if (![1, 2, 3, 4].includes(el.level)) {
325
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'level' must be 1, 2, 3, or 4. Got: ${el.level}`);
326
+ }
327
+ if (el.fontWeight !== undefined && ![400, 700].includes(el.fontWeight)) {
328
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'fontWeight' must be 400 or 700`);
329
+ }
330
+ if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
331
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'fontSize' must be a positive finite number`);
332
+ }
333
+ if (el.lineHeight !== undefined && typeof el.lineHeight === 'number') {
334
+ const effectiveFontSize = el.fontSize ?? 12;
335
+ if (el.lineHeight < effectiveFontSize) {
336
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): lineHeight (${el.lineHeight}) is less than fontSize (${effectiveFontSize}). Lines would overlap.`);
337
+ }
338
+ }
339
+ if (el.align !== undefined && !['left', 'center', 'right', 'justify'].includes(el.align)) {
340
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'align' must be 'left', 'center', 'right', or 'justify'`);
341
+ }
342
+ if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
343
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'color' must be a 6-digit hex string like '#ff0000'. Got: '${el.color}'`);
344
+ }
345
+ if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
346
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'bgColor' must be a 6-digit hex string`);
347
+ }
348
+ if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
349
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'spaceBefore' must be a non-negative finite number`);
350
+ }
351
+ if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
352
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'spaceAfter' must be a non-negative finite number`);
353
+ }
354
+ if (el.url !== undefined && (typeof el.url !== 'string' || el.url.trim() === '')) {
355
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'url' must be a non-empty string if provided`);
356
+ }
357
+ if (el.anchor !== undefined && (typeof el.anchor !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(el.anchor))) {
358
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'anchor' must be alphanumeric with hyphens/underscores only. Got: '${el.anchor}'`);
359
+ }
360
+ break;
361
+ }
362
+ case 'spacer': {
363
+ if (typeof el.height !== 'number' || el.height < 0 || !isFinite(el.height)) {
364
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (spacer): 'height' must be a non-negative finite number`);
365
+ }
366
+ break;
367
+ }
368
+ case 'table': {
369
+ if (!Array.isArray(el.columns) || el.columns.length === 0) {
370
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'columns' must be a non-empty array`);
371
+ }
372
+ if (!Array.isArray(el.rows) || el.rows.length === 0) {
373
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'rows' must be a non-empty array`);
374
+ }
375
+ const colCount = el.columns.length;
376
+ for (let ci = 0; ci < el.columns.length; ci++) {
377
+ const col = el.columns[ci];
378
+ if (typeof col.width === 'number') {
379
+ if (col.width <= 0 || !isFinite(col.width)) {
380
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].width must be a positive number. Got: ${col.width}`);
381
+ }
382
+ }
383
+ else if (typeof col.width === 'string') {
384
+ if (col.width !== 'auto' && !STAR_WIDTH_REGEX.test(col.width)) {
385
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].width must be a positive number, proportional string like '2*' or '*', or 'auto'. Got: '${col.width}'`);
386
+ }
387
+ }
388
+ else {
389
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].width must be a number or string like '2*' or 'auto'`);
390
+ }
391
+ if (col.align !== undefined && !['left', 'center', 'right'].includes(col.align)) {
392
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].align must be 'left', 'center', or 'right'`);
393
+ }
394
+ }
395
+ // Validate header row count
396
+ const headerRowCount = el.headerRows !== undefined
397
+ ? el.headerRows
398
+ : el.rows.filter(r => r.isHeader).length;
399
+ if (headerRowCount >= el.rows.length) {
400
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): table must have at least 1 non-header body row. headerRows=${headerRowCount}, total rows=${el.rows.length}`);
401
+ }
402
+ for (let ri = 0; ri < el.rows.length; ri++) {
403
+ const row = el.rows[ri];
404
+ if (!Array.isArray(row.cells)) {
405
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells must be an array`);
406
+ }
407
+ // Validate colspan sum equals colCount
408
+ let colspanSum = 0;
409
+ for (let cellI = 0; cellI < row.cells.length; cellI++) {
410
+ const cell = row.cells[cellI];
411
+ const cs = cell.colspan ?? 1;
412
+ if (typeof cs !== 'number' || cs < 1 || !Number.isInteger(cs)) {
413
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].colspan must be a positive integer`);
414
+ }
415
+ colspanSum += cs;
416
+ }
417
+ if (colspanSum !== colCount) {
418
+ throw new PretextPdfError('COLSPAN_OVERFLOW', `${prefix} (table): rows[${ri}] colspan sum is ${colspanSum} but table has ${colCount} columns. Sum of all colspan values in a row must equal the column count.`);
419
+ }
420
+ for (let cellI = 0; cellI < row.cells.length; cellI++) {
421
+ const cell = row.cells[cellI];
422
+ if (typeof cell.text !== 'string') {
423
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].text must be a string`);
424
+ }
425
+ if (cell.fontFamily !== undefined && (typeof cell.fontFamily !== 'string' || cell.fontFamily.trim() === '')) {
426
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].fontFamily must be a non-empty string`);
427
+ }
428
+ if (cell.fontWeight !== undefined && ![400, 700].includes(cell.fontWeight)) {
429
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].fontWeight must be 400 or 700`);
430
+ }
431
+ if (cell.fontSize !== undefined && (typeof cell.fontSize !== 'number' || cell.fontSize <= 0 || !isFinite(cell.fontSize))) {
432
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].fontSize must be a positive finite number`);
433
+ }
434
+ if (cell.color !== undefined && !HEX_COLOR_REGEX.test(cell.color)) {
435
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].color must be a 6-digit hex string`);
436
+ }
437
+ if (cell.bgColor !== undefined && !HEX_COLOR_REGEX.test(cell.bgColor)) {
438
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].bgColor must be a 6-digit hex string`);
439
+ }
440
+ }
441
+ }
442
+ if (el.borderColor !== undefined && !HEX_COLOR_REGEX.test(el.borderColor)) {
443
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'borderColor' must be a 6-digit hex string`);
444
+ }
445
+ if (el.headerBgColor !== undefined && !HEX_COLOR_REGEX.test(el.headerBgColor)) {
446
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'headerBgColor' must be a 6-digit hex string`);
447
+ }
448
+ if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
449
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'fontSize' must be a positive finite number`);
450
+ }
451
+ if (el.borderWidth !== undefined && (typeof el.borderWidth !== 'number' || el.borderWidth < 0)) {
452
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'borderWidth' must be a non-negative number`);
453
+ }
454
+ if (el.cellPaddingH !== undefined && (typeof el.cellPaddingH !== 'number' || el.cellPaddingH < 0)) {
455
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'cellPaddingH' must be a non-negative number`);
456
+ }
457
+ if (el.cellPaddingV !== undefined && (typeof el.cellPaddingV !== 'number' || el.cellPaddingV < 0)) {
458
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'cellPaddingV' must be a non-negative number`);
459
+ }
460
+ if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
461
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'spaceAfter' must be a non-negative finite number`);
462
+ }
463
+ if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
464
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'spaceBefore' must be a non-negative finite number`);
465
+ }
466
+ break;
467
+ }
468
+ case 'image': {
469
+ if (!el.src || (typeof el.src !== 'string' && !(el.src instanceof Uint8Array))) {
470
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'src' must be a non-empty string path or Uint8Array`);
471
+ }
472
+ const fmt = el.format ?? 'auto';
473
+ if (fmt !== 'png' && fmt !== 'jpg' && fmt !== 'auto') {
474
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'format' must be 'png', 'jpg', or 'auto'. Got: '${String(el.format)}'`);
475
+ }
476
+ if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
477
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'width' must be a positive finite number`);
478
+ }
479
+ if (el.height !== undefined && (typeof el.height !== 'number' || el.height <= 0 || !isFinite(el.height))) {
480
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'height' must be a positive finite number`);
481
+ }
482
+ if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
483
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'align' must be 'left', 'center', or 'right'`);
484
+ }
485
+ if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
486
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'spaceAfter' must be a non-negative finite number`);
487
+ }
488
+ if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
489
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'spaceBefore' must be a non-negative finite number`);
490
+ }
491
+ break;
492
+ }
493
+ case 'svg': {
494
+ if (typeof el.svg !== 'string' || el.svg.trim() === '') {
495
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'svg' must be a non-empty string`);
496
+ }
497
+ if (!el.svg.trim().startsWith('<')) {
498
+ throw new PretextPdfError('SVG_INVALID_MARKUP', `${prefix} (svg): 'svg' must be valid SVG markup (must start with '<')`);
499
+ }
500
+ if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
501
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'width' must be a positive finite number`);
502
+ }
503
+ if (el.height !== undefined && (typeof el.height !== 'number' || el.height <= 0 || !isFinite(el.height))) {
504
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'height' must be a positive finite number`);
505
+ }
506
+ if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
507
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'align' must be 'left', 'center', or 'right'`);
508
+ }
509
+ if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
510
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'spaceAfter' must be a non-negative finite number`);
511
+ }
512
+ if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
513
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'spaceBefore' must be a non-negative finite number`);
514
+ }
515
+ break;
516
+ }
517
+ case 'list': {
518
+ if (el.style !== 'ordered' && el.style !== 'unordered') {
519
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'style' must be 'ordered' or 'unordered'`);
520
+ }
521
+ if (!Array.isArray(el.items) || el.items.length === 0) {
522
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'items' must be a non-empty array`);
523
+ }
524
+ for (let ii = 0; ii < el.items.length; ii++) {
525
+ const item = el.items[ii];
526
+ if (typeof item.text !== 'string' || item.text.trim() === '') {
527
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].text must be a non-empty string`);
528
+ }
529
+ // NEW: Validate dir field (Phase 7F)
530
+ if (item.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(item.dir)) {
531
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].dir must be 'ltr', 'rtl', or 'auto'`);
532
+ }
533
+ if (item.fontWeight !== undefined && ![400, 700].includes(item.fontWeight)) {
534
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].fontWeight must be 400 or 700`);
535
+ }
536
+ // Validate nested items (1 level deep)
537
+ if (item.items) {
538
+ if (!Array.isArray(item.items) || item.items.length === 0) {
539
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].items must be a non-empty array if provided`);
540
+ }
541
+ for (let ni = 0; ni < item.items.length; ni++) {
542
+ const nested = item.items[ni];
543
+ if (typeof nested.text !== 'string' || nested.text.trim() === '') {
544
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].items[${ni}].text must be a non-empty string`);
545
+ }
546
+ if (nested.items && nested.items.length > 0) {
547
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].items[${ni}] has nested items — only 1 level of nesting is supported in Phase 2`);
548
+ }
549
+ }
550
+ }
551
+ }
552
+ if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
553
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'color' must be a 6-digit hex string like '#ff0000'. Got: '${el.color}'`);
554
+ }
555
+ if (el.nestedNumberingStyle !== undefined && !['continue', 'restart'].includes(el.nestedNumberingStyle)) {
556
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'nestedNumberingStyle' must be 'continue' or 'restart'. Got: '${el.nestedNumberingStyle}'`);
557
+ }
558
+ if (el.indent !== undefined && (typeof el.indent !== 'number' || el.indent < 0)) {
559
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'indent' must be a non-negative number`);
560
+ }
561
+ if (el.markerWidth !== undefined && (typeof el.markerWidth !== 'number' || el.markerWidth <= 0)) {
562
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'markerWidth' must be a positive number`);
563
+ }
564
+ if (el.marker !== undefined && (typeof el.marker !== 'string' || el.marker.trim() === '')) {
565
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'marker' must be a non-empty string`);
566
+ }
567
+ break;
568
+ }
569
+ case 'hr': {
570
+ if (el.thickness !== undefined && (typeof el.thickness !== 'number' || el.thickness < 0 || !isFinite(el.thickness))) {
571
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (hr): 'thickness' must be a non-negative finite number`);
572
+ }
573
+ if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
574
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (hr): 'color' must be a 6-digit hex string`);
575
+ }
576
+ if (el.spaceAbove !== undefined && (typeof el.spaceAbove !== 'number' || el.spaceAbove < 0)) {
577
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (hr): 'spaceAbove' must be a non-negative number`);
578
+ }
579
+ if (el.spaceBelow !== undefined && (typeof el.spaceBelow !== 'number' || el.spaceBelow < 0)) {
580
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (hr): 'spaceBelow' must be a non-negative number`);
581
+ }
582
+ break;
583
+ }
584
+ case 'page-break': {
585
+ // No fields to validate
586
+ break;
587
+ }
588
+ case 'rich-paragraph': {
589
+ if (!Array.isArray(el.spans) || el.spans.length === 0) {
590
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'spans' must be a non-empty array`);
591
+ }
592
+ for (let si = 0; si < el.spans.length; si++) {
593
+ const span = el.spans[si];
594
+ if (typeof span.text !== 'string') {
595
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].text must be a string`);
596
+ }
597
+ if (span.text === '') {
598
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].text cannot be an empty string. Use ' ' for a space between styled runs.`);
599
+ }
600
+ if (span.color !== undefined && !HEX_COLOR_REGEX.test(span.color)) {
601
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].color must be a 6-digit hex string`);
602
+ }
603
+ if (span.fontWeight !== undefined && ![400, 700].includes(span.fontWeight)) {
604
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].fontWeight must be 400 or 700`);
605
+ }
606
+ if (span.fontStyle !== undefined && !['normal', 'italic'].includes(span.fontStyle)) {
607
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].fontStyle must be 'normal' or 'italic'`);
608
+ }
609
+ if (span.fontSize !== undefined && (typeof span.fontSize !== 'number' || span.fontSize <= 0 || !isFinite(span.fontSize))) {
610
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].fontSize must be a positive finite number if provided`);
611
+ }
612
+ if (span.url !== undefined && (typeof span.url !== 'string' || span.url.trim() === '')) {
613
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].url must be a non-empty string if provided`);
614
+ }
615
+ if (span.href !== undefined && (typeof span.href !== 'string' || span.href.trim() === '')) {
616
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].href must be a non-empty string if provided`);
617
+ }
618
+ }
619
+ if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
620
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'fontSize' must be a positive finite number`);
621
+ }
622
+ if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
623
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'bgColor' must be a 6-digit hex string`);
624
+ }
625
+ if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
626
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'spaceAfter' must be a non-negative finite number`);
627
+ }
628
+ if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
629
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'spaceBefore' must be a non-negative finite number`);
630
+ }
631
+ if (el.columns !== undefined && (typeof el.columns !== 'number' || el.columns < 1 || !Number.isInteger(el.columns) || el.columns > 6)) {
632
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'columns' must be a positive integer between 1 and 6`);
633
+ }
634
+ if (el.columnGap !== undefined && (typeof el.columnGap !== 'number' || el.columnGap < 0 || !isFinite(el.columnGap))) {
635
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'columnGap' must be a non-negative finite number`);
636
+ }
637
+ if (el.align !== undefined && !['left', 'center', 'right', 'justify'].includes(el.align)) {
638
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'align' must be 'left', 'center', 'right', or 'justify'`);
639
+ }
640
+ break;
641
+ }
642
+ case 'code': {
643
+ if (typeof el.text !== 'string') {
644
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'text' must be a string`);
645
+ }
646
+ if (el.text.trim() === '') {
647
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'text' must be a non-empty string`);
648
+ }
649
+ if (!el.fontFamily || typeof el.fontFamily !== 'string') {
650
+ throw new PretextPdfError('MONOSPACE_FONT_REQUIRED', `${prefix} (code): 'fontFamily' is required. Provide a monospace TTF font family name that you have loaded in doc.fonts (e.g., 'JetBrains Mono', 'Fira Code', 'Courier Prime').`);
651
+ }
652
+ if (!loadedFamilies.has(el.fontFamily)) {
653
+ throw new PretextPdfError('MONOSPACE_FONT_REQUIRED', `${prefix} (code): fontFamily '${el.fontFamily}' is not loaded. Add { family: '${el.fontFamily}', src: '/path/to/font.ttf' } to doc.fonts.`);
654
+ }
655
+ if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
656
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'fontSize' must be a positive finite number`);
657
+ }
658
+ if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
659
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'bgColor' must be a 6-digit hex string`);
660
+ }
661
+ if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
662
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'color' must be a 6-digit hex string`);
663
+ }
664
+ if (el.padding !== undefined && (typeof el.padding !== 'number' || el.padding < 0 || !isFinite(el.padding))) {
665
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'padding' must be a non-negative finite number`);
666
+ }
667
+ if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
668
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'spaceAfter' must be a non-negative finite number`);
669
+ }
670
+ if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
671
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'spaceBefore' must be a non-negative finite number`);
672
+ }
673
+ break;
674
+ }
675
+ case 'blockquote': {
676
+ if (typeof el.text !== 'string') {
677
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'text' must be a string`);
678
+ }
679
+ if (el.text.trim() === '') {
680
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'text' must be a non-empty string`);
681
+ }
682
+ if (el.borderColor !== undefined && !HEX_COLOR_REGEX.test(el.borderColor)) {
683
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'borderColor' must be a 6-digit hex string`);
684
+ }
685
+ if (el.borderWidth !== undefined && (typeof el.borderWidth !== 'number' || el.borderWidth < 0 || !isFinite(el.borderWidth))) {
686
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'borderWidth' must be a non-negative finite number`);
687
+ }
688
+ if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
689
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'bgColor' must be a 6-digit hex string`);
690
+ }
691
+ if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
692
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'color' must be a 6-digit hex string`);
693
+ }
694
+ if (el.fontWeight !== undefined && ![400, 700].includes(el.fontWeight)) {
695
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'fontWeight' must be 400 or 700`);
696
+ }
697
+ if (el.fontStyle !== undefined && !['normal', 'italic'].includes(el.fontStyle)) {
698
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'fontStyle' must be 'normal' or 'italic'`);
699
+ }
700
+ if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
701
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'fontSize' must be a positive finite number`);
702
+ }
703
+ if (el.lineHeight !== undefined && typeof el.lineHeight === 'number') {
704
+ const effectiveFontSize = el.fontSize ?? 12;
705
+ if (el.lineHeight < effectiveFontSize) {
706
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): lineHeight (${el.lineHeight}) is less than fontSize (${effectiveFontSize}). Lines would overlap.`);
707
+ }
708
+ }
709
+ if (el.padding !== undefined && (typeof el.padding !== 'number' || el.padding < 0 || !isFinite(el.padding))) {
710
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'padding' must be a non-negative finite number`);
711
+ }
712
+ if (el.paddingH !== undefined && (typeof el.paddingH !== 'number' || el.paddingH < 0 || !isFinite(el.paddingH))) {
713
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'paddingH' must be a non-negative finite number`);
714
+ }
715
+ if (el.paddingV !== undefined && (typeof el.paddingV !== 'number' || el.paddingV < 0 || !isFinite(el.paddingV))) {
716
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'paddingV' must be a non-negative finite number`);
717
+ }
718
+ if (el.align !== undefined && !['left', 'center', 'right', 'justify'].includes(el.align)) {
719
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'align' must be 'left', 'center', 'right', or 'justify'`);
720
+ }
721
+ if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
722
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'spaceBefore' must be a non-negative finite number`);
723
+ }
724
+ if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
725
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'spaceAfter' must be a non-negative finite number`);
726
+ }
727
+ break;
728
+ }
729
+ case 'toc': {
730
+ if (el.minLevel !== undefined && ![1, 2, 3, 4].includes(el.minLevel)) {
731
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'minLevel' must be 1, 2, 3, or 4`);
732
+ }
733
+ if (el.maxLevel !== undefined && ![1, 2, 3, 4].includes(el.maxLevel)) {
734
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'maxLevel' must be 1, 2, 3, or 4`);
735
+ }
736
+ if (el.minLevel !== undefined && el.maxLevel !== undefined && el.minLevel > el.maxLevel) {
737
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'minLevel' cannot exceed 'maxLevel'`);
738
+ }
739
+ if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
740
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'fontSize' must be a positive finite number`);
741
+ }
742
+ if (el.titleFontSize !== undefined && (typeof el.titleFontSize !== 'number' || el.titleFontSize <= 0 || !isFinite(el.titleFontSize))) {
743
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'titleFontSize' must be a positive finite number`);
744
+ }
745
+ if (el.levelIndent !== undefined && (typeof el.levelIndent !== 'number' || el.levelIndent < 0 || !isFinite(el.levelIndent))) {
746
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'levelIndent' must be a non-negative finite number`);
747
+ }
748
+ if (el.leader !== undefined && (typeof el.leader !== 'string' || el.leader.length === 0)) {
749
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'leader' must be a non-empty string`);
750
+ }
751
+ if (el.entrySpacing !== undefined && (typeof el.entrySpacing !== 'number' || el.entrySpacing < 0 || !isFinite(el.entrySpacing))) {
752
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'entrySpacing' must be a non-negative finite number`);
753
+ }
754
+ if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
755
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'spaceBefore' must be a non-negative finite number`);
756
+ }
757
+ if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
758
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'spaceAfter' must be a non-negative finite number`);
759
+ }
760
+ break;
761
+ }
762
+ case 'toc-entry': {
763
+ // Internal type — should never appear in user input
764
+ throw new PretextPdfError('VALIDATION_ERROR', `${prefix}: 'toc-entry' is an internal type and cannot be used in document content`);
765
+ }
766
+ default: {
767
+ const type = el.type;
768
+ 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'`);
769
+ }
770
+ }
771
+ }
772
+ function validateFontSpec(font) {
773
+ if (!font.family || typeof font.family !== 'string') {
774
+ throw new PretextPdfError('VALIDATION_ERROR', `FontSpec: 'family' must be a non-empty string`);
775
+ }
776
+ if (font.weight !== undefined && ![400, 700].includes(font.weight)) {
777
+ throw new PretextPdfError('VALIDATION_ERROR', `FontSpec '${font.family}': 'weight' must be 400 or 700`);
778
+ }
779
+ if (font.style !== undefined && !['normal', 'italic'].includes(font.style)) {
780
+ throw new PretextPdfError('VALIDATION_ERROR', `FontSpec '${font.family}': 'style' must be 'normal' or 'italic'`);
781
+ }
782
+ if (font.src === undefined || font.src === null) {
783
+ throw new PretextPdfError('VALIDATION_ERROR', `FontSpec '${font.family}': 'src' is required (file path or Uint8Array)`);
784
+ }
785
+ }
786
+ //# sourceMappingURL=validate.js.map