fast-xml-parser
Advanced tools
| 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" | ||
| } | ||
| } | ||
| } |
+102
-18
@@ -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); |
+373
-127
| 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 '
'") | ||
| }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; | ||
| } | ||
| } | ||
| } |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
206581
-10.36%54
-21.74%4399
-22.02%22
83.33%Updated