pretext-pdf 1.1.1 → 1.6.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 +659 -0
- package/README.md +82 -7
- package/dist/allowed-props.d.ts +76 -0
- package/dist/allowed-props.d.ts.map +1 -1
- package/dist/allowed-props.js.map +1 -1
- package/dist/assets/generators/barcode.d.ts +9 -0
- package/dist/assets/generators/barcode.d.ts.map +1 -0
- package/dist/assets/generators/barcode.js +24 -0
- package/dist/assets/generators/barcode.js.map +1 -0
- package/dist/assets/generators/chart.d.ts +13 -0
- package/dist/assets/generators/chart.d.ts.map +1 -0
- package/dist/assets/generators/chart.js +32 -0
- package/dist/assets/generators/chart.js.map +1 -0
- package/dist/assets/generators/qr.d.ts +9 -0
- package/dist/assets/generators/qr.d.ts.map +1 -0
- package/dist/assets/generators/qr.js +25 -0
- package/dist/assets/generators/qr.js.map +1 -0
- package/dist/assets/index.d.ts +19 -0
- package/dist/assets/index.d.ts.map +1 -0
- package/dist/assets/index.js +19 -0
- package/dist/assets/index.js.map +1 -0
- package/dist/assets/loaders/images.d.ts +20 -0
- package/dist/assets/loaders/images.d.ts.map +1 -0
- package/dist/assets/loaders/images.js +69 -0
- package/dist/assets/loaders/images.js.map +1 -0
- package/dist/assets/loaders/orchestrator.d.ts +24 -0
- package/dist/assets/loaders/orchestrator.d.ts.map +1 -0
- package/dist/assets/loaders/orchestrator.js +109 -0
- package/dist/assets/loaders/orchestrator.js.map +1 -0
- package/dist/assets/loaders/vectors.d.ts +25 -0
- package/dist/assets/loaders/vectors.d.ts.map +1 -0
- package/dist/assets/loaders/vectors.js +118 -0
- package/dist/assets/loaders/vectors.js.map +1 -0
- package/dist/assets/loaders/watermark.d.ts +12 -0
- package/dist/assets/loaders/watermark.d.ts.map +1 -0
- package/dist/assets/loaders/watermark.js +40 -0
- package/dist/assets/loaders/watermark.js.map +1 -0
- package/dist/assets/security/fetch.d.ts +14 -0
- package/dist/assets/security/fetch.d.ts.map +1 -0
- package/dist/assets/security/fetch.js +112 -0
- package/dist/assets/security/fetch.js.map +1 -0
- package/dist/assets/security/ipv4-normalize.d.ts +28 -0
- package/dist/assets/security/ipv4-normalize.d.ts.map +1 -0
- package/dist/assets/security/ipv4-normalize.js +116 -0
- package/dist/assets/security/ipv4-normalize.js.map +1 -0
- package/dist/assets/security/path-allowlist.d.ts +12 -0
- package/dist/assets/security/path-allowlist.d.ts.map +1 -0
- package/dist/assets/security/path-allowlist.js +26 -0
- package/dist/assets/security/path-allowlist.js.map +1 -0
- package/dist/assets/security/url-validation.d.ts +22 -0
- package/dist/assets/security/url-validation.d.ts.map +1 -0
- package/dist/assets/security/url-validation.js +164 -0
- package/dist/assets/security/url-validation.js.map +1 -0
- package/dist/assets/svg/dimensions.d.ts +19 -0
- package/dist/assets/svg/dimensions.d.ts.map +1 -0
- package/dist/assets/svg/dimensions.js +43 -0
- package/dist/assets/svg/dimensions.js.map +1 -0
- package/dist/assets/svg/rasterize.d.ts +6 -0
- package/dist/assets/svg/rasterize.d.ts.map +1 -0
- package/dist/assets/svg/rasterize.js +38 -0
- package/dist/assets/svg/rasterize.js.map +1 -0
- package/dist/assets/svg/resolve-content.d.ts +16 -0
- package/dist/assets/svg/resolve-content.d.ts.map +1 -0
- package/dist/assets/svg/resolve-content.js +38 -0
- package/dist/assets/svg/resolve-content.js.map +1 -0
- package/dist/assets/svg/sanitize.d.ts +22 -0
- package/dist/assets/svg/sanitize.d.ts.map +1 -0
- package/dist/assets/svg/sanitize.js +46 -0
- package/dist/assets/svg/sanitize.js.map +1 -0
- package/dist/assets/util/redact-path.d.ts +14 -0
- package/dist/assets/util/redact-path.d.ts.map +1 -0
- package/dist/assets/util/redact-path.js +16 -0
- package/dist/assets/util/redact-path.js.map +1 -0
- package/dist/assets.d.ts +10 -27
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +10 -549
- package/dist/assets.js.map +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +2 -1
- package/dist/builder.js.map +1 -1
- package/dist/cli.js +11 -1
- package/dist/cli.js.map +1 -1
- package/dist/compat.d.ts +63 -1
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +42 -5
- package/dist/compat.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/fonts.d.ts.map +1 -1
- package/dist/fonts.js +8 -10
- package/dist/fonts.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/layout-state.d.ts +1 -1
- package/dist/layout-state.d.ts.map +1 -1
- package/dist/layout-state.js +5 -0
- package/dist/layout-state.js.map +1 -1
- package/dist/measure-blocks/float-group.d.ts +9 -0
- package/dist/measure-blocks/float-group.d.ts.map +1 -0
- package/dist/measure-blocks/float-group.js +103 -0
- package/dist/measure-blocks/float-group.js.map +1 -0
- package/dist/measure-blocks/helpers.d.ts +44 -0
- package/dist/measure-blocks/helpers.d.ts.map +1 -0
- package/dist/measure-blocks/helpers.js +43 -0
- package/dist/measure-blocks/helpers.js.map +1 -0
- package/dist/measure-blocks/highlight.d.ts +26 -0
- package/dist/measure-blocks/highlight.d.ts.map +1 -0
- package/dist/measure-blocks/highlight.js +169 -0
- package/dist/measure-blocks/highlight.js.map +1 -0
- package/dist/measure-blocks/image.d.ts +9 -0
- package/dist/measure-blocks/image.d.ts.map +1 -0
- package/dist/measure-blocks/image.js +136 -0
- package/dist/measure-blocks/image.js.map +1 -0
- package/dist/measure-blocks/index.d.ts +24 -0
- package/dist/measure-blocks/index.d.ts.map +1 -0
- package/dist/measure-blocks/index.js +179 -0
- package/dist/measure-blocks/index.js.map +1 -0
- package/dist/measure-blocks/list.d.ts +8 -0
- package/dist/measure-blocks/list.d.ts.map +1 -0
- package/dist/measure-blocks/list.js +108 -0
- package/dist/measure-blocks/list.js.map +1 -0
- package/dist/measure-blocks/simple-blocks.d.ts +18 -0
- package/dist/measure-blocks/simple-blocks.d.ts.map +1 -0
- package/dist/measure-blocks/simple-blocks.js +121 -0
- package/dist/measure-blocks/simple-blocks.js.map +1 -0
- package/dist/measure-blocks/table/columns.d.ts +17 -0
- package/dist/measure-blocks/table/columns.d.ts.map +1 -0
- package/dist/measure-blocks/table/columns.js +83 -0
- package/dist/measure-blocks/table/columns.js.map +1 -0
- package/dist/measure-blocks/table/measure.d.ts +8 -0
- package/dist/measure-blocks/table/measure.d.ts.map +1 -0
- package/dist/measure-blocks/table/measure.js +231 -0
- package/dist/measure-blocks/table/measure.js.map +1 -0
- package/dist/measure-blocks/table/spans.d.ts +25 -0
- package/dist/measure-blocks/table/spans.d.ts.map +1 -0
- package/dist/measure-blocks/table/spans.js +55 -0
- package/dist/measure-blocks/table/spans.js.map +1 -0
- package/dist/measure-blocks/text-blocks.d.ts +17 -0
- package/dist/measure-blocks/text-blocks.d.ts.map +1 -0
- package/dist/measure-blocks/text-blocks.js +242 -0
- package/dist/measure-blocks/text-blocks.js.map +1 -0
- package/dist/measure-text.d.ts +21 -3
- package/dist/measure-text.d.ts.map +1 -1
- package/dist/measure-text.js +87 -36
- package/dist/measure-text.js.map +1 -1
- package/dist/measure.d.ts +1 -1
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +8 -6
- package/dist/measure.js.map +1 -1
- package/dist/node-polyfill.d.ts.map +1 -1
- package/dist/node-polyfill.js +9 -0
- package/dist/node-polyfill.js.map +1 -1
- package/dist/pipeline-footnotes.d.ts +1 -1
- package/dist/pipeline-footnotes.d.ts.map +1 -1
- package/dist/pipeline-toc.d.ts +1 -1
- package/dist/pipeline-toc.d.ts.map +1 -1
- package/dist/pipeline.d.ts +3 -3
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +4 -5
- package/dist/pipeline.js.map +1 -1
- package/dist/plugin-types.d.ts +1 -1
- package/dist/plugin-types.d.ts.map +1 -1
- package/dist/post-process.d.ts +2 -2
- package/dist/post-process.d.ts.map +1 -1
- package/dist/post-process.js +32 -9
- package/dist/post-process.js.map +1 -1
- package/dist/render-blocks/blockquote.d.ts +7 -0
- package/dist/render-blocks/blockquote.d.ts.map +1 -0
- package/dist/render-blocks/blockquote.js +87 -0
- package/dist/render-blocks/blockquote.js.map +1 -0
- package/dist/render-blocks/callout.d.ts +7 -0
- package/dist/render-blocks/callout.d.ts.map +1 -0
- package/dist/render-blocks/callout.js +84 -0
- package/dist/render-blocks/callout.js.map +1 -0
- package/dist/render-blocks/code.d.ts +7 -0
- package/dist/render-blocks/code.d.ts.map +1 -0
- package/dist/render-blocks/code.js +84 -0
- package/dist/render-blocks/code.js.map +1 -0
- package/dist/render-blocks/footnote.d.ts +11 -0
- package/dist/render-blocks/footnote.d.ts.map +1 -0
- package/dist/render-blocks/footnote.js +45 -0
- package/dist/render-blocks/footnote.js.map +1 -0
- package/dist/render-blocks/header-footer.d.ts +11 -0
- package/dist/render-blocks/header-footer.d.ts.map +1 -0
- package/dist/render-blocks/header-footer.js +56 -0
- package/dist/render-blocks/header-footer.js.map +1 -0
- package/dist/render-blocks/hr.d.ts +7 -0
- package/dist/render-blocks/hr.d.ts.map +1 -0
- package/dist/render-blocks/hr.js +24 -0
- package/dist/render-blocks/hr.js.map +1 -0
- package/dist/render-blocks/image.d.ts +9 -0
- package/dist/render-blocks/image.d.ts.map +1 -0
- package/dist/render-blocks/image.js +135 -0
- package/dist/render-blocks/image.js.map +1 -0
- package/dist/render-blocks/index.d.ts +17 -0
- package/dist/render-blocks/index.d.ts.map +1 -0
- package/dist/render-blocks/index.js +17 -0
- package/dist/render-blocks/index.js.map +1 -0
- package/dist/render-blocks/list-item.d.ts +7 -0
- package/dist/render-blocks/list-item.d.ts.map +1 -0
- package/dist/render-blocks/list-item.js +80 -0
- package/dist/render-blocks/list-item.js.map +1 -0
- package/dist/render-blocks/rich.d.ts +7 -0
- package/dist/render-blocks/rich.d.ts.map +1 -0
- package/dist/render-blocks/rich.js +160 -0
- package/dist/render-blocks/rich.js.map +1 -0
- package/dist/render-blocks/table.d.ts +7 -0
- package/dist/render-blocks/table.d.ts.map +1 -0
- package/dist/render-blocks/table.js +139 -0
- package/dist/render-blocks/table.js.map +1 -0
- package/dist/render-blocks/text.d.ts +7 -0
- package/dist/render-blocks/text.d.ts.map +1 -0
- package/dist/render-blocks/text.js +183 -0
- package/dist/render-blocks/text.js.map +1 -0
- package/dist/render-blocks/watermark.d.ts +8 -0
- package/dist/render-blocks/watermark.d.ts.map +1 -0
- package/dist/render-blocks/watermark.js +52 -0
- package/dist/render-blocks/watermark.js.map +1 -0
- package/dist/render-extras.d.ts.map +1 -1
- package/dist/render-extras.js +1 -2
- package/dist/render-extras.js.map +1 -1
- package/dist/render-utils.d.ts.map +1 -1
- package/dist/render-utils.js +10 -6
- package/dist/render-utils.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +9 -3
- package/dist/render.js.map +1 -1
- package/dist/rich-text.d.ts +2 -1
- package/dist/rich-text.d.ts.map +1 -1
- package/dist/rich-text.js +0 -1
- package/dist/rich-text.js.map +1 -1
- package/dist/types-internal.d.ts +19 -3
- package/dist/types-internal.d.ts.map +1 -1
- package/dist/types-public/document.d.ts +261 -0
- package/dist/types-public/document.d.ts.map +1 -0
- package/dist/types-public/document.js +2 -0
- package/dist/types-public/document.js.map +1 -0
- package/dist/types-public/elements-block.d.ts +246 -0
- package/dist/types-public/elements-block.d.ts.map +1 -0
- package/dist/types-public/elements-block.js +8 -0
- package/dist/types-public/elements-block.js.map +1 -0
- package/dist/types-public/elements-media.d.ts +199 -0
- package/dist/types-public/elements-media.d.ts.map +1 -0
- package/dist/types-public/elements-media.js +2 -0
- package/dist/types-public/elements-media.js.map +1 -0
- package/dist/types-public/elements-text.d.ts +327 -0
- package/dist/types-public/elements-text.d.ts.map +1 -0
- package/dist/types-public/elements-text.js +2 -0
- package/dist/types-public/elements-text.js.map +1 -0
- package/dist/types-public/index.d.ts +14 -0
- package/dist/types-public/index.d.ts.map +1 -0
- package/dist/types-public/index.js +2 -0
- package/dist/types-public/index.js.map +1 -0
- package/dist/types-public/render-options.d.ts +38 -0
- package/dist/types-public/render-options.d.ts.map +1 -0
- package/dist/types-public/render-options.js +2 -0
- package/dist/types-public/render-options.js.map +1 -0
- package/dist/types-public/union.d.ts +13 -0
- package/dist/types-public/union.d.ts.map +1 -0
- package/dist/types-public/union.js +2 -0
- package/dist/types-public/union.js.map +1 -0
- package/dist/types-public/validation.d.ts +64 -0
- package/dist/types-public/validation.d.ts.map +1 -0
- package/dist/types-public/validation.js +2 -0
- package/dist/types-public/validation.js.map +1 -0
- package/dist/types-public.d.ts +5 -1081
- package/dist/types-public.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/validate/document.d.ts +28 -0
- package/dist/validate/document.d.ts.map +1 -0
- package/dist/validate/document.js +295 -0
- package/dist/validate/document.js.map +1 -0
- package/dist/validate/elements/forms-floats.d.ts +19 -0
- package/dist/validate/elements/forms-floats.d.ts.map +1 -0
- package/dist/validate/elements/forms-floats.js +96 -0
- package/dist/validate/elements/forms-floats.js.map +1 -0
- package/dist/validate/elements/list.d.ts +10 -0
- package/dist/validate/elements/list.d.ts.map +1 -0
- package/dist/validate/elements/list.js +66 -0
- package/dist/validate/elements/list.js.map +1 -0
- package/dist/validate/elements/media.d.ts +23 -0
- package/dist/validate/elements/media.d.ts.map +1 -0
- package/dist/validate/elements/media.js +179 -0
- package/dist/validate/elements/media.js.map +1 -0
- package/dist/validate/elements/structural-simple.d.ts +21 -0
- package/dist/validate/elements/structural-simple.d.ts.map +1 -0
- package/dist/validate/elements/structural-simple.js +63 -0
- package/dist/validate/elements/structural-simple.js.map +1 -0
- package/dist/validate/elements/structural.d.ts +12 -0
- package/dist/validate/elements/structural.d.ts.map +1 -0
- package/dist/validate/elements/structural.js +12 -0
- package/dist/validate/elements/structural.js.map +1 -0
- package/dist/validate/elements/table.d.ts +10 -0
- package/dist/validate/elements/table.d.ts.map +1 -0
- package/dist/validate/elements/table.js +165 -0
- package/dist/validate/elements/table.js.map +1 -0
- package/dist/validate/elements/text.d.ts +26 -0
- package/dist/validate/elements/text.d.ts.map +1 -0
- package/dist/validate/elements/text.js +331 -0
- package/dist/validate/elements/text.js.map +1 -0
- package/dist/validate/errors.d.ts +9 -0
- package/dist/validate/errors.d.ts.map +1 -0
- package/dist/validate/errors.js +43 -0
- package/dist/validate/errors.js.map +1 -0
- package/dist/validate/fonts.d.ts +11 -0
- package/dist/validate/fonts.d.ts.map +1 -0
- package/dist/validate/fonts.js +118 -0
- package/dist/validate/fonts.js.map +1 -0
- package/dist/validate/helpers.d.ts +76 -0
- package/dist/validate/helpers.d.ts.map +1 -0
- package/dist/validate/helpers.js +169 -0
- package/dist/validate/helpers.js.map +1 -0
- package/dist/validate/index.d.ts +37 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +279 -0
- package/dist/validate/index.js.map +1 -0
- package/dist/validate.d.ts +6 -18
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +6 -1585
- package/dist/validate.js.map +1 -1
- package/dist/vendor/pretext/VERSION.d.ts +3 -0
- package/dist/vendor/pretext/VERSION.d.ts.map +1 -0
- package/dist/vendor/pretext/VERSION.js +12 -0
- package/dist/vendor/pretext/VERSION.js.map +1 -0
- package/dist/version-check.d.ts +47 -0
- package/dist/version-check.d.ts.map +1 -0
- package/dist/version-check.js +75 -0
- package/dist/version-check.js.map +1 -0
- package/package.json +26 -7
- package/dist/measure-blocks.d.ts +0 -26
- package/dist/measure-blocks.d.ts.map +0 -1
- package/dist/measure-blocks.js +0 -1317
- package/dist/measure-blocks.js.map +0 -1
- package/dist/render-blocks.d.ts +0 -28
- package/dist/render-blocks.d.ts.map +0 -1
- package/dist/render-blocks.js +0 -1059
- package/dist/render-blocks.js.map +0 -1
package/dist/validate.js
CHANGED
|
@@ -1,1589 +1,10 @@
|
|
|
1
|
-
import { PretextPdfError } from './errors.js';
|
|
2
|
-
import { resolvePageDimensions } from './page-sizes.js';
|
|
3
|
-
import { ALLOWED_PROPS, ALLOWED_PROPS_SUB } from './allowed-props.js';
|
|
4
|
-
import { ELEMENT_TYPES } from './element-types.js';
|
|
5
|
-
import { findPlugin, runPluginValidate } from './plugin-registry.js';
|
|
6
1
|
/**
|
|
7
|
-
*
|
|
8
|
-
* Block-level coverage following Unicode DerivedBidiClass defaults.
|
|
9
|
-
* Source: https://www.unicode.org/Public/17.0.0/ucd/extracted/DerivedBidiClass.txt
|
|
2
|
+
* src/validate.ts — legacy shim, re-exports the new validate/ module.
|
|
10
3
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 0750–077F Arabic Supplement
|
|
16
|
-
* 0780–07BF Thaana (Maldivian)
|
|
17
|
-
* 07C0–07FF N'Ko (West African)
|
|
18
|
-
* 0800–083F Samaritan
|
|
19
|
-
* 0840–085F Mandaic
|
|
20
|
-
* 0860–086F Syriac Supplement
|
|
21
|
-
* 0870–089F Arabic Extended-B
|
|
22
|
-
* 08A0–08FF Arabic Extended-A
|
|
23
|
-
* FB1D–FB4F Hebrew Presentation Forms
|
|
24
|
-
* FB50–FDFF Arabic Presentation Forms-A
|
|
25
|
-
* FE70–FEFF Arabic Presentation Forms-B
|
|
26
|
-
* Supplementary plane blocks (requires /u flag):
|
|
27
|
-
* 10800–10CFF Ancient Semitic (Cypriot, Imperial Aramaic, Palmyrene,
|
|
28
|
-
* Nabataean, Hatran, Phoenician, Lydian, Old South/North
|
|
29
|
-
* Arabian, Manichaean, Avestan, Inscriptional Parthian/
|
|
30
|
-
* Pahlavi, Old Turkic, Old Hungarian)
|
|
31
|
-
* 10D00–10D3F Hanifi Rohingya
|
|
32
|
-
* 10E80–10EFF Yezidi + Arabic Extended-C
|
|
33
|
-
* 10F30–10FFF Sogdian, Old Uyghur, Chorasmian, Elymaic
|
|
34
|
-
* 1E800–1E95F Mende Kikakui + Adlam (Fulani)
|
|
35
|
-
* 1EC70–1ECBF Indic Siyaq Numbers (AL)
|
|
36
|
-
* 1EE00–1EEFF Arabic Mathematical Alphabetic Symbols
|
|
4
|
+
* Original (1834L) was split into validate/{index,helpers,fonts,errors}.ts +
|
|
5
|
+
* validate/elements/* in v1.4.0 #11a. This shim exists for one release so
|
|
6
|
+
* external callers importing from 'pretext-pdf/dist/validate.js' (or the
|
|
7
|
+
* source path) continue to work; it will be deleted in step 3 of the split.
|
|
37
8
|
*/
|
|
38
|
-
|
|
39
|
-
/** Valid 6-digit hex color */
|
|
40
|
-
const HEX_COLOR_REGEX = /^#[0-9A-Fa-f]{6}$/;
|
|
41
|
-
/** Allowed URL schemes for hyperlinks — blocks javascript:, data:, vbscript: */
|
|
42
|
-
const SAFE_URL_SCHEME = /^(https?|mailto|ftp|#)/i;
|
|
43
|
-
/** BCP47 language tag pattern for hyphenation.language — prevents dynamic-import path injection */
|
|
44
|
-
const LANGUAGE_TAG_REGEX = /^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{2,8})*$/;
|
|
45
|
-
/** Levenshtein distance, returns 999 if result would exceed 2 */
|
|
46
|
-
function levenshteinDist(a, b) {
|
|
47
|
-
if (a === b)
|
|
48
|
-
return 0;
|
|
49
|
-
if (Math.abs(a.length - b.length) > 2)
|
|
50
|
-
return 999;
|
|
51
|
-
const m = a.length;
|
|
52
|
-
const n = b.length;
|
|
53
|
-
const prev = Array(n + 1)
|
|
54
|
-
.fill(0)
|
|
55
|
-
.map((_, j) => j);
|
|
56
|
-
const curr = Array(n + 1).fill(0);
|
|
57
|
-
for (let i = 1; i <= m; i++) {
|
|
58
|
-
curr[0] = i;
|
|
59
|
-
for (let j = 1; j <= n; j++) {
|
|
60
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
61
|
-
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
62
|
-
}
|
|
63
|
-
for (let j = 0; j <= n; j++)
|
|
64
|
-
prev[j] = curr[j];
|
|
65
|
-
}
|
|
66
|
-
return prev[n] > 2 ? 999 : prev[n];
|
|
67
|
-
}
|
|
68
|
-
/** Find closest match with edit distance <= 2 */
|
|
69
|
-
function closestMatch(prop, allowed) {
|
|
70
|
-
let best = null;
|
|
71
|
-
let bestDist = 999;
|
|
72
|
-
for (const candidate of allowed) {
|
|
73
|
-
const d = levenshteinDist(prop, candidate);
|
|
74
|
-
if (d > 0 && d <= 2 && d < bestDist) {
|
|
75
|
-
best = candidate;
|
|
76
|
-
bestDist = d;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return best;
|
|
80
|
-
}
|
|
81
|
-
/** Accumulate unknown property errors for an object */
|
|
82
|
-
function assertUnknownProps(obj, allowed, path, errors) {
|
|
83
|
-
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
|
|
84
|
-
return;
|
|
85
|
-
for (const key of Object.keys(obj)) {
|
|
86
|
-
if (!allowed.has(key)) {
|
|
87
|
-
const suggestion = closestMatch(key, allowed);
|
|
88
|
-
const hint = suggestion ? `; did you mean "${suggestion}"` : '';
|
|
89
|
-
errors.push(`${path}.${key}: unknown property${hint}`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
/** Format accumulated errors into a single message (cap at 20 errors) */
|
|
94
|
-
function formatErrors(errors) {
|
|
95
|
-
if (errors.length === 0)
|
|
96
|
-
return '';
|
|
97
|
-
const header = `Strict validation failed (${errors.length} issue${errors.length === 1 ? '' : 's'}):\n`;
|
|
98
|
-
const msgs = errors.slice(0, 20);
|
|
99
|
-
const suffix = errors.length > 20 ? `\n... and ${errors.length - 20} more error(s)` : '';
|
|
100
|
-
return header + msgs.join('\n') + suffix;
|
|
101
|
-
}
|
|
102
|
-
/** Validate a hyperlink URL — throws VALIDATION_ERROR for unsafe schemes */
|
|
103
|
-
function validateUrl(url, prefix) {
|
|
104
|
-
if (!SAFE_URL_SCHEME.test(url)) {
|
|
105
|
-
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)}"`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/** Validate a metadata string field — rejects control chars and enforces length */
|
|
109
|
-
function validateMetadataString(value, fieldName) {
|
|
110
|
-
if (value.length > 1000) {
|
|
111
|
-
throw new PretextPdfError('VALIDATION_ERROR', `metadata.${fieldName} must not exceed 1000 characters`);
|
|
112
|
-
}
|
|
113
|
-
if (/[\x00\r\n]/.test(value)) {
|
|
114
|
-
throw new PretextPdfError('VALIDATION_ERROR', `metadata.${fieldName} must not contain null bytes or newline characters`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
/** Valid column width: positive number OR '2*', '*', '1.5*' format */
|
|
118
|
-
const STAR_WIDTH_REGEX = /^(\d*\.?\d+)?\*$/;
|
|
119
|
-
/** Families always available without explicit doc.fonts entry */
|
|
120
|
-
const BUNDLED_FAMILIES = new Set(['Inter']);
|
|
121
|
-
/** Font variants (family-weight-style) always available without explicit doc.fonts entry */
|
|
122
|
-
const BUNDLED_VARIANTS = new Set(['Inter-400-normal', 'Inter-700-normal', 'Inter-400-italic', 'Inter-700-italic']);
|
|
123
|
-
/**
|
|
124
|
-
* Validate a PdfDocument and throw a {@link PretextPdfError} if any errors are found.
|
|
125
|
-
* @public
|
|
126
|
-
*/
|
|
127
|
-
export function validate(doc, options) {
|
|
128
|
-
const strict = options?.strict ?? false;
|
|
129
|
-
const errors = [];
|
|
130
|
-
// Plugin pre-flight: enforce type string safety and no collision with built-ins
|
|
131
|
-
for (const plugin of options?.plugins ?? []) {
|
|
132
|
-
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(plugin.type)) {
|
|
133
|
-
throw new PretextPdfError('VALIDATION_ERROR', `Plugin type '${plugin.type}' is invalid. Must start with a letter and contain only letters, digits, hyphens, or underscores.`);
|
|
134
|
-
}
|
|
135
|
-
if (ELEMENT_TYPES.includes(plugin.type)) {
|
|
136
|
-
throw new PretextPdfError('VALIDATION_ERROR', `Plugin type '${plugin.type}' collides with a built-in element type. Choose a different type string.`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
// Strict: check doc-level properties
|
|
140
|
-
if (strict) {
|
|
141
|
-
assertUnknownProps(doc, ALLOWED_PROPS_SUB['document'], 'doc', errors);
|
|
142
|
-
}
|
|
143
|
-
// content must be a non-empty array
|
|
144
|
-
if (!Array.isArray(doc.content) || doc.content.length === 0) {
|
|
145
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'document.content must be a non-empty array');
|
|
146
|
-
}
|
|
147
|
-
// memory guard
|
|
148
|
-
if (doc.content.length > 50_000) {
|
|
149
|
-
throw new PretextPdfError('VALIDATION_ERROR', `document.content has ${doc.content.length} elements (hard limit: 50,000). Split into multiple documents.`);
|
|
150
|
-
}
|
|
151
|
-
if (doc.content.length > 10_000) {
|
|
152
|
-
;
|
|
153
|
-
(options?.logger?.warn ?? console.warn)(`[pretext-pdf] Performance advisory: document.content has ${doc.content.length} elements (recommended max: 10,000). Large documents may be slow.`);
|
|
154
|
-
}
|
|
155
|
-
// page size
|
|
156
|
-
if (Array.isArray(doc.pageSize)) {
|
|
157
|
-
const [w, h] = doc.pageSize;
|
|
158
|
-
if (typeof w !== 'number' || typeof h !== 'number' ||
|
|
159
|
-
!isFinite(w) || !isFinite(h) ||
|
|
160
|
-
w <= 0 || h <= 0) {
|
|
161
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'pageSize array must be [width, height] with two positive finite numbers in pt');
|
|
162
|
-
}
|
|
163
|
-
if (w > 14400 || h > 14400) {
|
|
164
|
-
throw new PretextPdfError('VALIDATION_ERROR', `pageSize [${w}, ${h}] exceeds maximum 14400pt (200 inches). Values this large cause rendering overflow.`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
// margins must be non-negative and can't make content area zero/negative
|
|
168
|
-
if (doc.margins) {
|
|
169
|
-
const m = doc.margins;
|
|
170
|
-
for (const side of ['top', 'bottom', 'left', 'right']) {
|
|
171
|
-
if (m[side] !== undefined && (typeof m[side] !== 'number' || m[side] < 0 || !isFinite(m[side]))) {
|
|
172
|
-
throw new PretextPdfError('VALIDATION_ERROR', `margins.${side} must be a non-negative finite number. Got: ${m[side]}`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const [pageW, pageH] = resolvePageDimensions(doc.pageSize);
|
|
176
|
-
const left = m.left ?? 72;
|
|
177
|
-
const right = m.right ?? 72;
|
|
178
|
-
const top = m.top ?? 72;
|
|
179
|
-
const bottom = m.bottom ?? 72;
|
|
180
|
-
if (pageW - left - right <= 0) {
|
|
181
|
-
throw new PretextPdfError('PAGE_TOO_SMALL', `Left+right margins (${left}+${right}) exceed page width (${pageW}pt). Content area would be zero or negative.`);
|
|
182
|
-
}
|
|
183
|
-
if (pageH - top - bottom <= 0) {
|
|
184
|
-
throw new PretextPdfError('PAGE_TOO_SMALL', `Top+bottom margins (${top}+${bottom}) exceed page height (${pageH}pt). Content area would be zero or negative.`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
// font specs
|
|
188
|
-
if (doc.fonts) {
|
|
189
|
-
for (const font of doc.fonts) {
|
|
190
|
-
validateFontSpec(font);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// header / footer
|
|
194
|
-
for (const [spec, label] of [[doc.header, 'doc.header'], [doc.footer, 'doc.footer']]) {
|
|
195
|
-
if (!spec)
|
|
196
|
-
continue;
|
|
197
|
-
if (typeof spec.text !== 'string') {
|
|
198
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${label}.text must be a string`);
|
|
199
|
-
}
|
|
200
|
-
if (spec.fontSize !== undefined && (typeof spec.fontSize !== 'number' || spec.fontSize <= 0 || !isFinite(spec.fontSize))) {
|
|
201
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${label}.fontSize must be a positive finite number`);
|
|
202
|
-
}
|
|
203
|
-
if (spec.align !== undefined && !['left', 'center', 'right'].includes(spec.align)) {
|
|
204
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${label}.align must be 'left', 'center', or 'right'`);
|
|
205
|
-
}
|
|
206
|
-
if (spec.fontWeight !== undefined && ![400, 700].includes(spec.fontWeight)) {
|
|
207
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${label}.fontWeight must be 400 or 700`);
|
|
208
|
-
}
|
|
209
|
-
if (spec.color !== undefined && !HEX_COLOR_REGEX.test(spec.color)) {
|
|
210
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${label}.color must be a 6-digit hex string like '#666666'. Got: '${spec.color}'`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// defaultParagraphStyle
|
|
214
|
-
if (doc.defaultParagraphStyle !== undefined) {
|
|
215
|
-
const dps = doc.defaultParagraphStyle;
|
|
216
|
-
if (typeof dps !== 'object' || dps === null || Array.isArray(dps)) {
|
|
217
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.defaultParagraphStyle must be an object');
|
|
218
|
-
}
|
|
219
|
-
if (dps.fontSize !== undefined && (typeof dps.fontSize !== 'number' || dps.fontSize <= 0 || dps.fontSize > 500 || !isFinite(dps.fontSize))) {
|
|
220
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.defaultParagraphStyle.fontSize must be a number > 0 and <= 500');
|
|
221
|
-
}
|
|
222
|
-
if (dps.lineHeight !== undefined && (typeof dps.lineHeight !== 'number' || dps.lineHeight <= 0 || !isFinite(dps.lineHeight))) {
|
|
223
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.defaultParagraphStyle.lineHeight must be a positive finite number');
|
|
224
|
-
}
|
|
225
|
-
if (dps.color !== undefined && !HEX_COLOR_REGEX.test(dps.color)) {
|
|
226
|
-
throw new PretextPdfError('VALIDATION_ERROR', `doc.defaultParagraphStyle.color must be a 6-digit hex string like '#000000'. Got: '${dps.color}'`);
|
|
227
|
-
}
|
|
228
|
-
if (dps.align !== undefined && !['left', 'center', 'right', 'justify'].includes(dps.align)) {
|
|
229
|
-
throw new PretextPdfError('VALIDATION_ERROR', "doc.defaultParagraphStyle.align must be 'left', 'center', 'right', or 'justify'");
|
|
230
|
-
}
|
|
231
|
-
if (dps.letterSpacing !== undefined && (typeof dps.letterSpacing !== 'number' || dps.letterSpacing < 0 || dps.letterSpacing > 200 || !isFinite(dps.letterSpacing))) {
|
|
232
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.defaultParagraphStyle.letterSpacing must be a number >= 0 and <= 200');
|
|
233
|
-
}
|
|
234
|
-
if (dps.fontWeight !== undefined && ![400, 700].includes(dps.fontWeight)) {
|
|
235
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.defaultParagraphStyle.fontWeight must be 400 or 700');
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
// sections
|
|
239
|
-
if (doc.sections !== undefined) {
|
|
240
|
-
if (!Array.isArray(doc.sections)) {
|
|
241
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.sections must be an array');
|
|
242
|
-
}
|
|
243
|
-
for (let i = 0; i < doc.sections.length; i++) {
|
|
244
|
-
const s = doc.sections[i];
|
|
245
|
-
const label = `doc.sections[${i}]`;
|
|
246
|
-
if (s.fromPage !== undefined && (typeof s.fromPage !== 'number' || !Number.isInteger(s.fromPage) || s.fromPage < 1)) {
|
|
247
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${label}.fromPage must be a positive integer`);
|
|
248
|
-
}
|
|
249
|
-
if (s.toPage !== undefined && (typeof s.toPage !== 'number' || !Number.isInteger(s.toPage) || s.toPage < 1)) {
|
|
250
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${label}.toPage must be a positive integer`);
|
|
251
|
-
}
|
|
252
|
-
if (s.fromPage !== undefined && s.toPage !== undefined && s.fromPage > s.toPage) {
|
|
253
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${label}.fromPage (${s.fromPage}) must be <= toPage (${s.toPage})`);
|
|
254
|
-
}
|
|
255
|
-
for (const [spec, slabel] of [[s.header, `${label}.header`], [s.footer, `${label}.footer`]]) {
|
|
256
|
-
if (!spec)
|
|
257
|
-
continue;
|
|
258
|
-
if (typeof spec.text !== 'string') {
|
|
259
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${slabel}.text must be a string`);
|
|
260
|
-
}
|
|
261
|
-
if (spec.fontSize !== undefined && (typeof spec.fontSize !== 'number' || spec.fontSize <= 0 || !isFinite(spec.fontSize))) {
|
|
262
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${slabel}.fontSize must be a positive finite number`);
|
|
263
|
-
}
|
|
264
|
-
if (spec.align !== undefined && !['left', 'center', 'right'].includes(spec.align)) {
|
|
265
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${slabel}.align must be 'left', 'center', or 'right'`);
|
|
266
|
-
}
|
|
267
|
-
if (spec.fontWeight !== undefined && ![400, 700].includes(spec.fontWeight)) {
|
|
268
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${slabel}.fontWeight must be 400 or 700`);
|
|
269
|
-
}
|
|
270
|
-
if (spec.color !== undefined && !HEX_COLOR_REGEX.test(spec.color)) {
|
|
271
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${slabel}.color must be a 6-digit hex string like '#666666'. Got: '${spec.color}'`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
// watermark
|
|
277
|
-
if (doc.watermark) {
|
|
278
|
-
const wm = doc.watermark;
|
|
279
|
-
if (!wm.text && !wm.image) {
|
|
280
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark requires either .text or .image');
|
|
281
|
-
}
|
|
282
|
-
if (wm.opacity !== undefined && (typeof wm.opacity !== 'number' || wm.opacity < 0 || wm.opacity > 1 || !isFinite(wm.opacity))) {
|
|
283
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark.opacity must be a number 0.0–1.0');
|
|
284
|
-
}
|
|
285
|
-
if (wm.fontSize !== undefined && (typeof wm.fontSize !== 'number' || wm.fontSize <= 0 || wm.fontSize > 500 || !isFinite(wm.fontSize))) {
|
|
286
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark.fontSize must be a positive finite number and <= 500');
|
|
287
|
-
}
|
|
288
|
-
if (wm.fontWeight !== undefined && ![400, 700].includes(wm.fontWeight)) {
|
|
289
|
-
throw new PretextPdfError('VALIDATION_ERROR', "doc.watermark.fontWeight must be 400 or 700");
|
|
290
|
-
}
|
|
291
|
-
if (wm.color !== undefined && !HEX_COLOR_REGEX.test(wm.color)) {
|
|
292
|
-
throw new PretextPdfError('VALIDATION_ERROR', `doc.watermark.color must be a 6-digit hex string. Got: '${wm.color}'`);
|
|
293
|
-
}
|
|
294
|
-
if (wm.rotation !== undefined) {
|
|
295
|
-
if (typeof wm.rotation !== 'number' || !isFinite(wm.rotation)) {
|
|
296
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark.rotation must be a finite number');
|
|
297
|
-
}
|
|
298
|
-
if (wm.rotation < -360 || wm.rotation > 360) {
|
|
299
|
-
throw new PretextPdfError('WATERMARK_ROTATION_OUT_OF_RANGE', 'doc.watermark.rotation must be between -360 and 360 degrees');
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
// encryption
|
|
304
|
-
if (doc.encryption) {
|
|
305
|
-
const enc = doc.encryption;
|
|
306
|
-
if (strict) {
|
|
307
|
-
assertUnknownProps(enc, ALLOWED_PROPS_SUB['encryption'], 'doc.encryption', errors);
|
|
308
|
-
}
|
|
309
|
-
if (enc.userPassword !== undefined && typeof enc.userPassword !== 'string') {
|
|
310
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.userPassword must be a string if provided');
|
|
311
|
-
}
|
|
312
|
-
if (enc.userPassword !== undefined && enc.userPassword === '') {
|
|
313
|
-
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.');
|
|
314
|
-
}
|
|
315
|
-
if (enc.ownerPassword !== undefined && typeof enc.ownerPassword !== 'string') {
|
|
316
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.ownerPassword must be a string if provided');
|
|
317
|
-
}
|
|
318
|
-
if (enc.ownerPassword !== undefined && enc.ownerPassword === '') {
|
|
319
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.ownerPassword must not be an empty string');
|
|
320
|
-
}
|
|
321
|
-
// permissions sub-fields are booleans — TypeScript enforces the type, no runtime check needed
|
|
322
|
-
}
|
|
323
|
-
if (doc.signature !== undefined) {
|
|
324
|
-
const sig = doc.signature;
|
|
325
|
-
if (sig.width !== undefined && (typeof sig.width !== 'number' || sig.width <= 0)) {
|
|
326
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'signature.width must be a positive number');
|
|
327
|
-
}
|
|
328
|
-
if (sig.height !== undefined && (typeof sig.height !== 'number' || sig.height <= 0)) {
|
|
329
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'signature.height must be a positive number');
|
|
330
|
-
}
|
|
331
|
-
if (sig.page !== undefined && (!Number.isInteger(sig.page) || sig.page < 0)) {
|
|
332
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'signature.page must be a non-negative integer');
|
|
333
|
-
}
|
|
334
|
-
if (sig.borderColor !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(sig.borderColor)) {
|
|
335
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'signature.borderColor must be a 6-digit hex color e.g. "#000000"');
|
|
336
|
-
}
|
|
337
|
-
if (sig.fontSize !== undefined && (typeof sig.fontSize !== 'number' || sig.fontSize <= 0)) {
|
|
338
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'signature.fontSize must be a positive number');
|
|
339
|
-
}
|
|
340
|
-
// Crypto signature validation
|
|
341
|
-
if (sig.p12 !== undefined) {
|
|
342
|
-
if (typeof sig.p12 !== 'string' && !(sig.p12 instanceof Uint8Array)) {
|
|
343
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'signature.p12 must be a file path string or Uint8Array of certificate bytes');
|
|
344
|
-
}
|
|
345
|
-
if (typeof sig.p12 === 'string' && sig.p12.trim() === '') {
|
|
346
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'signature.p12 must not be an empty string');
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
if (sig.passphrase !== undefined && typeof sig.passphrase !== 'string') {
|
|
350
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'signature.passphrase must be a string');
|
|
351
|
-
}
|
|
352
|
-
if (sig.contactInfo !== undefined && typeof sig.contactInfo !== 'string') {
|
|
353
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'signature.contactInfo must be a string');
|
|
354
|
-
}
|
|
355
|
-
if (sig.invisible !== undefined && typeof sig.invisible !== 'boolean') {
|
|
356
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'signature.invisible must be a boolean');
|
|
357
|
-
}
|
|
358
|
-
if (sig.p12 !== undefined && doc.encryption !== undefined) {
|
|
359
|
-
throw new PretextPdfError('SIGNATURE_CERT_AND_ENCRYPTION', 'Cannot use both signature.p12 (cryptographic signing) and encryption together — the encryption step would invalidate the cryptographic signature.');
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
// bookmarks
|
|
363
|
-
if (doc.bookmarks !== undefined && doc.bookmarks !== false) {
|
|
364
|
-
const bm = doc.bookmarks;
|
|
365
|
-
if (bm.minLevel !== undefined && ![1, 2, 3, 4].includes(bm.minLevel)) {
|
|
366
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.bookmarks.minLevel must be 1, 2, 3, or 4');
|
|
367
|
-
}
|
|
368
|
-
if (bm.maxLevel !== undefined && ![1, 2, 3, 4].includes(bm.maxLevel)) {
|
|
369
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.bookmarks.maxLevel must be 1, 2, 3, or 4');
|
|
370
|
-
}
|
|
371
|
-
if (bm.minLevel !== undefined && bm.maxLevel !== undefined && bm.minLevel > bm.maxLevel) {
|
|
372
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.bookmarks.minLevel must be ≤ maxLevel');
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
// hyphenation
|
|
376
|
-
if (doc.hyphenation) {
|
|
377
|
-
const h = doc.hyphenation;
|
|
378
|
-
if (!h.language || typeof h.language !== 'string') {
|
|
379
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.language is required (e.g. "en-us")');
|
|
380
|
-
}
|
|
381
|
-
if (!LANGUAGE_TAG_REGEX.test(h.language)) {
|
|
382
|
-
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}"`);
|
|
383
|
-
}
|
|
384
|
-
if (h.minWordLength !== undefined && (h.minWordLength < 2 || h.minWordLength > 20)) {
|
|
385
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.minWordLength must be 2–20');
|
|
386
|
-
}
|
|
387
|
-
if (h.leftMin !== undefined && (h.leftMin < 1 || h.leftMin > 5)) {
|
|
388
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.leftMin must be 1–5');
|
|
389
|
-
}
|
|
390
|
-
if (h.rightMin !== undefined && (h.rightMin < 1 || h.rightMin > 5)) {
|
|
391
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.rightMin must be 1–5');
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
// metadata
|
|
395
|
-
if (doc.metadata) {
|
|
396
|
-
const m = doc.metadata;
|
|
397
|
-
if (m.language !== undefined && (typeof m.language !== 'string' || m.language.trim() === '')) {
|
|
398
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'metadata.language must be a non-empty string (BCP47 tag e.g. "en-US")');
|
|
399
|
-
}
|
|
400
|
-
if (m.language !== undefined && typeof m.language === 'string')
|
|
401
|
-
validateMetadataString(m.language, 'language');
|
|
402
|
-
if (m.producer !== undefined && (typeof m.producer !== 'string' || m.producer.trim() === '')) {
|
|
403
|
-
throw new PretextPdfError('VALIDATION_ERROR', 'metadata.producer must be a non-empty string');
|
|
404
|
-
}
|
|
405
|
-
// Validate free-text fields for injection chars and length
|
|
406
|
-
for (const field of ['title', 'author', 'subject', 'keywords', 'creator', 'producer']) {
|
|
407
|
-
const val = m[field];
|
|
408
|
-
if (val !== undefined && typeof val === 'string')
|
|
409
|
-
validateMetadataString(val, field);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
// validate each content element
|
|
413
|
-
const loadedFamilies = new Set([
|
|
414
|
-
...BUNDLED_FAMILIES,
|
|
415
|
-
...(doc.fonts ?? []).map(f => f.family),
|
|
416
|
-
]);
|
|
417
|
-
// Check for duplicate form field names
|
|
418
|
-
const formFieldNames = new Set();
|
|
419
|
-
for (const el of doc.content) {
|
|
420
|
-
if (el.type === 'form-field') {
|
|
421
|
-
if (formFieldNames.has(el.name)) {
|
|
422
|
-
throw new PretextPdfError('FORM_FIELD_NAME_DUPLICATE', `Duplicate form field name: "${el.name}". Each form field must have a unique name.`);
|
|
423
|
-
}
|
|
424
|
-
formFieldNames.add(el.name);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
for (let i = 0; i < doc.content.length; i++) {
|
|
428
|
-
validateElement(doc.content[i], i, loadedFamilies, strict, errors, options);
|
|
429
|
-
}
|
|
430
|
-
// ── Footnote ref/def cross-validation ─────────────────────────────────────
|
|
431
|
-
const footnoteDefIds = new Map(); // id → content index
|
|
432
|
-
const footnoteRefIds = new Set();
|
|
433
|
-
// Collect all def ids
|
|
434
|
-
for (let i = 0; i < doc.content.length; i++) {
|
|
435
|
-
const el = doc.content[i];
|
|
436
|
-
if (el.type === 'footnote-def') {
|
|
437
|
-
if (footnoteDefIds.has(el.id)) {
|
|
438
|
-
throw new PretextPdfError('FOOTNOTE_DEF_DUPLICATE', `content[${i}] (footnote-def): duplicate id "${el.id}". Each footnote must have a unique id.`);
|
|
439
|
-
}
|
|
440
|
-
footnoteDefIds.set(el.id, i);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
// Collect all ref ids from rich-paragraph spans
|
|
444
|
-
for (const el of doc.content) {
|
|
445
|
-
if (el.type === 'rich-paragraph') {
|
|
446
|
-
for (const span of el.spans) {
|
|
447
|
-
if (span.footnoteRef) {
|
|
448
|
-
footnoteRefIds.add(span.footnoteRef);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
// Orphaned ref: ref id with no matching def
|
|
454
|
-
for (const refId of footnoteRefIds) {
|
|
455
|
-
if (!footnoteDefIds.has(refId)) {
|
|
456
|
-
throw new PretextPdfError('FOOTNOTE_REF_ORPHANED', `A rich-paragraph span references footnote id "${refId}" but no footnote-def with that id exists in doc.content.`);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
// Orphaned def: def id never referenced
|
|
460
|
-
for (const [defId] of footnoteDefIds) {
|
|
461
|
-
if (!footnoteRefIds.has(defId)) {
|
|
462
|
-
throw new PretextPdfError('FOOTNOTE_DEF_ORPHANED', `footnote-def "${defId}" is defined but never referenced by any rich-paragraph span.`);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
// validate all font references are loadable
|
|
466
|
-
validateFontReferences(doc, loadedFamilies);
|
|
467
|
-
// Throw collected strict validation errors
|
|
468
|
-
if (errors.length > 0) {
|
|
469
|
-
const msg = formatErrors(errors);
|
|
470
|
-
throw new PretextPdfError('VALIDATION_ERROR', msg);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* Validate a pretext-pdf document and return a structured result instead of throwing.
|
|
475
|
-
*
|
|
476
|
-
* Use this when you want to inspect all validation errors programmatically.
|
|
477
|
-
* The existing {@link validate} function throws on first error and is unchanged.
|
|
478
|
-
*
|
|
479
|
-
* @param doc - The document to validate (typed as `unknown` to accept unverified input)
|
|
480
|
-
* @param options - `{ strict?: boolean; logger?: Logger }` — strict defaults to false (matches render() behavior); logger routes diagnostic warnings away from console.warn
|
|
481
|
-
* @returns {@link ValidationResult} with `valid`, `errors[]`, and `errorCount`
|
|
482
|
-
* @public
|
|
483
|
-
*/
|
|
484
|
-
export function validateDocument(doc, options) {
|
|
485
|
-
try {
|
|
486
|
-
validate(doc, { strict: options?.strict ?? false, ...(options?.logger !== undefined ? { logger: options.logger } : {}) });
|
|
487
|
-
return { valid: true, errors: [], errorCount: 0, warningCount: 0 };
|
|
488
|
-
}
|
|
489
|
-
catch (err) {
|
|
490
|
-
if (err instanceof PretextPdfError) {
|
|
491
|
-
const errors = parseValidationErrorsStructured(err.message, err.code);
|
|
492
|
-
const warnings = errors.filter((e) => e.severity === 'warning');
|
|
493
|
-
const warningCount = warnings.length;
|
|
494
|
-
const countMatch = err.message.match(/Strict validation failed \((\d+) issue/);
|
|
495
|
-
const errorCount = countMatch?.[1] != null ? parseInt(countMatch[1], 10) : errors.length;
|
|
496
|
-
return { valid: false, errors, errorCount, warningCount };
|
|
497
|
-
}
|
|
498
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
499
|
-
return { valid: false, errors: [{ path: 'document', message: `Unexpected validation error: ${msg}`, severity: 'error', code: 'VALIDATION_ERROR' }], errorCount: 1, warningCount: 0 };
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
/** Parse a PretextPdfError message into structured ValidationError entries */
|
|
503
|
-
function parseValidationErrorsStructured(message, code) {
|
|
504
|
-
if (message.startsWith('Strict validation failed')) {
|
|
505
|
-
const lines = message
|
|
506
|
-
.split('\n')
|
|
507
|
-
.slice(1) // skip "Strict validation failed (N issues):" header
|
|
508
|
-
.filter(l => Boolean(l.trim()) && !l.startsWith('...'));
|
|
509
|
-
return lines.map(line => {
|
|
510
|
-
const colonIdx = line.indexOf(':');
|
|
511
|
-
const path = colonIdx > 0 ? line.slice(0, colonIdx).trim() : 'document';
|
|
512
|
-
const rest = colonIdx > 0 ? line.slice(colonIdx + 1).trim() : line.trim();
|
|
513
|
-
const suggMatch = rest.match(/did you mean "([^"]+)"/);
|
|
514
|
-
const suggestion = suggMatch ? suggMatch[1] : undefined;
|
|
515
|
-
const isUnknown = rest.includes('unknown property');
|
|
516
|
-
const unknownProp = isUnknown ? (/\.([^.[]+)$/.exec(path)?.[1] ?? path) : undefined;
|
|
517
|
-
return {
|
|
518
|
-
path,
|
|
519
|
-
message: rest,
|
|
520
|
-
code: 'UNKNOWN_PROPERTY',
|
|
521
|
-
severity: 'error',
|
|
522
|
-
...(unknownProp !== undefined && { unknownProp }),
|
|
523
|
-
...(suggestion !== undefined && { suggestion }),
|
|
524
|
-
};
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
const colonIdx = message.indexOf(':');
|
|
528
|
-
// A valid path (e.g. "content[0] (paragraph) spans[0].href") never contains ". " (period + space).
|
|
529
|
-
// Prose fragments like "margins.left must be a non-negative finite number. Got" do, so we reject them.
|
|
530
|
-
const candidate = message.slice(0, colonIdx).trim();
|
|
531
|
-
const hasPathPrefix = colonIdx > 0 && !/\. /.test(candidate);
|
|
532
|
-
const path = hasPathPrefix ? candidate : 'document';
|
|
533
|
-
const msgText = hasPathPrefix ? message.slice(colonIdx + 1).trim() : message;
|
|
534
|
-
return [{
|
|
535
|
-
path,
|
|
536
|
-
message: msgText,
|
|
537
|
-
code: code,
|
|
538
|
-
severity: 'error',
|
|
539
|
-
}];
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Validate that every font family referenced anywhere in the document
|
|
543
|
-
* is either bundled (Inter) or present in doc.fonts.
|
|
544
|
-
* Catches problems early instead of silently falling back or dropping content.
|
|
545
|
-
*/
|
|
546
|
-
function validateFontReferences(doc, loadedFamilies) {
|
|
547
|
-
const defaultFamily = doc.defaultFont ?? 'Inter';
|
|
548
|
-
// Build a variant-level set for italic checks: "Family-weight-style"
|
|
549
|
-
const loadedVariants = new Set(BUNDLED_VARIANTS);
|
|
550
|
-
for (const f of doc.fonts ?? []) {
|
|
551
|
-
loadedVariants.add(`${f.family}-${f.weight ?? 400}-${f.style ?? 'normal'}`);
|
|
552
|
-
}
|
|
553
|
-
const requireFamily = (family, context) => {
|
|
554
|
-
if (!/^[a-zA-Z0-9 ._+\-]+$/.test(family)) {
|
|
555
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${context}: font family name "${family}" contains invalid characters. Use only letters, digits, spaces, hyphens, and underscores.`);
|
|
556
|
-
}
|
|
557
|
-
if (!loadedFamilies.has(family)) {
|
|
558
|
-
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}').`);
|
|
559
|
-
}
|
|
560
|
-
};
|
|
561
|
-
const requireVariant = (family, weight, style, context) => {
|
|
562
|
-
const key = `${family}-${weight}-${style}`;
|
|
563
|
-
if (!loadedVariants.has(key)) {
|
|
564
|
-
if (style === 'italic') {
|
|
565
|
-
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.`);
|
|
566
|
-
}
|
|
567
|
-
throw new PretextPdfError('FONT_NOT_LOADED', `${context}: font variant '${key}' is not loaded. Add a matching FontSpec to doc.fonts.`);
|
|
568
|
-
}
|
|
569
|
-
};
|
|
570
|
-
// 1. defaultFont must be loadable
|
|
571
|
-
requireFamily(defaultFamily, `doc.defaultFont '${defaultFamily}'`);
|
|
572
|
-
// 2. header / footer fontFamily + fontWeight variant
|
|
573
|
-
for (const [spec, label] of [[doc.header, 'doc.header'], [doc.footer, 'doc.footer']]) {
|
|
574
|
-
if (!spec)
|
|
575
|
-
continue;
|
|
576
|
-
if (spec.fontFamily)
|
|
577
|
-
requireFamily(spec.fontFamily, `${label}.fontFamily`);
|
|
578
|
-
if ((spec.fontWeight ?? 400) === 700) {
|
|
579
|
-
const family = spec.fontFamily ?? defaultFamily;
|
|
580
|
-
requireVariant(family, 700, 'normal', `${label} fontWeight:700`);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
// 2b. watermark fontFamily (text watermark only)
|
|
584
|
-
if (doc.watermark?.text) {
|
|
585
|
-
if (doc.watermark.fontFamily)
|
|
586
|
-
requireFamily(doc.watermark.fontFamily, 'doc.watermark.fontFamily');
|
|
587
|
-
if ((doc.watermark.fontWeight ?? 400) === 700) {
|
|
588
|
-
const family = doc.watermark.fontFamily ?? defaultFamily;
|
|
589
|
-
requireVariant(family, 700, 'normal', 'doc.watermark fontWeight:700');
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
// 3. content elements
|
|
593
|
-
for (let i = 0; i < doc.content.length; i++) {
|
|
594
|
-
const el = doc.content[i];
|
|
595
|
-
const prefix = `content[${i}]`;
|
|
596
|
-
if (el.type === 'paragraph') {
|
|
597
|
-
if (el.fontFamily)
|
|
598
|
-
requireFamily(el.fontFamily, `${prefix} (paragraph).fontFamily`);
|
|
599
|
-
if ((el.fontWeight ?? 400) === 700) {
|
|
600
|
-
const family = el.fontFamily ?? defaultFamily;
|
|
601
|
-
requireVariant(family, 700, 'normal', `${prefix} (paragraph) fontWeight:700`);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
if (el.type === 'heading') {
|
|
605
|
-
if (el.fontFamily)
|
|
606
|
-
requireFamily(el.fontFamily, `${prefix} (heading).fontFamily`);
|
|
607
|
-
const family = el.fontFamily ?? defaultFamily;
|
|
608
|
-
const weight = el.fontWeight ?? 700;
|
|
609
|
-
requireVariant(family, weight, 'normal', `${prefix} (heading) fontWeight:${weight}`);
|
|
610
|
-
}
|
|
611
|
-
if (el.type === 'list') {
|
|
612
|
-
for (let ii = 0; ii < el.items.length; ii++) {
|
|
613
|
-
const item = el.items[ii];
|
|
614
|
-
if ((item.fontWeight ?? 400) === 700) {
|
|
615
|
-
requireVariant(defaultFamily, 700, 'normal', `${prefix} (list) items[${ii}] fontWeight:700`);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
if (el.type === 'table') {
|
|
620
|
-
for (let ri = 0; ri < el.rows.length; ri++) {
|
|
621
|
-
for (let ci = 0; ci < el.rows[ri].cells.length; ci++) {
|
|
622
|
-
const cell = el.rows[ri].cells[ci];
|
|
623
|
-
if (cell.fontFamily)
|
|
624
|
-
requireFamily(cell.fontFamily, `${prefix} (table) rows[${ri}].cells[${ci}].fontFamily`);
|
|
625
|
-
if ((cell.fontWeight ?? 400) === 700) {
|
|
626
|
-
const family = cell.fontFamily ?? defaultFamily;
|
|
627
|
-
requireVariant(family, 700, 'normal', `${prefix} (table) rows[${ri}].cells[${ci}] fontWeight:700`);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
if (el.type === 'rich-paragraph') {
|
|
633
|
-
for (let si = 0; si < el.spans.length; si++) {
|
|
634
|
-
const span = el.spans[si];
|
|
635
|
-
const spanFamily = span.fontFamily ?? defaultFamily;
|
|
636
|
-
const spanWeight = span.fontWeight ?? 400;
|
|
637
|
-
const spanStyle = span.fontStyle ?? 'normal';
|
|
638
|
-
if (span.fontFamily)
|
|
639
|
-
requireFamily(span.fontFamily, `${prefix} (rich-paragraph) spans[${si}].fontFamily`);
|
|
640
|
-
if (spanStyle === 'italic') {
|
|
641
|
-
requireVariant(spanFamily, spanWeight, 'italic', `${prefix} (rich-paragraph) spans[${si}]`);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
if (el.type === 'blockquote') {
|
|
646
|
-
if (el.fontFamily)
|
|
647
|
-
requireFamily(el.fontFamily, `${prefix} (blockquote).fontFamily`);
|
|
648
|
-
const family = el.fontFamily ?? defaultFamily;
|
|
649
|
-
const weight = el.fontWeight ?? 400;
|
|
650
|
-
const style = el.fontStyle ?? 'normal';
|
|
651
|
-
if (style === 'italic') {
|
|
652
|
-
requireVariant(family, weight, 'italic', `${prefix} (blockquote) fontStyle:italic`);
|
|
653
|
-
}
|
|
654
|
-
else if (weight === 700) {
|
|
655
|
-
requireVariant(family, 700, 'normal', `${prefix} (blockquote) fontWeight:700`);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
// code.fontFamily already validated against loadedFamilies in validateElement
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
function validateElement(el, index, loadedFamilies, strict, errors, options) {
|
|
662
|
-
const prefix = `content[${index}]`;
|
|
663
|
-
if (!el || typeof el !== 'object' || !('type' in el)) {
|
|
664
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix}: each element must have a 'type' field`);
|
|
665
|
-
}
|
|
666
|
-
// Strict: check element properties match allowed set for type
|
|
667
|
-
if (strict) {
|
|
668
|
-
const allowed = ALLOWED_PROPS[el.type];
|
|
669
|
-
if (allowed) {
|
|
670
|
-
assertUnknownProps(el, allowed, prefix, errors);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
switch (el.type) {
|
|
674
|
-
case 'paragraph': {
|
|
675
|
-
if (typeof el.text !== 'string') {
|
|
676
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'text' must be a string`);
|
|
677
|
-
}
|
|
678
|
-
// NEW: Validate dir field
|
|
679
|
-
if (el.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(el.dir)) {
|
|
680
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'dir' must be 'ltr', 'rtl', or 'auto'`);
|
|
681
|
-
}
|
|
682
|
-
if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
|
|
683
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'color' must be a 6-digit hex string like '#ff0000'. Got: '${el.color}'`);
|
|
684
|
-
}
|
|
685
|
-
if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize) || el.fontSize > 500)) {
|
|
686
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'fontSize' must be a positive finite number and <= 500`);
|
|
687
|
-
}
|
|
688
|
-
if (el.lineHeight !== undefined && typeof el.lineHeight === 'number') {
|
|
689
|
-
// Compare against explicit fontSize if set, or default (12pt) if not
|
|
690
|
-
const effectiveFontSize = el.fontSize ?? 12;
|
|
691
|
-
if (el.lineHeight < effectiveFontSize) {
|
|
692
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): lineHeight (${el.lineHeight}) is less than fontSize (${effectiveFontSize}). Lines would overlap. Set lineHeight >= fontSize.`);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
|
|
696
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'bgColor' must be a 6-digit hex string like '#f0f0f0'. Got: '${el.bgColor}'`);
|
|
697
|
-
}
|
|
698
|
-
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
699
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'spaceAfter' must be a non-negative finite number`);
|
|
700
|
-
}
|
|
701
|
-
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
702
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'spaceBefore' must be a non-negative finite number`);
|
|
703
|
-
}
|
|
704
|
-
if (el.columns !== undefined && (typeof el.columns !== 'number' || el.columns < 1 || !Number.isInteger(el.columns) || el.columns > 6)) {
|
|
705
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'columns' must be a positive integer between 1 and 6`);
|
|
706
|
-
}
|
|
707
|
-
if (el.columnGap !== undefined && (typeof el.columnGap !== 'number' || el.columnGap < 0 || !isFinite(el.columnGap))) {
|
|
708
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'columnGap' must be a non-negative finite number`);
|
|
709
|
-
}
|
|
710
|
-
if (el.align !== undefined && !['left', 'center', 'right', 'justify'].includes(el.align)) {
|
|
711
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'align' must be 'left', 'center', 'right', or 'justify'`);
|
|
712
|
-
}
|
|
713
|
-
if (el.url !== undefined && (typeof el.url !== 'string' || el.url.trim() === '')) {
|
|
714
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'url' must be a non-empty string if provided`);
|
|
715
|
-
}
|
|
716
|
-
if (el.url !== undefined && typeof el.url === 'string')
|
|
717
|
-
validateUrl(el.url, `${prefix} (paragraph) url`);
|
|
718
|
-
if (el.letterSpacing !== undefined && (typeof el.letterSpacing !== 'number' || el.letterSpacing < 0 || !isFinite(el.letterSpacing) || el.letterSpacing > 200)) {
|
|
719
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'letterSpacing' must be a non-negative finite number and <= 200`);
|
|
720
|
-
}
|
|
721
|
-
if (el.annotation) {
|
|
722
|
-
// Strict: validate annotation properties
|
|
723
|
-
if (strict) {
|
|
724
|
-
assertUnknownProps(el.annotation, ALLOWED_PROPS_SUB['annotation'], `${prefix}.annotation`, errors);
|
|
725
|
-
}
|
|
726
|
-
if (!el.annotation.contents || el.annotation.contents.trim() === '') {
|
|
727
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): annotation.contents is required and must be non-empty`);
|
|
728
|
-
}
|
|
729
|
-
if (el.annotation.contents.length > 5000) {
|
|
730
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): annotation.contents must be 5000 characters or fewer`);
|
|
731
|
-
}
|
|
732
|
-
if (el.annotation.color !== undefined && !/^#[0-9a-fA-F]{6}$/.test(el.annotation.color)) {
|
|
733
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): annotation.color must be a 6-digit hex color (e.g. "#FFFF00"). Got: "${el.annotation.color}"`);
|
|
734
|
-
}
|
|
735
|
-
if (el.annotation.author !== undefined && el.annotation.author.length > 100) {
|
|
736
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): annotation.author must be 100 characters or fewer`);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
break;
|
|
740
|
-
}
|
|
741
|
-
case 'heading': {
|
|
742
|
-
if (typeof el.text !== 'string') {
|
|
743
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'text' must be a string`);
|
|
744
|
-
}
|
|
745
|
-
if (el.text.trim() === '') {
|
|
746
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): heading text cannot be empty or whitespace-only`);
|
|
747
|
-
}
|
|
748
|
-
if (![1, 2, 3, 4].includes(el.level)) {
|
|
749
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'level' must be 1, 2, 3, or 4. Got: ${el.level}`);
|
|
750
|
-
}
|
|
751
|
-
if (el.fontWeight !== undefined && ![400, 700].includes(el.fontWeight)) {
|
|
752
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'fontWeight' must be 400 or 700`);
|
|
753
|
-
}
|
|
754
|
-
if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize) || el.fontSize > 500)) {
|
|
755
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'fontSize' must be a positive finite number and <= 500`);
|
|
756
|
-
}
|
|
757
|
-
if (el.lineHeight !== undefined && typeof el.lineHeight === 'number') {
|
|
758
|
-
const effectiveFontSize = el.fontSize ?? 12;
|
|
759
|
-
if (el.lineHeight < effectiveFontSize) {
|
|
760
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): lineHeight (${el.lineHeight}) is less than fontSize (${effectiveFontSize}). Lines would overlap.`);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
if (el.align !== undefined && !['left', 'center', 'right', 'justify'].includes(el.align)) {
|
|
764
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'align' must be 'left', 'center', 'right', or 'justify'`);
|
|
765
|
-
}
|
|
766
|
-
if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
|
|
767
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'color' must be a 6-digit hex string like '#ff0000'. Got: '${el.color}'`);
|
|
768
|
-
}
|
|
769
|
-
if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
|
|
770
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'bgColor' must be a 6-digit hex string`);
|
|
771
|
-
}
|
|
772
|
-
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
773
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'spaceBefore' must be a non-negative finite number`);
|
|
774
|
-
}
|
|
775
|
-
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
776
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'spaceAfter' must be a non-negative finite number`);
|
|
777
|
-
}
|
|
778
|
-
if (el.url !== undefined && (typeof el.url !== 'string' || el.url.trim() === '')) {
|
|
779
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'url' must be a non-empty string if provided`);
|
|
780
|
-
}
|
|
781
|
-
if (el.url !== undefined && typeof el.url === 'string')
|
|
782
|
-
validateUrl(el.url, `${prefix} (heading) url`);
|
|
783
|
-
if (el.anchor !== undefined && (typeof el.anchor !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(el.anchor))) {
|
|
784
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'anchor' must be alphanumeric with hyphens/underscores only. Got: '${el.anchor}'`);
|
|
785
|
-
}
|
|
786
|
-
if (el.letterSpacing !== undefined && (typeof el.letterSpacing !== 'number' || el.letterSpacing < 0 || !isFinite(el.letterSpacing) || el.letterSpacing > 200)) {
|
|
787
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'letterSpacing' must be a non-negative finite number and <= 200`);
|
|
788
|
-
}
|
|
789
|
-
if (el.annotation) {
|
|
790
|
-
if (!el.annotation.contents || el.annotation.contents.trim() === '') {
|
|
791
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): annotation.contents is required and must be non-empty`);
|
|
792
|
-
}
|
|
793
|
-
if (el.annotation.contents.length > 5000) {
|
|
794
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): annotation.contents must be 5000 characters or fewer`);
|
|
795
|
-
}
|
|
796
|
-
if (el.annotation.color !== undefined && !/^#[0-9a-fA-F]{6}$/.test(el.annotation.color)) {
|
|
797
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): annotation.color must be a 6-digit hex color (e.g. "#FFFF00"). Got: "${el.annotation.color}"`);
|
|
798
|
-
}
|
|
799
|
-
if (el.annotation.author !== undefined && el.annotation.author.length > 100) {
|
|
800
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): annotation.author must be 100 characters or fewer`);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
break;
|
|
804
|
-
}
|
|
805
|
-
case 'spacer': {
|
|
806
|
-
if (typeof el.height !== 'number' || el.height < 0 || el.height > 14400 || !isFinite(el.height)) {
|
|
807
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (spacer): 'height' must be a non-negative finite number and <= 14400pt (200 inches)`);
|
|
808
|
-
}
|
|
809
|
-
break;
|
|
810
|
-
}
|
|
811
|
-
case 'table': {
|
|
812
|
-
if (!Array.isArray(el.columns) || el.columns.length === 0) {
|
|
813
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'columns' must be a non-empty array`);
|
|
814
|
-
}
|
|
815
|
-
if (!Array.isArray(el.rows) || el.rows.length === 0) {
|
|
816
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'rows' must be a non-empty array`);
|
|
817
|
-
}
|
|
818
|
-
const colCount = el.columns.length;
|
|
819
|
-
for (let ci = 0; ci < el.columns.length; ci++) {
|
|
820
|
-
const col = el.columns[ci];
|
|
821
|
-
// Strict: validate column def properties
|
|
822
|
-
if (strict) {
|
|
823
|
-
assertUnknownProps(col, ALLOWED_PROPS_SUB['column-def'], `${prefix} (table).columns[${ci}]`, errors);
|
|
824
|
-
}
|
|
825
|
-
if (typeof col.width === 'number') {
|
|
826
|
-
if (col.width <= 0 || !isFinite(col.width)) {
|
|
827
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].width must be a positive number. Got: ${col.width}`);
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
else if (typeof col.width === 'string') {
|
|
831
|
-
if (col.width !== 'auto' && !STAR_WIDTH_REGEX.test(col.width)) {
|
|
832
|
-
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}'`);
|
|
833
|
-
}
|
|
834
|
-
if (col.width !== 'auto') {
|
|
835
|
-
const multiplier = parseFloat(col.width);
|
|
836
|
-
if (!isNaN(multiplier) && multiplier > 1000) {
|
|
837
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].width multiplier ${multiplier} exceeds maximum 1000`);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
else {
|
|
842
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].width must be a number or string like '2*' or 'auto'`);
|
|
843
|
-
}
|
|
844
|
-
if (col.align !== undefined && !['left', 'center', 'right'].includes(col.align)) {
|
|
845
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].align must be 'left', 'center', or 'right'`);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
// Validate header row count
|
|
849
|
-
const headerRowCount = el.headerRows !== undefined
|
|
850
|
-
? el.headerRows
|
|
851
|
-
: el.rows.filter(r => r.isHeader).length;
|
|
852
|
-
if (headerRowCount > el.rows.length) {
|
|
853
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): headerRows (${headerRowCount}) exceeds total row count (${el.rows.length})`);
|
|
854
|
-
}
|
|
855
|
-
// Build span occupancy grid for colspan validation (rowspan cells occupy future rows)
|
|
856
|
-
const spanOccupied = new Set();
|
|
857
|
-
for (let ri = 0; ri < el.rows.length; ri++) {
|
|
858
|
-
const row = el.rows[ri];
|
|
859
|
-
let ci = 0;
|
|
860
|
-
for (const cell of row.cells) {
|
|
861
|
-
while (spanOccupied.has(`${ri},${ci}`))
|
|
862
|
-
ci++;
|
|
863
|
-
const cs = cell.colspan ?? 1;
|
|
864
|
-
const rs = cell.rowspan ?? 1;
|
|
865
|
-
for (let r2 = ri + 1; r2 < ri + rs; r2++) {
|
|
866
|
-
for (let c2 = ci; c2 < ci + cs; c2++) {
|
|
867
|
-
spanOccupied.add(`${r2},${c2}`);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
ci += cs;
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
for (let ri = 0; ri < el.rows.length; ri++) {
|
|
874
|
-
const row = el.rows[ri];
|
|
875
|
-
if (!Array.isArray(row.cells)) {
|
|
876
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells must be an array`);
|
|
877
|
-
}
|
|
878
|
-
// Count how many columns in this row are occupied by rowspan cells from above
|
|
879
|
-
let occupiedCols = 0;
|
|
880
|
-
for (let ci = 0; ci < colCount; ci++) {
|
|
881
|
-
if (spanOccupied.has(`${ri},${ci}`))
|
|
882
|
-
occupiedCols++;
|
|
883
|
-
}
|
|
884
|
-
// Validate colspan sum equals colCount minus occupied columns
|
|
885
|
-
let colspanSum = 0;
|
|
886
|
-
for (let cellI = 0; cellI < row.cells.length; cellI++) {
|
|
887
|
-
const cell = row.cells[cellI];
|
|
888
|
-
const cs = cell.colspan ?? 1;
|
|
889
|
-
if (typeof cs !== 'number' || cs < 1 || !Number.isInteger(cs)) {
|
|
890
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].colspan must be a positive integer`);
|
|
891
|
-
}
|
|
892
|
-
colspanSum += cs;
|
|
893
|
-
const rs = cell.rowspan ?? 1;
|
|
894
|
-
if (typeof rs !== 'number' || rs < 1 || !Number.isInteger(rs)) {
|
|
895
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].rowspan must be a positive integer`);
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
if (colspanSum !== colCount - occupiedCols) {
|
|
899
|
-
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.`);
|
|
900
|
-
}
|
|
901
|
-
// Strict: validate row and column defs
|
|
902
|
-
if (strict) {
|
|
903
|
-
assertUnknownProps(row, ALLOWED_PROPS_SUB['table-row'], `${prefix}.rows[${ri}]`, errors);
|
|
904
|
-
}
|
|
905
|
-
for (let cellI = 0; cellI < row.cells.length; cellI++) {
|
|
906
|
-
const cell = row.cells[cellI];
|
|
907
|
-
// Strict: validate cell properties
|
|
908
|
-
if (strict) {
|
|
909
|
-
assertUnknownProps(cell, ALLOWED_PROPS_SUB['table-cell'], `${prefix}.rows[${ri}].cells[${cellI}]`, errors);
|
|
910
|
-
}
|
|
911
|
-
if (typeof cell.text !== 'string') {
|
|
912
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].text must be a string`);
|
|
913
|
-
}
|
|
914
|
-
if (cell.fontFamily !== undefined && (typeof cell.fontFamily !== 'string' || cell.fontFamily.trim() === '')) {
|
|
915
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].fontFamily must be a non-empty string`);
|
|
916
|
-
}
|
|
917
|
-
if (cell.fontWeight !== undefined && ![400, 700].includes(cell.fontWeight)) {
|
|
918
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].fontWeight must be 400 or 700`);
|
|
919
|
-
}
|
|
920
|
-
if (cell.fontSize !== undefined && (typeof cell.fontSize !== 'number' || cell.fontSize <= 0 || !isFinite(cell.fontSize))) {
|
|
921
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].fontSize must be a positive finite number`);
|
|
922
|
-
}
|
|
923
|
-
if (cell.color !== undefined && !HEX_COLOR_REGEX.test(cell.color)) {
|
|
924
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].color must be a 6-digit hex string`);
|
|
925
|
-
}
|
|
926
|
-
if (cell.bgColor !== undefined && !HEX_COLOR_REGEX.test(cell.bgColor)) {
|
|
927
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].bgColor must be a 6-digit hex string`);
|
|
928
|
-
}
|
|
929
|
-
if (cell.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(cell.dir)) {
|
|
930
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].dir must be 'ltr', 'rtl', or 'auto'`);
|
|
931
|
-
}
|
|
932
|
-
if (cell.align !== undefined && !['left', 'center', 'right'].includes(cell.align)) {
|
|
933
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].align must be 'left', 'center', or 'right'`);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
if (el.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(el.dir)) {
|
|
938
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'dir' must be 'ltr', 'rtl', or 'auto'`);
|
|
939
|
-
}
|
|
940
|
-
if (el.borderColor !== undefined && !HEX_COLOR_REGEX.test(el.borderColor)) {
|
|
941
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'borderColor' must be a 6-digit hex string`);
|
|
942
|
-
}
|
|
943
|
-
if (el.headerBgColor !== undefined && !HEX_COLOR_REGEX.test(el.headerBgColor)) {
|
|
944
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'headerBgColor' must be a 6-digit hex string`);
|
|
945
|
-
}
|
|
946
|
-
if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
|
|
947
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'fontSize' must be a positive finite number`);
|
|
948
|
-
}
|
|
949
|
-
if (el.borderWidth !== undefined && (typeof el.borderWidth !== 'number' || el.borderWidth < 0 || el.borderWidth > 50)) {
|
|
950
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'borderWidth' must be a non-negative number <= 50`);
|
|
951
|
-
}
|
|
952
|
-
if (el.cellPaddingH !== undefined && (typeof el.cellPaddingH !== 'number' || el.cellPaddingH < 0 || el.cellPaddingH > 200)) {
|
|
953
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'cellPaddingH' must be a non-negative number <= 200`);
|
|
954
|
-
}
|
|
955
|
-
if (el.cellPaddingV !== undefined && (typeof el.cellPaddingV !== 'number' || el.cellPaddingV < 0 || el.cellPaddingV > 200)) {
|
|
956
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'cellPaddingV' must be a non-negative number <= 200`);
|
|
957
|
-
}
|
|
958
|
-
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
959
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'spaceAfter' must be a non-negative finite number`);
|
|
960
|
-
}
|
|
961
|
-
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
962
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'spaceBefore' must be a non-negative finite number`);
|
|
963
|
-
}
|
|
964
|
-
break;
|
|
965
|
-
}
|
|
966
|
-
case 'image': {
|
|
967
|
-
if (!el.src || (typeof el.src !== 'string' && !(el.src instanceof Uint8Array))) {
|
|
968
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'src' must be a non-empty string path or Uint8Array`);
|
|
969
|
-
}
|
|
970
|
-
if (typeof el.src === 'string' && (el.src.startsWith('\\\\') || el.src.startsWith('//'))) {
|
|
971
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'src' must not be a UNC/network path`);
|
|
972
|
-
}
|
|
973
|
-
const fmt = el.format ?? 'auto';
|
|
974
|
-
if (fmt !== 'png' && fmt !== 'jpg' && fmt !== 'auto') {
|
|
975
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'format' must be 'png', 'jpg', or 'auto'. Got: '${String(el.format)}'`);
|
|
976
|
-
}
|
|
977
|
-
if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
|
|
978
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'width' must be a positive finite number`);
|
|
979
|
-
}
|
|
980
|
-
if (el.height !== undefined && (typeof el.height !== 'number' || el.height <= 0 || !isFinite(el.height))) {
|
|
981
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'height' must be a positive finite number`);
|
|
982
|
-
}
|
|
983
|
-
if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
|
|
984
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'align' must be 'left', 'center', or 'right'`);
|
|
985
|
-
}
|
|
986
|
-
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
987
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'spaceAfter' must be a non-negative finite number`);
|
|
988
|
-
}
|
|
989
|
-
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
990
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'spaceBefore' must be a non-negative finite number`);
|
|
991
|
-
}
|
|
992
|
-
// Float validation
|
|
993
|
-
if (el.float !== undefined && el.float !== 'left' && el.float !== 'right') {
|
|
994
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'float' must be 'left' or 'right'`);
|
|
995
|
-
}
|
|
996
|
-
if (el.float !== undefined && (!el.floatText || el.floatText.trim() === '') && (!el.floatSpans || el.floatSpans.length === 0)) {
|
|
997
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatText' or 'floatSpans' is required when 'float' is set`);
|
|
998
|
-
}
|
|
999
|
-
if (el.floatText !== undefined && el.floatSpans !== undefined) {
|
|
1000
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatText' and 'floatSpans' are mutually exclusive — use one or the other`);
|
|
1001
|
-
}
|
|
1002
|
-
if (el.floatText !== undefined && el.float === undefined) {
|
|
1003
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatText' has no effect without 'float'`);
|
|
1004
|
-
}
|
|
1005
|
-
if (el.floatSpans !== undefined && el.float === undefined) {
|
|
1006
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatSpans' has no effect without 'float'`);
|
|
1007
|
-
}
|
|
1008
|
-
if (el.floatWidth !== undefined && (typeof el.floatWidth !== 'number' || el.floatWidth <= 0 || !isFinite(el.floatWidth))) {
|
|
1009
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatWidth' must be a positive finite number`);
|
|
1010
|
-
}
|
|
1011
|
-
if (el.floatGap !== undefined && (typeof el.floatGap !== 'number' || el.floatGap < 0 || !isFinite(el.floatGap))) {
|
|
1012
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatGap' must be a non-negative number`);
|
|
1013
|
-
}
|
|
1014
|
-
if (el.floatFontSize !== undefined && (typeof el.floatFontSize !== 'number' || el.floatFontSize <= 0)) {
|
|
1015
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatFontSize' must be a positive number`);
|
|
1016
|
-
}
|
|
1017
|
-
break;
|
|
1018
|
-
}
|
|
1019
|
-
case 'svg': {
|
|
1020
|
-
const hasSvg = typeof el.svg === 'string' && el.svg.trim().length > 0;
|
|
1021
|
-
const hasSrc = typeof el.src === 'string' && el.src.trim().length > 0;
|
|
1022
|
-
if (!hasSvg && !hasSrc) {
|
|
1023
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): either 'svg' (inline markup) or 'src' (file path / https:// URL) is required`);
|
|
1024
|
-
}
|
|
1025
|
-
if (hasSvg && !el.svg.trim().startsWith('<')) {
|
|
1026
|
-
throw new PretextPdfError('SVG_INVALID_MARKUP', `${prefix} (svg): 'svg' must be valid SVG markup (must start with '<')`);
|
|
1027
|
-
}
|
|
1028
|
-
if (hasSrc) {
|
|
1029
|
-
const src = el.src;
|
|
1030
|
-
const isUNC = src.startsWith('\\\\') || src.startsWith('//');
|
|
1031
|
-
if (isUNC) {
|
|
1032
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'src' must not be a UNC/network path`);
|
|
1033
|
-
}
|
|
1034
|
-
if (!src.startsWith('/') && !src.startsWith('https://') && !/^[A-Za-z]:[/\\]/.test(src)) {
|
|
1035
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'src' must be an absolute file path or an https:// URL`);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
|
|
1039
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'width' must be a positive finite number`);
|
|
1040
|
-
}
|
|
1041
|
-
if (el.height !== undefined && (typeof el.height !== 'number' || el.height <= 0 || !isFinite(el.height))) {
|
|
1042
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'height' must be a positive finite number`);
|
|
1043
|
-
}
|
|
1044
|
-
if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
|
|
1045
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'align' must be 'left', 'center', or 'right'`);
|
|
1046
|
-
}
|
|
1047
|
-
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
1048
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'spaceAfter' must be a non-negative finite number`);
|
|
1049
|
-
}
|
|
1050
|
-
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
1051
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'spaceBefore' must be a non-negative finite number`);
|
|
1052
|
-
}
|
|
1053
|
-
break;
|
|
1054
|
-
}
|
|
1055
|
-
case 'qr-code': {
|
|
1056
|
-
if (typeof el.data !== 'string' || el.data.trim().length === 0) {
|
|
1057
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'data' must be a non-empty string`);
|
|
1058
|
-
}
|
|
1059
|
-
if (el.data.length > 2953) {
|
|
1060
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'data' exceeds maximum QR capacity of 2953 characters (got ${el.data.length})`);
|
|
1061
|
-
}
|
|
1062
|
-
if (el.errorCorrectionLevel !== undefined && !['L', 'M', 'Q', 'H'].includes(el.errorCorrectionLevel)) {
|
|
1063
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'errorCorrectionLevel' must be 'L', 'M', 'Q', or 'H'`);
|
|
1064
|
-
}
|
|
1065
|
-
if (el.size !== undefined && (typeof el.size !== 'number' || el.size <= 0 || !isFinite(el.size))) {
|
|
1066
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'size' must be a positive finite number`);
|
|
1067
|
-
}
|
|
1068
|
-
if (el.foreground !== undefined && !HEX_COLOR_REGEX.test(el.foreground)) {
|
|
1069
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'foreground' must be a 6-digit hex string like '#000000'`);
|
|
1070
|
-
}
|
|
1071
|
-
if (el.background !== undefined && !HEX_COLOR_REGEX.test(el.background)) {
|
|
1072
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'background' must be a 6-digit hex string like '#ffffff'`);
|
|
1073
|
-
}
|
|
1074
|
-
if (el.margin !== undefined && (typeof el.margin !== 'number' || el.margin < 0 || !isFinite(el.margin))) {
|
|
1075
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'margin' must be a non-negative finite number`);
|
|
1076
|
-
}
|
|
1077
|
-
if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
|
|
1078
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'align' must be 'left', 'center', or 'right'`);
|
|
1079
|
-
}
|
|
1080
|
-
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
1081
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'spaceAfter' must be a non-negative finite number`);
|
|
1082
|
-
}
|
|
1083
|
-
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
1084
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'spaceBefore' must be a non-negative finite number`);
|
|
1085
|
-
}
|
|
1086
|
-
break;
|
|
1087
|
-
}
|
|
1088
|
-
case 'barcode': {
|
|
1089
|
-
if (typeof el.symbology !== 'string' || el.symbology.trim().length === 0) {
|
|
1090
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'symbology' must be a non-empty string (e.g. 'ean13', 'code128')`);
|
|
1091
|
-
}
|
|
1092
|
-
if (typeof el.data !== 'string' || el.data.trim().length === 0) {
|
|
1093
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'data' must be a non-empty string`);
|
|
1094
|
-
}
|
|
1095
|
-
if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
|
|
1096
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'width' must be a positive finite number`);
|
|
1097
|
-
}
|
|
1098
|
-
if (el.height !== undefined && (typeof el.height !== 'number' || el.height <= 0 || !isFinite(el.height))) {
|
|
1099
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'height' must be a positive finite number`);
|
|
1100
|
-
}
|
|
1101
|
-
if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
|
|
1102
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'align' must be 'left', 'center', or 'right'`);
|
|
1103
|
-
}
|
|
1104
|
-
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
1105
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'spaceAfter' must be a non-negative finite number`);
|
|
1106
|
-
}
|
|
1107
|
-
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
1108
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'spaceBefore' must be a non-negative finite number`);
|
|
1109
|
-
}
|
|
1110
|
-
break;
|
|
1111
|
-
}
|
|
1112
|
-
case 'chart': {
|
|
1113
|
-
if (el.spec === null || typeof el.spec !== 'object' || Array.isArray(el.spec)) {
|
|
1114
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'spec' must be a plain vega-lite specification object`);
|
|
1115
|
-
}
|
|
1116
|
-
if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
|
|
1117
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'width' must be a positive finite number`);
|
|
1118
|
-
}
|
|
1119
|
-
if (el.height !== undefined && (typeof el.height !== 'number' || el.height <= 0 || !isFinite(el.height))) {
|
|
1120
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'height' must be a positive finite number`);
|
|
1121
|
-
}
|
|
1122
|
-
if (el.caption !== undefined && typeof el.caption !== 'string') {
|
|
1123
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'caption' must be a string`);
|
|
1124
|
-
}
|
|
1125
|
-
if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
|
|
1126
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'align' must be 'left', 'center', or 'right'`);
|
|
1127
|
-
}
|
|
1128
|
-
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
1129
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'spaceAfter' must be a non-negative finite number`);
|
|
1130
|
-
}
|
|
1131
|
-
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
1132
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'spaceBefore' must be a non-negative finite number`);
|
|
1133
|
-
}
|
|
1134
|
-
break;
|
|
1135
|
-
}
|
|
1136
|
-
case 'list': {
|
|
1137
|
-
if (el.style !== 'ordered' && el.style !== 'unordered') {
|
|
1138
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'style' must be 'ordered' or 'unordered'`);
|
|
1139
|
-
}
|
|
1140
|
-
if (!Array.isArray(el.items) || el.items.length === 0) {
|
|
1141
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'items' must be a non-empty array`);
|
|
1142
|
-
}
|
|
1143
|
-
for (let ii = 0; ii < el.items.length; ii++) {
|
|
1144
|
-
const item = el.items[ii];
|
|
1145
|
-
// Strict: validate list item properties
|
|
1146
|
-
if (strict) {
|
|
1147
|
-
assertUnknownProps(item, ALLOWED_PROPS_SUB['list-item'], `${prefix}.items[${ii}]`, errors);
|
|
1148
|
-
}
|
|
1149
|
-
if (typeof item.text !== 'string' || item.text.trim() === '') {
|
|
1150
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].text must be a non-empty string`);
|
|
1151
|
-
}
|
|
1152
|
-
// NEW: Validate dir field
|
|
1153
|
-
if (item.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(item.dir)) {
|
|
1154
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].dir must be 'ltr', 'rtl', or 'auto'`);
|
|
1155
|
-
}
|
|
1156
|
-
if (item.fontWeight !== undefined && ![400, 700].includes(item.fontWeight)) {
|
|
1157
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].fontWeight must be 400 or 700`);
|
|
1158
|
-
}
|
|
1159
|
-
// Validate nested items (1 level deep)
|
|
1160
|
-
if (item.items) {
|
|
1161
|
-
if (!Array.isArray(item.items) || item.items.length === 0) {
|
|
1162
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].items must be a non-empty array if provided`);
|
|
1163
|
-
}
|
|
1164
|
-
for (let ni = 0; ni < item.items.length; ni++) {
|
|
1165
|
-
const nested = item.items[ni];
|
|
1166
|
-
// Strict: validate nested list item properties
|
|
1167
|
-
if (strict) {
|
|
1168
|
-
assertUnknownProps(nested, ALLOWED_PROPS_SUB['list-item'], `${prefix}.items[${ii}].items[${ni}]`, errors);
|
|
1169
|
-
}
|
|
1170
|
-
if (typeof nested.text !== 'string' || nested.text.trim() === '') {
|
|
1171
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].items[${ni}].text must be a non-empty string`);
|
|
1172
|
-
}
|
|
1173
|
-
if (nested.items !== undefined) {
|
|
1174
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].items[${ni}].items is not allowed — maximum nesting depth is 2 levels`);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
|
|
1180
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'color' must be a 6-digit hex string like '#ff0000'. Got: '${el.color}'`);
|
|
1181
|
-
}
|
|
1182
|
-
if (el.nestedNumberingStyle !== undefined && !['continue', 'restart'].includes(el.nestedNumberingStyle)) {
|
|
1183
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'nestedNumberingStyle' must be 'continue' or 'restart'. Got: '${el.nestedNumberingStyle}'`);
|
|
1184
|
-
}
|
|
1185
|
-
if (el.indent !== undefined && (typeof el.indent !== 'number' || el.indent < 0)) {
|
|
1186
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'indent' must be a non-negative number`);
|
|
1187
|
-
}
|
|
1188
|
-
if (el.markerWidth !== undefined && (typeof el.markerWidth !== 'number' || el.markerWidth <= 0)) {
|
|
1189
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'markerWidth' must be a positive number`);
|
|
1190
|
-
}
|
|
1191
|
-
if (el.marker !== undefined && (typeof el.marker !== 'string' || el.marker.trim() === '')) {
|
|
1192
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'marker' must be a non-empty string`);
|
|
1193
|
-
}
|
|
1194
|
-
break;
|
|
1195
|
-
}
|
|
1196
|
-
case 'hr': {
|
|
1197
|
-
if (el.thickness !== undefined && (typeof el.thickness !== 'number' || el.thickness < 0 || !isFinite(el.thickness))) {
|
|
1198
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (hr): 'thickness' must be a non-negative finite number`);
|
|
1199
|
-
}
|
|
1200
|
-
if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
|
|
1201
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (hr): 'color' must be a 6-digit hex string`);
|
|
1202
|
-
}
|
|
1203
|
-
if (el.spaceAbove !== undefined && (typeof el.spaceAbove !== 'number' || el.spaceAbove < 0)) {
|
|
1204
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (hr): 'spaceAbove' must be a non-negative number`);
|
|
1205
|
-
}
|
|
1206
|
-
if (el.spaceBelow !== undefined && (typeof el.spaceBelow !== 'number' || el.spaceBelow < 0)) {
|
|
1207
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (hr): 'spaceBelow' must be a non-negative number`);
|
|
1208
|
-
}
|
|
1209
|
-
break;
|
|
1210
|
-
}
|
|
1211
|
-
case 'page-break': {
|
|
1212
|
-
// No fields to validate
|
|
1213
|
-
break;
|
|
1214
|
-
}
|
|
1215
|
-
case 'rich-paragraph': {
|
|
1216
|
-
if (!Array.isArray(el.spans) || el.spans.length === 0) {
|
|
1217
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'spans' must be a non-empty array`);
|
|
1218
|
-
}
|
|
1219
|
-
for (let si = 0; si < el.spans.length; si++) {
|
|
1220
|
-
const span = el.spans[si];
|
|
1221
|
-
// Strict: validate span properties
|
|
1222
|
-
if (strict) {
|
|
1223
|
-
assertUnknownProps(span, ALLOWED_PROPS_SUB['inline-span'], `${prefix}.spans[${si}]`, errors);
|
|
1224
|
-
}
|
|
1225
|
-
if (typeof span.text !== 'string') {
|
|
1226
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].text must be a string`);
|
|
1227
|
-
}
|
|
1228
|
-
if (span.text === '') {
|
|
1229
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].text cannot be an empty string. Use ' ' for a space between styled runs.`);
|
|
1230
|
-
}
|
|
1231
|
-
if (span.color !== undefined && !HEX_COLOR_REGEX.test(span.color)) {
|
|
1232
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].color must be a 6-digit hex string`);
|
|
1233
|
-
}
|
|
1234
|
-
if (span.fontWeight !== undefined && ![400, 700].includes(span.fontWeight)) {
|
|
1235
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].fontWeight must be 400 or 700`);
|
|
1236
|
-
}
|
|
1237
|
-
if (span.fontStyle !== undefined && !['normal', 'italic'].includes(span.fontStyle)) {
|
|
1238
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].fontStyle must be 'normal' or 'italic'`);
|
|
1239
|
-
}
|
|
1240
|
-
if (span.fontSize !== undefined && (typeof span.fontSize !== 'number' || span.fontSize <= 0 || !isFinite(span.fontSize))) {
|
|
1241
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].fontSize must be a positive finite number if provided`);
|
|
1242
|
-
}
|
|
1243
|
-
if (span.url !== undefined && (typeof span.url !== 'string' || span.url.trim() === '')) {
|
|
1244
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].url must be a non-empty string if provided`);
|
|
1245
|
-
}
|
|
1246
|
-
if (span.url !== undefined && typeof span.url === 'string')
|
|
1247
|
-
validateUrl(span.url, `${prefix} (rich-paragraph) spans[${si}].url`);
|
|
1248
|
-
if (span.href !== undefined && (typeof span.href !== 'string' || span.href.trim() === '')) {
|
|
1249
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].href must be a non-empty string if provided`);
|
|
1250
|
-
}
|
|
1251
|
-
if (span.href !== undefined && typeof span.href === 'string')
|
|
1252
|
-
validateUrl(span.href, `${prefix} (rich-paragraph) spans[${si}].href`);
|
|
1253
|
-
if (span.url !== undefined && span.href !== undefined) {
|
|
1254
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}] cannot have both 'url' and 'href' — use one or the other`);
|
|
1255
|
-
}
|
|
1256
|
-
if (span.verticalAlign !== undefined && span.verticalAlign !== 'superscript' && span.verticalAlign !== 'subscript') {
|
|
1257
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].verticalAlign must be "superscript" or "subscript"`);
|
|
1258
|
-
}
|
|
1259
|
-
if (span.letterSpacing !== undefined && (typeof span.letterSpacing !== 'number' || span.letterSpacing < 0 || !isFinite(span.letterSpacing) || span.letterSpacing > 200)) {
|
|
1260
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].letterSpacing must be a non-negative finite number and <= 200`);
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
if (el.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(el.dir)) {
|
|
1264
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'dir' must be 'ltr', 'rtl', or 'auto'`);
|
|
1265
|
-
}
|
|
1266
|
-
if (el.letterSpacing !== undefined && (typeof el.letterSpacing !== 'number' || el.letterSpacing < 0 || !isFinite(el.letterSpacing) || el.letterSpacing > 200)) {
|
|
1267
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'letterSpacing' must be a non-negative finite number and <= 200`);
|
|
1268
|
-
}
|
|
1269
|
-
if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
|
|
1270
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'fontSize' must be a positive finite number`);
|
|
1271
|
-
}
|
|
1272
|
-
if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
|
|
1273
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'bgColor' must be a 6-digit hex string`);
|
|
1274
|
-
}
|
|
1275
|
-
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
1276
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'spaceAfter' must be a non-negative finite number`);
|
|
1277
|
-
}
|
|
1278
|
-
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
1279
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'spaceBefore' must be a non-negative finite number`);
|
|
1280
|
-
}
|
|
1281
|
-
if (el.columns !== undefined && (typeof el.columns !== 'number' || el.columns < 1 || !Number.isInteger(el.columns) || el.columns > 6)) {
|
|
1282
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'columns' must be a positive integer between 1 and 6`);
|
|
1283
|
-
}
|
|
1284
|
-
if (el.columnGap !== undefined && (typeof el.columnGap !== 'number' || el.columnGap < 0 || !isFinite(el.columnGap))) {
|
|
1285
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'columnGap' must be a non-negative finite number`);
|
|
1286
|
-
}
|
|
1287
|
-
if (el.align !== undefined && !['left', 'center', 'right', 'justify'].includes(el.align)) {
|
|
1288
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'align' must be 'left', 'center', 'right', or 'justify'`);
|
|
1289
|
-
}
|
|
1290
|
-
break;
|
|
1291
|
-
}
|
|
1292
|
-
case 'code': {
|
|
1293
|
-
if (typeof el.text !== 'string') {
|
|
1294
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'text' must be a string`);
|
|
1295
|
-
}
|
|
1296
|
-
if (el.text.trim() === '') {
|
|
1297
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'text' must be a non-empty string`);
|
|
1298
|
-
}
|
|
1299
|
-
if (!el.fontFamily || typeof el.fontFamily !== 'string') {
|
|
1300
|
-
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').`);
|
|
1301
|
-
}
|
|
1302
|
-
if (!loadedFamilies.has(el.fontFamily)) {
|
|
1303
|
-
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.`);
|
|
1304
|
-
}
|
|
1305
|
-
if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
|
|
1306
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'fontSize' must be a positive finite number`);
|
|
1307
|
-
}
|
|
1308
|
-
if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
|
|
1309
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'bgColor' must be a 6-digit hex string`);
|
|
1310
|
-
}
|
|
1311
|
-
if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
|
|
1312
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'color' must be a 6-digit hex string`);
|
|
1313
|
-
}
|
|
1314
|
-
if (el.padding !== undefined && (typeof el.padding !== 'number' || el.padding < 0 || !isFinite(el.padding))) {
|
|
1315
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'padding' must be a non-negative finite number`);
|
|
1316
|
-
}
|
|
1317
|
-
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
1318
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'spaceAfter' must be a non-negative finite number`);
|
|
1319
|
-
}
|
|
1320
|
-
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
1321
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'spaceBefore' must be a non-negative finite number`);
|
|
1322
|
-
}
|
|
1323
|
-
if (el.language !== undefined && typeof el.language !== 'string') {
|
|
1324
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'language' must be a string`);
|
|
1325
|
-
}
|
|
1326
|
-
if (el.highlightTheme !== undefined) {
|
|
1327
|
-
if (typeof el.highlightTheme !== 'object' || el.highlightTheme === null) {
|
|
1328
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'highlightTheme' must be an object`);
|
|
1329
|
-
}
|
|
1330
|
-
for (const [k, v] of Object.entries(el.highlightTheme)) {
|
|
1331
|
-
if (v !== undefined && !HEX_COLOR_REGEX.test(v)) {
|
|
1332
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): highlightTheme.${k} must be a 6-digit hex string`);
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
break;
|
|
1337
|
-
}
|
|
1338
|
-
case 'blockquote': {
|
|
1339
|
-
if (typeof el.text !== 'string') {
|
|
1340
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'text' must be a string`);
|
|
1341
|
-
}
|
|
1342
|
-
if (el.text.trim() === '') {
|
|
1343
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'text' must be a non-empty string`);
|
|
1344
|
-
}
|
|
1345
|
-
if (el.borderColor !== undefined && !HEX_COLOR_REGEX.test(el.borderColor)) {
|
|
1346
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'borderColor' must be a 6-digit hex string`);
|
|
1347
|
-
}
|
|
1348
|
-
if (el.borderWidth !== undefined && (typeof el.borderWidth !== 'number' || el.borderWidth < 0 || !isFinite(el.borderWidth))) {
|
|
1349
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'borderWidth' must be a non-negative finite number`);
|
|
1350
|
-
}
|
|
1351
|
-
if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
|
|
1352
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'bgColor' must be a 6-digit hex string`);
|
|
1353
|
-
}
|
|
1354
|
-
if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
|
|
1355
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'color' must be a 6-digit hex string`);
|
|
1356
|
-
}
|
|
1357
|
-
if (el.fontWeight !== undefined && ![400, 700].includes(el.fontWeight)) {
|
|
1358
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'fontWeight' must be 400 or 700`);
|
|
1359
|
-
}
|
|
1360
|
-
if (el.fontStyle !== undefined && !['normal', 'italic'].includes(el.fontStyle)) {
|
|
1361
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'fontStyle' must be 'normal' or 'italic'`);
|
|
1362
|
-
}
|
|
1363
|
-
if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
|
|
1364
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'fontSize' must be a positive finite number`);
|
|
1365
|
-
}
|
|
1366
|
-
if (el.lineHeight !== undefined && typeof el.lineHeight === 'number') {
|
|
1367
|
-
const effectiveFontSize = el.fontSize ?? 12;
|
|
1368
|
-
if (el.lineHeight < effectiveFontSize) {
|
|
1369
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): lineHeight (${el.lineHeight}) is less than fontSize (${effectiveFontSize}). Lines would overlap.`);
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
if (el.padding !== undefined && (typeof el.padding !== 'number' || el.padding < 0 || !isFinite(el.padding))) {
|
|
1373
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'padding' must be a non-negative finite number`);
|
|
1374
|
-
}
|
|
1375
|
-
if (el.paddingH !== undefined && (typeof el.paddingH !== 'number' || el.paddingH < 0 || !isFinite(el.paddingH))) {
|
|
1376
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'paddingH' must be a non-negative finite number`);
|
|
1377
|
-
}
|
|
1378
|
-
if (el.paddingV !== undefined && (typeof el.paddingV !== 'number' || el.paddingV < 0 || !isFinite(el.paddingV))) {
|
|
1379
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'paddingV' must be a non-negative finite number`);
|
|
1380
|
-
}
|
|
1381
|
-
if (el.align !== undefined && !['left', 'center', 'right', 'justify'].includes(el.align)) {
|
|
1382
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'align' must be 'left', 'center', 'right', or 'justify'`);
|
|
1383
|
-
}
|
|
1384
|
-
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
1385
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'spaceBefore' must be a non-negative finite number`);
|
|
1386
|
-
}
|
|
1387
|
-
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
1388
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'spaceAfter' must be a non-negative finite number`);
|
|
1389
|
-
}
|
|
1390
|
-
break;
|
|
1391
|
-
}
|
|
1392
|
-
case 'callout': {
|
|
1393
|
-
if (!el.content || typeof el.content !== 'string' || el.content.trim() === '') {
|
|
1394
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (callout): 'content' is required and must be a non-empty string`);
|
|
1395
|
-
}
|
|
1396
|
-
const validStyles = ['info', 'warning', 'tip', 'note'];
|
|
1397
|
-
if (el.style !== undefined && !validStyles.includes(el.style)) {
|
|
1398
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (callout): 'style' must be one of: ${validStyles.join(', ')}`);
|
|
1399
|
-
}
|
|
1400
|
-
if (el.backgroundColor !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(el.backgroundColor)) {
|
|
1401
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (callout): 'backgroundColor' must be a 6-digit hex color`);
|
|
1402
|
-
}
|
|
1403
|
-
if (el.borderColor !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(el.borderColor)) {
|
|
1404
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (callout): 'borderColor' must be a 6-digit hex color`);
|
|
1405
|
-
}
|
|
1406
|
-
if (el.color !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(el.color)) {
|
|
1407
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (callout): 'color' must be a 6-digit hex color`);
|
|
1408
|
-
}
|
|
1409
|
-
if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0)) {
|
|
1410
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (callout): 'fontSize' must be a positive number`);
|
|
1411
|
-
}
|
|
1412
|
-
break;
|
|
1413
|
-
}
|
|
1414
|
-
case 'toc': {
|
|
1415
|
-
if (el.minLevel !== undefined && ![1, 2, 3, 4].includes(el.minLevel)) {
|
|
1416
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'minLevel' must be 1, 2, 3, or 4`);
|
|
1417
|
-
}
|
|
1418
|
-
if (el.maxLevel !== undefined && ![1, 2, 3, 4].includes(el.maxLevel)) {
|
|
1419
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'maxLevel' must be 1, 2, 3, or 4`);
|
|
1420
|
-
}
|
|
1421
|
-
if (el.minLevel !== undefined && el.maxLevel !== undefined && el.minLevel > el.maxLevel) {
|
|
1422
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'minLevel' cannot exceed 'maxLevel'`);
|
|
1423
|
-
}
|
|
1424
|
-
if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
|
|
1425
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'fontSize' must be a positive finite number`);
|
|
1426
|
-
}
|
|
1427
|
-
if (el.titleFontSize !== undefined && (typeof el.titleFontSize !== 'number' || el.titleFontSize <= 0 || !isFinite(el.titleFontSize))) {
|
|
1428
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'titleFontSize' must be a positive finite number`);
|
|
1429
|
-
}
|
|
1430
|
-
if (el.levelIndent !== undefined && (typeof el.levelIndent !== 'number' || el.levelIndent < 0 || !isFinite(el.levelIndent))) {
|
|
1431
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'levelIndent' must be a non-negative finite number`);
|
|
1432
|
-
}
|
|
1433
|
-
if (el.leader !== undefined && (typeof el.leader !== 'string' || el.leader.length === 0)) {
|
|
1434
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'leader' must be a non-empty string`);
|
|
1435
|
-
}
|
|
1436
|
-
if (el.entrySpacing !== undefined && (typeof el.entrySpacing !== 'number' || el.entrySpacing < 0 || !isFinite(el.entrySpacing))) {
|
|
1437
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'entrySpacing' must be a non-negative finite number`);
|
|
1438
|
-
}
|
|
1439
|
-
if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
|
|
1440
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'spaceBefore' must be a non-negative finite number`);
|
|
1441
|
-
}
|
|
1442
|
-
if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
|
|
1443
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'spaceAfter' must be a non-negative finite number`);
|
|
1444
|
-
}
|
|
1445
|
-
break;
|
|
1446
|
-
}
|
|
1447
|
-
case 'comment': {
|
|
1448
|
-
const commentEl = el;
|
|
1449
|
-
if (!commentEl.contents || typeof commentEl.contents !== 'string' || commentEl.contents.trim() === '') {
|
|
1450
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (comment): 'contents' is required and must be a non-empty string`);
|
|
1451
|
-
}
|
|
1452
|
-
if (commentEl.color !== undefined && !HEX_COLOR_REGEX.test(commentEl.color)) {
|
|
1453
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (comment): 'color' must be a valid 6-digit hex color`);
|
|
1454
|
-
}
|
|
1455
|
-
break;
|
|
1456
|
-
}
|
|
1457
|
-
case 'form-field': {
|
|
1458
|
-
const ff = el;
|
|
1459
|
-
const fieldTypes = ['text', 'checkbox', 'radio', 'dropdown', 'button'];
|
|
1460
|
-
if (!fieldTypes.includes(ff.fieldType)) {
|
|
1461
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): fieldType must be one of: ${fieldTypes.join(', ')}`);
|
|
1462
|
-
}
|
|
1463
|
-
if (!ff.name || ff.name.trim() === '') {
|
|
1464
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): name is required and must be a non-empty string`);
|
|
1465
|
-
}
|
|
1466
|
-
if ((ff.fieldType === 'radio' || ff.fieldType === 'dropdown') && (!ff.options || ff.options.length === 0)) {
|
|
1467
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): type "${ff.fieldType}" requires a non-empty options array`);
|
|
1468
|
-
}
|
|
1469
|
-
if (ff.width !== undefined && (typeof ff.width !== 'number' || ff.width <= 0)) {
|
|
1470
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): width must be a positive number`);
|
|
1471
|
-
}
|
|
1472
|
-
if (ff.height !== undefined && (typeof ff.height !== 'number' || ff.height <= 0)) {
|
|
1473
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): height must be a positive number`);
|
|
1474
|
-
}
|
|
1475
|
-
if (ff.borderColor !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(ff.borderColor)) {
|
|
1476
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): borderColor must be a 6-digit hex color`);
|
|
1477
|
-
}
|
|
1478
|
-
if (ff.backgroundColor !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(ff.backgroundColor)) {
|
|
1479
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): backgroundColor must be a 6-digit hex color`);
|
|
1480
|
-
}
|
|
1481
|
-
break;
|
|
1482
|
-
}
|
|
1483
|
-
case 'footnote-def': {
|
|
1484
|
-
const fn = el;
|
|
1485
|
-
if (!fn.id || typeof fn.id !== 'string' || fn.id.trim() === '') {
|
|
1486
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (footnote-def): 'id' must be a non-empty string`);
|
|
1487
|
-
}
|
|
1488
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(fn.id)) {
|
|
1489
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (footnote-def): 'id' must contain only letters, numbers, hyphens, or underscores. Got: "${fn.id}"`);
|
|
1490
|
-
}
|
|
1491
|
-
if (!fn.text || typeof fn.text !== 'string' || fn.text.trim() === '') {
|
|
1492
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (footnote-def): 'text' must be a non-empty string`);
|
|
1493
|
-
}
|
|
1494
|
-
if (fn.fontSize !== undefined && (typeof fn.fontSize !== 'number' || fn.fontSize <= 0)) {
|
|
1495
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (footnote-def): 'fontSize' must be a positive number`);
|
|
1496
|
-
}
|
|
1497
|
-
break;
|
|
1498
|
-
}
|
|
1499
|
-
case 'toc-entry': {
|
|
1500
|
-
// Internal type — should never appear in user input
|
|
1501
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix}: 'toc-entry' is an internal type and cannot be used in document content`);
|
|
1502
|
-
}
|
|
1503
|
-
case 'float-group': {
|
|
1504
|
-
const fg = el;
|
|
1505
|
-
// Validate image
|
|
1506
|
-
if (!fg.image || !fg.image.src || (typeof fg.image.src !== 'string' && !(fg.image.src instanceof Uint8Array))) {
|
|
1507
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'image.src' must be a non-empty string path or Uint8Array`);
|
|
1508
|
-
}
|
|
1509
|
-
if (fg.image.format !== undefined && fg.image.format !== 'png' && fg.image.format !== 'jpg' && fg.image.format !== 'auto') {
|
|
1510
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'image.format' must be 'png', 'jpg', or 'auto'`);
|
|
1511
|
-
}
|
|
1512
|
-
if (fg.image.height !== undefined && (typeof fg.image.height !== 'number' || fg.image.height <= 0)) {
|
|
1513
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'image.height' must be a positive number`);
|
|
1514
|
-
}
|
|
1515
|
-
// Validate float
|
|
1516
|
-
if (fg.float !== 'left' && fg.float !== 'right') {
|
|
1517
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'float' must be 'left' or 'right'`);
|
|
1518
|
-
}
|
|
1519
|
-
// Validate floatWidth
|
|
1520
|
-
if (fg.floatWidth !== undefined && (typeof fg.floatWidth !== 'number' || fg.floatWidth < 30)) {
|
|
1521
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'floatWidth' must be a number >= 30`);
|
|
1522
|
-
}
|
|
1523
|
-
// Validate floatGap
|
|
1524
|
-
if (fg.floatGap !== undefined && (typeof fg.floatGap !== 'number' || fg.floatGap < 0)) {
|
|
1525
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'floatGap' must be a non-negative number`);
|
|
1526
|
-
}
|
|
1527
|
-
// Validate content
|
|
1528
|
-
if (!Array.isArray(fg.content) || fg.content.length === 0) {
|
|
1529
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'content' must be a non-empty array`);
|
|
1530
|
-
}
|
|
1531
|
-
for (let i = 0; i < fg.content.length; i++) {
|
|
1532
|
-
const item = fg.content[i];
|
|
1533
|
-
if (!['paragraph', 'heading', 'rich-paragraph'].includes(item.type)) {
|
|
1534
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group).content[${i}]: only 'paragraph', 'heading', and 'rich-paragraph' elements are allowed in float groups`);
|
|
1535
|
-
}
|
|
1536
|
-
// Strict: validate nested content element properties
|
|
1537
|
-
if (strict) {
|
|
1538
|
-
const allowed = ALLOWED_PROPS[item.type];
|
|
1539
|
-
if (allowed) {
|
|
1540
|
-
assertUnknownProps(item, allowed, `${prefix} (float-group).content[${i}]`, errors);
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
// Validate spacing
|
|
1545
|
-
if (fg.spaceBefore !== undefined && (typeof fg.spaceBefore !== 'number' || fg.spaceBefore < 0)) {
|
|
1546
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'spaceBefore' must be a non-negative number`);
|
|
1547
|
-
}
|
|
1548
|
-
if (fg.spaceAfter !== undefined && (typeof fg.spaceAfter !== 'number' || fg.spaceAfter < 0)) {
|
|
1549
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'spaceAfter' must be a non-negative number`);
|
|
1550
|
-
}
|
|
1551
|
-
break;
|
|
1552
|
-
}
|
|
1553
|
-
default: {
|
|
1554
|
-
const type = el.type;
|
|
1555
|
-
const plugins = options?.plugins ?? [];
|
|
1556
|
-
const plugin = findPlugin(plugins, String(type));
|
|
1557
|
-
if (plugin) {
|
|
1558
|
-
let rejection;
|
|
1559
|
-
try {
|
|
1560
|
-
rejection = runPluginValidate(plugin, el);
|
|
1561
|
-
}
|
|
1562
|
-
catch (err) {
|
|
1563
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (${String(type)}): plugin validate hook threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
1564
|
-
}
|
|
1565
|
-
if (rejection) {
|
|
1566
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (${String(type)}): ${rejection}`);
|
|
1567
|
-
}
|
|
1568
|
-
break;
|
|
1569
|
-
}
|
|
1570
|
-
const validList = ELEMENT_TYPES.map(t => `'${t}'`).join(', ');
|
|
1571
|
-
throw new PretextPdfError('VALIDATION_ERROR', `${prefix}: unknown element type '${String(type)}'. Valid types: ${validList}`);
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
function validateFontSpec(font) {
|
|
1576
|
-
if (!font.family || typeof font.family !== 'string') {
|
|
1577
|
-
throw new PretextPdfError('VALIDATION_ERROR', `FontSpec: 'family' must be a non-empty string`);
|
|
1578
|
-
}
|
|
1579
|
-
if (font.weight !== undefined && ![400, 700].includes(font.weight)) {
|
|
1580
|
-
throw new PretextPdfError('VALIDATION_ERROR', `FontSpec '${font.family}': 'weight' must be 400 or 700`);
|
|
1581
|
-
}
|
|
1582
|
-
if (font.style !== undefined && !['normal', 'italic'].includes(font.style)) {
|
|
1583
|
-
throw new PretextPdfError('VALIDATION_ERROR', `FontSpec '${font.family}': 'style' must be 'normal' or 'italic'`);
|
|
1584
|
-
}
|
|
1585
|
-
if (font.src === undefined || font.src === null) {
|
|
1586
|
-
throw new PretextPdfError('VALIDATION_ERROR', `FontSpec '${font.family}': 'src' is required (file path or Uint8Array)`);
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
9
|
+
export * from './validate/index.js';
|
|
1589
10
|
//# sourceMappingURL=validate.js.map
|