@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
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Namespace Declaration Tracking
3
+ *
4
+ * Computes the `xmlns` declarations an element has to carry so that every
5
+ * namespace is declared where it is first used and never twice.
6
+ */
7
+
8
+ import { XMLNS_NAMESPACE, XML_NAMESPACE } from "./constants.js";
9
+
10
+ /**
11
+ * Create the namespace scope in effect above the result tree root.
12
+ *
13
+ * @returns {Map<string, string>} Prefix (empty string for the default) to URI
14
+ */
15
+ export function createNamespaceScope() {
16
+ return new Map([
17
+ ["", ""],
18
+ ["xml", XML_NAMESPACE],
19
+ ]);
20
+ }
21
+
22
+ /**
23
+ * Collect the namespace declarations an element must emit.
24
+ *
25
+ * @param {Element} element - Element being serialized
26
+ * @param {Map<string, string>} scope - Namespace scope inherited from the parent
27
+ * @returns {{declarations: Array<{prefix: string, uri: string}>, scope: Map<string, string>}}
28
+ * The declarations to write and the scope in effect for the children
29
+ */
30
+ export function collectNamespaceDeclarations(element, scope) {
31
+ const declarations = [];
32
+ let next = scope;
33
+
34
+ /**
35
+ * Record a declaration when the binding is not already in scope.
36
+ * @param {string} prefix - Namespace prefix, empty for the default namespace
37
+ * @param {string} uri - Namespace URI
38
+ * @returns {void}
39
+ */
40
+ const declare = (prefix, uri) => {
41
+ if (next.get(prefix) === uri) {
42
+ return;
43
+ }
44
+ if (next === scope) {
45
+ next = new Map(scope);
46
+ }
47
+ next.set(prefix, uri);
48
+ declarations.push({ prefix, uri });
49
+ };
50
+
51
+ declare(element.prefix || "", element.namespaceURI || "");
52
+
53
+ const attributes = Array.from(element.attributes || []);
54
+
55
+ for (const attribute of attributes) {
56
+ if (attribute.namespaceURI !== XMLNS_NAMESPACE && attribute.prefix) {
57
+ declare(attribute.prefix, attribute.namespaceURI || "");
58
+ }
59
+ }
60
+
61
+ for (const attribute of attributes) {
62
+ if (attribute.namespaceURI === XMLNS_NAMESPACE) {
63
+ declare(attribute.prefix ? attribute.localName : "", attribute.value);
64
+ }
65
+ }
66
+
67
+ return { declarations, scope: next };
68
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Raw Text Registry
3
+ *
4
+ * Tracks the text nodes produced with `disable-output-escaping="yes"`
5
+ * (XSLT 1.0 section 16.4) so the serializers can emit them verbatim.
6
+ */
7
+
8
+ /**
9
+ * Text nodes whose content must be written without escaping.
10
+ * @type {WeakSet<Node>}
11
+ */
12
+ export const rawTextNodes = new WeakSet();
13
+
14
+ /**
15
+ * Mark a text node as produced with `disable-output-escaping="yes"`.
16
+ *
17
+ * @param {Node|null} node - Text node to mark
18
+ * @returns {Node|null} The same node, for chaining
19
+ */
20
+ export function markRawText(node) {
21
+ if (node) {
22
+ rawTextNodes.add(node);
23
+ }
24
+ return node;
25
+ }
26
+
27
+ /**
28
+ * Check whether a node must be serialized without output escaping.
29
+ *
30
+ * Both the registry and the legacy `_disableOutputEscaping` flag set by the
31
+ * XSLT engine are honored so that either marking mechanism works.
32
+ *
33
+ * @param {Node|null} node - Node to test
34
+ * @returns {boolean} True when the node content must be emitted raw
35
+ */
36
+ export function isRawText(node) {
37
+ if (!node) {
38
+ return false;
39
+ }
40
+ return rawTextNodes.has(node) || node._disableOutputEscaping === true;
41
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Output Settings Resolution
3
+ *
4
+ * Normalizes the `xsl:output` settings collected by the XSLT engine into the
5
+ * shape the serializers consume (XSLT 1.0 section 16).
6
+ */
7
+
8
+ import { NODE_TYPE } from "./constants.js";
9
+
10
+ /**
11
+ * Test whether an `xsl:output` yes/no attribute is enabled.
12
+ *
13
+ * @param {string|boolean|undefined} value - Raw attribute value
14
+ * @returns {boolean} True when the value means "yes"
15
+ */
16
+ function isYes(value) {
17
+ return value === true || String(value).toLowerCase() === "yes";
18
+ }
19
+
20
+ /**
21
+ * Convert a `cdata-section-elements` value into a lookup set.
22
+ *
23
+ * @param {string|string[]|undefined} value - Whitespace separated names or array
24
+ * @returns {Set<string>} Element names requiring CDATA sections
25
+ */
26
+ function toNameSet(value) {
27
+ if (Array.isArray(value)) {
28
+ return new Set(value);
29
+ }
30
+ if (typeof value === "string") {
31
+ return new Set(value.split(/\s+/).filter(Boolean));
32
+ }
33
+ return new Set();
34
+ }
35
+
36
+ /**
37
+ * Find the first element node of a result tree.
38
+ *
39
+ * @param {Node|null} node - Document, fragment or element
40
+ * @returns {Element|null} The result document element, when there is one
41
+ */
42
+ export function findRootElement(node) {
43
+ if (!node) {
44
+ return null;
45
+ }
46
+ if (node.nodeType === NODE_TYPE.ELEMENT) {
47
+ return node;
48
+ }
49
+ for (const child of node.childNodes || []) {
50
+ if (child.nodeType === NODE_TYPE.ELEMENT) {
51
+ return child;
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Derive the default output method from the result tree.
59
+ *
60
+ * XSLT 1.0 section 16 defaults to `html` when the document element is `html`
61
+ * in no namespace, and to `xml` otherwise.
62
+ *
63
+ * @param {Node|null} node - Result tree root
64
+ * @returns {string} Either "html" or "xml"
65
+ */
66
+ export function detectOutputMethod(node) {
67
+ const root = findRootElement(node);
68
+ const isHtmlRoot =
69
+ root && !root.namespaceURI && root.localName.toLowerCase() === "html";
70
+ return isHtmlRoot ? "html" : "xml";
71
+ }
72
+
73
+ /**
74
+ * Normalize an `xsl:output` settings object.
75
+ *
76
+ * An absent, empty or "auto" method triggers the XSLT 1.0 default method
77
+ * detection based on the result tree.
78
+ *
79
+ * @param {object|null} outputSettings - Raw settings from the XSLT engine
80
+ * @param {Node|null} node - Result tree used for default method detection
81
+ * @returns {object} Normalized settings consumed by the serializers
82
+ */
83
+ export function resolveOutputSettings(outputSettings, node) {
84
+ const raw = outputSettings || {};
85
+ const declared = typeof raw.method === "string" ? raw.method.trim() : "";
86
+ const method =
87
+ declared && declared !== "auto"
88
+ ? declared.toLowerCase()
89
+ : detectOutputMethod(node);
90
+
91
+ return {
92
+ method,
93
+ version: raw.version || "1.0",
94
+ encoding: raw.encoding || "UTF-8",
95
+ standalone: raw.standalone || null,
96
+ indent: isYes(raw.indent),
97
+ omitXmlDeclaration: isYes(raw.omitXmlDeclaration),
98
+ doctypePublic: raw.doctypePublic || null,
99
+ doctypeSystem: raw.doctypeSystem || null,
100
+ mediaType: raw.mediaType || null,
101
+ cdataSectionElements: toNameSet(raw.cdataSectionElements),
102
+ };
103
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Text Output Serializer
3
+ *
4
+ * Implements the `text` output method of XSLT 1.0 section 16.3: the result is
5
+ * the concatenation of every descendant character data node, unescaped.
6
+ */
7
+
8
+ import { NODE_TYPE } from "./constants.js";
9
+
10
+ /**
11
+ * Serialize a result tree with the text output method.
12
+ *
13
+ * @param {Node} node - Document, fragment, element or character data node
14
+ * @returns {string} Concatenated character data
15
+ */
16
+ export function serializeText(node) {
17
+ if (
18
+ node.nodeType === NODE_TYPE.TEXT ||
19
+ node.nodeType === NODE_TYPE.CDATA_SECTION
20
+ ) {
21
+ return node.nodeValue || "";
22
+ }
23
+
24
+ let text = "";
25
+ for (const child of node.childNodes || []) {
26
+ text += serializeText(child);
27
+ }
28
+ return text;
29
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * XML Output Serializer
3
+ *
4
+ * Implements the `xml` output method of XSLT 1.0 section 16.1 and, with the
5
+ * `xhtml` option, the XHTML empty element convention. It is also the base of
6
+ * the html serializer, which overrides the dialect hooks defined here.
7
+ */
8
+
9
+ import { TEXT_MODE, VOID_ELEMENTS } from "./constants.js";
10
+ import { escapeXmlAttribute, escapeXmlText } from "./escape.js";
11
+ import { BaseWriter } from "./baseWriter.js";
12
+
13
+ export class XmlWriter extends BaseWriter {
14
+ /**
15
+ * Whether an XML declaration has to be written.
16
+ * @returns {boolean} True when the declaration is not omitted
17
+ */
18
+ get emitsXmlDeclaration() {
19
+ return !this.settings.omitXmlDeclaration;
20
+ }
21
+
22
+ /**
23
+ * Whether namespace declarations have to be written.
24
+ * @returns {boolean} Always true for XML output
25
+ */
26
+ get emitsNamespaces() {
27
+ return true;
28
+ }
29
+
30
+ /**
31
+ * Terminator of a processing instruction.
32
+ * @returns {string} The XML processing instruction terminator
33
+ */
34
+ get piTerminator() {
35
+ return "?>";
36
+ }
37
+
38
+ /**
39
+ * How a source CDATA section node has to be written.
40
+ * @returns {string} A {@link TEXT_MODE} value
41
+ */
42
+ get cdataNodeMode() {
43
+ return TEXT_MODE.CDATA;
44
+ }
45
+
46
+ /**
47
+ * Build the document type declaration for the xml output method.
48
+ *
49
+ * @param {Element|null} rootElement - Result document element
50
+ * @returns {string} Doctype markup, or an empty string when not applicable
51
+ */
52
+ doctypeMarkup(rootElement) {
53
+ const { doctypePublic, doctypeSystem } = this.settings;
54
+ if (!rootElement || !doctypeSystem) {
55
+ return "";
56
+ }
57
+
58
+ const name = rootElement.nodeName;
59
+ return doctypePublic
60
+ ? `<!DOCTYPE ${name} PUBLIC "${doctypePublic}" "${doctypeSystem}">`
61
+ : `<!DOCTYPE ${name} SYSTEM "${doctypeSystem}">`;
62
+ }
63
+
64
+ /**
65
+ * Determine how the character data children of an element are written.
66
+ *
67
+ * @param {Element} element - Parent element
68
+ * @returns {string} A {@link TEXT_MODE} value
69
+ */
70
+ childTextMode(element) {
71
+ const names = this.settings.cdataSectionElements;
72
+ return names.has(element.nodeName) || names.has(element.localName)
73
+ ? TEXT_MODE.CDATA
74
+ : TEXT_MODE.ESCAPE;
75
+ }
76
+
77
+ /**
78
+ * Whether the content of an element may be re-indented.
79
+ *
80
+ * @param {Element} _element - Element being inspected
81
+ * @returns {boolean} Always true for XML output
82
+ */
83
+ allowsIndentInside(_element) {
84
+ return true;
85
+ }
86
+
87
+ /**
88
+ * Build the markup closing an element that has no children.
89
+ *
90
+ * @param {Element} element - Empty element
91
+ * @param {string} _name - Element name as written
92
+ * @returns {string} Markup terminating the start tag
93
+ */
94
+ emptyElementMarkup(element, _name) {
95
+ return this.xhtml && this.isVoidElement(element) ? " />" : "/>";
96
+ }
97
+
98
+ /**
99
+ * Test whether an element is an HTML void element.
100
+ *
101
+ * @param {Element} element - Element to test
102
+ * @returns {boolean} True for void elements such as `br`
103
+ */
104
+ isVoidElement(element) {
105
+ return VOID_ELEMENTS.has(String(element.localName).toLowerCase());
106
+ }
107
+
108
+ /**
109
+ * Escape character data.
110
+ *
111
+ * @param {string} value - Text content
112
+ * @returns {string} Escaped text
113
+ */
114
+ escapeText(value) {
115
+ return escapeXmlText(value);
116
+ }
117
+
118
+ /**
119
+ * Escape an attribute value.
120
+ *
121
+ * @param {string} value - Attribute value
122
+ * @returns {string} Escaped value
123
+ */
124
+ escapeAttribute(value) {
125
+ return escapeXmlAttribute(value);
126
+ }
127
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * XSLT Output Serializer
3
+ *
4
+ * Serializes a result tree to a string honoring the `xsl:output` settings of
5
+ * the stylesheet (XSLT 1.0 section 16): output method, indentation, XML
6
+ * declaration, document type declaration, CDATA sections and
7
+ * `disable-output-escaping`.
8
+ *
9
+ * @example
10
+ * import { serializeResult } from './serializer.js';
11
+ *
12
+ * serializeResult(resultDocument, { method: 'xml', indent: 'yes' });
13
+ * // '<?xml version="1.0" encoding="UTF-8"?>\n<BAR>\n <QUX/>\n</BAR>'
14
+ */
15
+
16
+ import { resolveOutputSettings } from "./serializer/settings.js";
17
+ import { XmlWriter } from "./serializer/xmlSerializer.js";
18
+ import { HtmlWriter } from "./serializer/htmlSerializer.js";
19
+ import { serializeText } from "./serializer/textSerializer.js";
20
+
21
+ export { markRawText, isRawText, rawTextNodes } from "./serializer/rawText.js";
22
+ export {
23
+ resolveOutputSettings,
24
+ detectOutputMethod,
25
+ findRootElement,
26
+ } from "./serializer/settings.js";
27
+ export { XmlWriter } from "./serializer/xmlSerializer.js";
28
+ export { HtmlWriter } from "./serializer/htmlSerializer.js";
29
+ export { serializeText } from "./serializer/textSerializer.js";
30
+
31
+ /**
32
+ * Serialize a transformation result to a string.
33
+ *
34
+ * @param {Node|null} node - Result document, fragment or element
35
+ * @param {object} [outputSettings] - `xsl:output` settings, as collected by the
36
+ * XSLT engine (`method`, `version`, `encoding`, `standalone`, `indent`,
37
+ * `omitXmlDeclaration`, `doctypePublic`, `doctypeSystem`, `mediaType`,
38
+ * `cdataSectionElements`)
39
+ * @returns {string} The serialized result, or an empty string for a null node
40
+ */
41
+ export function serializeResult(node, outputSettings = {}) {
42
+ if (!node) {
43
+ return "";
44
+ }
45
+
46
+ const settings = resolveOutputSettings(outputSettings, node);
47
+
48
+ if (settings.method === "text") {
49
+ return serializeText(node);
50
+ }
51
+ if (settings.method === "html") {
52
+ return new HtmlWriter(settings).serialize(node);
53
+ }
54
+ return new XmlWriter(settings, {
55
+ xhtml: settings.method === "xhtml",
56
+ }).serialize(node);
57
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Default template priorities (XSLT 1.0 section 5.5).
3
+ *
4
+ * A template rule without an explicit `priority` attribute gets a default
5
+ * priority derived from the shape of its match pattern. A union pattern is
6
+ * treated as a set of template rules, one per alternative, so each
7
+ * alternative must be assigned its own priority by the caller.
8
+ *
9
+ * @module xslt/templatePriority
10
+ */
11
+
12
+ const NAME = String.raw`[A-Za-z_][\w.-]*`;
13
+ const QNAME = `(?:${NAME}:)?${NAME}`;
14
+
15
+ /** Patterns of the form `name`, `prefix:name`, `@name`, `@prefix:name`. */
16
+ const QNAME_PATTERN = new RegExp(`^(?:child::|attribute::|@)?${QNAME}$`);
17
+
18
+ /** Patterns of the form `prefix:*` or `@prefix:*`. */
19
+ const PREFIX_WILDCARD_PATTERN = new RegExp(
20
+ String.raw`^(?:child::|attribute::|@)?${NAME}:\*$`,
21
+ );
22
+
23
+ /** Patterns of the form `*`, `@*`, `node()`, `text()`, `comment()`, `processing-instruction()`. */
24
+ const NODE_TEST_PATTERN =
25
+ /^(?:child::|attribute::|@)?(?:\*|node\(\)|text\(\)|comment\(\)|processing-instruction\(\))$/;
26
+
27
+ /** `processing-instruction('literal')` patterns. */
28
+ const PI_LITERAL_PATTERN =
29
+ /^(?:child::)?processing-instruction\(\s*(?:"[^"]*"|'[^']*')\s*\)$/;
30
+
31
+ /**
32
+ * Compute the default priority of a single (non-union) match pattern.
33
+ *
34
+ * @param {string|null|undefined} pattern - The match pattern, already trimmed
35
+ * @returns {number} -0.5, -0.25, 0 or 0.5 as defined by the specification
36
+ */
37
+ export function calculatePriority(pattern) {
38
+ if (!pattern) return 0.5;
39
+
40
+ if (NODE_TEST_PATTERN.test(pattern)) return -0.5;
41
+ if (PREFIX_WILDCARD_PATTERN.test(pattern)) return -0.25;
42
+ if (QNAME_PATTERN.test(pattern) || PI_LITERAL_PATTERN.test(pattern)) return 0;
43
+
44
+ return 0.5;
45
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * URI helpers for XSLT stylesheet and document resolution.
3
+ *
4
+ * Kept deliberately small and dependency free: the engine only needs enough
5
+ * URI arithmetic to turn a relative `href` into something a host supplied
6
+ * loader can resolve, plus fragment removal for the `document()` function.
7
+ */
8
+
9
+ "use strict";
10
+
11
+ /** Matches an absolute URI such as `http://`, `https://` or `file://`. */
12
+ const ABSOLUTE_URI_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
13
+
14
+ /**
15
+ * Resolve a possibly relative URI against a base URI.
16
+ *
17
+ * Absolute URIs (with a scheme) and root relative URIs (starting with `/`)
18
+ * are returned untouched, as is any URI when no base is available.
19
+ *
20
+ * @param {string} href - The URI to resolve
21
+ * @param {string} [baseUri] - The base URI, typically the stylesheet location
22
+ * @returns {string} The resolved URI
23
+ *
24
+ * @example
25
+ * resolveUri('common.xsl', '/styles/main.xsl'); // '/styles/common.xsl'
26
+ */
27
+ export function resolveUri(href, baseUri) {
28
+ if (!href) return href;
29
+ if (!baseUri || isAbsoluteUri(href) || href.startsWith("/")) {
30
+ return href;
31
+ }
32
+
33
+ const lastSlash = baseUri.lastIndexOf("/");
34
+ const baseDir = lastSlash >= 0 ? baseUri.substring(0, lastSlash + 1) : "";
35
+
36
+ return baseDir + href;
37
+ }
38
+
39
+ /**
40
+ * Check whether a URI is absolute (has a scheme).
41
+ *
42
+ * @param {string} uri - The URI to inspect
43
+ * @returns {boolean} True when the URI carries a scheme
44
+ *
45
+ * @example
46
+ * isAbsoluteUri('https://example.com/a.xml'); // true
47
+ */
48
+ export function isAbsoluteUri(uri) {
49
+ return ABSOLUTE_URI_PATTERN.test(uri);
50
+ }
51
+
52
+ /**
53
+ * Remove a fragment identifier from a URI.
54
+ *
55
+ * XSLT 1.0 leaves the meaning of fragment identifiers passed to `document()`
56
+ * implementation defined; this processor ignores them.
57
+ *
58
+ * @param {string} uri - The URI, possibly carrying a `#fragment`
59
+ * @returns {string} The URI without its fragment
60
+ *
61
+ * @example
62
+ * stripFragment('data.xml#section'); // 'data.xml'
63
+ */
64
+ export function stripFragment(uri) {
65
+ if (typeof uri !== "string") return "";
66
+ const hash = uri.indexOf("#");
67
+ return hash === -1 ? uri : uri.substring(0, hash);
68
+ }