jtlt 0.1.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/.editorconfig +16 -0
- package/CHANGES.md +5 -0
- package/LICENSE-MIT.txt +21 -0
- package/README.md +534 -0
- package/dist/AbstractJoiningTransformer.d.ts +42 -0
- package/dist/AbstractJoiningTransformer.d.ts.map +1 -0
- package/dist/DOMJoiningTransformer.d.ts +113 -0
- package/dist/DOMJoiningTransformer.d.ts.map +1 -0
- package/dist/JSONJoiningTransformer.d.ts +160 -0
- package/dist/JSONJoiningTransformer.d.ts.map +1 -0
- package/dist/JSONPathTransformer.d.ts +95 -0
- package/dist/JSONPathTransformer.d.ts.map +1 -0
- package/dist/JSONPathTransformerContext.d.ts +263 -0
- package/dist/JSONPathTransformerContext.d.ts.map +1 -0
- package/dist/StringJoiningTransformer.d.ts +168 -0
- package/dist/StringJoiningTransformer.d.ts.map +1 -0
- package/dist/XPathTransformer.d.ts +51 -0
- package/dist/XPathTransformer.d.ts.map +1 -0
- package/dist/XPathTransformerContext.d.ts +260 -0
- package/dist/XPathTransformerContext.d.ts.map +1 -0
- package/dist/XSLTStyleJSONPathResolver.d.ts +16 -0
- package/dist/XSLTStyleJSONPathResolver.d.ts.map +1 -0
- package/dist/index.d.ts +168 -0
- package/dist/index.d.ts.map +1 -0
- package/docs/API.expanded.md +263 -0
- package/docs/API.md +69 -0
- package/eslint.config.js +30 -0
- package/package.json +53 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/AbstractJoiningTransformer.js +73 -0
- package/src/DOMJoiningTransformer.js +237 -0
- package/src/JSONJoiningTransformer.js +472 -0
- package/src/JSONPathTransformer.js +159 -0
- package/src/JSONPathTransformerContext.js +807 -0
- package/src/StringJoiningTransformer.js +589 -0
- package/src/XPathTransformer.js +94 -0
- package/src/XPathTransformerContext.js +496 -0
- package/src/XSLTStyleJSONPathResolver.js +39 -0
- package/src/index.js +299 -0
- package/src/types/xpath2-js.d.ts +2 -0
- package/tsconfig-prod.json +19 -0
- package/tsconfig.json +14 -0
- package/typings/xpath2-js.d.ts +2 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import {jml} from 'jamilih';
|
|
2
|
+
import * as JHTML from 'jhtml';
|
|
3
|
+
import AbstractJoiningTransformer from './AbstractJoiningTransformer.js';
|
|
4
|
+
|
|
5
|
+
const camelCase = /[a-z][A-Z]/gv;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Type guard to detect DOM Elements.
|
|
9
|
+
* @param {*} item
|
|
10
|
+
* @returns {item is Element}
|
|
11
|
+
*/
|
|
12
|
+
function _isElement (item) {
|
|
13
|
+
return item && typeof item === 'object' && item.nodeType === 1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} n0
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function _makeDatasetAttribute (n0) {
|
|
21
|
+
return n0.charAt(0) + '-' + n0.charAt(1).toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Joining transformer that builds a string result.
|
|
29
|
+
*
|
|
30
|
+
* This transformer provides a fluent API to compose strings while supporting
|
|
31
|
+
* object/array-building semantics similar to template languages. Most methods
|
|
32
|
+
* funnel through append(), which is state-aware:
|
|
33
|
+
*
|
|
34
|
+
* - Inside object(): values go to object properties via propOnly()/propValue().
|
|
35
|
+
* - Inside array(): values are pushed to the current array.
|
|
36
|
+
* - Otherwise: values are concatenated into the internal string buffer.
|
|
37
|
+
*
|
|
38
|
+
* Escaping rules:
|
|
39
|
+
* - text(): escapes HTML special chars (& and <) and will close an open tag.
|
|
40
|
+
* - string(): no HTML escaping or JSON stringification; context-aware.
|
|
41
|
+
* - plainText(): raw append to the top-level string buffer (bypasses state).
|
|
42
|
+
* - rawAppend(): like plainText() but documented as lower-level.
|
|
43
|
+
*
|
|
44
|
+
* HTML/XML helpers:
|
|
45
|
+
* - element() and attribute() allow building tags with optional auto-escaping
|
|
46
|
+
* for attribute values unless cfg.preEscapedAttributes is set.
|
|
47
|
+
*
|
|
48
|
+
* Configuration hints (see joiningConfig in JTLT):
|
|
49
|
+
* - cfg.xmlElements: switch element() serializer to XML mode (self-closing,
|
|
50
|
+
* name mapping rules differ, etc.).
|
|
51
|
+
* - cfg.preEscapedAttributes: skip escaping attribute values.
|
|
52
|
+
* - cfg.JHTMLForJSON / cfg.mode: affect how object()/array() serialize.
|
|
53
|
+
*/
|
|
54
|
+
class StringJoiningTransformer extends AbstractJoiningTransformer {
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} s - Initial string
|
|
57
|
+
* @param {object} cfg - Configuration object
|
|
58
|
+
*/
|
|
59
|
+
constructor (s, cfg) {
|
|
60
|
+
super(cfg); // Include this in any subclass of AbstractJoiningTransformer
|
|
61
|
+
|
|
62
|
+
this._str = s || '';
|
|
63
|
+
/** @type {any} */
|
|
64
|
+
this._objPropTemp = undefined;
|
|
65
|
+
/** @type {boolean | undefined} */
|
|
66
|
+
this.propOnlyState = undefined;
|
|
67
|
+
/** @type {boolean | undefined} */
|
|
68
|
+
this._arrItemState = undefined;
|
|
69
|
+
/** @type {boolean | undefined} */
|
|
70
|
+
this._objPropState = undefined;
|
|
71
|
+
/** @type {any} */
|
|
72
|
+
this._obj = undefined;
|
|
73
|
+
/** @type {any[]} */
|
|
74
|
+
this._arr = [];
|
|
75
|
+
/** @type {string | undefined} */
|
|
76
|
+
this._strTemp = undefined;
|
|
77
|
+
/** @type {Record<string, any>} */
|
|
78
|
+
this.propertySets = {};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string|*} s - String or value to append
|
|
83
|
+
* @returns {StringJoiningTransformer}
|
|
84
|
+
*/
|
|
85
|
+
append (s) {
|
|
86
|
+
// Todo: Could allow option to disallow elements within arrays, etc.
|
|
87
|
+
// (add states and state checking)
|
|
88
|
+
|
|
89
|
+
if (this.propOnlyState) {
|
|
90
|
+
this._obj[this._objPropTemp] = s;
|
|
91
|
+
this.propOnlyState = false;
|
|
92
|
+
this._objPropTemp = undefined;
|
|
93
|
+
} else if (this._arrItemState) {
|
|
94
|
+
this._arr.push(s);
|
|
95
|
+
} else if (this._objPropState) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
'Object values must be added via propValue() or after ' +
|
|
98
|
+
'propOnly() when in an object state.'
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
this._str += s;
|
|
102
|
+
}
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
get () {
|
|
110
|
+
return this._str;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} prop - Property name
|
|
115
|
+
* @param {*} val - Property value
|
|
116
|
+
* @returns {StringJoiningTransformer}
|
|
117
|
+
*/
|
|
118
|
+
propValue (prop, val) {
|
|
119
|
+
if (!this._objPropState) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
'propValue() can only be called after an object state has been set up.'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
this._obj[prop] = val;
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {string} prop - Property name
|
|
130
|
+
* @param {Function} cb - Callback function
|
|
131
|
+
* @returns {StringJoiningTransformer}
|
|
132
|
+
*/
|
|
133
|
+
propOnly (prop, cb) {
|
|
134
|
+
if (!this._objPropState) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
'propOnly() can only be called after an object state has been set up.'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (this.propOnlyState) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
'propOnly() can only be called again after a value is set'
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
this.propOnlyState = true;
|
|
145
|
+
/** @type {any} */
|
|
146
|
+
const oldPropTemp = this._objPropTemp;
|
|
147
|
+
this._objPropTemp = prop;
|
|
148
|
+
cb.call(this);
|
|
149
|
+
this._objPropTemp = oldPropTemp;
|
|
150
|
+
if (this.propOnlyState) {
|
|
151
|
+
throw new Error('propOnly() must be followed up with setting a value.');
|
|
152
|
+
}
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {object|Element} obj - Object to serialize
|
|
158
|
+
* @param {Function} cb - Callback function
|
|
159
|
+
* @param {any[]} [usePropertySets] - Property sets to use
|
|
160
|
+
* @param {object} [propSets] - Additional property sets
|
|
161
|
+
* @returns {StringJoiningTransformer}
|
|
162
|
+
*/
|
|
163
|
+
object (obj, cb, usePropertySets, propSets) {
|
|
164
|
+
// Builds up an internal object (or converts a supplied Element via JHTML)
|
|
165
|
+
// and, depending on context, either appends the object to the current
|
|
166
|
+
// array/object or serializes it into the output string (JSON, JavaScript,
|
|
167
|
+
// or JHTML), based on cfg.
|
|
168
|
+
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
169
|
+
const that = this;
|
|
170
|
+
this._requireSameChildren('string', 'object');
|
|
171
|
+
/** @type {any} */
|
|
172
|
+
const oldObjPropState = this._objPropState;
|
|
173
|
+
/** @type {any} */
|
|
174
|
+
const oldObj = this._obj;
|
|
175
|
+
this._obj = _isElement(obj)
|
|
176
|
+
? JHTML.toJSONObject(obj, {mode: this._cfg?.mode})
|
|
177
|
+
: obj || {};
|
|
178
|
+
|
|
179
|
+
// Todo: Allow in this and subsequent JSON methods ability to create
|
|
180
|
+
// jml-based JHTML
|
|
181
|
+
|
|
182
|
+
if (usePropertySets !== undefined) {
|
|
183
|
+
this._obj = usePropertySets.reduce(function (o, psName) {
|
|
184
|
+
return that._usePropertySets(o, psName);
|
|
185
|
+
}, this._obj);
|
|
186
|
+
}
|
|
187
|
+
if (propSets !== undefined) {
|
|
188
|
+
Object.assign(this._obj, propSets);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (cb) {
|
|
192
|
+
this._objPropState = true;
|
|
193
|
+
cb.call(this);
|
|
194
|
+
this._objPropState = oldObjPropState;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Not ready to serialize yet as still inside another array or object
|
|
198
|
+
if (oldObjPropState || this._arrItemState) {
|
|
199
|
+
this.append(this._obj);
|
|
200
|
+
} else if (this._cfg && this._cfg.JHTMLForJSON) {
|
|
201
|
+
this.append(JHTML.toJHTMLString(this._obj));
|
|
202
|
+
} else if (this._cfg && this._cfg.mode !== 'JavaScript') {
|
|
203
|
+
// Allow this method to operate on non-finite numbers and functions
|
|
204
|
+
const stringifier = new JHTML.Stringifier({mode: 'JavaScript'});
|
|
205
|
+
this.append(stringifier.walkJSONObject(this._obj));
|
|
206
|
+
} else {
|
|
207
|
+
this.append(JSON.stringify(this._obj));
|
|
208
|
+
}
|
|
209
|
+
this._obj = oldObj;
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {any[]|Element} [arr] - Array to serialize
|
|
215
|
+
* @param {Function} [cb] - Callback function
|
|
216
|
+
* @returns {StringJoiningTransformer}
|
|
217
|
+
*/
|
|
218
|
+
array (arr, cb) {
|
|
219
|
+
// Similar to object(), but for arrays. Context determines whether to
|
|
220
|
+
// append the array structure or to serialize into the string.
|
|
221
|
+
this._requireSameChildren('string', 'array');
|
|
222
|
+
/** @type {any} */
|
|
223
|
+
const oldArr = this._arr;
|
|
224
|
+
// Todo: copy array?
|
|
225
|
+
this._arr = _isElement(arr)
|
|
226
|
+
? /** @type {any[]} */ (JHTML.toJSONObject(arr, {mode: this._cfg?.mode}))
|
|
227
|
+
: arr || [];
|
|
228
|
+
|
|
229
|
+
/** @type {any} */
|
|
230
|
+
const oldArrItemState = this._arrItemState;
|
|
231
|
+
|
|
232
|
+
/* c8 ignore next 8 -- Callback handling for nested array building.
|
|
233
|
+
* Requires specific state combinations with nested objects/arrays. */
|
|
234
|
+
if (cb) {
|
|
235
|
+
const oldObjPropState = this._objPropState;
|
|
236
|
+
this._objPropState = false;
|
|
237
|
+
this._arrItemState = true;
|
|
238
|
+
cb.call(this);
|
|
239
|
+
this._arrItemState = oldArrItemState;
|
|
240
|
+
this._objPropState = oldObjPropState;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Not ready to serialize yet as still inside another array or object
|
|
244
|
+
if (oldArrItemState || this._objPropState) {
|
|
245
|
+
this.append(this._arr);
|
|
246
|
+
/* c8 ignore next 2 -- JHTMLForJSON is a specialized output mode rarely used
|
|
247
|
+
* in combination with nested array building at the root level. */
|
|
248
|
+
} else if (this._cfg && this._cfg.JHTMLForJSON) {
|
|
249
|
+
this.append(JHTML.toJHTMLString(this._arr));
|
|
250
|
+
} else if (this._cfg && this._cfg.mode !== 'JavaScript') {
|
|
251
|
+
// Allow this method to operate on non-finite numbers and functions
|
|
252
|
+
const stringifier = new JHTML.Stringifier({mode: 'JavaScript'});
|
|
253
|
+
this.append(stringifier.walkJSONObject(this._arr));
|
|
254
|
+
} else {
|
|
255
|
+
this.append(JSON.stringify(this._arr));
|
|
256
|
+
}
|
|
257
|
+
this._arr = oldArr;
|
|
258
|
+
return this;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {string|Element|object} str - String value or element
|
|
263
|
+
* @param {Function} [cb] - Callback function
|
|
264
|
+
* @returns {StringJoiningTransformer}
|
|
265
|
+
*/
|
|
266
|
+
string (str, cb) {
|
|
267
|
+
// Context-aware string emission. If inside object/array/propOnly states,
|
|
268
|
+
// the produced string participates in those structures via append().
|
|
269
|
+
// If a callback is provided, it composes a nested string value first.
|
|
270
|
+
if (_isElement(str)) {
|
|
271
|
+
str = /** @type {any} */ (
|
|
272
|
+
JHTML.toJSONObject(str, {mode: this._cfg?.mode})
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let tmpStr = '';
|
|
277
|
+
/** @type {any} */
|
|
278
|
+
const _oldStrTemp = this._strTemp;
|
|
279
|
+
if (cb) {
|
|
280
|
+
this._strTemp = '';
|
|
281
|
+
cb.call(this);
|
|
282
|
+
tmpStr = this._strTemp;
|
|
283
|
+
this._strTemp = _oldStrTemp;
|
|
284
|
+
}
|
|
285
|
+
if (_oldStrTemp !== undefined) {
|
|
286
|
+
this._strTemp = (this._strTemp || '') + str;
|
|
287
|
+
} else {
|
|
288
|
+
// Append to the output (or current container via append()).
|
|
289
|
+
this.append(tmpStr + str);
|
|
290
|
+
}
|
|
291
|
+
return this;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @param {number|Element|object} num - Number value or element
|
|
296
|
+
* @returns {StringJoiningTransformer}
|
|
297
|
+
*/
|
|
298
|
+
number (num) {
|
|
299
|
+
// Appends the number as a string; no localization/formatting is applied.
|
|
300
|
+
if (_isElement(num)) {
|
|
301
|
+
num = /** @type {any} */ (
|
|
302
|
+
JHTML.toJSONObject(num, {mode: this._cfg?.mode})
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
this.append(num.toString());
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {boolean|Element|object} bool - Boolean value or element
|
|
311
|
+
* @returns {StringJoiningTransformer}
|
|
312
|
+
*/
|
|
313
|
+
boolean (bool) {
|
|
314
|
+
// Appends 'true' or 'false'.
|
|
315
|
+
if (_isElement(bool)) {
|
|
316
|
+
bool = /** @type {any} */ (
|
|
317
|
+
JHTML.toJSONObject(bool, {mode: this._cfg?.mode})
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
this.append(bool ? 'true' : 'false');
|
|
321
|
+
return this;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @returns {StringJoiningTransformer}
|
|
326
|
+
*/
|
|
327
|
+
null () {
|
|
328
|
+
// Appends the literal 'null'.
|
|
329
|
+
this.append('null');
|
|
330
|
+
return this;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* @returns {StringJoiningTransformer}
|
|
335
|
+
*/
|
|
336
|
+
undefined () {
|
|
337
|
+
// Appends the literal 'undefined' (only in JavaScript mode).
|
|
338
|
+
if (this._cfg && this._cfg.mode !== 'JavaScript') {
|
|
339
|
+
throw new Error(
|
|
340
|
+
'undefined is not allowed unless added in JavaScript mode'
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
this.append('undefined');
|
|
344
|
+
return this;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @param {number|Element} num - Non-finite number (NaN, Infinity, -Infinity)
|
|
349
|
+
* @returns {StringJoiningTransformer}
|
|
350
|
+
*/
|
|
351
|
+
nonfiniteNumber (num) {
|
|
352
|
+
// Appends NaN/Infinity/-Infinity as-is (only in JavaScript mode).
|
|
353
|
+
if (this._cfg && this._cfg.mode !== 'JavaScript') {
|
|
354
|
+
throw new Error(
|
|
355
|
+
'Non-finite numbers are not allowed unless added in JavaScript mode'
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (_isElement(num)) {
|
|
359
|
+
num = /** @type {any} */ (
|
|
360
|
+
JHTML.toJSONObject(num, {mode: this._cfg?.mode})
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
this.append(num.toString());
|
|
364
|
+
return this;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @param {Function|Element} func - Function to stringify
|
|
369
|
+
* @returns {StringJoiningTransformer}
|
|
370
|
+
*/
|
|
371
|
+
function (func) {
|
|
372
|
+
// Appends function source (only in JavaScript mode).
|
|
373
|
+
if (this._cfg && this._cfg.mode !== 'JavaScript') {
|
|
374
|
+
throw new Error(
|
|
375
|
+
'function is not allowed unless added in JavaScript mode'
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (_isElement(func)) {
|
|
379
|
+
func = /** @type {any} */ (
|
|
380
|
+
JHTML.toJSONObject(func, {mode: this._cfg?.mode})
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
this.append(func.toString());
|
|
384
|
+
return this;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* @param {string|object} elName - Element name or element object
|
|
389
|
+
* @param {object} [atts] - Element attributes
|
|
390
|
+
* @param {any[]} [childNodes] - Child nodes
|
|
391
|
+
* @param {Function} [cb] - Callback function
|
|
392
|
+
* @returns {StringJoiningTransformer}
|
|
393
|
+
*/
|
|
394
|
+
element (elName, atts, childNodes, cb) {
|
|
395
|
+
// Emits an HTML/XML element using Jamilih under the hood, or allows a
|
|
396
|
+
// callback to build attributes/children incrementally. Attribute values
|
|
397
|
+
// are escaped unless cfg.preEscapedAttributes is true. When a callback is
|
|
398
|
+
// provided, this manages open-tag state so text() can close it safely.
|
|
399
|
+
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
400
|
+
const that = this;
|
|
401
|
+
if (Array.isArray(atts)) {
|
|
402
|
+
cb = /** @type {Function} */ (/** @type {unknown} */ (childNodes));
|
|
403
|
+
childNodes = atts;
|
|
404
|
+
atts = {};
|
|
405
|
+
} else if (typeof atts === 'function') {
|
|
406
|
+
cb = atts;
|
|
407
|
+
childNodes = [];
|
|
408
|
+
atts = {};
|
|
409
|
+
}
|
|
410
|
+
if (typeof childNodes === 'function') {
|
|
411
|
+
cb = childNodes;
|
|
412
|
+
childNodes = [];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Todo: allow for cfg to produce Jamilih string output or hXML
|
|
416
|
+
// string output
|
|
417
|
+
const method = this._cfg && this._cfg.xmlElements ? 'toXML' : 'toHTML';
|
|
418
|
+
if (!cb) {
|
|
419
|
+
// Note that Jamilih currently has an issue with 'selected', 'checked',
|
|
420
|
+
// 'value', 'defaultValue', 'for', 'on*', 'style' (workaround: pass
|
|
421
|
+
// an empty callback as the last argument to element())
|
|
422
|
+
this.append(
|
|
423
|
+
// Casts to satisfy TS when using JS + JSDoc
|
|
424
|
+
/** @type {any} */ (jml[method])(elName,
|
|
425
|
+
/** @type {any} */ (atts), /** @type {any} */ (childNodes))
|
|
426
|
+
);
|
|
427
|
+
return this;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (typeof elName === 'object') {
|
|
431
|
+
/** @type {Record<string, any>} */
|
|
432
|
+
const objAtts = {};
|
|
433
|
+
/** @type {any} */
|
|
434
|
+
const elObj = elName;
|
|
435
|
+
[...elObj.attributes].forEach(function (att, i) {
|
|
436
|
+
objAtts[att.name] = att.value;
|
|
437
|
+
});
|
|
438
|
+
atts = Object.assign(objAtts, atts);
|
|
439
|
+
elName = elObj.nodeName;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
this.append('<' + elName);
|
|
443
|
+
/** @type {any} */
|
|
444
|
+
const oldTagState = this._openTagState;
|
|
445
|
+
this._openTagState = true;
|
|
446
|
+
if (atts) {
|
|
447
|
+
const attsObj = /** @type {Record<string, any>} */ (atts);
|
|
448
|
+
Object.keys(attsObj).forEach((att) => {
|
|
449
|
+
that.attribute(att, attsObj[att], false);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
if (childNodes && childNodes.length) {
|
|
453
|
+
this._openTagState = false;
|
|
454
|
+
this.append(jml[method]({'#': childNodes}));
|
|
455
|
+
}
|
|
456
|
+
cb.call(this);
|
|
457
|
+
|
|
458
|
+
// Todo: Depending on an this._cfg.xmlElements option, allow for
|
|
459
|
+
// XML self-closing when empty or as per the tag, HTML
|
|
460
|
+
// self-closing tags (or polyglot-friendly self-closing)
|
|
461
|
+
if (this._openTagState) {
|
|
462
|
+
this.append('>');
|
|
463
|
+
}
|
|
464
|
+
this.append('</' + elName + '>');
|
|
465
|
+
this._openTagState = oldTagState;
|
|
466
|
+
return this;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* @param {string} name - Attribute name
|
|
471
|
+
* @param {string|object} val - Attribute value
|
|
472
|
+
* @param {boolean} [avoidAttEscape] - Whether to avoid escaping the
|
|
473
|
+
* attribute value
|
|
474
|
+
* @returns {StringJoiningTransformer}
|
|
475
|
+
*/
|
|
476
|
+
attribute (name, val, avoidAttEscape) {
|
|
477
|
+
// Adds an attribute to the most recently opened start tag. Supports
|
|
478
|
+
// special objects for dataset and ordered attributes ($a). Escapes '&'
|
|
479
|
+
// and '"' unless cfg.preEscapedAttributes or avoidAttEscape are set.
|
|
480
|
+
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
481
|
+
const that = this;
|
|
482
|
+
if (!this._openTagState) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
'An attribute cannot be added after an opening tag has been closed ' +
|
|
485
|
+
'(name: ' + name + '; value: ' + val + ')'
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (!this._cfg || !this._cfg.xmlElements) {
|
|
490
|
+
if (typeof val === 'object') {
|
|
491
|
+
/** @type {Record<string, any>} */
|
|
492
|
+
const valObj = /** @type {any} */ (val);
|
|
493
|
+
switch (name) {
|
|
494
|
+
case 'dataset': {
|
|
495
|
+
Object.keys(valObj).forEach(function (att) {
|
|
496
|
+
that.attribute(
|
|
497
|
+
'data-' + att.replaceAll(
|
|
498
|
+
camelCase, _makeDatasetAttribute
|
|
499
|
+
), valObj[att], false
|
|
500
|
+
);
|
|
501
|
+
});
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
case '$a': { // Ordered attributes
|
|
505
|
+
/** @type {any[]} */
|
|
506
|
+
const valArr = /** @type {any} */ (val);
|
|
507
|
+
valArr.forEach(function (attArr) {
|
|
508
|
+
that.attribute(attArr[0], attArr[1], false);
|
|
509
|
+
});
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
default:
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
return this;
|
|
516
|
+
}
|
|
517
|
+
name = {className: 'class', htmlFor: 'for'}[name] || name;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** @type {string} */
|
|
521
|
+
const valStr = /** @type {any} */ (val);
|
|
522
|
+
val = ((this._cfg && this._cfg.preEscapedAttributes) || avoidAttEscape)
|
|
523
|
+
? valStr
|
|
524
|
+
: valStr.replaceAll('&', '&').replaceAll('"', '"');
|
|
525
|
+
this.append(' ' + name + '="' + val + '"');
|
|
526
|
+
return this;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* @param {string} txt - Text content to escape and append
|
|
531
|
+
* @returns {StringJoiningTransformer}
|
|
532
|
+
*/
|
|
533
|
+
text (txt) {
|
|
534
|
+
// Adds escaped text content. If currently within an unclosed start tag,
|
|
535
|
+
// it will first close the tag ('>'). Escapes '&' and '<'.
|
|
536
|
+
if (this._openTagState) {
|
|
537
|
+
this.append('>');
|
|
538
|
+
this._openTagState = false;
|
|
539
|
+
}
|
|
540
|
+
this.append(txt.replaceAll('&', '&').replaceAll('<', '<'));
|
|
541
|
+
return this;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Unlike text(), does not escape for HTML; unlike string(), does not perform
|
|
546
|
+
* JSON stringification; unlike append(), does not do other checks (but still
|
|
547
|
+
* varies in its role across transformers).
|
|
548
|
+
* @param {string} str
|
|
549
|
+
* @returns {StringJoiningTransformer}
|
|
550
|
+
*/
|
|
551
|
+
rawAppend (str) {
|
|
552
|
+
// Lowest-level append: bypasses append() semantics and state checks.
|
|
553
|
+
this._str += str;
|
|
554
|
+
return this;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* @param {string} str - Plain text to append without escaping
|
|
559
|
+
* @returns {StringJoiningTransformer}
|
|
560
|
+
*/
|
|
561
|
+
plainText (str) {
|
|
562
|
+
// Bypasses append() semantics: always writes directly to the top-level
|
|
563
|
+
// string buffer with no escaping. Prefer string() when you want the value
|
|
564
|
+
// to participate in object/array contexts.
|
|
565
|
+
this._str += str;
|
|
566
|
+
return this;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Helper method to use property sets.
|
|
571
|
+
* @param {object} obj - Object to apply property set to
|
|
572
|
+
* @param {string} psName - Property set name
|
|
573
|
+
* @returns {object}
|
|
574
|
+
*/
|
|
575
|
+
_usePropertySets (obj, psName) {
|
|
576
|
+
// Merge named property set from this.propertySets into obj
|
|
577
|
+
if (this.propertySets && this.propertySets[psName]) {
|
|
578
|
+
return {
|
|
579
|
+
...obj,
|
|
580
|
+
...this.propertySets[psName]
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return obj;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Todo: Implement comment(), processingInstruction(), etc.
|
|
588
|
+
|
|
589
|
+
export default StringJoiningTransformer;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import XPathTransformerContext from './XPathTransformerContext.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Applies named XPath-driven templates to XML/HTML DOM data.
|
|
5
|
+
*
|
|
6
|
+
* Finds templates whose `path` XPath matches the current node (plus optional
|
|
7
|
+
* `mode`), sorts by priority, and invokes the winning template.
|
|
8
|
+
* Falls back to default rules when no template matches.
|
|
9
|
+
*/
|
|
10
|
+
class XPathTransformer {
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} config Configuration
|
|
13
|
+
* @param {boolean} [config.errorOnEqualPriority] Throw on equal priority
|
|
14
|
+
* @param {any[]} config.templates Template objects
|
|
15
|
+
* @param {number} [config.xpathVersion] XPath version (1|2)
|
|
16
|
+
*/
|
|
17
|
+
constructor (config) {
|
|
18
|
+
let map = /** @type {Record<string, boolean>} */ ({});
|
|
19
|
+
this._config = config;
|
|
20
|
+
/** @type {any[]} */
|
|
21
|
+
this.rootTemplates = [];
|
|
22
|
+
this.templates = config.templates;
|
|
23
|
+
this.templates = this.templates.map(function (template) {
|
|
24
|
+
if (Array.isArray(template)) {
|
|
25
|
+
return {path: template[0], template: template[1]};
|
|
26
|
+
}
|
|
27
|
+
return template;
|
|
28
|
+
});
|
|
29
|
+
this.templates.forEach((template) => {
|
|
30
|
+
if (template.name && map[template.name]) {
|
|
31
|
+
/* c8 ignore next 6 -- c8/Istanbul known limitation: arrow function
|
|
32
|
+
* predicates within filter assignments to instance properties are not
|
|
33
|
+
* instrumented. Functionality fully tested via direct assertions on
|
|
34
|
+
* rootTemplates and templates array lengths and contents in
|
|
35
|
+
* test suite. */
|
|
36
|
+
throw new Error('Templates must all have different names.');
|
|
37
|
+
}
|
|
38
|
+
map[template.name] = true;
|
|
39
|
+
});
|
|
40
|
+
// Collect root templates without mutating during iteration
|
|
41
|
+
this.rootTemplates = this.templates.filter((t) => t.path === '/');
|
|
42
|
+
this.templates = this.templates.filter((t) => t.path !== '/');
|
|
43
|
+
map = /** @type {any} */ (null);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @returns {void}
|
|
48
|
+
*/
|
|
49
|
+
_triggerEqualPriorityError () {
|
|
50
|
+
if (this._config.errorOnEqualPriority) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'You have configured XPathTransformer to throw errors on equal ' +
|
|
53
|
+
'priority templates and these have been found.'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} mode Transformation mode
|
|
60
|
+
* @returns {*} Result of transformation
|
|
61
|
+
*/
|
|
62
|
+
transform (mode) {
|
|
63
|
+
const xte = new XPathTransformerContext(
|
|
64
|
+
/** @type {any} */ (this._config), this.templates
|
|
65
|
+
);
|
|
66
|
+
const len = this.rootTemplates.length;
|
|
67
|
+
const templateObj = len
|
|
68
|
+
? this.rootTemplates.pop()
|
|
69
|
+
: XPathTransformer.DefaultTemplateRules.transformRoot;
|
|
70
|
+
if (len > 1) {
|
|
71
|
+
this._triggerEqualPriorityError();
|
|
72
|
+
}
|
|
73
|
+
const ret = templateObj.template.call(xte, undefined, {mode});
|
|
74
|
+
if (typeof ret !== 'undefined') {
|
|
75
|
+
/** @type {any} */ (xte)._getJoiningTransformer().append(ret);
|
|
76
|
+
}
|
|
77
|
+
return xte.getOutput();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static DefaultTemplateRules = {
|
|
81
|
+
transformRoot: {
|
|
82
|
+
/**
|
|
83
|
+
* @param {*} node Node
|
|
84
|
+
* @param {{mode:string}} cfg Config
|
|
85
|
+
* @returns {void}
|
|
86
|
+
*/
|
|
87
|
+
template (node, cfg) {
|
|
88
|
+
/** @type {any} */ (this).applyTemplates('.', cfg.mode);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default XPathTransformer;
|