jtlt 0.2.0 → 0.3.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.
Files changed (50) hide show
  1. package/CHANGES.md +18 -0
  2. package/README.md +16 -87
  3. package/demo/calltemplate-params-demo.js +138 -0
  4. package/demo/index.html +31 -0
  5. package/demo/index.js +30 -0
  6. package/demo/xpath2-placeholder.js +1 -0
  7. package/dist/AbstractJoiningTransformer.d.ts +83 -9
  8. package/dist/AbstractJoiningTransformer.d.ts.map +1 -1
  9. package/dist/DOMJoiningTransformer.d.ts +85 -25
  10. package/dist/DOMJoiningTransformer.d.ts.map +1 -1
  11. package/dist/JSONJoiningTransformer.d.ts +159 -51
  12. package/dist/JSONJoiningTransformer.d.ts.map +1 -1
  13. package/dist/JSONPathTransformer.d.ts +37 -38
  14. package/dist/JSONPathTransformer.d.ts.map +1 -1
  15. package/dist/JSONPathTransformerContext.d.ts +247 -121
  16. package/dist/JSONPathTransformerContext.d.ts.map +1 -1
  17. package/dist/StringJoiningTransformer.d.ts +132 -41
  18. package/dist/StringJoiningTransformer.d.ts.map +1 -1
  19. package/dist/XPathTransformer.d.ts +35 -20
  20. package/dist/XPathTransformer.d.ts.map +1 -1
  21. package/dist/XPathTransformerContext.d.ts +191 -99
  22. package/dist/XPathTransformerContext.d.ts.map +1 -1
  23. package/dist/index-browser.d.ts +4 -0
  24. package/dist/index-browser.d.ts.map +1 -0
  25. package/dist/index-node.d.ts +4 -0
  26. package/dist/index-node.d.ts.map +1 -0
  27. package/dist/index.d.ts +330 -57
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/types.d.ts +204 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/docs/API.expanded.md +167 -2
  32. package/docs/API.md +91 -1
  33. package/docs/TO-DO.md +144 -0
  34. package/docs/calltemplate-params.md +251 -0
  35. package/eslint.config.js +9 -5
  36. package/package.json +13 -7
  37. package/pnpm-workspace.yaml +1 -0
  38. package/src/AbstractJoiningTransformer.js +54 -15
  39. package/src/DOMJoiningTransformer.js +275 -28
  40. package/src/JSONJoiningTransformer.js +351 -70
  41. package/src/JSONPathTransformer.js +48 -30
  42. package/src/JSONPathTransformerContext.js +308 -104
  43. package/src/StringJoiningTransformer.js +311 -57
  44. package/src/XPathTransformer.js +27 -12
  45. package/src/XPathTransformerContext.js +467 -89
  46. package/src/index-browser.js +5 -0
  47. package/src/index-node.js +7 -0
  48. package/src/index.js +498 -97
  49. package/typings/xpath2-js.d.ts +40 -1
  50. package/src/types/xpath2-js.d.ts +0 -2
@@ -0,0 +1,251 @@
1
+ # callTemplate Parameter Access via `valueOf()`
2
+
3
+ ## Overview
4
+
5
+ Parameters can be accessed within the template using `valueOf({select: '$paramName'})`:
6
+
7
+ ```javascript
8
+ await jtlt({
9
+ templates: [{
10
+ name: 'myTemplate',
11
+ template () {
12
+ this.string('Value: ');
13
+ this.valueOf({select: '$paramName'});
14
+ }
15
+ }]
16
+ });
17
+ ```
18
+
19
+ Note: Named templates (those with a `name` property) that are only called via `callTemplate` do not need a `path` property.
20
+
21
+ ## Features
22
+
23
+ ### Named Parameters
24
+
25
+ You can pass named parameters using the `name` property in `withParam`:
26
+
27
+ ```javascript
28
+ await jtlt({
29
+ templates: [
30
+ {
31
+ path: '$',
32
+ template () {
33
+ this.callTemplate({
34
+ name: 'formatUser',
35
+ withParam: [
36
+ {name: 'userName', value: 'Alice'},
37
+ {name: 'userRole', select: '$.role'}
38
+ ]
39
+ });
40
+ }
41
+ }
42
+ // ...
43
+ ]
44
+ });
45
+ ```
46
+
47
+ Access them in the template:
48
+
49
+ ```js
50
+ await jtlt({
51
+ templates: [
52
+ // ...
53
+ {
54
+ name: 'formatUser',
55
+ template () {
56
+ this.valueOf({select: '$userName'});
57
+ this.string(' - ');
58
+ this.valueOf({select: '$userRole'});
59
+ }
60
+ }
61
+ ]
62
+ });
63
+ ```
64
+
65
+ ### Indexed Parameters
66
+
67
+ When no `name` is provided, parameters are accessible by their index (0, 1, 2, etc.):
68
+
69
+ ```js
70
+ await jtlt({
71
+ templates: [
72
+ {
73
+ path: '$',
74
+ template () {
75
+ this.callTemplate({
76
+ name: 'format',
77
+ withParam: [
78
+ {value: 'First'},
79
+ {value: 'Second'}
80
+ ]
81
+ });
82
+ }
83
+ }
84
+ ]
85
+ });
86
+ ```
87
+
88
+ Access by index:
89
+
90
+ ```js
91
+ await jtlt({
92
+ templates: [
93
+ {
94
+ name: 'format',
95
+ template () {
96
+ this.valueOf({select: '$0'}); // First
97
+ this.valueOf({select: '$1'}); // Second
98
+ }
99
+ }
100
+ ]
101
+ });
102
+ ```
103
+
104
+ ### Nested callTemplate Calls
105
+
106
+ Parameters are scoped to the current template call. When nesting `callTemplate` calls, each template has access to its own parameters:
107
+
108
+ ```js
109
+ await jtlt({
110
+ templates: [
111
+ {
112
+ name: 'outer',
113
+ template () {
114
+ this.valueOf({select: '$outerParam'}); // Accesses outer parameter
115
+ this.callTemplate({
116
+ name: 'inner',
117
+ withParam: [{name: 'innerParam', value: 'inner-value'}]
118
+ });
119
+ }
120
+ },
121
+ {
122
+ name: 'inner',
123
+ template () {
124
+ this.valueOf({select: '$innerParam'}); // Accesses inner parameter
125
+ }
126
+ }
127
+ ]
128
+ });
129
+ ```
130
+
131
+ ## Implementation Details
132
+
133
+ ### How It Works
134
+
135
+ 1. When `callTemplate()` is called, it stores the parameters in a temporary `_params` object on the context
136
+ 2. Parameters are stored either by name (if provided) or by index
137
+ 3. When `valueOf()` is called with a selector starting with `$`, it checks `_params` first
138
+ 4. If the parameter is not found, it falls back to normal JSONPath/XPath evaluation
139
+ 5. After the template completes, the previous parameter context is restored
140
+
141
+ ### Parameter Priority
142
+
143
+ 1. **Named parameters**: If `withParam[i].name` is provided, the parameter is stored with that name
144
+ 2. **Indexed parameters**: If no name is provided, the parameter is stored with its index as a string
145
+
146
+ ### Backward Compatibility
147
+
148
+ The old approach of receiving parameters as function arguments is no longer supported. Templates should be updated to use `valueOf()` for parameter access. This change was made to:
149
+
150
+ - Provide a more consistent API with XSLT-style parameter access
151
+ - Allow templates to use the fluent builder pattern throughout
152
+ - Enable better parameter naming and documentation
153
+ - Support both JSONPath and XPath contexts uniformly
154
+
155
+ ## Examples
156
+
157
+ ### Example 1: User Formatting
158
+
159
+ ```js
160
+ await jtlt({
161
+ data: {users: [{name: 'Alice', role: 'Admin'}]},
162
+ outputType: 'string',
163
+ templates: [
164
+ {
165
+ path: '$',
166
+ template () {
167
+ this.forEach('$.users[*]', function (user) {
168
+ this.callTemplate({
169
+ name: 'formatUser',
170
+ withParam: [
171
+ {name: 'userName', value: user.name},
172
+ {name: 'userRole', value: user.role}
173
+ ]
174
+ });
175
+ });
176
+ }
177
+ },
178
+ {
179
+ name: 'formatUser',
180
+ template () {
181
+ this.string(' (');
182
+ this.valueOf({select: '$userRole'});
183
+ this.string(')\n');
184
+ }
185
+ }
186
+ ]
187
+ });
188
+ // Output: "Alice (Admin)\n"
189
+ ```
190
+
191
+ ### Example 2: Nested callTemplate
192
+
193
+ ```js
194
+ await jtlt({
195
+ data: {company: 'ACME Corp', division: 'Engineering'},
196
+ outputType: 'string',
197
+ templates: [
198
+ {
199
+ path: '$',
200
+ template () {
201
+ this.callTemplate({
202
+ name: 'outer',
203
+ withParam: [
204
+ {name: 'companyName', select: '$.company'},
205
+ {name: 'divisionName', select: '$.division'}
206
+ ]
207
+ });
208
+ }
209
+ },
210
+ {
211
+ name: 'outer',
212
+ template () {
213
+ this.string('Company: ');
214
+ this.valueOf({select: '$companyName'});
215
+ this.string('\n');
216
+
217
+ this.callTemplate({
218
+ name: 'inner',
219
+ withParam: [
220
+ {name: 'division', select: '$divisionName'}
221
+ ]
222
+ });
223
+ }
224
+ },
225
+ {
226
+ name: 'inner',
227
+ template () {
228
+ this.string(' Division: ');
229
+ this.valueOf({select: '$division'});
230
+ this.string('\n');
231
+ }
232
+ }
233
+ ]
234
+ });
235
+ // Output: "Company: ACME Corp\n Division: Engineering\n"
236
+ ```
237
+
238
+ ## Testing
239
+
240
+ Run the test suite to verify the implementation:
241
+
242
+ ```bash
243
+ pnpm test test/test.calltemplate-params.js
244
+ ```
245
+
246
+ Run the demo to see examples in action:
247
+
248
+ ```bash
249
+ node demo/calltemplate-params-demo.js
250
+ ```
251
+
package/eslint.config.js CHANGED
@@ -3,18 +3,22 @@ import ashNazg from 'eslint-config-ash-nazg';
3
3
  export default [
4
4
  {
5
5
  ignores: [
6
- 'dist'
6
+ 'dist',
7
+ 'coverage'
7
8
  ]
8
9
  },
9
10
  ...ashNazg(['sauron']),
10
11
  {
11
12
  rules: {
12
- // Temporary only:
13
- 'sonarjs/public-static-readonly': 'off', // Until ash-nazg disables
14
- 'jsdoc/check-types': 0,
15
13
  'jsdoc/reject-any-type': 0,
16
- 'jsdoc/reject-function-type': 0,
14
+
15
+ // Temporary only:
17
16
  'no-unused-vars': 0,
17
+
18
+ // AI was frequently making egregious mistakes here, so make fixable
19
+ 'jsdoc/check-alignment': 'error',
20
+
21
+ // We frequently use callbacks for nested interactions
18
22
  'promise/prefer-await-to-callbacks': 0
19
23
  }
20
24
  },
package/package.json CHANGED
@@ -1,15 +1,20 @@
1
1
  {
2
2
  "name": "jtlt",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "author": "Brett Zamir",
6
6
  "contributors": [],
7
7
  "license": "MIT",
8
8
  "types": "./dist/index.d.ts",
9
- "main": "src/index.js",
9
+ "main": "src/index-node.js",
10
10
  "exports": {
11
11
  "types": "./dist/index.d.ts",
12
- "import": "./src/index.js"
12
+ "browser": {
13
+ "import": "./src/index-browser.js"
14
+ },
15
+ "node": {
16
+ "import": "./src/index-node.js"
17
+ }
13
18
  },
14
19
  "engines": {
15
20
  "node": ">=22.16.0"
@@ -20,25 +25,25 @@
20
25
  "url": "https://github.com/brettz9/jtlt.git"
21
26
  },
22
27
  "dependencies": {
23
- "jamilih": "0.61.4",
28
+ "jamilih": "0.62.1",
24
29
  "jhtml": "0.7.1",
25
30
  "jsdom": "27.1.0",
26
31
  "jsonpath-plus": "10.3.0",
27
- "lodash": "^4.17.21",
28
- "object-assign": "4.x",
29
32
  "simple-get-json": "10.0.0",
30
33
  "xpath2.js": "1.0.0-alpha-6"
31
34
  },
32
35
  "devDependencies": {
33
36
  "@arethetypeswrong/cli": "^0.18.2",
37
+ "@node-static/node-static": "^0.8.1",
34
38
  "@types/chai": "^5.2.3",
35
39
  "@types/jsdom": "^27.0.0",
36
40
  "@types/mocha": "^10.0.10",
37
41
  "c8": "^10.1.3",
38
42
  "chai": "^6.2.0",
39
43
  "eslint": "^9.39.1",
40
- "eslint-config-ash-nazg": "^39.7.1",
44
+ "eslint-config-ash-nazg": "^39.8.0",
41
45
  "mocha": "^11.7.5",
46
+ "open": "^10.2.0",
42
47
  "typescript": "^5.9.3"
43
48
  },
44
49
  "bugs": "https://github.com/brettz9/jtlt/issues",
@@ -50,6 +55,7 @@
50
55
  "javascript"
51
56
  ],
52
57
  "scripts": {
58
+ "start": "open http://localhost:8020/demo/ && static -p 8020",
53
59
  "attw": "attw --pack",
54
60
  "tsc": "tsc",
55
61
  "build": "tsc -p tsconfig-prod.json",
@@ -1,3 +1,4 @@
1
1
  onlyBuiltDependencies:
2
2
  - ejs
3
+ - is-hidden-file
3
4
  - unrs-resolver
@@ -2,6 +2,47 @@
2
2
  // mid-transformation (e.g., building strings with
3
3
  // string transformer but adding as text node in a DOM transformer)
4
4
 
5
+ /**
6
+ * @typedef {{
7
+ * requireSameChildren?: boolean,
8
+ * JHTMLForJSON?: boolean,
9
+ * mode?: "JSON"|"JavaScript"
10
+ * }} BaseTransformerConfig
11
+ */
12
+
13
+ /**
14
+ * @typedef {BaseTransformerConfig & {
15
+ * document: Document,
16
+ * exposeDocuments?: boolean
17
+ * }} DOMJoiningTransformerConfig
18
+ * When exposeDocuments is true, get() returns an array of XMLDocument
19
+ * objects (one per root element) instead of a DocumentFragment.
20
+ */
21
+ /**
22
+ * @typedef {object} JSONJoiningTransformerConfig
23
+ * @property {boolean} [requireSameChildren]
24
+ * @property {boolean} [unwrapSingleResult]
25
+ * @property {boolean} [exposeDocuments] - When true, get() returns an array
26
+ * of document wrapper objects (one per root element) instead of the raw array.
27
+ * @property {"JSON"|"JavaScript"} [mode]
28
+ */
29
+ /**
30
+ * @typedef {BaseTransformerConfig & {
31
+ * xmlElements?: boolean,
32
+ * preEscapedAttributes?: boolean,
33
+ * exposeDocuments?: boolean
34
+ * }} StringJoiningTransformerConfig
35
+ * When exposeDocuments is true, get() returns an array of document
36
+ * strings (one per root element) instead of a single concatenated string.
37
+ */
38
+ /**
39
+ * @template T
40
+ * @typedef {T extends "string" ? StringJoiningTransformerConfig :
41
+ * T extends "dom" ? DOMJoiningTransformerConfig :
42
+ * T extends "json" ? JSONJoiningTransformerConfig : never
43
+ * } JoiningTransformerConfig
44
+ */
45
+
5
46
  /**
6
47
  * Base class for joining transformers.
7
48
  *
@@ -16,18 +57,19 @@
16
57
  * based on the current state.
17
58
  * - get(): returns the accumulated result.
18
59
  * - config(): temporarily tweak a config flag for the duration of a callback.
60
+ * @template T
19
61
  */
20
62
  class AbstractJoiningTransformer {
21
63
  /**
22
- * @param {object} [cfg] - Configuration object
64
+ * @param {JoiningTransformerConfig<T>} [cfg] - Configuration object
23
65
  */
24
66
  constructor (cfg) {
25
67
  // Todo: Might set some reasonable defaults across all classes
26
- this.setConfig(cfg);
68
+ this._cfg = cfg ?? /** @type {JoiningTransformerConfig<T>} */ ({});
27
69
  }
28
70
 
29
71
  /**
30
- * @param {any} [cfg] - Configuration object
72
+ * @param {JoiningTransformerConfig<T>} cfg - Configuration object
31
73
  * @returns {void}
32
74
  */
33
75
  setConfig (cfg) {
@@ -40,8 +82,8 @@ class AbstractJoiningTransformer {
40
82
  * @returns {void}
41
83
  */
42
84
  _requireSameChildren (type, embedType) {
43
- if (this._cfg && /** @type {any} */ (this._cfg)[type] &&
44
- /** @type {any} */ (this._cfg)[type].requireSameChildren) {
85
+ const cfg = this._cfg;
86
+ if (cfg.requireSameChildren) {
45
87
  throw new Error(
46
88
  'Cannot embed ' + embedType + ' children for a ' + type +
47
89
  ' joining transformer.'
@@ -51,21 +93,18 @@ class AbstractJoiningTransformer {
51
93
 
52
94
  /**
53
95
  * @param {string} prop - Configuration property name
54
- * @param {*} val - Configuration property value
55
- * @param {Function} [cb] - Callback function
96
+ * @param {any} val - Configuration property value
97
+ * @param {(this: AbstractJoiningTransformer<T>) => void} [cb]
98
+ * Optional callback invoked with this instance
56
99
  * @returns {void}
57
100
  */
58
101
  config (prop, val, cb) {
59
- const oldCfgProp = this._cfg &&
60
- /** @type {any} */ (this._cfg)[prop];
61
- if (this._cfg) {
62
- /** @type {any} */ (this._cfg)[prop] = val;
63
- }
102
+ const cfg = /** @type {Record<string, unknown>} */ (this._cfg);
103
+ const oldCfgProp = cfg[prop];
104
+ cfg[prop] = val;
64
105
  if (cb) {
65
106
  cb.call(this);
66
- if (this._cfg) {
67
- /** @type {any} */ (this._cfg)[prop] = oldCfgProp;
68
- }
107
+ cfg[prop] = oldCfgProp;
69
108
  }
70
109
  }
71
110
  }