@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,186 @@
1
+ /**
2
+ * XSLT Processor CLI - Path Validation
3
+ *
4
+ * Every file the CLI reads or writes goes through this module first: the raw
5
+ * command line argument is resolved, canonicalized with `realpathSync`, checked
6
+ * to lie inside the trusted base directory and validated against the file
7
+ * system before any read or write is attempted. The base directory is the
8
+ * current working directory, or `XSLT_BASE_DIR` when that variable is set.
9
+ */
10
+
11
+ "use strict";
12
+
13
+ import { realpathSync, statSync } from "node:fs";
14
+ import { basename, dirname, join, resolve, sep } from "node:path";
15
+
16
+ /**
17
+ * Error raised for a command line path that cannot be used.
18
+ */
19
+ export class CliPathError extends Error {
20
+ /**
21
+ * @param {string} message - Human readable explanation
22
+ */
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = "CliPathError";
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Resolve a raw path argument to an absolute path.
31
+ *
32
+ * @param {string} rawPath - Raw command line argument
33
+ * @param {string} label - Human readable role of the path, used in errors
34
+ * @returns {string} The absolute path
35
+ * @throws {CliPathError} When the argument is empty or contains a NUL byte
36
+ */
37
+ function toAbsolutePath(rawPath, label) {
38
+ if (typeof rawPath !== "string" || rawPath.length === 0) {
39
+ throw new CliPathError(`${label} path is missing`);
40
+ }
41
+
42
+ if (rawPath.includes("\0")) {
43
+ throw new CliPathError(`${label} path contains a NUL byte`);
44
+ }
45
+
46
+ return resolve(rawPath);
47
+ }
48
+
49
+ /**
50
+ * Canonicalize an existing path, turning a missing entry into a CLI error.
51
+ *
52
+ * @param {string} absolute - Absolute path that should exist
53
+ * @param {string} message - Error message when nothing exists there
54
+ * @returns {string} The canonical path with symbolic links resolved
55
+ * @throws {CliPathError} When the path does not exist
56
+ */
57
+ function canonicalize(absolute, message) {
58
+ try {
59
+ return realpathSync(absolute);
60
+ } catch {
61
+ throw new CliPathError(message);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Ensure a canonical path lies inside the trusted base directory.
67
+ *
68
+ * This is the security boundary of the CLI: whatever the caller typed, the
69
+ * canonical path must be the base directory itself or a descendant of it.
70
+ *
71
+ * @param {string} canonical - Canonical absolute path
72
+ * @param {string} baseDir - Canonical absolute base directory
73
+ * @param {string} label - Human readable role of the path, used in errors
74
+ * @returns {string} The same path, now known to be inside baseDir
75
+ * @throws {CliPathError} When the path escapes the base directory
76
+ */
77
+ function assertInsideBase(canonical, baseDir, label) {
78
+ const inside =
79
+ canonical === baseDir ||
80
+ (canonical.startsWith(baseDir) && canonical.startsWith(baseDir + sep));
81
+
82
+ if (!inside) {
83
+ throw new CliPathError(
84
+ `${label} path is outside the allowed base directory (${baseDir}): ${canonical}. ` +
85
+ "Run the command from that directory or set XSLT_BASE_DIR.",
86
+ );
87
+ }
88
+
89
+ return canonical;
90
+ }
91
+
92
+ /**
93
+ * Resolve the trusted base directory all file arguments are confined to.
94
+ *
95
+ * Uses `XSLT_BASE_DIR` when set, otherwise the current working directory.
96
+ *
97
+ * @returns {string} The canonical absolute path of an existing directory
98
+ * @throws {CliPathError} When the configured directory does not exist or is not a directory
99
+ *
100
+ * @example
101
+ * const baseDir = resolveBaseDir(); // process.cwd() unless XSLT_BASE_DIR is set
102
+ */
103
+ export function resolveBaseDir() {
104
+ const configured = process.env.XSLT_BASE_DIR || process.cwd();
105
+ const absolute = toAbsolutePath(configured, "Base directory");
106
+ const canonical = canonicalize(
107
+ absolute,
108
+ `Base directory does not exist: ${absolute}`,
109
+ );
110
+
111
+ if (!statSync(canonical).isDirectory()) {
112
+ throw new CliPathError(`Base directory is not a directory: ${canonical}`);
113
+ }
114
+
115
+ return canonical;
116
+ }
117
+
118
+ /**
119
+ * Validate a path the CLI is going to read.
120
+ *
121
+ * @param {string} rawPath - Raw command line argument
122
+ * @param {string} label - Human readable role of the path, used in errors
123
+ * @param {string} baseDir - Canonical base directory from resolveBaseDir()
124
+ * @returns {string} The canonical path of an existing regular file inside baseDir
125
+ * @throws {CliPathError} When the path is malformed, missing, outside baseDir or not a file
126
+ *
127
+ * @example
128
+ * const xmlFile = resolveInputPath("data.xml", "XML", resolveBaseDir());
129
+ */
130
+ export function resolveInputPath(rawPath, label, baseDir) {
131
+ const absolute = toAbsolutePath(rawPath, label);
132
+ const canonical = assertInsideBase(
133
+ canonicalize(absolute, `File not found: ${absolute}`),
134
+ baseDir,
135
+ label,
136
+ );
137
+
138
+ if (!statSync(canonical).isFile()) {
139
+ throw new CliPathError(`${label} path is not a file: ${canonical}`);
140
+ }
141
+
142
+ return canonical;
143
+ }
144
+
145
+ /**
146
+ * Validate a path the CLI is going to write.
147
+ *
148
+ * The file itself may be missing, but its parent directory has to exist, lie
149
+ * inside the base directory, and an existing target has to be a regular file.
150
+ *
151
+ * @param {string} rawPath - Raw command line argument
152
+ * @param {string} baseDir - Canonical base directory from resolveBaseDir()
153
+ * @returns {string} The canonical path to write to
154
+ * @throws {CliPathError} When the path is malformed, outside baseDir or not writable as a file
155
+ *
156
+ * @example
157
+ * const target = resolveOutputPath("build/result.html", resolveBaseDir());
158
+ */
159
+ export function resolveOutputPath(rawPath, baseDir) {
160
+ const absolute = toAbsolutePath(rawPath, "Output");
161
+ const parent = assertInsideBase(
162
+ canonicalize(
163
+ dirname(absolute),
164
+ `Output directory does not exist: ${dirname(absolute)}`,
165
+ ),
166
+ baseDir,
167
+ "Output directory",
168
+ );
169
+
170
+ if (!statSync(parent).isDirectory()) {
171
+ throw new CliPathError(`Output directory is not a directory: ${parent}`);
172
+ }
173
+
174
+ const canonical = assertInsideBase(
175
+ join(parent, basename(absolute)),
176
+ baseDir,
177
+ "Output",
178
+ );
179
+ const targetStats = statSync(canonical, { throwIfNoEntry: false });
180
+
181
+ if (targetStats && !targetStats.isFile()) {
182
+ throw new CliPathError(`Output path is not a file: ${canonical}`);
183
+ }
184
+
185
+ return canonical;
186
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * XSLT Processor CLI - Transformation Helpers
3
+ *
4
+ * DOM environment setup, document parsing and the transformation itself.
5
+ * Output is serialized through XSLTProcessor#transformToString so that the
6
+ * xsl:output settings of the stylesheet are honored.
7
+ */
8
+
9
+ "use strict";
10
+
11
+ import { JSDOM } from "jsdom";
12
+ import { XSLTProcessor } from "../../src/XSLTProcessor.js";
13
+
14
+ /**
15
+ * Create a JSDOM based DOM environment and expose it globally.
16
+ *
17
+ * The XSLT engine builds its result documents through the global `document`,
18
+ * so the globals have to be installed before transforming.
19
+ *
20
+ * @returns {JSDOM} The created JSDOM instance
21
+ */
22
+ export function createDomEnvironment() {
23
+ const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
24
+ contentType: "text/html",
25
+ });
26
+
27
+ globalThis.document = dom.window.document;
28
+ globalThis.DOMParser = dom.window.DOMParser;
29
+ globalThis.XMLSerializer = dom.window.XMLSerializer;
30
+
31
+ return dom;
32
+ }
33
+
34
+ /**
35
+ * Parse an XML string, reporting parser errors as exceptions.
36
+ *
37
+ * @param {JSDOM} dom - DOM environment
38
+ * @param {string} content - XML source text
39
+ * @param {string} label - Human readable document label used in errors
40
+ * @returns {Document} Parsed document
41
+ */
42
+ export function parseDocument(dom, content, label) {
43
+ const doc = new dom.window.DOMParser().parseFromString(
44
+ content,
45
+ "application/xml",
46
+ );
47
+
48
+ const error = doc.querySelector("parsererror");
49
+ if (error) {
50
+ throw new Error(`Error parsing ${label}: ${error.textContent}`);
51
+ }
52
+
53
+ return doc;
54
+ }
55
+
56
+ /**
57
+ * Override the stylesheet xsl:output settings from the command line flags.
58
+ *
59
+ * @param {XSLTProcessor} processor - Processor with an imported stylesheet
60
+ * @param {object} values - Parsed command line option values
61
+ * @returns {object} The effective output settings
62
+ */
63
+ export function applyOutputOverrides(processor, values) {
64
+ const settings = processor._engine.outputSettings;
65
+
66
+ if (values.format || values.indent) {
67
+ settings.indent = "yes";
68
+ }
69
+ if (values.method) {
70
+ settings.method = values.method;
71
+ }
72
+ if (values["no-declaration"]) {
73
+ settings.omitXmlDeclaration = "yes";
74
+ }
75
+
76
+ return settings;
77
+ }
78
+
79
+ /**
80
+ * Run a transformation and serialize its result.
81
+ *
82
+ * @param {object} options - Transformation inputs
83
+ * @param {JSDOM} options.dom - DOM environment
84
+ * @param {string} options.xmlContent - XML source text
85
+ * @param {string} options.xsltContent - XSLT stylesheet text
86
+ * @param {Record<string, string>} options.params - Stylesheet parameters
87
+ * @param {object} options.values - Parsed command line option values
88
+ * @returns {string} The serialized transformation result
89
+ */
90
+ export function runTransformation({
91
+ dom,
92
+ xmlContent,
93
+ xsltContent,
94
+ params,
95
+ values,
96
+ }) {
97
+ const xmlDoc = parseDocument(dom, xmlContent, "XML");
98
+ const xsltDoc = parseDocument(dom, xsltContent, "XSLT");
99
+
100
+ const processor = new XSLTProcessor();
101
+ processor.importStylesheet(xsltDoc);
102
+
103
+ for (const [name, value] of Object.entries(params)) {
104
+ processor.setParameter(null, name, value);
105
+ }
106
+
107
+ applyOutputOverrides(processor, values);
108
+
109
+ const output = processor.transformToString(xmlDoc);
110
+ if (output === null) {
111
+ throw new Error("Transformation failed");
112
+ }
113
+
114
+ return output;
115
+ }
package/bin/xslt.js CHANGED
@@ -4,117 +4,64 @@
4
4
  * XSLT Processor CLI
5
5
  *
6
6
  * Command-line interface for transforming XML using XSLT stylesheets.
7
+ * The result is serialized according to the xsl:output element of the
8
+ * stylesheet (XSLT 1.0 section 16).
7
9
  */
8
10
 
9
- 'use strict';
11
+ "use strict";
12
+
13
+ import { readFile, writeFile } from "node:fs/promises";
14
+ import { parseArgs } from "node:util";
15
+ import {
16
+ CLI_OPTIONS,
17
+ parseParameters,
18
+ printHelp,
19
+ printVersion,
20
+ } from "./lib/options.js";
21
+ import { createDomEnvironment, runTransformation } from "./lib/transform.js";
22
+ import {
23
+ resolveBaseDir,
24
+ resolveInputPath,
25
+ resolveOutputPath,
26
+ } from "./lib/paths.js";
10
27
 
11
- import { readFile, writeFile } from 'node:fs/promises';
12
- import { parseArgs } from 'node:util';
13
- import { JSDOM } from 'jsdom';
14
- import { XSLTProcessor } from '../src/XSLTProcessor.js';
15
-
16
- const VERSION = '1.0.5';
17
-
18
- function printHelp() {
19
- console.log(`
20
- xslt-processor - Transform XML documents using XSLT stylesheets
21
-
22
- USAGE:
23
- xslt <xml-file> <xslt-file> [options]
24
-
25
- ARGUMENTS:
26
- <xml-file> Path to XML source document
27
- <xslt-file> Path to XSLT stylesheet
28
-
29
- OPTIONS:
30
- -o, --output <file> Write output to file instead of stdout
31
- -p, --param <n>=<v> Set XSLT parameter (can be used multiple times)
32
- -f, --format Format output with indentation
33
- -h, --help Show this help message
34
- -v, --version Show version number
35
-
36
- EXAMPLES:
37
- # Basic transformation
38
- xslt data.xml transform.xsl
39
-
40
- # Save output to file
41
- xslt data.xml transform.xsl -o result.html
42
-
43
- # With parameters
44
- xslt data.xml transform.xsl -p title="My Page" -p count=10
45
-
46
- # Multiple parameters with formatted output
47
- xslt data.xml transform.xsl -p lang=en -p debug=true -f -o output.html
48
- `);
49
- }
50
-
51
- function printVersion() {
52
- console.log(`xslt-processor v${VERSION}`);
53
- }
54
-
55
- function parseParameters(params) {
56
- const result = {};
57
-
58
- if (!params || !Array.isArray(params)) {
59
- return result;
60
- }
61
-
62
- for (const param of params) {
63
- const equalIndex = param.indexOf('=');
64
- if (equalIndex === -1) {
65
- console.error(`Warning: Invalid parameter format "${param}". Expected name=value`);
66
- continue;
67
- }
68
-
69
- const name = param.substring(0, equalIndex);
70
- const value = param.substring(equalIndex + 1);
71
- result[name] = value;
28
+ /**
29
+ * Parse the command line, exiting on malformed input.
30
+ *
31
+ * @returns {object} The parseArgs result
32
+ */
33
+ function readArguments() {
34
+ try {
35
+ return parseArgs({ options: CLI_OPTIONS, allowPositionals: true });
36
+ } catch (err) {
37
+ console.error(`Error: ${err.message}`);
38
+ process.exit(1);
72
39
  }
73
-
74
- return result;
75
40
  }
76
41
 
77
- function formatXml(xml) {
78
- let formatted = '';
79
- let indent = 0;
80
- const lines = xml.replace(/>\s*</g, '>\n<').split('\n');
81
-
82
- for (const line of lines) {
83
- const trimmed = line.trim();
84
- if (!trimmed) continue;
85
-
86
- if (trimmed.startsWith('</')) {
87
- indent = Math.max(0, indent - 1);
88
- }
89
-
90
- formatted += ' '.repeat(indent) + trimmed + '\n';
91
-
92
- if (trimmed.startsWith('<') && !trimmed.startsWith('</') &&
93
- !trimmed.startsWith('<?') && !trimmed.startsWith('<!') &&
94
- !trimmed.endsWith('/>') && !trimmed.includes('</')) {
95
- indent++;
96
- }
42
+ /**
43
+ * Write the transformation result to a file or to stdout.
44
+ *
45
+ * @param {string} output - Serialized transformation result
46
+ * @param {string|undefined} target - Validated absolute output path, if any
47
+ * @returns {Promise<void>} Resolves once the result has been written
48
+ */
49
+ async function writeOutput(output, target) {
50
+ if (target) {
51
+ await writeFile(target, output, "utf-8");
52
+ console.error(`Output written to ${target}`);
53
+ return;
97
54
  }
98
-
99
- return formatted;
55
+ console.log(output);
100
56
  }
101
57
 
58
+ /**
59
+ * CLI entry point.
60
+ *
61
+ * @returns {Promise<void>} Resolves once the CLI has finished
62
+ */
102
63
  async function main() {
103
- const options = {
104
- output: { type: 'string', short: 'o' },
105
- param: { type: 'string', short: 'p', multiple: true },
106
- format: { type: 'boolean', short: 'f', default: false },
107
- help: { type: 'boolean', short: 'h', default: false },
108
- version: { type: 'boolean', short: 'v', default: false }
109
- };
110
-
111
- let args;
112
- try {
113
- args = parseArgs({ options, allowPositionals: true });
114
- } catch (err) {
115
- console.error(`Error: ${err.message}`);
116
- process.exit(1);
117
- }
64
+ const args = readArguments();
118
65
 
119
66
  if (args.values.help) {
120
67
  printHelp();
@@ -129,78 +76,37 @@ async function main() {
129
76
  const [xmlPath, xsltPath] = args.positionals;
130
77
 
131
78
  if (!xmlPath || !xsltPath) {
132
- console.error('Error: Both XML and XSLT file paths are required');
79
+ console.error("Error: Both XML and XSLT file paths are required");
133
80
  console.error('Run "xslt --help" for usage information');
134
81
  process.exit(1);
135
82
  }
136
83
 
137
- // Setup JSDOM for DOM parsing
138
- const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
139
- contentType: 'text/html'
140
- });
141
- const { DOMParser, XMLSerializer } = dom.window;
84
+ const dom = createDomEnvironment();
142
85
 
143
86
  try {
144
- // Read input files
87
+ const baseDir = resolveBaseDir();
88
+ const xmlFile = resolveInputPath(xmlPath, "XML", baseDir);
89
+ const xsltFile = resolveInputPath(xsltPath, "XSLT", baseDir);
90
+ const outputFile = args.values.output
91
+ ? resolveOutputPath(args.values.output, baseDir)
92
+ : undefined;
93
+
145
94
  const [xmlContent, xsltContent] = await Promise.all([
146
- readFile(xmlPath, 'utf-8'),
147
- readFile(xsltPath, 'utf-8')
95
+ readFile(xmlFile, "utf-8"),
96
+ readFile(xsltFile, "utf-8"),
148
97
  ]);
149
98
 
150
- // Parse documents
151
- const parser = new DOMParser();
152
- const xmlDoc = parser.parseFromString(xmlContent, 'application/xml');
153
- const xsltDoc = parser.parseFromString(xsltContent, 'application/xml');
154
-
155
- // Check for parsing errors
156
- const xmlError = xmlDoc.querySelector('parsererror');
157
- if (xmlError) {
158
- console.error(`Error parsing XML: ${xmlError.textContent}`);
159
- process.exit(1);
160
- }
161
-
162
- const xsltError = xsltDoc.querySelector('parsererror');
163
- if (xsltError) {
164
- console.error(`Error parsing XSLT: ${xsltError.textContent}`);
165
- process.exit(1);
166
- }
167
-
168
- // Create processor
169
- const processor = new XSLTProcessor();
170
- processor.importStylesheet(xsltDoc);
171
-
172
- // Set parameters
173
- const params = parseParameters(args.values.param);
174
- for (const [name, value] of Object.entries(params)) {
175
- processor.setParameter(null, name, value);
176
- }
177
-
178
- // Transform
179
- const fragment = processor.transformToFragment(xmlDoc, dom.window.document);
180
-
181
- // Serialize result
182
- const serializer = new XMLSerializer();
183
- let output = serializer.serializeToString(fragment);
184
-
185
- // Format if requested
186
- if (args.values.format) {
187
- output = formatXml(output);
188
- }
189
-
190
- // Output result
191
- if (args.values.output) {
192
- await writeFile(args.values.output, output, 'utf-8');
193
- console.error(`Output written to ${args.values.output}`);
194
- } else {
195
- console.log(output);
196
- }
99
+ const output = runTransformation({
100
+ dom,
101
+ xmlContent,
102
+ xsltContent,
103
+ params: parseParameters(args.values.param),
104
+ values: args.values,
105
+ });
197
106
 
107
+ await writeOutput(output, outputFile);
198
108
  } catch (err) {
199
- if (err.code === 'ENOENT') {
200
- console.error(`Error: File not found: ${err.path}`);
201
- } else {
202
- console.error(`Error: ${err.message}`);
203
- }
109
+ console.error(`Error: ${err.message}`);
204
110
  process.exit(1);
205
111
  }
206
112
  }