@docx4j/core-ts 0.1.2 → 0.1.4

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.
@@ -1,4 +1,4 @@
1
- export { type XPathEngine, type XPathValue, DefaultXPathEngine, parsePrefixMappings, formatPrefixMappings, canonicalXPathOf, xmlOf, type CanonicalXPath } from './xpath.mjs';
1
+ export { type XPathEngine, type XPathValue, type BooleanConversionMode, booleanValue, DefaultXPathEngine, parsePrefixMappings, formatPrefixMappings, canonicalXPathOf, xmlOf, type CanonicalXPath } from './xpath.mjs';
2
2
  export { CustomXmlPart, CustomXmlNode, CustomXmlPrefixMappingCollection, type CustomXmlNodeType, type CustomXmlPrefixMapping, type CustomXmlPartOwner } from './CustomXmlPart.mjs';
3
3
  export { CustomXmlPartCollection, type CustomXmlHost } from './CustomXmlPartCollection.mjs';
4
4
  export { XmlMapping, type CustomXmlPartLookup } from './XmlMapping.mjs';
@@ -1,6 +1,6 @@
1
1
  // CR-002 phase E: custom XML parts, XML mapping, the XPath engine and the typed content controls
2
2
  // (CR-002 sections 3.5 and 3.6). Exported from '.' and './model', as the content API is.
3
- export { DefaultXPathEngine, parsePrefixMappings, formatPrefixMappings, canonicalXPathOf, xmlOf } from './xpath.mjs';
3
+ export { booleanValue, DefaultXPathEngine, parsePrefixMappings, formatPrefixMappings, canonicalXPathOf, xmlOf } from './xpath.mjs';
4
4
  export { CustomXmlPart, CustomXmlNode, CustomXmlPrefixMappingCollection } from './CustomXmlPart.mjs';
5
5
  export { CustomXmlPartCollection } from './CustomXmlPartCollection.mjs';
6
6
  export { XmlMapping } from './XmlMapping.mjs';
@@ -0,0 +1,64 @@
1
+ import type { BooleanConversionMode, XPathEngine, XPathValue } from './xpath.mjs';
2
+ /**
3
+ * XPath 3.1 through FontoXPath, for the `xpath2` boolean conversion mode and for expressions
4
+ * written in XPath 2.0 syntax. `select`, `selectValue` and the `java` and `xpath1` modes answer as
5
+ * `DefaultXPathEngine` does; `xpath2` is the mode this engine exists for.
6
+ *
7
+ * **In a browser or any bundled application, pass the module in.** The no-argument form loads
8
+ * `fontoxpath` by a dynamic `import()` of the bare specifier, which Node resolves and a browser
9
+ * does not: there is no import map, and the `@vite-ignore` that keeps a bundler from rewriting the
10
+ * specifier (needed so that a bundle never carries the optional peer against the consumer's will)
11
+ * also keeps it from resolving it. So the application imports the module itself, where its own
12
+ * bundler resolves it statically, and hands it over:
13
+ *
14
+ * import * as fontoxpath from 'fontoxpath';
15
+ * import { FontoXPathEngine } from '@docx4j/core-ts/xpath-fonto';
16
+ * pkg.xpathEngine = new FontoXPathEngine(fontoxpath); // ready at once
17
+ *
18
+ * In Node, `new FontoXPathEngine()` and `await ready()` still work and still say what to install
19
+ * if the peer is absent. Which module is loaded, and when, is the consumer's business either way -
20
+ * the same principle as `pkg.xpathEngine` being settable at all (CR-005 phase A, 2026-09-26).
21
+ */
22
+ export declare class FontoXPathEngine implements XPathEngine {
23
+ private module;
24
+ private loading;
25
+ /**
26
+ * @param fontoxpath the `fontoxpath` module, imported by the caller. Omit it in Node to have
27
+ * `ready()` import it; supply it in a browser or a bundle, where a bare dynamic specifier
28
+ * cannot be resolved. Either the module's namespace object or its default export is accepted,
29
+ * the published ESM build having no named exports under Node's interop.
30
+ */
31
+ constructor(fontoxpath?: unknown);
32
+ get isReady(): boolean;
33
+ ready(): Promise<void>;
34
+ select(expression: string, context: Node, namespaces?: Record<string, string>): Node[];
35
+ selectValue(expression: string, context: Node, namespaces?: Record<string, string>): XPathValue;
36
+ /**
37
+ * Table 7's three modes. `java` and `xpath1` are the rules the specification states, evaluated
38
+ * here rather than through the `booleanValue` function so that one engine answers all three.
39
+ * `xpath2` is the effective boolean value **with the specification's string rule**: see
40
+ * {@link xpath2BooleanValue}.
41
+ */
42
+ booleanValue(expression: string, context: Node, namespaces: Record<string, string>, mode: BooleanConversionMode): boolean;
43
+ /**
44
+ * The `xpath2` mode of table 7, **as docx4j does it**: the expression is wrapped in
45
+ * `xs:boolean(...)` and the engine performs the cast, which is `XmlPart.cachedXPathGetBoolean`
46
+ * with `opendope.conditions.Xpathref.XPathBoolean=cast2` (measured against Saxon-HE 9.9.0-2 by
47
+ * the docx4j session, 2026-09-25, and matched here case for case). Doing what docx4j does,
48
+ * rather than casting by hand, is what keeps the two from drifting: the lexical space, the
49
+ * whitespace collapse, the multi-item rule and the empty sequence all come from the engine.
50
+ *
51
+ * So: `true`, `1` are true; `false`, `0` are false; leading and trailing space is collapsed
52
+ * first, so `" false "` is false; an expression selecting **nothing** is false; and anything
53
+ * else raises - `"yes"`, `"True"` (`xs:boolean`'s lexical space is case-sensitive where the
54
+ * `java` mode ignores case), `""` from an empty element, and a sequence of more than one node.
55
+ *
56
+ * A raise is not a false. docx4j turns it into an `InputIntegrityException` and the whole bind
57
+ * fails; here it is a `Docx4JException` from this call, and what a processor does with it is
58
+ * the processor's business (CR-005 phase B). That is the one place the two behaviours are not
59
+ * yet the same thing, and the specification does not say which is right.
60
+ */
61
+ private xpath2BooleanValue;
62
+ private options;
63
+ private fonto;
64
+ }
@@ -0,0 +1,161 @@
1
+ // CR-005 phase A: an XPath 2.0 (in fact 3.1) `XPathEngine`, for the OpenDoPE specification's
2
+ // `xpath2` boolean conversion mode (its section 7.2, table 7).
3
+ //
4
+ // `DefaultXPathEngine` is XPath 1.0, which is what `document.evaluate` and the `xpath` package
5
+ // give. The specification's `xpath2` mode needs a 2.0 evaluator, and expressions written for it
6
+ // may use 2.0 syntax; docx4j uses Saxon. [FontoXPath](https://github.com/FontoXML/fontoxpath) is
7
+ // XPath 3.1 in JavaScript (MIT), over DOM nodes in browsers and in Node, and XPath 3.1 is a
8
+ // superset of 2.0.
9
+ //
10
+ // It is an **optional peer dependency** and this module is its own entry point
11
+ // (`@docx4j/core-ts/xpath-fonto`), so nothing that imports the package pays its ~665 KB: only a
12
+ // consumer who needs the `xpath2` mode installs it and imports this.
13
+ //
14
+ // import { FontoXPathEngine } from '@docx4j/core-ts/xpath-fonto';
15
+ // pkg.xpathEngine = new FontoXPathEngine();
16
+ // await pkg.customXmlParts.load();
17
+ import { Docx4JException } from '../../opc/exceptions.mjs';
18
+ /** `evaluateXPath`'s ANY_TYPE: the value in its natural type, which is what `selectValue` reports. */
19
+ const ANY_TYPE = 0;
20
+ /**
21
+ * XPath 3.1 through FontoXPath, for the `xpath2` boolean conversion mode and for expressions
22
+ * written in XPath 2.0 syntax. `select`, `selectValue` and the `java` and `xpath1` modes answer as
23
+ * `DefaultXPathEngine` does; `xpath2` is the mode this engine exists for.
24
+ *
25
+ * **In a browser or any bundled application, pass the module in.** The no-argument form loads
26
+ * `fontoxpath` by a dynamic `import()` of the bare specifier, which Node resolves and a browser
27
+ * does not: there is no import map, and the `@vite-ignore` that keeps a bundler from rewriting the
28
+ * specifier (needed so that a bundle never carries the optional peer against the consumer's will)
29
+ * also keeps it from resolving it. So the application imports the module itself, where its own
30
+ * bundler resolves it statically, and hands it over:
31
+ *
32
+ * import * as fontoxpath from 'fontoxpath';
33
+ * import { FontoXPathEngine } from '@docx4j/core-ts/xpath-fonto';
34
+ * pkg.xpathEngine = new FontoXPathEngine(fontoxpath); // ready at once
35
+ *
36
+ * In Node, `new FontoXPathEngine()` and `await ready()` still work and still say what to install
37
+ * if the peer is absent. Which module is loaded, and when, is the consumer's business either way -
38
+ * the same principle as `pkg.xpathEngine` being settable at all (CR-005 phase A, 2026-09-26).
39
+ */
40
+ export class FontoXPathEngine {
41
+ /**
42
+ * @param fontoxpath the `fontoxpath` module, imported by the caller. Omit it in Node to have
43
+ * `ready()` import it; supply it in a browser or a bundle, where a bare dynamic specifier
44
+ * cannot be resolved. Either the module's namespace object or its default export is accepted,
45
+ * the published ESM build having no named exports under Node's interop.
46
+ */
47
+ constructor(fontoxpath) {
48
+ if (fontoxpath !== undefined)
49
+ this.module = moduleOf(fontoxpath);
50
+ }
51
+ get isReady() {
52
+ return this.module !== undefined;
53
+ }
54
+ async ready() {
55
+ var _a;
56
+ if (this.isReady)
57
+ return;
58
+ (_a = this.loading) !== null && _a !== void 0 ? _a : (this.loading = (async () => {
59
+ const specifier = 'fontoxpath';
60
+ try {
61
+ const loaded = await import(/* @vite-ignore */ specifier);
62
+ this.module = moduleOf(loaded);
63
+ }
64
+ catch (e) {
65
+ throw new Docx4JException("FontoXPathEngine needs the optional peer dependency 'fontoxpath' (npm install fontoxpath); "
66
+ + 'in a browser or a bundle, import it yourself and pass it: new FontoXPathEngine(fontoxpath)', { cause: e });
67
+ }
68
+ })());
69
+ await this.loading;
70
+ }
71
+ select(expression, context, namespaces = {}) {
72
+ return this.fonto().evaluateXPathToNodes(expression, context, null, null, this.options(namespaces));
73
+ }
74
+ selectValue(expression, context, namespaces = {}) {
75
+ const value = this.fonto().evaluateXPath(expression, context, null, null, ANY_TYPE, this.options(namespaces));
76
+ return valueOf(value);
77
+ }
78
+ /**
79
+ * Table 7's three modes. `java` and `xpath1` are the rules the specification states, evaluated
80
+ * here rather than through the `booleanValue` function so that one engine answers all three.
81
+ * `xpath2` is the effective boolean value **with the specification's string rule**: see
82
+ * {@link xpath2BooleanValue}.
83
+ */
84
+ booleanValue(expression, context, namespaces, mode) {
85
+ switch (mode) {
86
+ case 'java': {
87
+ const value = this.selectValue(expression, context, namespaces);
88
+ return typeof value === 'boolean' ? value : String(value !== null && value !== void 0 ? value : '').toLowerCase() === 'true';
89
+ }
90
+ case 'xpath1':
91
+ // XPath 1.0's boolean() rules, asked for in that language so that 3.1's do not apply.
92
+ return this.fonto().evaluateXPathToBoolean(`boolean(${expression})`, context, null, null, { ...this.options(namespaces), language: 'XPath3.1' });
93
+ default:
94
+ return this.xpath2BooleanValue(expression, context, namespaces);
95
+ }
96
+ }
97
+ /**
98
+ * The `xpath2` mode of table 7, **as docx4j does it**: the expression is wrapped in
99
+ * `xs:boolean(...)` and the engine performs the cast, which is `XmlPart.cachedXPathGetBoolean`
100
+ * with `opendope.conditions.Xpathref.XPathBoolean=cast2` (measured against Saxon-HE 9.9.0-2 by
101
+ * the docx4j session, 2026-09-25, and matched here case for case). Doing what docx4j does,
102
+ * rather than casting by hand, is what keeps the two from drifting: the lexical space, the
103
+ * whitespace collapse, the multi-item rule and the empty sequence all come from the engine.
104
+ *
105
+ * So: `true`, `1` are true; `false`, `0` are false; leading and trailing space is collapsed
106
+ * first, so `" false "` is false; an expression selecting **nothing** is false; and anything
107
+ * else raises - `"yes"`, `"True"` (`xs:boolean`'s lexical space is case-sensitive where the
108
+ * `java` mode ignores case), `""` from an empty element, and a sequence of more than one node.
109
+ *
110
+ * A raise is not a false. docx4j turns it into an `InputIntegrityException` and the whole bind
111
+ * fails; here it is a `Docx4JException` from this call, and what a processor does with it is
112
+ * the processor's business (CR-005 phase B). That is the one place the two behaviours are not
113
+ * yet the same thing, and the specification does not say which is right.
114
+ */
115
+ xpath2BooleanValue(expression, context, namespaces) {
116
+ try {
117
+ return this.fonto().evaluateXPathToBoolean(`xs:boolean(${expression})`, context, null, null, this.options(namespaces));
118
+ }
119
+ catch (e) {
120
+ throw new Docx4JException(`The xpath2 boolean conversion mode casts to xs:boolean, which accepts only "true", "false", "1" and "0", `
121
+ + `and one value at a time: ${expression} gives none of them`, { cause: e });
122
+ }
123
+ }
124
+ options(namespaces) {
125
+ return { namespaceResolver: (prefix) => { var _a; return (_a = namespaces[prefix]) !== null && _a !== void 0 ? _a : null; } };
126
+ }
127
+ fonto() {
128
+ if (!this.module) {
129
+ throw new Docx4JException('The XPath engine is not ready yet: await pkg.customXmlParts.load() (or pkg.xpathEngine.ready()) once before selecting nodes');
130
+ }
131
+ return this.module;
132
+ }
133
+ }
134
+ /**
135
+ * The module, whether given as a namespace object or as a default export, checked for the one
136
+ * function every call goes through so that the wrong module is refused where it is passed rather
137
+ * than at the first evaluation.
138
+ */
139
+ function moduleOf(loaded) {
140
+ const candidate = loaded;
141
+ const module = typeof (candidate === null || candidate === void 0 ? void 0 : candidate.evaluateXPath) === 'function' ? candidate : candidate === null || candidate === void 0 ? void 0 : candidate.default;
142
+ if (!module || typeof module.evaluateXPath !== 'function') {
143
+ throw new Docx4JException('FontoXPathEngine was given something that is not the fontoxpath module: '
144
+ + "expected its evaluateXPath, as in new FontoXPathEngine(await import('fontoxpath'))");
145
+ }
146
+ return module;
147
+ }
148
+ /** A FontoXPath result in the four-type union `selectValue` reports. */
149
+ function valueOf(value) {
150
+ var _a;
151
+ if (value === null || value === undefined)
152
+ return undefined;
153
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
154
+ return value;
155
+ if (Array.isArray(value))
156
+ return value.length === 0 ? undefined : valueOf(value[0]);
157
+ if (typeof value === 'object' && typeof value.nodeType === 'number') {
158
+ return (_a = value.textContent) !== null && _a !== void 0 ? _a : '';
159
+ }
160
+ return String(value);
161
+ }
@@ -1,5 +1,19 @@
1
1
  /** The XPath result kinds `selectValue` can report. */
2
2
  export type XPathValue = string | number | boolean | undefined;
3
+ /**
4
+ * How an XPaths entry becomes a boolean (OpenDoPE specification v3 section 7.2, table 7, declared
5
+ * in the XPaths part's `booleanConversion` attribute; CR-005 phase A):
6
+ *
7
+ * - `java`: the string value is true only if it equals `true`, ignoring case. `"1"` and `"yes"`
8
+ * are false. docx4j's `Boolean.parseBoolean`, and the default for a template that declares no
9
+ * mode.
10
+ * - `xpath1`: XPath 1.0 `boolean()`. A node-set is true if non-empty, a string if non-empty - so
11
+ * `"false"` is **true**.
12
+ * - `xpath2`: the effective boolean value, a string cast to `xs:boolean` accepting only `true`,
13
+ * `false`, `1` and `0` - so `"false"` is false and `"yes"` is an **error**. Needs an XPath 2.0
14
+ * engine: `FontoXPathEngine` from `@docx4j/core-ts/xpath-fonto`.
15
+ */
16
+ export type BooleanConversionMode = 'java' | 'xpath1' | 'xpath2';
3
17
  /**
4
18
  * XPath 1.0 over a DOM (CR-002 section 3.6). `select` answers a node set; `selectValue` answers a
5
19
  * string, number or boolean result. `namespaces` maps prefixes used in the expression to URIs.
@@ -11,7 +25,25 @@ export interface XPathEngine {
11
25
  readonly isReady: boolean;
12
26
  select(expression: string, context: Node, namespaces?: Record<string, string>): Node[];
13
27
  selectValue(expression: string, context: Node, namespaces?: Record<string, string>): XPathValue;
28
+ /**
29
+ * The expression as a boolean in one conversion mode, for an engine that can do more than the
30
+ * function {@link booleanValue} can do over `selectValue` - in practice `xpath2`, which needs an
31
+ * XPath 2.0 evaluator. Optional: `XPathEngine` is public and a consumer may implement it, so a
32
+ * required member would break every implementation that predates this. Callers go through
33
+ * {@link booleanValue}, never here.
34
+ */
35
+ booleanValue?(expression: string, context: Node, namespaces: Record<string, string>, mode: BooleanConversionMode): boolean;
14
36
  }
37
+ /**
38
+ * The expression as a boolean in a declared conversion mode (table 7 above).
39
+ *
40
+ * `java` and `xpath1` need nothing an `XPathEngine` does not already have, so they work over any
41
+ * engine: `java` is the string value compared with `true`, `xpath1` is XPath 1.0 `boolean()` of the
42
+ * expression. `xpath2` is delegated to the engine's own `booleanValue` when it has one, and
43
+ * otherwise throws naming the engine to install - which is the specification's REQ-032, a processor
44
+ * evaluating in the declared mode or refusing to process the template.
45
+ */
46
+ export declare function booleanValue(engine: XPathEngine, expression: string, context: Node, namespaces?: Record<string, string>, mode?: BooleanConversionMode): boolean;
15
47
  /**
16
48
  * The default engine: `document.evaluate` where the runtime has it, else the `xpath` package,
17
49
  * imported on `ready()`. The import specifier is a variable so that a bundler does not follow it
@@ -12,6 +12,34 @@
12
12
  // afterwards is synchronous. A call before the engine is ready throws, naming the fix.
13
13
  import { Docx4JException } from '../../opc/exceptions.mjs';
14
14
  import { serializeXml } from '../../xml/dom.mjs';
15
+ /**
16
+ * The expression as a boolean in a declared conversion mode (table 7 above).
17
+ *
18
+ * `java` and `xpath1` need nothing an `XPathEngine` does not already have, so they work over any
19
+ * engine: `java` is the string value compared with `true`, `xpath1` is XPath 1.0 `boolean()` of the
20
+ * expression. `xpath2` is delegated to the engine's own `booleanValue` when it has one, and
21
+ * otherwise throws naming the engine to install - which is the specification's REQ-032, a processor
22
+ * evaluating in the declared mode or refusing to process the template.
23
+ */
24
+ export function booleanValue(engine, expression, context, namespaces = {}, mode = 'java') {
25
+ if (engine.booleanValue)
26
+ return engine.booleanValue(expression, context, namespaces, mode);
27
+ switch (mode) {
28
+ case 'java': {
29
+ const value = engine.selectValue(expression, context, namespaces);
30
+ return typeof value === 'boolean' ? value : String(value !== null && value !== void 0 ? value : '').toLowerCase() === 'true';
31
+ }
32
+ case 'xpath1': {
33
+ // boolean() is XPath 1.0's own conversion, so the engine applies the rules rather than this.
34
+ const value = engine.selectValue(`boolean(${expression})`, context, namespaces);
35
+ return value === true;
36
+ }
37
+ default:
38
+ throw new Docx4JException(`The boolean conversion mode "${mode}" needs an XPath 2.0 engine: `
39
+ + "import { FontoXPathEngine } from '@docx4j/core-ts/xpath-fonto' (npm install fontoxpath) "
40
+ + 'and set pkg.xpathEngine to it');
41
+ }
42
+ }
15
43
  // XPathResult constants, spelled out because `XPathResult` is not a global in Node.
16
44
  const ANY_TYPE = 0;
17
45
  const NUMBER_TYPE = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docx4j/core-ts",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "docx4j-core for TypeScript: Open Packaging (docx/pptx/xlsx zip and flat OPC), typed parts and relationships, style, numbering and font resolution over @docx4j/generated-objects-ts. Node, browsers, Word add-ins.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -33,6 +33,10 @@
33
33
  "./office-js": {
34
34
  "types": "./dist/office-js/index.d.mts",
35
35
  "import": "./dist/office-js/index.mjs"
36
+ },
37
+ "./xpath-fonto": {
38
+ "types": "./dist/model/customxml/xpath-fonto.d.mts",
39
+ "import": "./dist/model/customxml/xpath-fonto.mjs"
36
40
  }
37
41
  },
38
42
  "files": [
@@ -79,13 +83,18 @@
79
83
  "fflate": "^0.8.2"
80
84
  },
81
85
  "devDependencies": {
86
+ "fontoxpath": "^3.34.0",
82
87
  "typescript": "~5.6.3",
83
88
  "xpath": "^0.0.34"
84
89
  },
85
90
  "peerDependencies": {
91
+ "fontoxpath": "^3.30.0",
86
92
  "xpath": "^0.0.34"
87
93
  },
88
94
  "peerDependenciesMeta": {
95
+ "fontoxpath": {
96
+ "optional": true
97
+ },
89
98
  "xpath": {
90
99
  "optional": true
91
100
  }