Skip to content

Commit

Permalink
support unpaired tags
Browse files Browse the repository at this point in the history
  • Loading branch information
amitguptagwl committed Nov 19, 2021
1 parent 4d3b8be commit 81b1084
Show file tree
Hide file tree
Showing 16 changed files with 258 additions and 36 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
Note: If you find missing information about particular minor version, that version must have been changed without any functional change in this library.

**⚠️ 4.0.0-beta.2 / 2021-11-19**
* rename `attrMap` to `attibutes` in parser output when `preserveOrder:true`
* supports unpairedTags

**⚠️ 4.0.0-beta.1 / 2021-11-18**
* Parser returns an array now
* to make the structure common
Expand Down
29 changes: 29 additions & 0 deletions docs/v4/2.XMLparseOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -779,4 +779,33 @@ Output
}
```

## unpairedTags
Unpaired Tags are the tags which don't have matching closing tag. Eg `<br>` in HTML. You can parse unpaired tags by providing their list to the parser, validator and builder.

Eg
```js
const xmlData = `
<rootNode>
<tag>value</tag>
<empty />
<unpaired>
</rootNode>`;

const options = {
unpairedTags: ["unpaired"]
};
const parser = new XMLParser(options);
const output = parser.parse(xmlDataStr);
```
Output
```json
{
"rootNode": {
"tag": "value",
"empty": "",
"unpaired": ""
}
}
```

[> Next: XmlBuilder](./3.XMLBuilder.md)
43 changes: 43 additions & 0 deletions docs/v4/3.XMLBuilder.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,47 @@ To customize the bahaviour of parsing the text value of a tag to the string valu
## textNodeName
To recognize text value for a tag in the JS object so that they can be properly assigned to the tag.

## unpairedTags
Unpaired Tags are the tags which don't have matching closing tag. Eg `<br>` in HTML. You can parse unpaired tags by providing their list to the parser, validator and builder.

Eg
```js
const xmlData = `
<rootNode>
<tag>value</tag>
<empty />
<unpaired>
</rootNode>`;

const options = {
// suppressEmptyNode: true,
unpairedTags: ["unpaired"]
};
const parser = new XMLParser(options);
const result = parser.parse(xmlDataStr);

const builder = new XMLBuilder(options);
const output = builder.build(result);
```
Output
```xml
<rootNode>
<tag>value</tag>
<empty></empty>
<unpaired></unpaired>
</rootNode>
```

when you sets `suppressEmptyNode: true`;

```xml
<rootNode>
<tag>value</tag>
<empty />
<unpaired>
</rootNode>
```

# Restoring original XML

**Example 1**
Expand Down Expand Up @@ -239,4 +280,6 @@ Output
</car>
```



[> Next: XmlValidator](./4.XMLValidator.md)
14 changes: 13 additions & 1 deletion docs/v4/4.XMLValidator.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,16 @@ const result = XMLValidator.validate(xmlData, {

## Options

The only option supported by Validator is `allowBooleanAttributes`. You need to set it to `true` when a tag can have boolean attributes.
### allowBooleanAttributes

Set it to `true` when a tag can have boolean attributes.

### unpairedTags
Unpaired Tags are the tags which don't have matching closing tag. Eg `<br>` in HTML. You can parse unpaired tags by providing their list to the parser, validator and builder.

```js
const xmlData = `<parent><extra></parent>`;
const result = XMLValidator.validate( xmlData, {
unpairedTags: ["extra"]
});
```
48 changes: 25 additions & 23 deletions lib/fxparser.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/fxparser.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/fxparser.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/fxparser.min.js.map

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions spec/unpairedTags_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use strict";

const {XMLParser, XMLBuilder} = require("../src/fxp");

describe("unpaired and empty tags", function() {

it("should be parsed with paired tag when suppressEmptyNode:false", function() {
const xmlData = `<rootNode>
<tag>value</tag>
<empty />
<unpaired>
</rootNode>`;
const expectedXmlData = `<rootNode>
<tag>value</tag>
<empty></empty>
<unpaired></unpaired>
</rootNode>`;

const options = {
// format: true,
// preserveOrder: true,
// suppressEmptyNode: true,
unpairedTags: ["unpaired"]
};
const parser = new XMLParser(options);
let result = parser.parse(xmlData);
// console.log(JSON.stringify(result, null,4));

const builder = new XMLBuilder(options);
const output = builder.build(result);
// console.log(output);
expect(output.replace(/\s+/g, "")).toEqual(expectedXmlData.replace(/\s+/g, ""));
});

it("should be parsed without paired tag when suppressEmptyNode:true", function() {
const xmlData = `<rootNode>
<tag>value</tag>
<empty />
<unpaired>
</rootNode>`;

const options = {
// format: true,
// preserveOrder: true,
suppressEmptyNode: true,
unpairedTags: ["unpaired"]
};
const parser = new XMLParser(options);
let result = parser.parse(xmlData);
// console.log(JSON.stringify(result, null,4));

const builder = new XMLBuilder(options);
const output = builder.build(result);
// console.log(output);
expect(output.replace(/\s+/g, "")).toEqual(xmlData.replace(/\s+/g, ""));
});

it("should be parsed without paired tag when suppressEmptyNode:true and tags order is preserved", function() {
const xmlData = `<rootNode>
<tag>value</tag>
<empty />
<unpaired>
</rootNode>`;

const options = {
// format: true,
// preserveOrder: true,
suppressEmptyNode: true,
unpairedTags: ["unpaired"]
};
const parser = new XMLParser(options);
let result = parser.parse(xmlData);
// console.log(JSON.stringify(result, null,4));

const builder = new XMLBuilder(options);
const output = builder.build(result);
// console.log(output);
expect(output.replace(/\s+/g, "")).toEqual(xmlData.replace(/\s+/g, ""));
});
});
25 changes: 25 additions & 0 deletions spec/validator_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -433,3 +433,28 @@ describe("should report correct line numbers for unclosed tags", () => {
</parent>extra`,
{InvalidXml: "Extra text at the end"}, 2, 28));
});

describe("XML Validator with options", function () {
it('- Unpaired tags', () =>
validate(`<parent><extra></parent>`,
{InvalidTag: "Expected closing tag 'extra' (opened in line 1, col 9) instead of closing tag 'parent'."}, 1, 16));

it('- Maarked Unpaired tags', () =>{
const result = XMLValidator.validate(`<parent><extra></parent>`, {
unpairedTags: ["extra"]
});
// console.log(result);
expect(result).toBeTrue();
});
it('- allowBooleanAttributes:false', () =>
validate(`<parent extra></parent>`,
{InvalidAttr: "boolean attribute 'extra' is not allowed."}, 1, 9));

it('- allowBooleanAttributes:true', () =>{
const result = XMLValidator.validate(`<parent extra></parent>`, {
allowBooleanAttributes: true
});
expect(result).toBeTrue();
});

});
3 changes: 3 additions & 0 deletions src/fxp.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type X2jOptions = {
attributeValueProcessor: (attrName: string, attrValue: string, jPath: string) => string;
numberParseOptions: strnumOptions;
stopNodes: string[];
unpairedTags: string[];
alwaysCreateTextNode: boolean;
isArray: (tagName: string, jPath: string, isLeafNode: boolean, isAttribute: boolean) => boolean;
};
Expand All @@ -26,6 +27,7 @@ type strnumOptions = {
type X2jOptionsOptional = Partial<X2jOptions>;
type validationOptions = {
allowBooleanAttributes: boolean;
unpairedTags: string[];
};
type validationOptionsOptional = Partial<validationOptions>;

Expand All @@ -41,6 +43,7 @@ type XmlBuilderOptions = {
arrayNodeName: string;
suppressEmptyNode: boolean;
preserveOrder: boolean;
unpairedTags: string[];
tagValueProcessor: (name: string, value: string) => string;
attributeValueProcessor: (name: string, value: string) => string;
};
Expand Down
8 changes: 7 additions & 1 deletion src/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ const util = require('./util');

const defaultOptions = {
allowBooleanAttributes: false, //A tag can have attributes without any value
unpairedTags: []
};

const props = ['allowBooleanAttributes'];
const props = [
'allowBooleanAttributes',
'unpairedTags'
];

//const tagsPattern = new RegExp("<\\/?([\\w:\\-_\.]+)\\s*\/?>","g");
exports.validate = function (xmlData, options) {
Expand Down Expand Up @@ -130,6 +134,8 @@ exports.validate = function (xmlData, options) {
//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});
}
Expand Down
12 changes: 6 additions & 6 deletions src/xmlbuilder/json2xml.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const defaultOptions = {
return a;
},
preserveOrder: false,
commentPropName: false
commentPropName: false,
unpairedTags: [],
};

const props = [
Expand All @@ -36,6 +37,7 @@ const props = [
'arrayNodeName', //when array as root
'preserveOrder',
"commentPropName",
"unpairedTags",
// 'rootNodeName', //when jsObject have multiple properties on root level
];

Expand Down Expand Up @@ -208,7 +210,9 @@ function buildTextValNode(val, key, attrStr, level) {
}

function buildEmptyTextNode(val, key, attrStr, level) {
if (val !== '') {
if( val === '' && this.options.unpairedTags.indexOf(key) !== -1){
return this.indentate(level) + '<' + key + attrStr + this.tagEndChar;
}else if (val !== '') {
return this.buildTextValNode(val, key, attrStr, level);
} else {
return this.indentate(level) + '<' + key + attrStr + '/' + this.tagEndChar;
Expand All @@ -227,8 +231,4 @@ function isAttribute(name /*, options*/) {
}
}

//formatting
//indentation
//\n after each closing or self closing tag

module.exports = Builder;
6 changes: 5 additions & 1 deletion src/xmlbuilder/orderedJs2Xml.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ function arrToStr(arr, options, level){
let tagStart = indentation + `<${tagName}${attStr}`;
let tagValue = arrToStr(tagObj[tagName], options, level + 1);
if( (!tagValue || tagValue.length === 0) && options.suppressEmptyNode){
xmlStr += tagStart + "/>";
if(options.unpairedTags.indexOf(tagName) !== -1){
xmlStr += tagStart + ">";
}else{
xmlStr += tagStart + "/>";
}
}else{
//TODO: node with only text value should not parse the text value in next line
xmlStr += tagStart + `>${tagValue}${indentation}</${tagName}>` ;
Expand Down
4 changes: 3 additions & 1 deletion src/xmlparser/OptionsBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const defaultOptions = {
alwaysCreateTextNode: false,
isArray: () => false,
commentPropName: false,
unpairedTags: [],
};

const props = [
Expand All @@ -46,7 +47,8 @@ const props = [
'stopNodes',
'alwaysCreateTextNode',
'isArray',
'commentPropName'
'commentPropName',
'unpairedTags',
];

const util = require('../util');
Expand Down
12 changes: 12 additions & 0 deletions src/xmlparser/OrderedObjParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,18 @@ const parseToOrderedJsObj = function(xmlData, options) {
tagExp = tagExp.substr(0, tagExp.length - 1);
}

const childNode = new xmlNode(tagName);
if(tagName !== tagExp && shouldBuildAttributesMap){
childNode.attributes = buildAttributesMap(tagExp, jPath , options);
}
jPath = jPath.substr(0, jPath.lastIndexOf("."));
// tagsNodeStack.push(currentNode);
currentNode.addChild(childNode);
}
//boolean tags
else if(options.unpairedTags.indexOf(tagName) !== -1){
// tagExp = tagExp.substr(0, tagExp.length - 1);

const childNode = new xmlNode(tagName);
if(tagName !== tagExp && shouldBuildAttributesMap){
childNode.attributes = buildAttributesMap(tagExp, jPath , options);
Expand Down

0 comments on commit 81b1084

Please sign in to comment.