diff --git a/docs/v4/2.XMLparseOptions.md b/docs/v4/2.XMLparseOptions.md index b59f125c..1f097601 100644 --- a/docs/v4/2.XMLparseOptions.md +++ b/docs/v4/2.XMLparseOptions.md @@ -280,24 +280,95 @@ FXP by default parse XMl entities if `processEntities: true`. You can set `htmlE ## ignoreAttributes -By default `ignoreAttributes` is set to `true`. It means, attributes are ignored by the parser. If you set any configuration related to attributes without setting `ignoreAttributes: false`, it is useless. +By default, `ignoreAttributes` is set to `true`. This means that attributes are ignored by the parser. If you set any configuration related to attributes without setting `ignoreAttributes: false`, it will not have any effect. + +### Selective Attribute Ignoring + +You can specify an array of strings, regular expressions, or a callback function to selectively ignore specific attributes during parsing or building. + +### Example Input XML + +```xml + + value + +``` + +You can use the `ignoreAttributes` option in three different ways: + +1. **Array of Strings**: Ignore specific attributes by name. +2. **Array of Regular Expressions**: Ignore attributes that match a pattern. +3. **Callback Function**: Ignore attributes based on custom logic. + +### Example: Ignoring Attributes by Array of Strings -Eg ```js -const xmlDataStr = `wow`; +const options = { + attributeNamePrefix: "$", + ignoreAttributes: ['ns:attr1', 'ns:attr2'], + parseAttributeValue: true +}; +const parser = new XMLParser(options); +const output = parser.parse(xmlData); +``` + +Result: +```json +{ + "tag": { + "#text": "value", + "$ns2:attr3": "a3-value", + "$ns2:attr4": "a4-value" + } +} +``` +### Example: Ignoring Attributes by Regular Expressions + +```js const options = { - // ignoreAttributes: false, - attributeNamePrefix : "@_" + attributeNamePrefix: "$", + ignoreAttributes: [/^ns2:/], + parseAttributeValue: true }; const parser = new XMLParser(options); -const output = parser.parse(xmlDataStr); +const output = parser.parse(xmlData); ``` -Output + +Result: ```json { - "root": { - "a": "wow" + "tag": { + "#text": "value", + "$ns:attr1": "a1-value", + "$ns:attr2": "a2-value" + } +} +``` + +### Example: Ignoring Attributes via Callback Function + +```js +const options = { + attributeNamePrefix: "$", + ignoreAttributes: (aName, jPath) => aName.startsWith('ns:') || jPath === 'tag.tag2', + parseAttributeValue: true +}; +const parser = new XMLParser(options); +const output = parser.parse(xmlData); +``` + +Result: +```json +{ + "tag": { + "$ns2:attr3": "a3-value", + "$ns2:attr4": "a4-value", + "tag2": "value" } } ``` diff --git a/docs/v4/3.XMLBuilder.md b/docs/v4/3.XMLBuilder.md index 713b1481..20032e6a 100644 --- a/docs/v4/3.XMLBuilder.md +++ b/docs/v4/3.XMLBuilder.md @@ -170,7 +170,89 @@ It is recommended to use `preserveOrder: true` when you're parsing XML to js obj By default, parsed XML is single line XML string. By `format: true`, you can format it for better view. ## ignoreAttributes -Don't consider attributes while building XML. Other attributes related properties should be set to correctly identifying an attribute property. + +By default, the `ignoreAttributes` option skips attributes while building XML. However, you can specify an array of strings, regular expressions, or a callback function to selectively ignore specific attributes during the building process. + +### Selective Attribute Ignoring + +The `ignoreAttributes` option supports: + +1. **Array of Strings**: Ignore specific attributes by name while building XML. +2. **Array of Regular Expressions**: Ignore attributes that match a pattern while building XML. +3. **Callback Function**: Ignore attributes based on custom logic during the building process. + +### Example Input JSON + +```json +{ + "tag": { + "$ns:attr1": "a1-value", + "$ns:attr2": "a2-value", + "$ns2:attr3": "a3-value", + "$ns2:attr4": "a4-value", + "tag2": { + "$ns:attr1": "a1-value", + "$ns:attr2": "a2-value", + "$ns2:attr3": "a3-value", + "$ns2:attr4": "a4-value" + } + } +} +``` + +### Example: Ignoring Attributes by Array of Strings + +```js +const options = { + attributeNamePrefix: "$", + ignoreAttributes: ['ns:attr1', 'ns:attr2'] +}; +const builder = new XMLBuilder(options); +const xmlOutput = builder.build(jsonData); +``` + +Result: +```xml + + + +``` + +### Example: Ignoring Attributes by Regular Expressions + +```js +const options = { + attributeNamePrefix: "$", + ignoreAttributes: [/^ns2:/] +}; +const builder = new XMLBuilder(options); +const xmlOutput = builder.build(jsonData); +``` + +Result: +```xml + + + +``` + +### Example: Ignoring Attributes via Callback Function + +```js +const options = { + attributeNamePrefix: "$", + ignoreAttributes: (aName, jPath) => aName.startsWith('ns:') || jPath === 'tag.tag2' +}; +const builder = new XMLBuilder(options); +const xmlOutput = builder.build(jsonData); +``` + +Result: +```xml + + + +``` ## indentBy Applicable only if `format:true` is set. diff --git a/spec/attrIgnore_spec.js b/spec/attrIgnore_spec.js new file mode 100644 index 00000000..485a9ce4 --- /dev/null +++ b/spec/attrIgnore_spec.js @@ -0,0 +1,141 @@ +"use strict"; + +const { XMLParser, XMLBuilder, XMLValidator } = require("../src/fxp"); + +const xmlData = ` + + value +`; + +const jsonData = { + tag: { + '$ns:attr1': 'a1-value', + '$ns:attr2': 'a2-value', + '$ns2:attr3': 'a3-value', + '$ns2:attr4': 'a4-value', + tag2: { + '$ns:attr1': 'a1-value', + '$ns:attr2': 'a2-value', + '$ns2:attr3': 'a3-value', + '$ns2:attr4': 'a4-value', + } + } +} + +describe("XMLParser", function () { + + it('must ignore parsing attributes by array of strings', () => { + + const options = { + attributeNamePrefix: "$", + ignoreAttributes: ['ns:attr1', 'ns:attr2'], + parseAttributeValue: true + }; + const parser = new XMLParser(options); + expect(parser.parse(xmlData)).toEqual({ + tag: { + '#text': 'value', + '$ns2:attr3': 'a3-value', + '$ns2:attr4': 'a4-value', + }, + }) + + expect(XMLValidator.validate(xmlData)).toBe(true); + }) + + it('must ignore parsing attributes by array of RegExp', () => { + + const options = { + attributeNamePrefix: "$", + ignoreAttributes: [/^ns2:/], + parseAttributeValue: true + }; + const parser = new XMLParser(options); + expect(parser.parse(xmlData)).toEqual({ + tag: { + '#text': 'value', + '$ns:attr1': 'a1-value', + '$ns:attr2': 'a2-value', + }, + }) + + expect(XMLValidator.validate(xmlData)).toBe(true); + }) + + it('must ignore parsing attributes via callback fn', () => { + const xmlData = ` + + + value + + `; + + const options = { + attributeNamePrefix: "$", + ignoreAttributes: (aName, jPath) => aName.startsWith('ns:') || jPath === 'tag.tag2', + parseAttributeValue: true + }; + const parser = new XMLParser(options); + expect(parser.parse(xmlData)).toEqual({ + tag: { + '$ns2:attr3': 'a3-value', + '$ns2:attr4': 'a4-value', + tag2: 'value', + }, + }) + + expect(XMLValidator.validate(xmlData)).toBe(true); + }) + + + it('must ignore building attributes by array of strings', () => { + + const options = { + attributeNamePrefix: "$", + ignoreAttributes: ['ns:attr1', 'ns:attr2'], + parseAttributeValue: true + }; + const builder = new XMLBuilder(options); + expect(builder.build(jsonData)).toEqual('') + + expect(XMLValidator.validate(xmlData)).toBe(true); + }) + + it('must ignore building attributes by array of RegExp', () => { + + const options = { + attributeNamePrefix: "$", + ignoreAttributes: [/^ns2:/], + parseAttributeValue: true + }; + const builder = new XMLBuilder(options); + expect(builder.build(jsonData)).toEqual('') + + expect(XMLValidator.validate(xmlData)).toBe(true); + }) + + it('must ignore building attributes via callback fn', () => { + + const options = { + attributeNamePrefix: "$", + ignoreAttributes: (aName, jPath) => aName.startsWith('ns:') || jPath === 'tag.tag2', + parseAttributeValue: true + }; + const builder = new XMLBuilder(options); + expect(builder.build(jsonData)).toEqual('') + + expect(XMLValidator.validate(xmlData)).toBe(true); + }) +}) \ No newline at end of file diff --git a/src/fxp.d.ts b/src/fxp.d.ts index bddcfefe..7a48b9db 100644 --- a/src/fxp.d.ts +++ b/src/fxp.d.ts @@ -30,9 +30,17 @@ type X2jOptions = { /** * Whether to ignore attributes when parsing * + * When `true` - ignores all the attributes + * + * When `false` - parses all the attributes + * + * When `Array` - 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; + ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, jPath: string) => boolean); /** * Whether to remove namespace string from tag and attribute names @@ -250,11 +258,19 @@ type XmlBuilderOptions = { textNodeName?: string; /** - * Whether to ignore attributes when parsing + * Whether to ignore attributes when building + * + * When `true` - ignores all the attributes + * + * When `false` - builds all the attributes + * + * When `Array` - 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; + 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 diff --git a/src/ignoreAttributes.js b/src/ignoreAttributes.js new file mode 100644 index 00000000..9fb346bb --- /dev/null +++ b/src/ignoreAttributes.js @@ -0,0 +1,20 @@ +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 +} + +module.exports = getIgnoreAttributesFn \ No newline at end of file diff --git a/src/xmlbuilder/json2xml.js b/src/xmlbuilder/json2xml.js index f30604a4..d35c6921 100644 --- a/src/xmlbuilder/json2xml.js +++ b/src/xmlbuilder/json2xml.js @@ -1,6 +1,7 @@ 'use strict'; //parse Empty Node as self closing node const buildFromOrderedJs = require('./orderedJs2Xml'); +const getIgnoreAttributesFn = require('../ignoreAttributes') const defaultOptions = { attributeNamePrefix: '@_', @@ -38,11 +39,12 @@ const defaultOptions = { function Builder(options) { this.options = Object.assign({}, defaultOptions, options); - if (this.options.ignoreAttributes || this.options.attributesGroupName) { + if (this.options.ignoreAttributes === true || this.options.attributesGroupName) { this.isAttribute = function(/*a*/) { return false; }; } else { + this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes) this.attrPrefixLen = this.options.attributeNamePrefix.length; this.isAttribute = isAttribute; } @@ -71,13 +73,14 @@ Builder.prototype.build = function(jObj) { [this.options.arrayNodeName] : jObj } } - return this.j2x(jObj, 0).val; + return this.j2x(jObj, 0, []).val; } }; -Builder.prototype.j2x = function(jObj, level) { +Builder.prototype.j2x = function(jObj, level, ajPath) { let attrStr = ''; let val = ''; + const jPath = ajPath.join('.') for (let key in jObj) { if(!Object.prototype.hasOwnProperty.call(jObj, key)) continue; if (typeof jObj[key] === 'undefined') { @@ -100,9 +103,9 @@ Builder.prototype.j2x = function(jObj, level) { } else if (typeof jObj[key] !== 'object') { //premitive type const attr = this.isAttribute(key); - if (attr) { + if (attr && !this.ignoreAttributesFn(attr, jPath)) { attrStr += this.buildAttrPairStr(attr, '' + jObj[key]); - }else { + } else if (!attr) { //tag value if (key === this.options.textNodeName) { let newval = this.options.tagValueProcessor(key, '' + jObj[key]); @@ -126,13 +129,13 @@ Builder.prototype.j2x = function(jObj, level) { // val += this.indentate(level) + '<' + key + '/' + this.tagEndChar; } else if (typeof item === 'object') { if(this.options.oneListGroup){ - const result = this.j2x(item, level + 1); + const result = this.j2x(item, level + 1, ajPath.concat(key)); listTagVal += result.val; if (this.options.attributesGroupName && item.hasOwnProperty(this.options.attributesGroupName)) { listTagAttr += result.attrStr } }else{ - listTagVal += this.processTextOrObjNode(item, key, level) + listTagVal += this.processTextOrObjNode(item, key, level, ajPath) } } else { if (this.options.oneListGroup) { @@ -157,7 +160,7 @@ Builder.prototype.j2x = function(jObj, level) { attrStr += this.buildAttrPairStr(Ks[j], '' + jObj[key][Ks[j]]); } } else { - val += this.processTextOrObjNode(jObj[key], key, level) + val += this.processTextOrObjNode(jObj[key], key, level, ajPath) } } } @@ -172,8 +175,8 @@ Builder.prototype.buildAttrPairStr = function(attrName, val){ } else return ' ' + attrName + '="' + val + '"'; } -function processTextOrObjNode (object, key, level) { - const result = this.j2x(object, level + 1); +function processTextOrObjNode (object, key, level, ajPath) { + const result = this.j2x(object, level + 1, ajPath.concat(key)); if (object[this.options.textNodeName] !== undefined && Object.keys(object).length === 1) { return this.buildTextValNode(object[this.options.textNodeName], key, result.attrStr, level); } else { diff --git a/src/xmlparser/OrderedObjParser.js b/src/xmlparser/OrderedObjParser.js index ffd3f24f..70db0557 100644 --- a/src/xmlparser/OrderedObjParser.js +++ b/src/xmlparser/OrderedObjParser.js @@ -5,6 +5,7 @@ const util = require('../util'); const xmlNode = require('./xmlNode'); const readDocType = require("./DocTypeReader"); const toNumber = require("strnum"); +const getIgnoreAttributesFn = require('../ignoreAttributes') // const regx = // '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)' @@ -53,6 +54,7 @@ class OrderedObjParser{ this.readStopNodeData = readStopNodeData; this.saveTextToParentTag = saveTextToParentTag; this.addChild = addChild; + this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes) } } @@ -125,7 +127,7 @@ function resolveNameSpace(tagname) { const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm'); function buildAttributesMap(attrStr, jPath, tagName) { - if (!this.options.ignoreAttributes && typeof attrStr === 'string') { + if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') { // attrStr = attrStr.replace(/\r?\n/g, ' '); //attrStr = attrStr || attrStr.trim(); @@ -134,6 +136,9 @@ function buildAttributesMap(attrStr, jPath, tagName) { const attrs = {}; for (let i = 0; i < len; i++) { const attrName = this.resolveNameSpace(matches[i][1]); + if (this.ignoreAttributesFn(attrName, jPath)) { + continue + } let oldVal = matches[i][4]; let aName = this.options.attributeNamePrefix + attrName; if (attrName.length) {