diff --git a/spec/data_spec.js b/spec/data_spec.js
index 6cc585f4..a4e97561 100644
--- a/spec/data_spec.js
+++ b/spec/data_spec.js
@@ -115,7 +115,7 @@ describe("XMLParser", function() {
ignoreAttributes: false,
//parseAttributeValue: true,
allowBooleanAttributes: true
- }, true);
+ }, { ignoreNameSpace: true });
//console.log(JSON.stringify(result,null,4));
expect(result).toEqual(expected);
@@ -138,7 +138,7 @@ describe("XMLParser", function() {
ignoreAttributes: false,
//parseAttributeValue: true,
allowBooleanAttributes: true
- }, true);
+ }, { ignoreNameSpace: true });
//console.log(JSON.stringify(result,null,4));
expect(result).toEqual(expected);
diff --git a/spec/validator_spec.js b/spec/validator_spec.js
index 02f373fb..2daf0551 100644
--- a/spec/validator_spec.js
+++ b/spec/validator_spec.js
@@ -4,8 +4,8 @@ const fs = require("fs");
const path = require("path");
const validator = require("../src/validator");
-function validate(xmlData, error, line = 1) {
- const result = validator.validate(xmlData);
+function validate(xmlData, options, error, line = 1) {
+ const result = validator.validate(xmlData, options);
if (error) {
const keys = Object.keys(error);
@@ -20,183 +20,295 @@ function validate(xmlData, error, line = 1) {
}
}
+function validateIgnoringNS(xmlData, error, line) {
+ validate(xmlData, { ignoreNameSpace: true }, error, line);
+}
+
+function validateWithNS(xmlData, error, line) {
+ validate(xmlData, null, error, line);
+}
+
function validateFile(fileName, ...args) {
const fileNamePath = path.join(__dirname, "assets/" + fileName);
- validate(fs.readFileSync(fileNamePath).toString(), ...args);
+ validateIgnoringNS(fs.readFileSync(fileNamePath).toString(), ...args);
}
describe("XMLParser", function () {
it("should validate simple xml string", function () {
- validate("");
- validate(``);
+ validateIgnoringNS("");
+ validateIgnoringNS(``);
});
it("should not validate invalid starting tag", function () {
- validate("< rootNode>", {
+ validateIgnoringNS("< rootNode>", {
InvalidTag: "There is an unnecessary space between tag name and backward slash ' ..'."
});
});
it("should not validate incomplete xml string", function () {
- validate("", {
- InvalidXml: "Invalid '[ \"rootNode\"]' found."
+ validateIgnoringNS("", {
+ InvalidXml: "Invalid '[ { \"name\": \"rootNode\" }]' found."
});
});
it("should not validate invalid starting tag for following characters", function () {
- validate("", {
+ validateIgnoringNS("", {
InvalidTag: "Tag 'rootNode#@aa' is an invalid name."
});
});
it("should return false for non xml text", function () {
- validate("rootNode", {
+ validateIgnoringNS("rootNode", {
InvalidChar: "char 'r' is not expected."
});
});
it("should validate self closing tags", function () {
- validate("texttext");
+ validateIgnoringNS("texttext");
});
it("should not consider these as self closing tags", function () {
- validate("", {
+ validateIgnoringNS("", {
InvalidAttr: "boolean attribute 'tag' is not allowed."
});
- validate("", {
+ validateIgnoringNS("", {
InvalidAttr: "Attribute '/' has no space in starting."
});
});
it("should not validate xml string when closing tag is different", function () {
- validate("", {
+ validateIgnoringNS("", {
InvalidTag: "Closing tag 'rootNode' is expected inplace of 'rootnode'."
});
});
it("should not validate xml string when closing tag is invalid", function () {
- validate("< /rootnode>", {
+ validateIgnoringNS("< /rootnode>", {
InvalidTag: "There is an unnecessary space between tag name and backward slash ' ..'."
});
- validate(" rootnode>", {
+ validateIgnoringNS(" rootnode>", {
InvalidTag: "There is an unnecessary space between tag name and backward slash ' ..'."
});
- validate("", {
+ validateIgnoringNS("", {
InvalidTag: "Closing tag 'rootnode' can't have attributes or invalid starting."
});
});
+
+ it("should validate tag with namespace", function () {
+ validateWithNS("");
+ });
+
+ it("should validate attribute with namespace", function () {
+ validateWithNS("");
+ });
+
+ it("should not validate namespace attribute with empty URI", function () {
+ validateWithNS("", {
+ InvalidAttr: "Invalid URI for namespace root"
+ });
+ });
+
+ it("should validate all namespaces defined in a tag", function () {
+ validateWithNS(`
+
+
+
+ `);
+ });
+
+ it("should validate self closing tag with namespace", function () {
+ validateWithNS("");
+ });
+
+ it("should validate attributes in self closing tag with namespace", function () {
+ validateWithNS("");
+ });
+
+ it("should not validate other tags with namespace when namespace is defined in self closing tag", function () {
+ validateWithNS("", {
+ InvalidTag: "Namespace prefix 'ns' is not defined for 'ns:testTag'"
+ });
+ });
- it("should validate simple xml string with namespace", function () {
- validate("");
+ it("should not validate attributes outside self closing tag with namespace definition", function () {
+ validateWithNS("", {
+ InvalidAttr: "Namespace prefix 'ns' is not defined for 'ns:attr'"
+ });
+ });
+
+ it("should not validate tags with namespace when namespace is defined in a sibling tag", function () {
+ validateWithNS("", {
+ InvalidTag: "Namespace prefix 'ns' is not defined for 'ns:child2'"
+ });
+ });
+
+ it("should not validate tag when namespace is not defined", function () {
+ validateWithNS("", {
+ InvalidTag: "Namespace prefix 'root' is not defined for 'root:Node'"
+ });
+ });
+
+ it("should not validate attribute when namespace is not defined", function () {
+ validateWithNS("", {
+ InvalidAttr: "Namespace prefix 'ns' is not defined for 'ns:attr'"
+ });
+ });
+
+ it("should not validate tag when namespace is defined later", function () {
+ validateWithNS(`
+
+
+ `, {
+ InvalidTag: "Namespace prefix 'root' is not defined for 'root:Node'"
+ });
+ });
+
+ it("should not validate attribute when namespace is defined later", function () {
+ validateWithNS(`
+
+
+
+
+ `, {
+ InvalidAttr: "Namespace prefix 'ns' is not defined for 'ns:attr'"
+ }, 2);
+ });
+
+ it("should not validate tag when multiple namespace prefixes are present", function () {
+ validateWithNS("", {
+ InvalidTag: "'root:ns:Node' cannot have multiple namespace prefixes"
+ });
+ });
+
+ it("should not validate attribute when multiple namespace prefixes are present", function () {
+ validateWithNS("", {
+ InvalidAttr: "'ns1:ns2:attr' cannot have multiple namespace prefixes"
+ });
+ });
+
+ it("should not validate attributes with same name and same namespace prefix", function () {
+ validateWithNS("", {
+ InvalidAttr: "Attribute 'attr' in namespace 'urn:none' is repeated."
+ });
+ });
+
+ it("should not validate attributes with same name and same namespace", function () {
+ validateWithNS("",{
+ InvalidAttr: "Attribute 'attr' in namespace 'urn:none' is repeated."
+ });
+ });
+
+ it("should validate attributes with same name and different namespace", function () {
+ validateWithNS("");
});
it("should not validate xml string with namespace when closing tag is diffrent", function () {
- validate("", {
+ validateIgnoringNS("", {
InvalidTag: "Closing tag 'root:Node' is expected inplace of 'root:node'."
});
});
it("should validate simple xml string with value", function () {
- validate("some value");
+ validateIgnoringNS("some value");
});
it("should not validate simple xml string with value but not matching closing tag", function () {
- validate("some value", {
+ validateIgnoringNS("some value", {
InvalidTag: "Closing tag 'root:Node' is expected inplace of 'root'."
});
});
it("should not validate simple xml string with value but no closing tag", function () {
- validate("some value", {
- InvalidXml: "Invalid '[ \"root:Node\"]' found."
+ validateIgnoringNS("some value", {
+ InvalidXml: "Invalid '[ { \"name\": \"root:Node\" }]' found."
});
});
it("should validate xml with nested tags", function () {
- validate("1val");
+ validateIgnoringNS("1val");
});
it("should not validate xml with wrongly nested tags", function () {
- validate("1val", {
+ validateIgnoringNS("1val", {
InvalidTag: "Closing tag 'tag1' is expected inplace of 'tag'."
});
});
it("should validate xml with comment", function () {
- validate("1val");
+ validateIgnoringNS("1val");
});
it("should validate xml with comment", function () {
- validate("1val");
+ validateIgnoringNS("1val");
});
it("should not validate xml with comment in a open tag", function () {
- validate(" -- -->>1val", {
+ validateIgnoringNS(" -- -->>1val", {
InvalidTag: "Tag 'rootNode >1val", {
+ validateIgnoringNS(" -- --> >1val", {
InvalidAttr: "boolean attribute '" +
"!bla>", {
+ validateIgnoringNS("!bla>", {
InvalidTag: "Tag '!bla' is an invalid name."
});
});
it("should not validate XML when prolog doesn't start from 1st char", function () {
- validate(" Hello World.", {
+ validateIgnoringNS(" Hello World.", {
InvalidXml: "XML declaration allowed only at the start of the document."
});
});
it("should not validate XML with prolog only", function () {
- validate("", {
+ validateIgnoringNS("", {
InvalidXml: "Start tag expected."
});
});
it("should not validate XML with prolog & DOCTYPE but not any other tag", function () {
- validate("" +
+ validateIgnoringNS("" +
"" +
"" +
@@ -267,14 +379,14 @@ describe("XMLParser", function () {
});
it("should validate XML PIs", function () {
- validate('' +
+ validateIgnoringNS('' +
'' +
'' +
'');
});
it("should not validate XML PIs with invalid values", function () {
- validate('' +
+ validateIgnoringNS('' +
'" ?>' +
'' +
'', {
@@ -283,17 +395,23 @@ describe("XMLParser", function () {
});
it('should validate xml with a "length" attribute', function () {
- validate('');
+ validateIgnoringNS('');
});
it("should not validate xml with repeated attributes", function () {
- validate('', {
+ validateIgnoringNS('', {
InvalidAttr: "Attribute 'length' is repeated."
});
});
+ it("should not validate attributes with same name and different namespace prefix, if namespace is ignored", function () {
+ validateIgnoringNS("", {
+ InvalidAttr: "Attribute 'attr' is repeated."
+ });
+ });
+
it('should validate xml with a tag attribute splitted on more lines', () => {
- validate(`
+ validateIgnoringNS(`
{
- validate(`
+ validateIgnoringNS(`
{
- validate(`
@@ -323,7 +441,7 @@ attribute2="attribute2"
});
it('should detect error line when having multiple attributes 2', () => {
- validate(`jekyll & hyde');
- validate('jekyll { hyde');
- validate('jekyll hyde');
- validate('jekyll h; hyde', error);
- validate('jekyll a; hyde', error);
- validate('jekyll { hyde', error);
- validate('jekyll abcd hyde', error);
- validate('jekyll & hyde', error);
- validate('jekyll &aa', error);
- validate('jekyll &abcdefghij1234567890;');
- validate('jekyll &abcdefghij1234567890a;', error); // limit to 20 chars
+ validateIgnoringNS('jekyll & hyde');
+ validateIgnoringNS('jekyll { hyde');
+ validateIgnoringNS('jekyll hyde');
+ validateIgnoringNS('jekyll h; hyde', error);
+ validateIgnoringNS('jekyll a; hyde', error);
+ validateIgnoringNS('jekyll { hyde', error);
+ validateIgnoringNS('jekyll abcd hyde', error);
+ validateIgnoringNS('jekyll & hyde', error);
+ validateIgnoringNS('jekyll &aa', error);
+ validateIgnoringNS('jekyll &abcdefghij1234567890;');
+ validateIgnoringNS('jekyll &abcdefghij1234567890a;', error); // limit to 20 chars
});
});
describe("should not validate XML documents with multiple root nodes", () => {
it('when root nodes are repeated', () => {
- validate(``, {
+ validateIgnoringNS(``, {
InvalidXml: 'Multiple possible root nodes found.'
});
});
it('when root nodes are different', () => {
- validate('', {
+ validateIgnoringNS('', {
InvalidXml: 'Multiple possible root nodes found.'
});
});
it('when root nodes have more nested tags', () => {
- validate(`
+ validateIgnoringNS(`
diff --git a/src/validator.js b/src/validator.js
index 783771c3..f4c94b64 100644
--- a/src/validator.js
+++ b/src/validator.js
@@ -4,9 +4,10 @@ const util = require('./util');
const defaultOptions = {
allowBooleanAttributes: false, //A tag can have attributes without any value
+ ignoreNameSpace: false //Ignore namespace verification
};
-const props = ['allowBooleanAttributes'];
+const props = ['allowBooleanAttributes', 'ignoreNameSpace'];
//const tagsPattern = new RegExp("<\\/?([\\w:\\-_\.]+)\\s*\/?>","g");
exports.validate = function (xmlData, options) {
@@ -16,6 +17,7 @@ exports.validate = function (xmlData, options) {
//xmlData = xmlData.replace(/(^\s*<\?xml.*?\?>)/g,"");//Remove XML starting tag
//xmlData = xmlData.replace(/()/g,"");//Remove DOCTYPE
const tags = [];
+ let nameSpaces = [];
let tagFound = false;
//indicates that the root tag has been closed (aka. depth 0 has been reached)
@@ -77,19 +79,40 @@ exports.validate = function (xmlData, options) {
return getErrorObject('InvalidTag', msg, getLineNumberForPosition(xmlData, i));
}
- const result = readAttributeStr(xmlData, i);
+ const result = readAttributeStr(xmlData, i, options);
if (result === false) {
return getErrorObject('InvalidAttr', "Attributes for '"+tagName+"' have open quote.", getLineNumberForPosition(xmlData, i));
}
+
+ if (!options.ignoreNameSpace) {
+ if (result.nsError) {
+ return getErrorObject('InvalidAttr', result.nsError, getLineNumberForPosition(xmlData, i));
+ }
+
+ //Pushing namespaces defined in tag
+ Array.prototype.push.apply(nameSpaces, result.nsArray);
+
+ const nsResult = validateNameSpace(tagName, nameSpaces);
+
+ if (nsResult !== true) {
+ return getErrorObject('InvalidTag', nsResult, getLineNumberForPosition(xmlData, i));
+ }
+ }
+
let attrStr = result.value;
i = result.index;
if (attrStr[attrStr.length - 1] === '/') {
//self closing tag
attrStr = attrStr.substring(0, attrStr.length - 1);
- const isValid = validateAttributeString(attrStr, options);
+ const isValid = validateAttributeString(attrStr, nameSpaces, options);
if (isValid === true) {
tagFound = true;
+
+ if (!options.ignoreNameSpace) {
+ //Removing namespaces defined in current tag
+ nameSpaces.length -= result.nsArray.length;
+ }
//continue; //text may presents after self closing tag
} else {
//the result from the nested function returns the position of the error within the attribute
@@ -104,8 +127,13 @@ exports.validate = function (xmlData, options) {
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 getErrorObject('InvalidTag', "Closing tag '"+otg+"' is expected inplace of '"+tagName+"'.", getLineNumberForPosition(xmlData, i));
+ if (tagName !== otg.name) {
+ return getErrorObject('InvalidTag', "Closing tag '"+otg.name+"' is expected inplace of '"+tagName+"'.", getLineNumberForPosition(xmlData, i));
+ }
+
+ if (!options.ignoreNameSpace) {
+ //Removing namespaces defined in current tag
+ nameSpaces.length -= otg.nsArray.length;
}
//when there are no more tags, we reached the root level.
@@ -114,7 +142,7 @@ exports.validate = function (xmlData, options) {
}
}
} else {
- const isValid = validateAttributeString(attrStr, options);
+ const isValid = validateAttributeString(attrStr, nameSpaces, 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
@@ -126,7 +154,15 @@ exports.validate = function (xmlData, options) {
if (reachedRoot === true) {
return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
} else {
- tags.push(tagName);
+ let tagObject = {
+ name: tagName
+ };
+
+ if (!options.ignoreNameSpace) {
+ tagObject["nsArray"] = result.nsArray;
+ }
+
+ tags.push(tagObject);
}
tagFound = true;
}
@@ -255,7 +291,7 @@ var singleQuote = "'";
* @param {string} xmlData
* @param {number} i
*/
-function readAttributeStr(xmlData, i) {
+function readAttributeStr(xmlData, i, options) {
let attrStr = '';
let startChar = '';
let tagClosed = false;
@@ -281,11 +317,22 @@ function readAttributeStr(xmlData, i) {
return false;
}
- return {
+ let result = {
value: attrStr,
index: i,
tagClosed: tagClosed
};
+
+ if (!options.ignoreNameSpace) {
+ const nsResult = getNameSpaceDefinitions(attrStr);
+ if (Array.isArray(nsResult)) {
+ result["nsArray"] = nsResult;
+ } else {
+ result["nsError"] = nsResult;
+ }
+ }
+
+ return result;
}
/**
@@ -295,7 +342,7 @@ const validAttrStrRegxp = new RegExp('(\\s*)([^\\s=]+)(\\s*=)?(\\s*([\'"])(([\\s
//attr, ="sd", a="amit's", a="sd"b="saf", ab cd=""
-function validateAttributeString(attrStr, options) {
+function validateAttributeString(attrStr, nsArray, options) {
//console.log("start:"+attrStr+":end");
//if(attrStr.trim().length === 0) return true; //empty string
@@ -314,15 +361,41 @@ function validateAttributeString(attrStr, options) {
/* 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];
+ let attrName = matches[i][2];
if (!validateAttrName(attrName)) {
return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is an invalid name.", getPositionFromMatch(attrStr, matches[i][0]));
}
+
+ if (!options.ignoreNameSpace) {
+ const nsDefMatches = util.getAllMatches(matches[i][0], nameSpaceDefinitionRegex);
+ //Skipping namespace definition attribute
+ if (!nsDefMatches || nsDefMatches.length === 0 || attrName !== nsDefMatches[0][1]) {
+ const nsResult = validateNameSpace(attrName, nsArray);
+ if (nsResult !== true) {
+ return getErrorObject('InvalidAttr', nsResult, getPositionFromMatch(attrStr, matches[i][0]));
+ }
+ }
+ const attrSplit = attrName.split(":");
+ if (attrSplit.length > 1) {
+ for (let i=0; i < nsArray.length; i++) {
+ const nsSplit = nsArray[i].split("'");
+ if (nsSplit[0] === attrSplit[0]) {
+ attrName = nsSplit[1] + "'" + attrSplit[1];
+ break;
+ }
+ }
+ }
+ } else {
+ attrName = attrName.replace(/[^:]+:/, "");
+ }
+
if (!attrNames.hasOwnProperty(attrName)) {
- //check for duplicate attribute.
- attrNames[attrName] = 1;
+ //check for duplicate attribute.
+ attrNames[attrName] = 1;
} else {
- return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is repeated.", getPositionFromMatch(attrStr, matches[i][0]));
+ const attrSplit = attrName.split("'");
+ const msg = attrSplit.length === 2 ? "'" + attrSplit[1] + "' in namespace '" + attrSplit[0] + "'": "'" + attrName + "'";
+ return getErrorObject('InvalidAttr', "Attribute "+ msg +" is repeated.", getPositionFromMatch(attrStr, matches[i][0]));
}
}
@@ -384,6 +457,38 @@ function validateTagName(tagname) {
return util.isName(tagname) /* && !tagname.match(startsWithXML) */;
}
+const nameSpaceDefinitionRegex = new RegExp(/(xmlns:([^:=]+))=['"]([^'"]*)['"]/, 'g');
+
+function validateNameSpace(elemName, nsArray) {
+ let elemSplit = elemName.split(":");
+ switch (elemSplit.length){
+ case 1:
+ return true;
+ case 2:
+ for (let i=0; i< nsArray.length; i++) {
+ if (nsArray[i].split("'")[0] === elemSplit[0]){
+ return true;
+ }
+ }
+ return "Namespace prefix '" + elemSplit[0] + "' is not defined for '" + elemName + "'";
+ default:
+ return "'" + elemName + "' cannot have multiple namespace prefixes";
+ }
+}
+
+function getNameSpaceDefinitions(attributeString) {
+ let nsArray = [];
+ let matches = util.getAllMatches(attributeString, nameSpaceDefinitionRegex);
+ for (let i=0; i< matches.length; i++){
+ if (matches[i][3]) {
+ nsArray.push(matches[i][2] + "'" + matches[i][3]);
+ } else {
+ return "Invalid URI for namespace " + matches[i][2];
+ }
+ }
+ return nsArray;
+}
+
//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/);