Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a13620e
Added support for node streams and ajusted copyright year
rmraya Nov 13, 2025
e4dbd43
Fixed internal subset handling and improved entity resolution
rmraya Nov 13, 2025
daab18f
Fixed attribute parsing
rmraya Nov 13, 2025
152b7ff
Implemented DTD attributes normailzation
rmraya Nov 13, 2025
1af4538
Fixed entity declaration override
rmraya Nov 13, 2025
da14f53
Fixed last entity replacement issue for processing valid files
rmraya Nov 13, 2025
17e6c9e
Improved canonicalizer
rmraya Nov 13, 2025
ffda877
Improved XMLCanonicalizer
rmraya Nov 13, 2025
40c99c8
Improved Unicode chars handling
rmraya Nov 13, 2025
8c12a80
Improved attribute normalization
rmraya Nov 13, 2025
f8c060a
Fixed remaining attribute handling for valid files
rmraya Nov 13, 2025
27ce812
Fixed line ending normalization
rmraya Nov 13, 2025
dd58849
Extended test suote to handle not standalone files
rmraya Nov 13, 2025
5ae8a83
Improved external entitiies parsing
rmraya Nov 13, 2025
48c240f
Improved external attribute declarations handling
rmraya Nov 13, 2025
94f09f6
Updated test suit and fixed external entity loading
rmraya Nov 13, 2025
403a404
All valid test cases from the Test Suite pass
rmraya Nov 13, 2025
9e64353
Implemented compliance enforcement for invalid test files
rmraya Nov 13, 2025
45e23e2
Initial testing of not-well formed files
rmraya Nov 13, 2025
b2bce88
Improved character validation
rmraya Nov 13, 2025
1ca2b54
Unforced more stric adherence to XML standard
rmraya Nov 13, 2025
1d9eba4
Improved handling of not well-formed files
rmraya Nov 14, 2025
ebe901d
improved detection of not well-formed markup
rmraya Nov 14, 2025
5c28987
Tightened checks for parameter entities
rmraya Nov 14, 2025
e0a066d
Completed handlilng of standalone not well-formed files
rmraya Nov 14, 2025
657c367
Completed pass for validation test suite
rmraya Nov 14, 2025
743516b
Mentioned that the library passes DTD test suite
rmraya Nov 14, 2025
69934f9
Code cleanup with SonarQube
rmraya Nov 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ dist/
.vscode/
.scannerwork/
ts/test.ts
test.xml
test.xml
/tests/
64 changes: 42 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
# TypesXML

Open source XML library written in TypeScript with DOM and SAX support.
Open source XML library written in TypeScript with DOM and SAX support.

- Full DTD parsing and validation.
- Full support for OASIS XML Catalogs.
✅ Fully passes the complete W3C XML Conformance Test Suite (valid, invalid, not-wf, and external entity cases).

- Full DTD parsing and validation.
- Full support for OASIS XML Catalogs.

## SAX Parser

TypesXML implements a SAX parser that exposes these methods from the `ContentHandler` interface:

* initialize(): void;
* setCatalog(catalog: Catalog): void;
* startDocument(): void;
* endDocument(): void;
* xmlDeclaration(version: string, encoding: string, standalone: string): void;
* startElement(name: string, atts: Array\<XMLAttribute>): void;
* endElement(name: string): void;
* internalSubset(declaration: string): void;
* characters(ch: string): void;
* ignorableWhitespace(ch: string): void;
* comment(ch: string): void;
* processingInstruction(target: string, data: string): void;
* startCDATA(): void;
* endCDATA(): void;
* startDTD(name: string, publicId: string, systemId: string): void;
* endDTD(): void;
* skippedEntity(name: string): void;
- initialize(): void;
- setCatalog(catalog: Catalog): void;
- startDocument(): void;
- endDocument(): void;
- xmlDeclaration(version: string, encoding: string, standalone: string): void;
- startElement(name: string, atts: Array\<XMLAttribute>): void;
- endElement(name: string): void;
- internalSubset(declaration: string): void;
- characters(ch: string): void;
- ignorableWhitespace(ch: string): void;
- comment(ch: string): void;
- processingInstruction(target: string, data: string): void;
- startCDATA(): void;
- endCDATA(): void;
- startDTD(name: string, publicId: string, systemId: string): void;
- endDTD(): void;
- skippedEntity(name: string): void;

## DOM support

Class `DOMBuilder` implements the `ContentHandler` interface and builds a DOM tree from an XML document.

## On the Roadmap

* Support for XML Schemas
* Support for RelaxNG
- Support for XML Schemas
- Support for RelaxNG

## Installation

Expand Down Expand Up @@ -82,3 +84,21 @@ export class Test {

new Test();
```

## Running the W3C XML Test Suite

To exercise TypesXML against the full W3C XML Conformance Test Suite:

1. Download the latest archive from the [W3C XML Test Suite page](https://www.w3.org/XML/Test/)
(for example, `xmlts20080827.zip`).
2. Extract the contents of the archive into the repository at `./tests/xmltest` so that the
directory contains the `valid`, `invalid`, and `not-wf` folders from the suite.
3. Install project dependencies if you have not already done so: `npm install`.
4. Run the DTD regression command:

```bash
npm run testDtd
```

The script compiles the TypeScript sources and executes the harness in
`ts/tests/DTDTestSuite.ts`, reporting any conformance failures.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "typesxml",
"productName": "TypesXML",
"version": "1.12.0",
"version": "1.13.0",
"description": "Open source XML library written in TypeScript",
"keywords": [
"XML",
Expand All @@ -13,7 +13,8 @@
"TypeScript"
],
"scripts": {
"build": "tsc"
"build": "tsc",
"testDtd": "tsc && node dist/tests/DTDTestSuite.js"
},
"author": {
"name": "Rodolfo M. Raya",
Expand All @@ -31,7 +32,7 @@
"url": "https://github.com/rmraya/TypesXML.git"
},
"devDependencies": {
"@types/node": "^24.10.0",
"@types/node": "^24.10.1",
"typescript": "^5.9.3"
}
}
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
sonar.projectKey=TypesXML
# this is the name displayed in the SonarQube UI
sonar.projectName=TypesXML
sonar.projectVersion=1.3.1
sonar.projectVersion=1.13.0

# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
# Since SonarQube 4.2, this property is optional if sonar.modules is set.
Expand Down
2 changes: 1 addition & 1 deletion ts/CData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023 - 2024 Maxprograms.
* Copyright (c) 2023 - 2025 Maxprograms.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse License 1.0
Expand Down
28 changes: 14 additions & 14 deletions ts/Catalog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023 - 2024 Maxprograms.
* Copyright (c) 2023 - 2025 Maxprograms.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse License 1.0
Expand All @@ -10,8 +10,8 @@
* Maxprograms - initial API and implementation
*******************************************************************************/

import { existsSync } from "fs";
import * as path from "node:path";
import { existsSync } from "node:fs";
import { basename, dirname, isAbsolute, resolve } from "node:path";
import { DOMBuilder } from "./DOMBuilder";
import { SAXParser } from "./SAXParser";
import { XMLAttribute } from "./XMLAttribute";
Expand All @@ -32,7 +32,7 @@ export class Catalog {
base: string;

constructor(catalogFile: string) {
if (!path.isAbsolute(catalogFile)) {
if (!isAbsolute(catalogFile)) {
throw new Error('Catalog file must be absolute: ' + catalogFile);
}
if (!existsSync(catalogFile)) {
Expand All @@ -45,7 +45,7 @@ export class Catalog {
this.dtdCatalog = new Map<string, string>();
this.uriRewrites = new Array<string[]>();
this.systemRewrites = new Array<string[]>();
this.workDir = path.dirname(catalogFile);
this.workDir = dirname(catalogFile);
this.base = '';

let contentHandler: DOMBuilder = new DOMBuilder();
Expand Down Expand Up @@ -75,8 +75,8 @@ export class Catalog {
if (!this.base.endsWith('/')) {
this.base += '/';
}
if (!path.isAbsolute(this.base)) {
this.base = path.resolve(this.workDir, this.base);
if (!isAbsolute(this.base)) {
this.base = resolve(this.workDir, this.base);
}
if (!existsSync(this.base)) {
throw new Error('Invalid xml:base: ' + this.base);
Expand All @@ -100,7 +100,7 @@ export class Catalog {
if (existsSync(uri)) {
this.publicCatalog.set(publicId, uri);
if (uri.endsWith(".dtd") || uri.endsWith(".ent") || uri.endsWith(".mod")) {
let name: string = path.basename(uri);
let name: string = basename(uri);
if (!this.dtdCatalog.has(name)) {
this.dtdCatalog.set(name, uri);
}
Expand All @@ -121,7 +121,7 @@ export class Catalog {
}
this.systemCatalog.set(systemId.getValue(), uri);
if (uri.endsWith(".dtd")) {
let name: string = path.basename(uri);
let name: string = basename(uri);
if (!this.dtdCatalog.has(name)) {
this.dtdCatalog.set(name, uri);
}
Expand All @@ -141,7 +141,7 @@ export class Catalog {
}
this.uriCatalog.set(nameAttribute.getValue(), uri);
if (uri.endsWith(".dtd") || uri.endsWith(".ent") || uri.endsWith(".mod")) {
let name: string = path.basename(uri);
let name: string = basename(uri);
if (!this.dtdCatalog.has(name)) {
this.dtdCatalog.set(name, uri);
}
Expand Down Expand Up @@ -229,11 +229,11 @@ export class Catalog {

makeAbsolute(uri: string): string {
let file: string = this.base + uri;
if (!path.isAbsolute(file)) {
if (!isAbsolute(file)) {
if (this.base !== '') {
return path.resolve(this.base, uri);
return resolve(this.base, uri);
}
return path.resolve(this.workDir, uri);
return resolve(this.workDir, uri);
}
return this.base + uri;
}
Expand Down Expand Up @@ -300,7 +300,7 @@ export class Catalog {
if (this.systemCatalog.has(systemId)) {
return this.systemCatalog.get(systemId);
}
let fileName: string = path.basename(systemId);
let fileName: string = basename(systemId);
if (this.dtdCatalog.has(fileName)) {
return this.dtdCatalog.get(fileName);
}
Expand Down
2 changes: 1 addition & 1 deletion ts/Constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023 - 2024 Maxprograms.
* Copyright (c) 2023 - 2025 Maxprograms.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse License 1.0
Expand Down
3 changes: 2 additions & 1 deletion ts/ContentHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023 - 2024 Maxprograms.
* Copyright (c) 2023 - 2025 Maxprograms.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse License 1.0
Expand Down Expand Up @@ -43,4 +43,5 @@ export interface ContentHandler {
skippedEntity(name: string): void;

getGrammar(): Grammar | undefined;
setGrammar(grammar: Grammar | undefined): void;
}
58 changes: 51 additions & 7 deletions ts/DOMBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
/*******************************************************************************
* Copyright (c) 2023 - 2025 Maxprograms.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse License 1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/epl-v10.html
*
* Contributors:
* Maxprograms - initial API and implementation
*******************************************************************************/

import { CData } from "./CData";
import { Catalog } from "./Catalog";
import { ContentHandler } from "./ContentHandler";
Expand All @@ -11,6 +23,7 @@ import { XMLDocumentType } from "./XMLDocumentType";
import { XMLElement } from "./XMLElement";
import { XMLUtils } from "./XMLUtils";
import { DTDParser } from "./dtd/DTDParser";
import { DTDGrammar } from "./dtd/DTDGrammar";
import { Grammar } from "./grammar/Grammar";

export class DOMBuilder implements ContentHandler {
Expand All @@ -29,6 +42,17 @@ export class DOMBuilder implements ContentHandler {
this.inCdData = false;
}

setGrammar(grammar: Grammar | undefined): void {
this.grammar = grammar;
if (grammar instanceof DTDGrammar) {
if (!this.dtdParser) {
this.dtdParser = new DTDParser(grammar);
} else {
this.dtdParser.setGrammar(grammar);
}
}
}

setCatalog(catalog: Catalog): void {
this.catalog = catalog;
}
Expand Down Expand Up @@ -172,15 +196,13 @@ export class DOMBuilder implements ContentHandler {
let docType: XMLDocumentType = new XMLDocumentType(name, publicId, systemId);
this.document?.setDocumentType(docType);
if (this.catalog) {
let url = this.catalog.resolveEntity(publicId, systemId);
const url = this.catalog.resolveEntity(publicId, systemId);
if (url) {
if (!this.dtdParser) {
this.dtdParser = new DTDParser();
}
let dtdGrammar: Grammar = this.dtdParser.parseDTD(url);
if (dtdGrammar) {
this.grammar = dtdGrammar;
}
const dtdGrammar: DTDGrammar = this.dtdParser.parseDTD(url);
this.setGrammar(dtdGrammar);
}
}
}
Expand All @@ -194,7 +216,29 @@ export class DOMBuilder implements ContentHandler {
}

skippedEntity(name: string): void {
// TODO
throw new Error("Method not implemented.");
const replacement: string | undefined = this.grammar?.resolveEntity(name);
if (replacement && replacement.length > 0) {
this.characters(replacement);
return;
}

if (this.grammar instanceof DTDGrammar) {
const entityDecl = this.grammar.getEntity(name);
if (entityDecl && entityDecl.isExternal() && this.dtdParser) {
try {
const externalText = this.dtdParser.loadExternalEntity(entityDecl.getPublicId(), entityDecl.getSystemId(), true);
if (externalText.length > 0) {
entityDecl.setValue(externalText);
this.characters(externalText);
return;
}
} catch (error) {
throw new Error(`Could not resolve external entity "${name}": ${(error as Error).message}`);
}
}
}

// Preserve the reference if no replacement text is available
this.characters('&' + name + ';');
}
}
Loading
Loading