@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
package/README.md CHANGED
@@ -1,12 +1,18 @@
1
1
  # @tradik/xslt-processor
2
2
 
3
+ [![GitHub](https://img.shields.io/badge/GitHub-Repository-181717?logo=github)](https://github.com/spagu/XSLT-Processor)
4
+ [![GitHub stars](https://img.shields.io/github/stars/spagu/XSLT-Processor?style=social)](https://github.com/spagu/XSLT-Processor/stargazers)
5
+ [![GitHub forks](https://img.shields.io/github/forks/spagu/XSLT-Processor?style=social)](https://github.com/spagu/XSLT-Processor/network/members)
6
+
3
7
  [![CI](https://github.com/spagu/XSLT-Processor/actions/workflows/test.yml/badge.svg)](https://github.com/spagu/XSLT-Processor/actions/workflows/test.yml)
4
8
  [![Release](https://github.com/spagu/XSLT-Processor/actions/workflows/release.yml/badge.svg)](https://github.com/spagu/XSLT-Processor/actions/workflows/release.yml)
5
9
  [![npm version](https://img.shields.io/npm/v/@tradik/xslt-processor.svg)](https://www.npmjs.com/package/@tradik/xslt-processor)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
- [![Node.js Version](https://img.shields.io/badge/node-%3E%3D25.0.0-brightgreen.svg)](https://nodejs.org/)
10
+ [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
11
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.19.0-brightgreen.svg)](https://nodejs.org/)
8
12
  [![Test Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/spagu/XSLT-Processor)
9
13
 
14
+ > **Source Code:** [github.com/spagu/XSLT-Processor](https://github.com/spagu/XSLT-Processor)
15
+
10
16
  JavaScript implementation of XSLTProcessor for browser environments and Node.js CLI. This package provides a complete implementation of the W3C XSLTProcessor API that can be used as a drop-in replacement for the native browser implementation.
11
17
 
12
18
  ## Background
@@ -22,6 +28,7 @@ This library ensures your XSLT-based applications continue to work regardless of
22
28
  - **1:1 Native API Compatibility**: Drop-in replacement for native `XSLTProcessor`
23
29
  - **Full XSLT 1.0 Support**: Implements the complete W3C XSLT 1.0 specification
24
30
  - **XPath 1.0 Engine**: Built-in XPath evaluator with all core functions
31
+ - **`xsl:output` Serialization**: `transformToString()` honors method, indent, doctype, CDATA sections and `disable-output-escaping`
25
32
  - **Zero Dependencies**: Standalone implementation with no external dependencies
26
33
  - **Multiple Formats**: ESM, CommonJS, and browser IIFE bundles
27
34
  - **TypeScript Support**: Includes TypeScript declarations
@@ -136,6 +143,22 @@ xslt data.xml template.xsl -p title="My Page" -p count=10
136
143
 
137
144
  # Format output with indentation
138
145
  xslt data.xml template.xsl -f -o output.html
146
+
147
+ # Override the output method and drop the XML declaration
148
+ xslt data.xml template.xsl --method text
149
+ xslt data.xml template.xsl --no-declaration
150
+ ```
151
+
152
+ The output is serialized according to the `xsl:output` element of the
153
+ stylesheet (see [Serializing output](#serializing-output-xsloutput)); the
154
+ options below override individual `xsl:output` settings.
155
+
156
+ All file arguments must live inside the current working directory (symbolic
157
+ links are resolved first). To work with files elsewhere, run the command from
158
+ that directory or point `XSLT_BASE_DIR` at it:
159
+
160
+ ```bash
161
+ XSLT_BASE_DIR=/srv/data xslt /srv/data/in.xml /srv/data/t.xsl -o /srv/data/out.html
139
162
  ```
140
163
 
141
164
  #### CLI Options
@@ -144,7 +167,10 @@ xslt data.xml template.xsl -f -o output.html
144
167
  |--------|-------------|
145
168
  | `-o, --output <file>` | Write output to file instead of stdout |
146
169
  | `-p, --param <n>=<v>` | Set XSLT parameter (can be used multiple times) |
147
- | `-f, --format` | Format output with indentation |
170
+ | `-f, --format` | Format output with indentation (same as `--indent`) |
171
+ | `--indent` | Override `xsl:output` to `indent="yes"` |
172
+ | `--method <m>` | Override the `xsl:output` method (`xml`, `html`, `xhtml`, `text`) |
173
+ | `--no-declaration` | Override `xsl:output` to omit the XML declaration |
148
174
  | `-h, --help` | Show help message |
149
175
  | `-v, --version` | Show version number |
150
176
 
@@ -162,14 +188,78 @@ const processor = new XSLTProcessor();
162
188
 
163
189
  | Method | Description |
164
190
  |--------|-------------|
165
- | `importStylesheet(node)` | Imports an XSLT stylesheet from a Document or Element node |
191
+ | `importStylesheet(node, stylesheetUri?)` | Imports an XSLT stylesheet from a Document or Element node. The optional `stylesheetUri` is the base URI used to resolve relative `xsl:import`/`xsl:include` hrefs |
166
192
  | `transformToFragment(source, output)` | Transforms XML and returns a DocumentFragment |
167
193
  | `transformToDocument(source)` | Transforms XML and returns an XMLDocument |
194
+ | `transformToString(source)` | Transforms XML and returns the serialized result honoring `xsl:output` (non-W3C extension) |
168
195
  | `setParameter(namespaceURI, localName, value)` | Sets an XSLT parameter |
169
196
  | `getParameter(namespaceURI, localName)` | Gets an XSLT parameter value |
170
197
  | `removeParameter(namespaceURI, localName)` | Removes an XSLT parameter |
171
198
  | `clearParameters()` | Removes all parameters |
172
- | `reset()` | Resets the processor, removing stylesheet and parameters |
199
+ | `reset()` | Resets the processor, removing stylesheet and parameters (the stylesheet and document loaders are kept) |
200
+ | `setStylesheetLoader(loader)` | Sets the loader used to resolve `xsl:import`/`xsl:include`. Returns the processor for chaining |
201
+ | `setDocumentLoader(loader)` | Sets the loader used to resolve the XSLT `document()` function. Returns the processor for chaining |
202
+
203
+ #### Properties
204
+
205
+ | Property | Description |
206
+ |----------|-------------|
207
+ | `engine` | Read-only access to the underlying `XsltEngine` (advanced usage). It is `null` until `importStylesheet()` has been called |
208
+
209
+ ### Serializing output (xsl:output)
210
+
211
+ `transformToString(source)` serializes the result tree according to the
212
+ `xsl:output` element of the stylesheet (XSLT 1.0 section 16). Unlike the DOM
213
+ based methods it returns ready to write markup, so the CLI and Node.js users do
214
+ not need a separate `XMLSerializer` or a hand rolled re-indenter.
215
+
216
+ ```javascript
217
+ const xslt = parser.parseFromString(`<?xml version="1.0"?>
218
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
219
+ <xsl:output method="xml" indent="yes"/>
220
+ <xsl:template match="/"><BAR><QUX/></BAR></xsl:template>
221
+ </xsl:stylesheet>`, 'application/xml');
222
+
223
+ const processor = new XSLTProcessor();
224
+ processor.importStylesheet(xslt);
225
+
226
+ processor.transformToString(xmlDoc);
227
+ // <?xml version="1.0" encoding="UTF-8"?>
228
+ // <BAR>
229
+ // <QUX/>
230
+ // </BAR>
231
+ ```
232
+
233
+ Supported `xsl:output` attributes:
234
+
235
+ | Attribute | Behavior |
236
+ |-----------|----------|
237
+ | `method="xml"` | XML declaration, minimal escaping, empty elements as `<x/>` (default) |
238
+ | `method="html"` | No XML declaration, void elements as `<br>`, minimized boolean attributes, unescaped `script`/`style` |
239
+ | `method="xhtml"` | XML rules with void elements written as `<br />` |
240
+ | `method="text"` | Concatenation of all text nodes, no escaping |
241
+ | `indent="yes"` | Newline plus two-space indentation for element-only content; mixed content and `pre`/`script`/`style`/`textarea` are left untouched |
242
+ | `encoding`, `version`, `standalone` | Written into the XML declaration |
243
+ | `omit-xml-declaration="yes"` | Suppresses the XML declaration |
244
+ | `doctype-public`, `doctype-system` | Emit a `<!DOCTYPE ...>` before the document element |
245
+ | `cdata-section-elements` | Text children of the listed elements are wrapped in `<![CDATA[...]]>`, split around any `]]>` |
246
+ | `media-type` | Parsed and exposed on the settings object |
247
+
248
+ `disable-output-escaping="yes"` on `xsl:text` and `xsl:value-of` is honored: the
249
+ generated text is emitted verbatim, so `&lt;b&gt;` reaches the output as `<b>`.
250
+
251
+ The serializer can also be used on its own, for example on a fragment produced
252
+ by `transformToFragment`:
253
+
254
+ ```javascript
255
+ import { serializeResult } from '@tradik/xslt-processor';
256
+
257
+ serializeResult(fragment, { method: 'html', indent: 'yes' });
258
+ ```
259
+
260
+ When `method` is absent (or `auto`), the output method is derived from the
261
+ result tree: `html` when the document element is `html` in no namespace, `xml`
262
+ otherwise.
173
263
 
174
264
  ### Parameters Example
175
265
 
@@ -190,30 +280,135 @@ processor.clearParameters();
190
280
 
191
281
  ### Using xsl:import and xsl:include
192
282
 
193
- To use `xsl:import` and `xsl:include` elements in your stylesheets, you need to configure a stylesheet loader that tells the processor how to fetch external stylesheets:
283
+ To use `xsl:import` and `xsl:include` elements in your stylesheets, configure a
284
+ stylesheet loader that tells the processor how to fetch external stylesheets.
285
+
286
+ Call `processor.setStylesheetLoader(...)` **before** `importStylesheet()` -
287
+ references are resolved while the main stylesheet is being compiled, so a loader
288
+ set afterwards is too late for that stylesheet (it still applies to the next
289
+ `importStylesheet()` call).
290
+
291
+ The loader is **synchronous**. It receives the resolved `href` and the `baseUri`
292
+ of the importing stylesheet, and must return either a `Document` or an XML
293
+ `string` (which is parsed automatically). Promises are not awaited, so pre-load
294
+ remote stylesheets before importing.
194
295
 
195
296
  ```javascript
196
297
  import { XSLTProcessor } from '@tradik/xslt-processor';
197
298
 
198
299
  const processor = new XSLTProcessor();
199
300
 
200
- // Configure stylesheet loader
201
- processor.engine.setStylesheetLoader((href, baseUri) => {
202
- // href: the href attribute from xsl:import/xsl:include
203
- // baseUri: the URI of the importing stylesheet
301
+ // Pre-loaded stylesheets, keyed by resolved URI
302
+ const stylesheets = {
303
+ '/styles/base.xsl': baseXslText,
304
+ '/styles/utils.xsl': utilsXslText,
305
+ };
306
+
307
+ processor.setStylesheetLoader((href, baseUri) => {
308
+ // href: resolved URI of the xsl:import/xsl:include target
309
+ // baseUri: URI of the importing stylesheet (the stylesheetUri you passed in)
310
+ const xml = stylesheets[href];
311
+ if (!xml) {
312
+ throw new Error(`Unknown stylesheet: ${href} (from ${baseUri})`);
313
+ }
314
+ return xml; // a Document is also accepted
315
+ });
204
316
 
205
- // Option 1: Return a parsed Document
206
- const response = await fetch(href);
207
- const text = await response.text();
208
- const parser = new DOMParser();
209
- return parser.parseFromString(text, 'application/xml');
317
+ // The second argument is the base URI used to resolve relative hrefs
318
+ processor.importStylesheet(mainStylesheet, '/styles/main.xsl');
319
+ ```
320
+
321
+ If you need remote stylesheets in the browser, fetch them first and hand the
322
+ loader a ready-made map:
323
+
324
+ ```javascript
325
+ const hrefs = ['/styles/base.xsl', '/styles/utils.xsl'];
326
+ const texts = await Promise.all(
327
+ hrefs.map((href) => fetch(href).then((response) => response.text())),
328
+ );
329
+ const cache = Object.fromEntries(hrefs.map((href, i) => [href, texts[i]]));
210
330
 
211
- // Option 2: Return XML string (will be parsed automatically)
212
- return await fetch(href).then(r => r.text());
331
+ processor.setStylesheetLoader((href) => cache[href]);
332
+ processor.importStylesheet(mainStylesheet, '/styles/main.xsl');
333
+ ```
334
+
335
+ #### Node.js example (filesystem loader)
336
+
337
+ ```javascript
338
+ import { readFileSync } from 'node:fs';
339
+ import path from 'node:path';
340
+ import { DOMParser } from '@xmldom/xmldom'; // or: new JSDOM(...).window.DOMParser
341
+ import { XSLTProcessor } from '@tradik/xslt-processor';
342
+
343
+ const parser = new DOMParser();
344
+ const mainPath = path.resolve('./styles/main.xsl');
345
+
346
+ const processor = new XSLTProcessor();
347
+
348
+ processor.setStylesheetLoader((href, baseUri) => {
349
+ const filePath = path.resolve(path.dirname(baseUri), href);
350
+ return readFileSync(filePath, 'utf8'); // returned XML string is parsed for you
351
+ });
352
+
353
+ const mainStylesheet = parser.parseFromString(
354
+ readFileSync(mainPath, 'utf8'),
355
+ 'application/xml',
356
+ );
357
+
358
+ processor.importStylesheet(mainStylesheet, mainPath);
359
+
360
+ const xml = parser.parseFromString(readFileSync('./data.xml', 'utf8'), 'application/xml');
361
+ const result = processor.transformToDocument(xml);
362
+ ```
363
+
364
+ #### Advanced: the engine accessor
365
+
366
+ After `importStylesheet()`, `processor.engine` exposes the underlying
367
+ `XsltEngine` for advanced inspection (output settings, compiled templates). It
368
+ is `null` before the first import, which is why the loader must be configured
369
+ through `processor.setStylesheetLoader(...)` rather than `processor.engine`.
370
+
371
+ ### Using the document() function
372
+
373
+ The XSLT `document()` function loads additional XML documents at transformation
374
+ time. Configure a **synchronous** document loader with
375
+ `processor.setDocumentLoader(...)`; it receives the resolved URI and the base URI
376
+ and returns a `Document`, an XML `string` (parsed automatically) or `null`.
377
+
378
+ Semantics:
379
+
380
+ - `document('')` returns the stylesheet itself, per the XSLT 1.0 specification.
381
+ - A node-set argument loads one document per node and returns their union.
382
+ - Relative URIs are resolved against the `stylesheetUri` passed to
383
+ `importStylesheet()`; fragment identifiers are ignored.
384
+ - Without a loader, or when the loader returns `null`, `document()` evaluates to
385
+ an **empty node-set** instead of failing the transformation.
386
+ - Each resolved URI is loaded once and cached for the life of the processor.
387
+
388
+ ```javascript
389
+ import { readFileSync } from 'node:fs';
390
+ import path from 'node:path';
391
+ import { XSLTProcessor } from '@tradik/xslt-processor';
392
+
393
+ const processor = new XSLTProcessor();
394
+
395
+ processor.setDocumentLoader((uri, baseUri) => {
396
+ const filePath = path.resolve(path.dirname(baseUri || '.'), uri);
397
+ try {
398
+ return readFileSync(filePath, 'utf8'); // XML string is parsed for you
399
+ } catch {
400
+ return null; // -> empty node-set, the transformation keeps going
401
+ }
213
402
  });
214
403
 
215
- // Now xsl:import and xsl:include will work
216
404
  processor.importStylesheet(mainStylesheet, '/styles/main.xsl');
405
+ const result = processor.transformToDocument(xmlDoc);
406
+ ```
407
+
408
+ ```xml
409
+ <xsl:template match="/">
410
+ <rate><xsl:value-of select="document('rates.xml')/rates/eur"/></rate>
411
+ </xsl:template>
217
412
  ```
218
413
 
219
414
  #### Import vs Include Behavior
@@ -236,7 +431,12 @@ processor.importStylesheet(mainStylesheet, '/styles/main.xsl');
236
431
  ### Utility Functions
237
432
 
238
433
  ```javascript
239
- import { isNativeXSLTSupported, installGlobal } from '@tradik/xslt-processor';
434
+ import {
435
+ isNativeXSLTSupported,
436
+ installGlobal,
437
+ serializeResult,
438
+ markRawText
439
+ } from '@tradik/xslt-processor';
240
440
 
241
441
  // Check if native XSLT is functional
242
442
  if (!isNativeXSLTSupported()) {
@@ -246,6 +446,12 @@ if (!isNativeXSLTSupported()) {
246
446
  // Install as global XSLTProcessor
247
447
  installGlobal(); // Only if native not available
248
448
  installGlobal(true); // Force install
449
+
450
+ // Serialize any result tree with xsl:output settings
451
+ serializeResult(node, { method: 'xml', indent: 'yes' });
452
+
453
+ // Mark a text node so that it is emitted without escaping
454
+ markRawText(document.createTextNode('<b>raw</b>'));
249
455
  ```
250
456
 
251
457
  ## Complete Example
@@ -392,6 +598,13 @@ const result = evaluator.evaluate(ast, context);
392
598
  | `xsl:with-param` | Supported |
393
599
  | `xsl:import` | Supported |
394
600
  | `xsl:include` | Supported |
601
+ | `xsl:apply-imports` | Supported |
602
+ | `xsl:attribute-set` | Supported (also via `xsl:use-attribute-sets` on literal result elements) |
603
+ | `xsl:key` | Supported (see `key()`) |
604
+ | `xsl:decimal-format` | Supported (see `format-number()`) |
605
+ | `xsl:namespace-alias` | Supported |
606
+ | `xsl:strip-space` / `xsl:preserve-space` | Supported |
607
+ | `xsl:fallback` | Parsed, never instantiated (no extension elements) |
395
608
 
396
609
  ## XPath Functions Supported
397
610
 
@@ -407,11 +620,28 @@ const result = evaluator.evaluate(ast, context);
407
620
  ### Number Functions
408
621
  - `ceiling()`, `floor()`, `number()`, `round()`, `sum()`
409
622
 
623
+ ### XSLT-Defined Functions
624
+ - `current()` - the XSLT current node, also inside predicates
625
+ - `document(object, base?)` - external documents, see `setDocumentLoader()`
626
+ - `element-available(name)`, `function-available(name)` - reflect the real element and function tables
627
+ - `format-number(number, pattern, decimalFormat?)` - full XSLT 1.0 picture strings, honouring `xsl:decimal-format`
628
+ - `generate-id(nodeSet?)` - stable identifier for the life of the transformation
629
+ - `key(name, value)` - `xsl:key` lookup, with lazily built per-document indexes
630
+ - `system-property(name)` - `xsl:version`, `xsl:vendor`, `xsl:vendor-url`
631
+ - `unparsed-entity-uri(name)` - always returns `''` (unparsed entities are not exposed by the DOM)
632
+
633
+ ### Conformance Notes
634
+ - CDATA sections count as text everywhere (string-value, `text()`, `xsl:value-of`, `xsl:copy-of`)
635
+ - The identity transform `<xsl:template match="@*|node()"><xsl:copy><xsl:apply-templates select="@*|node()"/></xsl:copy></xsl:template>` round-trips a document exactly
636
+ - `xsl:number` supports `level="single|multiple|any"` with `count`, `from` and the `1`, `01`, `a`, `A`, `i`, `I` format tokens
637
+ - The result tree is built in a neutral XML document and imported into the output
638
+ document at the end, so element names and namespaces survive an HTML owner document
639
+
410
640
  ## Development
411
641
 
412
642
  ### Prerequisites
413
643
 
414
- - Node.js 25+ (for native test runner)
644
+ - Node.js 22+ (native test runner; CI runs 22, 24 and 26)
415
645
  - Docker (optional, for containerized testing)
416
646
 
417
647
  ### Setup
@@ -455,30 +685,38 @@ docker-compose run build
455
685
 
456
686
  ### Publishing to npm
457
687
 
458
- The package is automatically published to npm when a GitHub release is created or a version tag is pushed.
688
+ The package is published to npm automatically by the `Release` workflow when a
689
+ `v*` tag is pushed (or a GitHub release is published). Publishing uses npm
690
+ **Trusted Publishing** (OIDC): no `NPM_TOKEN` secret and no OTP are involved,
691
+ and every release carries provenance attestations.
459
692
 
460
- **Prerequisites:**
461
- 1. Set up `NPM_TOKEN` secret in GitHub repository settings
462
- 2. Ensure version in `package.json` matches the release tag
693
+ **One-time prerequisite** (npmjs.com -> package `@tradik/xslt-processor` ->
694
+ Settings -> Trusted Publisher): provider *GitHub Actions*, owner `spagu`,
695
+ repository `XSLT-Processor`, workflow `release.yml`, environment left empty.
696
+ Alternatively add an `NPM_TOKEN` repository secret (granular access token
697
+ with publish rights and 2FA bypass); the publish step passes it as
698
+ `NODE_AUTH_TOKEN`. Without either, the `publish` job fails with `E404` and the
699
+ package must be published manually with
700
+ `npm publish --provenance --access public --otp=CODE`.
463
701
 
464
- **Release Process:**
702
+ **Release process:**
465
703
 
466
704
  ```bash
467
- # 1. Update version in package.json
468
- npm version patch # or minor, major
469
-
470
- # 2. Push the tag
471
- git push origin --tags
705
+ # 1. Bump the version in package.json, package-lock.json, src/index.js and
706
+ # add a CHANGELOG.md entry, then commit to main.
472
707
 
473
- # 3. Create a GitHub release (or push triggers automatically)
708
+ # 2. Tag and push the tag; the workflow refuses to publish if the tag does
709
+ # not match package.json.
710
+ git tag v1.1.1
711
+ git push origin v1.1.1
474
712
  ```
475
713
 
476
- **Automated Workflow:**
477
- 1. Runs all tests and linting checks
478
- 2. Performs security audit with `npm audit`
479
- 3. Builds distribution bundles
480
- 4. Publishes to npm with provenance (supply chain security)
481
- 5. Uploads build artifacts to GitHub
714
+ **Automated workflow:**
715
+ 1. Runs lint, formatting check and tests on Node.js 22, 24 and 26
716
+ 2. Builds the distribution bundles and verifies the package contents
717
+ 3. Checks that the tag matches the `package.json` version
718
+ 4. Uploads the `dist/` build artifacts to GitHub
719
+ 5. Publishes to npm with provenance
482
720
 
483
721
  ## Browser Compatibility
484
722
 
@@ -488,13 +726,13 @@ This library provides a JavaScript polyfill for XSLTProcessor that works across
488
726
 
489
727
  | Browser | Minimum Version | ES Modules | Status |
490
728
  |---------|-----------------|------------|--------|
491
- | Chrome | 90+ | Yes | Fully Supported |
492
- | Firefox | 88+ | Yes | Fully Supported |
493
- | Safari | 14+ | Yes | Fully Supported |
494
- | Edge | 90+ | Yes | Fully Supported |
495
- | Opera | 76+ | Yes | Fully Supported |
496
- | Samsung Internet | 15+ | Yes | Fully Supported |
497
- | Node.js | 25+ | Yes | Fully Supported |
729
+ | Chrome | 92+ | Yes | Fully Supported |
730
+ | Firefox | 92+ | Yes | Fully Supported |
731
+ | Safari | 15.4+ | Yes | Fully Supported |
732
+ | Edge | 92+ | Yes | Fully Supported |
733
+ | Opera | 78+ | Yes | Fully Supported |
734
+ | Samsung Internet | 16+ | Yes | Fully Supported |
735
+ | Node.js | 20.19+ | Yes | Fully Supported (CI: 22, 24, 26) |
498
736
 
499
737
  ### Native XSLT Deprecation Timeline
500
738
 
@@ -560,7 +798,12 @@ This implementation follows these W3C specifications with comprehensive test cov
560
798
  | 13 | Messages | Supported | `xsl:message` with `terminate` attribute |
561
799
  | 14 | Extensions | Partial | `xsl:fallback` supported |
562
800
  | 15 | Fallback | Supported | `xsl:fallback` element |
563
- | 16 | Output | Supported | `xsl:output` with method, encoding, indent |
801
+ | 16 | Output | Supported | `xsl:output` honored by `transformToString()` / `serializeResult()` |
802
+ | 16.1 | XML Output Method | Supported | XML declaration (`encoding`, `version`, `standalone`), `omit-xml-declaration`, `doctype-public`/`doctype-system`, namespace declarations, `indent="yes"` for element-only content |
803
+ | 16.1 | CDATA Sections | Supported | `cdata-section-elements`, split around `]]>` |
804
+ | 16.2 | HTML Output Method | Supported | Void elements as `<br>`, minimized boolean attributes, unescaped `script`/`style`, no re-indent inside `pre`/`script`/`style`/`textarea` |
805
+ | 16.3 | Text Output Method | Supported | Concatenation of all text nodes, no escaping |
806
+ | 16.4 | Disabling Output Escaping | Supported | `disable-output-escaping` on `xsl:text` and `xsl:value-of` |
564
807
 
565
808
  ### XPath 1.0 Specification Compliance
566
809
 
@@ -625,6 +868,7 @@ This implementation provides full compatibility with the [MDN XSLTProcessor API]
625
868
  | `importStylesheet(node)` | Supported | Accepts Document or Element |
626
869
  | `transformToFragment(source, output)` | Supported | Returns DocumentFragment |
627
870
  | `transformToDocument(source)` | Supported | Returns XMLDocument |
871
+ | `transformToString(source)` | Extension | Not part of the W3C API; returns the `xsl:output` serialized result |
628
872
  | `setParameter(namespaceURI, localName, value)` | Supported | Full namespace support |
629
873
  | `getParameter(namespaceURI, localName)` | Supported | Returns parameter value |
630
874
  | `removeParameter(namespaceURI, localName)` | Supported | Removes single parameter |
@@ -640,8 +884,9 @@ This implementation provides full compatibility with the [MDN XSLTProcessor API]
640
884
  | XPath 1.0 Axes | 26+ | All 13 axes |
641
885
  | DOM Level 3 | 20+ | Core interfaces |
642
886
  | XSLTProcessor API | 39+ | All methods |
887
+ | Output Serialization | 79+ | `xsl:output`, CLI, `transformToString()` |
643
888
  | Security | 34+ | DoS prevention, prototype pollution |
644
- | **Total** | **441** | **99.41% line coverage** |
889
+ | **Total** | **560** | **100% line coverage** |
645
890
 
646
891
  ## Style Guide
647
892
 
@@ -660,7 +905,7 @@ All colors meet WCAG 2.2 AA contrast requirements for accessibility.
660
905
 
661
906
  ## License
662
907
 
663
- MIT License - see [LICENSE](../../LICENSE) for details.
908
+ BSD-3-Clause License - see [LICENSE.md](LICENSE.md) for details.
664
909
 
665
910
  ## Related
666
911
 
@@ -0,0 +1,114 @@
1
+ /**
2
+ * XSLT Processor CLI - Option Handling
3
+ *
4
+ * Command line option definitions, help/version banners and parameter parsing.
5
+ */
6
+
7
+ "use strict";
8
+
9
+ import { createRequire } from "node:module";
10
+
11
+ const require = createRequire(import.meta.url);
12
+
13
+ /**
14
+ * Version of the CLI, read from the package manifest.
15
+ * @type {string}
16
+ */
17
+ export const VERSION = require("../../package.json").version;
18
+
19
+ /**
20
+ * parseArgs option definitions.
21
+ * @type {object}
22
+ */
23
+ export const CLI_OPTIONS = {
24
+ output: { type: "string", short: "o" },
25
+ param: { type: "string", short: "p", multiple: true },
26
+ format: { type: "boolean", short: "f", default: false },
27
+ indent: { type: "boolean", default: false },
28
+ method: { type: "string" },
29
+ "no-declaration": { type: "boolean", default: false },
30
+ help: { type: "boolean", short: "h", default: false },
31
+ version: { type: "boolean", short: "v", default: false },
32
+ };
33
+
34
+ /**
35
+ * Print the usage banner.
36
+ * @returns {void}
37
+ */
38
+ export function printHelp() {
39
+ console.log(`
40
+ xslt-processor - Transform XML documents using XSLT stylesheets
41
+
42
+ USAGE:
43
+ xslt <xml-file> <xslt-file> [options]
44
+
45
+ ARGUMENTS:
46
+ <xml-file> Path to XML source document
47
+ <xslt-file> Path to XSLT stylesheet
48
+
49
+ OPTIONS:
50
+ -o, --output <file> Write output to file instead of stdout
51
+ -p, --param <n>=<v> Set XSLT parameter (can be used multiple times)
52
+ -f, --format Format output with indentation (same as --indent)
53
+ --indent Override xsl:output to indent="yes"
54
+ --method <m> Override xsl:output method (xml|html|xhtml|text)
55
+ --no-declaration Override xsl:output to omit the XML declaration
56
+ -h, --help Show this help message
57
+ -v, --version Show version number
58
+
59
+ The output is serialized according to the xsl:output element of the
60
+ stylesheet; the options above override individual xsl:output settings.
61
+
62
+ All file arguments must live inside the current working directory, or inside
63
+ the directory named by the XSLT_BASE_DIR environment variable when it is set.
64
+
65
+ EXAMPLES:
66
+ # Basic transformation
67
+ xslt data.xml transform.xsl
68
+
69
+ # Save output to file
70
+ xslt data.xml transform.xsl -o result.html
71
+
72
+ # With parameters
73
+ xslt data.xml transform.xsl -p title="My Page" -p count=10
74
+
75
+ # Multiple parameters with formatted output
76
+ xslt data.xml transform.xsl -p lang=en -p debug=true -f -o output.html
77
+ `);
78
+ }
79
+
80
+ /**
81
+ * Print the version banner.
82
+ * @returns {void}
83
+ */
84
+ export function printVersion() {
85
+ console.log(`xslt-processor v${VERSION}`);
86
+ }
87
+
88
+ /**
89
+ * Parse `name=value` parameter arguments.
90
+ *
91
+ * @param {string[]|undefined} params - Raw `--param` values
92
+ * @returns {Record<string, string>} Parsed parameters
93
+ */
94
+ export function parseParameters(params) {
95
+ const result = {};
96
+
97
+ if (!params || !Array.isArray(params)) {
98
+ return result;
99
+ }
100
+
101
+ for (const param of params) {
102
+ const equalIndex = param.indexOf("=");
103
+ if (equalIndex === -1) {
104
+ console.error(
105
+ `Warning: Invalid parameter format "${param}". Expected name=value`,
106
+ );
107
+ continue;
108
+ }
109
+
110
+ result[param.substring(0, equalIndex)] = param.substring(equalIndex + 1);
111
+ }
112
+
113
+ return result;
114
+ }