Update node modules

This commit is contained in:
Stanley Goldman
2019-12-13 09:19:15 -05:00
parent 9e7ce49a73
commit dc257a3a4f
85 changed files with 4949 additions and 6699 deletions

View File

@ -12,12 +12,16 @@ const convertToJson = function(node, options) {
//otherwise create a textnode if node has some text
if (util.isExist(node.val)) {
if (!(typeof node.val === 'string' && (node.val === '' || node.val === options.cdataPositionChar))) {
jObj[options.textNodeName] = node.val;
if(options.arrayMode === "strict"){
jObj[options.textNodeName] = [ node.val ];
}else{
jObj[options.textNodeName] = node.val;
}
}
}
}
util.merge(jObj, node.attrsMap);
util.merge(jObj, node.attrsMap, options.arrayMode);
const keys = Object.keys(node.child);
for (let index = 0; index < keys.length; index++) {
@ -28,7 +32,17 @@ const convertToJson = function(node, options) {
jObj[tagname].push(convertToJson(node.child[tagname][tag], options));
}
} else {
jObj[tagname] = convertToJson(node.child[tagname][0], options);
if(options.arrayMode === true){
const result = convertToJson(node.child[tagname][0], options)
if(typeof result === 'object')
jObj[tagname] = [ result ];
else
jObj[tagname] = result;
}else if(options.arrayMode === "strict"){
jObj[tagname] = [convertToJson(node.child[tagname][0], options) ];
}else{
jObj[tagname] = convertToJson(node.child[tagname][0], options);
}
}
}

View File

@ -7,17 +7,22 @@ type X2jOptions = {
allowBooleanAttributes: boolean;
parseNodeValue: boolean;
parseAttributeValue: boolean;
arrayMode: boolean;
arrayMode: boolean | 'strict';
trimValues: boolean;
cdataTagName: false | string;
cdataPositionChar: string;
localeRange: string;
parseTrueNumberOnly: boolean;
tagValueProcessor: (tagValue: string) => string;
attrValueProcessor: (attrValue: string) => string;
tagValueProcessor: (tagValue: string, tagName: string) => string;
attrValueProcessor: (attrValue: string, attrName: string) => string;
stopNodes: string[];
};
type X2jOptionsOptional = Partial<X2jOptions>;
type validationOptions = {
allowBooleanAttributes: boolean;
localeRange: string;
};
type validationOptionsOptional = Partial<validationOptions>;
type J2xOptions = {
attributeNamePrefix: string;
attrNodeName: false | string;
@ -39,7 +44,7 @@ type ValidationError = {
err: { code: string; msg: string };
};
export function parse(xmlData: string, options?: X2jOptionsOptional): any;
export function parse(xmlData: string, options?: X2jOptionsOptional, validationOptions?: validationOptionsOptional | boolean): any;
export function convert2nimn(
node: any,
e_schema: ESchema,
@ -56,7 +61,7 @@ export function convertToJsonString(
): string;
export function validate(
xmlData: string,
options?: { allowBooleanAttributes?: boolean }
options?: validationOptionsOptional
): true | ValidationError;
export class j2xParser {
constructor(options: J2xOptionsOptional);

View File

@ -4,8 +4,17 @@ const nodeToJson = require('./node2json');
const xmlToNodeobj = require('./xmlstr2xmlnode');
const x2xmlnode = require('./xmlstr2xmlnode');
const buildOptions = require('./util').buildOptions;
const validator = require('./validator');
exports.parse = function(xmlData, options) {
exports.parse = function(xmlData, options, validationOption) {
if( validationOption){
if(validationOption === true) validationOption = {}
const result = validator.validate(xmlData, validationOption);
if (result !== true) {
throw Error( result.err.msg)
}
}
options = buildOptions(options, x2xmlnode.defaultOptions, x2xmlnode.props);
return nodeToJson.convertToJson(xmlToNodeobj.getTraversalObj(xmlData, options), options);
};
@ -13,7 +22,7 @@ exports.convertTonimn = require('../src/nimndata').convert2nimn;
exports.getTraversalObj = xmlToNodeobj.getTraversalObj;
exports.convertToJson = nodeToJson.convertToJson;
exports.convertToJsonString = require('./node2json_str').convertToJsonString;
exports.validate = require('./validator').validate;
exports.validate = validator.validate;
exports.j2xParser = require('./json2xml');
exports.parseToNimn = function(xmlData, schema, options) {
return exports.convertTonimn(exports.getTraversalObj(xmlData, options), schema, options);

View File

@ -37,12 +37,16 @@ exports.isEmptyObject = function(obj) {
* @param {*} target
* @param {*} a
*/
exports.merge = function(target, a) {
exports.merge = function(target, a, arrayMode) {
if (a) {
const keys = Object.keys(a); // will return an array of own properties
const len = keys.length; //don't make it inline
for (let i = 0; i < len; i++) {
target[keys[i]] = a[keys[i]];
if(arrayMode === 'strict'){
target[keys[i]] = [ a[keys[i]] ];
}else{
target[keys[i]] = a[keys[i]];
}
}
}
};

View File

@ -10,21 +10,30 @@ const defaultOptions = {
const props = ['allowBooleanAttributes', 'localeRange'];
//const tagsPattern = new RegExp("<\\/?([\\w:\\-_\.]+)\\s*\/?>","g");
exports.validate = function(xmlData, options) {
exports.validate = function (xmlData, options) {
options = util.buildOptions(options, defaultOptions, props);
//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 localRangeRegex = new RegExp(`[${options.localeRange}]`);
if (localRangeRegex.test("<#$'\"\\\/:0")) {
return getErrorObject('InvalidOptions', 'Invalid localeRange', 1);
}
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);
}
const regxAttrName = new RegExp('^[_w][\\w\\-.:]*$'.replace('_w', '_' + options.localeRange));
const regxTagName = new RegExp('^([w]|_)[\\w.\\-_:]*'.replace('([w', '([' + options.localeRange));
const regxAttrName = new RegExp(`^[${options.localeRange}_][${options.localeRange}0-9\\-\\.:]*$`);
const regxTagName = new RegExp(`^([${options.localeRange}_])[${options.localeRange}0-9\\.\\-_:]*$`);
for (let i = 0; i < xmlData.length; i++) {
if (xmlData[i] === '<') {
//starting of tag
@ -66,15 +75,22 @@ exports.validate = function(xmlData, options) {
if (tagName[tagName.length - 1] === '/') {
//self closing tag without attributes
tagName = tagName.substring(0, tagName.length - 1);
continue;
//continue;
i--;
}
if (!validateTagName(tagName, regxTagName)) {
return {err: {code: 'InvalidTag', msg: 'Tag ' + tagName + ' is an invalid name.'}};
let msg;
if(tagName.trim().length === 0) {
msg = "There is an unnecessary space between tag name and backward slash '</ ..'.";
}else{
msg = `Tag '${tagName}' is an invalid name.`;
}
return getErrorObject('InvalidTag', msg, getLineNumberForPosition(xmlData, i));
}
const result = readAttributeStr(xmlData, i);
if (result === false) {
return {err: {code: 'InvalidAttr', msg: 'Attributes for ' + tagName + ' have open quote'}};
return getErrorObject('InvalidAttr', `Attributes for '${tagName}' have open quote.`, getLineNumberForPosition(xmlData, i));
}
let attrStr = result.value;
i = result.index;
@ -87,27 +103,43 @@ exports.validate = function(xmlData, options) {
tagFound = true;
//continue; //text may presents after self closing tag
} else {
return isValid;
//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));
}
} else if (closingTag) {
if (attrStr.trim().length > 0) {
return {
err: {code: 'InvalidTag', msg: 'closing tag ' + tagName + " can't have attributes or invalid starting."},
};
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, i));
} else {
const otg = tags.pop();
if (tagName !== otg) {
return {
err: {code: 'InvalidTag', msg: 'closing tag ' + otg + ' is expected inplace of ' + tagName + '.'},
};
return getErrorObject('InvalidTag', `Closing tag '${otg}' is expected inplace of '${tagName}'.`, getLineNumberForPosition(xmlData, i));
}
//when there are no more tags, we reached the root level.
if(tags.length == 0)
{
reachedRoot = true;
}
}
} else {
const isValid = validateAttributeString(attrStr, options, regxAttrName);
if (isValid !== true) {
return isValid;
//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 {
tags.push(tagName);
}
tags.push(tagName);
tagFound = true;
}
@ -133,16 +165,14 @@ exports.validate = function(xmlData, options) {
if (xmlData[i] === ' ' || xmlData[i] === '\t' || xmlData[i] === '\n' || xmlData[i] === '\r') {
continue;
}
return {err: {code: 'InvalidChar', msg: 'char ' + xmlData[i] + ' is not expected .'}};
return getErrorObject('InvalidChar', `char '${xmlData[i]}' is not expected.`, getLineNumberForPosition(xmlData, i));
}
}
if (!tagFound) {
return {err: {code: 'InvalidXml', msg: 'Start tag expected.'}};
return getErrorObject('InvalidXml', 'Start tag expected.', 1);
} else if (tags.length > 0) {
return {
err: {code: 'InvalidXml', msg: 'Invalid ' + JSON.stringify(tags, null, 4).replace(/\r?\n/g, '') + ' found.'},
};
return getErrorObject('InvalidXml', `Invalid '${JSON.stringify(tags, null, 4).replace(/\r?\n/g, '')}' found.`, 1);
}
return true;
@ -160,7 +190,7 @@ function readPI(xmlData, i) {
//tagname
var tagname = xmlData.substr(start, i - start);
if (i > 5 && tagname === 'xml') {
return {err: {code: 'InvalidXml', msg: 'XML declaration allowed only at the start of the document.'}};
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++;
@ -235,6 +265,7 @@ var singleQuote = "'";
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 === '') {
@ -247,6 +278,7 @@ function readAttributeStr(xmlData, i) {
}
} else if (xmlData[i] === '>') {
if (startChar === '') {
tagClosed = true;
break;
}
}
@ -256,7 +288,7 @@ function readAttributeStr(xmlData, i) {
return false;
}
return {value: attrStr, index: i};
return { value: attrStr, index: i, tagClosed: tagClosed };
}
/**
@ -275,34 +307,40 @@ function validateAttributeString(attrStr, options, regxAttrName) {
const attrNames = {};
for (let i = 0; i < matches.length; i++) {
//console.log(matches[i]);
if (matches[i][1].length === 0) {
//nospace before attribute name: a="sd"b="saf"
return {err: {code: 'InvalidAttr', msg: 'attribute ' + matches[i][2] + ' has no space in starting.'}};
return getErrorObject('InvalidAttr', `Attribute '${matches[i][2]}' has no space in starting.`, getPositionFromMatch(attrStr, matches[i][0]))
} else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {
//independent attribute: ab
return {err: {code: 'InvalidAttr', msg: 'boolean attribute ' + matches[i][2] + ' is not allowed.'}};
return getErrorObject('InvalidAttr', `boolean attribute '${matches[i][2]}' is not allowed.`, getPositionFromMatch(attrStr, matches[i][0]));
}
/* 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, regxAttrName)) {
return {err: {code: 'InvalidAttr', msg: 'attribute ' + attrName + ' is an invalid name.'}};
return getErrorObject('InvalidAttr', `Attribute '${attrName}' is an invalid name.`, getPositionFromMatch(attrStr, matches[i][0]));
}
if (!attrNames.hasOwnProperty(attrName)) {
//check for duplicate attribute.
attrNames[attrName] = 1;
} else {
return {err: {code: 'InvalidAttr', msg: 'attribute ' + attrName + ' is repeated.'}};
return getErrorObject('InvalidAttr', `Attribute '${attrName}' is repeated.`, getPositionFromMatch(attrStr, matches[i][0]));
}
}
return true;
}
// const validAttrRegxp = /^[_a-zA-Z][\w\-.:]*$/;
function getErrorObject(code, message, lineNumber) {
return {
err: {
code: code,
msg: message,
line: lineNumber,
},
};
}
function validateAttrName(attrName, regxAttrName) {
// const validAttrRegxp = new RegExp(regxAttrName);
@ -315,5 +353,17 @@ function validateAttrName(attrName, regxAttrName) {
function validateTagName(tagname, regxTagName) {
/*if(util.doesMatch(tagname,startsWithXML)) return false;
else*/
//return !tagname.toLowerCase().startsWith("xml") || !util.doesNotMatch(tagname, regxTagName);
return !util.doesNotMatch(tagname, regxTagName);
}
//this function returns the line number for the character at the given index
function getLineNumberForPosition(xmlData, index) {
var lines = xmlData.substring(0, index).split(/\r?\n/);
return lines.length;
}
//this function returns the position of the last character of match within attrStr
function getPositionFromMatch(attrStr, match) {
return attrStr.indexOf(match) + match.length;
}

View File

@ -33,10 +33,10 @@ const defaultOptions = {
cdataTagName: false,
cdataPositionChar: '\\c',
localeRange: '',
tagValueProcessor: function(a) {
tagValueProcessor: function(a, tagName) {
return a;
},
attrValueProcessor: function(a) {
attrValueProcessor: function(a, attrName) {
return a;
},
stopNodes: []
@ -84,7 +84,7 @@ const getTraversalObj = function(xmlData, options) {
if (tagType === TagType.CLOSING) {
//add parsed data to parent node
if (currentNode.parent && tag[14]) {
currentNode.parent.val = util.getValue(currentNode.parent.val) + '' + processTagValue(tag[14], options);
currentNode.parent.val = util.getValue(currentNode.parent.val) + '' + processTagValue(tag, options, currentNode.parent.tagname);
}
if (options.stopNodes.length && options.stopNodes.includes(currentNode.tagname)) {
currentNode.child = []
@ -102,14 +102,14 @@ const getTraversalObj = function(xmlData, options) {
currentNode.val = util.getValue(currentNode.val) + options.cdataPositionChar;
//add rest value to parent node
if (tag[14]) {
currentNode.val += processTagValue(tag[14], options);
currentNode.val += processTagValue(tag, options);
}
} else {
currentNode.val = (currentNode.val || '') + (tag[3] || '') + processTagValue(tag[14], options);
currentNode.val = (currentNode.val || '') + (tag[3] || '') + processTagValue(tag, options);
}
} else if (tagType === TagType.SELF) {
if (currentNode && tag[14]) {
currentNode.val = util.getValue(currentNode.val) + '' + processTagValue(tag[14], options);
currentNode.val = util.getValue(currentNode.val) + '' + processTagValue(tag, options);
}
const childNode = new xmlNode(options.ignoreNameSpace ? tag[7] : tag[5], currentNode, '');
@ -123,7 +123,7 @@ const getTraversalObj = function(xmlData, options) {
const childNode = new xmlNode(
options.ignoreNameSpace ? tag[7] : tag[5],
currentNode,
processTagValue(tag[14], options)
processTagValue(tag, options)
);
if (options.stopNodes.length && options.stopNodes.includes(childNode.tagname)) {
childNode.startIndex=tag.index + tag[1].length
@ -140,12 +140,14 @@ const getTraversalObj = function(xmlData, options) {
return xmlObj;
};
function processTagValue(val, options) {
function processTagValue(parsedTags, options, parentTagName) {
const tagName = parsedTags[7] || parentTagName;
let val = parsedTags[14];
if (val) {
if (options.trimValues) {
val = val.trim();
}
val = options.tagValueProcessor(val);
val = options.tagValueProcessor(val, tagName);
val = parseValue(val, options.parseNodeValue, options.parseTrueNumberOnly);
}
@ -189,6 +191,7 @@ function parseValue(val, shouldParse, parseTrueNumberOnly) {
parsed = Number.parseInt(val, 16);
} else if (val.indexOf('.') !== -1) {
parsed = Number.parseFloat(val);
val = val.replace(/0+$/,"");
} else {
parsed = Number.parseInt(val, 10);
}
@ -225,7 +228,7 @@ function buildAttributesMap(attrStr, options) {
if (options.trimValues) {
matches[i][4] = matches[i][4].trim();
}
matches[i][4] = options.attrValueProcessor(matches[i][4]);
matches[i][4] = options.attrValueProcessor(matches[i][4], attrName);
attrs[options.attributeNamePrefix + attrName] = parseValue(
matches[i][4],
options.parseAttributeValue,