@tradik/xslt-processor 1.0.2 → 1.1.1

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 (47) hide show
  1. package/README.md +292 -47
  2. package/bin/lib/options.js +114 -0
  3. package/bin/lib/paths.js +186 -0
  4. package/bin/lib/transform.js +115 -0
  5. package/bin/xslt.js +68 -162
  6. package/dist/xslt-processor.browser.js +2073 -163
  7. package/dist/xslt-processor.browser.js.map +4 -4
  8. package/dist/xslt-processor.browser.min.js +6 -2
  9. package/dist/xslt-processor.browser.min.js.map +4 -4
  10. package/dist/xslt-processor.cjs +2077 -162
  11. package/dist/xslt-processor.cjs.map +4 -4
  12. package/dist/xslt-processor.d.cts +299 -0
  13. package/dist/xslt-processor.d.ts +92 -4
  14. package/dist/xslt-processor.js +2072 -161
  15. package/dist/xslt-processor.js.map +4 -4
  16. package/package.json +27 -16
  17. package/src/XSLTProcessor.js +177 -8
  18. package/src/index.js +11 -5
  19. package/src/xpath/evaluator.js +48 -7
  20. package/src/xslt/elements.js +57 -0
  21. package/src/xslt/engine.js +474 -185
  22. package/src/xslt/formatNumber.js +220 -0
  23. package/src/xslt/functions.js +191 -0
  24. package/src/xslt/index.js +31 -0
  25. package/src/xslt/keys.js +141 -0
  26. package/src/xslt/literalResult.js +167 -0
  27. package/src/xslt/number.js +178 -0
  28. package/src/xslt/numberFormat.js +155 -0
  29. package/src/xslt/resultTree.js +74 -0
  30. package/src/xslt/serializer/baseWriter.js +283 -0
  31. package/src/xslt/serializer/constants.js +78 -0
  32. package/src/xslt/serializer/escape.js +98 -0
  33. package/src/xslt/serializer/htmlSerializer.js +141 -0
  34. package/src/xslt/serializer/indent.js +51 -0
  35. package/src/xslt/serializer/namespaces.js +68 -0
  36. package/src/xslt/serializer/rawText.js +41 -0
  37. package/src/xslt/serializer/settings.js +103 -0
  38. package/src/xslt/serializer/textSerializer.js +29 -0
  39. package/src/xslt/serializer/xmlSerializer.js +127 -0
  40. package/src/xslt/serializer.js +57 -0
  41. package/src/xslt/templatePriority.js +45 -0
  42. package/src/xslt/uri.js +68 -0
  43. package/src/xslt/whitespace.js +184 -0
  44. package/src/XSLTProcessor.test.js +0 -930
  45. package/src/xpath/evaluator.test.js +0 -1852
  46. package/src/xpath/tokenizer.test.js +0 -224
  47. package/src/xslt/engine.test.js +0 -3130
@@ -781,12 +781,22 @@ var FORBIDDEN_VARIABLE_NAMES = Object.freeze([
781
781
  "__lookupSetter__"
782
782
  ]);
783
783
  var XPathContext = class _XPathContext {
784
- constructor(node, position = 1, size = 1, variables = {}, namespaces = {}) {
784
+ /**
785
+ * @param {Node} node - The context node
786
+ * @param {number} [position] - The context position (1-based)
787
+ * @param {number} [size] - The context size
788
+ * @param {Object} [variables] - Variable bindings by name
789
+ * @param {Object} [namespaces] - Namespace bindings by prefix
790
+ * @param {*} [hostContext] - Opaque context of the host language (XSLT),
791
+ * carried through unchanged so host defined functions can reach it
792
+ */
793
+ constructor(node, position = 1, size = 1, variables = {}, namespaces = {}, hostContext = null) {
785
794
  this.node = node;
786
795
  this.position = position;
787
796
  this.size = size;
788
797
  this.variables = variables;
789
798
  this.namespaces = namespaces;
799
+ this.hostContext = hostContext;
790
800
  }
791
801
  clone(overrides = {}) {
792
802
  return new _XPathContext(
@@ -794,13 +804,17 @@ var XPathContext = class _XPathContext {
794
804
  overrides.position ?? this.position,
795
805
  overrides.size ?? this.size,
796
806
  overrides.variables ?? this.variables,
797
- overrides.namespaces ?? this.namespaces
807
+ overrides.namespaces ?? this.namespaces,
808
+ overrides.hostContext ?? this.hostContext
798
809
  );
799
810
  }
800
811
  };
801
812
  var XPathEvaluator = class {
802
813
  constructor(options = {}) {
803
- this.functions = this.initCoreFunctions();
814
+ this.functions = Object.assign(
815
+ /* @__PURE__ */ Object.create(null),
816
+ this.initCoreFunctions()
817
+ );
804
818
  this.maxRecursionDepth = options.maxRecursionDepth ?? XPathLimits.MAX_RECURSION_DEPTH;
805
819
  this.maxResultSize = options.maxResultSize ?? XPathLimits.MAX_RESULT_SIZE;
806
820
  this.maxStringLength = options.maxStringLength ?? XPathLimits.MAX_STRING_LENGTH;
@@ -1150,7 +1164,7 @@ var XPathEvaluator = class {
1150
1164
  case "node":
1151
1165
  return true;
1152
1166
  case "text":
1153
- return node.nodeType === 3;
1167
+ return node.nodeType === 3 || node.nodeType === 4;
1154
1168
  case "comment":
1155
1169
  return node.nodeType === 8;
1156
1170
  case "processing-instruction":
@@ -1191,13 +1205,31 @@ var XPathEvaluator = class {
1191
1205
  }
1192
1206
  return context.variables[name];
1193
1207
  }
1208
+ /**
1209
+ * Register additional functions, for example the XSLT function library.
1210
+ *
1211
+ * Existing names are overwritten, so a host language can also specialise a
1212
+ * core function. Each function is called as `fn(args, context)` with the
1213
+ * evaluator as `this`.
1214
+ *
1215
+ * @param {Object<string, Function>} functions - Functions by name
1216
+ * @returns {XPathEvaluator} This evaluator, to allow chaining
1217
+ *
1218
+ * @example
1219
+ * evaluator.registerFunctions({ 'my:double': (args, ctx) => 2 });
1220
+ */
1221
+ registerFunctions(functions) {
1222
+ for (const [name, fn] of Object.entries(functions)) {
1223
+ this.functions[name] = fn;
1224
+ }
1225
+ return this;
1226
+ }
1194
1227
  evalFunctionCall(ast, context) {
1195
1228
  const name = ast.prefix ? `${ast.prefix}:${ast.name}` : ast.name;
1196
- const fn = this.functions[name];
1197
- if (!fn) {
1229
+ if (!Object.hasOwn(this.functions, name)) {
1198
1230
  throw new Error(`Unknown function: ${name}`);
1199
1231
  }
1200
- return fn.call(this, ast.args, context);
1232
+ return this.functions[name].call(this, ast.args, context);
1201
1233
  }
1202
1234
  // Type conversion functions
1203
1235
  toBoolean(value) {
@@ -1254,7 +1286,7 @@ var XPathEvaluator = class {
1254
1286
  case 11: {
1255
1287
  let text = "";
1256
1288
  const walker = (n) => {
1257
- if (n.nodeType === 3) {
1289
+ if (n.nodeType === 3 || n.nodeType === 4) {
1258
1290
  text += n.nodeValue || "";
1259
1291
  } else if (n.childNodes) {
1260
1292
  for (const child of n.childNodes) {
@@ -1559,8 +1591,1486 @@ var XPathEvaluator = class {
1559
1591
  }
1560
1592
  };
1561
1593
 
1594
+ // src/xslt/elements.js
1595
+ var XSLT_NAMESPACE = "http://www.w3.org/1999/XSL/Transform";
1596
+ var XSLT_ELEMENTS = Object.freeze([
1597
+ "apply-imports",
1598
+ "apply-templates",
1599
+ "attribute",
1600
+ "call-template",
1601
+ "choose",
1602
+ "comment",
1603
+ "copy",
1604
+ "copy-of",
1605
+ "element",
1606
+ "fallback",
1607
+ "for-each",
1608
+ "if",
1609
+ "message",
1610
+ "number",
1611
+ "otherwise",
1612
+ "param",
1613
+ "processing-instruction",
1614
+ "sort",
1615
+ "text",
1616
+ "value-of",
1617
+ "variable",
1618
+ "when",
1619
+ "with-param"
1620
+ ]);
1621
+ var ELEMENT_SET = new Set(XSLT_ELEMENTS);
1622
+ function isXsltElementAvailable(localName) {
1623
+ return ELEMENT_SET.has(localName);
1624
+ }
1625
+
1626
+ // src/xslt/formatNumber.js
1627
+ var DEFAULT_DECIMAL_FORMAT = Object.freeze({
1628
+ decimalSeparator: ".",
1629
+ groupingSeparator: ",",
1630
+ percent: "%",
1631
+ perMille: "\u2030",
1632
+ zeroDigit: "0",
1633
+ digit: "#",
1634
+ patternSeparator: ";",
1635
+ infinity: "Infinity",
1636
+ nan: "NaN",
1637
+ minusSign: "-"
1638
+ });
1639
+ function splitSubPatterns(pattern, format) {
1640
+ const index = pattern.indexOf(format.patternSeparator);
1641
+ if (index === -1) return { positive: pattern, negative: null };
1642
+ return {
1643
+ positive: pattern.substring(0, index),
1644
+ negative: pattern.substring(index + format.patternSeparator.length)
1645
+ };
1646
+ }
1647
+ function parseSubPattern(subPattern, format) {
1648
+ const special = /* @__PURE__ */ new Set([
1649
+ format.digit,
1650
+ format.zeroDigit,
1651
+ format.groupingSeparator,
1652
+ format.decimalSeparator
1653
+ ]);
1654
+ let start = 0;
1655
+ while (start < subPattern.length && !special.has(subPattern[start])) start++;
1656
+ let end = start;
1657
+ while (end < subPattern.length && special.has(subPattern[end])) end++;
1658
+ const prefix = subPattern.substring(0, start);
1659
+ const numeric = subPattern.substring(start, end);
1660
+ const suffix = subPattern.substring(end);
1661
+ const decimalIndex = numeric.indexOf(format.decimalSeparator);
1662
+ const integerPart = decimalIndex === -1 ? numeric : numeric.substring(0, decimalIndex);
1663
+ const fractionPart = decimalIndex === -1 ? "" : numeric.substring(decimalIndex + 1);
1664
+ const groupingIndex = integerPart.lastIndexOf(format.groupingSeparator);
1665
+ const affixes = prefix + suffix;
1666
+ let multiplier = 1;
1667
+ if (affixes.includes(format.percent)) multiplier = 100;
1668
+ else if (affixes.includes(format.perMille)) multiplier = 1e3;
1669
+ return {
1670
+ prefix,
1671
+ suffix,
1672
+ multiplier,
1673
+ minInteger: countOccurrences(integerPart, format.zeroDigit),
1674
+ minFraction: countOccurrences(fractionPart, format.zeroDigit),
1675
+ maxFraction: Math.min(fractionPart.length, 100),
1676
+ groupingSize: groupingIndex === -1 ? 0 : integerPart.length - groupingIndex - 1
1677
+ };
1678
+ }
1679
+ function countOccurrences(text, char) {
1680
+ let total = 0;
1681
+ for (const current of text) {
1682
+ if (current === char) total++;
1683
+ }
1684
+ return total;
1685
+ }
1686
+ function applyGrouping(digits, size, separator) {
1687
+ if (size <= 0 || digits.length <= size) return digits;
1688
+ let result = "";
1689
+ for (let i = 0; i < digits.length; i++) {
1690
+ const fromEnd = digits.length - i;
1691
+ if (i > 0 && fromEnd % size === 0) result += separator;
1692
+ result += digits[i];
1693
+ }
1694
+ return result;
1695
+ }
1696
+ function translateDigits(text, zeroDigit) {
1697
+ const offset = zeroDigit.codePointAt(0) - 48;
1698
+ if (offset === 0) return text;
1699
+ return text.replaceAll(
1700
+ /\d/g,
1701
+ (digit) => String.fromCodePoint(digit.codePointAt(0) + offset)
1702
+ );
1703
+ }
1704
+ function formatMagnitude(magnitude, spec, format) {
1705
+ const fixed = magnitude.toFixed(spec.maxFraction);
1706
+ const [rawInteger, rawFraction = ""] = fixed.split(".");
1707
+ let fraction = rawFraction;
1708
+ while (fraction.length > spec.minFraction && fraction.endsWith("0")) {
1709
+ fraction = fraction.slice(0, -1);
1710
+ }
1711
+ let integer = rawInteger.padStart(spec.minInteger, "0");
1712
+ if (spec.minInteger === 0 && integer === "0" && fraction.length > 0) {
1713
+ integer = "";
1714
+ }
1715
+ integer = applyGrouping(integer, spec.groupingSize, format.groupingSeparator);
1716
+ const body = fraction.length > 0 ? integer + format.decimalSeparator + fraction : integer;
1717
+ return translateDigits(body, format.zeroDigit);
1718
+ }
1719
+ function formatNumber(value, pattern, decimalFormat = DEFAULT_DECIMAL_FORMAT) {
1720
+ const format = { ...DEFAULT_DECIMAL_FORMAT, ...decimalFormat };
1721
+ if (typeof value !== "number" || Number.isNaN(value)) return format.nan;
1722
+ const subPatterns = splitSubPatterns(pattern, format);
1723
+ const positive = parseSubPattern(subPatterns.positive, format);
1724
+ const isNegative = value < 0;
1725
+ let spec = positive;
1726
+ let prefix = positive.prefix;
1727
+ let suffix = positive.suffix;
1728
+ if (isNegative) {
1729
+ if (subPatterns.negative !== null) {
1730
+ spec = parseSubPattern(subPatterns.negative, format);
1731
+ prefix = spec.prefix;
1732
+ suffix = spec.suffix;
1733
+ } else {
1734
+ prefix = format.minusSign + positive.prefix;
1735
+ }
1736
+ }
1737
+ const magnitude = Math.abs(value) * spec.multiplier;
1738
+ const body = Number.isFinite(magnitude) ? formatMagnitude(magnitude, spec, format) : format.infinity;
1739
+ return prefix + body + suffix;
1740
+ }
1741
+
1742
+ // src/xslt/functions.js
1743
+ var VENDOR = "@tradik/xslt-processor";
1744
+ var VENDOR_URL = "https://github.com/spagu/XSLT-Processor";
1745
+ var SYSTEM_PROPERTIES = Object.freeze({
1746
+ "xsl:version": "1",
1747
+ "xsl:vendor": VENDOR,
1748
+ "xsl:vendor-url": VENDOR_URL
1749
+ });
1750
+ function ownerDocumentOf(node) {
1751
+ return node.ownerDocument || node;
1752
+ }
1753
+ function toStringList(evaluator, stringify, value) {
1754
+ if (Array.isArray(value)) {
1755
+ return value.map((node) => evaluator.getStringValue(node));
1756
+ }
1757
+ return [stringify(value)];
1758
+ }
1759
+ function splitQName(qname) {
1760
+ const colon = qname.indexOf(":");
1761
+ if (colon === -1) return { prefix: null, localName: qname };
1762
+ return {
1763
+ prefix: qname.substring(0, colon),
1764
+ localName: qname.substring(colon + 1)
1765
+ };
1766
+ }
1767
+ function createXsltFunctions(engine) {
1768
+ const evaluator = engine.xpathEvaluator;
1769
+ const stringify = evaluator.toString.bind(evaluator);
1770
+ const evaluate2 = (arg, ctx) => evaluator.evaluate(arg, ctx);
1771
+ const asString = (arg, ctx) => stringify(evaluate2(arg, ctx));
1772
+ return {
1773
+ /**
1774
+ * `document(object, base?)` - load external XML documents.
1775
+ *
1776
+ * An empty URI denotes the stylesheet itself. Without a document loader, or
1777
+ * when the loader returns null, the result is an empty node-set. The
1778
+ * optional second argument is read as a base URI string.
1779
+ */
1780
+ document: (args, ctx) => {
1781
+ const baseUri = args.length > 1 ? asString(args[1], ctx) : engine.baseUri;
1782
+ const uris = toStringList(evaluator, stringify, evaluate2(args[0], ctx));
1783
+ const result = [];
1784
+ for (const uri of uris) {
1785
+ const doc = engine.loadDocument(uri, baseUri || engine.baseUri);
1786
+ if (doc && !result.includes(doc)) result.push(doc);
1787
+ }
1788
+ return result;
1789
+ },
1790
+ /** `key(name, value)` - look up nodes through an `xsl:key` index. */
1791
+ key: (args, ctx) => {
1792
+ const name = asString(args[0], ctx);
1793
+ const values = toStringList(evaluator, stringify, evaluate2(args[1], ctx));
1794
+ return engine.keyRegistry.lookup(name, values, ownerDocumentOf(ctx.node));
1795
+ },
1796
+ /** `format-number(number, pattern, decimalFormat?)`. */
1797
+ "format-number": (args, ctx) => {
1798
+ const value = evaluator.toNumber(evaluate2(args[0], ctx));
1799
+ const pattern = asString(args[1], ctx);
1800
+ const formatName = args.length > 2 ? asString(args[2], ctx) : "";
1801
+ const format = engine.decimalFormats[formatName] || DEFAULT_DECIMAL_FORMAT;
1802
+ return formatNumber(value, pattern, format);
1803
+ },
1804
+ /** `current()` - the XSLT current node, not the XPath context node. */
1805
+ current: (args, ctx) => {
1806
+ const currentNode = ctx.hostContext?.currentNode;
1807
+ return currentNode ? [currentNode] : [ctx.node];
1808
+ },
1809
+ /** `generate-id(node-set?)` - a stable id for the life of the transform. */
1810
+ "generate-id": (args, ctx) => {
1811
+ let node = ctx.node;
1812
+ if (args.length > 0) {
1813
+ const nodeSet = evaluate2(args[0], ctx);
1814
+ node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
1815
+ }
1816
+ return node ? engine.generateId(node) : "";
1817
+ },
1818
+ /** `system-property(name)` - XSLT version and vendor information. */
1819
+ "system-property": (args, ctx) => {
1820
+ const name = asString(args[0], ctx);
1821
+ return Object.hasOwn(SYSTEM_PROPERTIES, name) ? SYSTEM_PROPERTIES[name] : "";
1822
+ },
1823
+ /** `function-available(name)` - reflects the evaluator function table. */
1824
+ "function-available": (args, ctx) => {
1825
+ const name = asString(args[0], ctx);
1826
+ return Object.hasOwn(evaluator.functions, name);
1827
+ },
1828
+ /** `element-available(name)` - reflects the XSLT elements the engine runs. */
1829
+ "element-available": (args, ctx) => {
1830
+ const { prefix, localName } = splitQName(asString(args[0], ctx));
1831
+ if (!prefix) return false;
1832
+ const namespaceUri = ctx.namespaces[prefix] ?? (prefix === "xsl" ? XSLT_NAMESPACE : null);
1833
+ return namespaceUri === XSLT_NAMESPACE && isXsltElementAvailable(localName);
1834
+ },
1835
+ /**
1836
+ * `unparsed-entity-uri(name)` - always empty.
1837
+ *
1838
+ * Unparsed entity declarations are not exposed by the DOM, so this
1839
+ * processor cannot resolve them; returning the empty string keeps
1840
+ * stylesheets that call the function working.
1841
+ */
1842
+ "unparsed-entity-uri": () => ""
1843
+ };
1844
+ }
1845
+
1846
+ // src/xslt/keys.js
1847
+ var KeyIndexRegistry = class {
1848
+ /**
1849
+ * @param {Object} options - Registry configuration
1850
+ * @param {Object<string, {match: string, use: string}>} options.keys - Declared keys by name
1851
+ * @param {(node: Node, pattern: string) => boolean} options.matchesPattern - XSLT pattern matcher
1852
+ * @param {(node: Node, expression: string) => string[]} options.evaluateUse - `use` evaluator returning key values
1853
+ */
1854
+ constructor({ keys, matchesPattern, evaluateUse }) {
1855
+ this.keys = keys;
1856
+ this.matchesPattern = matchesPattern;
1857
+ this.evaluateUse = evaluateUse;
1858
+ this.cache = /* @__PURE__ */ new WeakMap();
1859
+ }
1860
+ /**
1861
+ * Drop every cached index, for example after the key declarations changed.
1862
+ *
1863
+ * @returns {void}
1864
+ *
1865
+ * @example
1866
+ * registry.clear();
1867
+ */
1868
+ clear() {
1869
+ this.cache = /* @__PURE__ */ new WeakMap();
1870
+ }
1871
+ /**
1872
+ * Look up the nodes indexed under one or more key values.
1873
+ *
1874
+ * @param {string} name - The key name
1875
+ * @param {string|string[]} values - One key value, or several to union
1876
+ * @param {Document} doc - The document to search
1877
+ * @returns {Node[]} Matching nodes in document order, without duplicates
1878
+ * @throws {Error} When the key name was never declared
1879
+ *
1880
+ * @example
1881
+ * registry.lookup('byId', 'a1', xmlDoc);
1882
+ */
1883
+ lookup(name, values, doc) {
1884
+ if (!Object.hasOwn(this.keys, name)) {
1885
+ throw new Error(`Undefined key: ${name}`);
1886
+ }
1887
+ const index = this.getIndex(name, doc);
1888
+ const wanted = Array.isArray(values) ? values : [values];
1889
+ const result = [];
1890
+ for (const value of wanted) {
1891
+ for (const node of index.get(value) || []) {
1892
+ if (!result.includes(node)) result.push(node);
1893
+ }
1894
+ }
1895
+ return result;
1896
+ }
1897
+ /**
1898
+ * Get (building if needed) the index of one key for one document.
1899
+ *
1900
+ * @param {string} name - The key name
1901
+ * @param {Document} doc - The document being indexed
1902
+ * @returns {Map<string, Node[]>} Key value to nodes
1903
+ */
1904
+ getIndex(name, doc) {
1905
+ let byName = this.cache.get(doc);
1906
+ if (!byName) {
1907
+ byName = /* @__PURE__ */ new Map();
1908
+ this.cache.set(doc, byName);
1909
+ }
1910
+ let index = byName.get(name);
1911
+ if (!index) {
1912
+ index = this.buildIndex(name, doc);
1913
+ byName.set(name, index);
1914
+ }
1915
+ return index;
1916
+ }
1917
+ /**
1918
+ * Build the index of one key for one document.
1919
+ *
1920
+ * @param {string} name - The key name
1921
+ * @param {Document} doc - The document being indexed
1922
+ * @returns {Map<string, Node[]>} Key value to nodes
1923
+ */
1924
+ buildIndex(name, doc) {
1925
+ const { match, use } = this.keys[name];
1926
+ const index = /* @__PURE__ */ new Map();
1927
+ for (const node of documentOrderNodes(doc)) {
1928
+ if (!this.matchesPattern(node, match)) continue;
1929
+ for (const value of this.evaluateUse(node, use)) {
1930
+ const bucket = index.get(value);
1931
+ if (bucket) bucket.push(node);
1932
+ else index.set(value, [node]);
1933
+ }
1934
+ }
1935
+ return index;
1936
+ }
1937
+ };
1938
+ function* documentOrderNodes(root) {
1939
+ const stack = [root];
1940
+ while (stack.length > 0) {
1941
+ const current = stack.pop();
1942
+ yield current;
1943
+ if (current.nodeType === 1 && current.attributes) {
1944
+ for (const attribute of current.attributes) yield attribute;
1945
+ }
1946
+ const children = current.childNodes;
1947
+ if (children) {
1948
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
1949
+ }
1950
+ }
1951
+ }
1952
+
1953
+ // src/xslt/number.js
1954
+ var COUNTABLE_NODE_TYPES = /* @__PURE__ */ new Set([1, 3, 4, 7, 8]);
1955
+ function matchesDefaultCount(candidate, node) {
1956
+ if (candidate.nodeType !== node.nodeType) return false;
1957
+ if (candidate.nodeType === 1) return candidate.nodeName === node.nodeName;
1958
+ return true;
1959
+ }
1960
+ function createCountPredicate(node, count, matcher) {
1961
+ if (count) return (candidate) => matcher(candidate, count);
1962
+ return (candidate) => matchesDefaultCount(candidate, node);
1963
+ }
1964
+ function createFromPredicate(from, matcher) {
1965
+ if (!from) return () => false;
1966
+ return (candidate) => matcher(candidate, from);
1967
+ }
1968
+ function siblingPosition(node, isCounted) {
1969
+ let position = 1;
1970
+ let sibling = node.previousSibling;
1971
+ while (sibling) {
1972
+ if (COUNTABLE_NODE_TYPES.has(sibling.nodeType) && isCounted(sibling)) {
1973
+ position++;
1974
+ }
1975
+ sibling = sibling.previousSibling;
1976
+ }
1977
+ return position;
1978
+ }
1979
+ function nodesUpToTarget(target) {
1980
+ const root = target.ownerDocument || target;
1981
+ const result = [];
1982
+ const stack = [root];
1983
+ while (stack.length > 0) {
1984
+ const current = stack.pop();
1985
+ result.push(current);
1986
+ if (current === target) break;
1987
+ const children = current.childNodes;
1988
+ if (children) {
1989
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
1990
+ }
1991
+ }
1992
+ return result;
1993
+ }
1994
+ function countSingle(node, isCounted, isFrom) {
1995
+ let current = node;
1996
+ while (current && current.nodeType !== 9) {
1997
+ if (isFrom(current)) return [];
1998
+ if (isCounted(current)) return [siblingPosition(current, isCounted)];
1999
+ current = current.parentNode;
2000
+ }
2001
+ return [];
2002
+ }
2003
+ function countMultiple(node, isCounted, isFrom) {
2004
+ const numbers = [];
2005
+ let current = node;
2006
+ while (current && current.nodeType !== 9) {
2007
+ if (isFrom(current)) break;
2008
+ if (isCounted(current)) {
2009
+ numbers.unshift(siblingPosition(current, isCounted));
2010
+ }
2011
+ current = current.parentNode;
2012
+ }
2013
+ return numbers;
2014
+ }
2015
+ function countAny(node, isCounted, isFrom) {
2016
+ let total = 0;
2017
+ for (const candidate of nodesUpToTarget(node)) {
2018
+ if (!COUNTABLE_NODE_TYPES.has(candidate.nodeType)) continue;
2019
+ if (isFrom(candidate)) {
2020
+ total = 0;
2021
+ continue;
2022
+ }
2023
+ if (isCounted(candidate)) total++;
2024
+ }
2025
+ return total > 0 ? [total] : [];
2026
+ }
2027
+ function countXsltNumber(node, options, matcher) {
2028
+ const { level = "single", count = null, from = null } = options;
2029
+ const isCounted = createCountPredicate(node, count, matcher);
2030
+ const isFrom = createFromPredicate(from, matcher);
2031
+ if (level === "any") return countAny(node, isCounted, isFrom);
2032
+ if (level === "multiple") return countMultiple(node, isCounted, isFrom);
2033
+ return countSingle(node, isCounted, isFrom);
2034
+ }
2035
+
2036
+ // src/xslt/numberFormat.js
2037
+ function toAlphabetic(value, upperCase) {
2038
+ let remaining = value;
2039
+ let result = "";
2040
+ while (remaining > 0) {
2041
+ const index = (remaining - 1) % 26;
2042
+ result = String.fromCodePoint((upperCase ? 65 : 97) + index) + result;
2043
+ remaining = Math.floor((remaining - 1) / 26);
2044
+ }
2045
+ return result;
2046
+ }
2047
+ var ROMAN_NUMERALS = Object.freeze([
2048
+ ["M", 1e3],
2049
+ ["CM", 900],
2050
+ ["D", 500],
2051
+ ["CD", 400],
2052
+ ["C", 100],
2053
+ ["XC", 90],
2054
+ ["L", 50],
2055
+ ["XL", 40],
2056
+ ["X", 10],
2057
+ ["IX", 9],
2058
+ ["V", 5],
2059
+ ["IV", 4],
2060
+ ["I", 1]
2061
+ ]);
2062
+ function toRoman(value) {
2063
+ let remaining = value;
2064
+ let result = "";
2065
+ for (const [numeral, amount] of ROMAN_NUMERALS) {
2066
+ while (remaining >= amount) {
2067
+ result += numeral;
2068
+ remaining -= amount;
2069
+ }
2070
+ }
2071
+ return result;
2072
+ }
2073
+ function formatToken(value, token) {
2074
+ if (/^\d+$/.test(token)) {
2075
+ return String(value).padStart(token.length, "0");
2076
+ }
2077
+ if (value <= 0) return String(value);
2078
+ switch (token) {
2079
+ case "a":
2080
+ return toAlphabetic(value, false);
2081
+ case "A":
2082
+ return toAlphabetic(value, true);
2083
+ case "i":
2084
+ return toRoman(value).toLowerCase();
2085
+ case "I":
2086
+ return toRoman(value);
2087
+ default:
2088
+ return String(value);
2089
+ }
2090
+ }
2091
+ function parseFormat(format) {
2092
+ const parts = format.match(/[a-zA-Z0-9]+|[^a-zA-Z0-9]+/g) || [];
2093
+ const isToken = (part) => /^[a-zA-Z0-9]+$/.test(part);
2094
+ const tokens = [];
2095
+ const separators = [];
2096
+ let prefix = "";
2097
+ let suffix = "";
2098
+ for (const part of parts) {
2099
+ if (isToken(part)) tokens.push(part);
2100
+ else if (tokens.length === 0) prefix = part;
2101
+ else separators.push(part);
2102
+ }
2103
+ if (parts.length > 0 && tokens.length > 0 && !isToken(parts.at(-1))) {
2104
+ suffix = separators.pop();
2105
+ }
2106
+ if (tokens.length === 0) tokens.push("1");
2107
+ return { prefix, suffix, tokens, separators };
2108
+ }
2109
+ function formatXsltNumber(numbers, format = "1") {
2110
+ if (numbers.length === 0) return "";
2111
+ const { prefix, suffix, tokens, separators } = parseFormat(format);
2112
+ let result = prefix;
2113
+ numbers.forEach((value, index) => {
2114
+ if (index > 0) {
2115
+ const separator = separators[index - 1] ?? separators.at(-1) ?? ".";
2116
+ result += separator;
2117
+ }
2118
+ result += formatToken(value, tokens[index] ?? tokens.at(-1));
2119
+ });
2120
+ return result + suffix;
2121
+ }
2122
+
2123
+ // src/xslt/uri.js
2124
+ var ABSOLUTE_URI_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
2125
+ function resolveUri(href, baseUri) {
2126
+ if (!href) return href;
2127
+ if (!baseUri || isAbsoluteUri(href) || href.startsWith("/")) {
2128
+ return href;
2129
+ }
2130
+ const lastSlash = baseUri.lastIndexOf("/");
2131
+ const baseDir = lastSlash >= 0 ? baseUri.substring(0, lastSlash + 1) : "";
2132
+ return baseDir + href;
2133
+ }
2134
+ function isAbsoluteUri(uri) {
2135
+ return ABSOLUTE_URI_PATTERN.test(uri);
2136
+ }
2137
+ function stripFragment(uri) {
2138
+ if (typeof uri !== "string") return "";
2139
+ const hash = uri.indexOf("#");
2140
+ return hash === -1 ? uri : uri.substring(0, hash);
2141
+ }
2142
+
2143
+ // src/xslt/whitespace.js
2144
+ var DOCUMENT_TYPE_NODE = 10;
2145
+ function nameTestPriority(nameTest) {
2146
+ if (nameTest === "*") return -0.5;
2147
+ if (nameTest.endsWith(":*")) return -0.25;
2148
+ return 0;
2149
+ }
2150
+ function matchesNameTest(element, nameTest) {
2151
+ if (nameTest === "*") return true;
2152
+ if (nameTest.endsWith(":*")) {
2153
+ const prefix = nameTest.slice(0, -2);
2154
+ return element.nodeName.startsWith(`${prefix}:`);
2155
+ }
2156
+ return element.nodeName === nameTest || element.localName === nameTest;
2157
+ }
2158
+ var WhitespaceFilter = class {
2159
+ /**
2160
+ * @param {string[]} [stripSpace] - Name tests from `xsl:strip-space`
2161
+ * @param {string[]} [preserveSpace] - Name tests from `xsl:preserve-space`
2162
+ */
2163
+ constructor(stripSpace = [], preserveSpace = []) {
2164
+ this.stripSpace = stripSpace;
2165
+ this.preserveSpace = preserveSpace;
2166
+ }
2167
+ /**
2168
+ * Whether the filter can remove anything at all.
2169
+ *
2170
+ * @returns {boolean} True when at least one `xsl:strip-space` was declared
2171
+ *
2172
+ * @example
2173
+ * new WhitespaceFilter(['*']).isActive(); // true
2174
+ */
2175
+ isActive() {
2176
+ return this.stripSpace.length > 0;
2177
+ }
2178
+ /**
2179
+ * Whether whitespace-only children of an element are stripped.
2180
+ *
2181
+ * The most specific name test wins; `xsl:preserve-space` wins ties.
2182
+ *
2183
+ * @param {Element} element - The parent element
2184
+ * @returns {boolean} True when whitespace-only text children are removed
2185
+ *
2186
+ * @example
2187
+ * new WhitespaceFilter(['*'], ['pre']).isStripped(preElement); // false
2188
+ */
2189
+ isStripped(element) {
2190
+ let stripPriority = -Infinity;
2191
+ let preservePriority = -Infinity;
2192
+ for (const nameTest of this.stripSpace) {
2193
+ if (matchesNameTest(element, nameTest)) {
2194
+ stripPriority = Math.max(stripPriority, nameTestPriority(nameTest));
2195
+ }
2196
+ }
2197
+ for (const nameTest of this.preserveSpace) {
2198
+ if (matchesNameTest(element, nameTest)) {
2199
+ preservePriority = Math.max(
2200
+ preservePriority,
2201
+ nameTestPriority(nameTest)
2202
+ );
2203
+ }
2204
+ }
2205
+ return stripPriority > -Infinity && stripPriority > preservePriority;
2206
+ }
2207
+ };
2208
+ function hasXmlSpacePreserve(node) {
2209
+ let current = node;
2210
+ while (current?.nodeType === 1) {
2211
+ const value = current.getAttribute("xml:space");
2212
+ if (value === "preserve") return true;
2213
+ if (value === "default") return false;
2214
+ current = current.parentNode;
2215
+ }
2216
+ return false;
2217
+ }
2218
+ function pruneWhitespace(root, filter) {
2219
+ const doomed = [];
2220
+ const stack = [root];
2221
+ while (stack.length > 0) {
2222
+ const current = stack.pop();
2223
+ if ((current.nodeType === 3 || current.nodeType === 4) && current.nodeValue !== null && current.nodeValue.trim() === "" && current.parentNode?.nodeType === 1 && filter.isStripped(current.parentNode) && !hasXmlSpacePreserve(current.parentNode)) {
2224
+ doomed.push(current);
2225
+ }
2226
+ const children = current.childNodes;
2227
+ if (children) {
2228
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
2229
+ }
2230
+ }
2231
+ for (const node of doomed) node.remove();
2232
+ }
2233
+ function stripWhitespaceNodes(sourceNode, filter, targetDoc) {
2234
+ let root;
2235
+ if (sourceNode.nodeType === 9) {
2236
+ root = targetDoc;
2237
+ for (const child of Array.from(sourceNode.childNodes)) {
2238
+ if (child.nodeType === DOCUMENT_TYPE_NODE) continue;
2239
+ targetDoc.appendChild(targetDoc.importNode(child, true));
2240
+ }
2241
+ } else {
2242
+ root = targetDoc.importNode(sourceNode, true);
2243
+ targetDoc.appendChild(root);
2244
+ }
2245
+ pruneWhitespace(root, filter);
2246
+ return root;
2247
+ }
2248
+
2249
+ // src/xslt/literalResult.js
2250
+ function lookupNamespaceUri(node, prefix) {
2251
+ if (typeof node.lookupNamespaceURI === "function") {
2252
+ const found = node.lookupNamespaceURI(prefix);
2253
+ if (found) return found;
2254
+ }
2255
+ const attributeName = prefix ? `xmlns:${prefix}` : "xmlns";
2256
+ let current = node;
2257
+ while (current?.nodeType === 1) {
2258
+ const value = current.getAttribute(attributeName);
2259
+ if (value) return value;
2260
+ current = current.parentNode;
2261
+ }
2262
+ return null;
2263
+ }
2264
+ var NamespaceAliasMap = class {
2265
+ constructor() {
2266
+ this.byUri = /* @__PURE__ */ new Map();
2267
+ }
2268
+ /**
2269
+ * Record one `xsl:namespace-alias` declaration.
2270
+ *
2271
+ * @param {Element} node - The `xsl:namespace-alias` element
2272
+ * @returns {void}
2273
+ *
2274
+ * @example
2275
+ * aliases.add(namespaceAliasElement);
2276
+ */
2277
+ add(node) {
2278
+ const stylesheetPrefix = node.getAttribute("stylesheet-prefix");
2279
+ const resultPrefix = node.getAttribute("result-prefix");
2280
+ if (!stylesheetPrefix || !resultPrefix) return;
2281
+ const fromUri = lookupNamespaceUri(
2282
+ node,
2283
+ stylesheetPrefix === "#default" ? null : stylesheetPrefix
2284
+ );
2285
+ if (!fromUri) return;
2286
+ const isDefaultResult = resultPrefix === "#default";
2287
+ const toUri = lookupNamespaceUri(
2288
+ node,
2289
+ isDefaultResult ? null : resultPrefix
2290
+ );
2291
+ this.byUri.set(fromUri, {
2292
+ uri: toUri,
2293
+ prefix: isDefaultResult ? null : resultPrefix
2294
+ });
2295
+ }
2296
+ /**
2297
+ * Whether any alias was declared.
2298
+ *
2299
+ * @returns {boolean} True when at least one alias is known
2300
+ *
2301
+ * @example
2302
+ * aliases.isEmpty();
2303
+ */
2304
+ isEmpty() {
2305
+ return this.byUri.size === 0;
2306
+ }
2307
+ /**
2308
+ * Apply aliasing to a literal result name.
2309
+ *
2310
+ * @param {string|null} namespaceUri - The namespace of the stylesheet node
2311
+ * @param {string} localName - The local name of the stylesheet node
2312
+ * @returns {{namespaceUri: (string|null), qname: string}|null} The aliased name, or null when no alias applies
2313
+ *
2314
+ * @example
2315
+ * aliases.resolve('http://www.w3.org/1999/XSL/TransformAlias', 'stylesheet');
2316
+ * // { namespaceUri: 'http://www.w3.org/1999/XSL/Transform', qname: 'xsl:stylesheet' }
2317
+ */
2318
+ resolve(namespaceUri, localName) {
2319
+ const alias = namespaceUri ? this.byUri.get(namespaceUri) : void 0;
2320
+ if (!alias) return null;
2321
+ return {
2322
+ namespaceUri: alias.uri,
2323
+ qname: alias.prefix ? `${alias.prefix}:${localName}` : localName
2324
+ };
2325
+ }
2326
+ };
2327
+ function shouldCopyAttribute(attribute, xsltNamespace) {
2328
+ if (attribute.namespaceURI === xsltNamespace) return false;
2329
+ if (attribute.name === "xmlns" || attribute.name.startsWith("xmlns:")) {
2330
+ return false;
2331
+ }
2332
+ return !attribute.name.startsWith("xsl:");
2333
+ }
2334
+ function getXsltAttribute(node, localName, xsltNamespace) {
2335
+ if (!node.attributes) return null;
2336
+ for (const attribute of node.attributes) {
2337
+ const matchesNamespace = attribute.namespaceURI === xsltNamespace && (attribute.localName || attribute.name) === localName;
2338
+ if (matchesNamespace || attribute.name === `xsl:${localName}`) {
2339
+ return attribute.value;
2340
+ }
2341
+ }
2342
+ return null;
2343
+ }
2344
+
2345
+ // src/xslt/resultTree.js
2346
+ function createResultDocument(ownerDocument) {
2347
+ return ownerDocument.implementation.createDocument(null, null, null);
2348
+ }
2349
+ function importResultNode(node, targetDoc) {
2350
+ const copy = targetDoc.importNode(node, false);
2351
+ if (node._disableOutputEscaping) {
2352
+ copy._disableOutputEscaping = true;
2353
+ }
2354
+ if (node.childNodes) {
2355
+ for (const child of node.childNodes) {
2356
+ copy.appendChild(importResultNode(child, targetDoc));
2357
+ }
2358
+ }
2359
+ return copy;
2360
+ }
2361
+ function importResultFragment(fragment, targetDoc) {
2362
+ if (fragment.ownerDocument === targetDoc) return fragment;
2363
+ const imported = targetDoc.createDocumentFragment();
2364
+ for (const child of fragment.childNodes) {
2365
+ imported.appendChild(importResultNode(child, targetDoc));
2366
+ }
2367
+ return imported;
2368
+ }
2369
+
2370
+ // src/xslt/templatePriority.js
2371
+ var NAME = String.raw`[A-Za-z_][\w.-]*`;
2372
+ var QNAME = `(?:${NAME}:)?${NAME}`;
2373
+ var QNAME_PATTERN = new RegExp(`^(?:child::|attribute::|@)?${QNAME}$`);
2374
+ var PREFIX_WILDCARD_PATTERN = new RegExp(
2375
+ String.raw`^(?:child::|attribute::|@)?${NAME}:\*$`
2376
+ );
2377
+ var NODE_TEST_PATTERN = /^(?:child::|attribute::|@)?(?:\*|node\(\)|text\(\)|comment\(\)|processing-instruction\(\))$/;
2378
+ var PI_LITERAL_PATTERN = /^(?:child::)?processing-instruction\(\s*(?:"[^"]*"|'[^']*')\s*\)$/;
2379
+ function calculatePriority(pattern) {
2380
+ if (!pattern) return 0.5;
2381
+ if (NODE_TEST_PATTERN.test(pattern)) return -0.5;
2382
+ if (PREFIX_WILDCARD_PATTERN.test(pattern)) return -0.25;
2383
+ if (QNAME_PATTERN.test(pattern) || PI_LITERAL_PATTERN.test(pattern)) return 0;
2384
+ return 0.5;
2385
+ }
2386
+
2387
+ // src/xslt/serializer/constants.js
2388
+ var NODE_TYPE = {
2389
+ ELEMENT: 1,
2390
+ TEXT: 3,
2391
+ CDATA_SECTION: 4,
2392
+ PROCESSING_INSTRUCTION: 7,
2393
+ COMMENT: 8,
2394
+ DOCUMENT: 9,
2395
+ DOCUMENT_FRAGMENT: 11
2396
+ };
2397
+ var XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
2398
+ var XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
2399
+ var TEXT_MODE = {
2400
+ ESCAPE: "escape",
2401
+ CDATA: "cdata",
2402
+ RAW: "raw"
2403
+ };
2404
+ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
2405
+ "area",
2406
+ "base",
2407
+ "br",
2408
+ "col",
2409
+ "embed",
2410
+ "hr",
2411
+ "img",
2412
+ "input",
2413
+ "link",
2414
+ "meta",
2415
+ "param",
2416
+ "source",
2417
+ "track",
2418
+ "wbr"
2419
+ ]);
2420
+ var RAW_TEXT_ELEMENTS = /* @__PURE__ */ new Set(["script", "style"]);
2421
+ var PRESERVE_SPACE_ELEMENTS = /* @__PURE__ */ new Set([
2422
+ "pre",
2423
+ "script",
2424
+ "style",
2425
+ "textarea"
2426
+ ]);
2427
+ var INDENT_UNIT = " ";
2428
+
2429
+ // src/xslt/serializer/settings.js
2430
+ function isYes(value) {
2431
+ return value === true || String(value).toLowerCase() === "yes";
2432
+ }
2433
+ function toNameSet(value) {
2434
+ if (Array.isArray(value)) {
2435
+ return new Set(value);
2436
+ }
2437
+ if (typeof value === "string") {
2438
+ return new Set(value.split(/\s+/).filter(Boolean));
2439
+ }
2440
+ return /* @__PURE__ */ new Set();
2441
+ }
2442
+ function findRootElement(node) {
2443
+ if (!node) {
2444
+ return null;
2445
+ }
2446
+ if (node.nodeType === NODE_TYPE.ELEMENT) {
2447
+ return node;
2448
+ }
2449
+ for (const child of node.childNodes || []) {
2450
+ if (child.nodeType === NODE_TYPE.ELEMENT) {
2451
+ return child;
2452
+ }
2453
+ }
2454
+ return null;
2455
+ }
2456
+ function detectOutputMethod(node) {
2457
+ const root = findRootElement(node);
2458
+ const isHtmlRoot = root && !root.namespaceURI && root.localName.toLowerCase() === "html";
2459
+ return isHtmlRoot ? "html" : "xml";
2460
+ }
2461
+ function resolveOutputSettings(outputSettings, node) {
2462
+ const raw = outputSettings || {};
2463
+ const declared = typeof raw.method === "string" ? raw.method.trim() : "";
2464
+ const method = declared && declared !== "auto" ? declared.toLowerCase() : detectOutputMethod(node);
2465
+ return {
2466
+ method,
2467
+ version: raw.version || "1.0",
2468
+ encoding: raw.encoding || "UTF-8",
2469
+ standalone: raw.standalone || null,
2470
+ indent: isYes(raw.indent),
2471
+ omitXmlDeclaration: isYes(raw.omitXmlDeclaration),
2472
+ doctypePublic: raw.doctypePublic || null,
2473
+ doctypeSystem: raw.doctypeSystem || null,
2474
+ mediaType: raw.mediaType || null,
2475
+ cdataSectionElements: toNameSet(raw.cdataSectionElements)
2476
+ };
2477
+ }
2478
+
2479
+ // src/xslt/serializer/escape.js
2480
+ var XML_TEXT_ESCAPES = { "&": "&amp;", "<": "&lt;" };
2481
+ var HTML_TEXT_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;" };
2482
+ var XML_ATTRIBUTE_ESCAPES = {
2483
+ "&": "&amp;",
2484
+ "<": "&lt;",
2485
+ ">": "&gt;",
2486
+ '"': "&quot;",
2487
+ " ": "&#9;",
2488
+ "\n": "&#10;",
2489
+ "\r": "&#13;"
2490
+ };
2491
+ var HTML_ATTRIBUTE_ESCAPES = {
2492
+ "&": "&amp;",
2493
+ "<": "&lt;",
2494
+ ">": "&gt;",
2495
+ '"': "&quot;"
2496
+ };
2497
+ function escapeWith(value, pattern, escapes) {
2498
+ return String(value).replaceAll(pattern, (character) => escapes[character]);
2499
+ }
2500
+ function escapeXmlText(value) {
2501
+ return escapeWith(value, /[&<]/g, XML_TEXT_ESCAPES).replaceAll(
2502
+ "]]>",
2503
+ "]]&gt;"
2504
+ );
2505
+ }
2506
+ function escapeXmlAttribute(value) {
2507
+ return escapeWith(value, /[&<>"\t\n\r]/g, XML_ATTRIBUTE_ESCAPES);
2508
+ }
2509
+ function escapeHtmlText(value) {
2510
+ return escapeWith(value, /[&<>]/g, HTML_TEXT_ESCAPES);
2511
+ }
2512
+ function escapeHtmlAttribute(value) {
2513
+ return escapeWith(value, /[&<>"]/g, HTML_ATTRIBUTE_ESCAPES);
2514
+ }
2515
+ function wrapCdata(value) {
2516
+ return `<![CDATA[${String(value).replaceAll("]]>", "]]]]><![CDATA[>")}]]>`;
2517
+ }
2518
+
2519
+ // src/xslt/serializer/namespaces.js
2520
+ function createNamespaceScope() {
2521
+ return /* @__PURE__ */ new Map([
2522
+ ["", ""],
2523
+ ["xml", XML_NAMESPACE]
2524
+ ]);
2525
+ }
2526
+ function collectNamespaceDeclarations(element, scope) {
2527
+ const declarations = [];
2528
+ let next = scope;
2529
+ const declare = (prefix, uri) => {
2530
+ if (next.get(prefix) === uri) {
2531
+ return;
2532
+ }
2533
+ if (next === scope) {
2534
+ next = new Map(scope);
2535
+ }
2536
+ next.set(prefix, uri);
2537
+ declarations.push({ prefix, uri });
2538
+ };
2539
+ declare(element.prefix || "", element.namespaceURI || "");
2540
+ const attributes = Array.from(element.attributes || []);
2541
+ for (const attribute of attributes) {
2542
+ if (attribute.namespaceURI !== XMLNS_NAMESPACE && attribute.prefix) {
2543
+ declare(attribute.prefix, attribute.namespaceURI || "");
2544
+ }
2545
+ }
2546
+ for (const attribute of attributes) {
2547
+ if (attribute.namespaceURI === XMLNS_NAMESPACE) {
2548
+ declare(attribute.prefix ? attribute.localName : "", attribute.value);
2549
+ }
2550
+ }
2551
+ return { declarations, scope: next };
2552
+ }
2553
+
2554
+ // src/xslt/serializer/indent.js
2555
+ function isWhitespaceOnlyText(node) {
2556
+ return !/\S/.test(node.nodeValue || "");
2557
+ }
2558
+ function getIndentableChildren(element) {
2559
+ const indentable = [];
2560
+ for (const child of element.childNodes) {
2561
+ if (child.nodeType === NODE_TYPE.TEXT) {
2562
+ if (isWhitespaceOnlyText(child)) {
2563
+ continue;
2564
+ }
2565
+ return null;
2566
+ }
2567
+ if (child.nodeType !== NODE_TYPE.ELEMENT && child.nodeType !== NODE_TYPE.COMMENT && child.nodeType !== NODE_TYPE.PROCESSING_INSTRUCTION) {
2568
+ return null;
2569
+ }
2570
+ indentable.push(child);
2571
+ }
2572
+ return indentable.length > 0 ? indentable : null;
2573
+ }
2574
+
2575
+ // src/xslt/serializer/rawText.js
2576
+ var rawTextNodes = /* @__PURE__ */ new WeakSet();
2577
+ function markRawText(node) {
2578
+ if (node) {
2579
+ rawTextNodes.add(node);
2580
+ }
2581
+ return node;
2582
+ }
2583
+ function isRawText(node) {
2584
+ if (!node) {
2585
+ return false;
2586
+ }
2587
+ return rawTextNodes.has(node) || node._disableOutputEscaping === true;
2588
+ }
2589
+
2590
+ // src/xslt/serializer/baseWriter.js
2591
+ var BaseWriter = class {
2592
+ /**
2593
+ * @param {object} settings - Normalized output settings
2594
+ * @param {{xhtml?: boolean}} [options] - Dialect options
2595
+ */
2596
+ constructor(settings, options = {}) {
2597
+ this.settings = settings;
2598
+ this.xhtml = options.xhtml === true;
2599
+ this.parts = [];
2600
+ }
2601
+ /**
2602
+ * Serialize a result tree node.
2603
+ *
2604
+ * @param {Node} node - Document, fragment or element to serialize
2605
+ * @returns {string} Serialized output
2606
+ */
2607
+ serialize(node) {
2608
+ this.parts = [];
2609
+ this.writeProlog(node);
2610
+ this.writeNode(node, createNamespaceScope(), 0, TEXT_MODE.ESCAPE);
2611
+ return this.parts.join("");
2612
+ }
2613
+ /**
2614
+ * Write the XML declaration and the document type declaration.
2615
+ *
2616
+ * @param {Node} node - Result tree root
2617
+ * @returns {void}
2618
+ */
2619
+ writeProlog(node) {
2620
+ if (this.emitsXmlDeclaration) {
2621
+ const { version, encoding, standalone } = this.settings;
2622
+ const standalonePart = standalone ? ` standalone="${standalone}"` : "";
2623
+ this.parts.push(
2624
+ `<?xml version="${version}" encoding="${encoding}"${standalonePart}?>
2625
+ `
2626
+ );
2627
+ }
2628
+ const doctype = this.doctypeMarkup(findRootElement(node));
2629
+ if (doctype) {
2630
+ this.parts.push(`${doctype}
2631
+ `);
2632
+ }
2633
+ }
2634
+ /**
2635
+ * Write any result tree node.
2636
+ *
2637
+ * @param {Node} node - Node to write
2638
+ * @param {Map<string, string>} scope - Namespace scope in effect
2639
+ * @param {number} depth - Current indentation depth
2640
+ * @param {string} textMode - {@link TEXT_MODE} for character data children
2641
+ * @returns {void}
2642
+ */
2643
+ writeNode(node, scope, depth, textMode) {
2644
+ switch (node.nodeType) {
2645
+ case NODE_TYPE.ELEMENT:
2646
+ this.writeElement(node, scope, depth);
2647
+ break;
2648
+ case NODE_TYPE.TEXT:
2649
+ case NODE_TYPE.CDATA_SECTION:
2650
+ this.writeText(node, textMode);
2651
+ break;
2652
+ case NODE_TYPE.COMMENT:
2653
+ this.parts.push(`<!--${node.nodeValue}-->`);
2654
+ break;
2655
+ case NODE_TYPE.PROCESSING_INSTRUCTION:
2656
+ this.writeProcessingInstruction(node);
2657
+ break;
2658
+ case NODE_TYPE.DOCUMENT:
2659
+ case NODE_TYPE.DOCUMENT_FRAGMENT:
2660
+ this.writeChildNodes(node, scope, depth, textMode);
2661
+ break;
2662
+ default:
2663
+ break;
2664
+ }
2665
+ }
2666
+ /**
2667
+ * Write every child of a node without adding whitespace.
2668
+ *
2669
+ * @param {Node} node - Parent node
2670
+ * @param {Map<string, string>} scope - Namespace scope in effect
2671
+ * @param {number} depth - Current indentation depth
2672
+ * @param {string} textMode - {@link TEXT_MODE} for character data children
2673
+ * @returns {void}
2674
+ */
2675
+ writeChildNodes(node, scope, depth, textMode) {
2676
+ for (const child of node.childNodes) {
2677
+ this.writeNode(child, scope, depth, textMode);
2678
+ }
2679
+ }
2680
+ /**
2681
+ * Write an element with its namespaces, attributes and children.
2682
+ *
2683
+ * @param {Element} element - Element to write
2684
+ * @param {Map<string, string>} scope - Namespace scope inherited from the parent
2685
+ * @param {number} depth - Current indentation depth
2686
+ * @returns {void}
2687
+ */
2688
+ writeElement(element, scope, depth) {
2689
+ const namespaces = this.emitsNamespaces ? collectNamespaceDeclarations(element, scope) : { declarations: [], scope };
2690
+ const name = element.nodeName;
2691
+ this.parts.push(
2692
+ `<${name}${this.namespaceMarkup(namespaces.declarations)}` + this.attributesMarkup(element)
2693
+ );
2694
+ if (!element.firstChild) {
2695
+ this.parts.push(this.emptyElementMarkup(element, name));
2696
+ return;
2697
+ }
2698
+ this.parts.push(">");
2699
+ this.writeElementChildren(element, namespaces.scope, depth);
2700
+ this.parts.push(`</${name}>`);
2701
+ }
2702
+ /**
2703
+ * Write the children of an element, indenting element-only content.
2704
+ *
2705
+ * @param {Element} element - Parent element
2706
+ * @param {Map<string, string>} scope - Namespace scope in effect
2707
+ * @param {number} depth - Depth of the parent element
2708
+ * @returns {void}
2709
+ */
2710
+ writeElementChildren(element, scope, depth) {
2711
+ const textMode = this.childTextMode(element);
2712
+ const indentable = this.indentableChildren(element, textMode);
2713
+ if (!indentable) {
2714
+ this.writeChildNodes(element, scope, depth, textMode);
2715
+ return;
2716
+ }
2717
+ const childIndent = `
2718
+ ${INDENT_UNIT.repeat(depth + 1)}`;
2719
+ for (const child of indentable) {
2720
+ this.parts.push(childIndent);
2721
+ this.writeNode(child, scope, depth + 1, textMode);
2722
+ }
2723
+ this.parts.push(`
2724
+ ${INDENT_UNIT.repeat(depth)}`);
2725
+ }
2726
+ /**
2727
+ * Determine the children to indent inside an element.
2728
+ *
2729
+ * @param {Element} element - Parent element
2730
+ * @param {string} textMode - {@link TEXT_MODE} for character data children
2731
+ * @returns {Node[]|null} Children to indent, or null when indenting is off
2732
+ */
2733
+ indentableChildren(element, textMode) {
2734
+ if (!this.settings.indent || textMode !== TEXT_MODE.ESCAPE) {
2735
+ return null;
2736
+ }
2737
+ if (!this.allowsIndentInside(element)) {
2738
+ return null;
2739
+ }
2740
+ return getIndentableChildren(element);
2741
+ }
2742
+ /**
2743
+ * Build the namespace declaration markup of an element.
2744
+ *
2745
+ * @param {Array<{prefix: string, uri: string}>} declarations - Declarations
2746
+ * @returns {string} Attribute markup, starting with a space when non-empty
2747
+ */
2748
+ namespaceMarkup(declarations) {
2749
+ return declarations.map(({ prefix, uri }) => {
2750
+ const name = prefix ? `xmlns:${prefix}` : "xmlns";
2751
+ return ` ${name}="${this.escapeAttribute(uri)}"`;
2752
+ }).join("");
2753
+ }
2754
+ /**
2755
+ * Build the attribute markup of an element, skipping namespace declarations.
2756
+ *
2757
+ * @param {Element} element - Element being written
2758
+ * @returns {string} Attribute markup, starting with a space when non-empty
2759
+ */
2760
+ attributesMarkup(element) {
2761
+ let markup = "";
2762
+ for (const attribute of Array.from(element.attributes || [])) {
2763
+ if (attribute.namespaceURI !== XMLNS_NAMESPACE) {
2764
+ markup += this.attributeMarkup(attribute);
2765
+ }
2766
+ }
2767
+ return markup;
2768
+ }
2769
+ /**
2770
+ * Build the markup of a single attribute.
2771
+ *
2772
+ * @param {Attr} attribute - Attribute to write
2773
+ * @returns {string} Attribute markup, starting with a space
2774
+ */
2775
+ attributeMarkup(attribute) {
2776
+ return ` ${attribute.name}="${this.escapeAttribute(attribute.value)}"`;
2777
+ }
2778
+ /**
2779
+ * Write a character data node.
2780
+ *
2781
+ * Nodes produced with `disable-output-escaping="yes"` are written verbatim.
2782
+ *
2783
+ * @param {Node} node - Text or CDATA section node
2784
+ * @param {string} textMode - {@link TEXT_MODE} requested by the parent
2785
+ * @returns {void}
2786
+ */
2787
+ writeText(node, textMode) {
2788
+ const value = node.nodeValue || "";
2789
+ if (isRawText(node)) {
2790
+ this.parts.push(value);
2791
+ return;
2792
+ }
2793
+ const mode = this.resolveTextMode(node, textMode);
2794
+ if (mode === TEXT_MODE.CDATA) {
2795
+ this.parts.push(wrapCdata(value));
2796
+ } else if (mode === TEXT_MODE.RAW) {
2797
+ this.parts.push(value);
2798
+ } else {
2799
+ this.parts.push(this.escapeText(value));
2800
+ }
2801
+ }
2802
+ /**
2803
+ * Resolve the effective text mode of a character data node.
2804
+ *
2805
+ * @param {Node} node - Text or CDATA section node
2806
+ * @param {string} textMode - {@link TEXT_MODE} requested by the parent
2807
+ * @returns {string} A {@link TEXT_MODE} value
2808
+ */
2809
+ resolveTextMode(node, textMode) {
2810
+ if (textMode !== TEXT_MODE.ESCAPE) {
2811
+ return textMode;
2812
+ }
2813
+ return node.nodeType === NODE_TYPE.CDATA_SECTION ? this.cdataNodeMode : TEXT_MODE.ESCAPE;
2814
+ }
2815
+ /**
2816
+ * Write a processing instruction node.
2817
+ *
2818
+ * @param {ProcessingInstruction} node - Node to write
2819
+ * @returns {void}
2820
+ */
2821
+ writeProcessingInstruction(node) {
2822
+ const data = node.nodeValue || "";
2823
+ const separator = data ? " " : "";
2824
+ this.parts.push(`<?${node.target}${separator}${data}${this.piTerminator}`);
2825
+ }
2826
+ };
2827
+
2828
+ // src/xslt/serializer/xmlSerializer.js
2829
+ var XmlWriter = class extends BaseWriter {
2830
+ /**
2831
+ * Whether an XML declaration has to be written.
2832
+ * @returns {boolean} True when the declaration is not omitted
2833
+ */
2834
+ get emitsXmlDeclaration() {
2835
+ return !this.settings.omitXmlDeclaration;
2836
+ }
2837
+ /**
2838
+ * Whether namespace declarations have to be written.
2839
+ * @returns {boolean} Always true for XML output
2840
+ */
2841
+ get emitsNamespaces() {
2842
+ return true;
2843
+ }
2844
+ /**
2845
+ * Terminator of a processing instruction.
2846
+ * @returns {string} The XML processing instruction terminator
2847
+ */
2848
+ get piTerminator() {
2849
+ return "?>";
2850
+ }
2851
+ /**
2852
+ * How a source CDATA section node has to be written.
2853
+ * @returns {string} A {@link TEXT_MODE} value
2854
+ */
2855
+ get cdataNodeMode() {
2856
+ return TEXT_MODE.CDATA;
2857
+ }
2858
+ /**
2859
+ * Build the document type declaration for the xml output method.
2860
+ *
2861
+ * @param {Element|null} rootElement - Result document element
2862
+ * @returns {string} Doctype markup, or an empty string when not applicable
2863
+ */
2864
+ doctypeMarkup(rootElement) {
2865
+ const { doctypePublic, doctypeSystem } = this.settings;
2866
+ if (!rootElement || !doctypeSystem) {
2867
+ return "";
2868
+ }
2869
+ const name = rootElement.nodeName;
2870
+ return doctypePublic ? `<!DOCTYPE ${name} PUBLIC "${doctypePublic}" "${doctypeSystem}">` : `<!DOCTYPE ${name} SYSTEM "${doctypeSystem}">`;
2871
+ }
2872
+ /**
2873
+ * Determine how the character data children of an element are written.
2874
+ *
2875
+ * @param {Element} element - Parent element
2876
+ * @returns {string} A {@link TEXT_MODE} value
2877
+ */
2878
+ childTextMode(element) {
2879
+ const names = this.settings.cdataSectionElements;
2880
+ return names.has(element.nodeName) || names.has(element.localName) ? TEXT_MODE.CDATA : TEXT_MODE.ESCAPE;
2881
+ }
2882
+ /**
2883
+ * Whether the content of an element may be re-indented.
2884
+ *
2885
+ * @param {Element} _element - Element being inspected
2886
+ * @returns {boolean} Always true for XML output
2887
+ */
2888
+ allowsIndentInside(_element) {
2889
+ return true;
2890
+ }
2891
+ /**
2892
+ * Build the markup closing an element that has no children.
2893
+ *
2894
+ * @param {Element} element - Empty element
2895
+ * @param {string} _name - Element name as written
2896
+ * @returns {string} Markup terminating the start tag
2897
+ */
2898
+ emptyElementMarkup(element, _name) {
2899
+ return this.xhtml && this.isVoidElement(element) ? " />" : "/>";
2900
+ }
2901
+ /**
2902
+ * Test whether an element is an HTML void element.
2903
+ *
2904
+ * @param {Element} element - Element to test
2905
+ * @returns {boolean} True for void elements such as `br`
2906
+ */
2907
+ isVoidElement(element) {
2908
+ return VOID_ELEMENTS.has(String(element.localName).toLowerCase());
2909
+ }
2910
+ /**
2911
+ * Escape character data.
2912
+ *
2913
+ * @param {string} value - Text content
2914
+ * @returns {string} Escaped text
2915
+ */
2916
+ escapeText(value) {
2917
+ return escapeXmlText(value);
2918
+ }
2919
+ /**
2920
+ * Escape an attribute value.
2921
+ *
2922
+ * @param {string} value - Attribute value
2923
+ * @returns {string} Escaped value
2924
+ */
2925
+ escapeAttribute(value) {
2926
+ return escapeXmlAttribute(value);
2927
+ }
2928
+ };
2929
+
2930
+ // src/xslt/serializer/htmlSerializer.js
2931
+ var HtmlWriter = class extends XmlWriter {
2932
+ /**
2933
+ * The html output method never writes an XML declaration.
2934
+ * @returns {boolean} Always false
2935
+ */
2936
+ get emitsXmlDeclaration() {
2937
+ return false;
2938
+ }
2939
+ /**
2940
+ * The html output method never writes namespace declarations.
2941
+ * @returns {boolean} Always false
2942
+ */
2943
+ get emitsNamespaces() {
2944
+ return false;
2945
+ }
2946
+ /**
2947
+ * HTML processing instructions are terminated by `>` alone.
2948
+ * @returns {string} The HTML processing instruction terminator
2949
+ */
2950
+ get piTerminator() {
2951
+ return ">";
2952
+ }
2953
+ /**
2954
+ * HTML has no CDATA sections, so such nodes are escaped as text.
2955
+ * @returns {string} A {@link TEXT_MODE} value
2956
+ */
2957
+ get cdataNodeMode() {
2958
+ return TEXT_MODE.ESCAPE;
2959
+ }
2960
+ /**
2961
+ * Build the document type declaration for the html output method.
2962
+ *
2963
+ * @param {Element|null} rootElement - Result document element
2964
+ * @returns {string} Doctype markup, or an empty string when not applicable
2965
+ */
2966
+ doctypeMarkup(rootElement) {
2967
+ const { doctypePublic, doctypeSystem } = this.settings;
2968
+ if (!doctypePublic && !doctypeSystem) {
2969
+ return "";
2970
+ }
2971
+ const name = rootElement ? rootElement.nodeName : "html";
2972
+ if (doctypePublic && doctypeSystem) {
2973
+ return `<!DOCTYPE ${name} PUBLIC "${doctypePublic}" "${doctypeSystem}">`;
2974
+ }
2975
+ if (doctypePublic) {
2976
+ return `<!DOCTYPE ${name} PUBLIC "${doctypePublic}">`;
2977
+ }
2978
+ return `<!DOCTYPE ${name} SYSTEM "${doctypeSystem}">`;
2979
+ }
2980
+ /**
2981
+ * Script and style content is written verbatim.
2982
+ *
2983
+ * @param {Element} element - Parent element
2984
+ * @returns {string} A {@link TEXT_MODE} value
2985
+ */
2986
+ childTextMode(element) {
2987
+ return RAW_TEXT_ELEMENTS.has(String(element.localName).toLowerCase()) ? TEXT_MODE.RAW : TEXT_MODE.ESCAPE;
2988
+ }
2989
+ /**
2990
+ * Content of `pre`, `script`, `style` and `textarea` is never re-indented.
2991
+ *
2992
+ * @param {Element} element - Element being inspected
2993
+ * @returns {boolean} True when the content may be indented
2994
+ */
2995
+ allowsIndentInside(element) {
2996
+ return !PRESERVE_SPACE_ELEMENTS.has(
2997
+ String(element.localName).toLowerCase()
2998
+ );
2999
+ }
3000
+ /**
3001
+ * Void elements have no end tag; every other element gets one.
3002
+ *
3003
+ * @param {Element} element - Empty element
3004
+ * @param {string} name - Element name as written
3005
+ * @returns {string} Markup terminating the start tag
3006
+ */
3007
+ emptyElementMarkup(element, name) {
3008
+ return this.isVoidElement(element) ? ">" : `></${name}>`;
3009
+ }
3010
+ /**
3011
+ * Boolean attributes are minimized to their name alone.
3012
+ *
3013
+ * @param {Attr} attribute - Attribute to write
3014
+ * @returns {string} Attribute markup, starting with a space
3015
+ */
3016
+ attributeMarkup(attribute) {
3017
+ const { name, value } = attribute;
3018
+ if (String(value).toLowerCase() === name.toLowerCase()) {
3019
+ return ` ${name}`;
3020
+ }
3021
+ return ` ${name}="${this.escapeAttribute(value)}"`;
3022
+ }
3023
+ /**
3024
+ * Escape character data for HTML.
3025
+ *
3026
+ * @param {string} value - Text content
3027
+ * @returns {string} Escaped text
3028
+ */
3029
+ escapeText(value) {
3030
+ return escapeHtmlText(value);
3031
+ }
3032
+ /**
3033
+ * Escape an attribute value for HTML.
3034
+ *
3035
+ * @param {string} value - Attribute value
3036
+ * @returns {string} Escaped value
3037
+ */
3038
+ escapeAttribute(value) {
3039
+ return escapeHtmlAttribute(value);
3040
+ }
3041
+ };
3042
+
3043
+ // src/xslt/serializer/textSerializer.js
3044
+ function serializeText(node) {
3045
+ if (node.nodeType === NODE_TYPE.TEXT || node.nodeType === NODE_TYPE.CDATA_SECTION) {
3046
+ return node.nodeValue || "";
3047
+ }
3048
+ let text = "";
3049
+ for (const child of node.childNodes || []) {
3050
+ text += serializeText(child);
3051
+ }
3052
+ return text;
3053
+ }
3054
+
3055
+ // src/xslt/serializer.js
3056
+ function serializeResult(node, outputSettings = {}) {
3057
+ if (!node) {
3058
+ return "";
3059
+ }
3060
+ const settings = resolveOutputSettings(outputSettings, node);
3061
+ if (settings.method === "text") {
3062
+ return serializeText(node);
3063
+ }
3064
+ if (settings.method === "html") {
3065
+ return new HtmlWriter(settings).serialize(node);
3066
+ }
3067
+ return new XmlWriter(settings, {
3068
+ xhtml: settings.method === "xhtml"
3069
+ }).serialize(node);
3070
+ }
3071
+
1562
3072
  // src/xslt/engine.js
1563
- var XSLT_NS = "http://www.w3.org/1999/XSL/Transform";
3073
+ var XSLT_NS = XSLT_NAMESPACE;
1564
3074
  var XsltContext = class _XsltContext {
1565
3075
  constructor(options = {}) {
1566
3076
  this.currentNode = options.currentNode;
@@ -1576,6 +3086,8 @@ var XsltContext = class _XsltContext {
1576
3086
  this.decimalFormats = options.decimalFormats || {};
1577
3087
  this.outputMethod = options.outputMethod || "xml";
1578
3088
  this.xpathEvaluator = options.xpathEvaluator || new XPathEvaluator();
3089
+ this.currentTemplate = options.currentTemplate || null;
3090
+ this.currentMode = options.currentMode ?? null;
1579
3091
  }
1580
3092
  clone(overrides = {}) {
1581
3093
  return new _XsltContext({
@@ -1591,7 +3103,9 @@ var XsltContext = class _XsltContext {
1591
3103
  keys: this.keys,
1592
3104
  decimalFormats: this.decimalFormats,
1593
3105
  outputMethod: this.outputMethod,
1594
- xpathEvaluator: this.xpathEvaluator
3106
+ xpathEvaluator: this.xpathEvaluator,
3107
+ currentTemplate: overrides.currentTemplate ?? this.currentTemplate,
3108
+ currentMode: overrides.currentMode ?? this.currentMode
1595
3109
  });
1596
3110
  }
1597
3111
  getVariable(name) {
@@ -1616,7 +3130,9 @@ var XsltEngine = class {
1616
3130
  this.globalParameters = {};
1617
3131
  this.outputSettings = {
1618
3132
  method: "xml",
3133
+ version: "1.0",
1619
3134
  encoding: "UTF-8",
3135
+ standalone: null,
1620
3136
  indent: "no",
1621
3137
  omitXmlDeclaration: "no",
1622
3138
  doctypePublic: null,
@@ -1628,13 +3144,105 @@ var XsltEngine = class {
1628
3144
  this.decimalFormats = {};
1629
3145
  this.stylesheetDoc = null;
1630
3146
  this.attributeSets = {};
1631
- this.namespaceAliases = {};
3147
+ this.namespaceAliases = new NamespaceAliasMap();
1632
3148
  this.stripSpace = [];
1633
3149
  this.preserveSpace = [];
1634
3150
  this.stylesheetLoader = options.stylesheetLoader || null;
1635
3151
  this.currentImportPrecedence = 0;
1636
3152
  this.processedStylesheets = /* @__PURE__ */ new Set();
1637
3153
  this.baseUri = options.baseUri || "";
3154
+ this.documentLoader = options.documentLoader || null;
3155
+ this.loadedDocuments = /* @__PURE__ */ new Map();
3156
+ this.generatedIds = /* @__PURE__ */ new WeakMap();
3157
+ this.generatedIdCount = 0;
3158
+ this.rootContext = null;
3159
+ this.keyRegistry = new KeyIndexRegistry({
3160
+ keys: this.keys,
3161
+ matchesPattern: (node, pattern) => this.matchesPattern(node, pattern, this.rootContext),
3162
+ evaluateUse: (node, expression) => this.evaluateKeyValues(node, expression)
3163
+ });
3164
+ this.xpathEvaluator.registerFunctions(createXsltFunctions(this));
3165
+ }
3166
+ /**
3167
+ * Set the loader used by the XSLT `document()` function.
3168
+ *
3169
+ * The loader is synchronous and must return a `Document`, an XML string or
3170
+ * null. Returning null (or configuring no loader at all) makes `document()`
3171
+ * evaluate to an empty node-set instead of failing the transformation.
3172
+ *
3173
+ * @param {((uri: string, baseUri?: string) => (Document|string|null))|null} loader - The loader, or null to remove it
3174
+ * @returns {XsltEngine} This engine, to allow chaining
3175
+ *
3176
+ * @example
3177
+ * engine.setDocumentLoader((uri) => readFileSync(uri, 'utf8'));
3178
+ */
3179
+ setDocumentLoader(loader) {
3180
+ this.documentLoader = loader ?? null;
3181
+ this.loadedDocuments.clear();
3182
+ return this;
3183
+ }
3184
+ /**
3185
+ * Load an external document for the `document()` function.
3186
+ *
3187
+ * Results are cached per resolved URI for the life of the engine, so the same
3188
+ * URI always yields the identical node-set.
3189
+ *
3190
+ * @param {string} uri - The requested URI, fragment identifiers are ignored
3191
+ * @param {string} [baseUri] - Base URI used to resolve relative references
3192
+ * @returns {Document|null} The loaded document, or null when unavailable
3193
+ *
3194
+ * @example
3195
+ * engine.loadDocument('data.xml', '/styles/main.xsl');
3196
+ */
3197
+ loadDocument(uri, baseUri) {
3198
+ const target = stripFragment(uri);
3199
+ if (target === "") return this.stylesheetDoc;
3200
+ if (!this.documentLoader) return null;
3201
+ const resolved = resolveUri(target, baseUri);
3202
+ if (this.loadedDocuments.has(resolved)) {
3203
+ return this.loadedDocuments.get(resolved);
3204
+ }
3205
+ const loaded = this.documentLoader(resolved, baseUri);
3206
+ const doc = typeof loaded === "string" ? this.parseXmlString(loaded) : loaded || null;
3207
+ this.loadedDocuments.set(resolved, doc);
3208
+ return doc;
3209
+ }
3210
+ /**
3211
+ * Return the stable identifier of a node for `generate-id()`.
3212
+ *
3213
+ * @param {Node} node - The node to identify
3214
+ * @returns {string} An identifier starting with a letter
3215
+ *
3216
+ * @example
3217
+ * engine.generateId(element); // 'N1'
3218
+ */
3219
+ generateId(node) {
3220
+ let id = this.generatedIds.get(node);
3221
+ if (!id) {
3222
+ this.generatedIdCount++;
3223
+ id = `N${this.generatedIdCount}`;
3224
+ this.generatedIds.set(node, id);
3225
+ }
3226
+ return id;
3227
+ }
3228
+ /**
3229
+ * Evaluate the `use` expression of an `xsl:key` for one node.
3230
+ *
3231
+ * @param {Node} node - The node being indexed
3232
+ * @param {string} expression - The `use` expression
3233
+ * @returns {string[]} The key values contributed by the node
3234
+ */
3235
+ evaluateKeyValues(node, expression) {
3236
+ const context = this.rootContext.clone({
3237
+ currentNode: node,
3238
+ currentNodeList: [node],
3239
+ position: 1
3240
+ });
3241
+ const value = this.evaluateXPath(expression, context);
3242
+ if (Array.isArray(value)) {
3243
+ return value.map((item) => this.xpathEvaluator.getStringValue(item));
3244
+ }
3245
+ return [this.xpathEvaluator.toString(value)];
1638
3246
  }
1639
3247
  /**
1640
3248
  * Set the stylesheet loader function for xsl:import and xsl:include
@@ -1645,14 +3253,13 @@ var XsltEngine = class {
1645
3253
  }
1646
3254
  /**
1647
3255
  * Resolve a relative URI against a base URI
3256
+ *
3257
+ * @param {string} href - The URI to resolve
3258
+ * @param {string} [baseUri] - The base URI
3259
+ * @returns {string} The resolved URI
1648
3260
  */
1649
3261
  resolveUri(href, baseUri) {
1650
- if (!baseUri || href.startsWith("http://") || href.startsWith("https://") || href.startsWith("/")) {
1651
- return href;
1652
- }
1653
- const lastSlash = baseUri.lastIndexOf("/");
1654
- const baseDir = lastSlash >= 0 ? baseUri.substring(0, lastSlash + 1) : "";
1655
- return baseDir + href;
3262
+ return resolveUri(href, baseUri);
1656
3263
  }
1657
3264
  /**
1658
3265
  * Load an external stylesheet document
@@ -1774,7 +3381,8 @@ var XsltEngine = class {
1774
3381
  this.currentImportPrecedence = savedPrecedence;
1775
3382
  } catch (error) {
1776
3383
  throw new Error(
1777
- `Failed to include stylesheet "${href}": ${error.message}`
3384
+ `Failed to include stylesheet "${href}": ${error.message}`,
3385
+ { cause: error }
1778
3386
  );
1779
3387
  }
1780
3388
  }
@@ -1802,7 +3410,8 @@ var XsltEngine = class {
1802
3410
  this.currentImportPrecedence++;
1803
3411
  } catch (error) {
1804
3412
  throw new Error(
1805
- `Failed to import stylesheet "${href}": ${error.message}`
3413
+ `Failed to import stylesheet "${href}": ${error.message}`,
3414
+ { cause: error }
1806
3415
  );
1807
3416
  }
1808
3417
  }
@@ -1878,39 +3487,50 @@ var XsltEngine = class {
1878
3487
  }
1879
3488
  }
1880
3489
  }
3490
+ /**
3491
+ * Register a template rule.
3492
+ *
3493
+ * A union match pattern is equivalent to a set of template rules, one per
3494
+ * alternative (XSLT 1.0 section 5.5), so each alternative is registered
3495
+ * separately with its own default priority.
3496
+ *
3497
+ * @param {Element} node - The xsl:template element
3498
+ */
1881
3499
  registerTemplate(node) {
1882
3500
  const match = node.getAttribute("match");
1883
3501
  const name = node.getAttribute("name");
1884
3502
  const mode = node.getAttribute("mode") || null;
1885
3503
  const priorityAttr = node.getAttribute("priority");
1886
- const priority = priorityAttr ? parseFloat(priorityAttr) : this.calculatePriority(match);
1887
- this.templates.push({
1888
- match,
1889
- name,
1890
- mode,
1891
- priority,
1892
- importPrecedence: this.currentImportPrecedence,
1893
- node
1894
- });
3504
+ const alternatives = match ? this.splitUnionPattern(match).map((p) => p.trim()) : [null];
3505
+ for (const alternative of alternatives) {
3506
+ this.templates.push({
3507
+ match: alternative,
3508
+ name,
3509
+ mode,
3510
+ priority: priorityAttr ? parseFloat(priorityAttr) : this.calculatePriority(alternative),
3511
+ importPrecedence: this.currentImportPrecedence,
3512
+ node
3513
+ });
3514
+ }
1895
3515
  }
3516
+ /**
3517
+ * Default priority of a single match pattern (see templatePriority.js).
3518
+ *
3519
+ * @param {string|null} matchPattern - The match pattern
3520
+ * @returns {number} The default priority
3521
+ */
1896
3522
  calculatePriority(matchPattern) {
1897
- if (!matchPattern) return 0.5;
1898
- if (matchPattern === "*" || matchPattern === "node()" || matchPattern === "text()" || matchPattern === "comment()" || matchPattern === "processing-instruction()") {
1899
- return -0.5;
1900
- }
1901
- if (matchPattern.includes(":*")) {
1902
- return -0.25;
1903
- }
1904
- if (/^[a-zA-Z_][\w.-]*$/.test(matchPattern)) {
1905
- return 0;
1906
- }
1907
- return 0.5;
3523
+ return calculatePriority(matchPattern ? matchPattern.trim() : matchPattern);
1908
3524
  }
1909
3525
  processOutput(node) {
1910
3526
  const method = node.getAttribute("method");
1911
3527
  if (method) this.outputSettings.method = method;
3528
+ const version = node.getAttribute("version");
3529
+ if (version) this.outputSettings.version = version;
1912
3530
  const encoding = node.getAttribute("encoding");
1913
3531
  if (encoding) this.outputSettings.encoding = encoding;
3532
+ const standalone = node.getAttribute("standalone");
3533
+ if (standalone) this.outputSettings.standalone = standalone;
1914
3534
  const indent = node.getAttribute("indent");
1915
3535
  if (indent) this.outputSettings.indent = indent;
1916
3536
  const omit = node.getAttribute("omit-xml-declaration");
@@ -1931,16 +3551,80 @@ var XsltEngine = class {
1931
3551
  const select2 = node.getAttribute("select");
1932
3552
  this.globalVariables[name] = { node, select: select2 };
1933
3553
  }
3554
+ /**
3555
+ * Register an `xsl:param` top level declaration.
3556
+ *
3557
+ * A value supplied from outside (through `setParameter`) has precedence over
3558
+ * the declared default, so it survives compilation of the stylesheet.
3559
+ *
3560
+ * @param {Element} node - The `xsl:param` element
3561
+ * @returns {void}
3562
+ */
1934
3563
  processGlobalParam(node) {
1935
3564
  const name = node.getAttribute("name");
1936
3565
  const select2 = node.getAttribute("select");
1937
- this.globalParameters[name] = { node, select: select2 };
3566
+ const existing = this.globalParameters[name];
3567
+ const definition = { node, select: select2 };
3568
+ if (existing && "value" in existing) {
3569
+ definition.value = existing.value;
3570
+ }
3571
+ this.globalParameters[name] = definition;
3572
+ }
3573
+ /**
3574
+ * Supply the value of a global parameter from outside the stylesheet.
3575
+ *
3576
+ * The value is merged into the `xsl:param` declaration when there is one, so
3577
+ * removing the value later restores the declared default.
3578
+ *
3579
+ * @param {string} name - The parameter name, `{uri}local` when namespaced
3580
+ * @param {*} value - The value to use
3581
+ * @returns {void}
3582
+ *
3583
+ * @example
3584
+ * engine.setParameterValue('sortOrder', 'ascending');
3585
+ */
3586
+ setParameterValue(name, value) {
3587
+ const definition = this.globalParameters[name];
3588
+ if (definition) definition.value = value;
3589
+ else this.globalParameters[name] = { value };
3590
+ }
3591
+ /**
3592
+ * Remove an externally supplied parameter value.
3593
+ *
3594
+ * The `xsl:param` declaration of the stylesheet is kept, so the parameter
3595
+ * falls back to its declared default instead of becoming undefined.
3596
+ *
3597
+ * @param {string} name - The parameter name, `{uri}local` when namespaced
3598
+ * @returns {void}
3599
+ *
3600
+ * @example
3601
+ * engine.clearParameterValue('sortOrder');
3602
+ */
3603
+ clearParameterValue(name) {
3604
+ const definition = this.globalParameters[name];
3605
+ if (!definition) return;
3606
+ if (definition.node) delete definition.value;
3607
+ else delete this.globalParameters[name];
3608
+ }
3609
+ /**
3610
+ * Remove every externally supplied parameter value.
3611
+ *
3612
+ * @returns {void}
3613
+ *
3614
+ * @example
3615
+ * engine.clearParameterValues();
3616
+ */
3617
+ clearParameterValues() {
3618
+ for (const name of Object.keys(this.globalParameters)) {
3619
+ this.clearParameterValue(name);
3620
+ }
1938
3621
  }
1939
3622
  processKey(node) {
1940
3623
  const name = node.getAttribute("name");
1941
3624
  const match = node.getAttribute("match");
1942
3625
  const use = node.getAttribute("use");
1943
3626
  this.keys[name] = { match, use };
3627
+ this.keyRegistry.clear();
1944
3628
  }
1945
3629
  processDecimalFormat(node) {
1946
3630
  const name = node.getAttribute("name") || "";
@@ -1958,9 +3642,7 @@ var XsltEngine = class {
1958
3642
  };
1959
3643
  }
1960
3644
  processNamespaceAlias(node) {
1961
- const stylesheet = node.getAttribute("stylesheet-prefix");
1962
- const result = node.getAttribute("result-prefix");
1963
- this.namespaceAliases[stylesheet] = result;
3645
+ this.namespaceAliases.add(node);
1964
3646
  }
1965
3647
  processAttributeSet(node) {
1966
3648
  const name = node.getAttribute("name");
@@ -1990,11 +3672,13 @@ var XsltEngine = class {
1990
3672
  if (!doc) {
1991
3673
  throw new Error("No output document available");
1992
3674
  }
3675
+ const resultDocument = createResultDocument(doc);
3676
+ const source = this.prepareSource(sourceNode, doc);
1993
3677
  const context = new XsltContext({
1994
- currentNode: sourceNode,
1995
- currentNodeList: [sourceNode],
3678
+ currentNode: source,
3679
+ currentNodeList: [source],
1996
3680
  position: 1,
1997
- outputDocument: doc,
3681
+ outputDocument: resultDocument,
1998
3682
  stylesheet: this.stylesheetDoc,
1999
3683
  namespaces: { ...this.namespaces },
2000
3684
  templates: this.templates,
@@ -2003,6 +3687,7 @@ var XsltEngine = class {
2003
3687
  outputMethod: this.outputSettings.method,
2004
3688
  xpathEvaluator: this.xpathEvaluator
2005
3689
  });
3690
+ this.rootContext = context;
2006
3691
  for (const [name, def] of Object.entries(this.globalParameters)) {
2007
3692
  if (!(name in context.parameters)) {
2008
3693
  context.parameters[name] = this.evaluateVariable(def, context);
@@ -2011,33 +3696,91 @@ var XsltEngine = class {
2011
3696
  for (const [name, def] of Object.entries(this.globalVariables)) {
2012
3697
  context.variables[name] = this.evaluateVariable(def, context);
2013
3698
  }
2014
- const fragment = doc.createDocumentFragment();
2015
- this.applyTemplates(
2016
- [sourceNode.documentElement || sourceNode],
2017
- null,
2018
- context,
2019
- fragment
3699
+ const fragment = resultDocument.createDocumentFragment();
3700
+ this.applyTemplates([source], null, context, fragment);
3701
+ return importResultFragment(fragment, doc);
3702
+ }
3703
+ /**
3704
+ * Apply `xsl:strip-space` to the source tree.
3705
+ *
3706
+ * Stripping produces a copy so the caller's document is never modified; when
3707
+ * no `xsl:strip-space` is declared the original node is used unchanged.
3708
+ *
3709
+ * @param {Node} sourceNode - The source document or element
3710
+ * @param {Document} ownerDocument - Document providing the DOM implementation
3711
+ * @returns {Node} The source to transform
3712
+ */
3713
+ prepareSource(sourceNode, ownerDocument) {
3714
+ const filter = new WhitespaceFilter(this.stripSpace, this.preserveSpace);
3715
+ if (!filter.isActive()) return sourceNode;
3716
+ return stripWhitespaceNodes(
3717
+ sourceNode,
3718
+ filter,
3719
+ createResultDocument(ownerDocument)
2020
3720
  );
2021
- return fragment;
2022
3721
  }
2023
3722
  /**
2024
3723
  * Transform to a complete document
2025
3724
  */
2026
3725
  transformToDocument(sourceNode) {
2027
- const doc = this.createDocument();
3726
+ const doc = this.createDocument(sourceNode);
2028
3727
  const fragment = this.transform(sourceNode, doc);
2029
3728
  while (fragment.firstChild) {
2030
3729
  doc.appendChild(fragment.firstChild);
2031
3730
  }
2032
3731
  return doc;
2033
3732
  }
2034
- createDocument() {
3733
+ /**
3734
+ * Transform a source document and serialize the result to a string.
3735
+ *
3736
+ * Non-W3C convenience method: the result tree is serialized honoring the
3737
+ * `xsl:output` settings of the stylesheet (XSLT 1.0 section 16).
3738
+ *
3739
+ * @param {Node} sourceNode - Source document or element to transform
3740
+ * @returns {string} The serialized transformation result
3741
+ */
3742
+ transformToString(sourceNode) {
3743
+ const fragment = this.transform(
3744
+ sourceNode,
3745
+ this.createDocument(sourceNode)
3746
+ );
3747
+ return serializeResult(fragment, this.outputSettings);
3748
+ }
3749
+ /**
3750
+ * Create an empty XML document to hold a transformation result.
3751
+ *
3752
+ * Uses the global `document` when running in a browser and otherwise falls
3753
+ * back to the DOM implementation owning `referenceNode` (e.g. a jsdom or
3754
+ * xmldom document in Node.js).
3755
+ *
3756
+ * @param {Node} [referenceNode] - Any node whose DOM implementation can be reused
3757
+ * @returns {Document} A new empty document
3758
+ * @throws {Error} When no DOM implementation is available
3759
+ */
3760
+ createDocument(referenceNode) {
2035
3761
  if (typeof document !== "undefined") {
2036
3762
  return document.implementation.createDocument(null, null, null);
2037
3763
  }
3764
+ const ownerDocument = referenceNode && (referenceNode.nodeType === 9 ? referenceNode : referenceNode.ownerDocument);
3765
+ if (ownerDocument?.implementation) {
3766
+ return ownerDocument.implementation.createDocument(null, null, null);
3767
+ }
2038
3768
  throw new Error("Document creation not available in this environment");
2039
3769
  }
3770
+ /**
3771
+ * Compute the value of a variable or parameter definition.
3772
+ *
3773
+ * A value supplied from outside (`setParameter`) wins over the `select`
3774
+ * expression and over the instantiated content of the declaration.
3775
+ *
3776
+ * @param {{value?: *, select?: string, node?: Element}} def - The definition
3777
+ * @param {XsltContext} context - The context used for evaluation
3778
+ * @returns {*} The variable value
3779
+ */
2040
3780
  evaluateVariable(def, context) {
3781
+ if ("value" in def) {
3782
+ return def.value;
3783
+ }
2041
3784
  if (def.select) {
2042
3785
  return this.evaluateXPath(def.select, context);
2043
3786
  }
@@ -2057,7 +3800,9 @@ var XsltEngine = class {
2057
3800
  const newContext = context.clone({
2058
3801
  currentNode: node,
2059
3802
  currentNodeList: nodeList,
2060
- position: i + 1
3803
+ position: i + 1,
3804
+ currentTemplate: template,
3805
+ currentMode: mode
2061
3806
  });
2062
3807
  this.processTemplate(template.node, newContext, output);
2063
3808
  } else {
@@ -2068,13 +3813,14 @@ var XsltEngine = class {
2068
3813
  /**
2069
3814
  * Find the best matching template for a node
2070
3815
  */
2071
- findMatchingTemplate(node, mode, context) {
3816
+ findMatchingTemplate(node, mode, context, maxImportPrecedence = Infinity) {
2072
3817
  let bestMatch = null;
2073
3818
  let bestPriority = -Infinity;
2074
3819
  let bestImportPrecedence = -Infinity;
2075
3820
  for (const template of this.templates) {
2076
3821
  if (template.mode !== mode) continue;
2077
3822
  if (!template.match) continue;
3823
+ if ((template.importPrecedence || 0) >= maxImportPrecedence) continue;
2078
3824
  if (this.matchesPattern(node, template.match, context)) {
2079
3825
  const priority = template.priority;
2080
3826
  const importPrecedence = template.importPrecedence || 0;
@@ -2147,19 +3893,22 @@ var XsltEngine = class {
2147
3893
  1,
2148
3894
  1,
2149
3895
  { ...context.variables, ...context.parameters },
2150
- context.namespaces
3896
+ context.namespaces,
3897
+ context
2151
3898
  );
2152
3899
  const result2 = this.xpathEvaluator.evaluate(ast, xpathContext2);
2153
3900
  const nodes2 = Array.isArray(result2) ? result2 : [result2];
2154
3901
  return nodes2.includes(node);
2155
3902
  }
2156
- if (node.parentNode) {
3903
+ const parent = node.nodeType === 2 ? node.ownerElement : node.parentNode;
3904
+ if (parent) {
2157
3905
  const xpathContext2 = new XPathContext(
2158
- node.parentNode,
3906
+ parent,
2159
3907
  1,
2160
3908
  1,
2161
3909
  { ...context.variables, ...context.parameters },
2162
- context.namespaces
3910
+ context.namespaces,
3911
+ context
2163
3912
  );
2164
3913
  const result2 = this.xpathEvaluator.evaluate(ast, xpathContext2);
2165
3914
  const nodes2 = Array.isArray(result2) ? result2 : [result2];
@@ -2170,7 +3919,8 @@ var XsltEngine = class {
2170
3919
  1,
2171
3920
  1,
2172
3921
  { ...context.variables, ...context.parameters },
2173
- context.namespaces
3922
+ context.namespaces,
3923
+ context
2174
3924
  );
2175
3925
  const result = this.xpathEvaluator.evaluate(ast, xpathContext);
2176
3926
  const nodes = Array.isArray(result) ? result : [result];
@@ -2284,6 +4034,9 @@ var XsltEngine = class {
2284
4034
  case "apply-templates":
2285
4035
  this.xslApplyTemplates(node, context, output);
2286
4036
  break;
4037
+ case "apply-imports":
4038
+ this.xslApplyImports(node, context, output);
4039
+ break;
2287
4040
  case "call-template":
2288
4041
  this.xslCallTemplate(node, context, output);
2289
4042
  break;
@@ -2343,34 +4096,47 @@ var XsltEngine = class {
2343
4096
  }
2344
4097
  /**
2345
4098
  * Process a literal result element (non-XSLT)
4099
+ *
4100
+ * Applies `xsl:namespace-alias` to the element and its attributes, honours
4101
+ * `xsl:use-attribute-sets` and keeps XSLT-only attributes and namespace
4102
+ * declarations out of the result tree.
4103
+ *
4104
+ * @param {Element} node - The literal result element in the stylesheet
4105
+ * @param {XsltContext} context - The current XSLT context
4106
+ * @param {Node} output - The result tree node receiving the element
4107
+ * @returns {void}
2346
4108
  */
2347
4109
  processLiteralResultElement(node, context, output) {
2348
- let outputElement;
2349
- const namespaceURI = node.namespaceURI;
2350
- const nodeName = node.nodeName;
2351
- let resolvedNS = namespaceURI;
2352
- if (namespaceURI) {
2353
- for (const [from, to] of Object.entries(this.namespaceAliases)) {
2354
- if (this.namespaces[from] === namespaceURI) {
2355
- resolvedNS = this.namespaces[to] || to;
2356
- break;
2357
- }
2358
- }
2359
- }
2360
- if (resolvedNS && context.outputDocument.createElementNS) {
2361
- outputElement = context.outputDocument.createElementNS(
2362
- resolvedNS,
2363
- nodeName
2364
- );
2365
- } else {
2366
- outputElement = context.outputDocument.createElement(nodeName);
4110
+ const localName = node.localName || node.nodeName;
4111
+ const alias = this.namespaceAliases.resolve(node.namespaceURI, localName);
4112
+ const namespaceUri = alias ? alias.namespaceUri : node.namespaceURI;
4113
+ const qname = alias ? alias.qname : node.nodeName;
4114
+ const outputElement = namespaceUri && context.outputDocument.createElementNS ? context.outputDocument.createElementNS(namespaceUri, qname) : context.outputDocument.createElement(qname);
4115
+ const useAttributeSets = getXsltAttribute(
4116
+ node,
4117
+ "use-attribute-sets",
4118
+ XSLT_NS
4119
+ );
4120
+ if (useAttributeSets) {
4121
+ this.applyAttributeSets(useAttributeSets, context, outputElement);
2367
4122
  }
2368
4123
  if (node.attributes) {
2369
4124
  for (const attr of node.attributes) {
2370
- if (attr.namespaceURI === XSLT_NS) continue;
2371
- if (attr.name.startsWith("xmlns")) continue;
4125
+ if (!shouldCopyAttribute(attr, XSLT_NS)) continue;
2372
4126
  const value = this.processAttributeValueTemplate(attr.value, context);
2373
- outputElement.setAttribute(attr.name, value);
4127
+ const attrAlias = this.namespaceAliases.resolve(
4128
+ attr.namespaceURI,
4129
+ attr.localName || attr.name
4130
+ );
4131
+ if (attrAlias) {
4132
+ outputElement.setAttributeNS(
4133
+ attrAlias.namespaceUri,
4134
+ attrAlias.qname,
4135
+ value
4136
+ );
4137
+ } else {
4138
+ outputElement.setAttribute(attr.name, value);
4139
+ }
2374
4140
  }
2375
4141
  }
2376
4142
  this.processChildren(node, context, outputElement);
@@ -2457,6 +4223,38 @@ var XsltEngine = class {
2457
4223
  });
2458
4224
  this.applyTemplates(nodes, mode, newContext, output);
2459
4225
  }
4226
+ /**
4227
+ * Instantiate `xsl:apply-imports`.
4228
+ *
4229
+ * Only templates with a lower import precedence than the template being
4230
+ * instantiated are considered; when none matches, the built-in template rules
4231
+ * apply, exactly as for `xsl:apply-templates`.
4232
+ *
4233
+ * @param {Element} node - The `xsl:apply-imports` element
4234
+ * @param {XsltContext} context - The current XSLT context
4235
+ * @param {Node} output - The result tree node receiving the output
4236
+ * @returns {void}
4237
+ */
4238
+ xslApplyImports(node, context, output) {
4239
+ const currentNode = context.currentNode;
4240
+ const mode = context.currentMode ?? null;
4241
+ const precedence = context.currentTemplate ? context.currentTemplate.importPrecedence || 0 : 0;
4242
+ const template = this.findMatchingTemplate(
4243
+ currentNode,
4244
+ mode,
4245
+ context,
4246
+ precedence
4247
+ );
4248
+ if (!template) {
4249
+ this.applyBuiltinTemplate(currentNode, mode, context, output);
4250
+ return;
4251
+ }
4252
+ this.processTemplate(
4253
+ template.node,
4254
+ context.clone({ currentTemplate: template }),
4255
+ output
4256
+ );
4257
+ }
2460
4258
  xslCallTemplate(node, context, output) {
2461
4259
  const name = node.getAttribute("name");
2462
4260
  const template = this.templates.find((t) => t.name === name);
@@ -2781,79 +4579,47 @@ var XsltEngine = class {
2781
4579
  xslNumber(node, context, output) {
2782
4580
  const value = node.getAttribute("value");
2783
4581
  const format = node.getAttribute("format") || "1";
2784
- const level = node.getAttribute("level") || "single";
2785
- let number;
4582
+ let numbers;
2786
4583
  if (value) {
2787
- number = Math.round(
2788
- this.xpathEvaluator.toNumber(this.evaluateXPath(value, context))
2789
- );
4584
+ numbers = [
4585
+ Math.round(
4586
+ this.xpathEvaluator.toNumber(this.evaluateXPath(value, context))
4587
+ )
4588
+ ];
2790
4589
  } else {
2791
- number = this.countNumber(context.currentNode, level, node, context);
4590
+ numbers = countXsltNumber(
4591
+ context.currentNode,
4592
+ {
4593
+ level: node.getAttribute("level") || "single",
4594
+ count: node.getAttribute("count"),
4595
+ from: node.getAttribute("from")
4596
+ },
4597
+ (candidate, pattern) => this.matchesPattern(candidate, pattern, context)
4598
+ );
2792
4599
  }
2793
- const formatted = this.formatNumber(number, format);
2794
- const text = context.outputDocument.createTextNode(formatted);
4600
+ const text = context.outputDocument.createTextNode(
4601
+ formatXsltNumber(numbers, format)
4602
+ );
2795
4603
  output.appendChild(text);
2796
4604
  }
2797
- countNumber(node, level, spec, context) {
2798
- const count = spec.getAttribute("count");
2799
- const _from = spec.getAttribute("from");
2800
- if (level === "single") {
2801
- let n = 1;
2802
- let sibling = node.previousSibling;
2803
- while (sibling) {
2804
- if (sibling.nodeType === 1) {
2805
- if (!count || this.matchesPattern(sibling, count, context)) {
2806
- n++;
2807
- }
2808
- }
2809
- sibling = sibling.previousSibling;
2810
- }
2811
- return n;
2812
- }
2813
- return 1;
2814
- }
4605
+ /**
4606
+ * Format a single number with an `xsl:number` format token.
4607
+ *
4608
+ * @param {number} number - The number to format
4609
+ * @param {string} format - The format token, e.g. `1`, `01`, `a`, `I`
4610
+ * @returns {string} The formatted number
4611
+ */
2815
4612
  formatNumber(number, format) {
2816
- if (/^[0-9]+$/.test(format)) {
2817
- return String(number).padStart(format.length, "0");
2818
- }
2819
- if (format === "a") {
2820
- return String.fromCharCode(96 + (number - 1) % 26 + 1);
2821
- }
2822
- if (format === "A") {
2823
- return String.fromCharCode(64 + (number - 1) % 26 + 1);
2824
- }
2825
- if (format === "i") {
2826
- return this.toRoman(number).toLowerCase();
2827
- }
2828
- if (format === "I") {
2829
- return this.toRoman(number);
2830
- }
2831
- return String(number);
4613
+ return formatXsltNumber([number], format);
2832
4614
  }
4615
+ /**
4616
+ * Convert a number to an upper case Roman numeral.
4617
+ *
4618
+ * @param {number} num - The number to convert
4619
+ * @returns {string} The Roman numeral
4620
+ */
2833
4621
  toRoman(num) {
2834
- const romanNumerals = [
2835
- ["M", 1e3],
2836
- ["CM", 900],
2837
- ["D", 500],
2838
- ["CD", 400],
2839
- ["C", 100],
2840
- ["XC", 90],
2841
- ["L", 50],
2842
- ["XL", 40],
2843
- ["X", 10],
2844
- ["IX", 9],
2845
- ["V", 5],
2846
- ["IV", 4],
2847
- ["I", 1]
2848
- ];
2849
- let result = "";
2850
- for (const [numeral, value] of romanNumerals) {
2851
- while (num >= value) {
2852
- result += numeral;
2853
- num -= value;
2854
- }
2855
- }
2856
- return result;
4622
+ return toRoman(num);
2857
4623
  }
2858
4624
  xslMessage(node, context, _output) {
2859
4625
  const terminate = node.getAttribute("terminate") === "yes";
@@ -2937,7 +4703,8 @@ var XsltEngine = class {
2937
4703
  context.position,
2938
4704
  context.currentNodeList.length,
2939
4705
  { ...context.variables, ...context.parameters },
2940
- context.namespaces
4706
+ context.namespaces,
4707
+ context
2941
4708
  );
2942
4709
  return this.xpathEvaluator.evaluate(ast, xpathContext);
2943
4710
  }
@@ -2957,6 +4724,94 @@ var XSLTProcessor = class {
2957
4724
  this._engine = null;
2958
4725
  this._stylesheet = null;
2959
4726
  this._parameters = /* @__PURE__ */ new Map();
4727
+ this._stylesheetLoader = null;
4728
+ this._documentLoader = null;
4729
+ }
4730
+ /**
4731
+ * The underlying XSLT engine (advanced usage).
4732
+ *
4733
+ * The engine is created lazily by {@link XSLTProcessor#importStylesheet},
4734
+ * so this getter returns `null` until a stylesheet has been imported.
4735
+ * Prefer the public {@link XSLTProcessor#setStylesheetLoader} over reaching
4736
+ * into the engine directly.
4737
+ *
4738
+ * @returns {import('./xslt/engine.js').XsltEngine|null} The engine, or null before import
4739
+ *
4740
+ * @example
4741
+ * processor.importStylesheet(xslDoc, '/styles/main.xsl');
4742
+ * console.log(processor.engine.outputSettings.method);
4743
+ */
4744
+ get engine() {
4745
+ return this._engine;
4746
+ }
4747
+ /**
4748
+ * Sets the loader used to resolve `xsl:import` and `xsl:include` references.
4749
+ *
4750
+ * The loader is synchronous: it MUST return the external stylesheet as a
4751
+ * `Document` or as an XML string (which is parsed automatically). Promises
4752
+ * are not awaited by the engine, so pre-load remote stylesheets before
4753
+ * calling `importStylesheet`.
4754
+ *
4755
+ * The loader may be set before or after `importStylesheet`. When set before,
4756
+ * it is passed to the engine on creation, which is required for the loader to
4757
+ * be used while the stylesheet is being compiled. When set after, the live
4758
+ * engine is updated as well.
4759
+ *
4760
+ * @param {((href: string, baseUri?: string) => (Document|string))|null} loader
4761
+ * The loader function, or null to remove a previously configured loader
4762
+ * @returns {XSLTProcessor} This processor, to allow chaining
4763
+ * @throws {TypeError} If the loader is neither a function nor null
4764
+ *
4765
+ * @example
4766
+ * processor.setStylesheetLoader((href) => readFileSync(href, 'utf8'));
4767
+ * processor.importStylesheet(mainStylesheet, '/styles/main.xsl');
4768
+ */
4769
+ setStylesheetLoader(loader) {
4770
+ if (loader !== null && loader !== void 0 && typeof loader !== "function") {
4771
+ throw new TypeError(
4772
+ "Failed to execute 'setStylesheetLoader' on 'XSLTProcessor': The loader argument must be a function or null."
4773
+ );
4774
+ }
4775
+ this._stylesheetLoader = loader ?? null;
4776
+ if (this._engine) {
4777
+ this._engine.setStylesheetLoader(this._stylesheetLoader);
4778
+ }
4779
+ return this;
4780
+ }
4781
+ /**
4782
+ * Sets the loader used to resolve the XSLT `document()` function.
4783
+ *
4784
+ * The loader is synchronous: it MUST return the referenced document as a
4785
+ * `Document`, as an XML string (which is parsed automatically) or as `null`
4786
+ * when the document cannot be provided. Returning `null`, like configuring no
4787
+ * loader at all, makes `document()` evaluate to an empty node-set rather than
4788
+ * failing the transformation.
4789
+ *
4790
+ * The loader may be set before or after `importStylesheet`; a live engine is
4791
+ * kept in sync.
4792
+ *
4793
+ * @param {((uri: string, baseUri?: string) => (Document|string|null))|null} loader
4794
+ * The loader function, or null to remove a previously configured loader
4795
+ * @returns {XSLTProcessor} This processor, to allow chaining
4796
+ * @throws {TypeError} If the loader is neither a function nor null
4797
+ *
4798
+ * @example
4799
+ * // Node.js: resolve document() against the file system
4800
+ * import { readFileSync } from 'node:fs';
4801
+ * processor.setDocumentLoader((uri) => readFileSync(uri, 'utf8'));
4802
+ * processor.importStylesheet(xslDoc, '/styles/main.xsl');
4803
+ */
4804
+ setDocumentLoader(loader) {
4805
+ if (loader !== null && loader !== void 0 && typeof loader !== "function") {
4806
+ throw new TypeError(
4807
+ "Failed to execute 'setDocumentLoader' on 'XSLTProcessor': The loader argument must be a function or null."
4808
+ );
4809
+ }
4810
+ this._documentLoader = loader ?? null;
4811
+ if (this._engine) {
4812
+ this._engine.setDocumentLoader(this._documentLoader);
4813
+ }
4814
+ return this;
2960
4815
  }
2961
4816
  /**
2962
4817
  * Imports the XSLT stylesheet.
@@ -2966,14 +4821,17 @@ var XSLTProcessor = class {
2966
4821
  * <xsl:stylesheet> or <xsl:transform> element.
2967
4822
  *
2968
4823
  * @param {Node} style - The XSLT stylesheet to import (Document or Element)
4824
+ * @param {string} [stylesheetUri] - Optional URI of the stylesheet, used as the
4825
+ * base URI when resolving relative `xsl:import`/`xsl:include` hrefs. When
4826
+ * omitted, hrefs are passed to the loader unresolved.
2969
4827
  * @returns {void}
2970
4828
  *
2971
4829
  * @example
2972
4830
  * const parser = new DOMParser();
2973
4831
  * const xslDoc = parser.parseFromString(xslText, 'application/xml');
2974
- * processor.importStylesheet(xslDoc);
4832
+ * processor.importStylesheet(xslDoc, '/styles/main.xsl');
2975
4833
  */
2976
- importStylesheet(style) {
4834
+ importStylesheet(style, stylesheetUri) {
2977
4835
  if (!style) {
2978
4836
  throw new TypeError(
2979
4837
  "Failed to execute 'importStylesheet' on 'XSLTProcessor': 1 argument required, but only 0 present."
@@ -2989,11 +4847,14 @@ var XSLTProcessor = class {
2989
4847
  throw new Error("XSLT stylesheet contains parse errors");
2990
4848
  }
2991
4849
  this._stylesheet = style;
2992
- this._engine = new XsltEngine();
4850
+ this._engine = new XsltEngine({
4851
+ stylesheetLoader: this._stylesheetLoader,
4852
+ documentLoader: this._documentLoader
4853
+ });
2993
4854
  for (const [key, value] of this._parameters) {
2994
- this._engine.globalParameters[key] = { value };
4855
+ this._engine.setParameterValue(key, value);
2995
4856
  }
2996
- this._engine.importStylesheet(style);
4857
+ this._engine.importStylesheet(style, stylesheetUri);
2997
4858
  }
2998
4859
  /**
2999
4860
  * Transforms the node source by applying the XSLT stylesheet.
@@ -3074,6 +4935,45 @@ var XSLTProcessor = class {
3074
4935
  return null;
3075
4936
  }
3076
4937
  }
4938
+ /**
4939
+ * Transforms the node source by applying the XSLT stylesheet and serializes
4940
+ * the result to a string honoring the stylesheet `xsl:output` settings.
4941
+ *
4942
+ * Non-W3C convenience method: the native XSLTProcessor has no equivalent.
4943
+ * Output method, indentation, XML declaration, document type declaration,
4944
+ * CDATA sections and `disable-output-escaping` are all honored
4945
+ * (XSLT 1.0 section 16).
4946
+ *
4947
+ * @param {Node} source - The XML document to transform
4948
+ * @returns {string|null} The serialized result, or null on a transformation error
4949
+ *
4950
+ * @example
4951
+ * const xml = processor.transformToString(xmlDoc);
4952
+ * // '<?xml version="1.0" encoding="UTF-8"?>\n<BAR>\n <QUX/>\n</BAR>'
4953
+ */
4954
+ transformToString(source) {
4955
+ if (!source) {
4956
+ throw new TypeError(
4957
+ "Failed to execute 'transformToString' on 'XSLTProcessor': 1 argument required, but only 0 present."
4958
+ );
4959
+ }
4960
+ if (!this._engine || !this._stylesheet) {
4961
+ throw new Error(
4962
+ "Failed to execute 'transformToString' on 'XSLTProcessor': No stylesheet has been imported."
4963
+ );
4964
+ }
4965
+ if (source.nodeType !== 1 && source.nodeType !== 9 && source.nodeType !== 11) {
4966
+ throw new TypeError(
4967
+ "Failed to execute 'transformToString' on 'XSLTProcessor': The source is not a valid node type."
4968
+ );
4969
+ }
4970
+ try {
4971
+ return this._engine.transformToString(source);
4972
+ } catch (error) {
4973
+ console.error("XSLT transformation error:", error);
4974
+ return null;
4975
+ }
4976
+ }
3077
4977
  /**
3078
4978
  * Sets a parameter in the XSLT stylesheet.
3079
4979
  *
@@ -3100,7 +5000,7 @@ var XSLTProcessor = class {
3100
5000
  const key = namespaceURI ? `{${namespaceURI}}${localName}` : localName;
3101
5001
  this._parameters.set(key, value);
3102
5002
  if (this._engine) {
3103
- this._engine.globalParameters[key] = { value };
5003
+ this._engine.setParameterValue(key, value);
3104
5004
  }
3105
5005
  }
3106
5006
  /**
@@ -3157,7 +5057,7 @@ var XSLTProcessor = class {
3157
5057
  const key = namespaceURI ? `{${namespaceURI}}${localName}` : localName;
3158
5058
  this._parameters.delete(key);
3159
5059
  if (this._engine) {
3160
- delete this._engine.globalParameters[key];
5060
+ this._engine.clearParameterValue(key);
3161
5061
  }
3162
5062
  }
3163
5063
  /**
@@ -3173,12 +5073,19 @@ var XSLTProcessor = class {
3173
5073
  clearParameters() {
3174
5074
  this._parameters.clear();
3175
5075
  if (this._engine) {
3176
- this._engine.globalParameters = {};
5076
+ this._engine.clearParameterValues();
3177
5077
  }
3178
5078
  }
3179
5079
  /**
3180
5080
  * Removes all parameters and stylesheets from the XSLTProcessor.
3181
5081
  *
5082
+ * Per the W3C `XSLTProcessor` semantics, `reset()` clears stylesheet state and
5083
+ * parameters only. The stylesheet and document loaders are processor
5084
+ * configuration rather than stylesheet state, so they are deliberately
5085
+ * preserved and stay effective for the next `importStylesheet()` call. Pass
5086
+ * `null` to {@link XSLTProcessor#setStylesheetLoader} or
5087
+ * {@link XSLTProcessor#setDocumentLoader} to remove them explicitly.
5088
+ *
3182
5089
  * @returns {void}
3183
5090
  *
3184
5091
  * @example
@@ -3247,9 +5154,9 @@ function selectFirst(expression, contextNode, options = {}) {
3247
5154
  }
3248
5155
 
3249
5156
  // src/index.js
3250
- var VERSION = "1.0.0";
5157
+ var VERSION = "1.1.1";
3251
5158
  var isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
3252
- var isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
5159
+ var isNode = typeof process !== "undefined" && process.versions?.node != null;
3253
5160
  export {
3254
5161
  VERSION,
3255
5162
  XPathContext,
@@ -3264,8 +5171,12 @@ export {
3264
5171
  isBrowser,
3265
5172
  isNativeXSLTSupported,
3266
5173
  isNode,
5174
+ isRawText,
5175
+ markRawText,
3267
5176
  parse as parseXPath,
5177
+ resolveOutputSettings,
3268
5178
  selectFirst as selectFirstXPath,
3269
- select as selectXPath
5179
+ select as selectXPath,
5180
+ serializeResult
3270
5181
  };
3271
5182
  //# sourceMappingURL=xslt-processor.js.map