Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

fast-xml-parser

Package Overview
Dependencies
Maintainers
1
Versions
169
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fast-xml-parser - npm Package Compare versions

Comparing version
4.5.3
to
4.5.4
+21
test_output_2.txt
Error: While loading /home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js: SyntaxError: The requested module './TagPath.js' does not provide an export named 'TagPath'
at fixupImportException (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/loader.js:152:12)
at /home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/loader.js:31:37
at async Jasmine._loadFiles (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:142:7)
... 5 lines matching cause stack trace ...
at async Command.run (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/command.js:72:9) {
[cause]: file:///home/amit/code/git/temp/fast-xml-parser/src/v6/TagPathMatcher.js:1
import {TagPath} from './TagPath.js';
^^^^^^^
SyntaxError: The requested module './TagPath.js' does not provide an export named 'TagPath'
at ModuleJob._instantiate (node:internal/modules/esm/module_job:180:21)
at async ModuleJob.run (node:internal/modules/esm/module_job:263:5)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:578:26)
at async Jasmine._loadFiles (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:142:7)
at async Jasmine.loadSpecs (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:133:5)
at async /home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:223:9
at async Jasmine.withinGlobalSetup_ (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/runner_base.js:420:7)
at async Jasmine.execute (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:222:7)
at async runJasmine (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/command.js:209:5)
at async Command.run (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/command.js:72:9)
}
TypeError: Cannot read properties of undefined (reading 'preserveOrder')
at buildOptions (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/OutputBuilders/ParserOptionsBuilder.js:53:12)
at new OutputBuilder (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/OutputBuilders/JsObjBuilder.js:7:22)
at file:///home/amit/code/git/temp/fast-xml-parser/src/v6/OptionsBuilder.js:35:18
at ModuleJob.run (node:internal/modules/esm/module_job:271:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:578:26)
at async Jasmine._loadFiles (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:142:7)
at async Jasmine.loadSpecs (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:133:5)
at async /home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:223:9
at async Jasmine.withinGlobalSetup_ (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/runner_base.js:420:7)
at async Jasmine.execute (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:222:7)
Randomized with seed 20147
Started
..F.F
Failures:
1) XMLParser v6 should parse only true numbers
Message:
Expected $.rootNode.intTag = 45 to equal '045'.
Stack:
at <Jasmine>
at UserContext.<anonymous> (file:///home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js:63:20)
at <Jasmine>
2) XMLParser v6 should not parse values to primitive type
Message:
Expected $.rootNode.boolean = true to equal 'true'.
Expected $.rootNode.intTag = 45 to equal '045'.
Expected $.rootNode.floatTag = 65.34 to equal '65.34'.
Stack:
at <Jasmine>
at UserContext.<anonymous> (file:///home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js:120:20)
at <Jasmine>
5 specs, 2 failures
Finished in 0.016 seconds
Randomized with seed 20147 (jasmine --random=true --seed=20147)
Randomized with seed 74872
Started
..F..
Failures:
1) XMLParser v6 should parse all values as string, int, boolean, float, hexadecimal
Message:
Expected $.rootNode.boolean = 'true' to equal true.
Expected $.rootNode.intTag = '045' to equal 45.
Stack:
at <Jasmine>
at UserContext.<anonymous> (file:///home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js:28:20)
at <Jasmine>
5 specs, 1 failure
Finished in 0.014 seconds
Randomized with seed 74872 (jasmine --random=true --seed=74872)
Randomized with seed 51104
Started
TagName: tag ValueParsers: [
'trim', 'join',
'number', 'boolean',
'currency', 'trim',
'number', 'boolean',
'currency'
]
TagName: boolean ValueParsers: [
'trim', 'join',
'number', 'boolean',
'currency', 'trim',
'number', 'boolean',
'currency'
]
TagName: intTag ValueParsers: [
'trim', 'join',
'number', 'boolean',
'currency', 'trim',
'number', 'boolean',
'currency'
]
TagName: floatTag ValueParsers: [
'trim', 'join',
'number', 'boolean',
'currency', 'trim',
'number', 'boolean',
'currency'
]
TagName: hexadecimal ValueParsers: [
'trim', 'join',
'number', 'boolean',
'currency', 'trim',
'number', 'boolean',
'currency'
]
TagName: rootNode ValueParsers: [
'trim', 'join',
'number', 'boolean',
'currency', 'trim',
'number', 'boolean',
'currency'
]
.TagName: tag ValueParsers: []
TagName: boolean ValueParsers: []
TagName: intTag ValueParsers: []
TagName: floatTag ValueParsers: []
TagName: rootNode ValueParsers: []
.TagName: tag ValueParsers: []
TagName: rootNode ValueParsers: []
.TagName: floatTag0 ValueParsers: [ numParser { options: { leadingZeros: false } } ]
TagName: floatTag1 ValueParsers: [ numParser { options: { leadingZeros: false } } ]
TagName: floatTag2 ValueParsers: [ numParser { options: { leadingZeros: false } } ]
TagName: floatTag3 ValueParsers: [ numParser { options: { leadingZeros: false } } ]
TagName: rootNode ValueParsers: [ numParser { options: { leadingZeros: false } } ]
.TagName: tag ValueParsers: [
'boolean',
numParser {
options: { hex: true, leadingZeros: false, eNotation: true }
}
]
TagName: boolean ValueParsers: [
'boolean',
numParser {
options: { hex: true, leadingZeros: false, eNotation: true }
}
]
TagName: intTag ValueParsers: [
'boolean',
numParser {
options: { hex: true, leadingZeros: false, eNotation: true }
}
]
TagName: floatTag ValueParsers: [
'boolean',
numParser {
options: { hex: true, leadingZeros: false, eNotation: true }
}
]
TagName: long ValueParsers: [
'boolean',
numParser {
options: { hex: true, leadingZeros: false, eNotation: true }
}
]
TagName: rootNode ValueParsers: [
'boolean',
numParser {
options: { hex: true, leadingZeros: false, eNotation: true }
}
]
.
5 specs, 0 failures
Finished in 0.015 seconds
Randomized with seed 51104 (jasmine --random=true --seed=51104)
Randomized with seed 51667
Started
......FFF.F
Failures:
1) XMLParser v6 should skip tag arguments
Message:
Expected $.rootNode.intTag = '45' to equal 45.
Expected $.rootNode.floatTag = '65.34' to equal 65.34.
Stack:
at <Jasmine>
at UserContext.<anonymous> (file:///home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js:165:20)
at <Jasmine>
2) XMLParser v6 should ignore namespace and text node attributes
Message:
Expected object to have properties
node: Object({ tag: Object({ @_arg: 'value', #text: 'value' }), intTag: Object({ @_arg: 'value', @_arg2: 'value2', #text: 45 }), floatTag: 65.34, nsTag: Object({ @_attr: 'tns' }), nsTagNoAttr: '' })
Expected object not to have properties
root:node: Object({ tag: Object({ @_ns:arg: 'value', #text: 'value' }), intTag: Object({ @_ns:arg: 'value', @_ns:arg2: 'value2', #text: '45' }), floatTag: '65.34', nsTag: Object({ @_xmlns:tns-ns: 'urn:none', @_tns-ns:attr: 'tns' }), nsTagNoAttr: Object({ @_xmlns:tns-ns: 'urn:none' }) })
Stack:
at <Jasmine>
at UserContext.<anonymous> (file:///home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js:207:20)
at <Jasmine>
3) XMLParser v6 should parse all values as string, int, boolean, float, hexadecimal
Message:
Expected $.rootNode.boolean = 'true' to equal true.
Expected $.rootNode.intTag = '045' to equal 45.
Expected $.rootNode.floatTag = '65.34' to equal 65.34.
Expected $.rootNode.hexadecimal = '0x15' to equal 21.
Stack:
at <Jasmine>
at UserContext.<anonymous> (file:///home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js:28:20)
at <Jasmine>
4) XMLParser v6 should parse repeated nodes in array
Message:
Expected $.rootNode.tag[1] = '45' to equal 45.
Expected $.rootNode.tag[2] = '65.34' to equal 65.34.
Stack:
at <Jasmine>
at UserContext.<anonymous> (file:///home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js:259:20)
at <Jasmine>
11 specs, 4 failures
Finished in 0.023 seconds
Randomized with seed 51667 (jasmine --random=true --seed=51667)
Randomized with seed 18744
Started
.....FF....
Failures:
1) XMLParser v6 should parse nested nodes in nested properties
Message:
Expected $.rootNode.parenttag.tag[1] = '45' to equal 45.
Expected $.rootNode.parenttag.tag[2] = '65.34' to equal 65.34.
Stack:
at <Jasmine>
at UserContext.<anonymous> (file:///home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js:281:20)
at <Jasmine>
2) XMLParser v6 should ignore namespace and text node attributes
Message:
ReferenceError: reportError is not defined
Stack:
at resolveNameSpace (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/Xml2JsParser.js:232:12)
at Xml2JsParser.processTagName (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/Xml2JsParser.js:185:12)
at Xml2JsParser.readClosingTag (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/Xml2JsParser.js:87:26)
at Xml2JsParser.parseXml (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/Xml2JsParser.js:69:16)
at Xml2JsParser.parse (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/Xml2JsParser.js:35:10)
at XMLParser.parse (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/XMLParser.js:34:23)
at UserContext.<anonymous> (file:///home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js:205:25)
at <Jasmine>
11 specs, 2 failures
Finished in 0.02 seconds
Randomized with seed 18744 (jasmine --random=true --seed=18744)
Randomized with seed 42237
Started
.F.....FF..
Failures:
1) XMLParser v6 should ignore namespace and text node attributes
Message:
Error: Multiple namespaces tag
Stack:
at resolveNameSpace (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/Xml2JsParser.js:232:18)
at Xml2JsParser.processTagName (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/Xml2JsParser.js:185:12)
at Xml2JsParser.readClosingTag (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/Xml2JsParser.js:87:26)
at Xml2JsParser.parseXml (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/Xml2JsParser.js:69:16)
at Xml2JsParser.parse (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/Xml2JsParser.js:35:10)
at XMLParser.parse (file:///home/amit/code/git/temp/fast-xml-parser/src/v6/XMLParser.js:34:23)
at UserContext.<anonymous> (file:///home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js:205:25)
at <Jasmine>
2) XMLParser v6 should parse all values as string, int, boolean, float, hexadecimal
Message:
Expected $.rootNode.boolean = 'true' to equal true.
Expected $.rootNode.intTag = '045' to equal 45.
Expected $.rootNode.floatTag = '65.34' to equal 65.34.
Expected $.rootNode.hexadecimal = '0x15' to equal 21.
Stack:
at <Jasmine>
at UserContext.<anonymous> (file:///home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js:28:20)
at <Jasmine>
3) XMLParser v6 should parse nested nodes in nested properties
Message:
Expected $.rootNode.parenttag.tag[1] = '45' to equal 45.
Expected $.rootNode.parenttag.tag[2] = '65.34' to equal 65.34.
Stack:
at <Jasmine>
at UserContext.<anonymous> (file:///home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js:281:20)
at <Jasmine>
11 specs, 3 failures
Finished in 0.02 seconds
Randomized with seed 42237 (jasmine --random=true --seed=42237)
Error: While loading /home/amit/code/git/temp/fast-xml-parser/spec/v6/xmlParser_spec.js: SyntaxError: The requested module './OutputBuilders/JsObjBuilder.js' does not provide an export named 'JsObjOutputBuilder'
at fixupImportException (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/loader.js:152:12)
at /home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/loader.js:31:37
at async Jasmine._loadFiles (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:142:7)
... 5 lines matching cause stack trace ...
at async Command.run (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/command.js:72:9) {
[cause]: file:///home/amit/code/git/temp/fast-xml-parser/src/v6/OptionsBuilder.js:2
import {JsObjOutputBuilder} from './OutputBuilders/JsObjBuilder.js';
^^^^^^^^^^^^^^^^^^
SyntaxError: The requested module './OutputBuilders/JsObjBuilder.js' does not provide an export named 'JsObjOutputBuilder'
at ModuleJob._instantiate (node:internal/modules/esm/module_job:180:21)
at async ModuleJob.run (node:internal/modules/esm/module_job:263:5)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:578:26)
at async Jasmine._loadFiles (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:142:7)
at async Jasmine.loadSpecs (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:133:5)
at async /home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:223:9
at async Jasmine.withinGlobalSetup_ (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/runner_base.js:420:7)
at async Jasmine.execute (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/jasmine.js:222:7)
at async runJasmine (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/command.js:209:5)
at async Command.run (/home/amit/code/git/temp/fast-xml-parser/node_modules/jasmine/lib/command.js:72:9)
}
+17
-0
<small>Note: If you find missing information about particular minor version, that version must have been changed without any functional change in this library.</small>
**4.5.4 / 2026-02-26**
- support strictReservedNames
- support captureMetaData
- support maxNestedTags
- handle non-array input for XML builder when preserveOrder is true (By Angelo Coetzee)
- Improve security and performance of entity processing
- new options maxEntitySize, maxExpansionDepth, maxTotalExpansions, maxExpandedLength, allowedTags,tagFilter
- fast return when no edtity is present
- improvement replacement logic to reduce number of calls
- fix: Escape regex char in entity name
- fix: handle HTML numeric and hex entities when out of range
- fix #775: transformTagName with allowBooleanAttributes adds an unnecessary attribute
- Use Uint8Array in place of Buffer in Parser
- Support EMPTY and ANY with ELEMENT in DOCTYPE
- fix: support numeric entities with values over 0xFFFF (#726) (By Marc Durdin)
**4.5.2 / 2025-02-18**

@@ -4,0 +21,0 @@ - Fix null CDATA to comply with undefined behavior (#701) (By [Matthieu BOHEAS](https://github.com/Kelgors))

+5
-6
{
"name": "fast-xml-parser",
"version": "4.5.3",
"version": "4.5.4",
"description": "Validate XML, Parse XML, Build XML without C/C++ based libraries",
"main": "./src/fxp.js",
"scripts": {
"test": "nyc --reporter=lcov --reporter=text jasmine spec/*spec.js",
"test": "c8 --reporter=lcov --reporter=text jasmine spec/*spec.js",
"test-types": "tsc --noEmit spec/typings/typings-test.ts",
"unit": "jasmine",
"coverage": "nyc report --reporter html --reporter text -t .nyc_output --report-dir .nyc_output/summary",
"perf": "node ./benchmark/perfTest3.js",

@@ -52,6 +51,6 @@ "lint": "eslint src/*.js spec/*.js",

"babel-loader": "^8.2.2",
"c8": "^10.1.3",
"eslint": "^8.3.0",
"he": "^1.2.0",
"jasmine": "^3.6.4",
"nyc": "^15.1.0",
"prettier": "^1.19.1",

@@ -71,4 +70,4 @@ "publish-please": "^5.5.2",

"dependencies": {
"strnum": "^1.1.1"
"strnum": "^1.0.5"
}
}
}

@@ -1,3 +0,59 @@

type X2jOptions = {
export type ProcessEntitiesOptions = {
/**
* Whether to enable entity processing
*
* Defaults to `true`
*/
enabled?: boolean;
/**
* Maximum size in characters for a single entity definition
*
* Defaults to `10000`
*/
maxEntitySize?: number;
/**
* Maximum depth for nested entity references (reserved for future use)
*
* Defaults to `10`
*/
maxExpansionDepth?: number;
/**
* Maximum total number of entity expansions allowed
*
* Defaults to `1000`
*/
maxTotalExpansions?: number;
/**
* Maximum total expanded content length in characters
*
* Defaults to `100000`
*/
maxExpandedLength?: number;
/**
* Array of tag names where entity replacement is allowed.
* If null, entities are replaced in all tags.
*
* Defaults to `null`
*/
allowedTags?: string[] | null;
/**
* Custom filter function to determine if entities should be replaced in a tag
*
* @param tagName - The name of the current tag
* @param jPath - The jPath of the current tag
* @returns `true` to allow entity replacement, `false` to skip
*
* Defaults to `null`
*/
tagFilter?: ((tagName: string, jPath: string) => boolean) | null;
};
export type X2jOptions = {
/**
* Preserve the order of tags in resulting JS object

@@ -13,3 +69,3 @@ *

* Defaults to '@_'
*/
*/
attributeNamePrefix?: string;

@@ -68,3 +124,3 @@

/**
* Whether to parse tag value with `strnum` package
* Whether to parse attribute value with `strnum` package
*

@@ -166,5 +222,11 @@ * Defaults to `false`

*
* When `true` - enables entity processing with default limits
*
* When `false` - disables all entity processing
*
* When `ProcessEntitiesOptions` - enables entity processing with custom configuration
*
* Defaults to `true`
*/
processEntities?: boolean;
processEntities?: boolean | ProcessEntitiesOptions;

@@ -215,6 +277,28 @@ /**

*/
updateTag?: (tagName: string, jPath: string, attrs: {[k: string]: string}) => string | boolean;
updateTag?: (tagName: string, jPath: string, attrs: { [k: string]: string }) => string | boolean;
/**
* If true, adds a Symbol to all object nodes, accessible by {@link XMLParser.getMetaDataSymbol} with
* metadata about each the node in the XML file.
*/
captureMetaData?: boolean;
/**
* Maximum number of nested tags
*
* Defaults to `100`
*/
maxNestedTags?: number;
/**
* Whether to strictly validate tag names
*
* Defaults to `true`
*/
strictReservedNames?: boolean;
};
type strnumOptions = {
export type strnumOptions = {
hex: boolean;

@@ -226,3 +310,3 @@ leadingZeros: boolean,

type validationOptions = {
export type validationOptions = {
/**

@@ -234,3 +318,3 @@ * Whether to allow attributes without value

allowBooleanAttributes?: boolean;
/**

@@ -244,3 +328,3 @@ * List of tags without closing tags

type XmlBuilderOptions = {
export type XmlBuilderOptions = {
/**

@@ -250,3 +334,3 @@ * Give a prefix to the attribute name in the resulting JS object

* Defaults to '@_'
*/
*/
attributeNamePrefix?: string;

@@ -398,10 +482,10 @@

type ESchema = string | object | Array<string|object>;
type ESchema = string | object | Array<string | object>;
type ValidationError = {
err: {
export type ValidationError = {
err: {
code: string;
msg: string,
line: number,
col: number
col: number
};

@@ -412,3 +496,3 @@ };

constructor(options?: X2jOptions);
parse(xmlData: string | Buffer ,validationOptions?: validationOptions | boolean): any;
parse(xmlData: string | Uint8Array, validationOptions?: validationOptions | boolean): any;
/**

@@ -422,8 +506,8 @@ * Add Entity which is not by default supported by this library

export class XMLValidator{
static validate( xmlData: string, options?: validationOptions): true | ValidationError;
export class XMLValidator {
static validate(xmlData: string, options?: validationOptions): true | ValidationError;
}
export class XMLBuilder {
constructor(options?: XmlBuilderOptions);
build(jObj: any): any;
build(jObj: any): string;
}

@@ -21,6 +21,17 @@ const EOL = "\n";

if (!Array.isArray(arr)) {
// Non-array values (e.g. string tag values) should be treated as text content
if (arr !== undefined && arr !== null) {
let text = arr.toString();
text = replaceEntitiesValue(text, options);
return text;
}
return "";
}
for (let i = 0; i < arr.length; i++) {
const tagObj = arr[i];
const tagName = propName(tagObj);
if(tagName === undefined) continue;
if (tagName === undefined) continue;

@@ -96,3 +107,3 @@ let newJPath = "";

const key = keys[i];
if(!obj.hasOwnProperty(key)) continue;
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
if (key !== ":@") return key;

@@ -106,3 +117,3 @@ }

for (let attr in attrMap) {
if(!attrMap.hasOwnProperty(attr)) continue;
if (!Object.prototype.hasOwnProperty.call(attrMap, attr)) continue;
let attrVal = options.attributeValueProcessor(attr, attrMap[attr]);

@@ -109,0 +120,0 @@ attrVal = replaceEntitiesValue(attrVal, options);

const util = require('../util');
//TODO: handle comments
function readDocType(xmlData, i){
const entities = {};
if( xmlData[i + 3] === 'O' &&
xmlData[i + 4] === 'C' &&
xmlData[i + 5] === 'T' &&
xmlData[i + 6] === 'Y' &&
xmlData[i + 7] === 'P' &&
xmlData[i + 8] === 'E')
{
i = i+9;
let angleBracketsCount = 1;
let hasBody = false, comment = false;
let exp = "";
for(;i<xmlData.length;i++){
if (xmlData[i] === '<' && !comment) { //Determine the tag type
if( hasBody && isEntity(xmlData, i)){
i += 7;
let entityName, val;
[entityName, val,i] = readEntityExp(xmlData,i+1);
if(val.indexOf("&") === -1) //Parameter entities are not supported
entities[ validateEntityName(entityName) ] = {
regx : RegExp( `&${entityName};`,"g"),
val: val
};
}
else if( hasBody && isElement(xmlData, i)) i += 8;//Not supported
else if( hasBody && isAttlist(xmlData, i)) i += 8;//Not supported
else if( hasBody && isNotation(xmlData, i)) i += 9;//Not supported
else if( isComment) comment = true;
else throw new Error("Invalid DOCTYPE");
class DocTypeReader {
constructor(options) {
this.suppressValidationErr = !options;
this.options = options || {};
}
angleBracketsCount++;
exp = "";
} else if (xmlData[i] === '>') { //Read tag content
if(comment){
if( xmlData[i - 1] === "-" && xmlData[i - 2] === "-"){
comment = false;
readDocType(xmlData, i) {
const entities = Object.create(null);
if (xmlData[i + 3] === 'O' &&
xmlData[i + 4] === 'C' &&
xmlData[i + 5] === 'T' &&
xmlData[i + 6] === 'Y' &&
xmlData[i + 7] === 'P' &&
xmlData[i + 8] === 'E') {
i = i + 9;
let angleBracketsCount = 1;
let hasBody = false, comment = false;
let exp = "";
for (; i < xmlData.length; i++) {
if (xmlData[i] === '<' && !comment) { //Determine the tag type
if (hasBody && hasSeq(xmlData, "!ENTITY", i)) {
i += 7;
let entityName, val;
[entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr);
if (val.indexOf("&") === -1) { //Parameter entities are not supported
const escaped = entityName.replace(/[.\-+*:]/g, '\\.');
entities[entityName] = {
regx: RegExp(`&${escaped};`, "g"),
val: val
};
}
} else if (hasBody && hasSeq(xmlData, "!ELEMENT", i)) {
i += 8; //Not supported
const { index } = this.readElementExp(xmlData, i + 1);
i = index;
} else if (hasBody && hasSeq(xmlData, "!ATTLIST", i)) {
i += 8; //Not supported
// const {index} = this.readAttlistExp(xmlData,i+1);
// i = index;
} else if (hasBody && hasSeq(xmlData, "!NOTATION", i)) {
i += 9; //Not supported
const { index } = this.readNotationExp(xmlData, i + 1, this.suppressValidationErr);
i = index;
} else if (hasSeq(xmlData, "!--", i)) {
comment = true;
} else {
throw new Error(`Invalid DOCTYPE`);
}
angleBracketsCount++;
exp = "";
} else if (xmlData[i] === '>') { //Read tag content
if (comment) {
if (xmlData[i - 1] === "-" && xmlData[i - 2] === "-") {
comment = false;
angleBracketsCount--;
}
} else {
angleBracketsCount--;
}
}else{
angleBracketsCount--;
if (angleBracketsCount === 0) {
break;
}
} else if (xmlData[i] === '[') {
hasBody = true;
} else {
exp += xmlData[i];
}
if (angleBracketsCount === 0) {
break;
}
}else if( xmlData[i] === '['){
hasBody = true;
}else{
exp += xmlData[i];
}
if (angleBracketsCount !== 0) {
throw new Error(`Unclosed DOCTYPE`);
}
} else {
throw new Error(`Invalid Tag instead of DOCTYPE`);
}
if(angleBracketsCount !== 0){
throw new Error(`Unclosed DOCTYPE`);
return { entities, i };
}
readEntityExp(xmlData, i) {
//External entities are not supported
// <!ENTITY ext SYSTEM "http://normal-website.com" >
//Parameter entities are not supported
// <!ENTITY entityname "&anotherElement;">
//Internal entities are supported
// <!ENTITY entityname "replacement text">
// Skip leading whitespace after <!ENTITY
i = skipWhitespace(xmlData, i);
// Read entity name
let entityName = "";
while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") {
entityName += xmlData[i];
i++;
}
}else{
throw new Error(`Invalid Tag instead of DOCTYPE`);
validateEntityName(entityName);
// Skip whitespace after entity name
i = skipWhitespace(xmlData, i);
// Check for unsupported constructs (external entities or parameter entities)
if (!this.suppressValidationErr) {
if (xmlData.substring(i, i + 6).toUpperCase() === "SYSTEM") {
throw new Error("External entities are not supported");
} else if (xmlData[i] === "%") {
throw new Error("Parameter entities are not supported");
}
}
// Read entity value (internal entity)
let entityValue = "";
[i, entityValue] = this.readIdentifierVal(xmlData, i, "entity");
// Validate entity size
if (this.options.enabled !== false &&
this.options.maxEntitySize &&
entityValue.length > this.options.maxEntitySize) {
throw new Error(
`Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})`
);
}
i--;
return [entityName, entityValue, i];
}
return {entities, i};
}
function readEntityExp(xmlData,i){
//External entities are not supported
// <!ENTITY ext SYSTEM "http://normal-website.com" >
readNotationExp(xmlData, i) {
// Skip leading whitespace after <!NOTATION
i = skipWhitespace(xmlData, i);
//Parameter entities are not supported
// <!ENTITY entityname "&anotherElement;">
// Read notation name
let notationName = "";
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
notationName += xmlData[i];
i++;
}
!this.suppressValidationErr && validateEntityName(notationName);
//Internal entities are supported
// <!ENTITY entityname "replacement text">
//read EntityName
let entityName = "";
for (; i < xmlData.length && (xmlData[i] !== "'" && xmlData[i] !== '"' ); i++) {
// if(xmlData[i] === " ") continue;
// else
entityName += xmlData[i];
// Skip whitespace after notation name
i = skipWhitespace(xmlData, i);
// Check identifier type (SYSTEM or PUBLIC)
const identifierType = xmlData.substring(i, i + 6).toUpperCase();
if (!this.suppressValidationErr && identifierType !== "SYSTEM" && identifierType !== "PUBLIC") {
throw new Error(`Expected SYSTEM or PUBLIC, found "${identifierType}"`);
}
i += identifierType.length;
// Skip whitespace after identifier type
i = skipWhitespace(xmlData, i);
// Read public identifier (if PUBLIC)
let publicIdentifier = null;
let systemIdentifier = null;
if (identifierType === "PUBLIC") {
[i, publicIdentifier] = this.readIdentifierVal(xmlData, i, "publicIdentifier");
// Skip whitespace after public identifier
i = skipWhitespace(xmlData, i);
// Optionally read system identifier
if (xmlData[i] === '"' || xmlData[i] === "'") {
[i, systemIdentifier] = this.readIdentifierVal(xmlData, i, "systemIdentifier");
}
} else if (identifierType === "SYSTEM") {
// Read system identifier (mandatory for SYSTEM)
[i, systemIdentifier] = this.readIdentifierVal(xmlData, i, "systemIdentifier");
if (!this.suppressValidationErr && !systemIdentifier) {
throw new Error("Missing mandatory system identifier for SYSTEM notation");
}
}
return { notationName, publicIdentifier, systemIdentifier, index: --i };
}
entityName = entityName.trim();
if(entityName.indexOf(" ") !== -1) throw new Error("External entites are not supported");
//read Entity Value
const startChar = xmlData[i++];
let val = ""
for (; i < xmlData.length && xmlData[i] !== startChar ; i++) {
val += xmlData[i];
readIdentifierVal(xmlData, i, type) {
let identifierVal = "";
const startChar = xmlData[i];
if (startChar !== '"' && startChar !== "'") {
throw new Error(`Expected quoted string, found "${startChar}"`);
}
i++;
while (i < xmlData.length && xmlData[i] !== startChar) {
identifierVal += xmlData[i];
i++;
}
if (xmlData[i] !== startChar) {
throw new Error(`Unterminated ${type} value`);
}
i++;
return [i, identifierVal];
}
return [entityName, val, i];
}
function isComment(xmlData, i){
if(xmlData[i+1] === '!' &&
xmlData[i+2] === '-' &&
xmlData[i+3] === '-') return true
return false
readElementExp(xmlData, i) {
// <!ELEMENT br EMPTY>
// <!ELEMENT div ANY>
// <!ELEMENT title (#PCDATA)>
// <!ELEMENT book (title, author+)>
// <!ELEMENT name (content-model)>
// Skip leading whitespace after <!ELEMENT
i = skipWhitespace(xmlData, i);
// Read element name
let elementName = "";
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
elementName += xmlData[i];
i++;
}
// Validate element name
if (!this.suppressValidationErr && !util.isName(elementName)) {
throw new Error(`Invalid element name: "${elementName}"`);
}
// Skip whitespace after element name
i = skipWhitespace(xmlData, i);
let contentModel = "";
// Expect '(' to start content model
if (xmlData[i] === "E" && hasSeq(xmlData, "MPTY", i)) {
i += 4;
} else if (xmlData[i] === "A" && hasSeq(xmlData, "NY", i)) {
i += 2;
} else if (xmlData[i] === "(") {
i++; // Move past '('
// Read content model
while (i < xmlData.length && xmlData[i] !== ")") {
contentModel += xmlData[i];
i++;
}
if (xmlData[i] !== ")") {
throw new Error("Unterminated content model");
}
} else if (!this.suppressValidationErr) {
throw new Error(`Invalid Element Expression, found "${xmlData[i]}"`);
}
return {
elementName,
contentModel: contentModel.trim(),
index: i
};
}
readAttlistExp(xmlData, i) {
// Skip leading whitespace after <!ATTLIST
i = skipWhitespace(xmlData, i);
// Read element name
let elementName = "";
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
elementName += xmlData[i];
i++;
}
// Validate element name
validateEntityName(elementName);
// Skip whitespace after element name
i = skipWhitespace(xmlData, i);
// Read attribute name
let attributeName = "";
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
attributeName += xmlData[i];
i++;
}
// Validate attribute name
if (!validateEntityName(attributeName)) {
throw new Error(`Invalid attribute name: "${attributeName}"`);
}
// Skip whitespace after attribute name
i = skipWhitespace(xmlData, i);
// Read attribute type
let attributeType = "";
if (xmlData.substring(i, i + 8).toUpperCase() === "NOTATION") {
attributeType = "NOTATION";
i += 8; // Move past "NOTATION"
// Skip whitespace after "NOTATION"
i = skipWhitespace(xmlData, i);
// Expect '(' to start the list of notations
if (xmlData[i] !== "(") {
throw new Error(`Expected '(', found "${xmlData[i]}"`);
}
i++; // Move past '('
// Read the list of allowed notations
let allowedNotations = [];
while (i < xmlData.length && xmlData[i] !== ")") {
let notation = "";
while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") {
notation += xmlData[i];
i++;
}
// Validate notation name
notation = notation.trim();
if (!validateEntityName(notation)) {
throw new Error(`Invalid notation name: "${notation}"`);
}
allowedNotations.push(notation);
// Skip '|' separator or exit loop
if (xmlData[i] === "|") {
i++; // Move past '|'
i = skipWhitespace(xmlData, i); // Skip optional whitespace after '|'
}
}
if (xmlData[i] !== ")") {
throw new Error("Unterminated list of notations");
}
i++; // Move past ')'
// Store the allowed notations as part of the attribute type
attributeType += " (" + allowedNotations.join("|") + ")";
} else {
// Handle simple types (e.g., CDATA, ID, IDREF, etc.)
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
attributeType += xmlData[i];
i++;
}
// Validate simple attribute type
const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"];
if (!this.suppressValidationErr && !validTypes.includes(attributeType.toUpperCase())) {
throw new Error(`Invalid attribute type: "${attributeType}"`);
}
}
// Skip whitespace after attribute type
i = skipWhitespace(xmlData, i);
// Read default value
let defaultValue = "";
if (xmlData.substring(i, i + 8).toUpperCase() === "#REQUIRED") {
defaultValue = "#REQUIRED";
i += 8;
} else if (xmlData.substring(i, i + 7).toUpperCase() === "#IMPLIED") {
defaultValue = "#IMPLIED";
i += 7;
} else {
[i, defaultValue] = this.readIdentifierVal(xmlData, i, "ATTLIST");
}
return {
elementName,
attributeName,
attributeType,
defaultValue,
index: i
};
}
}
function isEntity(xmlData, i){
if(xmlData[i+1] === '!' &&
xmlData[i+2] === 'E' &&
xmlData[i+3] === 'N' &&
xmlData[i+4] === 'T' &&
xmlData[i+5] === 'I' &&
xmlData[i+6] === 'T' &&
xmlData[i+7] === 'Y') return true
return false
}
function isElement(xmlData, i){
if(xmlData[i+1] === '!' &&
xmlData[i+2] === 'E' &&
xmlData[i+3] === 'L' &&
xmlData[i+4] === 'E' &&
xmlData[i+5] === 'M' &&
xmlData[i+6] === 'E' &&
xmlData[i+7] === 'N' &&
xmlData[i+8] === 'T') return true
return false
}
function isAttlist(xmlData, i){
if(xmlData[i+1] === '!' &&
xmlData[i+2] === 'A' &&
xmlData[i+3] === 'T' &&
xmlData[i+4] === 'T' &&
xmlData[i+5] === 'L' &&
xmlData[i+6] === 'I' &&
xmlData[i+7] === 'S' &&
xmlData[i+8] === 'T') return true
return false
// Helper functions
const skipWhitespace = (data, index) => {
while (index < data.length && /\s/.test(data[index])) {
index++;
}
return index;
};
function hasSeq(data, seq, i) {
for (let j = 0; j < seq.length; j++) {
if (seq[j] !== data[i + j + 1]) return false;
}
return true;
}
function isNotation(xmlData, i){
if(xmlData[i+1] === '!' &&
xmlData[i+2] === 'N' &&
xmlData[i+3] === 'O' &&
xmlData[i+4] === 'T' &&
xmlData[i+5] === 'A' &&
xmlData[i+6] === 'T' &&
xmlData[i+7] === 'I' &&
xmlData[i+8] === 'O' &&
xmlData[i+9] === 'N') return true
return false
}
function validateEntityName(name){
function validateEntityName(name) {
if (util.isName(name))
return name;
return name;
else

@@ -153,2 +399,2 @@ throw new Error(`Invalid entity name ${name}`);

module.exports = readDocType;
module.exports = DocTypeReader;
const defaultOptions = {
preserveOrder: false,
attributeNamePrefix: '@_',
attributesGroupName: false,
textNodeName: '#text',
ignoreAttributes: true,
removeNSPrefix: false, // remove NS from tag name or attribute name if true
allowBooleanAttributes: false, //a tag can have attributes without any value
//ignoreRootElement : false,
parseTagValue: true,
parseAttributeValue: false,
trimValues: true, //Trim string values of tag and attributes
cdataPropName: false,
numberParseOptions: {
hex: true,
leadingZeros: true,
eNotation: true
},
tagValueProcessor: function(tagName, val) {
return val;
},
attributeValueProcessor: function(attrName, val) {
return val;
},
stopNodes: [], //nested tags will not be parsed even for errors
alwaysCreateTextNode: false,
isArray: () => false,
commentPropName: false,
unpairedTags: [],
processEntities: true,
htmlEntities: false,
ignoreDeclaration: false,
ignorePiTags: false,
transformTagName: false,
transformAttributeName: false,
updateTag: function(tagName, jPath, attrs){
return tagName
},
// skipEmptyListItem: false
preserveOrder: false,
attributeNamePrefix: '@_',
attributesGroupName: false,
textNodeName: '#text',
ignoreAttributes: true,
removeNSPrefix: false, // remove NS from tag name or attribute name if true
allowBooleanAttributes: false, //a tag can have attributes without any value
//ignoreRootElement : false,
parseTagValue: true,
parseAttributeValue: false,
trimValues: true, //Trim string values of tag and attributes
cdataPropName: false,
numberParseOptions: {
hex: true,
leadingZeros: true,
eNotation: true
},
tagValueProcessor: function (tagName, val) {
return val;
},
attributeValueProcessor: function (attrName, val) {
return val;
},
stopNodes: [], //nested tags will not be parsed even for errors
alwaysCreateTextNode: false,
isArray: () => false,
commentPropName: false,
unpairedTags: [],
processEntities: true,
htmlEntities: false,
ignoreDeclaration: false,
ignorePiTags: false,
transformTagName: false,
transformAttributeName: false,
updateTag: function (tagName, jPath, attrs) {
return tagName
},
// skipEmptyListItem: false
captureMetaData: false,
maxNestedTags: 100,
strictReservedNames: true,
};
const buildOptions = function(options) {
return Object.assign({}, defaultOptions, options);
/**
* Normalizes processEntities option for backward compatibility
* @param {boolean|object} value
* @returns {object} Always returns normalized object
*/
function normalizeProcessEntities(value) {
// Boolean backward compatibility
if (typeof value === 'boolean') {
return {
enabled: value, // true or false
maxEntitySize: 10000,
maxExpansionDepth: 10,
maxTotalExpansions: 1000,
maxExpandedLength: 100000,
allowedTags: null,
tagFilter: null
};
}
// Object config - merge with defaults
if (typeof value === 'object' && value !== null) {
return {
enabled: value.enabled !== false, // default true if not specified
maxEntitySize: value.maxEntitySize ?? 10000,
maxExpansionDepth: value.maxExpansionDepth ?? 10,
maxTotalExpansions: value.maxTotalExpansions ?? 1000,
maxExpandedLength: value.maxExpandedLength ?? 100000,
allowedTags: value.allowedTags ?? null,
tagFilter: value.tagFilter ?? null
};
}
// Default to enabled with limits
return normalizeProcessEntities(true);
}
const buildOptions = function (options) {
const built = Object.assign({}, defaultOptions, options);
// Always normalize processEntities for backward compatibility and validation
built.processEntities = normalizeProcessEntities(built.processEntities);
//console.debug(built.processEntities)
return built;
};

@@ -46,0 +90,0 @@

@@ -6,3 +6,3 @@ 'use strict';

const xmlNode = require('./xmlNode');
const readDocType = require("./DocTypeReader");
const DocTypeReader = require('./DocTypeReader');
const toNumber = require("strnum");

@@ -18,4 +18,4 @@ const getIgnoreAttributesFn = require('../ignoreAttributes')

class OrderedObjParser{
constructor(options){
class OrderedObjParser {
constructor(options) {
this.options = options;

@@ -26,8 +26,8 @@ this.currentNode = null;

this.lastEntities = {
"apos" : { regex: /&(apos|#39|#x27);/g, val : "'"},
"gt" : { regex: /&(gt|#62|#x3E);/g, val : ">"},
"lt" : { regex: /&(lt|#60|#x3C);/g, val : "<"},
"quot" : { regex: /&(quot|#34|#x22);/g, val : "\""},
"apos": { regex: /&(apos|#39|#x27);/g, val: "'" },
"gt": { regex: /&(gt|#62|#x3E);/g, val: ">" },
"lt": { regex: /&(lt|#60|#x3C);/g, val: "<" },
"quot": { regex: /&(quot|#34|#x22);/g, val: "\"" },
};
this.ampEntity = { regex: /&(amp|#38|#x26);/g, val : "&"};
this.ampEntity = { regex: /&(amp|#38|#x26);/g, val: "&" };
this.htmlEntities = {

@@ -40,11 +40,11 @@ "space": { regex: /&(nbsp|#160);/g, val: " " },

// "apos" : { regex: /&(apos|#39);/g, val: "'" },
"cent" : { regex: /&(cent|#162);/g, val: "¢" },
"pound" : { regex: /&(pound|#163);/g, val: "£" },
"yen" : { regex: /&(yen|#165);/g, val: "¥" },
"euro" : { regex: /&(euro|#8364);/g, val: "€" },
"copyright" : { regex: /&(copy|#169);/g, val: "©" },
"reg" : { regex: /&(reg|#174);/g, val: "®" },
"inr" : { regex: /&(inr|#8377);/g, val: "₹" },
"num_dec": { regex: /&#([0-9]{1,7});/g, val : (_, str) => String.fromCharCode(Number.parseInt(str, 10)) },
"num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val : (_, str) => String.fromCharCode(Number.parseInt(str, 16)) },
"cent": { regex: /&(cent|#162);/g, val: "¢" },
"pound": { regex: /&(pound|#163);/g, val: "£" },
"yen": { regex: /&(yen|#165);/g, val: "¥" },
"euro": { regex: /&(euro|#8364);/g, val: "€" },
"copyright": { regex: /&(copy|#169);/g, val: "©" },
"reg": { regex: /&(reg|#174);/g, val: "®" },
"inr": { regex: /&(inr|#8377);/g, val: "₹" },
"num_dec": { regex: /&#([0-9]{1,7});/g, val: (_, str) => fromCodePoint(str, 10, "&#") },
"num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => fromCodePoint(str, 16, "&#x") },
};

@@ -62,2 +62,18 @@ this.addExternalEntities = addExternalEntities;

this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes)
this.entityExpansionCount = 0;
this.currentExpandedLength = 0;
if (this.options.stopNodes && this.options.stopNodes.length > 0) {
this.stopNodesExact = new Set();
this.stopNodesWildcard = new Set();
for (let i = 0; i < this.options.stopNodes.length; i++) {
const stopNodeExp = this.options.stopNodes[i];
if (typeof stopNodeExp !== 'string') continue;
if (stopNodeExp.startsWith("*.")) {
this.stopNodesWildcard.add(stopNodeExp.substring(2));
} else {
this.stopNodesExact.add(stopNodeExp);
}
}
}
}

@@ -67,9 +83,10 @@

function addExternalEntities(externalEntities){
function addExternalEntities(externalEntities) {
const entKeys = Object.keys(externalEntities);
for (let i = 0; i < entKeys.length; i++) {
const ent = entKeys[i];
const escaped = ent.replace(/[.\-+*:]/g, '\\.');
this.lastEntities[ent] = {
regex: new RegExp("&"+ent+";","g"),
val : externalEntities[ent]
regex: new RegExp("&" + escaped + ";", "g"),
val: externalEntities[ent]
}

@@ -93,19 +110,19 @@ }

}
if(val.length > 0){
if(!escapeEntities) val = this.replaceEntitiesValue(val);
if (val.length > 0) {
if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath);
const newval = this.options.tagValueProcessor(tagName, val, jPath, hasAttributes, isLeafNode);
if(newval === null || newval === undefined){
if (newval === null || newval === undefined) {
//don't parse
return val;
}else if(typeof newval !== typeof val || newval !== val){
} else if (typeof newval !== typeof val || newval !== val) {
//overwrite
return newval;
}else if(this.options.trimValues){
} else if (this.options.trimValues) {
return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
}else{
} else {
const trimmedVal = val.trim();
if(trimmedVal === val){
if (trimmedVal === val) {
return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
}else{
} else {
return val;

@@ -155,3 +172,3 @@ }

}
if(aName === "__proto__") aName = "#__proto__";
if (aName === "__proto__") aName = "#__proto__";
if (oldVal !== undefined) {

@@ -161,11 +178,11 @@ if (this.options.trimValues) {

}
oldVal = this.replaceEntitiesValue(oldVal);
oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath);
const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPath);
if(newVal === null || newVal === undefined){
if (newVal === null || newVal === undefined) {
//don't parse
attrs[aName] = oldVal;
}else if(typeof newVal !== typeof oldVal || newVal !== oldVal){
} else if (typeof newVal !== typeof oldVal || newVal !== oldVal) {
//overwrite
attrs[aName] = newVal;
}else{
} else {
//parse

@@ -195,3 +212,3 @@ attrs[aName] = parseValue(

const parseXml = function(xmlData) {
const parseXml = function (xmlData) {
xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line

@@ -202,23 +219,29 @@ const xmlObj = new xmlNode('!xml');

let jPath = "";
for(let i=0; i< xmlData.length; i++){//for each char in XML data
// Reset entity expansion counters for this document
this.entityExpansionCount = 0;
this.currentExpandedLength = 0;
const docTypeReader = new DocTypeReader(this.options.processEntities);
for (let i = 0; i < xmlData.length; i++) {//for each char in XML data
const ch = xmlData[i];
if(ch === '<'){
if (ch === '<') {
// const nextIndex = i+1;
// const _2ndChar = xmlData[nextIndex];
if( xmlData[i+1] === '/') {//Closing Tag
if (xmlData[i + 1] === '/') {//Closing Tag
const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.")
let tagName = xmlData.substring(i+2,closeIndex).trim();
let tagName = xmlData.substring(i + 2, closeIndex).trim();
if(this.options.removeNSPrefix){
if (this.options.removeNSPrefix) {
const colonIndex = tagName.indexOf(":");
if(colonIndex !== -1){
tagName = tagName.substr(colonIndex+1);
if (colonIndex !== -1) {
tagName = tagName.substr(colonIndex + 1);
}
}
if(this.options.transformTagName) {
if (this.options.transformTagName) {
tagName = this.options.transformTagName(tagName);
}
if(currentNode){
if (currentNode) {
textData = this.saveTextToParentTag(textData, currentNode, jPath);

@@ -228,11 +251,11 @@ }

//check if last tag of nested tag was unpaired tag
const lastTagName = jPath.substring(jPath.lastIndexOf(".")+1);
if(tagName && this.options.unpairedTags.indexOf(tagName) !== -1 ){
const lastTagName = jPath.substring(jPath.lastIndexOf(".") + 1);
if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) {
throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`);
}
let propIndex = 0
if(lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1 ){
propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.')-1)
if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) {
propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.') - 1)
this.tagsNodeStack.pop();
}else{
} else {
propIndex = jPath.lastIndexOf(".");

@@ -245,20 +268,19 @@ }

i = closeIndex;
} else if( xmlData[i+1] === '?') {
} else if (xmlData[i + 1] === '?') {
let tagData = readTagExp(xmlData,i, false, "?>");
if(!tagData) throw new Error("Pi Tag is not closed.");
let tagData = readTagExp(xmlData, i, false, "?>");
if (!tagData) throw new Error("Pi Tag is not closed.");
textData = this.saveTextToParentTag(textData, currentNode, jPath);
if( (this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags){
if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) {
//do nothing
} else {
}else{
const childNode = new xmlNode(tagData.tagName);
childNode.add(this.options.textNodeName, "");
if(tagData.tagName !== tagData.tagExp && tagData.attrExpPresent){
if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath, tagData.tagName);
}
this.addChild(currentNode, childNode, jPath)
this.addChild(currentNode, childNode, jPath, i);
}

@@ -268,5 +290,5 @@

i = tagData.closeIndex + 1;
} else if(xmlData.substr(i + 1, 3) === '!--') {
const endIndex = findClosingIndex(xmlData, "-->", i+4, "Comment is not closed.")
if(this.options.commentPropName){
} else if (xmlData.substr(i + 1, 3) === '!--') {
const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.")
if (this.options.commentPropName) {
const comment = xmlData.substring(i + 4, endIndex - 2);

@@ -276,12 +298,12 @@

currentNode.add(this.options.commentPropName, [ { [this.options.textNodeName] : comment } ]);
currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]);
}
i = endIndex;
} else if( xmlData.substr(i + 1, 2) === '!D') {
const result = readDocType(xmlData, i);
} else if (xmlData.substr(i + 1, 2) === '!D') {
const result = docTypeReader.readDocType(xmlData, i);
this.docTypeEntities = result.entities;
i = result.i;
}else if(xmlData.substr(i + 1, 2) === '![') {
} else if (xmlData.substr(i + 1, 2) === '![') {
const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
const tagExp = xmlData.substring(i + 9,closeIndex);
const tagExp = xmlData.substring(i + 9, closeIndex);

@@ -291,15 +313,15 @@ textData = this.saveTextToParentTag(textData, currentNode, jPath);

let val = this.parseTextData(tagExp, currentNode.tagname, jPath, true, false, true, true);
if(val == undefined) val = "";
if (val == undefined) val = "";
//cdata should be set even if it is 0 length string
if(this.options.cdataPropName){
currentNode.add(this.options.cdataPropName, [ { [this.options.textNodeName] : tagExp } ]);
}else{
if (this.options.cdataPropName) {
currentNode.add(this.options.cdataPropName, [{ [this.options.textNodeName]: tagExp }]);
} else {
currentNode.add(this.options.textNodeName, val);
}
i = closeIndex + 2;
}else {//Opening tag
let result = readTagExp(xmlData,i, this.options.removeNSPrefix);
let tagName= result.tagName;
} else {//Opening tag
let result = readTagExp(xmlData, i, this.options.removeNSPrefix);
let tagName = result.tagName;
const rawTagName = result.rawTagName;

@@ -311,8 +333,20 @@ let tagExp = result.tagExp;

if (this.options.transformTagName) {
tagName = this.options.transformTagName(tagName);
//console.log(tagExp, tagName)
const newTagName = this.options.transformTagName(tagName);
if (tagExp === tagName) {
tagExp = newTagName
}
tagName = newTagName;
}
if (this.options.strictReservedNames &&
(tagName === this.options.commentPropName
|| tagName === this.options.cdataPropName
)) {
throw new Error(`Invalid tag name: ${tagName}`);
}
//save text as child node
if (currentNode && textData) {
if(currentNode.tagname !== '!xml'){
if (currentNode.tagname !== '!xml') {
//when nested tag is found

@@ -325,18 +359,19 @@ textData = this.saveTextToParentTag(textData, currentNode, jPath, false);

const lastTag = currentNode;
if(lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1 ){
if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) {
currentNode = this.tagsNodeStack.pop();
jPath = jPath.substring(0, jPath.lastIndexOf("."));
}
if(tagName !== xmlObj.tagname){
if (tagName !== xmlObj.tagname) {
jPath += jPath ? "." + tagName : tagName;
}
if (this.isItStopNode(this.options.stopNodes, jPath, tagName)) {
const startIndex = i;
if (this.isItStopNode(this.stopNodesExact, this.stopNodesWildcard, jPath, tagName)) {
let tagContent = "";
//self-closing tag
if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){
if(tagName[tagName.length - 1] === "/"){ //remove trailing '/'
if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
if (tagName[tagName.length - 1] === "/") { //remove trailing '/'
tagName = tagName.substr(0, tagName.length - 1);
jPath = jPath.substr(0, jPath.length - 1);
tagExp = tagName;
}else{
} else {
tagExp = tagExp.substr(0, tagExp.length - 1);

@@ -347,11 +382,11 @@ }

//unpaired tag
else if(this.options.unpairedTags.indexOf(tagName) !== -1){
else if (this.options.unpairedTags.indexOf(tagName) !== -1) {
i = result.closeIndex;
}
//normal tag
else{
else {
//read until closing tag is found
const result = this.readStopNodeData(xmlData, rawTagName, closeIndex + 1);
if(!result) throw new Error(`Unexpected end of ${rawTagName}`);
if (!result) throw new Error(`Unexpected end of ${rawTagName}`);
i = result.i;

@@ -362,41 +397,59 @@ tagContent = result.tagContent;

const childNode = new xmlNode(tagName);
if(tagName !== tagExp && attrExpPresent){
if (tagName !== tagExp && attrExpPresent) {
childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
}
if(tagContent) {
if (tagContent) {
tagContent = this.parseTextData(tagContent, tagName, jPath, true, attrExpPresent, true, true);
}
jPath = jPath.substr(0, jPath.lastIndexOf("."));
childNode.add(this.options.textNodeName, tagContent);
this.addChild(currentNode, childNode, jPath)
}else{
//selfClosing tag
if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){
if(tagName[tagName.length - 1] === "/"){ //remove trailing '/'
this.addChild(currentNode, childNode, jPath, startIndex);
} else {
//selfClosing tag
if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
if (tagName[tagName.length - 1] === "/") { //remove trailing '/'
tagName = tagName.substr(0, tagName.length - 1);
jPath = jPath.substr(0, jPath.length - 1);
tagExp = tagName;
}else{
} else {
tagExp = tagExp.substr(0, tagExp.length - 1);
}
if(this.options.transformTagName) {
tagName = this.options.transformTagName(tagName);
if (this.options.transformTagName) {
const newTagName = this.options.transformTagName(tagName);
if (tagExp === tagName) {
tagExp = newTagName
}
tagName = newTagName;
}
const childNode = new xmlNode(tagName);
if(tagName !== tagExp && attrExpPresent){
if (tagName !== tagExp && attrExpPresent) {
childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
}
this.addChild(currentNode, childNode, jPath)
this.addChild(currentNode, childNode, jPath, startIndex);
jPath = jPath.substr(0, jPath.lastIndexOf("."));
}
//opening tag
else{
const childNode = new xmlNode( tagName);
else if (this.options.unpairedTags.indexOf(tagName) !== -1) {//unpaired tag
const childNode = new xmlNode(tagName);
if (tagName !== tagExp && attrExpPresent) {
childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
}
this.addChild(currentNode, childNode, jPath, startIndex);
jPath = jPath.substr(0, jPath.lastIndexOf("."));
i = result.closeIndex;
// Continue to next iteration without changing currentNode
continue;
}
//opening tag
else {
const childNode = new xmlNode(tagName);
if (this.tagsNodeStack.length > this.options.maxNestedTags) {
throw new Error("Maximum nested tags exceeded");
}
this.tagsNodeStack.push(currentNode);
if(tagName !== tagExp && attrExpPresent){
if (tagName !== tagExp && attrExpPresent) {
childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);

@@ -411,3 +464,3 @@ }

}
}else{
} else {
textData += xmlData[i];

@@ -419,47 +472,110 @@ }

function addChild(currentNode, childNode, jPath){
function addChild(currentNode, childNode, jPath, startIndex) {
// unset startIndex if not requested
if (!this.options.captureMetaData) startIndex = undefined;
const result = this.options.updateTag(childNode.tagname, jPath, childNode[":@"])
if(result === false){
}else if(typeof result === "string"){
if (result === false) {
//do nothing
} else if (typeof result === "string") {
childNode.tagname = result
currentNode.addChild(childNode);
}else{
currentNode.addChild(childNode);
currentNode.addChild(childNode, startIndex);
} else {
currentNode.addChild(childNode, startIndex);
}
}
const replaceEntitiesValue = function(val){
const replaceEntitiesValue = function (val, tagName, jPath) {
// Performance optimization: Early return if no entities to replace
if (val.indexOf('&') === -1) {
return val;
}
if(this.options.processEntities){
for(let entityName in this.docTypeEntities){
const entity = this.docTypeEntities[entityName];
val = val.replace( entity.regx, entity.val);
const entityConfig = this.options.processEntities;
if (!entityConfig.enabled) {
return val;
}
// Check tag-specific filtering
if (entityConfig.allowedTags) {
if (!entityConfig.allowedTags.includes(tagName)) {
return val; // Skip entity replacement for current tag as not set
}
for(let entityName in this.lastEntities){
const entity = this.lastEntities[entityName];
val = val.replace( entity.regex, entity.val);
}
if (entityConfig.tagFilter) {
if (!entityConfig.tagFilter(tagName, jPath)) {
return val; // Skip based on custom filter
}
if(this.options.htmlEntities){
for(let entityName in this.htmlEntities){
const entity = this.htmlEntities[entityName];
val = val.replace( entity.regex, entity.val);
}
// Replace DOCTYPE entities
for (let entityName in this.docTypeEntities) {
const entity = this.docTypeEntities[entityName];
const matches = val.match(entity.regx);
if (matches) {
// Track expansions
this.entityExpansionCount += matches.length;
// Check expansion limit
if (entityConfig.maxTotalExpansions &&
this.entityExpansionCount > entityConfig.maxTotalExpansions) {
throw new Error(
`Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
);
}
// Store length before replacement
const lengthBefore = val.length;
val = val.replace(entity.regx, entity.val);
// Check expanded length immediately after replacement
if (entityConfig.maxExpandedLength) {
this.currentExpandedLength += (val.length - lengthBefore);
if (this.currentExpandedLength > entityConfig.maxExpandedLength) {
throw new Error(
`Total expanded content size exceeded: ${this.currentExpandedLength} > ${entityConfig.maxExpandedLength}`
);
}
}
}
val = val.replace( this.ampEntity.regex, this.ampEntity.val);
}
if (val.indexOf('&') === -1) return val; // Early exit
// Replace standard entities
for (let entityName in this.lastEntities) {
const entity = this.lastEntities[entityName];
val = val.replace(entity.regex, entity.val);
}
if (val.indexOf('&') === -1) return val; // Early exit
// Replace HTML entities if enabled
if (this.options.htmlEntities) {
for (let entityName in this.htmlEntities) {
const entity = this.htmlEntities[entityName];
val = val.replace(entity.regex, entity.val);
}
}
// Replace ampersand entity last
val = val.replace(this.ampEntity.regex, this.ampEntity.val);
return val;
}
function saveTextToParentTag(textData, currentNode, jPath, isLeafNode) {
function saveTextToParentTag(textData, parentNode, jPath, isLeafNode) {
if (textData) { //store previously collected data as textNode
if(isLeafNode === undefined) isLeafNode = currentNode.child.length === 0
if (isLeafNode === undefined) isLeafNode = parentNode.child.length === 0
textData = this.parseTextData(textData,
currentNode.tagname,
parentNode.tagname,
jPath,
false,
currentNode[":@"] ? Object.keys(currentNode[":@"]).length !== 0 : false,
parentNode[":@"] ? Object.keys(parentNode[":@"]).length !== 0 : false,
isLeafNode);
if (textData !== undefined && textData !== "")
currentNode.add(this.options.textNodeName, textData);
parentNode.add(this.options.textNodeName, textData);
textData = "";

@@ -472,13 +588,10 @@ }

/**
*
* @param {string[]} stopNodes
* @param {Set} stopNodesExact
* @param {Set} stopNodesWildcard
* @param {string} jPath
* @param {string} currentTagName
* @param {string} currentTagName
*/
function isItStopNode(stopNodes, jPath, currentTagName){
const allNodesExp = "*." + currentTagName;
for (const stopNodePath in stopNodes) {
const stopNodeExp = stopNodes[stopNodePath];
if( allNodesExp === stopNodeExp || jPath === stopNodeExp ) return true;
}
function isItStopNode(stopNodesExact, stopNodesWildcard, jPath, currentTagName) {
if (stopNodesWildcard && stopNodesWildcard.has(currentTagName)) return true;
if (stopNodesExact && stopNodesExact.has(jPath)) return true;
return false;

@@ -493,3 +606,3 @@ }

*/
function tagExpWithClosingIndex(xmlData, i, closingChar = ">"){
function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
let attrBoundary;

@@ -500,8 +613,8 @@ let tagExp = "";

if (attrBoundary) {
if (ch === attrBoundary) attrBoundary = "";//reset
if (ch === attrBoundary) attrBoundary = "";//reset
} else if (ch === '"' || ch === "'") {
attrBoundary = ch;
attrBoundary = ch;
} else if (ch === closingChar[0]) {
if(closingChar[1]){
if(xmlData[index + 1] === closingChar[1]){
if (closingChar[1]) {
if (xmlData[index + 1] === closingChar[1]) {
return {

@@ -512,3 +625,3 @@ data: tagExp,

}
}else{
} else {
return {

@@ -526,7 +639,7 @@ data: tagExp,

function findClosingIndex(xmlData, str, i, errMsg){
function findClosingIndex(xmlData, str, i, errMsg) {
const closingIndex = xmlData.indexOf(str, i);
if(closingIndex === -1){
if (closingIndex === -1) {
throw new Error(errMsg)
}else{
} else {
return closingIndex + str.length - 1;

@@ -536,5 +649,5 @@ }

function readTagExp(xmlData,i, removeNSPrefix, closingChar = ">"){
const result = tagExpWithClosingIndex(xmlData, i+1, closingChar);
if(!result) return;
function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") {
const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar);
if (!result) return;
let tagExp = result.data;

@@ -545,3 +658,3 @@ const closeIndex = result.index;

let attrExpPresent = true;
if(separatorIndex !== -1){//separate tag name and attributes expression
if (separatorIndex !== -1) {//separate tag name and attributes expression
tagName = tagExp.substring(0, separatorIndex);

@@ -552,6 +665,6 @@ tagExp = tagExp.substring(separatorIndex + 1).trimStart();

const rawTagName = tagName;
if(removeNSPrefix){
if (removeNSPrefix) {
const colonIndex = tagName.indexOf(":");
if(colonIndex !== -1){
tagName = tagName.substr(colonIndex+1);
if (colonIndex !== -1) {
tagName = tagName.substr(colonIndex + 1);
attrExpPresent = tagName !== result.data.substr(colonIndex + 1);

@@ -575,3 +688,3 @@ }

*/
function readStopNodeData(xmlData, tagName, i){
function readStopNodeData(xmlData, tagName, i) {
const startIndex = i;

@@ -582,37 +695,37 @@ // Starting at 1 since we already have an open tag

for (; i < xmlData.length; i++) {
if( xmlData[i] === "<"){
if (xmlData[i+1] === "/") {//close tag
const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`);
let closeTagName = xmlData.substring(i+2,closeIndex).trim();
if(closeTagName === tagName){
openTagCount--;
if (openTagCount === 0) {
return {
tagContent: xmlData.substring(startIndex, i),
i : closeIndex
}
if (xmlData[i] === "<") {
if (xmlData[i + 1] === "/") {//close tag
const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`);
let closeTagName = xmlData.substring(i + 2, closeIndex).trim();
if (closeTagName === tagName) {
openTagCount--;
if (openTagCount === 0) {
return {
tagContent: xmlData.substring(startIndex, i),
i: closeIndex
}
}
i=closeIndex;
} else if(xmlData[i+1] === '?') {
const closeIndex = findClosingIndex(xmlData, "?>", i+1, "StopNode is not closed.")
i=closeIndex;
} else if(xmlData.substr(i + 1, 3) === '!--') {
const closeIndex = findClosingIndex(xmlData, "-->", i+3, "StopNode is not closed.")
i=closeIndex;
} else if(xmlData.substr(i + 1, 2) === '![') {
const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
i=closeIndex;
} else {
const tagData = readTagExp(xmlData, i, '>')
}
i = closeIndex;
} else if (xmlData[i + 1] === '?') {
const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.")
i = closeIndex;
} else if (xmlData.substr(i + 1, 3) === '!--') {
const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.")
i = closeIndex;
} else if (xmlData.substr(i + 1, 2) === '![') {
const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
i = closeIndex;
} else {
const tagData = readTagExp(xmlData, i, '>')
if (tagData) {
const openTagName = tagData && tagData.tagName;
if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length-1] !== "/") {
openTagCount++;
}
i=tagData.closeIndex;
if (tagData) {
const openTagName = tagData && tagData.tagName;
if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length - 1] !== "/") {
openTagCount++;
}
i = tagData.closeIndex;
}
}
}
}//end for loop

@@ -625,4 +738,4 @@ }

const newval = val.trim();
if(newval === 'true' ) return true;
else if(newval === 'false' ) return false;
if (newval === 'true') return true;
else if (newval === 'false') return false;
else return toNumber(val, options);

@@ -638,3 +751,12 @@ } else {

function fromCodePoint(str, base, prefix) {
const codePoint = Number.parseInt(str, base);
if (codePoint >= 0 && codePoint <= 0x10FFFF) {
return String.fromCodePoint(codePoint);
} else {
return prefix + str + ";";
}
}
module.exports = OrderedObjParser;
export default {
"<" : "<", //tag start
">" : ">", //tag end
"/" : "/", //close tag
"!" : "!", //comment or docttype
"!--" : "!--", //comment
"-->" : "-->", //comment end
"?" : "?", //pi
"?>" : "?>", //pi end
"?xml" : "?xml", //pi end
"![" : "![", //cdata
"]]>" : "]]>", //cdata end
"[" : "[",
"-" : "-",
"D" : "D",
}
const ampEntity = { regex: /&(amp|#38|#x26);/g, val : "&"};
const htmlEntities = {
"space": { regex: /&(nbsp|#160);/g, val: " " },
// "lt" : { regex: /&(lt|#60);/g, val: "<" },
// "gt" : { regex: /&(gt|#62);/g, val: ">" },
// "amp" : { regex: /&(amp|#38);/g, val: "&" },
// "quot" : { regex: /&(quot|#34);/g, val: "\"" },
// "apos" : { regex: /&(apos|#39);/g, val: "'" },
"cent" : { regex: /&(cent|#162);/g, val: "¢" },
"pound" : { regex: /&(pound|#163);/g, val: "£" },
"yen" : { regex: /&(yen|#165);/g, val: "¥" },
"euro" : { regex: /&(euro|#8364);/g, val: "€" },
"copyright" : { regex: /&(copy|#169);/g, val: "©" },
"reg" : { regex: /&(reg|#174);/g, val: "®" },
"inr" : { regex: /&(inr|#8377);/g, val: "₹" },
"num_dec": { regex: /&#([0-9]{1,7});/g, val : (_, str) => String.fromCharCode(Number.parseInt(str, 10)) },
"num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val : (_, str) => String.fromCharCode(Number.parseInt(str, 16)) },
};
export default class EntitiesParser{
constructor(replaceHtmlEntities) {
this.replaceHtmlEntities = replaceHtmlEntities;
this.docTypeEntities = {};
this.lastEntities = {
"apos" : { regex: /&(apos|#39|#x27);/g, val : "'"},
"gt" : { regex: /&(gt|#62|#x3E);/g, val : ">"},
"lt" : { regex: /&(lt|#60|#x3C);/g, val : "<"},
"quot" : { regex: /&(quot|#34|#x22);/g, val : "\""},
};
}
addExternalEntities(externalEntities){
const entKeys = Object.keys(externalEntities);
for (let i = 0; i < entKeys.length; i++) {
const ent = entKeys[i];
this.addExternalEntity(ent,externalEntities[ent])
}
}
addExternalEntity(key,val){
validateEntityName(key);
if(val.indexOf("&") !== -1) {
reportWarning(`Entity ${key} is not added as '&' is found in value;`)
return;
}else{
this.lastEntities[ent] = {
regex: new RegExp("&"+key+";","g"),
val : val
}
}
}
addDocTypeEntities(entities){
const entKeys = Object.keys(entities);
for (let i = 0; i < entKeys.length; i++) {
const ent = entKeys[i];
this.docTypeEntities[ent] = {
regex: new RegExp("&"+ent+";","g"),
val : entities[ent]
}
}
}
parse(val){
return this.replaceEntitiesValue(val)
}
/**
* 1. Replace DOCTYPE entities
* 2. Replace external entities
* 3. Replace HTML entities if asked
* @param {string} val
*/
replaceEntitiesValue(val){
if(typeof val === "string" && val.length > 0){
for(let entityName in this.docTypeEntities){
const entity = this.docTypeEntities[entityName];
val = val.replace( entity.regx, entity.val);
}
for(let entityName in this.lastEntities){
const entity = this.lastEntities[entityName];
val = val.replace( entity.regex, entity.val);
}
if(this.replaceHtmlEntities){
for(let entityName in htmlEntities){
const entity = htmlEntities[entityName];
val = val.replace( entity.regex, entity.val);
}
}
val = val.replace( ampEntity.regex, ampEntity.val);
}
return val;
}
};
//an entity name should not contains special characters that may be used in regex
//Eg !?\\\/[]$%{}^&*()<>
const specialChar = "!?\\\/[]$%{}^&*()<>|+";
function validateEntityName(name){
for (let i = 0; i < specialChar.length; i++) {
const ch = specialChar[i];
if(name.indexOf(ch) !== -1) throw new Error(`Invalid character ${ch} in entity name`);
}
return name;
}
const Constants = {
space: 32,
tab: 9
}
export default class BufferSource{
constructor(bytesArr){
this.line = 1;
this.cols = 0;
this.buffer = bytesArr;
this.startIndex = 0;
}
readCh() {
return String.fromCharCode(this.buffer[this.startIndex++]);
}
readChAt(index) {
return String.fromCharCode(this.buffer[this.startIndex+index]);
}
readStr(n,from){
if(typeof from === "undefined") from = this.startIndex;
return this.buffer.slice(from, from + n).toString();
}
readUpto(stopStr) {
const inputLength = this.buffer.length;
const stopLength = stopStr.length;
const stopBuffer = Buffer.from(stopStr);
for (let i = this.startIndex; i < inputLength; i++) {
let match = true;
for (let j = 0; j < stopLength; j++) {
if (this.buffer[i + j] !== stopBuffer[j]) {
match = false;
break;
}
}
if (match) {
const result = this.buffer.slice(this.startIndex, i).toString();
this.startIndex = i + stopLength;
return result;
}
}
throw new Error(`Unexpected end of source. Reading '${stopStr}'`);
}
readUptoCloseTag(stopStr) { //stopStr: "</tagname"
const inputLength = this.buffer.length;
const stopLength = stopStr.length;
const stopBuffer = Buffer.from(stopStr);
let stopIndex = 0;
//0: non-matching, 1: matching stop string, 2: matching closing
let match = 0;
for (let i = this.startIndex; i < inputLength; i++) {
if(match === 1){//initial part matched
if(stopIndex === 0) stopIndex = i;
if(this.buffer[i] === Constants.space || this.buffer[i] === Constants.tab) continue;
else if(this.buffer[i] === '>'){ //TODO: if it should be equivalent ASCII
match = 2;
//tag boundary found
// this.startIndex
}
}else{
match = 1;
for (let j = 0; j < stopLength; j++) {
if (this.buffer[i + j] !== stopBuffer[j]) {
match = 0;
break;
}
}
}
if (match === 2) {//matched closing part
const result = this.buffer.slice(this.startIndex, stopIndex - 1 ).toString();
this.startIndex = i + 1;
return result;
}
}
throw new Error(`Unexpected end of source. Reading '${stopStr}'`);
}
readFromBuffer(n, shouldUpdate) {
let ch;
if (n === 1) {
ch = this.buffer[this.startIndex];
if (ch === 10) {
this.line++;
this.cols = 1;
} else {
this.cols++;
}
ch = String.fromCharCode(ch);
} else {
this.cols += n;
ch = this.buffer.slice(this.startIndex, this.startIndex + n).toString();
}
if (shouldUpdate) this.updateBuffer(n);
return ch;
}
updateBufferBoundary(n = 1) { //n: number of characters read
this.startIndex += n;
}
canRead(n){
n = n || this.startIndex;
return this.buffer.length - n + 1 > 0;
}
}
const whiteSpaces = [" ", "\n", "\t"];
export default class StringSource{
constructor(str){
this.line = 1;
this.cols = 0;
this.buffer = str;
//a boundary pointer to indicate where from the buffer dat should be read
// data before this pointer can be deleted to free the memory
this.startIndex = 0;
}
readCh() {
return this.buffer[this.startIndex++];
}
readChAt(index) {
return this.buffer[this.startIndex+index];
}
readStr(n,from){
if(typeof from === "undefined") from = this.startIndex;
return this.buffer.substring(from, from + n);
}
readUpto(stopStr) {
const inputLength = this.buffer.length;
const stopLength = stopStr.length;
for (let i = this.startIndex; i < inputLength; i++) {
let match = true;
for (let j = 0; j < stopLength; j++) {
if (this.buffer[i + j] !== stopStr[j]) {
match = false;
break;
}
}
if (match) {
const result = this.buffer.substring(this.startIndex, i);
this.startIndex = i + stopLength;
return result;
}
}
throw new Error(`Unexpected end of source. Reading '${stopStr}'`);
}
readUptoCloseTag(stopStr) { //stopStr: "</tagname"
const inputLength = this.buffer.length;
const stopLength = stopStr.length;
let stopIndex = 0;
//0: non-matching, 1: matching stop string, 2: matching closing
let match = 0;
for (let i = this.startIndex; i < inputLength; i++) {
if(match === 1){//initial part matched
if(stopIndex === 0) stopIndex = i;
if(this.buffer[i] === ' ' || this.buffer[i] === '\t') continue;
else if(this.buffer[i] === '>'){
match = 2;
//tag boundary found
// this.startIndex
}
}else{
match = 1;
for (let j = 0; j < stopLength; j++) {
if (this.buffer[i + j] !== stopStr[j]) {
match = 0;
break;
}
}
}
if (match === 2) {//matched closing part
const result = this.buffer.substring(this.startIndex, stopIndex - 1 );
this.startIndex = i + 1;
return result;
}
}
throw new Error(`Unexpected end of source. Reading '${stopStr}'`);
}
readFromBuffer(n, updateIndex){
let ch;
if(n===1){
ch = this.buffer[this.startIndex];
// if(ch === "\n") {
// this.line++;
// this.cols = 1;
// }else{
// this.cols++;
// }
}else{
ch = this.buffer.substring(this.startIndex, this.startIndex + n);
// if("".indexOf("\n") !== -1){
// //TODO: handle the scenario when there are multiple lines
// //TODO: col should be set to number of chars after last '\n'
// // this.cols = 1;
// }else{
// this.cols += n;
// }
}
if(updateIndex) this.updateBufferBoundary(n);
return ch;
}
//TODO: rename to updateBufferReadIndex
updateBufferBoundary(n = 1) { //n: number of characters read
this.startIndex += n;
}
canRead(n){
n = n || this.startIndex;
return this.buffer.length - n + 1 > 0;
}
}
import {JsObjOutputBuilder} from './OutputBuilders/JsObjBuilder.js';
export const defaultOptions = {
preserveOrder: false,
removeNSPrefix: false, // remove NS from tag name or attribute name if true
//ignoreRootElement : false,
stopNodes: [], //nested tags will not be parsed even for errors
// isArray: () => false, //User will set it
htmlEntities: false,
// skipEmptyListItem: false
tags:{
unpaired: [],
nameFor:{
cdata: false,
comment: false,
text: '#text'
},
separateTextProperty: false,
},
attributes:{
ignore: false,
booleanType: true,
entities: true,
},
// select: ["img[src]"],
// stop: ["anim", "[ads]"]
only: [], // rest tags will be skipped. It will result in flat array
hierarchy: false, //will be used when a particular tag is set to be parsed.
skip: [], // will be skipped from parse result. on('skip') will be triggered
select: [], // on('select', tag => tag ) will be called if match
stop: [], //given tagPath will not be parsed. innerXML will be set as string value
OutputBuilder: new JsObjOutputBuilder(),
};
export const buildOptions = function(options) {
const finalOptions = { ... defaultOptions};
copyProperties(finalOptions,options)
return finalOptions;
};
function copyProperties(target, source) {
for (let key in source) {
if (source.hasOwnProperty(key)) {
if (key === 'OutputBuilder') {
target[key] = source[key];
}else if (typeof source[key] === 'object' && !Array.isArray(source[key])) {
// Recursively copy nested properties
if (typeof target[key] === 'undefined') {
target[key] = {};
}
copyProperties(target[key], source[key]);
} else {
// Copy non-nested properties
target[key] = source[key];
}
}
}
}
export default class BaseOutputBuilder{
constructor(){
// this.attributes = {};
}
addAttribute(name, value){
if(this.options.onAttribute){
//TODO: better to pass tag path
const v = this.options.onAttribute(name, value, this.tagName);
if(v) this.attributes[v.name] = v.value;
}else{
name = this.options.attributes.prefix + name + this.options.attributes.suffix;
this.attributes[name] = this.parseValue(value, this.options.attributes.valueParsers);
}
}
/**
* parse value by chain of parsers
* @param {string} val
* @returns {any} parsed value if matching parser found
*/
parseValue = function(val, valParsers){
for (let i = 0; i < valParsers.length; i++) {
let valParser = valParsers[i];
if(typeof valParser === "string"){
valParser = this.registeredParsers[valParser];
}
if(valParser){
val = valParser.parse(val);
}
}
return val;
}
/**
* To add a nested empty tag.
* @param {string} key
* @param {any} val
*/
_addChild(key, val){}
/**
* skip the comment if property is not set
*/
addComment(text){
if(this.options.nameFor.comment)
this._addChild(this.options.nameFor.comment, text);
}
//store CDATA separately if property is set
//otherwise add to tag's value
addCdata(text){
if (this.options.nameFor.cdata) {
this._addChild(this.options.nameFor.cdata, text);
} else {
this.addRawValue(text || "");
}
}
addRawValue = text => this.addValue(text);
addDeclaration(){
if(!this.options.declaration){
}else{
this.addPi("?xml");
}
this.attributes = {}
}
}
import {buildOptions,registerCommonValueParsers} from './ParserOptionsBuilder.js';
export default class OutputBuilder{
constructor(options){
this.options = buildOptions(options);
this.registeredParsers = registerCommonValueParsers(this.options);
}
registerValueParser(name,parserInstance){//existing name will override the parser without warning
this.registeredParsers[name] = parserInstance;
}
getInstance(parserOptions){
return new JsArrBuilder(parserOptions, this.options, this.registeredParsers);
}
}
const rootName = '!js_arr';
import BaseOutputBuilder from './BaseOutputBuilder.js';
class JsArrBuilder extends BaseOutputBuilder{
constructor(parserOptions, options,registeredParsers) {
super();
this.tagsStack = [];
this.parserOptions = parserOptions;
this.options = options;
this.registeredParsers = registeredParsers;
this.root = new Node(rootName);
this.currentNode = this.root;
this.attributes = {};
}
addTag(tag){
//when a new tag is added, it should be added as child of current node
//TODO: shift this check to the parser
if(tag.name === "__proto__") tag.name = "#__proto__";
this.tagsStack.push(this.currentNode);
this.currentNode = new Node(tag.name, this.attributes);
this.attributes = {};
}
/**
* Check if the node should be added by checking user's preference
* @param {Node} node
* @returns boolean: true if the node should not be added
*/
closeTag(){
const node = this.currentNode;
this.currentNode = this.tagsStack.pop(); //set parent node in scope
if(this.options.onClose !== undefined){
//TODO TagPathMatcher
const resultTag = this.options.onClose(node,
new TagPathMatcher(this.tagsStack,node));
if(resultTag) return;
}
this.currentNode.child.push(node); //to parent node
}
//Called by parent class methods
_addChild(key, val){
// if(key === "__proto__") tagName = "#__proto__";
this.currentNode.child.push( {[key]: val });
// this.currentNode.leafType = false;
}
/**
* Add text value child node
* @param {string} text
*/
addValue(text){
this.currentNode.child.push( {[this.options.nameFor.text]: this.parseValue(text, this.options.tags.valueParsers) });
}
addPi(name){
//TODO: set pi flag
if(!this.options.ignorePiTags){
const node = new Node(name, this.attributes);
this.currentNode[":@"] = this.attributes;
this.currentNode.child.push(node);
}
this.attributes = {};
}
getOutput(){
return this.root.child[0];
}
}
class Node{
constructor(tagname, attributes){
this.tagname = tagname;
this.child = []; //nested tags, text, cdata, comments
if(attributes && Object.keys(attributes).length > 0)
this[":@"] = attributes;
}
}
module.exports = OutputBuilder;
import {buildOptions,registerCommonValueParsers} from"./ParserOptionsBuilder";
export default class OutputBuilder{
constructor(options){
this.options = buildOptions(options);
this.registeredParsers = registerCommonValueParsers(this.options);
}
registerValueParser(name,parserInstance){//existing name will override the parser without warning
this.registeredParsers[name] = parserInstance;
}
getInstance(parserOptions){
return new JsMinArrBuilder(parserOptions, this.options, this.registeredParsers);
}
}
import BaseOutputBuilder from "./BaseOutputBuilder.js";
const rootName = '^';
class JsMinArrBuilder extends BaseOutputBuilder{
constructor(parserOptions, options,registeredParsers) {
super();
this.tagsStack = [];
this.parserOptions = parserOptions;
this.options = options;
this.registeredParsers = registeredParsers;
this.root = {[rootName]: []};
this.currentNode = this.root;
this.currentNodeTagName = rootName;
this.attributes = {};
}
addTag(tag){
//when a new tag is added, it should be added as child of current node
//TODO: shift this check to the parser
if(tag.name === "__proto__") tag.name = "#__proto__";
this.tagsStack.push([this.currentNodeTagName,this.currentNode]); //this.currentNode is parent node here
this.currentNodeTagName = tag.name;
this.currentNode = { [tag.name]:[]}
if(Object.keys(this.attributes).length > 0){
this.currentNode[":@"] = this.attributes;
this.attributes = {};
}
}
/**
* Check if the node should be added by checking user's preference
* @param {Node} node
* @returns boolean: true if the node should not be added
*/
closeTag(){
const node = this.currentNode;
const nodeName = this.currentNodeTagName;
const arr = this.tagsStack.pop(); //set parent node in scope
this.currentNodeTagName = arr[0];
this.currentNode = arr[1];
if(this.options.onClose !== undefined){
//TODO TagPathMatcher
const resultTag = this.options.onClose(node,
new TagPathMatcher(this.tagsStack,node));
if(resultTag) return;
}
this.currentNode[this.currentNodeTagName].push(node); //to parent node
}
//Called by parent class methods
_addChild(key, val){
// if(key === "__proto__") tagName = "#__proto__";
this.currentNode.push( {[key]: val });
// this.currentNode.leafType = false;
}
/**
* Add text value child node
* @param {string} text
*/
addValue(text){
this.currentNode[this.currentNodeTagName].push( {[this.options.nameFor.text]: this.parseValue(text, this.options.tags.valueParsers) });
}
addPi(name){
if(!this.options.ignorePiTags){
const node = { [name]:[]}
if(this.attributes){
node[":@"] = this.attributes;
}
this.currentNode.push(node);
}
this.attributes = {};
}
getOutput(){
return this.root[rootName];
}
}
import {buildOptions,registerCommonValueParsers} from './ParserOptionsBuilder.js';
export default class OutputBuilder{
constructor(builderOptions){
this.options = buildOptions(builderOptions);
this.registeredParsers = registerCommonValueParsers(this.options);
}
registerValueParser(name,parserInstance){//existing name will override the parser without warning
this.registeredParsers[name] = parserInstance;
}
getInstance(parserOptions){
return new JsObjBuilder(parserOptions, this.options, this.registeredParsers);
}
}
import BaseOutputBuilder from './BaseOutputBuilder.js';
const rootName = '^';
class JsObjBuilder extends BaseOutputBuilder{
constructor(parserOptions, builderOptions,registeredParsers) {
super();
//hold the raw detail of a tag and sequence with reference to the output
this.tagsStack = [];
this.parserOptions = parserOptions;
this.options = builderOptions;
this.registeredParsers = registeredParsers;
this.root = {};
this.parent = this.root;
this.tagName = rootName;
this.value = {};
this.textValue = "";
this.attributes = {};
}
addTag(tag){
let value = "";
if( !isEmpty(this.attributes)){
value = {};
if(this.options.attributes.groupBy){
value[this.options.attributes.groupBy] = this.attributes;
}else{
value = this.attributes;
}
}
this.tagsStack.push([this.tagName, this.textValue, this.value]); //parent tag, parent text value, parent tag value (jsobj)
this.tagName = tag.name;
this.value = value;
this.textValue = "";
this.attributes = {};
}
/**
* Check if the node should be added by checking user's preference
* @param {Node} node
* @returns boolean: true if the node should not be added
*/
closeTag(){
const tagName = this.tagName;
let value = this.value;
let textValue = this.textValue;
//update tag text value
if(typeof value !== "object" && !Array.isArray(value)){
value = this.parseValue(textValue.trim(), this.options.tags.valueParsers);
}else if(textValue.length > 0){
value[this.options.nameFor.text] = this.parseValue(textValue.trim(), this.options.tags.valueParsers);
}
let resultTag= {
tagName: tagName,
value: value
};
if(this.options.onTagClose !== undefined){
//TODO TagPathMatcher
resultTag = this.options.onClose(tagName, value, this.textValue, new TagPathMatcher(this.tagsStack,node));
if(!resultTag) return;
}
//set parent node in scope
let arr = this.tagsStack.pop();
let parentTag = arr[2];
parentTag=this._addChildTo(resultTag.tagName, resultTag.value, parentTag);
this.tagName = arr[0];
this.textValue = arr[1];
this.value = parentTag;
}
_addChild(key, val){
if(typeof this.value === "string"){
this.value = { [this.options.nameFor.text] : this.value };
}
this._addChildTo(key, val, this.value);
// this.currentNode.leafType = false;
this.attributes = {};
}
_addChildTo(key, val, node){
if(typeof node === 'string') node = {};
if(!node[key]){
node[key] = val;
}else{ //Repeated
if(!Array.isArray(node[key])){ //but not stored as array
node[key] = [node[key]];
}
node[key].push(val);
}
return node;
}
/**
* Add text value child node
* @param {string} text
*/
addValue(text){
//TODO: use bytes join
if(this.textValue.length > 0) this.textValue += " " + text;
else this.textValue = text;
}
addPi(name){
let value = "";
if( !isEmpty(this.attributes)){
value = {};
if(this.options.attributes.groupBy){
value[this.options.attributes.groupBy] = this.attributes;
}else{
value = this.attributes;
}
}
this._addChild(name, value);
}
getOutput(){
return this.value;
}
}
function isEmpty(obj) {
return Object.keys(obj).length === 0;
}
import trimParser from "../valueParsers/trim";
import booleanParser from "../valueParsers/booleanParser";
import currencyParser from "../valueParsers/currency";
import numberParser from "../valueParsers/number";
const defaultOptions={
nameFor:{
text: "#text",
comment: "",
cdata: "",
},
// onTagClose: () => {},
// onAttribute: () => {},
piTag: false,
declaration: false, //"?xml"
tags: {
valueParsers: [
// "trim",
// "boolean",
// "number",
// "currency",
// "date",
]
},
attributes:{
prefix: "@_",
suffix: "",
groupBy: "",
valueParsers: [
// "trim",
// "boolean",
// "number",
// "currency",
// "date",
]
},
dataType:{
}
}
//TODO
const withJoin = ["trim","join", /*"entities",*/"number","boolean","currency"/*, "date"*/]
const withoutJoin = ["trim", /*"entities",*/"number","boolean","currency"/*, "date"*/]
export function buildOptions(options){
//clone
const finalOptions = { ... defaultOptions};
//add config missed in cloning
finalOptions.tags.valueParsers.push(...withJoin)
if(!this.preserveOrder)
finalOptions.tags.valueParsers.push(...withoutJoin);
//add config missed in cloning
finalOptions.attributes.valueParsers.push(...withJoin)
//override configuration
copyProperties(finalOptions,options);
return finalOptions;
}
function copyProperties(target, source) {
for (let key in source) {
if (source.hasOwnProperty(key)) {
if (typeof source[key] === 'object' && !Array.isArray(source[key])) {
// Recursively copy nested properties
if (typeof target[key] === 'undefined') {
target[key] = {};
}
copyProperties(target[key], source[key]);
} else {
// Copy non-nested properties
target[key] = source[key];
}
}
}
}
export function registerCommonValueParsers(options){
return {
"trim": new trimParser(),
// "join": this.entityParser.parse,
"boolean": new booleanParser(),
"number": new numberParser({
hex: true,
leadingZeros: true,
eNotation: true
}),
"currency": new currencyParser(),
// "date": this.entityParser.parse,
}
}
export default class TagPath{
constructor(pathStr){
let text = "";
let tName = "";
let pos;
let aName = "";
let aVal = "";
this.stack = []
for (let i = 0; i < pathStr.length; i++) {
let ch = pathStr[i];
if(ch === " ") {
if(text.length === 0) continue;
tName = text; text = "";
}else if(ch === "["){
if(tName.length === 0){
tName = text; text = "";
}
i++;
for (; i < pathStr.length; i++) {
ch = pathStr[i];
if(ch=== "=") continue;
else if(ch=== "]") {aName = text.trim(); text=""; break; i--;}
else if(ch === "'" || ch === '"'){
let attrEnd = pathStr.indexOf(ch,i+1);
aVal = pathStr.substring(i+1, attrEnd);
i = attrEnd;
}else{
text +=ch;
}
}
}else if(ch !== " " && text.length === 0 && tName.length > 0){//reading tagName
//save previous tag
this.stack.push(new TagPathNode(tName,pos,aName,aVal));
text = ch; tName = ""; aName = ""; aVal = "";
}else{
text+=ch;
}
}
//last tag in the path
if(tName.length >0 || text.length>0){
this.stack.push(new TagPathNode(text||tName,pos,aName,aVal));
}
}
match(tagStack,node){
if(this.stack[0].name !== "*"){
if(this.stack.length !== tagStack.length +1) return false;
//loop through tagPath and tagStack and match
for (let i = 0; i < this.tagStack.length; i++) {
if(!this.stack[i].match(tagStack[i])) return false;
}
}
if(!this.stack[this.stack.length - 1].match(node)) return false;
return true;
}
}
class TagPathNode{
constructor(name,position,attrName,attrVal){
this.name = name;
this.position = position;
this.attrName = attrName,
this.attrVal = attrVal;
}
match(node){
let matching = true;
matching = node.name === this.name;
if(this.position) matching = node.position === this.position;
if(this.attrName) matching = node.attrs[this.attrName !== undefined];
if(this.attrVal) matching = node.attrs[this.attrName !== this.attrVal];
return matching;
}
}
// console.log((new TagPath("* b[b]")).stack);
// console.log((new TagPath("a[a] b[b] c")).stack);
// console.log((new TagPath(" b [ b= 'cf sdadwa' ] a ")).stack);
import {TagPath} from './TagPath.js';
export default class TagPathMatcher{
constructor(stack,node){
this.stack = stack;
this.node= node;
}
match(path){
const tagPath = new TagPath(path);
return tagPath.match(this.stack, this.node);
}
}
export default class boolParser{
constructor(trueList, falseList){
if(trueList)
this.trueList = trueList;
else
this.trueList = ["true"];
if(falseList)
this.falseList = falseList;
else
this.falseList = ["false"];
}
parse(val){
if (typeof val === 'string') {
//TODO: performance: don't convert
const temp = val.toLowerCase();
if(this.trueList.indexOf(temp) !== -1) return true;
else if(this.falseList.indexOf(temp) !== -1 ) return false;
}
return val;
}
}
export default function boolParserExt(val){
if(isArray(val)){
for (let i = 0; i < val.length; i++) {
val[i] = parse(val[i])
}
}else{
val = parse(val)
}
return val;
}
function parse(val){
if (typeof val === 'string') {
const temp = val.toLowerCase();
if(temp === 'true' || temp ==="yes" || temp==="1") return true;
else if(temp === 'false' || temp ==="no" || temp==="0") return false;
}
return val;
}
const defaultOptions = {
maxLength: 200,
// locale: "en-IN"
}
const localeMap = {
"$":"en-US",
"€":"de-DE",
"£":"en-GB",
"¥":"ja-JP",
"₹":"en-IN",
}
const sign = "(?:-|\+)?";
const digitsAndSeparator = "(?:\d+|\d{1,3}(?:,\d{3})+)";
const decimalPart = "(?:\.\d{1,2})?";
const symbol = "(?:\$|€|¥|₹)?";
const currencyCheckRegex = /^\s*(?:-|\+)?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d{1,2})?\s*(?:\$|€|¥|₹)?\s*$/u;
export default class CurrencyParser{
constructor(options){
this.options = options || defaultOptions;
}
parse(val){
if (typeof val === 'string' && val.length <= this.options.maxLength) {
if(val.indexOf(",,") !== -1 && val.indexOf(".." !== -1)){
const match = val.match(currencyCheckRegex);
if(match){
const locale = this.options.locale || localeMap[match[2]||match[5]||"₹"];
const formatter = new Intl.NumberFormat(locale)
val = val.replace(/[^0-9,.]/g, '').trim();
val = Number(val.replace(formatter.format(1000)[1], ''));
}
}
}
return val;
}
}
CurrencyParser.defaultOptions = defaultOptions;
const ampEntity = { regex: /&(amp|#38|#x26);/g, val : "&"};
const htmlEntities = {
"space": { regex: /&(nbsp|#160);/g, val: " " },
// "lt" : { regex: /&(lt|#60);/g, val: "<" },
// "gt" : { regex: /&(gt|#62);/g, val: ">" },
// "amp" : { regex: /&(amp|#38);/g, val: "&" },
// "quot" : { regex: /&(quot|#34);/g, val: "\"" },
// "apos" : { regex: /&(apos|#39);/g, val: "'" },
"cent" : { regex: /&(cent|#162);/g, val: "¢" },
"pound" : { regex: /&(pound|#163);/g, val: "£" },
"yen" : { regex: /&(yen|#165);/g, val: "¥" },
"euro" : { regex: /&(euro|#8364);/g, val: "€" },
"copyright" : { regex: /&(copy|#169);/g, val: "©" },
"reg" : { regex: /&(reg|#174);/g, val: "®" },
"inr" : { regex: /&(inr|#8377);/g, val: "₹" },
"num_dec": { regex: /&#([0-9]{1,7});/g, val : (_, str) => String.fromCharCode(Number.parseInt(str, 10)) },
"num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val : (_, str) => String.fromCharCode(Number.parseInt(str, 16)) },
};
export default class EntitiesParser{
constructor(replaceHtmlEntities) {
this.replaceHtmlEntities = replaceHtmlEntities;
this.docTypeEntities = {};
this.lastEntities = {
"apos" : { regex: /&(apos|#39|#x27);/g, val : "'"},
"gt" : { regex: /&(gt|#62|#x3E);/g, val : ">"},
"lt" : { regex: /&(lt|#60|#x3C);/g, val : "<"},
"quot" : { regex: /&(quot|#34|#x22);/g, val : "\""},
};
}
addExternalEntities(externalEntities){
const entKeys = Object.keys(externalEntities);
for (let i = 0; i < entKeys.length; i++) {
const ent = entKeys[i];
this.addExternalEntity(ent,externalEntities[ent])
}
}
addExternalEntity(key,val){
validateEntityName(key);
if(val.indexOf("&") !== -1) {
reportWarning(`Entity ${key} is not added as '&' is found in value;`)
return;
}else{
this.lastEntities[ent] = {
regex: new RegExp("&"+key+";","g"),
val : val
}
}
}
addDocTypeEntities(entities){
const entKeys = Object.keys(entities);
for (let i = 0; i < entKeys.length; i++) {
const ent = entKeys[i];
this.docTypeEntities[ent] = {
regex: new RegExp("&"+ent+";","g"),
val : entities[ent]
}
}
}
parse(val){
return this.replaceEntitiesValue(val)
}
/**
* 1. Replace DOCTYPE entities
* 2. Replace external entities
* 3. Replace HTML entities if asked
* @param {string} val
*/
replaceEntitiesValue(val){
if(typeof val === "string" && val.length > 0){
for(let entityName in this.docTypeEntities){
const entity = this.docTypeEntities[entityName];
val = val.replace( entity.regx, entity.val);
}
for(let entityName in this.lastEntities){
const entity = this.lastEntities[entityName];
val = val.replace( entity.regex, entity.val);
}
if(this.replaceHtmlEntities){
for(let entityName in htmlEntities){
const entity = htmlEntities[entityName];
val = val.replace( entity.regex, entity.val);
}
}
val = val.replace( ampEntity.regex, ampEntity.val);
}
return val;
}
};
//an entity name should not contains special characters that may be used in regex
//Eg !?\\\/[]$%{}^&*()<>
const specialChar = "!?\\\/[]$%{}^&*()<>|+";
function validateEntityName(name){
for (let i = 0; i < specialChar.length; i++) {
const ch = specialChar[i];
if(name.indexOf(ch) !== -1) throw new Error(`Invalid character ${ch} in entity name`);
}
return name;
}
/**
*
* @param {array} val
* @param {string} by
* @returns
*/
export default function join(val, by=" "){
if(isArray(val)){
val.join(by)
}
return val;
}
import toNumber from "strnum";
export default class numParser{
constructor(options){
this.options = options;
}
parse(val){
if (typeof val === 'string') {
val = toNumber(val,this.options);
}
return val;
}
}
export default class trimmer{
parse(val){
if(typeof val === "string") return val.trim();
else return val;
}
}
import StringSource from './inputSource/StringSource.js';
import BufferSource from './inputSource/BufferSource.js';
import {readTagExp,readClosingTagName} from './XmlPartReader.js';
import {readComment, readCdata,readDocType,readPiTag} from './XmlSpecialTagsReader.js';
import TagPath from './TagPath.js';
import TagPathMatcher from './TagPathMatcher.js';
import EntitiesParser from './EntitiesParser.js';
//To hold the data of current tag
//This is usually used to compare jpath expression against current tag
class TagDetail{
constructor(name){
this.name = name;
this.position = 0;
// this.attributes = {};
}
}
export default class Xml2JsParser {
constructor(options) {
this.options = options;
this.currentTagDetail = null;
this.tagTextData = "";
this.tagsStack = [];
this.entityParser = new EntitiesParser(options.htmlEntities);
this.stopNodes = [];
for (let i = 0; i < this.options.stopNodes.length; i++) {
this.stopNodes.push(new TagPath(this.options.stopNodes[i]));
}
}
parse(strData) {
this.source = new StringSource(strData);
this.parseXml();
return this.outputBuilder.getOutput();
}
parseBytesArr(data) {
this.source = new BufferSource(data );
this.parseXml();
return this.outputBuilder.getOutput();
}
parseXml() {
//TODO: Separate TagValueParser as separate class. So no scope issue in node builder class
//OutputBuilder should be set in XML Parser
this.outputBuilder = this.options.OutputBuilder.getInstance(this.options);
this.root = { root: true};
this.currentTagDetail = this.root;
while(this.source.canRead()){
let ch = this.source.readCh();
if (ch === "") break;
if(ch === "<"){//tagStart
let nextChar = this.source.readChAt(0);
if (nextChar === "" ) throw new Error("Unexpected end of source");
if(nextChar === "!" || nextChar === "?"){
this.source.updateBufferBoundary();
//previously collected text should be added to current node
this.addTextNode();
this.readSpecialTag(nextChar);// Read DOCTYPE, comment, CDATA, PI tag
}else if(nextChar === "/"){
this.source.updateBufferBoundary();
this.readClosingTag();
// console.log(this.source.buffer.length, this.source.readable);
// console.log(this.tagsStack.length);
}else{//opening tag
this.readOpeningTag();
}
}else{
this.tagTextData += ch;
}
}//End While loop
if(this.tagsStack.length > 0 || ( this.tagTextData !== "undefined" && this.tagTextData.trimEnd().length > 0) ) throw new Error("Unexpected data in the end of document");
}
/**
* read closing paired tag. Set parent tag in scope.
* skip a node on user's choice
*/
readClosingTag(){
const tagName = this.processTagName(readClosingTagName(this.source));
// console.log(tagName, this.tagsStack.length);
this.validateClosingTag(tagName);
// All the text data collected, belongs to current tag.
if(!this.currentTagDetail.root) this.addTextNode();
this.outputBuilder.closeTag();
// Since the tag is closed now, parent tag comes in scope
this.currentTagDetail = this.tagsStack.pop();
}
validateClosingTag(tagName){
// This can't be unpaired tag, or a stop tag.
if(this.isUnpaired(tagName) || this.isStopNode(tagName)) throw new Error(`Unexpected closing tag '${tagName}'`);
// This must match with last opening tag
else if(tagName !== this.currentTagDetail.name)
throw new Error(`Unexpected closing tag '${tagName}' expecting '${this.currentTagDetail.name}'`)
}
/**
* Read paired, unpaired, self-closing, stop and special tags.
* Create a new node
* Push paired tag in stack.
*/
readOpeningTag(){
//save previously collected text data to current node
this.addTextNode();
//create new tag
let tagExp = readTagExp(this, ">" );
// process and skip from tagsStack For unpaired tag, self closing tag, and stop node
const tagDetail = new TagDetail(tagExp.tagName);
if(this.isUnpaired(tagExp.tagName)) {
//TODO: this will lead 2 extra stack operation
this.outputBuilder.addTag(tagDetail);
this.outputBuilder.closeTag();
} else if(tagExp.selfClosing){
this.outputBuilder.addTag(tagDetail);
this.outputBuilder.closeTag();
} else if(this.isStopNode(this.currentTagDetail)){
// TODO: let's user set a stop node boundary detector for complex contents like script tag
//TODO: pass tag name only to avoid string operations
const content = source.readUptoCloseTag(`</${tagExp.tagName}`);
this.outputBuilder.addTag(tagDetail);
this.outputBuilder.addValue(content);
this.outputBuilder.closeTag();
}else{//paired tag
//set new nested tag in scope.
this.tagsStack.push(this.currentTagDetail);
this.outputBuilder.addTag(tagDetail);
this.currentTagDetail = tagDetail;
}
// console.log(tagExp.tagName,this.tagsStack.length);
// this.options.onClose()
}
readSpecialTag(startCh){
if(startCh == "!"){
let nextChar = this.source.readCh();
if (nextChar === null || nextChar === undefined) throw new Error("Unexpected ending of the source");
if(nextChar === "-"){//comment
readComment(this);
}else if(nextChar === "["){//CDATA
readCdata(this);
}else if(nextChar === "D"){//DOCTYPE
readDocType(this);
}
}else if(startCh === "?"){
readPiTag(this);
}else{
throw new Error(`Invalid tag '<${startCh}' at ${this.source.line}:${this.source.col}`)
}
}
addTextNode = function() {
// if(this.currentTagDetail){
//save text as child node
// if(this.currentTagDetail.tagname !== '!xml')
if (this.tagTextData !== undefined && this.tagTextData !== "") { //store previously collected data as textNode
if(this.tagTextData.trim().length > 0){
//TODO: shift parsing to output builder
this.outputBuilder.addValue(this.replaceEntities(this.tagTextData));
}
this.tagTextData = "";
}
// }
}
processAttrName(name){
if(name === "__proto__") name = "#__proto__";
name = resolveNameSpace(name, this.removeNSPrefix);
return name;
}
processTagName(name){
if(name === "__proto__") name = "#__proto__";
name = resolveNameSpace(name, this.removeNSPrefix);
return name;
}
/**
* Generate tags path from tagsStack
*/
tagsPath(tagName){
//TODO: return TagPath Object. User can call match method with path
return "";
}
isUnpaired(tagName){
return this.options.tags.unpaired.indexOf(tagName) !== -1;
}
/**
* valid expressions are
* tag nested
* * nested
* tag nested[attribute]
* tag nested[attribute=""]
* tag nested[attribute!=""]
* tag nested:0 //for future
* @param {string} tagName
* @returns
*/
isStopNode(node){
for (let i = 0; i < this.stopNodes.length; i++) {
const givenPath = this.stopNodes[i];
if(givenPath.match(this.tagsStack, node)) return true;
}
return false
}
replaceEntities(text){
//TODO: if option is set then replace entities
return this.entityParser.parse(text)
}
}
function resolveNameSpace(name, removeNSPrefix) {
if (removeNSPrefix) {
const parts = name.split(':');
if(parts.length === 2){
if (parts[0] === 'xmlns') return '';
else return parts[1];
}else reportError(`Multiple namespaces ${name}`)
}
return name;
}
import { buildOptions} from './OptionsBuilder.js';
import Xml2JsParser from './Xml2JsParser.js';
export default class XMLParser{
constructor(options){
this.externalEntities = {};
this.options = buildOptions(options);
// console.log(this.options)
}
/**
* Parse XML data string to JS object
* @param {string|Buffer} xmlData
* @param {boolean|Object} validationOption
*/
parse(xmlData){
if(Array.isArray(xmlData) && xmlData.byteLength !== undefined){
return this.parse(xmlData);
}else if( xmlData.toString){
xmlData = xmlData.toString();
}else{
throw new Error("XML data is accepted in String or Bytes[] form.")
}
// if( validationOption){
// if(validationOption === true) validationOption = {}; //validate with default options
// const result = validator.validate(xmlData, validationOption);
// if (result !== true) {
// throw Error( `${result.err.msg}:${result.err.line}:${result.err.col}` )
// }
// }
const parser = new Xml2JsParser(this.options);
parser.entityParser.addExternalEntities(this.externalEntities);
return parser.parse(xmlData);
}
/**
* Parse XML data buffer to JS object
* @param {string|Buffer} xmlData
* @param {boolean|Object} validationOption
*/
parseBytesArr(xmlData){
if(Array.isArray(xmlData) && xmlData.byteLength !== undefined){
}else{
throw new Error("XML data is accepted in Bytes[] form.")
}
const parser = new Xml2JsParser(this.options);
parser.entityParser.addExternalEntities(this.externalEntities);
return parser.parseBytesArr(xmlData);
}
/**
* Parse XML data stream to JS object
* @param {fs.ReadableStream} xmlDataStream
*/
parseStream(xmlDataStream){
if(!isStream(xmlDataStream)) throw new Error("FXP: Invalid stream input");
const orderedObjParser = new Xml2JsParser(this.options);
orderedObjParser.entityParser.addExternalEntities(this.externalEntities);
return orderedObjParser.parseStream(xmlDataStream);
}
/**
* Add Entity which is not by default supported by this library
* @param {string} key
* @param {string} value
*/
addEntity(key, value){
if(value.indexOf("&") !== -1){
throw new Error("Entity value can't have '&'")
}else if(key.indexOf("&") !== -1 || key.indexOf(";") !== -1){
throw new Error("An entity must be set without '&' and ';'. Eg. use '#xD' for '&#xD;'")
}else if(value === "&"){
throw new Error("An entity with value '&' is not permitted");
}else{
this.externalEntities[key] = value;
}
}
}
function isStream(stream){
if(stream && typeof stream.read === "function" && typeof stream.on === "function" && typeof stream.readableEnded === "boolean") return true;
return false;
}
'use strict';
/**
* find paired tag for a stop node
* @param {string} xmlDoc
* @param {string} tagName
* @param {number} i : start index
*/
export function readStopNode(xmlDoc, tagName, i){
const startIndex = i;
// Starting at 1 since we already have an open tag
let openTagCount = 1;
for (; i < xmlDoc.length; i++) {
if( xmlDoc[i] === "<"){
if (xmlDoc[i+1] === "/") {//close tag
const closeIndex = findSubStrIndex(xmlDoc, ">", i, `${tagName} is not closed`);
let closeTagName = xmlDoc.substring(i+2,closeIndex).trim();
if(closeTagName === tagName){
openTagCount--;
if (openTagCount === 0) {
return {
tagContent: xmlDoc.substring(startIndex, i),
i : closeIndex
}
}
}
i=closeIndex;
} else if(xmlDoc[i+1] === '?') {
const closeIndex = findSubStrIndex(xmlDoc, "?>", i+1, "StopNode is not closed.")
i=closeIndex;
} else if(xmlDoc.substr(i + 1, 3) === '!--') {
const closeIndex = findSubStrIndex(xmlDoc, "-->", i+3, "StopNode is not closed.")
i=closeIndex;
} else if(xmlDoc.substr(i + 1, 2) === '![') {
const closeIndex = findSubStrIndex(xmlDoc, "]]>", i, "StopNode is not closed.") - 2;
i=closeIndex;
} else {
const tagData = readTagExp(xmlDoc, i, '>')
if (tagData) {
const openTagName = tagData && tagData.tagName;
if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length-1] !== "/") {
openTagCount++;
}
i=tagData.closeIndex;
}
}
}
}//end for loop
}
/**
* Read closing tag name
* @param {Source} source
* @returns tag name
*/
export function readClosingTagName(source){
let text = ""; //temporary data
while(source.canRead()){
let ch = source.readCh();
// if (ch === null || ch === undefined) break;
// source.updateBuffer();
if (ch === ">") return text.trimEnd();
else text += ch;
}
throw new Error(`Unexpected end of source. Reading '${substr}'`);
}
/**
* Read XML tag and build attributes map
* This function can be used to read normal tag, pi tag.
* This function can't be used to read comment, CDATA, DOCTYPE.
* Eg <tag attr = ' some"' attr= ">" bool>
* @param {string} xmlDoc
* @param {number} startIndex starting index
* @returns tag expression includes tag name & attribute string
*/
export function readTagExp(parser) {
let inSingleQuotes = false;
let inDoubleQuotes = false;
let i;
let EOE = false;
for (i = 0; parser.source.canRead(i); i++) {
const char = parser.source.readChAt(i);
if (char === "'" && !inDoubleQuotes) {
inSingleQuotes = !inSingleQuotes;
} else if (char === '"' && !inSingleQuotes) {
inDoubleQuotes = !inDoubleQuotes;
} else if (char === '>' && !inSingleQuotes && !inDoubleQuotes) {
// If not inside quotes, stop reading at '>'
EOE = true;
break;
}
}
if(inSingleQuotes || inDoubleQuotes){
throw new Error("Invalid attribute expression. Quote is not properly closed");
}else if(!EOE) throw new Error("Unexpected closing of source. Waiting for '>'");
const exp = parser.source.readStr(i);
parser.source.updateBufferBoundary(i + 1);
return buildTagExpObj(exp, parser)
}
export function readPiExp(parser) {
let inSingleQuotes = false;
let inDoubleQuotes = false;
let i;
let EOE = false;
for (i = 0; parser.source.canRead(i) ; i++) {
const currentChar = parser.source.readChAt(i);
const nextChar = parser.source.readChAt(i+1);
if (currentChar === "'" && !inDoubleQuotes) {
inSingleQuotes = !inSingleQuotes;
} else if (currentChar === '"' && !inSingleQuotes) {
inDoubleQuotes = !inDoubleQuotes;
}
if (!inSingleQuotes && !inDoubleQuotes) {
if (currentChar === '?' && nextChar === '>') {
EOE = true;
break; // Exit the loop when '?>' is found
}
}
}
if(inSingleQuotes || inDoubleQuotes){
throw new Error("Invalid attribute expression. Quote is not properly closed in PI tag expression");
}else if(!EOE) throw new Error("Unexpected closing of source. Waiting for '?>'");
if(!parser.options.attributes.ignore){
//TODO: use regex to verify attributes if not set to ignore
}
const exp = parser.source.readStr(i);
parser.source.updateBufferBoundary(i + 1);
return buildTagExpObj(exp, parser)
}
function buildTagExpObj(exp, parser){
const tagExp = {
tagName: "",
selfClosing: false
};
let attrsExp = "";
// Check for self-closing tag before setting the name
if(exp[exp.length -1] === "/") {
tagExp.selfClosing = true;
exp = exp.slice(0, -1); // Remove the trailing slash
}
//separate tag name
let i = 0;
for (; i < exp.length; i++) {
const char = exp[i];
if(char === " "){
tagExp.tagName = exp.substring(0, i);
attrsExp = exp.substring(i + 1);
break;
}
}
//only tag
if(tagExp.tagName.length === 0 && i === exp.length)tagExp.tagName = exp;
tagExp.tagName = tagExp.tagName.trimEnd();
if(!parser.options.attributes.ignore && attrsExp.length > 0){
parseAttributesExp(attrsExp,parser)
}
return tagExp;
}
const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm');
function parseAttributesExp(attrStr, parser) {
const matches = getAllMatches(attrStr, attrsRegx);
const len = matches.length; //don't make it inline
for (let i = 0; i < len; i++) {
let attrName = parser.processAttrName(matches[i][1]);
let attrVal = parser.replaceEntities(matches[i][4] || true);
parser.outputBuilder.addAttribute(attrName, attrVal);
}
}
const getAllMatches = function(string, regex) {
const matches = [];
let match = regex.exec(string);
while (match) {
const allmatches = [];
allmatches.startIndex = regex.lastIndex - match[0].length;
const len = match.length;
for (let index = 0; index < len; index++) {
allmatches.push(match[index]);
}
matches.push(allmatches);
match = regex.exec(string);
}
return matches;
};
import {readPiExp} from './XmlPartReader.js';
export function readCdata(parser){
//<![ are already read till this point
let str = parser.source.readStr(6); //CDATA[
parser.source.updateBufferBoundary(6);
if(str !== "CDATA[") throw new Error(`Invalid CDATA expression at ${parser.source.line}:${parser.source.cols}`);
let text = parser.source.readUpto("]]>");
parser.outputBuilder.addCdata(text);
}
export function readPiTag(parser){
//<? are already read till this point
let tagExp = readPiExp(parser, "?>");
if(!tagExp) throw new Error("Invalid Pi Tag expression.");
if (tagExp.tagName === "?xml") {//TODO: test if tagName is just xml
parser.outputBuilder.addDeclaration();
} else {
parser.outputBuilder.addPi("?"+tagExp.tagName);
}
}
export function readComment(parser){
//<!- are already read till this point
let ch = parser.source.readCh();
if(ch !== "-") throw new Error(`Invalid comment expression at ${parser.source.line}:${parser.source.cols}`);
let text = parser.source.readUpto("-->");
parser.outputBuilder.addComment(text);
}
const DOCTYPE_tags = {
"EL":/^EMENT\s+([^\s>]+)\s+(ANY|EMPTY|\(.+\)\s*$)/m,
"AT":/^TLIST\s+[^\s]+\s+[^\s]+\s+[^\s]+\s+[^\s]+\s+$/m,
"NO":/^TATION.+$/m
}
export function readDocType(parser){
//<!D are already read till this point
let str = parser.source.readStr(6); //OCTYPE
parser.source.updateBufferBoundary(6);
if(str !== "OCTYPE") throw new Error(`Invalid DOCTYPE expression at ${parser.source.line}:${parser.source.cols}`);
let hasBody = false, lastch = "";
while(parser.source.canRead()){
//TODO: use readChAt like used in partReader
let ch = parser.source.readCh();
if(hasBody){
if (ch === '<') { //Determine the tag type
let str = parser.source.readStr(2);
parser.source.updateBufferBoundary(2);
if(str === "EN"){ //ENTITY
let str = parser.source.readStr(4);
parser.source.updateBufferBoundary(4);
if(str !== "TITY") throw new Error("Invalid DOCTYPE ENTITY expression");
registerEntity(parser);
}else if(str === "!-") {//comment
readComment(parser);
}else{ //ELEMENT, ATTLIST, NOTATION
let dTagExp = parser.source.readUpto(">");
const regx = DOCTYPE_tags[str];
if(regx){
const match = dTagExp.match(regx);
if(!match) throw new Error("Invalid DOCTYPE");
}else throw new Error("Invalid DOCTYPE");
}
}else if( ch === '>' && lastch === "]"){//end of doctype
return;
}
}else if( ch === '>'){//end of doctype
return;
}else if( ch === '['){
hasBody = true;
}else{
lastch = ch;
}
}//End While loop
}
function registerEntity(parser){
//read Entity
let attrBoundary="";
let name ="", val ="";
while(source.canRead()){
let ch = source.readCh();
if(attrBoundary){
if (ch === attrBoundary){
val = text;
text = ""
}
}else if(ch === " " || ch === "\t"){
if(!name){
name = text.trimStart();
text = "";
}
}else if (ch === '"' || ch === "'") {//start of attrBoundary
attrBoundary = ch;
}else if(ch === ">"){
parser.entityParser.addExternalEntity(name,val);
return;
}else{
text+=ch;
}
}
}