diff --git a/.vscode/launch.json b/.vscode/launch.json index a8e11b0..b6a45ab 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,7 +4,6 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { "type": "node", "request": "launch", diff --git a/lib/xml2js.js b/lib/xml2js.js index 4ce7cdf..11ead12 100644 --- a/lib/xml2js.js +++ b/lib/xml2js.js @@ -35,6 +35,16 @@ function validateOptions(userOptions) { helper.ensureKeyExists('name', options); helper.ensureKeyExists('elements', options); helper.ensureKeyExists('parent', options); + helper.checkFnExists('doctype', options); + helper.checkFnExists('instruction', options); + helper.checkFnExists('cdata', options); + helper.checkFnExists('comment', options); + helper.checkFnExists('text', options); + helper.checkFnExists('instructionName', options); + helper.checkFnExists('elementName', options); + helper.checkFnExists('attributeName', options); + helper.checkFnExists('attributeValue', options); + helper.checkFnExists('attributes', options); return options; } @@ -52,7 +62,8 @@ function nativeType(value) { return value; } -function addField(type, value, options) { +function addField(type, value) { + var key; if (options.compact) { if (!currentElement[options[type + 'Key']] && options.alwaysArray) { currentElement[options[type + 'Key']] = []; @@ -60,6 +71,22 @@ function addField(type, value, options) { if (currentElement[options[type + 'Key']] && !(currentElement[options[type + 'Key']] instanceof Array)) { currentElement[options[type + 'Key']] = [currentElement[options[type + 'Key']]]; } + if (type + 'Fn' in options && typeof value === 'string') { + value = options[type + 'Fn'](value); + } + if (type === 'instruction' && ('instructionFn' in options || 'instructionNameFn' in options)) { + for (key in value) { + if (value.hasOwnProperty(key)) { + if ('instructionFn' in options) { + value[key] = options.instructionFn(value[key], key, currentElement); + } else { + var temp = value[key]; + delete value[key]; + value[options.instructionNameFn(key, currentElement)] = temp; + } + } + } + } if (currentElement[options[type + 'Key']] instanceof Array) { currentElement[options[type + 'Key']].push(value); } else { @@ -69,21 +96,30 @@ function addField(type, value, options) { if (!currentElement[options.elementsKey]) { currentElement[options.elementsKey] = []; } - var key, element = {}; + var element = {}; element[options.typeKey] = type; - if (type === 'instruction' && typeof value === 'object') { + if (type === 'instruction') { for (key in value) { if (value.hasOwnProperty(key)) { break; } } - element[options.nameKey] = key; + element[options.nameKey] = 'instructionNameFn' in options ? options.instructionNameFn(key, currentElement) : key; if (options.instructionHasAttributes) { element[options.attributesKey] = value[key][options.attributesKey]; + if ('attributeNameFn' in options) { + element[options.attributesKey] = options[type + 'Fn'](element[options.attributesKey]); + } } else { + if (type + 'Fn' in options) { + value[key] = options[type + 'Fn'](value[key]); + } element[options[type + 'Key']] = value[key]; } } else { + if (type + 'Fn' in options) { + value = options[type + 'Fn'](value); + } element[options[type + 'Key']] = value; } if (options.addParent) { @@ -93,6 +129,27 @@ function addField(type, value, options) { } } +function manipulateAttributes(attributes) { + if ('attributesFn' in options && attributes) { + attributes = options.attributesFn(attributes, currentElement); + } + if ((options.trim || 'attributeValueFn' in options || 'attributeNameFn' in options) && attributes) { + var key; + for (key in attributes) { + if (attributes.hasOwnProperty(key)) { + if (options.trim) attributes[key] = attributes[key].trim(); + if ('attributeValueFn' in options) attributes[key] = options.attributeValueFn(attributes[key], key, currentElement); + if ('attributeNameFn' in options) { + var temp = attributes[key]; + delete attributes[key]; + attributes[options.attributeNameFn(key, attributes[key], currentElement)] = temp; + } + } + } + } + return attributes; +} + function onInstruction(instruction) { var attributes = {}; if (instruction.body && (instruction.name.toLowerCase() === 'xml' || options.instructionHasAttributes)) { @@ -101,6 +158,7 @@ function onInstruction(instruction) { while ((match = attrsRegExp.exec(instruction.body)) !== null) { attributes[match[1]] = match[2] || match[3] || match[4]; } + attributes = manipulateAttributes(attributes); } if (instruction.name.toLowerCase() === 'xml') { if (options.ignoreDeclaration) { @@ -127,27 +185,25 @@ function onInstruction(instruction) { } else { value[instruction.name] = instruction.body; } - addField('instruction', value, options); + addField('instruction', value); } } function onStartElement(name, attributes) { - var key, element; + var element; if (typeof name === 'object') { attributes = name.attributes; name = name.name; } - if (options.trim && attributes) { - for (key in attributes) { - if (attributes.hasOwnProperty(key)) { - attributes[key] = attributes[key].trim(); - } - } + attributes = manipulateAttributes(attributes); + if ('elementNameFn' in options) { + name = options.elementNameFn(name, currentElement); } if (options.compact) { element = {}; if (!options.ignoreAttributes && attributes && Object.keys(attributes).length) { element[options.attributesKey] = {}; + var key; for (key in attributes) { if (attributes.hasOwnProperty(key)) { element[options.attributesKey][key] = attributes[key]; @@ -202,7 +258,7 @@ function onText(text) { if (options.sanitize) { text = text.replace(/&/g, '&').replace(//g, '>'); } - addField('text', text, options); + addField('text', text); } function onComment(comment) { @@ -212,7 +268,7 @@ function onComment(comment) { if (options.trim) { comment = comment.trim(); } - addField('comment', comment, options); + addField('comment', comment); } function onEndElement(name) { @@ -230,7 +286,7 @@ function onCdata(cdata) { if (options.trim) { cdata = cdata.trim(); } - addField('cdata', cdata, options); + addField('cdata', cdata); } function onDoctype(doctype) { @@ -241,7 +297,7 @@ function onDoctype(doctype) { if (options.trim) { doctype = doctype.trim(); } - addField('doctype', doctype, options); + addField('doctype', doctype); } function onError(error) { diff --git a/test/test-items.js b/test/test-items.js index 30d2f3c..5335d92 100644 --- a/test/test-items.js +++ b/test/test-items.js @@ -20,10 +20,15 @@ var cases = [ js1: {"_declaration":{},"a":{"b":{}}}, js2: {"declaration":{},"elements":[{"type":"element","name":"a","elements":[{"type":"element","name":"b"}]}]} }, { - desc: 'processing instruction ', + desc: 'processing instruction ', xml: '', js1: {"_instruction":{"go": "there"}}, js2: {"elements":[{"type":"instruction","name":"go","instruction":"there"}]} + }, { + desc: '2 processing instructions ', + xml: '', + js1: {"_instruction":[{"go": "there"},{"come": "here"}]}, + js2: {"elements":[{"type":"instruction","name":"go","instruction":"there"},{"type":"instruction","name":"come","instruction":"here"}]} }, { desc: 'should convert comment', xml: '', @@ -100,21 +105,24 @@ var cases = [ module.exports = function (direction, options) { var i, tests = []; options = options || {}; - function applyOptions (obj, fullKey) { - var key, fn; + function applyOptions (obj, pathKey) { + var key, fullKey; + pathKey = pathKey || ''; if (obj instanceof Array) { obj = obj.filter(function (el) { return !(options.ignoreText && el.type === 'text' || options.ignoreComment && el.type === 'comment' || options.ignoreCdata && el.type === 'cdata' || options.ignoreDoctype && el.type === 'doctype' || options.ignoreDeclaration && el.type === 'declaration' || options.ignoreInstruction && el.type === 'instruction'); }).map(function (el) { - return manipulate(el, fullKey); + return manipulate(el, pathKey); }); } else if (typeof obj === 'object') { for (key in obj) { - fullKey = (fullKey? fullKey + '.' : '') + key; + fullKey = (pathKey ? pathKey + '.' : '') + key; if (options.compact && options.alwaysArray && !(obj[key] instanceof Array) && key !== '_declaration' && (key === '_instruction' || fullKey.indexOf('_instruction') < 0) && fullKey.indexOf('_attributes') < 0) { obj[key] = [obj[key]]; } + key = applyNameCallbacks(obj, key, pathKey.split('.').pop()); + key = applyAttributesCallback(obj, key, pathKey.split('.').pop()); if (key.indexOf('_') === 0 && obj[key] instanceof Array) { obj[key] = obj[key].map(function (el) { return manipulate(el, fullKey); @@ -154,10 +162,13 @@ module.exports = function (direction, options) { // } } return obj; - function manipulate(x, key) { - if (x instanceof Array || typeof x === 'object') { - return applyOptions(x, key); + function manipulate(x, fullKey) { + if (x instanceof Array) { + return applyOptions(x, fullKey); + } if (typeof x === 'object') { + return applyOptions(x, fullKey); } else if (typeof x === 'string') { + x = applyValueCallbacks(x, fullKey.split('.').pop(), fullKey.split('.')[fullKey.split('.').length - 2] || ''); return options.trim? x.trim() : x; } else if (typeof x === 'number' || typeof x === 'boolean') { return options.nativeType? x.toString() : x; @@ -176,15 +187,57 @@ module.exports = function (direction, options) { } return js; } + function applyNameCallbacks(obj, key, parentKey) { + if ('instructionNameFn' in options && (options.compact && parentKey === '_instruction' || !options.compact && obj.type === 'instruction') + || 'elementNameFn' in options && (options.compact && key.indexOf('_') < 0 && parentKey !== '_attributes' && parentKey !== '_instruction' || !options.compact && obj.type === 'element')) { + if (options.compact) { + var temp = obj[key]; + delete obj[key]; + key = 'elementNameFn' in options ? options.elementNameFn(key) : options.instructionNameFn(key); + obj[key] = temp; + } else { + obj.name = 'elementNameFn' in options ? options.elementNameFn(obj.name) : options.instructionNameFn(obj.name); + } + } + return key; + } + function applyAttributesCallback(obj, key, parentKey) { + if (('attributeNameFn' in options || 'attributeValueFn' in options) && (parentKey === '_attributes' || parentKey === 'attributes')) { + if ('attributeNameFn' in options) { + var temp = obj[key]; + delete obj[key]; + key = options.attributeNameFn(key); + obj[key] = temp; + } + if ('attributeValueFn' in options) { + obj[key] = options.attributeValueFn(obj[key]); + } + } + if ('attributesFn' in options && (key === '_attributes' || key === 'attributes')) { + obj[key] = options.attributesFn(obj[key]); + } + return key; + } + function applyValueCallbacks(value, key, parentKey) { + var fn; + for (fn in options) { + if (fn.match(/Fn$/) && !fn.match(/NameFn$/)) { + var callbackName = (options.compact ? '_' : '') + fn.replace('Fn', ''); + if (key === callbackName || parentKey === callbackName) { + value = options[fn](value); + } + } + } + return value; + } for (i = 0; i < cases.length; ++i) { tests.push({desc: cases[i].desc, xml: null, js: null}); tests[i].js = options.compact ? cases[i].js1 : cases[i].js2; + tests[i].xml = cases[i].xml; if (direction === 'xml2js') { tests[i].js = applyOptions(JSON.parse(JSON.stringify(tests[i].js))); tests[i].js = applyKeyNames(tests[i].js); - } - tests[i].xml = cases[i].xml; - if (direction === 'js2xml') { + } else if (direction === 'js2xml') { if (!('spaces' in options) || options.spaces === 0 || typeof options.spaces === 'boolean') { tests[i].xml = tests[i].xml.replace(/>\n\v*/gm, '>'); } if ('spaces' in options && options.spaces !== 0 && typeof options.spaces === 'number') { tests[i].xml = tests[i].xml.replace(/\v/g, Array(options.spaces + 1).join(' ')); } if ('spaces' in options && typeof options.spaces === 'string') { tests[i].xml = tests[i].xml.replace(/\v/g, options.spaces); } diff --git a/test/xml2js-callbacks_test.js b/test/xml2js-callbacks_test.js new file mode 100644 index 0000000..6127da4 --- /dev/null +++ b/test/xml2js-callbacks_test.js @@ -0,0 +1,295 @@ +var convert = require('../lib'); +var testItems = require('./test-items'); + +/*global describe,it,expect*/ + +function manipulate(val) { + return val.toUpperCase(); +} + +function manipulateAttribute(obj) { + var key, temp; + for (key in obj) { + temp = obj[key]; + delete obj[key]; + obj[key.toUpperCase()] = temp.toUpperCase(); + } + return obj; +} + +describe('Testing xml2js.js:', function () { + + describe('Adding function callbacks, options = {compact: false}', function () { + + describe('options = {doctypeFn: manipulate}', function () { + + var options = {compact: false, doctypeFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {instructionFn: manipulate}', function () { + + var options = {compact: false, instructionFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + // console.log(JSON.stringify(convert.xml2js(test.xml, options))); + }); + }); + + }); + + describe('options = {cdataFn: manipulate}', function () { + + var options = {compact: false, cdataFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {commentFn: manipulate}', function () { + + var options = {compact: false, commentFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {textFn: manipulate}', function () { + + var options = {compact: false, textFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {instructionNameFn: manipulate}', function () { + + var options = {compact: false, instructionNameFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {elementNameFn: manipulate}', function () { + + var options = {compact: false, elementNameFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {attributeNameFn: manipulate}', function () { + + var options = {compact: false, attributeNameFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {attributeValueFn: manipulate}', function () { + + var options = {compact: false, attributeValueFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {attributesFn: manipulateAttribute}', function () { + + var options = {compact: false, attributesFn: manipulateAttribute}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {doctypeFn: manipulate, instructionFn: manipulate, cdataFn: manipulate, commentFn: manipulate, textFn: manipulate}', function () { + + var options = {compact: false, doctypeFn: manipulate, instructionFn: manipulate, cdataFn: manipulate, commentFn: manipulate, textFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {instructionNameFn: manipulate, elementNameFn: manipulate, attributeNameFn: manipulate, attributeValueFn: manipulate}', function () { + + var options = {compact: false, instructionNameFn: manipulate, elementNameFn: manipulate, attributeNameFn: manipulate, attributeValueFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + }); + + describe('Adding function callbacks, options = {compact: true}', function () { + + describe('options = {doctypeFn: manipulate}', function () { + + var options = {compact: true, doctypeFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {instructionFn: manipulate}', function () { + + var options = {compact: true, instructionFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {cdataFn: manipulate}', function () { + + var options = {compact: true, cdataFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {commentFn: manipulate}', function () { + + var options = {compact: true, commentFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {textFn: manipulate}', function () { + + var options = {compact: true, textFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {instructionNameFn: manipulate}', function () { + + var options = {compact: true, instructionNameFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {elementNameFn: manipulate}', function () { + + var options = {compact: true, elementNameFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {attributeNameFn: manipulate}', function () { + + var options = {compact: true, attributeNameFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {attributeValueFn: manipulate}', function () { + + var options = {compact: true, attributeValueFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {attributesFn: manipulateAttribute}', function () { + + var options = {compact: true, attributesFn: manipulateAttribute}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {doctypeFn: manipulate, instructionFn: manipulate, cdataFn: manipulate, commentFn: manipulate, textFn: manipulate}', function () { + + var options = {compact: true, doctypeFn: manipulate, instructionFn: manipulate, cdataFn: manipulate, commentFn: manipulate, textFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + describe('options = {instructionNameFn: manipulate, elementNameFn: manipulate, attributeNameFn: manipulate, attributeValueFn: manipulate}', function () { + + var options = {compact: true, instructionNameFn: manipulate, elementNameFn: manipulate, attributeNameFn: manipulate, attributeValueFn: manipulate}; + testItems('xml2js', options).forEach(function (test) { + it(test.desc, function () { + expect(convert.xml2js(test.xml, options)).toEqual(test.js); + }); + }); + + }); + + }); + +});