初始化环境文件

This commit is contained in:
CN-JS-HuiBai
2026-04-04 12:49:09 +08:00
parent 07742d2688
commit c607af6fac
5971 changed files with 515160 additions and 18 deletions

97
node_modules/fast-xml-parser/src/cli/cli.js generated vendored Normal file
View File

@@ -0,0 +1,97 @@
#!/usr/bin/env node
'use strict';
/*eslint-disable no-console*/
import fs from 'fs';
import { resolve } from 'path';
import {XMLParser, XMLValidator} from "../fxp.js";
import ReadToEnd from './read.js';
import cmdDetail from "./man.js"
console.warn("\x1b[33m%s\x1b[0m", "⚠️ Warning: The built-in CLI interface is now deprecated.");
console.warn("Please install the dedicated CLI package instead:");
console.warn(" npm install -g fxp-cli");
if (process.argv[2] === '--help' || process.argv[2] === '-h') {
console.log(cmdDetail);
} else if (process.argv[2] === '--version') {
const packageJsonPath = resolve(process.cwd(), 'package.json');
const version = JSON.parse(fs.readFileSync(packageJsonPath).toString()).version;
console.log(version);
} else {
const options = {
removeNSPrefix: true,
ignoreAttributes: false,
parseTagValue: true,
parseAttributeValue: true,
};
let fileName = '';
let outputFileName;
let validate = false;
let validateOnly = false;
for (let i = 2; i < process.argv.length; i++) {
if (process.argv[i] === '-ns') {
options.removeNSPrefix = false;
} else if (process.argv[i] === '-a') {
options.ignoreAttributes = true;
} else if (process.argv[i] === '-c') {
options.parseTagValue = false;
options.parseAttributeValue = false;
} else if (process.argv[i] === '-o') {
outputFileName = process.argv[++i];
} else if (process.argv[i] === '-v') {
validate = true;
} else if (process.argv[i] === '-V') {
validateOnly = true;
} else {
//filename
fileName = process.argv[i];
}
}
const callback = function(xmlData) {
let output = '';
if (validateOnly) {
output = XMLValidator.validate(xmlData);
process.exitCode = output === true ? 0 : 1;
} else {
const parser = new XMLParser(options);
output = JSON.stringify(parser.parse(xmlData,validate), null, 4);
}
if (outputFileName) {
writeToFile(outputFileName, output);
} else {
console.log(output);
}
};
try {
if (!fileName) {
ReadToEnd.readToEnd(process.stdin, function(err, data) {
if (err) {
throw err;
}
callback(data.toString());
});
} else {
fs.readFile(fileName, function(err, data) {
if (err) {
throw err;
}
callback(data.toString());
});
}
} catch (e) {
console.log('Seems an invalid file or stream.' + e);
}
}
function writeToFile(fileName, data) {
fs.writeFile(fileName, data, function(err) {
if (err) {
throw err;
}
console.log('JSON output has been written to ' + fileName);
});
}

17
node_modules/fast-xml-parser/src/cli/man.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
import fs from 'fs';
import { resolve } from 'path';
const packageJsonPath = resolve(process.cwd(), 'package.json');
const version = JSON.parse(fs.readFileSync(packageJsonPath).toString()).version;
export default `Fast XML Parser ${version}
----------------
$ fxparser [-ns|-a|-c|-v|-V] <filename> [-o outputfile.json]
$ cat xmlfile.xml | fxparser [-ns|-a|-c|-v|-V] [-o outputfile.json]
Options
----------------
-ns: remove namespace from tag and atrribute name.
-a: don't parse attributes.
-c: parse values to premitive type.
-v: validate before parsing.
-V: validate only.`

43
node_modules/fast-xml-parser/src/cli/read.js generated vendored Normal file
View File

@@ -0,0 +1,43 @@
'use strict';
import { Transform } from 'stream';
export default class ReadToEnd extends Transform {
constructor(options = {}) {
super(options);
this._encoding = options.encoding || 'utf8';
this._buffer = '';
}
_transform(chunk, encoding, done) {
this._buffer += chunk.toString(this._encoding);
this.push(chunk);
done();
}
_flush(done) {
this.emit('complete', null, this._buffer);
done();
}
static readToEnd(stream, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
const dest = new ReadToEnd(options);
stream.pipe(dest);
stream.on('error', (err) => {
stream.unpipe(dest);
callback(err);
});
dest.on('complete', callback);
dest.resume();
return dest;
}
}

579
node_modules/fast-xml-parser/src/fxp.d.ts generated vendored Normal file
View File

@@ -0,0 +1,579 @@
import type { Expression, ReadonlyMatcher } from './pem';
// jPath: true → string
// jPath: false → ReadonlyMatcher
type JPathOrMatcher = string | ReadonlyMatcher;
type JPathOrExpression = string | Expression;
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;
/**
* Maximum number of entities allowed in the XML
*
* Defaults to `100`
*/
maxEntityCount?: 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 jPathOrMatcher - The jPath string (if jPath: true) or Matcher instance (if jPath: false)
* @returns `true` to allow entity replacement, `false` to skip
*
* Defaults to `null`
*/
tagFilter?: ((tagName: string, jPathOrMatcher: JPathOrMatcher) => boolean) | null;
};
export type X2jOptions = {
/**
* Preserve the order of tags in resulting JS object
*
* Defaults to `false`
*/
preserveOrder?: boolean;
/**
* Give a prefix to the attribute name in the resulting JS object
*
* Defaults to '@_'
*/
attributeNamePrefix?: string;
/**
* A name to group all attributes of a tag under, or `false` to disable
*
* Defaults to `false`
*/
attributesGroupName?: false | string;
/**
* The name of the next node in the resulting JS
*
* Defaults to `#text`
*/
textNodeName?: string;
/**
* Whether to ignore attributes when parsing
*
* When `true` - ignores all the attributes
*
* When `false` - parses all the attributes
*
* When `Array<string | RegExp>` - filters out attributes that match provided patterns
*
* When `Function` - calls the function for each attribute and filters out those for which the function returned `true`
*
* Defaults to `true`
*/
ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, jPathOrMatcher: JPathOrMatcher) => boolean);
/**
* Whether to remove namespace string from tag and attribute names
*
* Defaults to `false`
*/
removeNSPrefix?: boolean;
/**
* Whether to allow attributes without value
*
* Defaults to `false`
*/
allowBooleanAttributes?: boolean;
/**
* Whether to parse tag value with `strnum` package
*
* Defaults to `true`
*/
parseTagValue?: boolean;
/**
* Whether to parse attribute value with `strnum` package
*
* Defaults to `false`
*/
parseAttributeValue?: boolean;
/**
* Whether to remove surrounding whitespace from tag or attribute value
*
* Defaults to `true`
*/
trimValues?: boolean;
/**
* Give a property name to set CDATA values to instead of merging to tag's text value
*
* Defaults to `false`
*/
cdataPropName?: false | string;
/**
* If set, parse comments and set as this property
*
* Defaults to `false`
*/
commentPropName?: false | string;
/**
* Control how tag value should be parsed. Called only if tag value is not empty
*
* @param tagName - The name of the tag
* @param tagValue - The value of the tag
* @param jPathOrMatcher - The jPath string (if jPath: true) or Matcher instance (if jPath: false)
* @param hasAttributes - Whether the tag has attributes
* @param isLeafNode - Whether the tag is a leaf node
* @returns {undefined|null} `undefined` or `null` to set original value.
* @returns {unknown}
*
* 1. Different value or value with different data type to set new value.
* 2. Same value to set parsed value if `parseTagValue: true`.
*
* Defaults to `(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode) => val`
*/
tagValueProcessor?: (tagName: string, tagValue: string, jPathOrMatcher: JPathOrMatcher, hasAttributes: boolean, isLeafNode: boolean) => unknown;
/**
* Control how attribute value should be parsed
*
* @param attrName - The name of the attribute
* @param attrValue - The value of the attribute
* @param jPathOrMatcher - The jPath string (if jPath: true) or Matcher instance (if jPath: false)
* @returns {undefined|null} `undefined` or `null` to set original value
* @returns {unknown}
*
* Defaults to `(attrName, val, jPathOrMatcher) => val`
*/
attributeValueProcessor?: (attrName: string, attrValue: string, jPathOrMatcher: JPathOrMatcher) => unknown;
/**
* Options to pass to `strnum` for parsing numbers
*
* Defaults to `{ hex: true, leadingZeros: true, eNotation: true }`
*/
numberParseOptions?: strnumOptions;
/**
* Nodes to stop parsing at
*
* Accepts string patterns or Expression objects from path-expression-matcher
*
* String patterns starting with "*." are automatically converted to ".." for backward compatibility
*
* Defaults to `[]`
*/
stopNodes?: JPathOrExpression[];
/**
* List of tags without closing tags
*
* Defaults to `[]`
*/
unpairedTags?: string[];
/**
* Whether to always create a text node
*
* Defaults to `false`
*/
alwaysCreateTextNode?: boolean;
/**
* Determine whether a tag should be parsed as an array
*
* @param tagName - The name of the tag
* @param jPathOrMatcher - The jPath string (if jPath: true) or Matcher instance (if jPath: false)
* @param isLeafNode - Whether the tag is a leaf node
* @param isAttribute - Whether this is an attribute
* @returns {boolean}
*
* Defaults to `() => false`
*/
isArray?: (tagName: string, jPathOrMatcher: JPathOrMatcher, isLeafNode: boolean, isAttribute: boolean) => boolean;
/**
* Whether to process default and DOCTYPE entities
*
* 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 | ProcessEntitiesOptions;
/**
* Whether to process HTML entities
*
* Defaults to `false`
*/
htmlEntities?: boolean;
/**
* Whether to ignore the declaration tag from output
*
* Defaults to `false`
*/
ignoreDeclaration?: boolean;
/**
* Whether to ignore Pi tags
*
* Defaults to `false`
*/
ignorePiTags?: boolean;
/**
* Transform tag names
*
* Defaults to `false`
*/
transformTagName?: ((tagName: string) => string) | false;
/**
* Transform attribute names
*
* Defaults to `false`
*/
transformAttributeName?: ((attributeName: string) => string) | false;
/**
* Change the tag name when a different name is returned. Skip the tag from parsed result when false is returned.
* Modify `attrs` object to control attributes for the given tag.
*
* @param tagName - The name of the tag
* @param jPathOrMatcher - The jPath string (if jPath: true) or Matcher instance (if jPath: false)
* @param attrs - The attributes object
* @returns {string} new tag name.
* @returns false to skip the tag
*
* Defaults to `(tagName, jPathOrMatcher, attrs) => tagName`
*/
updateTag?: (tagName: string, jPathOrMatcher: JPathOrMatcher, 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;
/**
* Controls whether callbacks receive jPath as string or Matcher instance
*
* When `true` - callbacks receive jPath as string (backward compatible)
*
* When `false` - callbacks receive Matcher instance for advanced pattern matching
*
* Defaults to `true`
*/
jPath?: boolean;
/**
* Function to sanitize dangerous property names
*
* @param name - The name of the property
* @returns {string} The sanitized name
*
* Defaults to `(name) => __name`
*/
onDangerousProperty?: (name: string) => string;
};
export type strnumOptions = {
hex: boolean;
leadingZeros: boolean,
skipLike?: RegExp,
eNotation?: boolean
}
export type validationOptions = {
/**
* Whether to allow attributes without value
*
* Defaults to `false`
*/
allowBooleanAttributes?: boolean;
/**
* List of tags without closing tags
*
* Defaults to `[]`
*/
unpairedTags?: string[];
};
export type XmlBuilderOptions = {
/**
* Give a prefix to the attribute name in the resulting JS object
*
* Defaults to '@_'
*/
attributeNamePrefix?: string;
/**
* A name to group all attributes of a tag under, or `false` to disable
*
* Defaults to `false`
*/
attributesGroupName?: false | string;
/**
* The name of the next node in the resulting JS
*
* Defaults to `#text`
*/
textNodeName?: string;
/**
* Whether to ignore attributes when building
*
* When `true` - ignores all the attributes
*
* When `false` - builds all the attributes
*
* When `Array<string | RegExp>` - filters out attributes that match provided patterns
*
* When `Function` - calls the function for each attribute and filters out those for which the function returned `true`
*
* Defaults to `true`
*/
ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, jPath: string) => boolean);
/**
* Give a property name to set CDATA values to instead of merging to tag's text value
*
* Defaults to `false`
*/
cdataPropName?: false | string;
/**
* If set, parse comments and set as this property
*
* Defaults to `false`
*/
commentPropName?: false | string;
/**
* Whether to make output pretty instead of single line
*
* Defaults to `false`
*/
format?: boolean;
/**
* If `format` is set to `true`, sets the indent string
*
* Defaults to ` `
*/
indentBy?: string;
/**
* Give a name to a top-level array
*
* Defaults to `undefined`
*/
arrayNodeName?: string;
/**
* Create empty tags for tags with no text value
*
* Defaults to `false`
*/
suppressEmptyNode?: boolean;
/**
* Suppress an unpaired tag
*
* Defaults to `true`
*/
suppressUnpairedNode?: boolean;
/**
* Don't put a value for boolean attributes
*
* Defaults to `true`
*/
suppressBooleanAttributes?: boolean;
/**
* Preserve the order of tags in resulting JS object
*
* Defaults to `false`
*/
preserveOrder?: boolean;
/**
* List of tags without closing tags
*
* Defaults to `[]`
*/
unpairedTags?: string[];
/**
* Nodes to stop parsing at
*
* Accepts string patterns or Expression objects from path-expression-matcher
*
* Defaults to `[]`
*/
stopNodes?: JPathOrExpression[];
/**
* Control how tag value should be parsed. Called only if tag value is not empty
*
* @returns {undefined|null} `undefined` or `null` to set original value.
* @returns {unknown}
*
* 1. Different value or value with different data type to set new value.
* 2. Same value to set parsed value if `parseTagValue: true`.
*
* Defaults to `(tagName, val, jPath, hasAttributes, isLeafNode) => val`
*/
tagValueProcessor?: (name: string, value: unknown) => unknown;
/**
* Control how attribute value should be parsed
*
* @param attrName
* @param attrValue
* @param jPath
* @returns {undefined|null} `undefined` or `null` to set original value
* @returns {unknown}
*
* Defaults to `(attrName, val, jPath) => val`
*/
attributeValueProcessor?: (name: string, value: unknown) => unknown;
/**
* Whether to process default and DOCTYPE entities
*
* Defaults to `true`
*/
processEntities?: boolean;
oneListGroup?: boolean;
/**
* Maximum number of nested tags
*
* Defaults to `100`
*/
maxNestedTags?: number;
};
type ESchema = string | object | Array<string | object>;
export type ValidationError = {
err: {
code: string;
msg: string,
line: number,
col: number
};
};
export class XMLParser {
constructor(options?: X2jOptions);
parse(xmlData: string | Uint8Array, validationOptions?: validationOptions | boolean): any;
/**
* Add Entity which is not by default supported by this library
* @param entityIdentifier {string} Eg: 'ent' for &ent;
* @param entityValue {string} Eg: '\r'
*/
addEntity(entityIdentifier: string, entityValue: string): void;
/**
* Returns a Symbol that can be used to access the {@link XMLMetaData}
* property on a node.
*
* If Symbol is not available in the environment, an ordinary property is used
* and the name of the property is here returned.
*
* The XMLMetaData property is only present when {@link X2jOptions.captureMetaData}
* is true in the options.
*/
static getMetaDataSymbol(): Symbol;
}
export class XMLValidator {
static validate(xmlData: string, options?: validationOptions): true | ValidationError;
}
export class XMLBuilder {
constructor(options?: XmlBuilderOptions);
build(jObj: any): string;
}
/**
* This object is available on nodes via the symbol {@link XMLParser.getMetaDataSymbol}
* when {@link X2jOptions.captureMetaData} is true.
*/
export interface XMLMetaData {
/** The index, if available, of the character where the XML node began in the input stream. */
startIndex?: number;
}

14
node_modules/fast-xml-parser/src/fxp.js generated vendored Normal file
View File

@@ -0,0 +1,14 @@
'use strict';
import { validate } from './validator.js';
import XMLParser from './xmlparser/XMLParser.js';
import XMLBuilder from './xmlbuilder/json2xml.js';
const XMLValidator = {
validate: validate
}
export {
XMLParser,
XMLValidator,
XMLBuilder
};

18
node_modules/fast-xml-parser/src/ignoreAttributes.js generated vendored Normal file
View File

@@ -0,0 +1,18 @@
export default function getIgnoreAttributesFn(ignoreAttributes) {
if (typeof ignoreAttributes === 'function') {
return ignoreAttributes
}
if (Array.isArray(ignoreAttributes)) {
return (attrName) => {
for (const pattern of ignoreAttributes) {
if (typeof pattern === 'string' && attrName === pattern) {
return true
}
if (pattern instanceof RegExp && pattern.test(attrName)) {
return true
}
}
}
}
return () => false
}

135
node_modules/fast-xml-parser/src/pem.d.ts generated vendored Normal file
View File

@@ -0,0 +1,135 @@
/**
* Types copied from path-expression-matcher
* @version <version>
* @updated <date>
*
* Update this file when path-expression-matcher releases a new version.
* Source: https://github.com/NaturalIntelligence/path-expression-matcher
*/
/**
* Options for creating an Expression
*/
export interface ExpressionOptions {
/**
* Path separator character
* @default '.'
*/
separator?: string;
}
/**
* Parsed segment from an expression pattern
*/
export interface Segment {
type: 'tag' | 'deep-wildcard';
tag?: string;
namespace?: string;
attrName?: string;
attrValue?: string;
position?: 'first' | 'last' | 'odd' | 'even' | 'nth';
positionValue?: number;
}
/**
* Expression - Parses and stores a tag pattern expression.
* Patterns are parsed once and stored in an optimized structure for fast matching.
*
* @example
* ```typescript
* const expr = new Expression("root.users.user");
* const expr2 = new Expression("..user[id]:first");
* const expr3 = new Expression("root/users/user", { separator: '/' });
* ```
*
* Pattern Syntax:
* - `root.users.user` — Match exact path
* - `..user` — Match "user" at any depth (deep wildcard)
* - `user[id]` — Match user tag with "id" attribute
* - `user[id=123]` — Match user tag where id="123"
* - `user:first` — Match first occurrence of user tag
* - `ns::user` — Match user tag with namespace "ns"
* - `ns::user[id]:first` — Combine namespace, attribute, and position
*/
export class Expression {
readonly pattern: string;
readonly separator: string;
readonly segments: Segment[];
constructor(pattern: string, options?: ExpressionOptions);
get length(): number;
hasDeepWildcard(): boolean;
hasAttributeCondition(): boolean;
hasPositionSelector(): boolean;
toString(): string;
}
// ---------------------------------------------------------------------------
// ReadonlyMatcher
// ---------------------------------------------------------------------------
/**
* A live read-only view of a {@link Matcher} instance, returned by {@link Matcher.readOnly}.
*
* All query and inspection methods work normally and always reflect the current
* state of the underlying matcher. State-mutating methods (`push`, `pop`,
* `reset`, `updateCurrent`, `restore`) are not present — calling them on the
* underlying Proxy throws a `TypeError` at runtime.
*
* This is the type received by all FXP user callbacks when `jPath: false`.
*/
export interface ReadonlyMatcher {
readonly separator: string;
/** Check if current path matches an Expression. */
matches(expression: Expression): boolean;
/** Get current tag name, or `undefined` if path is empty. */
getCurrentTag(): string | undefined;
/** Get current namespace, or `undefined` if not present. */
getCurrentNamespace(): string | undefined;
/** Get attribute value of the current node. */
getAttrValue(attrName: string): any;
/** Check if the current node has a given attribute. */
hasAttr(attrName: string): boolean;
/** Sibling position of the current node (child index in parent). */
getPosition(): number;
/** Occurrence counter of the current tag name at this level. */
getCounter(): number;
/** Number of nodes in the current path. */
getDepth(): number;
/** Current path as a string (e.g. `"root.users.user"`). */
toString(separator?: string, includeNamespace?: boolean): string;
/** Current path as an array of tag names. */
toArray(): string[];
/**
* Create a snapshot of the current state.
* The snapshot can be passed to the real {@link Matcher.restore} if needed.
*/
snapshot(): MatcherSnapshot;
}
/** Internal node structure — exposed via snapshot only. */
export interface PathNode {
tag: string;
namespace?: string;
position: number;
counter: number;
values?: Record<string, any>;
}
/** Snapshot of matcher state returned by `snapshot()` and `readOnly().snapshot()`. */
export interface MatcherSnapshot {
path: PathNode[];
siblingStacks: Map<string, number>[];
}

61
node_modules/fast-xml-parser/src/util.js generated vendored Normal file
View File

@@ -0,0 +1,61 @@
'use strict';
const nameStartChar = ':A-Za-z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD';
const nameChar = nameStartChar + '\\-.\\d\\u00B7\\u0300-\\u036F\\u203F-\\u2040';
export const nameRegexp = '[' + nameStartChar + '][' + nameChar + ']*';
const regexName = new RegExp('^' + nameRegexp + '$');
export function getAllMatches(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;
}
export const isName = function (string) {
const match = regexName.exec(string);
return !(match === null || typeof match === 'undefined');
}
export function isExist(v) {
return typeof v !== 'undefined';
}
export function isEmptyObject(obj) {
return Object.keys(obj).length === 0;
}
export function getValue(v) {
if (exports.isExist(v)) {
return v;
} else {
return '';
}
}
/**
* Dangerous property names that could lead to prototype pollution or security issues
*/
export const DANGEROUS_PROPERTY_NAMES = [
// '__proto__',
// 'constructor',
// 'prototype',
'hasOwnProperty',
'toString',
'valueOf',
'__defineGetter__',
'__defineSetter__',
'__lookupGetter__',
'__lookupSetter__'
];
export const criticalProperties = ["__proto__", "constructor", "prototype"];

16
node_modules/fast-xml-parser/src/v6/CharsSymbol.js generated vendored Normal file
View File

@@ -0,0 +1,16 @@
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",
}

106
node_modules/fast-xml-parser/src/v6/EntitiesParser.js generated vendored Normal file
View File

@@ -0,0 +1,106 @@
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.fromCodePoint(Number.parseInt(str, 10)) },
"num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => String.fromCodePoint(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);
const escaped = key.replace(/[.\-+*:]/g, '\\.');
if (val.indexOf("&") !== -1) {
reportWarning(`Entity ${key} is not added as '&' is found in value;`)
return;
} else {
this.lastEntities[key] = {
regex: new RegExp("&" + escaped + ";", "g"),
val: val
}
}
}
addDocTypeEntities(entities) {
const entKeys = Object.keys(entities);
for (let i = 0; i < entKeys.length; i++) {
const ent = entKeys[i];
const escaped = ent.replace(/[.\-+*:]/g, '\\.');
this.docTypeEntities[ent] = {
regex: new RegExp("&" + escaped + ";", "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;
}

61
node_modules/fast-xml-parser/src/v6/OptionsBuilder.js generated vendored Normal file
View File

@@ -0,0 +1,61 @@
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 (Object.prototype.hasOwnProperty.call(source, 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];
}
}
}
}

View File

@@ -0,0 +1,69 @@
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 = {}
}
}

View File

@@ -0,0 +1,103 @@
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;

View File

@@ -0,0 +1,100 @@
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 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];
}
}

View File

@@ -0,0 +1,154 @@
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;
}

View File

@@ -0,0 +1,94 @@
import trimParser from "../valueParsers/trim.js";
import booleanParser from "../valueParsers/booleanParser.js";
import currencyParser from "../valueParsers/currency.js";
import numberParser from "../valueParsers/number.js";
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 (Object.prototype.hasOwnProperty.call(source, 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,
}
}

0
node_modules/fast-xml-parser/src/v6/Report.js generated vendored Normal file
View File

81
node_modules/fast-xml-parser/src/v6/TagPath.js generated vendored Normal file
View File

@@ -0,0 +1,81 @@
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);

13
node_modules/fast-xml-parser/src/v6/TagPathMatcher.js generated vendored Normal file
View File

@@ -0,0 +1,13 @@
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);
}
}

83
node_modules/fast-xml-parser/src/v6/XMLParser.js generated vendored Normal file
View File

@@ -0,0 +1,83 @@
import { buildOptions } from './OptionsBuilder.js';
import Xml2JsParser from './Xml2JsParser.js';
export default class XMLParser {
constructor(options) {
this.externalEntities = {};
this.options = buildOptions(options);
// console.log(this.options)
}
/**
* Parse XML data string to JS object
* @param {string|Buffer} xmlData
* @param {boolean|Object} validationOption
*/
parse(xmlData) {
if (Array.isArray(xmlData) && xmlData.byteLength !== undefined) {
return this.parse(xmlData);
} else if (xmlData.toString) {
xmlData = xmlData.toString();
} else {
throw new Error("XML data is accepted in String or Bytes[] form.")
}
// if( validationOption){
// if(validationOption === true) validationOption = {}; //validate with default options
// const result = validator.validate(xmlData, validationOption);
// if (result !== true) {
// throw Error( `${result.err.msg}:${result.err.line}:${result.err.col}` )
// }
// }
const parser = new Xml2JsParser(this.options);
parser.entityParser.addExternalEntities(this.externalEntities);
return parser.parse(xmlData);
}
/**
* Parse XML data buffer to JS object
* @param {string|Buffer} xmlData
* @param {boolean|Object} validationOption
*/
parseBytesArr(xmlData) {
if (Array.isArray(xmlData) && xmlData.byteLength !== undefined) {
} else {
throw new Error("XML data is accepted in Bytes[] form.")
}
const parser = new Xml2JsParser(this.options);
parser.entityParser.addExternalEntities(this.externalEntities);
return parser.parseBytesArr(xmlData);
}
/**
* Parse XML data stream to JS object
* @param {fs.ReadableStream} xmlDataStream
*/
parseStream(xmlDataStream) {
if (!isStream(xmlDataStream)) throw new Error("FXP: Invalid stream input");
const orderedObjParser = new Xml2JsParser(this.options);
orderedObjParser.entityParser.addExternalEntities(this.externalEntities);
return orderedObjParser.parseStream(xmlDataStream);
}
/**
* Add Entity which is not by default supported by this library
* @param {string} key
* @param {string} value
*/
addEntity(key, value) {
if (value.indexOf("&") !== -1) {
throw new Error("Entity value can't have '&'")
} else if (key.indexOf("&") !== -1 || key.indexOf(";") !== -1) {
throw new Error("An entity must be set without '&' and ';'. Eg. use '#xD' for '&#xD;'")
} else if (value === "&") {
throw new Error("An entity with value '&' is not permitted");
} else {
this.externalEntities[key] = value;
}
}
}
function isStream(stream) {
if (stream && typeof stream.read === "function" && typeof stream.on === "function" && typeof stream.readableEnded === "boolean") return true;
return false;
}

235
node_modules/fast-xml-parser/src/v6/Xml2JsParser.js generated vendored Normal file
View File

@@ -0,0 +1,235 @@
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;
}

210
node_modules/fast-xml-parser/src/v6/XmlPartReader.js generated vendored Normal file
View File

@@ -0,0 +1,210 @@
'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;
};

View File

@@ -0,0 +1,111 @@
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;
}
}
}

View File

@@ -0,0 +1,116 @@
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;
}
}

View File

@@ -0,0 +1,121 @@
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;
}
}

View File

@@ -0,0 +1,105 @@
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.fromCodePoint(Number.parseInt(str, 10)) },
"num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => String.fromCodePoint(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;
}

View File

@@ -0,0 +1,22 @@
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;
}
}

View File

@@ -0,0 +1,19 @@
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;
}

View File

@@ -0,0 +1,38 @@
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;

View File

@@ -0,0 +1,13 @@
/**
*
* @param {array} val
* @param {string} by
* @returns
*/
export default function join(val, by=" "){
if(isArray(val)){
val.join(by)
}
return val;
}

View File

@@ -0,0 +1,14 @@
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;
}
}

View File

@@ -0,0 +1,6 @@
export default class trimmer{
parse(val){
if(typeof val === "string") return val.trim();
else return val;
}
}

425
node_modules/fast-xml-parser/src/validator.js generated vendored Normal file
View File

@@ -0,0 +1,425 @@
'use strict';
import { getAllMatches, isName } from './util.js';
const defaultOptions = {
allowBooleanAttributes: false, //A tag can have attributes without any value
unpairedTags: []
};
//const tagsPattern = new RegExp("<\\/?([\\w:\\-_\.]+)\\s*\/?>","g");
export function validate(xmlData, options) {
options = Object.assign({}, defaultOptions, options);
//xmlData = xmlData.replace(/(\r\n|\n|\r)/gm,"");//make it single line
//xmlData = xmlData.replace(/(^\s*<\?xml.*?\?>)/g,"");//Remove XML starting tag
//xmlData = xmlData.replace(/(<!DOCTYPE[\s\w\"\.\/\-\:]+(\[.*\])*\s*>)/g,"");//Remove DOCTYPE
const tags = [];
let tagFound = false;
//indicates that the root tag has been closed (aka. depth 0 has been reached)
let reachedRoot = false;
if (xmlData[0] === '\ufeff') {
// check for byte order mark (BOM)
xmlData = xmlData.substr(1);
}
for (let i = 0; i < xmlData.length; i++) {
if (xmlData[i] === '<' && xmlData[i + 1] === '?') {
i += 2;
i = readPI(xmlData, i);
if (i.err) return i;
} else if (xmlData[i] === '<') {
//starting of tag
//read until you reach to '>' avoiding any '>' in attribute value
let tagStartPos = i;
i++;
if (xmlData[i] === '!') {
i = readCommentAndCDATA(xmlData, i);
continue;
} else {
let closingTag = false;
if (xmlData[i] === '/') {
//closing tag
closingTag = true;
i++;
}
//read tagname
let tagName = '';
for (; i < xmlData.length &&
xmlData[i] !== '>' &&
xmlData[i] !== ' ' &&
xmlData[i] !== '\t' &&
xmlData[i] !== '\n' &&
xmlData[i] !== '\r'; i++
) {
tagName += xmlData[i];
}
tagName = tagName.trim();
//console.log(tagName);
if (tagName[tagName.length - 1] === '/') {
//self closing tag without attributes
tagName = tagName.substring(0, tagName.length - 1);
//continue;
i--;
}
if (!validateTagName(tagName)) {
let msg;
if (tagName.trim().length === 0) {
msg = "Invalid space after '<'.";
} else {
msg = "Tag '" + tagName + "' is an invalid name.";
}
return getErrorObject('InvalidTag', msg, getLineNumberForPosition(xmlData, i));
}
const result = readAttributeStr(xmlData, i);
if (result === false) {
return getErrorObject('InvalidAttr', "Attributes for '" + tagName + "' have open quote.", getLineNumberForPosition(xmlData, i));
}
let attrStr = result.value;
i = result.index;
if (attrStr[attrStr.length - 1] === '/') {
//self closing tag
const attrStrStart = i - attrStr.length;
attrStr = attrStr.substring(0, attrStr.length - 1);
const isValid = validateAttributeString(attrStr, options);
if (isValid === true) {
tagFound = true;
//continue; //text may presents after self closing tag
} else {
//the result from the nested function returns the position of the error within the attribute
//in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute
//this gives us the absolute index in the entire xml, which we can use to find the line at last
return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, attrStrStart + isValid.err.line));
}
} else if (closingTag) {
if (!result.tagClosed) {
return getErrorObject('InvalidTag', "Closing tag '" + tagName + "' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
} else if (attrStr.trim().length > 0) {
return getErrorObject('InvalidTag', "Closing tag '" + tagName + "' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
} else if (tags.length === 0) {
return getErrorObject('InvalidTag', "Closing tag '" + tagName + "' has not been opened.", getLineNumberForPosition(xmlData, tagStartPos));
} else {
const otg = tags.pop();
if (tagName !== otg.tagName) {
let openPos = getLineNumberForPosition(xmlData, otg.tagStartPos);
return getErrorObject('InvalidTag',
"Expected closing tag '" + otg.tagName + "' (opened in line " + openPos.line + ", col " + openPos.col + ") instead of closing tag '" + tagName + "'.",
getLineNumberForPosition(xmlData, tagStartPos));
}
//when there are no more tags, we reached the root level.
if (tags.length == 0) {
reachedRoot = true;
}
}
} else {
const isValid = validateAttributeString(attrStr, options);
if (isValid !== true) {
//the result from the nested function returns the position of the error within the attribute
//in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute
//this gives us the absolute index in the entire xml, which we can use to find the line at last
return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, i - attrStr.length + isValid.err.line));
}
//if the root level has been reached before ...
if (reachedRoot === true) {
return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
} else if (options.unpairedTags.indexOf(tagName) !== -1) {
//don't push into stack
} else {
tags.push({ tagName, tagStartPos });
}
tagFound = true;
}
//skip tag text value
//It may include comments and CDATA value
for (i++; i < xmlData.length; i++) {
if (xmlData[i] === '<') {
if (xmlData[i + 1] === '!') {
//comment or CADATA
i++;
i = readCommentAndCDATA(xmlData, i);
continue;
} else if (xmlData[i + 1] === '?') {
i = readPI(xmlData, ++i);
if (i.err) return i;
} else {
break;
}
} else if (xmlData[i] === '&') {
const afterAmp = validateAmpersand(xmlData, i);
if (afterAmp == -1)
return getErrorObject('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i));
i = afterAmp;
} else {
if (reachedRoot === true && !isWhiteSpace(xmlData[i])) {
return getErrorObject('InvalidXml', "Extra text at the end", getLineNumberForPosition(xmlData, i));
}
}
} //end of reading tag text value
if (xmlData[i] === '<') {
i--;
}
}
} else {
if (isWhiteSpace(xmlData[i])) {
continue;
}
return getErrorObject('InvalidChar', "char '" + xmlData[i] + "' is not expected.", getLineNumberForPosition(xmlData, i));
}
}
if (!tagFound) {
return getErrorObject('InvalidXml', 'Start tag expected.', 1);
} else if (tags.length == 1) {
return getErrorObject('InvalidTag', "Unclosed tag '" + tags[0].tagName + "'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
} else if (tags.length > 0) {
return getErrorObject('InvalidXml', "Invalid '" +
JSON.stringify(tags.map(t => t.tagName), null, 4).replace(/\r?\n/g, '') +
"' found.", { line: 1, col: 1 });
}
return true;
};
function isWhiteSpace(char) {
return char === ' ' || char === '\t' || char === '\n' || char === '\r';
}
/**
* Read Processing insstructions and skip
* @param {*} xmlData
* @param {*} i
*/
function readPI(xmlData, i) {
const start = i;
for (; i < xmlData.length; i++) {
if (xmlData[i] == '?' || xmlData[i] == ' ') {
//tagname
const tagname = xmlData.substr(start, i - start);
if (i > 5 && tagname === 'xml') {
return getErrorObject('InvalidXml', 'XML declaration allowed only at the start of the document.', getLineNumberForPosition(xmlData, i));
} else if (xmlData[i] == '?' && xmlData[i + 1] == '>') {
//check if valid attribut string
i++;
break;
} else {
continue;
}
}
}
return i;
}
function readCommentAndCDATA(xmlData, i) {
if (xmlData.length > i + 5 && xmlData[i + 1] === '-' && xmlData[i + 2] === '-') {
//comment
for (i += 3; i < xmlData.length; i++) {
if (xmlData[i] === '-' && xmlData[i + 1] === '-' && xmlData[i + 2] === '>') {
i += 2;
break;
}
}
} else if (
xmlData.length > i + 8 &&
xmlData[i + 1] === 'D' &&
xmlData[i + 2] === 'O' &&
xmlData[i + 3] === 'C' &&
xmlData[i + 4] === 'T' &&
xmlData[i + 5] === 'Y' &&
xmlData[i + 6] === 'P' &&
xmlData[i + 7] === 'E'
) {
let angleBracketsCount = 1;
for (i += 8; i < xmlData.length; i++) {
if (xmlData[i] === '<') {
angleBracketsCount++;
} else if (xmlData[i] === '>') {
angleBracketsCount--;
if (angleBracketsCount === 0) {
break;
}
}
}
} else if (
xmlData.length > i + 9 &&
xmlData[i + 1] === '[' &&
xmlData[i + 2] === 'C' &&
xmlData[i + 3] === 'D' &&
xmlData[i + 4] === 'A' &&
xmlData[i + 5] === 'T' &&
xmlData[i + 6] === 'A' &&
xmlData[i + 7] === '['
) {
for (i += 8; i < xmlData.length; i++) {
if (xmlData[i] === ']' && xmlData[i + 1] === ']' && xmlData[i + 2] === '>') {
i += 2;
break;
}
}
}
return i;
}
const doubleQuote = '"';
const singleQuote = "'";
/**
* Keep reading xmlData until '<' is found outside the attribute value.
* @param {string} xmlData
* @param {number} i
*/
function readAttributeStr(xmlData, i) {
let attrStr = '';
let startChar = '';
let tagClosed = false;
for (; i < xmlData.length; i++) {
if (xmlData[i] === doubleQuote || xmlData[i] === singleQuote) {
if (startChar === '') {
startChar = xmlData[i];
} else if (startChar !== xmlData[i]) {
//if vaue is enclosed with double quote then single quotes are allowed inside the value and vice versa
} else {
startChar = '';
}
} else if (xmlData[i] === '>') {
if (startChar === '') {
tagClosed = true;
break;
}
}
attrStr += xmlData[i];
}
if (startChar !== '') {
return false;
}
return {
value: attrStr,
index: i,
tagClosed: tagClosed
};
}
/**
* Select all the attributes whether valid or invalid.
*/
const validAttrStrRegxp = new RegExp('(\\s*)([^\\s=]+)(\\s*=)?(\\s*([\'"])(([\\s\\S])*?)\\5)?', 'g');
//attr, ="sd", a="amit's", a="sd"b="saf", ab cd=""
function validateAttributeString(attrStr, options) {
//console.log("start:"+attrStr+":end");
//if(attrStr.trim().length === 0) return true; //empty string
const matches = getAllMatches(attrStr, validAttrStrRegxp);
const attrNames = {};
for (let i = 0; i < matches.length; i++) {
if (matches[i][1].length === 0) {
//nospace before attribute name: a="sd"b="saf"
return getErrorObject('InvalidAttr', "Attribute '" + matches[i][2] + "' has no space in starting.", getPositionFromMatch(matches[i]))
} else if (matches[i][3] !== undefined && matches[i][4] === undefined) {
return getErrorObject('InvalidAttr', "Attribute '" + matches[i][2] + "' is without value.", getPositionFromMatch(matches[i]));
} else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {
//independent attribute: ab
return getErrorObject('InvalidAttr', "boolean attribute '" + matches[i][2] + "' is not allowed.", getPositionFromMatch(matches[i]));
}
/* else if(matches[i][6] === undefined){//attribute without value: ab=
return { err: { code:"InvalidAttr",msg:"attribute " + matches[i][2] + " has no value assigned."}};
} */
const attrName = matches[i][2];
if (!validateAttrName(attrName)) {
return getErrorObject('InvalidAttr', "Attribute '" + attrName + "' is an invalid name.", getPositionFromMatch(matches[i]));
}
if (!Object.prototype.hasOwnProperty.call(attrNames, attrName)) {
//check for duplicate attribute.
attrNames[attrName] = 1;
} else {
return getErrorObject('InvalidAttr', "Attribute '" + attrName + "' is repeated.", getPositionFromMatch(matches[i]));
}
}
return true;
}
function validateNumberAmpersand(xmlData, i) {
let re = /\d/;
if (xmlData[i] === 'x') {
i++;
re = /[\da-fA-F]/;
}
for (; i < xmlData.length; i++) {
if (xmlData[i] === ';')
return i;
if (!xmlData[i].match(re))
break;
}
return -1;
}
function validateAmpersand(xmlData, i) {
// https://www.w3.org/TR/xml/#dt-charref
i++;
if (xmlData[i] === ';')
return -1;
if (xmlData[i] === '#') {
i++;
return validateNumberAmpersand(xmlData, i);
}
let count = 0;
for (; i < xmlData.length; i++, count++) {
if (xmlData[i].match(/\w/) && count < 20)
continue;
if (xmlData[i] === ';')
break;
return -1;
}
return i;
}
function getErrorObject(code, message, lineNumber) {
return {
err: {
code: code,
msg: message,
line: lineNumber.line || lineNumber,
col: lineNumber.col,
},
};
}
function validateAttrName(attrName) {
return isName(attrName);
}
// const startsWithXML = /^xml/i;
function validateTagName(tagname) {
return isName(tagname) /* && !tagname.match(startsWithXML) */;
}
//this function returns the line number for the character at the given index
function getLineNumberForPosition(xmlData, index) {
const lines = xmlData.substring(0, index).split(/\r?\n/);
return {
line: lines.length,
// column number is last line's length + 1, because column numbering starts at 1:
col: lines[lines.length - 1].length + 1
};
}
//this function returns the position of the first character of match within attrStr
function getPositionFromMatch(match) {
return match.startIndex + match[1].length;
}

View File

@@ -0,0 +1,6 @@
// Re-export from fast-xml-builder for backward compatibility
import XMLBuilder from 'fast-xml-builder';
export default XMLBuilder;
// If there are any named exports you also want to re-export:
export * from 'fast-xml-builder';

View File

@@ -0,0 +1,410 @@
import { isName } from '../util.js';
export default class DocTypeReader {
constructor(options) {
this.suppressValidationErr = !options;
this.options = options;
}
readDocType(xmlData, i) {
const entities = Object.create(null);
let entityCount = 0;
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
if (this.options.enabled !== false &&
this.options.maxEntityCount != null &&
entityCount >= this.options.maxEntityCount) {
throw new Error(
`Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})`
);
}
//const escaped = entityName.replace(/[.\-+*:]/g, '\\.');
const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
entities[entityName] = {
regx: RegExp(`&${escaped};`, "g"),
val: val
};
entityCount++;
}
}
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--;
}
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`);
}
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
const startIndex = i;
while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") {
i++;
}
let entityName = xmlData.substring(startIndex, i);
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 != null &&
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];
}
readNotationExp(xmlData, i) {
// Skip leading whitespace after <!NOTATION
i = skipWhitespace(xmlData, i);
// Read notation name
const startIndex = i;
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
i++;
}
let notationName = xmlData.substring(startIndex, i);
!this.suppressValidationErr && validateEntityName(notationName);
// 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 };
}
readIdentifierVal(xmlData, i, type) {
let identifierVal = "";
const startChar = xmlData[i];
if (startChar !== '"' && startChar !== "'") {
throw new Error(`Expected quoted string, found "${startChar}"`);
}
i++;
const startIndex = i;
while (i < xmlData.length && xmlData[i] !== startChar) {
i++;
}
identifierVal = xmlData.substring(startIndex, i);
if (xmlData[i] !== startChar) {
throw new Error(`Unterminated ${type} value`);
}
i++;
return [i, identifierVal];
}
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
const startIndex = i;
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
i++;
}
let elementName = xmlData.substring(startIndex, i);
// Validate element name
if (!this.suppressValidationErr && !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
const startIndex = i;
while (i < xmlData.length && xmlData[i] !== ")") {
i++;
}
contentModel = xmlData.substring(startIndex, 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 startIndex = i;
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
i++;
}
let elementName = xmlData.substring(startIndex, i);
// Validate element name
validateEntityName(elementName)
// Skip whitespace after element name
i = skipWhitespace(xmlData, i);
// Read attribute name
startIndex = i;
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
i++;
}
let attributeName = xmlData.substring(startIndex, 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] !== ")") {
const startIndex = i;
while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") {
i++;
}
let notation = xmlData.substring(startIndex, 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.)
const startIndex = i;
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
i++;
}
attributeType += xmlData.substring(startIndex, 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
}
}
}
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 validateEntityName(name) {
if (isName(name))
return name;
else
throw new Error(`Invalid entity name ${name}`);
}

View File

@@ -0,0 +1,159 @@
import { DANGEROUS_PROPERTY_NAMES, criticalProperties } from "../util.js";
const defaultOnDangerousProperty = (name) => {
if (DANGEROUS_PROPERTY_NAMES.includes(name)) {
return "__" + name;
}
return name;
};
export 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
captureMetaData: false,
maxNestedTags: 100,
strictReservedNames: true,
jPath: true, // if true, pass jPath string to callbacks; if false, pass matcher instance
onDangerousProperty: defaultOnDangerousProperty
};
/**
* Validates that a property name is safe to use
* @param {string} propertyName - The property name to validate
* @param {string} optionName - The option field name (for error message)
* @throws {Error} If property name is dangerous
*/
function validatePropertyName(propertyName, optionName) {
if (typeof propertyName !== 'string') {
return; // Only validate string property names
}
const normalized = propertyName.toLowerCase();
if (DANGEROUS_PROPERTY_NAMES.some(dangerous => normalized === dangerous.toLowerCase())) {
throw new Error(
`[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution`
);
}
if (criticalProperties.some(dangerous => normalized === dangerous.toLowerCase())) {
throw new Error(
`[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution`
);
}
}
/**
* 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,
maxEntityCount: 100,
allowedTags: null,
tagFilter: null
};
}
// Object config - merge with defaults
if (typeof value === 'object' && value !== null) {
return {
enabled: value.enabled !== false,
maxEntitySize: Math.max(1, value.maxEntitySize ?? 10000),
maxExpansionDepth: Math.max(1, value.maxExpansionDepth ?? 10),
maxTotalExpansions: Math.max(1, value.maxTotalExpansions ?? 1000),
maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 100000),
maxEntityCount: Math.max(1, value.maxEntityCount ?? 100),
allowedTags: value.allowedTags ?? null,
tagFilter: value.tagFilter ?? null
};
}
// Default to enabled with limits
return normalizeProcessEntities(true);
}
export const buildOptions = function (options) {
const built = Object.assign({}, defaultOptions, options);
// Validate property names to prevent prototype pollution
const propertyNameOptions = [
{ value: built.attributeNamePrefix, name: 'attributeNamePrefix' },
{ value: built.attributesGroupName, name: 'attributesGroupName' },
{ value: built.textNodeName, name: 'textNodeName' },
{ value: built.cdataPropName, name: 'cdataPropName' },
{ value: built.commentPropName, name: 'commentPropName' }
];
for (const { value, name } of propertyNameOptions) {
if (value) {
validatePropertyName(value, name);
}
}
if (built.onDangerousProperty === null) {
built.onDangerousProperty = defaultOnDangerousProperty;
}
// Always normalize processEntities for backward compatibility and validation
built.processEntities = normalizeProcessEntities(built.processEntities);
// Convert old-style stopNodes for backward compatibility
if (built.stopNodes && Array.isArray(built.stopNodes)) {
built.stopNodes = built.stopNodes.map(node => {
if (typeof node === 'string' && node.startsWith('*.')) {
// Old syntax: *.tagname meant "tagname anywhere"
// Convert to new syntax: ..tagname
return '..' + node.substring(2);
}
return node;
});
}
//console.debug(built.processEntities)
return built;
};

View File

@@ -0,0 +1,911 @@
'use strict';
///@ts-check
import { getAllMatches, isExist, DANGEROUS_PROPERTY_NAMES, criticalProperties } from '../util.js';
import xmlNode from './xmlNode.js';
import DocTypeReader from './DocTypeReader.js';
import toNumber from "strnum";
import getIgnoreAttributesFn from "../ignoreAttributes.js";
import { Expression, Matcher } from 'path-expression-matcher';
// const regx =
// '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
// .replace(/NAME/g, util.nameRegexp);
//const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g");
//const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g");
// Helper functions for attribute and namespace handling
/**
* Extract raw attributes (without prefix) from prefixed attribute map
* @param {object} prefixedAttrs - Attributes with prefix from buildAttributesMap
* @param {object} options - Parser options containing attributeNamePrefix
* @returns {object} Raw attributes for matcher
*/
function extractRawAttributes(prefixedAttrs, options) {
if (!prefixedAttrs) return {};
// Handle attributesGroupName option
const attrs = options.attributesGroupName
? prefixedAttrs[options.attributesGroupName]
: prefixedAttrs;
if (!attrs) return {};
const rawAttrs = {};
for (const key in attrs) {
// Remove the attribute prefix to get raw name
if (key.startsWith(options.attributeNamePrefix)) {
const rawName = key.substring(options.attributeNamePrefix.length);
rawAttrs[rawName] = attrs[key];
} else {
// Attribute without prefix (shouldn't normally happen, but be safe)
rawAttrs[key] = attrs[key];
}
}
return rawAttrs;
}
/**
* Extract namespace from raw tag name
* @param {string} rawTagName - Tag name possibly with namespace (e.g., "soap:Envelope")
* @returns {string|undefined} Namespace or undefined
*/
function extractNamespace(rawTagName) {
if (!rawTagName || typeof rawTagName !== 'string') return undefined;
const colonIndex = rawTagName.indexOf(':');
if (colonIndex !== -1 && colonIndex > 0) {
const ns = rawTagName.substring(0, colonIndex);
// Don't treat xmlns as a namespace
if (ns !== 'xmlns') {
return ns;
}
}
return undefined;
}
export default class OrderedObjParser {
constructor(options) {
this.options = options;
this.currentNode = null;
this.tagsNodeStack = [];
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: "\"" },
};
this.ampEntity = { regex: /&(amp|#38|#x26);/g, val: "&" };
this.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) => fromCodePoint(str, 10, "&#") },
"num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => fromCodePoint(str, 16, "&#x") },
};
this.addExternalEntities = addExternalEntities;
this.parseXml = parseXml;
this.parseTextData = parseTextData;
this.resolveNameSpace = resolveNameSpace;
this.buildAttributesMap = buildAttributesMap;
this.isItStopNode = isItStopNode;
this.replaceEntitiesValue = replaceEntitiesValue;
this.readStopNodeData = readStopNodeData;
this.saveTextToParentTag = saveTextToParentTag;
this.addChild = addChild;
this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes)
this.entityExpansionCount = 0;
this.currentExpandedLength = 0;
// Initialize path matcher for path-expression-matcher
this.matcher = new Matcher();
// Live read-only proxy of matcher — PEM creates and caches this internally.
// All user callbacks receive this instead of the mutable matcher.
this.readonlyMatcher = this.matcher.readOnly();
// Flag to track if current node is a stop node (optimization)
this.isCurrentNodeStopNode = false;
// Pre-compile stopNodes expressions
if (this.options.stopNodes && this.options.stopNodes.length > 0) {
this.stopNodeExpressions = [];
for (let i = 0; i < this.options.stopNodes.length; i++) {
const stopNodeExp = this.options.stopNodes[i];
if (typeof stopNodeExp === 'string') {
// Convert string to Expression object
this.stopNodeExpressions.push(new Expression(stopNodeExp));
} else if (stopNodeExp instanceof Expression) {
// Already an Expression object
this.stopNodeExpressions.push(stopNodeExp);
}
}
}
}
}
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("&" + escaped + ";", "g"),
val: externalEntities[ent]
}
}
}
/**
* @param {string} val
* @param {string} tagName
* @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath
* @param {boolean} dontTrim
* @param {boolean} hasAttributes
* @param {boolean} isLeafNode
* @param {boolean} escapeEntities
*/
function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) {
if (val !== undefined) {
if (this.options.trimValues && !dontTrim) {
val = val.trim();
}
if (val.length > 0) {
if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath);
// Pass jPath string or matcher based on options.jPath setting
const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
const newval = this.options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode);
if (newval === null || newval === undefined) {
//don't parse
return val;
} else if (typeof newval !== typeof val || newval !== val) {
//overwrite
return newval;
} else if (this.options.trimValues) {
return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
} else {
const trimmedVal = val.trim();
if (trimmedVal === val) {
return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
} else {
return val;
}
}
}
}
}
function resolveNameSpace(tagname) {
if (this.options.removeNSPrefix) {
const tags = tagname.split(':');
const prefix = tagname.charAt(0) === '/' ? '/' : '';
if (tags[0] === 'xmlns') {
return '';
}
if (tags.length === 2) {
tagname = prefix + tags[1];
}
}
return tagname;
}
//TODO: change regex to capture NS
//const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm");
const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm');
function buildAttributesMap(attrStr, jPath, tagName) {
if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') {
// attrStr = attrStr.replace(/\r?\n/g, ' ');
//attrStr = attrStr || attrStr.trim();
const matches = getAllMatches(attrStr, attrsRegx);
const len = matches.length; //don't make it inline
const attrs = {};
// First pass: parse all attributes and update matcher with raw values
// This ensures the matcher has all attribute values when processors run
const rawAttrsForMatcher = {};
for (let i = 0; i < len; i++) {
const attrName = this.resolveNameSpace(matches[i][1]);
const oldVal = matches[i][4];
if (attrName.length && oldVal !== undefined) {
let parsedVal = oldVal;
if (this.options.trimValues) {
parsedVal = parsedVal.trim();
}
parsedVal = this.replaceEntitiesValue(parsedVal, tagName, this.readonlyMatcher);
rawAttrsForMatcher[attrName] = parsedVal;
}
}
// Update matcher with raw attribute values BEFORE running processors
if (Object.keys(rawAttrsForMatcher).length > 0 && typeof jPath === 'object' && jPath.updateCurrent) {
jPath.updateCurrent(rawAttrsForMatcher);
}
// Second pass: now process attributes with matcher having full attribute context
for (let i = 0; i < len; i++) {
const attrName = this.resolveNameSpace(matches[i][1]);
// Convert jPath to string if needed for ignoreAttributesFn
const jPathStr = this.options.jPath ? jPath.toString() : this.readonlyMatcher;
if (this.ignoreAttributesFn(attrName, jPathStr)) {
continue
}
let oldVal = matches[i][4];
let aName = this.options.attributeNamePrefix + attrName;
if (attrName.length) {
if (this.options.transformAttributeName) {
aName = this.options.transformAttributeName(aName);
}
//if (aName === "__proto__") aName = "#__proto__";
aName = sanitizeName(aName, this.options);
if (oldVal !== undefined) {
if (this.options.trimValues) {
oldVal = oldVal.trim();
}
oldVal = this.replaceEntitiesValue(oldVal, tagName, this.readonlyMatcher);
// Pass jPath string or readonlyMatcher based on options.jPath setting
const jPathOrMatcher = this.options.jPath ? jPath.toString() : this.readonlyMatcher;
const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPathOrMatcher);
if (newVal === null || newVal === undefined) {
//don't parse
attrs[aName] = oldVal;
} else if (typeof newVal !== typeof oldVal || newVal !== oldVal) {
//overwrite
attrs[aName] = newVal;
} else {
//parse
attrs[aName] = parseValue(
oldVal,
this.options.parseAttributeValue,
this.options.numberParseOptions
);
}
} else if (this.options.allowBooleanAttributes) {
attrs[aName] = true;
}
}
}
if (!Object.keys(attrs).length) {
return;
}
if (this.options.attributesGroupName) {
const attrCollection = {};
attrCollection[this.options.attributesGroupName] = attrs;
return attrCollection;
}
return attrs
}
}
const parseXml = function (xmlData) {
xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line
const xmlObj = new xmlNode('!xml');
let currentNode = xmlObj;
let textData = "";
// Reset matcher for new document
this.matcher.reset();
// 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 === '<') {
// const nextIndex = i+1;
// const _2ndChar = xmlData[nextIndex];
if (xmlData[i + 1] === '/') {//Closing Tag
const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.")
let tagName = xmlData.substring(i + 2, closeIndex).trim();
if (this.options.removeNSPrefix) {
const colonIndex = tagName.indexOf(":");
if (colonIndex !== -1) {
tagName = tagName.substr(colonIndex + 1);
}
}
tagName = transformTagName(this.options.transformTagName, tagName, "", this.options).tagName;
if (currentNode) {
textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
}
//check if last tag of nested tag was unpaired tag
const lastTagName = this.matcher.getCurrentTag();
if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) {
throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`);
}
if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) {
// Pop the unpaired tag
this.matcher.pop();
this.tagsNodeStack.pop();
}
// Pop the closing tag
this.matcher.pop();
this.isCurrentNodeStopNode = false; // Reset flag when closing tag
currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope
textData = "";
i = closeIndex;
} else if (xmlData[i + 1] === '?') {
let tagData = readTagExp(xmlData, i, false, "?>");
if (!tagData) throw new Error("Pi Tag is not closed.");
textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) {
//do nothing
} else {
const childNode = new xmlNode(tagData.tagName);
childNode.add(this.options.textNodeName, "");
if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName);
}
this.addChild(currentNode, childNode, this.readonlyMatcher, i);
}
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) {
const comment = xmlData.substring(i + 4, endIndex - 2);
textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]);
}
i = endIndex;
} 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) === '![') {
const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
const tagExp = xmlData.substring(i + 9, closeIndex);
textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
let val = this.parseTextData(tagExp, currentNode.tagname, this.readonlyMatcher, true, false, true, true);
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 {
currentNode.add(this.options.textNodeName, val);
}
i = closeIndex + 2;
} else {//Opening tag
let result = readTagExp(xmlData, i, this.options.removeNSPrefix);
// Safety check: readTagExp can return undefined
if (!result) {
// Log context for debugging
const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlData.length, i + 50));
throw new Error(`readTagExp returned undefined at position ${i}. Context: "${context}"`);
}
let tagName = result.tagName;
const rawTagName = result.rawTagName;
let tagExp = result.tagExp;
let attrExpPresent = result.attrExpPresent;
let closeIndex = result.closeIndex;
({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options));
if (this.options.strictReservedNames &&
(tagName === this.options.commentPropName
|| tagName === this.options.cdataPropName
|| tagName === this.options.textNodeName
|| tagName === this.options.attributesGroupName
)) {
throw new Error(`Invalid tag name: ${tagName}`);
}
//save text as child node
if (currentNode && textData) {
if (currentNode.tagname !== '!xml') {
//when nested tag is found
textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher, false);
}
}
//check if last tag was unpaired tag
const lastTag = currentNode;
if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) {
currentNode = this.tagsNodeStack.pop();
this.matcher.pop();
}
// Clean up self-closing syntax BEFORE processing attributes
// This is where tagExp gets the trailing / removed
let isSelfClosing = false;
if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
isSelfClosing = true;
if (tagName[tagName.length - 1] === "/") {
tagName = tagName.substr(0, tagName.length - 1);
tagExp = tagName;
} else {
tagExp = tagExp.substr(0, tagExp.length - 1);
}
// Re-check attrExpPresent after cleaning
attrExpPresent = (tagName !== tagExp);
}
// Now process attributes with CLEAN tagExp (no trailing /)
let prefixedAttrs = null;
let rawAttrs = {};
let namespace = undefined;
// Extract namespace from rawTagName
namespace = extractNamespace(rawTagName);
// Push tag to matcher FIRST (with empty attrs for now) so callbacks see correct path
if (tagName !== xmlObj.tagname) {
this.matcher.push(tagName, {}, namespace);
}
// Now build attributes - callbacks will see correct matcher state
if (tagName !== tagExp && attrExpPresent) {
// Build attributes (returns prefixed attributes for the tree)
// Note: buildAttributesMap now internally updates the matcher with raw attributes
prefixedAttrs = this.buildAttributesMap(tagExp, this.matcher, tagName);
if (prefixedAttrs) {
// Extract raw attributes (without prefix) for our use
rawAttrs = extractRawAttributes(prefixedAttrs, this.options);
}
}
// Now check if this is a stop node (after attributes are set)
if (tagName !== xmlObj.tagname) {
this.isCurrentNodeStopNode = this.isItStopNode(this.stopNodeExpressions, this.matcher);
}
const startIndex = i;
if (this.isCurrentNodeStopNode) {
let tagContent = "";
// For self-closing tags, content is empty
if (isSelfClosing) {
i = result.closeIndex;
}
//unpaired tag
else if (this.options.unpairedTags.indexOf(tagName) !== -1) {
i = result.closeIndex;
}
//normal tag
else {
//read until closing tag is found
const result = this.readStopNodeData(xmlData, rawTagName, closeIndex + 1);
if (!result) throw new Error(`Unexpected end of ${rawTagName}`);
i = result.i;
tagContent = result.tagContent;
}
const childNode = new xmlNode(tagName);
if (prefixedAttrs) {
childNode[":@"] = prefixedAttrs;
}
// For stop nodes, store raw content as-is without any processing
childNode.add(this.options.textNodeName, tagContent);
this.matcher.pop(); // Pop the stop node tag
this.isCurrentNodeStopNode = false; // Reset flag
this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
} else {
//selfClosing tag
if (isSelfClosing) {
({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options));
const childNode = new xmlNode(tagName);
if (prefixedAttrs) {
childNode[":@"] = prefixedAttrs;
}
this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
this.matcher.pop(); // Pop self-closing tag
this.isCurrentNodeStopNode = false; // Reset flag
}
else if (this.options.unpairedTags.indexOf(tagName) !== -1) {//unpaired tag
const childNode = new xmlNode(tagName);
if (prefixedAttrs) {
childNode[":@"] = prefixedAttrs;
}
this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
this.matcher.pop(); // Pop unpaired tag
this.isCurrentNodeStopNode = false; // Reset flag
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 (prefixedAttrs) {
childNode[":@"] = prefixedAttrs;
}
this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
currentNode = childNode;
}
textData = "";
i = closeIndex;
}
}
} else {
textData += xmlData[i];
}
}
return xmlObj.child;
}
function addChild(currentNode, childNode, matcher, startIndex) {
// unset startIndex if not requested
if (!this.options.captureMetaData) startIndex = undefined;
// Pass jPath string or matcher based on options.jPath setting
const jPathOrMatcher = this.options.jPath ? matcher.toString() : matcher;
const result = this.options.updateTag(childNode.tagname, jPathOrMatcher, childNode[":@"])
if (result === false) {
//do nothing
} else if (typeof result === "string") {
childNode.tagname = result
currentNode.addChild(childNode, startIndex);
} else {
currentNode.addChild(childNode, startIndex);
}
}
/**
* @param {object} val - Entity object with regex and val properties
* @param {string} tagName - Tag name
* @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath
*/
function replaceEntitiesValue(val, tagName, jPath) {
const entityConfig = this.options.processEntities;
if (!entityConfig || !entityConfig.enabled) {
return val;
}
// Check if tag is allowed to contain entities
if (entityConfig.allowedTags) {
const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
const allowed = Array.isArray(entityConfig.allowedTags)
? entityConfig.allowedTags.includes(tagName)
: entityConfig.allowedTags(tagName, jPathOrMatcher);
if (!allowed) {
return val;
}
}
// Apply custom tag filter if provided
if (entityConfig.tagFilter) {
const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
if (!entityConfig.tagFilter(tagName, jPathOrMatcher)) {
return val; // Skip based on custom filter
}
}
// Replace DOCTYPE entities
for (const entityName of Object.keys(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}`
);
}
}
}
}
// Replace standard entities
for (const entityName of Object.keys(this.lastEntities)) {
const entity = this.lastEntities[entityName];
const matches = val.match(entity.regex);
if (matches) {
this.entityExpansionCount += matches.length;
if (entityConfig.maxTotalExpansions &&
this.entityExpansionCount > entityConfig.maxTotalExpansions) {
throw new Error(
`Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
);
}
}
val = val.replace(entity.regex, entity.val);
}
if (val.indexOf('&') === -1) return val;
// Replace HTML entities if enabled
if (this.options.htmlEntities) {
for (const entityName of Object.keys(this.htmlEntities)) {
const entity = this.htmlEntities[entityName];
const matches = val.match(entity.regex);
if (matches) {
//console.log(matches);
this.entityExpansionCount += matches.length;
if (entityConfig.maxTotalExpansions &&
this.entityExpansionCount > entityConfig.maxTotalExpansions) {
throw new Error(
`Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
);
}
}
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, parentNode, matcher, isLeafNode) {
if (textData) { //store previously collected data as textNode
if (isLeafNode === undefined) isLeafNode = parentNode.child.length === 0
textData = this.parseTextData(textData,
parentNode.tagname,
matcher,
false,
parentNode[":@"] ? Object.keys(parentNode[":@"]).length !== 0 : false,
isLeafNode);
if (textData !== undefined && textData !== "")
parentNode.add(this.options.textNodeName, textData);
textData = "";
}
return textData;
}
//TODO: use jPath to simplify the logic
/**
* @param {Array<Expression>} stopNodeExpressions - Array of compiled Expression objects
* @param {Matcher} matcher - Current path matcher
*/
function isItStopNode(stopNodeExpressions, matcher) {
if (!stopNodeExpressions || stopNodeExpressions.length === 0) return false;
for (let i = 0; i < stopNodeExpressions.length; i++) {
if (matcher.matches(stopNodeExpressions[i])) {
return true;
}
}
return false;
}
/**
* Returns the tag Expression and where it is ending handling single-double quotes situation
* @param {string} xmlData
* @param {number} i starting index
* @returns
*/
function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
let attrBoundary;
let tagExp = "";
for (let index = i; index < xmlData.length; index++) {
let ch = xmlData[index];
if (attrBoundary) {
if (ch === attrBoundary) attrBoundary = "";//reset
} else if (ch === '"' || ch === "'") {
attrBoundary = ch;
} else if (ch === closingChar[0]) {
if (closingChar[1]) {
if (xmlData[index + 1] === closingChar[1]) {
return {
data: tagExp,
index: index
}
}
} else {
return {
data: tagExp,
index: index
}
}
} else if (ch === '\t') {
ch = " "
}
tagExp += ch;
}
}
function findClosingIndex(xmlData, str, i, errMsg) {
const closingIndex = xmlData.indexOf(str, i);
if (closingIndex === -1) {
throw new Error(errMsg)
} else {
return closingIndex + str.length - 1;
}
}
function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") {
const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar);
if (!result) return;
let tagExp = result.data;
const closeIndex = result.index;
const separatorIndex = tagExp.search(/\s/);
let tagName = tagExp;
let attrExpPresent = true;
if (separatorIndex !== -1) {//separate tag name and attributes expression
tagName = tagExp.substring(0, separatorIndex);
tagExp = tagExp.substring(separatorIndex + 1).trimStart();
}
const rawTagName = tagName;
if (removeNSPrefix) {
const colonIndex = tagName.indexOf(":");
if (colonIndex !== -1) {
tagName = tagName.substr(colonIndex + 1);
attrExpPresent = tagName !== result.data.substr(colonIndex + 1);
}
}
return {
tagName: tagName,
tagExp: tagExp,
closeIndex: closeIndex,
attrExpPresent: attrExpPresent,
rawTagName: rawTagName,
}
}
/**
* find paired tag for a stop node
* @param {string} xmlData
* @param {string} tagName
* @param {number} i
*/
function readStopNodeData(xmlData, tagName, i) {
const startIndex = i;
// Starting at 1 since we already have an open tag
let openTagCount = 1;
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
}
}
}
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;
}
}
}
}//end for loop
}
function parseValue(val, shouldParse, options) {
if (shouldParse && typeof val === 'string') {
//console.log(options)
const newval = val.trim();
if (newval === 'true') return true;
else if (newval === 'false') return false;
else return toNumber(val, options);
} else {
if (isExist(val)) {
return val;
} else {
return '';
}
}
}
function fromCodePoint(str, base, prefix) {
const codePoint = Number.parseInt(str, base);
if (codePoint >= 0 && codePoint <= 0x10FFFF) {
return String.fromCodePoint(codePoint);
} else {
return prefix + str + ";";
}
}
function transformTagName(fn, tagName, tagExp, options) {
if (fn) {
const newTagName = fn(tagName);
if (tagExp === tagName) {
tagExp = newTagName
}
tagName = newTagName;
}
tagName = sanitizeName(tagName, options);
return { tagName, tagExp };
}
function sanitizeName(name, options) {
if (criticalProperties.includes(name)) {
throw new Error(`[SECURITY] Invalid name: "${name}" is a reserved JavaScript keyword that could cause prototype pollution`);
} else if (DANGEROUS_PROPERTY_NAMES.includes(name)) {
return options.onDangerousProperty(name);
}
return name;
}

View File

@@ -0,0 +1,71 @@
import { buildOptions } from './OptionsBuilder.js';
import OrderedObjParser from './OrderedObjParser.js';
import prettify from './node2json.js';
import { validate } from "../validator.js";
import XmlNode from './xmlNode.js';
export default class XMLParser {
constructor(options) {
this.externalEntities = {};
this.options = buildOptions(options);
}
/**
* Parse XML dats to JS object
* @param {string|Uint8Array} xmlData
* @param {boolean|Object} validationOption
*/
parse(xmlData, validationOption) {
if (typeof xmlData !== "string" && xmlData.toString) {
xmlData = xmlData.toString();
} else if (typeof xmlData !== "string") {
throw new Error("XML data is accepted in String or Bytes[] form.")
}
if (validationOption) {
if (validationOption === true) validationOption = {}; //validate with default options
const result = validate(xmlData, validationOption);
if (result !== true) {
throw Error(`${result.err.msg}:${result.err.line}:${result.err.col}`)
}
}
const orderedObjParser = new OrderedObjParser(this.options);
orderedObjParser.addExternalEntities(this.externalEntities);
const orderedResult = orderedObjParser.parseXml(xmlData);
if (this.options.preserveOrder || orderedResult === undefined) return orderedResult;
else return prettify(orderedResult, this.options, orderedObjParser.matcher, orderedObjParser.readonlyMatcher);
}
/**
* Add Entity which is not by default supported by this library
* @param {string} key
* @param {string} value
*/
addEntity(key, value) {
if (value.indexOf("&") !== -1) {
throw new Error("Entity value can't have '&'")
} else if (key.indexOf("&") !== -1 || key.indexOf(";") !== -1) {
throw new Error("An entity must be set without '&' and ';'. Eg. use '#xD' for '&#xD;'")
} else if (value === "&") {
throw new Error("An entity with value '&' is not permitted");
} else {
this.externalEntities[key] = value;
}
}
/**
* Returns a Symbol that can be used to access the metadata
* property on a node.
*
* If Symbol is not available in the environment, an ordinary property is used
* and the name of the property is here returned.
*
* The XMLMetaData property is only present when `captureMetaData`
* is true in the options.
*/
static getMetaDataSymbol() {
return XmlNode.getMetaDataSymbol();
}
}

173
node_modules/fast-xml-parser/src/xmlparser/node2json.js generated vendored Normal file
View File

@@ -0,0 +1,173 @@
'use strict';
import XmlNode from './xmlNode.js';
import { Matcher } from 'path-expression-matcher';
const METADATA_SYMBOL = XmlNode.getMetaDataSymbol();
/**
* Helper function to strip attribute prefix from attribute map
* @param {object} attrs - Attributes with prefix (e.g., {"@_class": "code"})
* @param {string} prefix - Attribute prefix to remove (e.g., "@_")
* @returns {object} Attributes without prefix (e.g., {"class": "code"})
*/
function stripAttributePrefix(attrs, prefix) {
if (!attrs || typeof attrs !== 'object') return {};
if (!prefix) return attrs;
const rawAttrs = {};
for (const key in attrs) {
if (key.startsWith(prefix)) {
const rawName = key.substring(prefix.length);
rawAttrs[rawName] = attrs[key];
} else {
// Attribute without prefix (shouldn't normally happen, but be safe)
rawAttrs[key] = attrs[key];
}
}
return rawAttrs;
}
/**
*
* @param {array} node
* @param {any} options
* @param {Matcher} matcher - Path matcher instance
* @returns
*/
export default function prettify(node, options, matcher, readonlyMatcher) {
return compress(node, options, matcher, readonlyMatcher);
}
/**
* @param {array} arr
* @param {object} options
* @param {Matcher} matcher - Path matcher instance
* @returns object
*/
function compress(arr, options, matcher, readonlyMatcher) {
let text;
const compressedObj = {}; //This is intended to be a plain object
for (let i = 0; i < arr.length; i++) {
const tagObj = arr[i];
const property = propName(tagObj);
// Push current property to matcher WITH RAW ATTRIBUTES (no prefix)
if (property !== undefined && property !== options.textNodeName) {
const rawAttrs = stripAttributePrefix(
tagObj[":@"] || {},
options.attributeNamePrefix
);
matcher.push(property, rawAttrs);
}
if (property === options.textNodeName) {
if (text === undefined) text = tagObj[property];
else text += "" + tagObj[property];
} else if (property === undefined) {
continue;
} else if (tagObj[property]) {
let val = compress(tagObj[property], options, matcher, readonlyMatcher);
const isLeaf = isLeafTag(val, options);
if (tagObj[":@"]) {
assignAttributes(val, tagObj[":@"], readonlyMatcher, options);
} else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) {
val = val[options.textNodeName];
} else if (Object.keys(val).length === 0) {
if (options.alwaysCreateTextNode) val[options.textNodeName] = "";
else val = "";
}
if (tagObj[METADATA_SYMBOL] !== undefined && typeof val === "object" && val !== null) {
val[METADATA_SYMBOL] = tagObj[METADATA_SYMBOL]; // copy over metadata
}
if (compressedObj[property] !== undefined && Object.prototype.hasOwnProperty.call(compressedObj, property)) {
if (!Array.isArray(compressedObj[property])) {
compressedObj[property] = [compressedObj[property]];
}
compressedObj[property].push(val);
} else {
//TODO: if a node is not an array, then check if it should be an array
//also determine if it is a leaf node
// Pass jPath string or readonlyMatcher based on options.jPath setting
const jPathOrMatcher = options.jPath ? readonlyMatcher.toString() : readonlyMatcher;
if (options.isArray(property, jPathOrMatcher, isLeaf)) {
compressedObj[property] = [val];
} else {
compressedObj[property] = val;
}
}
// Pop property from matcher after processing
if (property !== undefined && property !== options.textNodeName) {
matcher.pop();
}
}
}
// if(text && text.length > 0) compressedObj[options.textNodeName] = text;
if (typeof text === "string") {
if (text.length > 0) compressedObj[options.textNodeName] = text;
} else if (text !== undefined) compressedObj[options.textNodeName] = text;
return compressedObj;
}
function propName(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key !== ":@") return key;
}
}
function assignAttributes(obj, attrMap, readonlyMatcher, options) {
if (attrMap) {
const keys = Object.keys(attrMap);
const len = keys.length; //don't make it inline
for (let i = 0; i < len; i++) {
const atrrName = keys[i]; // This is the PREFIXED name (e.g., "@_class")
// Strip prefix for matcher path (for isArray callback)
const rawAttrName = atrrName.startsWith(options.attributeNamePrefix)
? atrrName.substring(options.attributeNamePrefix.length)
: atrrName;
// For attributes, we need to create a temporary path
// Pass jPath string or matcher based on options.jPath setting
const jPathOrMatcher = options.jPath
? readonlyMatcher.toString() + "." + rawAttrName
: readonlyMatcher;
if (options.isArray(atrrName, jPathOrMatcher, true, true)) {
obj[atrrName] = [attrMap[atrrName]];
} else {
obj[atrrName] = attrMap[atrrName];
}
}
}
}
function isLeafTag(obj, options) {
const { textNodeName } = options;
const propCount = Object.keys(obj).length;
if (propCount === 0) {
return true;
}
if (
propCount === 1 &&
(obj[textNodeName] || typeof obj[textNodeName] === "boolean" || obj[textNodeName] === 0)
) {
return true;
}
return false;
}

40
node_modules/fast-xml-parser/src/xmlparser/xmlNode.js generated vendored Normal file
View File

@@ -0,0 +1,40 @@
'use strict';
let METADATA_SYMBOL;
if (typeof Symbol !== "function") {
METADATA_SYMBOL = "@@xmlMetadata";
} else {
METADATA_SYMBOL = Symbol("XML Node Metadata");
}
export default class XmlNode {
constructor(tagname) {
this.tagname = tagname;
this.child = []; //nested tags, text, cdata, comments in order
this[":@"] = Object.create(null); //attributes map
}
add(key, val) {
// this.child.push( {name : key, val: val, isCdata: isCdata });
if (key === "__proto__") key = "#__proto__";
this.child.push({ [key]: val });
}
addChild(node, startIndex) {
if (node.tagname === "__proto__") node.tagname = "#__proto__";
if (node[":@"] && Object.keys(node[":@"]).length > 0) {
this.child.push({ [node.tagname]: node.child, [":@"]: node[":@"] });
} else {
this.child.push({ [node.tagname]: node.child });
}
// if requested, add the startIndex
if (startIndex !== undefined) {
// Note: for now we just overwrite the metadata. If we had more complex metadata,
// we might need to do an object append here: metadata = { ...metadata, startIndex }
this.child[this.child.length - 1][METADATA_SYMBOL] = { startIndex };
}
}
/** symbol used for metadata */
static getMetaDataSymbol() {
return METADATA_SYMBOL;
}
}