diff --git a/README.md b/README.md index 2ab59b1..c75b7ca 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Open source XML library written in TypeScript -Implements a SAX parser that exposes the these methods from the `ContentHandler` interface: +Implements a SAX parser that exposes these methods from the `ContentHandler` interface: * initialize(): void; * setCatalog(catalog: Catalog): void; @@ -26,7 +26,7 @@ Class `DOMBuilder` implements the `ContentHandler` interface and builds a DOM tr ## Features currently in development -* Parsing of the Internal Subset specified in the declaration +* Parsing of DTDs and internal subsets from ## Limitations diff --git a/package.json b/package.json index cc3c6e8..8c64815 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,15 @@ { "name": "typesxml", "productName": "TypesXML", - "version": "1.2.1", + "version": "1.3.0", "description": "Open source XML library written in TypeScript", + "keywords": [ + "XML", + "Parser", + "DOM", + "SAX", + "DTD" + ], "scripts": { "build": "tsc" }, @@ -22,7 +29,7 @@ "url": "https://github.com/rmraya/TypesXML.git" }, "devDependencies": { - "@types/node": "^20.10.0", - "typescript": "^5.3.2" + "@types/node": "^20.10.4", + "typescript": "^5.3.3" } } \ No newline at end of file diff --git a/ts/CData.ts b/ts/CData.ts index c55aea6..24bd692 100644 --- a/ts/CData.ts +++ b/ts/CData.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/Catalog.ts b/ts/Catalog.ts index 4cffbf0..fa85c98 100644 --- a/ts/Catalog.ts +++ b/ts/Catalog.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 @@ -10,8 +10,8 @@ * Maxprograms - initial API and implementation *******************************************************************************/ -import path = require("path"); import { existsSync } from "fs"; +import * as path from "node:path"; import { ContentHandler } from "./ContentHandler"; import { DOMBuilder } from "./DOMBuilder"; import { SAXParser } from "./SAXParser"; @@ -84,7 +84,7 @@ export class Catalog { let uri: string = this.makeAbsolute(child.getAttribute("uri").getValue()); if (existsSync(uri)) { this.publicCatalog.set(publicId, uri); - if (uri.endsWith(".dtd")) { + if (uri.endsWith(".dtd") || uri.endsWith(".ent") || uri.endsWith(".mod")) { let name: string = path.basename(uri); if (!this.dtdCatalog.has(name)) { this.dtdCatalog.set(name, uri); @@ -109,7 +109,7 @@ export class Catalog { let uri: string = this.makeAbsolute(child.getAttribute("uri").getValue()); if (existsSync(uri)) { this.uriCatalog.set(child.getAttribute("name").getValue(), uri); - if (uri.endsWith(".dtd")) { + if (uri.endsWith(".dtd") || uri.endsWith(".ent") || uri.endsWith(".mod")) { let name: string = path.basename(uri); if (!this.dtdCatalog.has(name)) { this.dtdCatalog.set(name, uri); @@ -135,25 +135,25 @@ export class Catalog { let nextCatalog: string = this.makeAbsolute(child.getAttribute("catalog").getValue()); let catalog: Catalog = new Catalog(nextCatalog); let map: Map = catalog.getSystemCatalog(); - map.forEach((key, value) => { + map.forEach((value, key) => { if (!this.systemCatalog.has(key)) { this.systemCatalog.set(key, value); } }); map = catalog.getPublicCatalog(); - map.forEach((key, value) => { + map.forEach((value, key) => { if (!this.publicCatalog.has(key)) { this.publicCatalog.set(key, value); } }); map = catalog.getUriCatalog(); - map.forEach((key, value) => { + map.forEach((value, key) => { if (!this.uriCatalog.has(key)) { this.uriCatalog.set(key, value); } }); map = catalog.getDtdCatalog(); - map.forEach((key, value) => { + map.forEach((value, key) => { if (!this.dtdCatalog.has(key)) { this.dtdCatalog.set(key, value); } diff --git a/ts/Constants.ts b/ts/Constants.ts index b5700e9..b995028 100644 --- a/ts/Constants.ts +++ b/ts/Constants.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 @@ -23,4 +23,11 @@ export class Constants { static readonly XML_DECLARATION_NODE: number = 8; static readonly ATTRIBUTE_LIST_DECL_NODE: number = 9; static readonly DOCUMENT_TYPE_NODE: number = 10; + + // constants for DTD parser + + static readonly ATTRIBUTE_DECL_NODE: number = 11; + static readonly ELEMENT_DECL_NODE: number = 12; + static readonly INTERNAL_SUBSET_NODE: number = 13; + static readonly NOTATION_DECL_NODE: number = 14; } \ No newline at end of file diff --git a/ts/ContentHandler.ts b/ts/ContentHandler.ts index 8e8243c..60ddf50 100644 --- a/ts/ContentHandler.ts +++ b/ts/ContentHandler.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/DOMBuilder.ts b/ts/DOMBuilder.ts index 9b9357c..5d5c28f 100644 --- a/ts/DOMBuilder.ts +++ b/ts/DOMBuilder.ts @@ -10,6 +10,8 @@ import { CData } from "./CData"; import { XMLDocumentType } from "./XMLDocumentType"; import { Catalog } from "./Catalog"; import { XMLUtils } from "./XMLUtils"; +import { DTDParser } from "./dtd/DTDParser"; +import { Grammar } from "./grammar/Grammar"; export class DOMBuilder implements ContentHandler { @@ -18,7 +20,9 @@ export class DOMBuilder implements ContentHandler { document: XMLDocument; stack: Array; catalog: Catalog; + dtdParser: DTDParser; grammarUrl: string; + grammar: Grammar; initialize(): void { this.document = new XMLDocument(); @@ -30,6 +34,10 @@ export class DOMBuilder implements ContentHandler { this.catalog = catalog; } + setDTDParser(dtdParser: DTDParser): void { + this.dtdParser = dtdParser; + } + getDocument(): XMLDocument { return this.document; } @@ -175,6 +183,13 @@ export class DOMBuilder implements ContentHandler { this.document.setDocumentType(docType); if (this.catalog) { this.grammarUrl = this.catalog.resolveEntity(publicId, systemId); + // TODO check grammar type (DTD, XDS or RelaxNG) and use the ritght parser + if (this.dtdParser && this.grammarUrl) { + let dtdGrammar :Grammar = this.dtdParser.parseDTD(this.grammarUrl); + if (dtdGrammar) { + this.grammar = dtdGrammar; + } + } } } diff --git a/ts/FileReader.ts b/ts/FileReader.ts index eb47368..931fc52 100644 --- a/ts/FileReader.ts +++ b/ts/FileReader.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/Indenter.ts b/ts/Indenter.ts index 4a7f683..9dcf11f 100644 --- a/ts/Indenter.ts +++ b/ts/Indenter.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/ProcessingInstruction.ts b/ts/ProcessingInstruction.ts index 975260c..4376e7d 100644 --- a/ts/ProcessingInstruction.ts +++ b/ts/ProcessingInstruction.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/SAXParser.ts b/ts/SAXParser.ts index 703891c..00e8950 100644 --- a/ts/SAXParser.ts +++ b/ts/SAXParser.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 @@ -27,6 +27,8 @@ export class SAXParser { characterRun: string; rootParsed: boolean; + static readonly MIN_BUFFER_SIZE: number = 2048; + constructor() { this.characterRun = ''; this.elementStack = 0; @@ -65,7 +67,7 @@ export class SAXParser { this.contentHandler.startDocument(); while (this.pointer < this.buffer.length) { if (this.lookingAt(' this.buffer.length && this.reader.dataAvailable()) { + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { this.buffer += this.reader.read(); } if (this.rootParsed && this.elementStack === 0) { @@ -128,7 +130,7 @@ export class SAXParser { let name: string = ''; while (!this.lookingAt(';')) { name += this.buffer.charAt(this.pointer++); - if (this.pointer > this.buffer.length && this.reader.dataAvailable()) { + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { this.buffer += this.reader.read(); } } @@ -162,14 +164,14 @@ export class SAXParser { let name: string = ''; while (!XMLUtils.isXmlSpace(this.buffer.charAt(this.pointer)) && !this.lookingAt('>') && !this.lookingAt('/>')) { name += this.buffer.charAt(this.pointer++); - if (this.pointer > this.buffer.length && this.reader.dataAvailable()) { + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { this.buffer += this.reader.read(); } } let rest: string = ''; while (!this.lookingAt('>') && !this.lookingAt('/>')) { rest += this.buffer.charAt(this.pointer++); - if (this.pointer > this.buffer.length && this.reader.dataAvailable()) { + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { this.buffer += this.reader.read(); } } @@ -177,6 +179,7 @@ export class SAXParser { let attributesMap: Map = this.parseAttributes(rest); let attributes: Array = []; attributesMap.forEach((value: string, key: string) => { + // TODO https://www.w3.org/TR/REC-xml/#AVNormalize let attribute: XMLAttribute = new XMLAttribute(key, value); attributes.push(attribute); }); @@ -186,6 +189,7 @@ export class SAXParser { this.rootParsed = true; } if (this.lookingAt('/>')) { + this.cleanCharacterRun(); this.contentHandler.endElement(name); this.elementStack--; this.pointer += 2; // skip '/>' @@ -274,147 +278,189 @@ export class SAXParser { } parseDoctype() { - let declaration: string = ''; + this.cleanCharacterRun(); this.pointer += 9; // skip '= this.buffer.length && this.reader.dataAvailable()) { + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { this.buffer += this.reader.read(); } } - this.pointer += i; - i = 0; // read name let name: string = ''; - for (; i < this.buffer.length; i++) { - let char: string = this.buffer.charAt(this.pointer + i); + for (; this.pointer < this.buffer.length; this.pointer++) { + let char: string = this.buffer.charAt(this.pointer); if (XMLUtils.isXmlSpace(char)) { break; } name += char; - if (this.pointer + i + 1 >= this.buffer.length && this.reader.dataAvailable()) { + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { this.buffer += this.reader.read(); } } - this.pointer += i; - i = 0; // skip spaces after root name - for (; i < this.buffer.length; i++) { - let char: string = this.buffer.charAt(this.pointer + i); + for (; this.pointer < this.buffer.length; this.pointer++) { + let char = this.buffer[this.pointer]; if (!XMLUtils.isXmlSpace(char)) { break; } - if (this.pointer + i + 1 >= this.buffer.length && this.reader.dataAvailable()) { + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { this.buffer += this.reader.read(); } } - this.pointer += i; - // read the rest of the declaration - let stack: number = 1; + // read external identifiers + let systemId: string = ''; + if (this.lookingAt('SYSTEM')) { + systemId = this.parseSystemDeclaration(); + } + let publicId: string = ''; + if (this.lookingAt('PUBLIC')) { + let pair: string[] = this.parsePublicDeclaration(); + publicId = pair[0]; + systemId = pair[1]; + } + this.contentHandler.startDTD(name, publicId, systemId); + // skip spaces after SYSTEM or PUBLIC for (; this.pointer < this.buffer.length; this.pointer++) { - let char: string = this.buffer[this.pointer]; - if ('<' === char) { - stack++; + let char = this.buffer[this.pointer]; + if (!XMLUtils.isXmlSpace(char)) { + break; + } + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { + this.buffer += this.reader.read(); } - if ('>' === char) { - stack--; - if (stack === 0) { + } + // check internal subset + let internalSubset: string = ''; + if (this.lookingAt('[')) { + this.pointer++; // skip '[' + for (; this.pointer < this.buffer.length; this.pointer++) { + let char: string = this.buffer[this.pointer]; + if (']' === char) { break; } + internalSubset += char; + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { + this.buffer += this.reader.read(); + } + } + this.pointer++; // skip ']' + } + // skip spaces after internal subset + for (; this.pointer < this.buffer.length; this.pointer++) { + let char = this.buffer[this.pointer]; + if (!XMLUtils.isXmlSpace(char)) { + break; } - declaration += char; - if (this.pointer + 1 > this.buffer.length && this.reader.dataAvailable()) { + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { this.buffer += this.reader.read(); } } - this.buffer = this.buffer.substring(this.pointer + 1); // skip '>' + this.pointer++; // skip '>' + this.buffer = this.buffer.substring(this.pointer); this.pointer = 0; - let systemId: string = this.extractSystem(declaration); - let publicId: string = this.extractPublic(declaration); - let internalSubset: string = this.extractInternal(declaration); - this.contentHandler.startDTD(name, publicId, systemId); if (internalSubset !== '') { this.contentHandler.internalSubset(internalSubset); } this.contentHandler.endDTD(); } - extractInternal(declaration: string): string { - let index = declaration.indexOf('['); - if (index === -1) { - return ''; - } - let end = declaration.indexOf(']'); - if (end === -1) { - return ''; - } - return declaration.substring(index + 1, end); - } - - extractPublic(declaration: string): string { - let index = declaration.indexOf('PUBLIC'); - if (index === -1) { - return ''; - } + parsePublicDeclaration(): string[] { + this.pointer += 6; // skip 'PUBLIC' // skip spaces after PUBLIC - let i: number = 6; - for (; i < declaration.length; i++) { - let char = declaration[i]; + for (; this.pointer < this.buffer.length; this.pointer++) { + let char = this.buffer[this.pointer]; if (!XMLUtils.isXmlSpace(char)) { break; } + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { + this.buffer += this.reader.read(); + } } let separator: string = ''; let publicId: string = ''; - for (; i < declaration.length; i++) { - let char = declaration[i]; + for (; this.pointer < this.buffer.length; this.pointer++) { + let char = this.buffer[this.pointer]; if (separator === '' && ('\'' === char || '"' === char)) { separator = char; continue; } if (char === separator) { + this.pointer++; // skip separator break; } publicId += char; + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { + this.buffer += this.reader.read(); + } + } + // skip spaces after publicId + for (; this.pointer < this.buffer.length; this.pointer++) { + let char = this.buffer[this.pointer]; + if (!XMLUtils.isXmlSpace(char)) { + break; + } + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { + this.buffer += this.reader.read(); + } } - return publicId; + separator = ''; + let systemIdId: string = ''; + for (; this.pointer < this.buffer.length; this.pointer++) { + let char = this.buffer[this.pointer]; + if (separator === '' && ('\'' === char || '"' === char)) { + separator = char; + continue; + } + if (char === separator) { + this.pointer++; // skip separator + break; + } + systemIdId += char; + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { + this.buffer += this.reader.read(); + } + } + return [publicId, systemIdId]; } - extractSystem(declaration: string): string { - let index: number = declaration.indexOf('SYSTEM'); - if (index === -1) { - return ''; - } + parseSystemDeclaration(): string { + this.pointer += 6; // skip 'SYSTEM' // skip spaces after SYSTEM - let i: number = 6; - for (; i < declaration.length; i++) { - let char = declaration[i]; + for (; this.pointer < this.buffer.length; this.pointer++) { + let char = this.buffer[this.pointer]; if (!XMLUtils.isXmlSpace(char)) { break; } + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { + this.buffer += this.reader.read(); + } } let separator: string = ''; let systemId: string = ''; - for (; i < declaration.length; i++) { - let char = declaration[i]; + for (; this.pointer < this.buffer.length; this.pointer++) { + let char = this.buffer[this.pointer]; if (separator === '' && ('\'' === char || '"' === char)) { separator = char; continue; } if (char === separator) { + this.pointer++; // skip separator break; } systemId += char; + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { + this.buffer += this.reader.read(); + } } return systemId; } - parseXMLDecl() { + parseXMLDeclaration() { let declarationText: string = ''; this.pointer += 6; // skip '')) { @@ -429,7 +475,7 @@ export class SAXParser { lookingAt(text: string): boolean { let length: number = text.length; - if (this.pointer + length > this.buffer.length && this.reader.dataAvailable()) { + if (this.buffer.length - this.pointer < SAXParser.MIN_BUFFER_SIZE && this.reader.dataAvailable()) { this.buffer += this.reader.read(); } if (this.pointer + length > this.buffer.length) { @@ -498,5 +544,4 @@ export class SAXParser { this.pointer = 0; this.contentHandler.endCDATA(); } - } \ No newline at end of file diff --git a/ts/StringReader.ts b/ts/StringReader.ts index 508532c..2ddcdd5 100644 --- a/ts/StringReader.ts +++ b/ts/StringReader.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/TextNode.ts b/ts/TextNode.ts index 6dc2d72..e066a17 100644 --- a/ts/TextNode.ts +++ b/ts/TextNode.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/XMLAttribute.ts b/ts/XMLAttribute.ts index 3dd958a..b00586b 100644 --- a/ts/XMLAttribute.ts +++ b/ts/XMLAttribute.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/XMLComment.ts b/ts/XMLComment.ts index 269e227..ae7df6f 100644 --- a/ts/XMLComment.ts +++ b/ts/XMLComment.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/XMLDeclaration.ts b/ts/XMLDeclaration.ts index 49f99b1..78f98c1 100644 --- a/ts/XMLDeclaration.ts +++ b/ts/XMLDeclaration.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/XMLDocument.ts b/ts/XMLDocument.ts index fe330c7..da70e40 100644 --- a/ts/XMLDocument.ts +++ b/ts/XMLDocument.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/XMLDocumentType.ts b/ts/XMLDocumentType.ts index 64a1d1c..8979283 100644 --- a/ts/XMLDocumentType.ts +++ b/ts/XMLDocumentType.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/XMLElement.ts b/ts/XMLElement.ts index 8931b95..aeed238 100644 --- a/ts/XMLElement.ts +++ b/ts/XMLElement.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 @@ -102,6 +102,19 @@ export class XMLElement implements XMLNode { return Constants.ELEMENT_NODE; } + getHead(): string { + let result: string = '<' + this.name; + this.attributes.forEach((value: XMLAttribute) => { + result += ' ' + value.toString(); + }); + result += '>'; + return result; + } + + getTail(): string { + return ''; + } + toString(): string { let result: string = '<' + this.name; this.attributes.forEach((value: XMLAttribute) => { diff --git a/ts/XMLNode.ts b/ts/XMLNode.ts index b977784..e57633b 100644 --- a/ts/XMLNode.ts +++ b/ts/XMLNode.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/XMLReader.ts b/ts/XMLReader.ts index f5f3a77..ad8b79e 100644 --- a/ts/XMLReader.ts +++ b/ts/XMLReader.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 diff --git a/ts/XMLUtils.ts b/ts/XMLUtils.ts index 14828e4..31d6acd 100644 --- a/ts/XMLUtils.ts +++ b/ts/XMLUtils.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 @@ -33,6 +33,24 @@ export class XMLUtils { return this.SPACES.indexOf(char) > -1; } + static hasParameterEntity(text: string) { + let index: number = text.indexOf('%'); + if (index === -1) { + return false; + } + let length: number = text.length; + for (let i = index + 1; i < length; i++) { + let c: string = text.charAt(i); + if (this.isXmlSpace(c) ) { + return false; + } + if (c === ';') { + return true; + } + } + return false; + } + static normalizeSpaces(text: string): string { return text.replace(/\s+/g, ' '); } diff --git a/ts/XMLWriter.ts b/ts/XMLWriter.ts index 4f79d76..5c25031 100644 --- a/ts/XMLWriter.ts +++ b/ts/XMLWriter.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 @@ -10,19 +10,66 @@ * Maxprograms - initial API and implementation *******************************************************************************/ -import { writeFileSync } from "fs"; +import { appendFileSync, writeFileSync } from "fs"; import { XMLDeclaration } from "./XMLDeclaration"; import { XMLDocument } from "./XMLDocument"; +import { XMLNode } from "./XMLNode"; export class XMLWriter { + static UTF16: Buffer = Buffer.from([-2, -1]); + + file: string; + options: any = { + encoding: 'utf8' + } + started: boolean; + + constructor(file: string) { + this.file = file; + this.started = false; + } + + writeNode(node: XMLNode): void { + if (node instanceof XMLDeclaration) { + let enc: string = node.getEncoding(); + if (enc === 'UTF-16LE') { + // write BOM for UTF-16LE + this.options.encoding = 'utf16le'; + writeFileSync(this.file, XMLWriter.UTF16, this.options); + this.started = true; + } + } + if (!this.started) { + this.started = true; + writeFileSync(this.file, node.toString(), this.options); + return; + } + appendFileSync(this.file, node.toString(), this.options); + } + + writeString(str: string): void { + if (!this.started) { + this.started = true; + writeFileSync(this.file, str, this.options); + return; + } + appendFileSync(this.file, str, this.options); + } + static writeDocument(doc: XMLDocument, file: string): void { let options: any = { encoding: 'utf8' }; let decl: XMLDeclaration = doc.getXmlDeclaration(); - if (decl) { - options.encoding = decl.getEncoding(); + if (decl && decl.getEncoding() === 'UTF-16LE') { + options.encoding = 'utf16le'; + } + if (options.encoding === 'utf16le') { + // write BOM for UTF-16LE + writeFileSync(file, XMLWriter.UTF16, options); + appendFileSync(file, doc.toString(), options); + return; } writeFileSync(file, doc.toString(), options); } diff --git a/ts/dtd/AttDecl.ts b/ts/dtd/AttDecl.ts new file mode 100644 index 0000000..d222f0c --- /dev/null +++ b/ts/dtd/AttDecl.ts @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2023 - 2024 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 { Constants } from "../Constants"; +import { XMLNode } from "../XMLNode"; + +export class AttDecl implements XMLNode { + + private name: string; + private attType: string; + private defaultDecl: string; + private defaultValue: string; + + constructor(name: string, attType: string, defaultDecl: string, defaultValue: string) { + this.name = name; + this.attType = attType; + this.defaultDecl = defaultDecl; + this.defaultValue = defaultValue; + } + + getNodeType(): number { + return Constants.ATTRIBUTE_DECL_NODE; + } + + equals(node: XMLNode): boolean { + if (node instanceof AttDecl) { + return this.name === node.name && this.attType === node.attType && this.defaultDecl === node.defaultDecl && this.defaultValue === node.defaultValue; + } + return false; + } + + toString(): string { + return (this.name + ' ' + this.attType + ' ' + this.defaultDecl + ' ' + this.defaultValue).trim(); + } +} \ No newline at end of file diff --git a/ts/dtd/AttListDecl.ts b/ts/dtd/AttListDecl.ts new file mode 100644 index 0000000..9027542 --- /dev/null +++ b/ts/dtd/AttListDecl.ts @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright (c) 2023 - 2024 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 { Constants } from "../Constants"; +import { XMLNode } from "../XMLNode"; +import { AttDecl } from "./AttDecl"; + +export class AttListDecl implements XMLNode { + + private name: string; + private attributes: Map; + + static attTypes: string[] = ['CDATA', 'ID', 'IDREF', 'IDREFS', 'ENTITY', 'ENTITIES', 'NMTOKEN', 'NMTOKENS']; + + constructor(name: string, attributesText: string) { + this.name = name; + this.attributes = new Map(); + this.parseAttributes(attributesText); + } + + getName(): string { + return this.name; + } + + getAttributes(): Map { + return this.attributes; + } + + parseAttributes(text: string) { + let parts: string[] = this.split(text); + let index: number = 0; + while (index < parts.length) { + let name: string = parts[index++]; + let attType: string = parts[index++]; + let defaultDecl: string = ''; + let defaultValue: string = ''; + if (AttListDecl.attTypes.includes(attType)) { + defaultDecl = parts[index++]; + if (defaultDecl === '#FIXED') { + defaultValue = parts[index++]; + } + } else { + if (attType === 'NOTATION') { + // TODO parse the notations in the ennumeration that follows + } else { + defaultValue = parts[index++]; + } + } + let att: AttDecl = new AttDecl(name, attType, defaultDecl, defaultValue); + this.attributes.set(name, att); + } + } + + split(text: string): string[] { + let result: string[] = []; + let word: string = ''; + for (let i = 0; i < text.length; i++) { + let c: string = text.charAt(i); + if (c === '(') { + // starts an enumeration + let enumeration: string = '('; + while (c !== ')') { + c = text.charAt(++i); + enumeration += c; + } + result.push(enumeration); + continue; + } + if (c === ' ' || c === '\n' || c === '\r' || c === '\t') { + if (word.length > 0) { + result.push(word); + word = ''; + } + } else { + word += c; + } + } + if (word.length > 0) { + result.push(word); + } + return result; + } + + getNodeType(): number { + return Constants.ATTRIBUTE_LIST_DECL_NODE; + } + + toString(): string { + let result: string = ' { + result += ' ' + a.toString() + '\n'; + }); + return result + '>'; + } + + equals(node: XMLNode): boolean { + if (node instanceof AttListDecl) { + let nodeAtts: Map = node.getAttributes(); + if (this.name !== node.getName() || this.attributes.size !== nodeAtts.size) { + return false; + } + this.attributes.forEach((value: AttDecl, key: string) => { + if (!value.equals(nodeAtts.get(key))) { + return false; + } + }); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/ts/dtd/DTDParser.ts b/ts/dtd/DTDParser.ts new file mode 100644 index 0000000..4bc4e07 --- /dev/null +++ b/ts/dtd/DTDParser.ts @@ -0,0 +1,745 @@ +/******************************************************************************* + * Copyright (c) 2023 - 2024 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 { Stats, closeSync, openSync, readSync, statSync } from "fs"; +import * as path from "node:path"; +import { Catalog } from "../Catalog"; +import { XMLUtils } from "../XMLUtils"; +import { Grammar } from "../grammar/Grammar"; +import { AttListDecl } from "./AttListDecl"; +import { ElementDecl } from "./ElementDecl"; +import { EntityDecl } from "./EntityDecl"; +import { NotationDecl } from "./NotationDecl"; + +export class DTDParser { + + private grammar: Grammar; + private catalog: Catalog; + private pointer: number = 0; + private source: string; + private currentFile: string; + + constructor(grammar?: Grammar) { + this.currentFile = ''; + if (grammar) { + this.grammar = grammar; + } else { + this.grammar = new Grammar(); + } + } + + setCatalog(catalog: Catalog) { + this.catalog = catalog; + } + + parseDTD(file: string): Grammar { + this.parseFile(file); + this.grammar.processModels(); + return this.grammar; + } + + parseFile(file: string): Grammar { + this.source = ''; + let stats: Stats = statSync(file, { bigint: false, throwIfNoEntry: true }); + this.currentFile = file; + let blockSize: number = stats.blksize; + let fileHandle = openSync(file, 'r'); + let buffer = Buffer.alloc(blockSize); + let bytesRead: number = readSync(fileHandle, buffer, 0, blockSize, 0); + while (bytesRead > 0) { + this.source += buffer.toString('utf8', 0, bytesRead); + bytesRead = readSync(fileHandle, buffer, 0, blockSize, this.source.length); + } + closeSync(fileHandle); + return this.parse(); + } + + parseString(source: string): Grammar { + this.source = source; + this.parse(); + this.grammar.processModels(); + return this.grammar; + } + + parse(): Grammar { + this.pointer = 0; + while (this.pointer < this.source.length) { + if (this.lookingAt('', this.pointer); + if (index === -1) { + throw new Error('Malformed element declaration'); + } + let elementText: string = this.source.substring(this.pointer, index + '>'.length); + let length = elementText.length; + let elementDecl: ElementDecl = this.parseElementDeclaration(elementText); + this.grammar.addElement(elementDecl); + this.pointer += length; + continue; + } + if (this.lookingAt('', this.pointer); + if (index === -1) { + throw new Error('Malformed attribute declaration'); + } + let attListText: string = this.source.substring(this.pointer, index + '>'.length); + let length = attListText.length; + let attList: AttListDecl = this.parseAttributesListDeclaration(attListText); + this.grammar.addAttributesList(attList); + this.pointer += length; + continue; + } + if (this.lookingAt('', this.pointer); + if (index === -1) { + throw new Error('Malformed entity declaration'); + } + let entityDeclText: string = this.source.substring(this.pointer, index + '>'.length); + let entityDecl: EntityDecl = this.parseEntityDeclaration(entityDeclText); + this.grammar.addEntity(entityDecl); + this.pointer += entityDeclText.length; + continue; + } + if (this.lookingAt('', this.pointer); + if (index === -1) { + throw new Error('Malformed notation declaration'); + } + let notationDeclText: string = this.source.substring(this.pointer, index + '>'.length); + if (XMLUtils.hasParameterEntity(notationDeclText)) { + notationDeclText = this.resolveEntities(notationDeclText); + } + let notation: NotationDecl = this.parseNotationDeclaration(notationDeclText); + this.grammar.addNotation(notation); + this.pointer += notationDeclText.length; + continue; + } + if (this.lookingAt('')) { + this.endConditionalSection(); + continue; + } + if (this.lookingAt('', this.pointer); + if (index === -1) { + throw new Error('Malformed processing instruction'); + } + // skip processing instructions + this.pointer = index + '?>'.length; + continue; + } + if (this.lookingAt('', this.pointer); + if (index === -1) { + throw new Error('Malformed comment'); + } + // skip comments + this.pointer = index + '-->'.length; + continue; + } + if (this.lookingAt('%')) { + let index: number = this.source.indexOf(';', this.pointer); + if (index == -1) { + throw new Error('Malformed entity reference'); + } + let entityName: string = this.source.substring(this.pointer + '%'.length, index); + let entity: EntityDecl = this.grammar.getEntity(entityName); + if (entity === undefined) { + throw new Error('Unknown entity: ' + entityName); + } + let value: string = entity.getValue(); + if (value !== '') { + let start: string = this.source.substring(0, this.pointer); + let end: string = this.source.substring(index + ';'.length); + this.source = start + value + end; + this.pointer += value.length; + } else if (entity.getSystemId() !== '' || entity.getPublicId() !== '') { + let location = this.resolveEntity(entity.getPublicId(), entity.getSystemId()); + let parser: DTDParser = new DTDParser(this.grammar); + parser.setCatalog(this.catalog); + let externalGrammar: Grammar = parser.parseFile(location); + this.grammar.merge(externalGrammar); + this.pointer = index + ';'.length; + } else { + // empty entity, ignore + this.pointer = index + ';'.length; + } + continue; + } + let char: string = this.source.charAt(this.pointer); + if (XMLUtils.isXmlSpace(char)) { + this.pointer++; + continue; + } + throw new Error('Error parsing ' + this.currentFile + ' at ' + this.source.substring(this.pointer - 10, this.pointer) + ' @ ' + this.source.substring(this.pointer, this.pointer + 30)); + } + return this.grammar; + } + + endConditionalSection() { + // jump over ]]> + this.pointer += ']]>'.length; + } + + parseConditionalSection() { + this.pointer += '')) { + stack--; + this.pointer += ']]>'.length; + if (stack === 0) { + return; + } + } else { + this.pointer++; + } + } + } + + resolveEntities(fragment: string): string { + while (XMLUtils.hasParameterEntity(fragment)) { + let start = fragment.indexOf('%'); + let end = fragment.indexOf(';'); + let entityName = fragment.substring(start + '%'.length, end); + let entity: EntityDecl = this.grammar.getEntity(entityName); + if (entity === undefined) { + throw new Error('Unknown entity: ' + entityName); + } + fragment = fragment.replace('%' + entityName + ';', entity.getValue()); + } + return fragment; + } + + parseEntityDeclaration(declaration: string): EntityDecl { + let name: string = ''; + let i: number = '') { + break; + } + attributesText += char; + } + let list: AttListDecl = new AttListDecl(name, attributesText) + return list; + } + + parseElementDeclaration(declaration: string): ElementDecl { + let name: string = ''; + let i: number = '') { + break; + } + contentSpec += char; + } + return new ElementDecl(name, contentSpec); + } + + lookingAt(text: string): boolean { + let length: number = text.length; + if (this.pointer + length > this.source.length) { + return false; + } + for (let i = 0; i < length; i++) { + if (this.source[this.pointer + i] !== text[i]) { + return false; + } + } + return true; + } + + resolveEntity(publicId: string, systemId: string): string { + let location: string = this.catalog.resolveEntity(publicId, systemId); + if (!location && systemId !== '' && !systemId.startsWith('http')) { + location = this.makeAbsolute(systemId); + } + if (location) { + return location; + } + if (systemId.startsWith('http')) { + return systemId; + } + throw new Error('Entity not found: "' + publicId + '" "' + systemId + '"'); + } + + makeAbsolute(uri: string): string { + let currentPath: string = path.dirname(this.currentFile); + return currentPath + path.sep + uri; + } + + getGrammar(): Grammar { + return this.grammar; + } +} \ No newline at end of file diff --git a/ts/dtd/ElementDecl.ts b/ts/dtd/ElementDecl.ts new file mode 100644 index 0000000..d7fcf52 --- /dev/null +++ b/ts/dtd/ElementDecl.ts @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2023 - 2024 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 { Constants } from "../Constants"; +import { XMLNode } from "../XMLNode"; + +export class ElementDecl implements XMLNode { + + private name: string; + contentSpec: string; + + constructor(name: string, contentSpec: string) { + this.name = name; + this.contentSpec = contentSpec; + } + + getName(): string { + return this.name; + } + + getContentSpec(): string { + return this.contentSpec; + } + + getNodeType(): number { + return Constants.ELEMENT_DECL_NODE; + } + + toString(): string { + return ''; + } + + equals(node: XMLNode): boolean { + // TODO + throw new Error("Method not implemented."); + } +} \ No newline at end of file diff --git a/ts/dtd/EntityDecl.ts b/ts/dtd/EntityDecl.ts new file mode 100644 index 0000000..422e1f0 --- /dev/null +++ b/ts/dtd/EntityDecl.ts @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2023 - 2024 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 { Constants } from "../Constants"; +import { XMLNode } from "../XMLNode"; + +export class EntityDecl implements XMLNode { + + private name: string; + private parameterEntity: boolean; + private value: string; systemId: string; + private publicId: string; + private ndata: string; + + constructor(name: string, parameterEntity: boolean, value: string, systemId: string, publicId: string, ndata: string) { + // parameterEntities are only used in DTDs + this.name = name; + this.parameterEntity = parameterEntity; + this.value = value; + this.systemId = systemId; + this.publicId = publicId; + this.ndata = ndata; + } + + getName(): string { + return this.name; + } + + getValue(): string { + return this.value; + } + + getSystemId(): string { + return this.systemId; + } + + getPublicId(): string { + return this.publicId; + } + + getNodeType(): number { + return Constants.ENTITY_DECL_NODE; + } + + toString(): string { + let result = ''; + } else if (this.systemId !== '') { + result += ' SYSTEM "' + this.systemId + '">'; + } else { + result += ' "' + this.value + '">'; + } + return result; + } + + equals(node: XMLNode): boolean { + // TODO + return false; + } +} \ No newline at end of file diff --git a/ts/dtd/InternalSubset.ts b/ts/dtd/InternalSubset.ts new file mode 100644 index 0000000..de07cf0 --- /dev/null +++ b/ts/dtd/InternalSubset.ts @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2023 - 2024 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 { Grammar } from "../grammar/Grammar"; +import { Constants } from "../Constants"; +import { XMLNode } from "../XMLNode"; +import { DTDParser } from "./DTDParser"; + +export class InternalSubset implements XMLNode { + + declarationText: string; + grammar: Grammar; + + constructor(declaration: string) { + this.declarationText = declaration; + let parser:DTDParser = new DTDParser(); + this.grammar = parser.parseString(declaration.substring(1, declaration.length - 1)); + } + + getNodeType(): number { + return Constants.INTERNAL_SUBSET_NODE; + } + + toString(): string { + return this.declarationText; + } + + equals(node: XMLNode): boolean { + // TODO Auto-generated method stub + return false; + } +} \ No newline at end of file diff --git a/ts/dtd/NotationDecl.ts b/ts/dtd/NotationDecl.ts new file mode 100644 index 0000000..ec134df --- /dev/null +++ b/ts/dtd/NotationDecl.ts @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2023 - 2024 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 { Constants } from "../Constants"; +import { XMLNode } from "../XMLNode"; + +export class NotationDecl implements XMLNode { + + private name: string; + private publicId: string; + private systemId: string; + + constructor(name: string, publicId: string, systemId: string) { + this.name = name; + this.publicId = publicId; + this.systemId = systemId; + } + + getName():string { + return this.name; + } + + getNodeType(): number { + return Constants.NOTATION_DECL_NODE; + } + + toString(): string { + // TODO + throw new Error("Method not implemented."); + } + + equals(node: XMLNode): boolean { + // TODO + throw new Error("Method not implemented."); + } + +} \ No newline at end of file diff --git a/ts/grammar/ContentModel.ts b/ts/grammar/ContentModel.ts new file mode 100644 index 0000000..81f0e45 --- /dev/null +++ b/ts/grammar/ContentModel.ts @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2023 - 2024 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 { AttDecl } from "../dtd/AttDecl"; + +export class ContentModel { + + private name: string; + private attributes: Map; + + constructor(name: string, contentSpec: string) { + this.name = name; + } + + addAttributes(attributes: Map) { + this.attributes = attributes; + } + + getAttributes(): Map { + return this.attributes; + } + + toString(): string { + return this.name; + } +} \ No newline at end of file diff --git a/ts/grammar/Grammar.ts b/ts/grammar/Grammar.ts new file mode 100644 index 0000000..3ad7e1a --- /dev/null +++ b/ts/grammar/Grammar.ts @@ -0,0 +1,165 @@ +/******************************************************************************* + * Copyright (c) 2023 - 2024 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 { XMLUtils } from "../XMLUtils"; +import { AttListDecl } from "../dtd/AttListDecl"; +import { ElementDecl } from "../dtd/ElementDecl"; +import { EntityDecl } from "../dtd/EntityDecl"; +import { NotationDecl } from "../dtd/NotationDecl"; +import { ContentModel } from "./ContentModel"; + +export class Grammar { + + private models: Map; + + private entitiesMap: Map; + private attributeListMap: Map; + private elementDeclMap: Map; + private notationsMap: Map; + + constructor() { + this.models = new Map(); + this.elementDeclMap = new Map(); + this.attributeListMap = new Map(); + this.entitiesMap = new Map(); + this.notationsMap = new Map(); + this.addPredefinedEntities(); + } + + addPredefinedEntities() { + this.addEntity(new EntityDecl('lt', false, '<', '', '', '')); + this.addEntity(new EntityDecl('gt', false, '>', '', '', '')); + this.addEntity(new EntityDecl('amp', false, '&', '', '', '')); + this.addEntity(new EntityDecl('apos', false, "'", '', '', '')); + this.addEntity(new EntityDecl('quot', false, '"', '', '', '')); + } + + getContentModel(elementName: string): ContentModel { + return this.models.get(elementName); + } + + toString(): string { + let result: string; + this.models.forEach((value: ContentModel) => { + result = result + value.toString() + '\n'; + }); + return result; + } + + addElement(elementDecl: ElementDecl) { + if (!this.elementDeclMap.has(elementDecl.getName())) { + this.elementDeclMap.set(elementDecl.getName(), elementDecl); + } + } + + resolveParameterEntities(text: string): string { + while (XMLUtils.hasParameterEntity(text)) { + let start = text.indexOf('%'); + let end = text.indexOf(';'); + let entityName = text.substring(start + '%'.length, end); + let entity: EntityDecl = this.getEntity(entityName); + if (entity === undefined) { + throw new Error('Unknown entity: ' + entityName); + } + text = text.replace('%' + entityName + ';', entity.getValue()); + } + return text; + } + + addAttributesList(attList: AttListDecl) { + if (!this.attributeListMap.has(attList.getName())) { + this.attributeListMap.set(attList.getName(), attList); + } + } + + addEntity(entityDecl: EntityDecl) { + if (!this.entitiesMap.has(entityDecl.getName())) { + this.entitiesMap.set(entityDecl.getName(), entityDecl); + } + } + + getEntity(entityName: string): EntityDecl { + return this.entitiesMap.get(entityName); + } + + addNotation(notation: NotationDecl) { + if (!this.notationsMap.has(notation.getName())) { + this.notationsMap.set(notation.getName(), notation); + } + } + + merge(grammar: Grammar): void { + grammar.getEntitiesMap().forEach((value: EntityDecl, key: string) => { + if (!this.entitiesMap.has(key)) { + this.entitiesMap.set(key, value); + } + }); + grammar.getAttributeListMap().forEach((value: AttListDecl, key: string) => { + if (!this.attributeListMap.has(key)) { + this.attributeListMap.set(key, value); + } + }); + grammar.getElementDeclMap().forEach((value: ElementDecl, key: string) => { + if (!this.elementDeclMap.has(key)) { + this.elementDeclMap.set(key, value); + } + }); + grammar.getNotationsMap().forEach((value: NotationDecl, key: string) => { + if (!this.notationsMap.has(key)) { + this.notationsMap.set(key, value); + } + }); + } + + getNotationsMap(): Map { + return this.notationsMap; + } + + getElementDeclMap(): Map { + return this.elementDeclMap; + } + + getAttributeListMap(): Map { + return this.attributeListMap; + } + + getEntitiesMap(): Map { + return this.entitiesMap + } + + processModels() { + this.elementDeclMap.forEach((elementDecl: ElementDecl) => { + let name: string = elementDecl.getName(); + if (XMLUtils.hasParameterEntity(name)) { + name = this.resolveParameterEntities(name); + } + let contentSpec: string = elementDecl.getContentSpec(); + if (XMLUtils.hasParameterEntity(contentSpec)) { + contentSpec = this.resolveParameterEntities(contentSpec); + } + let model: ContentModel = new ContentModel(name, contentSpec); + this.models.set(name, model); + }); + this.attributeListMap.forEach((attList: AttListDecl) => { + let name: string = attList.getName(); + if (XMLUtils.hasParameterEntity(name)) { + name = this.resolveParameterEntities(name); + } + let model: ContentModel = this.models.get(name); + if (model) { + model.addAttributes(attList.getAttributes()); + } + this.models.set(name, model); + }); + } + +} \ No newline at end of file diff --git a/ts/index.ts b/ts/index.ts index c698051..6d5c830 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Maxprograms. + * Copyright (c) 2023 - 2024 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse License 1.0 @@ -10,8 +10,8 @@ * Maxprograms - initial API and implementation *******************************************************************************/ -export { Catalog } from "./Catalog"; export { CData } from "./CData"; +export { Catalog } from "./Catalog"; export { Constants } from "./Constants"; export { ContentHandler } from "./ContentHandler"; export { DOMBuilder } from "./DOMBuilder"; @@ -19,6 +19,7 @@ export { FileReader } from "./FileReader"; export { Indenter } from "./Indenter"; export { ProcessingInstruction } from "./ProcessingInstruction"; export { SAXParser } from "./SAXParser"; +export { StringReader } from "./StringReader"; export { TextNode } from "./TextNode"; export { XMLAttribute } from "./XMLAttribute"; export { XMLComment } from "./XMLComment"; @@ -27,5 +28,17 @@ export { XMLDocument } from "./XMLDocument"; export { XMLDocumentType } from "./XMLDocumentType"; export { XMLElement } from "./XMLElement"; export { XMLNode } from "./XMLNode"; +export { XMLReader } from "./XMLReader"; export { XMLUtils } from "./XMLUtils"; -export { XMLWriter } from "./XMLWriter"; \ No newline at end of file +export { XMLWriter } from "./XMLWriter"; + +export { AttDecl } from "./dtd/AttDecl"; +export { AttListDecl } from "./dtd/AttListDecl"; +export { DTDParser } from "./dtd/DTDParser"; +export { ElementDecl } from "./dtd/ElementDecl"; +export { EntityDecl } from "./dtd/EntityDecl"; +export { InternalSubset } from "./dtd/InternalSubset"; +export { NotationDecl } from "./dtd/NotationDecl"; + +export { ContentModel } from "./grammar/ContentModel"; +export { Grammar } from "./grammar/Grammar"; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index af2b8f2..d38ff25 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "ES6", - "moduleResolution": "node", - "module": "CommonJS", + "target": "esnext", + "module": "NodeNext", + "moduleResolution": "NodeNext", "noImplicitAny": true, "allowUnreachableCode": false, "noImplicitThis": true,