@tradik/xslt-processor 1.0.3 → 1.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 (131) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +110 -520
  3. package/bin/lib/decode.js +15 -0
  4. package/bin/lib/dom.js +177 -0
  5. package/bin/lib/loaders.js +127 -0
  6. package/bin/lib/options.js +131 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +186 -0
  9. package/bin/lib/transform.js +206 -0
  10. package/bin/xslt.js +73 -168
  11. package/dist/xslt-processor.browser.js +9564 -1585
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -2
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +9572 -1586
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +658 -0
  18. package/dist/xslt-processor.d.ts +459 -12
  19. package/dist/xslt-processor.js +9546 -1582
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +71 -20
  22. package/src/XSLTProcessor.js +494 -48
  23. package/src/async/abort.js +63 -0
  24. package/src/async/documentUris.js +128 -0
  25. package/src/async/loaders.js +134 -0
  26. package/src/async/preload.js +159 -0
  27. package/src/async/processor.js +206 -0
  28. package/src/async/stream.js +125 -0
  29. package/src/bridge/engine.js +221 -0
  30. package/src/bridge/loader.js +78 -0
  31. package/src/bridge/results.js +75 -0
  32. package/src/bridge/version.js +63 -0
  33. package/src/index.js +26 -8
  34. package/src/io/decode.js +140 -0
  35. package/src/io/readSource.js +167 -0
  36. package/src/xpath/axes.js +562 -0
  37. package/src/xpath/documentOrder.js +270 -0
  38. package/src/xpath/evaluator.js +518 -357
  39. package/src/xpath/index.js +8 -2
  40. package/src/xpath/namespaceNodes.js +172 -0
  41. package/src/xpath/nodeSetFunctions.js +169 -0
  42. package/src/xpath/parser.js +30 -5
  43. package/src/xpath/strings.js +183 -0
  44. package/src/xpath/tokenizer.js +37 -23
  45. package/src/xslt/attributeSets.js +95 -0
  46. package/src/xslt/avt.js +103 -0
  47. package/src/xslt/computedNames.js +91 -0
  48. package/src/xslt/copying.js +212 -0
  49. package/src/xslt/declarationNames.js +80 -0
  50. package/src/xslt/domParsing.js +95 -0
  51. package/src/xslt/elements.js +57 -0
  52. package/src/xslt/engine/bindings.js +195 -0
  53. package/src/xslt/engine/context.js +105 -0
  54. package/src/xslt/engine/controlFlow.js +145 -0
  55. package/src/xslt/engine/copyInstructions.js +133 -0
  56. package/src/xslt/engine/declarations.js +233 -0
  57. package/src/xslt/engine/functionSupport.js +103 -0
  58. package/src/xslt/engine/methods.js +33 -0
  59. package/src/xslt/engine/nodeConstruction.js +187 -0
  60. package/src/xslt/engine/numbering.js +104 -0
  61. package/src/xslt/engine/outputDeclaration.js +77 -0
  62. package/src/xslt/engine/sequenceConstructor.js +228 -0
  63. package/src/xslt/engine/stylesheetLoading.js +208 -0
  64. package/src/xslt/engine/templateInvocation.js +253 -0
  65. package/src/xslt/engine/templateRules.js +243 -0
  66. package/src/xslt/engine/textInstructions.js +171 -0
  67. package/src/xslt/engine/topLevel.js +130 -0
  68. package/src/xslt/engine/transformation.js +263 -0
  69. package/src/xslt/engine/workStack.js +245 -0
  70. package/src/xslt/engine.js +184 -1736
  71. package/src/xslt/exslt/arguments.js +99 -0
  72. package/src/xslt/exslt/calendar.js +120 -0
  73. package/src/xslt/exslt/common.js +44 -0
  74. package/src/xslt/exslt/dateCalc.js +261 -0
  75. package/src/xslt/exslt/dateFormat.js +150 -0
  76. package/src/xslt/exslt/dateParse.js +265 -0
  77. package/src/xslt/exslt/dates.js +259 -0
  78. package/src/xslt/exslt/duration.js +207 -0
  79. package/src/xslt/exslt/dynamic.js +59 -0
  80. package/src/xslt/exslt/index.js +59 -0
  81. package/src/xslt/exslt/math.js +177 -0
  82. package/src/xslt/exslt/sets.js +96 -0
  83. package/src/xslt/exslt/stringOps.js +163 -0
  84. package/src/xslt/exslt/strings.js +147 -0
  85. package/src/xslt/exslt/uri.js +92 -0
  86. package/src/xslt/formatNumber.js +233 -0
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +270 -0
  89. package/src/xslt/index.js +38 -1
  90. package/src/xslt/keys.js +164 -0
  91. package/src/xslt/literalResult.js +223 -0
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +271 -0
  94. package/src/xslt/numberFormat.js +253 -0
  95. package/src/xslt/outputNames.js +58 -0
  96. package/src/xslt/patternCompiler.js +175 -0
  97. package/src/xslt/patterns.js +324 -0
  98. package/src/xslt/qname.js +90 -0
  99. package/src/xslt/resultDocument.js +98 -0
  100. package/src/xslt/resultNamespaces.js +219 -0
  101. package/src/xslt/resultTree.js +211 -0
  102. package/src/xslt/serializer/baseWriter.js +390 -0
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +92 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +135 -0
  107. package/src/xslt/serializer/frames.js +168 -0
  108. package/src/xslt/serializer/htmlDoctype.js +102 -0
  109. package/src/xslt/serializer/htmlEntities.js +77 -0
  110. package/src/xslt/serializer/htmlSerializer.js +239 -0
  111. package/src/xslt/serializer/indent.js +51 -0
  112. package/src/xslt/serializer/namespaces.js +68 -0
  113. package/src/xslt/serializer/rawText.js +41 -0
  114. package/src/xslt/serializer/settings.js +179 -0
  115. package/src/xslt/serializer/textSerializer.js +77 -0
  116. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  117. package/src/xslt/serializer/xmlSerializer.js +227 -0
  118. package/src/xslt/serializer.js +90 -0
  119. package/src/xslt/sort.js +151 -0
  120. package/src/xslt/spaceNameTests.js +115 -0
  121. package/src/xslt/stylesheetChecks.js +206 -0
  122. package/src/xslt/stylesheetNamespaces.js +266 -0
  123. package/src/xslt/templatePriority.js +45 -0
  124. package/src/xslt/uri.js +68 -0
  125. package/src/xslt/variables.js +152 -0
  126. package/src/xslt/whitespace.js +200 -0
  127. package/LICENSE +0 -29
  128. package/src/XSLTProcessor.test.js +0 -930
  129. package/src/xpath/evaluator.test.js +0 -1852
  130. package/src/xpath/tokenizer.test.js +0 -224
  131. package/src/xslt/engine.test.js +0 -3130
@@ -82,6 +82,14 @@ const NODE_TYPES = new Set([
82
82
 
83
83
  const OPERATORS = new Set(["and", "or", "mod", "div"]);
84
84
 
85
+ /** Token type of each OperatorName. */
86
+ const OPERATOR_TOKEN_TYPES = Object.freeze({
87
+ and: TokenType.AND,
88
+ or: TokenType.OR,
89
+ mod: TokenType.MOD,
90
+ div: TokenType.DIV,
91
+ });
92
+
85
93
  export class Token {
86
94
  constructor(type, value, position) {
87
95
  this.type = type;
@@ -145,6 +153,8 @@ export class XPathTokenizer {
145
153
  const lastToken = this.getLastToken();
146
154
  // No preceding token means we're at the start - not an operator context
147
155
  if (!lastToken) return false;
156
+ // The local part of a QName (`html:div`) is a name, never an operator
157
+ if (lastToken.type === TokenType.COLON) return false;
148
158
  // If preceding token is a "blocker", it's not an operator context
149
159
  if (OPERATOR_CONTEXT_BLOCKERS.has(lastToken.type)) return false;
150
160
  // Otherwise, it IS an operator context (after names, numbers, closing brackets, etc.)
@@ -296,9 +306,9 @@ export class XPathTokenizer {
296
306
  value += this.consume();
297
307
  }
298
308
 
299
- // Decimal part (handles both 1.5 and .5 style numbers)
300
- // For .5 style: entry condition ensures digit follows, so this branch handles it
301
- if (this.peek() === "." && /[0-9]/.test(this.peek(1))) {
309
+ // Decimal part: Number ::= Digits ('.' Digits?)? | '.' Digits, so `5.`
310
+ // is a number too. A `..` after digits is left alone (never valid there).
311
+ if (this.peek() === "." && this.peek(1) !== ".") {
302
312
  value += this.consume(); // .
303
313
  while (
304
314
  this.position < this.expression.length &&
@@ -311,6 +321,20 @@ export class XPathTokenizer {
311
321
  return new Token(TokenType.NUMBER, parseFloat(value), startPos);
312
322
  }
313
323
 
324
+ /**
325
+ * Read an NCName and classify it using the XPath 1.0 lexical
326
+ * disambiguation rules (section 3.7), applied in the order the
327
+ * specification lists them:
328
+ *
329
+ * 1. After an operand (the preceding token is not `@`, `::`, `(`, `[`, `,`
330
+ * or an operator) `and`, `or`, `div` and `mod` are OperatorNames, even
331
+ * when a `(` follows, as in `(a) or (b)`.
332
+ * 2. A name followed by `(` is a NodeType or a FunctionName.
333
+ * 3. A name followed by `::` is an AxisName.
334
+ * 4. Anything else is a NameTest.
335
+ *
336
+ * @returns {Token} The classified token
337
+ */
314
338
  readName() {
315
339
  const startPos = this.position;
316
340
  let value = "";
@@ -322,35 +346,25 @@ export class XPathTokenizer {
322
346
  value += this.consume();
323
347
  }
324
348
 
325
- // Check for axis name followed by ::
326
- this.skipWhitespace();
327
- if (AXIS_NAMES.has(value) && this.peek() === ":" && this.peek(1) === ":") {
328
- return new Token(TokenType.AXIS, value, startPos);
349
+ if (OPERATORS.has(value) && this.isOperatorContext()) {
350
+ return new Token(OPERATOR_TOKEN_TYPES[value], value, startPos);
329
351
  }
330
352
 
331
- // Check for function call (followed by '(')
332
353
  const savedPos = this.position;
333
354
  this.skipWhitespace();
355
+
334
356
  if (this.peek() === "(") {
335
- // Check if it's a node type test
336
- if (NODE_TYPES.has(value)) {
337
- return new Token(TokenType.NODE_TYPE, value, startPos);
338
- }
339
- return new Token(TokenType.FUNCTION, value, startPos);
357
+ const type = NODE_TYPES.has(value)
358
+ ? TokenType.NODE_TYPE
359
+ : TokenType.FUNCTION;
360
+ return new Token(type, value, startPos);
340
361
  }
341
- this.position = savedPos;
342
362
 
343
- // Check for operators - only in operator context per XPath 1.0 disambiguation rules
344
- if (OPERATORS.has(value) && this.isOperatorContext()) {
345
- const opTokens = {
346
- and: TokenType.AND,
347
- or: TokenType.OR,
348
- mod: TokenType.MOD,
349
- div: TokenType.DIV,
350
- };
351
- return new Token(opTokens[value], value, startPos);
363
+ if (AXIS_NAMES.has(value) && this.peek() === ":" && this.peek(1) === ":") {
364
+ return new Token(TokenType.AXIS, value, startPos);
352
365
  }
353
366
 
367
+ this.position = savedPos;
354
368
  return new Token(TokenType.NAME, value, startPos);
355
369
  }
356
370
 
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Named attribute sets (XSLT 1.0 section 7.1.4).
3
+ *
4
+ * Every `xsl:attribute-set` declaration is kept: declarations with the same
5
+ * expanded name are merged into one set. Applying a set applies its
6
+ * declarations from the lowest import precedence to the highest (stylesheet
7
+ * order within one precedence), each one its `use-attribute-sets` first and
8
+ * then its own `xsl:attribute` children. A later attribute of the same name
9
+ * replaces an earlier one, so attributes of a higher import precedence win
10
+ * over imported ones and a declaration's own attributes win over the sets it
11
+ * uses, as in libxslt.
12
+ *
13
+ * @module xslt/attributeSets
14
+ */
15
+
16
+ "use strict";
17
+
18
+ import {
19
+ requireExpandedName,
20
+ requireExpandedNames,
21
+ } from "./declarationNames.js";
22
+
23
+ /**
24
+ * @typedef {Object} AttributeSetDeclaration
25
+ * @property {Element} node - The xsl:attribute-set element
26
+ * @property {string[]} uses - Expanded names of its use-attribute-sets
27
+ * @property {number} importPrecedence - Import precedence of its stylesheet
28
+ */
29
+
30
+ /**
31
+ * Register an `xsl:attribute-set` declaration.
32
+ *
33
+ * @param {Object<string, AttributeSetDeclaration[]>} sets - Declarations by
34
+ * expanded name; updated in place
35
+ * @param {Element} node - The xsl:attribute-set element
36
+ * @param {number} importPrecedence - Import precedence of its stylesheet
37
+ * @returns {void}
38
+ * @throws {Error} When the name or a used name is not a QName with a
39
+ * declared prefix (libxslt rejects such a stylesheet)
40
+ */
41
+ export function registerAttributeSet(sets, node, importPrecedence) {
42
+ const key = requireExpandedName(
43
+ node.getAttribute("name") ?? "",
44
+ node,
45
+ "xsl:attribute-set name",
46
+ );
47
+ const uses = requireExpandedNames(
48
+ node.getAttribute("use-attribute-sets"),
49
+ node,
50
+ "xsl:attribute-set use-attribute-sets",
51
+ );
52
+ const declarations = Object.hasOwn(sets, key) ? sets[key] : [];
53
+ declarations.push({ node, uses, importPrecedence });
54
+ // Stable sort: stylesheet order is kept within one import precedence
55
+ declarations.sort((a, b) => a.importPrecedence - b.importPrecedence);
56
+ sets[key] = declarations;
57
+ }
58
+
59
+ /**
60
+ * Apply attribute sets to a result element.
61
+ *
62
+ * @param {Object<string, AttributeSetDeclaration[]>} sets - Declarations by expanded name
63
+ * @param {string[]} keys - Expanded names of the sets to apply, in order
64
+ * @param {(node: Element) => void} applyDeclaration - Instantiates the
65
+ * xsl:attribute children of one declaration
66
+ * @param {Set<string>} [active] - Sets being applied (recursion guard)
67
+ * @returns {void}
68
+ * @throws {Error} When a set uses itself, directly or indirectly (an error
69
+ * in XSLT 1.0 section 7.1.4)
70
+ *
71
+ * @example
72
+ * applyAttributeSets(engine.attributeSets, ["common"], (node) =>
73
+ * engine.processChildren(node, context, element));
74
+ */
75
+ export function applyAttributeSets(
76
+ sets,
77
+ keys,
78
+ applyDeclaration,
79
+ active = new Set(),
80
+ ) {
81
+ for (const key of keys) {
82
+ if (!Object.hasOwn(sets, key)) continue;
83
+ if (active.has(key)) {
84
+ throw new Error(
85
+ `xsl:attribute-set ${key} uses itself (XSLT 1.0 section 7.1.4)`,
86
+ );
87
+ }
88
+ active.add(key);
89
+ for (const declaration of sets[key]) {
90
+ applyAttributeSets(sets, declaration.uses, applyDeclaration, active);
91
+ applyDeclaration(declaration.node);
92
+ }
93
+ active.delete(key);
94
+ }
95
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Attribute value templates (XSLT 1.0 section 7.6.2).
3
+ *
4
+ * An AVT is literal text with XPath expressions in curly braces. A brace
5
+ * inside a string literal of an expression is part of the literal, so the
6
+ * template is split with a small scanner that knows '...' and "..." literals;
7
+ * `{{` and `}}` outside expressions stand for single braces. A lone `}` in the
8
+ * literal text is an error in the recommendation; it is kept as a character
9
+ * instead, like browsers do. Parsed templates are cached per string.
10
+ *
11
+ * @module xslt/avt
12
+ */
13
+
14
+ "use strict";
15
+
16
+ /**
17
+ * @typedef {string | {expr: string}} AvtPart
18
+ * A literal text part, or an expression part.
19
+ */
20
+
21
+ const cache = new Map();
22
+
23
+ /**
24
+ * Find the `}` closing an expression that starts at `start`.
25
+ *
26
+ * @param {string} value - The whole template
27
+ * @param {number} start - Index just after the opening `{`
28
+ * @returns {number} Index of the closing `}`
29
+ * @throws {Error} When the expression is not closed
30
+ */
31
+ function closingBrace(value, start) {
32
+ let quote = null;
33
+ for (let i = start; i < value.length; i++) {
34
+ const char = value[i];
35
+ if (quote) {
36
+ if (char === quote) quote = null;
37
+ } else if (char === "'" || char === '"') {
38
+ quote = char;
39
+ } else if (char === "}") {
40
+ return i;
41
+ }
42
+ }
43
+ throw new Error(`Unclosed expression in attribute value template: ${value}`);
44
+ }
45
+
46
+ /**
47
+ * Split an attribute value template into literal and expression parts.
48
+ *
49
+ * @param {string} value - The attribute value
50
+ * @returns {AvtPart[]} The parts, adjacent literals merged
51
+ * @throws {Error} When an expression is not closed
52
+ *
53
+ * @example
54
+ * parseAvt("a{@b}c"); // ["a", { expr: "@b" }, "c"]
55
+ */
56
+ export function parseAvt(value) {
57
+ let parts = cache.get(value);
58
+ if (parts) return parts;
59
+
60
+ parts = [];
61
+ let text = "";
62
+ let i = 0;
63
+ while (i < value.length) {
64
+ const char = value[i];
65
+ if ((char === "{" || char === "}") && value[i + 1] === char) {
66
+ text += char;
67
+ i += 2;
68
+ } else if (char === "{") {
69
+ const end = closingBrace(value, i + 1);
70
+ if (text) parts.push(text);
71
+ text = "";
72
+ parts.push({ expr: value.slice(i + 1, end) });
73
+ i = end + 1;
74
+ } else {
75
+ text += char;
76
+ i++;
77
+ }
78
+ }
79
+ if (text) parts.push(text);
80
+
81
+ cache.set(value, parts);
82
+ return parts;
83
+ }
84
+
85
+ /**
86
+ * Evaluate an attribute value template.
87
+ *
88
+ * @param {string} value - The attribute value
89
+ * @param {(expr: string) => string} evaluate - Evaluates one expression to a string
90
+ * @returns {string} The resulting text
91
+ *
92
+ * @example
93
+ * evaluateAvt("{1+1}px", (e) => String(eval(e))); // "2px"
94
+ */
95
+ export function evaluateAvt(value, evaluate) {
96
+ if (!value.includes("{") && !value.includes("}")) return value;
97
+
98
+ let result = "";
99
+ for (const part of parseAvt(value)) {
100
+ result += typeof part === "string" ? part : evaluate(part.expr);
101
+ }
102
+ return result;
103
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Validation of the names computed by `xsl:element` and `xsl:attribute`
3
+ * (XSLT 1.0 sections 7.1.2 and 7.1.3).
4
+ *
5
+ * The `name` attribute is an attribute value template, so its value is only
6
+ * known at run time. It must be a QName whose prefix (when there is no
7
+ * `namespace` attribute) is declared in scope on the instruction, and an
8
+ * attribute may not be called `xmlns` (nor use the `xmlns` prefix). Like
9
+ * libxslt, the processor recovers from these errors by reporting them and not
10
+ * creating the element (with its content) or the attribute.
11
+ *
12
+ * @module xslt/computedNames
13
+ */
14
+
15
+ "use strict";
16
+
17
+ import { isQName } from "./qname.js";
18
+ import { attributeName, elementName, splitQName } from "./resultNamespaces.js";
19
+ import { resolvePrefix } from "./stylesheetNamespaces.js";
20
+
21
+ /**
22
+ * @typedef {object} ComputedName
23
+ * @property {{namespaceUri: (string|null), qname: string}} [name] - The
24
+ * expanded name, when valid
25
+ * @property {string} [error] - Why no node can be created, when invalid
26
+ */
27
+
28
+ /**
29
+ * Check the parts shared by element and attribute names.
30
+ *
31
+ * @param {string} kind - "element" or "attribute", used in messages
32
+ * @param {string} qname - The evaluated `name` attribute
33
+ * @param {string|null} namespace - The evaluated `namespace` attribute, null when absent
34
+ * @param {Object<string, string>} scope - Namespaces in scope on the instruction
35
+ * @returns {string|null} The error message, or null when the name is usable
36
+ */
37
+ function nameError(kind, qname, namespace, scope) {
38
+ if (!isQName(qname)) {
39
+ return `xsl:${kind}: "${qname}" is not a valid ${kind} name (QName), the ${kind} is not created`;
40
+ }
41
+ const { prefix } = splitQName(qname);
42
+ if (namespace === null && prefix && !resolvePrefix(scope, prefix)) {
43
+ return `xsl:${kind}: undefined namespace prefix "${prefix}" in "${qname}", the ${kind} is not created`;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * Resolve the name computed by `xsl:element`.
50
+ *
51
+ * @param {string} qname - The evaluated `name` attribute
52
+ * @param {string|null} namespace - The evaluated `namespace` attribute, null when absent
53
+ * @param {Object<string, string>} scope - Namespaces in scope on the instruction
54
+ * @returns {ComputedName} The expanded name, or the error to report
55
+ *
56
+ * @example
57
+ * computedElementName("p:e", null, { p: "urn:p" }).name;
58
+ * // { namespaceUri: "urn:p", qname: "p:e" }
59
+ * computedElementName("x{", null, {}).error; // 'xsl:element: "x{" is not ...'
60
+ */
61
+ export function computedElementName(qname, namespace, scope) {
62
+ const error = nameError("element", qname, namespace, scope);
63
+ return error ? { error } : { name: elementName(qname, namespace, scope) };
64
+ }
65
+
66
+ /**
67
+ * Resolve the name computed by `xsl:attribute`; `xmlns` and `xmlns:*` are
68
+ * rejected because namespace declarations are not attributes.
69
+ *
70
+ * @param {string} qname - The evaluated `name` attribute
71
+ * @param {string|null} namespace - The evaluated `namespace` attribute, null when absent
72
+ * @param {Object<string, string>} scope - Namespaces in scope on the instruction
73
+ * @returns {ComputedName} The expanded name, or the error to report
74
+ *
75
+ * @example
76
+ * computedAttributeName("xmlns", null, {}).error;
77
+ * // 'xsl:attribute: "xmlns" cannot be used as an attribute name ...'
78
+ */
79
+ export function computedAttributeName(qname, namespace, scope) {
80
+ // With a namespace attribute, the prefix of `xmlns:a` is only a hint and
81
+ // is replaced (libxslt REC/test-7.1.3)
82
+ const xmlnsName =
83
+ qname === "xmlns" || (namespace === null && qname.startsWith("xmlns:"));
84
+ if (xmlnsName) {
85
+ return {
86
+ error: `xsl:attribute: "${qname}" cannot be used as an attribute name (XSLT 1.0 section 7.1.3), the attribute is not created`,
87
+ };
88
+ }
89
+ const error = nameError("attribute", qname, namespace, scope);
90
+ return error ? { error } : { name: attributeName(qname, namespace, scope) };
91
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Copying source nodes to the result tree: `xsl:copy-of` (XSLT 1.0 section
3
+ * 11.3) and the node kinds of `xsl:copy` (section 7.5).
4
+ *
5
+ * Copies follow the XPath data model rather than the raw DOM: a run of
6
+ * adjacent Text/CDATA nodes is one text node (its first DOM node stands for
7
+ * the run), copying a root node copies its children, and copying an attribute
8
+ * or a namespace node adds it to the result element being built. Names keep their namespace and
9
+ * elements keep their namespace declarations.
10
+ *
11
+ * @module xslt/copying
12
+ */
13
+
14
+ "use strict";
15
+
16
+ import { childAxis } from "../xpath/axes.js";
17
+ import { NAMESPACE_NODE, inScopeBindings } from "../xpath/namespaceNodes.js";
18
+ import { copyNamespaceDeclarations } from "./resultNamespaces.js";
19
+ import { XMLNS_NAMESPACE } from "./stylesheetNamespaces.js";
20
+
21
+ /**
22
+ * Copy an attribute node onto a result element. A namespace declaration or a
23
+ * target that is not an element is ignored; so is an attribute added after
24
+ * the element got children (see {@link canAddAttribute}).
25
+ *
26
+ * @param {Attr} attribute - The source attribute
27
+ * @param {Node} target - The result node receiving it
28
+ * @param {(element: Node) => boolean} canAddAttribute - Guard for late attributes
29
+ * @returns {void}
30
+ */
31
+ export function copyAttribute(attribute, target, canAddAttribute) {
32
+ if (attribute.namespaceURI === XMLNS_NAMESPACE) return;
33
+ if (!canAddAttribute(target)) return;
34
+ if (attribute.namespaceURI) {
35
+ target.setAttributeNS(
36
+ attribute.namespaceURI,
37
+ attribute.name,
38
+ attribute.value,
39
+ );
40
+ } else {
41
+ target.setAttribute(attribute.name, attribute.value);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Copy a namespace node onto a result element as a namespace declaration
47
+ * (XSLT 1.0 sections 7.5 and 11.3). As in libxslt, nothing is declared for
48
+ * the implicit `xml` binding, for a prefix the element's own name binds to
49
+ * another namespace, for a prefix the element already declares differently,
50
+ * or when the element already has children (see `canAddAttribute`).
51
+ *
52
+ * @param {import('../xpath/namespaceNodes.js').NamespaceNode} namespace - The namespace node
53
+ * @param {Node} target - The result node receiving it
54
+ * @param {(element: Node) => boolean} canAddNamespace - Guard for late nodes
55
+ * @returns {void}
56
+ *
57
+ * @example
58
+ * copyNamespaceNode(nsNode, resultElement, () => true); // xmlns:a="urn:a"
59
+ */
60
+ export function copyNamespaceNode(namespace, target, canAddNamespace) {
61
+ const prefix = namespace.localName;
62
+ if (prefix === "xml" || !canAddNamespace(target)) return;
63
+ const uri = namespace.nodeValue;
64
+ if ((target.prefix ?? "") === prefix && (target.namespaceURI ?? "") !== uri) {
65
+ return;
66
+ }
67
+ const declared = target.getAttributeNS(XMLNS_NAMESPACE, prefix || "xmlns");
68
+ if (declared !== null && declared !== uri) return;
69
+ target.setAttributeNS(
70
+ XMLNS_NAMESPACE,
71
+ prefix ? `xmlns:${prefix}` : "xmlns",
72
+ uri,
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Create a shallow copy of an element: same expanded name and prefix, and the
78
+ * same namespace declarations.
79
+ *
80
+ * @param {Element} element - The source element
81
+ * @param {Document} doc - The result document
82
+ * @returns {Element} The empty copy
83
+ */
84
+ export function shallowCopyElement(element, doc) {
85
+ const copy = element.namespaceURI
86
+ ? doc.createElementNS(element.namespaceURI, element.nodeName)
87
+ : doc.createElement(element.nodeName);
88
+ copyNamespaceDeclarations(element, copy);
89
+ return copy;
90
+ }
91
+
92
+ /**
93
+ * Deep copy of a node that can be a child in the result tree.
94
+ *
95
+ * @param {Node} node - Element, text, CDATA, comment, PI or document fragment
96
+ * @param {Document} doc - The result document
97
+ * @param {(node: Node) => string} stringValue - XPath string value (text runs)
98
+ * @returns {Node|null} The copy, or null for nodes that cannot be children
99
+ *
100
+ * @example
101
+ * cloneNode(sourceElement, resultDocument, (n) => evaluator.getStringValue(n));
102
+ */
103
+ export function cloneNode(node, doc, stringValue) {
104
+ switch (node.nodeType) {
105
+ case 1: {
106
+ const copy = shallowCopyElement(node, doc);
107
+ for (const attribute of node.attributes) {
108
+ copyAttribute(attribute, copy, () => true);
109
+ }
110
+ appendChildCopies(node, copy, doc, stringValue);
111
+ return copy;
112
+ }
113
+ case 3:
114
+ case 4:
115
+ return doc.createTextNode(stringValue(node));
116
+ case 7:
117
+ return doc.createProcessingInstruction(node.target, node.data);
118
+ case 8:
119
+ return doc.createComment(node.nodeValue);
120
+ case 11: {
121
+ const fragment = doc.createDocumentFragment();
122
+ appendChildCopies(node, fragment, doc, stringValue);
123
+ return fragment;
124
+ }
125
+ default:
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Append deep copies of the (XPath) children of a node.
132
+ *
133
+ * @param {Node} node - The source parent
134
+ * @param {Node} target - The result parent
135
+ * @param {Document} doc - The result document
136
+ * @param {(node: Node) => string} stringValue - XPath string value
137
+ * @returns {void}
138
+ */
139
+ function appendChildCopies(node, target, doc, stringValue) {
140
+ for (const child of childAxis(node)) {
141
+ const copy = cloneNode(child, doc, stringValue);
142
+ if (copy) target.appendChild(copy);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Declare on the copy of an element every namespace in scope on the source
148
+ * element that is not in scope on the result parent already: `xsl:copy-of`
149
+ * copies the namespace nodes of an element (XSLT 1.0 section 11.3), those
150
+ * declared on its ancestors included, as libxslt does for the top element of
151
+ * a copied tree (its descendants inherit them in the result).
152
+ *
153
+ * @param {Element} source - The copied source element
154
+ * @param {Element} copy - Its copy, not yet attached
155
+ * @param {Node} output - The result node receiving the copy
156
+ * @returns {void}
157
+ */
158
+ function copyInScopeNamespaces(source, copy, output) {
159
+ const inOutput = (prefix, uri) =>
160
+ output.nodeType === 1 && output.lookupNamespaceURI(prefix || null) === uri;
161
+ for (const [prefix, uri] of inScopeBindings(source)) {
162
+ if (!inOutput(prefix, uri)) {
163
+ copyNamespaceNode(
164
+ { localName: prefix, nodeValue: uri },
165
+ copy,
166
+ () => true,
167
+ );
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Copy the result of an `xsl:copy-of` select expression to the result tree.
174
+ *
175
+ * Node-sets are copied node by node (roots as their children, attributes onto
176
+ * the current result element); any other value is written as text.
177
+ *
178
+ * @param {*} value - The evaluated expression
179
+ * @param {Node} output - The result node receiving the copy
180
+ * @param {object} host - Engine services
181
+ * @param {Document} host.doc - The result document
182
+ * @param {(node: Node) => string} host.stringValue - XPath string value
183
+ * @param {(value: *) => string} host.toString - XPath string() conversion
184
+ * @param {(element: Node) => boolean} host.canAddAttribute - Guard for attributes
185
+ * @returns {void}
186
+ */
187
+ export function copyOf(value, output, host) {
188
+ if (Array.isArray(value)) {
189
+ for (const item of value) copyOf(item, output, host);
190
+ return;
191
+ }
192
+
193
+ if (value === null || value === undefined) return;
194
+ if (!value.nodeType) {
195
+ const text = host.toString(value);
196
+ if (text) output.appendChild(host.doc.createTextNode(text));
197
+ return;
198
+ }
199
+
200
+ if (value.nodeType === 2) {
201
+ copyAttribute(value, output, host.canAddAttribute);
202
+ } else if (value.nodeType === NAMESPACE_NODE) {
203
+ copyNamespaceNode(value, output, host.canAddAttribute);
204
+ } else if (value.nodeType === 9 || value.nodeType === 11) {
205
+ // Children one by one: xmldom mishandles appending a DocumentFragment
206
+ appendChildCopies(value, output, host.doc, host.stringValue);
207
+ } else {
208
+ const copy = cloneNode(value, host.doc, host.stringValue);
209
+ if (value.nodeType === 1) copyInScopeNamespaces(value, copy, output);
210
+ if (copy) output.appendChild(copy);
211
+ }
212
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Names of named stylesheet objects (XSLT 1.0 section 2.4).
3
+ *
4
+ * Named templates, attribute sets and decimal formats are named by QNames
5
+ * that are expanded with the namespace declarations in scope on the element
6
+ * carrying the name; the default namespace is not used for unprefixed names.
7
+ * Two names are the same when their expanded names are, whatever prefixes
8
+ * they use, so every table of such objects is keyed by the expanded name in
9
+ * Clark notation (`{uri}local`, or the bare local name in no namespace).
10
+ *
11
+ * @module xslt/declarationNames
12
+ */
13
+
14
+ "use strict";
15
+
16
+ import { isQName } from "./qname.js";
17
+ import { splitQName } from "./resultNamespaces.js";
18
+ import { inScopeNamespaces, resolvePrefix } from "./stylesheetNamespaces.js";
19
+ import { expandedNameKey } from "./serializer/settings.js";
20
+
21
+ /**
22
+ * Expand a QName-valued attribute of a stylesheet element.
23
+ *
24
+ * @param {string} qname - The QName, e.g. `p:format` or `common`
25
+ * @param {Element} element - The element in whose scope the prefix resolves
26
+ * @returns {{key: string}|{error: string}} The expanded name key, or why the
27
+ * value is not a QName with a declared prefix
28
+ *
29
+ * @example
30
+ * // <xsl:decimal-format xmlns:p="urn:p" name="p:f"/>
31
+ * expandName("p:f", element); // { key: "{urn:p}f" }
32
+ * expandName("q:f", element); // { error: 'undeclared namespace prefix "q" in "q:f"' }
33
+ */
34
+ export function expandName(qname, element) {
35
+ if (!isQName(qname)) return { error: `invalid QName "${qname}"` };
36
+ const { prefix, localName } = splitQName(qname);
37
+ if (!prefix) return { key: localName };
38
+ const namespaceUri = resolvePrefix(inScopeNamespaces(element), prefix);
39
+ if (!namespaceUri) {
40
+ return { error: `undeclared namespace prefix "${prefix}" in "${qname}"` };
41
+ }
42
+ return { key: expandedNameKey(namespaceUri, localName) };
43
+ }
44
+
45
+ /**
46
+ * Expand a QName that must be valid, failing with a message naming where it
47
+ * comes from.
48
+ *
49
+ * @param {string} qname - The QName
50
+ * @param {Element} element - The element in whose scope the prefix resolves
51
+ * @param {string} where - The attribute holding it, e.g. "xsl:template name"
52
+ * @returns {string} The expanded name key
53
+ * @throws {Error} When the value is not a QName or its prefix is undeclared
54
+ *
55
+ * @example
56
+ * requireExpandedName("p:t", templateElement, "xsl:template name"); // "{urn:p}t"
57
+ */
58
+ export function requireExpandedName(qname, element, where) {
59
+ const expanded = expandName(qname, element);
60
+ if (expanded.error) throw new Error(`${where}: ${expanded.error}`);
61
+ return expanded.key;
62
+ }
63
+
64
+ /**
65
+ * Expand the whitespace separated QNames of an attribute such as
66
+ * `use-attribute-sets`.
67
+ *
68
+ * @param {string|null} value - The attribute value
69
+ * @param {Element} element - The element carrying the attribute
70
+ * @param {string} where - Attribute description for error messages
71
+ * @returns {string[]} The expanded name keys, in order
72
+ * @throws {Error} When one of the names is not a QName with a declared prefix
73
+ */
74
+ export function requireExpandedNames(value, element, where) {
75
+ if (!value) return [];
76
+ return value
77
+ .split(/[ \t\r\n]+/)
78
+ .filter(Boolean)
79
+ .map((qname) => requireExpandedName(qname, element, where));
80
+ }