xml2json.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. /*
  2. Copyright 2011-2013 Abdulla Abdurakhmanov
  3. Original sources are available at https://code.google.com/p/x2js/
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. */
  14. var DOMParser = require('@xmldom/xmldom').DOMParser;
  15. var x2js = function (config) {
  16. 'use strict';
  17. var VERSION = "1.2.0";
  18. config = config || {};
  19. initConfigDefaults();
  20. initRequiredPolyfills();
  21. function initConfigDefaults() {
  22. if(config.escapeMode === undefined) {
  23. config.escapeMode = true;
  24. }
  25. config.attributePrefix = config.attributePrefix || "_";
  26. config.arrayAccessForm = config.arrayAccessForm || "none";
  27. config.emptyNodeForm = config.emptyNodeForm || "text";
  28. if(config.enableToStringFunc === undefined) {
  29. config.enableToStringFunc = true;
  30. }
  31. config.arrayAccessFormPaths = config.arrayAccessFormPaths || [];
  32. if(config.skipEmptyTextNodesForObj === undefined) {
  33. config.skipEmptyTextNodesForObj = true;
  34. }
  35. if(config.stripWhitespaces === undefined) {
  36. config.stripWhitespaces = true;
  37. }
  38. config.datetimeAccessFormPaths = config.datetimeAccessFormPaths || [];
  39. if(config.useDoubleQuotes === undefined) {
  40. config.useDoubleQuotes = false;
  41. }
  42. config.xmlElementsFilter = config.xmlElementsFilter || [];
  43. config.jsonPropertiesFilter = config.jsonPropertiesFilter || [];
  44. if(config.keepCData === undefined) {
  45. config.keepCData = false;
  46. }
  47. }
  48. var DOMNodeTypes = {
  49. ELEMENT_NODE : 1,
  50. TEXT_NODE : 3,
  51. CDATA_SECTION_NODE : 4,
  52. COMMENT_NODE : 8,
  53. DOCUMENT_NODE : 9
  54. };
  55. function initRequiredPolyfills() {
  56. }
  57. function getNodeLocalName( node ) {
  58. var nodeLocalName = node.localName;
  59. if(nodeLocalName == null) // Yeah, this is IE!!
  60. nodeLocalName = node.baseName;
  61. if(nodeLocalName == null || nodeLocalName=="") // =="" is IE too
  62. nodeLocalName = node.nodeName;
  63. return nodeLocalName;
  64. }
  65. function getNodePrefix(node) {
  66. return node.prefix;
  67. }
  68. function escapeXmlChars(str) {
  69. if(typeof(str) == "string")
  70. return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
  71. else
  72. return str;
  73. }
  74. function unescapeXmlChars(str) {
  75. return str.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, '&');
  76. }
  77. function checkInStdFiltersArrayForm(stdFiltersArrayForm, obj, name, path) {
  78. var idx = 0;
  79. for(; idx < stdFiltersArrayForm.length; idx++) {
  80. var filterPath = stdFiltersArrayForm[idx];
  81. if( typeof filterPath === "string" ) {
  82. if(filterPath == path)
  83. break;
  84. }
  85. else
  86. if( filterPath instanceof RegExp) {
  87. if(filterPath.test(path))
  88. break;
  89. }
  90. else
  91. if( typeof filterPath === "function") {
  92. if(filterPath(obj, name, path))
  93. break;
  94. }
  95. }
  96. return idx!=stdFiltersArrayForm.length;
  97. }
  98. function toArrayAccessForm(obj, childName, path) {
  99. switch(config.arrayAccessForm) {
  100. case "property":
  101. if(!(obj[childName] instanceof Array))
  102. obj[childName+"_asArray"] = [obj[childName]];
  103. else
  104. obj[childName+"_asArray"] = obj[childName];
  105. break;
  106. /*case "none":
  107. break;*/
  108. }
  109. if(!(obj[childName] instanceof Array) && config.arrayAccessFormPaths.length > 0) {
  110. if(checkInStdFiltersArrayForm(config.arrayAccessFormPaths, obj, childName, path)) {
  111. obj[childName] = [obj[childName]];
  112. }
  113. }
  114. }
  115. function fromXmlDateTime(prop) {
  116. // Implementation based up on http://stackoverflow.com/questions/8178598/xml-datetime-to-javascript-date-object
  117. // Improved to support full spec and optional parts
  118. var bits = prop.split(/[-T:+Z]/g);
  119. var d = new Date(bits[0], bits[1]-1, bits[2]);
  120. var secondBits = bits[5].split("\.");
  121. d.setHours(bits[3], bits[4], secondBits[0]);
  122. if(secondBits.length>1)
  123. d.setMilliseconds(secondBits[1]);
  124. // Get supplied time zone offset in minutes
  125. if(bits[6] && bits[7]) {
  126. var offsetMinutes = bits[6] * 60 + Number(bits[7]);
  127. var sign = /\d\d-\d\d:\d\d$/.test(prop)? '-' : '+';
  128. // Apply the sign
  129. offsetMinutes = 0 + (sign == '-'? -1 * offsetMinutes : offsetMinutes);
  130. // Apply offset and local timezone
  131. d.setMinutes(d.getMinutes() - offsetMinutes - d.getTimezoneOffset())
  132. }
  133. else
  134. if(prop.indexOf("Z", prop.length - 1) !== -1) {
  135. d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()));
  136. }
  137. // d is now a local time equivalent to the supplied time
  138. return d;
  139. }
  140. function checkFromXmlDateTimePaths(value, childName, fullPath) {
  141. if(config.datetimeAccessFormPaths.length > 0) {
  142. var path = fullPath.split("\.#")[0];
  143. if(checkInStdFiltersArrayForm(config.datetimeAccessFormPaths, value, childName, path)) {
  144. return fromXmlDateTime(value);
  145. }
  146. else
  147. return value;
  148. }
  149. else
  150. return value;
  151. }
  152. function checkXmlElementsFilter(obj, childType, childName, childPath) {
  153. if( childType == DOMNodeTypes.ELEMENT_NODE && config.xmlElementsFilter.length > 0) {
  154. return checkInStdFiltersArrayForm(config.xmlElementsFilter, obj, childName, childPath);
  155. }
  156. else
  157. return true;
  158. }
  159. function parseDOMChildren( node, path ) {
  160. if(node.nodeType == DOMNodeTypes.DOCUMENT_NODE) {
  161. var result = new Object;
  162. var nodeChildren = node.childNodes;
  163. // Alternative for firstElementChild which is not supported in some environments
  164. for(var cidx=0; cidx <nodeChildren.length; cidx++) {
  165. var child = nodeChildren.item(cidx);
  166. if(child.nodeType == DOMNodeTypes.ELEMENT_NODE) {
  167. var childName = getNodeLocalName(child);
  168. result[childName] = parseDOMChildren(child, childName);
  169. }
  170. }
  171. return result;
  172. }
  173. else
  174. if(node.nodeType == DOMNodeTypes.ELEMENT_NODE) {
  175. var result = new Object;
  176. result.__cnt=0;
  177. var nodeChildren = node.childNodes;
  178. // Children nodes
  179. for(var cidx=0; cidx <nodeChildren.length; cidx++) {
  180. var child = nodeChildren.item(cidx); // nodeChildren[cidx];
  181. var childName = getNodeLocalName(child);
  182. if(child.nodeType!= DOMNodeTypes.COMMENT_NODE) {
  183. var childPath = path+"."+childName;
  184. if (checkXmlElementsFilter(result,child.nodeType,childName,childPath)) {
  185. result.__cnt++;
  186. if(result[childName] == null) {
  187. result[childName] = parseDOMChildren(child, childPath);
  188. toArrayAccessForm(result, childName, childPath);
  189. }
  190. else {
  191. if(result[childName] != null) {
  192. if( !(result[childName] instanceof Array)) {
  193. result[childName] = [result[childName]];
  194. toArrayAccessForm(result, childName, childPath);
  195. }
  196. }
  197. (result[childName])[result[childName].length] = parseDOMChildren(child, childPath);
  198. }
  199. }
  200. }
  201. }
  202. // Attributes
  203. for(var aidx=0; aidx <node.attributes.length; aidx++) {
  204. var attr = node.attributes.item(aidx); // [aidx];
  205. result.__cnt++;
  206. result[config.attributePrefix+attr.name]=attr.value;
  207. }
  208. // Node namespace prefix
  209. var nodePrefix = getNodePrefix(node);
  210. if(nodePrefix!=null && nodePrefix!="") {
  211. result.__cnt++;
  212. result.__prefix=nodePrefix;
  213. }
  214. if(result["#text"]!=null) {
  215. result.__text = result["#text"];
  216. if(result.__text instanceof Array) {
  217. result.__text = result.__text.join("\n");
  218. }
  219. //if(config.escapeMode)
  220. // result.__text = unescapeXmlChars(result.__text);
  221. if(config.stripWhitespaces)
  222. result.__text = result.__text.trim();
  223. delete result["#text"];
  224. if(config.arrayAccessForm=="property")
  225. delete result["#text_asArray"];
  226. result.__text = checkFromXmlDateTimePaths(result.__text, childName, path+"."+childName);
  227. }
  228. if(result["#cdata-section"]!=null) {
  229. result.__cdata = result["#cdata-section"];
  230. delete result["#cdata-section"];
  231. if(config.arrayAccessForm=="property")
  232. delete result["#cdata-section_asArray"];
  233. }
  234. if( result.__cnt == 0 && config.emptyNodeForm=="text" ) {
  235. result = '';
  236. }
  237. else
  238. if( result.__cnt == 1 && result.__text!=null ) {
  239. result = result.__text;
  240. }
  241. else
  242. if( result.__cnt == 1 && result.__cdata!=null && !config.keepCData ) {
  243. result = result.__cdata;
  244. }
  245. else
  246. if ( result.__cnt > 1 && result.__text!=null && config.skipEmptyTextNodesForObj) {
  247. if( (config.stripWhitespaces && result.__text=="") || (result.__text.trim()=="")) {
  248. delete result.__text;
  249. }
  250. }
  251. delete result.__cnt;
  252. if( config.enableToStringFunc && (result.__text!=null || result.__cdata!=null )) {
  253. result.toString = function() {
  254. return (this.__text!=null? this.__text:'')+( this.__cdata!=null ? this.__cdata:'');
  255. };
  256. }
  257. return result;
  258. }
  259. else
  260. if(node.nodeType == DOMNodeTypes.TEXT_NODE || node.nodeType == DOMNodeTypes.CDATA_SECTION_NODE) {
  261. return node.nodeValue;
  262. }
  263. }
  264. function startTag(jsonObj, element, attrList, closed) {
  265. var resultStr = "<"+ ( (jsonObj!=null && jsonObj.__prefix!=null)? (jsonObj.__prefix+":"):"") + element;
  266. if(attrList!=null) {
  267. for(var aidx = 0; aidx < attrList.length; aidx++) {
  268. var attrName = attrList[aidx];
  269. var attrVal = jsonObj[attrName];
  270. if(config.escapeMode)
  271. attrVal=escapeXmlChars(attrVal);
  272. resultStr+=" "+attrName.substr(config.attributePrefix.length)+"=";
  273. if(config.useDoubleQuotes)
  274. resultStr+='"'+attrVal+'"';
  275. else
  276. resultStr+="'"+attrVal+"'";
  277. }
  278. }
  279. if(!closed)
  280. resultStr+=">";
  281. else
  282. resultStr+="/>";
  283. return resultStr;
  284. }
  285. function endTag(jsonObj,elementName) {
  286. return "</"+ (jsonObj.__prefix!=null? (jsonObj.__prefix+":"):"")+elementName+">";
  287. }
  288. function endsWith(str, suffix) {
  289. return str.indexOf(suffix, str.length - suffix.length) !== -1;
  290. }
  291. function jsonXmlSpecialElem ( jsonObj, jsonObjField ) {
  292. if((config.arrayAccessForm=="property" && endsWith(jsonObjField.toString(),("_asArray")))
  293. || jsonObjField.toString().indexOf(config.attributePrefix)==0
  294. || jsonObjField.toString().indexOf("__")==0
  295. || (jsonObj[jsonObjField] instanceof Function) )
  296. return true;
  297. else
  298. return false;
  299. }
  300. function jsonXmlElemCount ( jsonObj ) {
  301. var elementsCnt = 0;
  302. if(jsonObj instanceof Object ) {
  303. for( var it in jsonObj ) {
  304. if(jsonXmlSpecialElem ( jsonObj, it) )
  305. continue;
  306. elementsCnt++;
  307. }
  308. }
  309. return elementsCnt;
  310. }
  311. function checkJsonObjPropertiesFilter(jsonObj, propertyName, jsonObjPath) {
  312. return config.jsonPropertiesFilter.length == 0
  313. || jsonObjPath==""
  314. || checkInStdFiltersArrayForm(config.jsonPropertiesFilter, jsonObj, propertyName, jsonObjPath);
  315. }
  316. function parseJSONAttributes ( jsonObj ) {
  317. var attrList = [];
  318. if(jsonObj instanceof Object ) {
  319. for( var ait in jsonObj ) {
  320. if(ait.toString().indexOf("__")== -1 && ait.toString().indexOf(config.attributePrefix)==0) {
  321. attrList.push(ait);
  322. }
  323. }
  324. }
  325. return attrList;
  326. }
  327. function parseJSONTextAttrs ( jsonTxtObj ) {
  328. var result ="";
  329. if(jsonTxtObj.__cdata!=null) {
  330. result+="<![CDATA["+jsonTxtObj.__cdata+"]]>";
  331. }
  332. if(jsonTxtObj.__text!=null) {
  333. if(config.escapeMode)
  334. result+=escapeXmlChars(jsonTxtObj.__text);
  335. else
  336. result+=jsonTxtObj.__text;
  337. }
  338. return result;
  339. }
  340. function parseJSONTextObject ( jsonTxtObj ) {
  341. var result ="";
  342. if( jsonTxtObj instanceof Object ) {
  343. result+=parseJSONTextAttrs ( jsonTxtObj );
  344. }
  345. else
  346. if(jsonTxtObj!=null) {
  347. if(config.escapeMode)
  348. result+=escapeXmlChars(jsonTxtObj);
  349. else
  350. result+=jsonTxtObj;
  351. }
  352. return result;
  353. }
  354. function getJsonPropertyPath(jsonObjPath, jsonPropName) {
  355. if (jsonObjPath==="") {
  356. return jsonPropName;
  357. }
  358. else
  359. return jsonObjPath+"."+jsonPropName;
  360. }
  361. function parseJSONArray ( jsonArrRoot, jsonArrObj, attrList, jsonObjPath ) {
  362. var result = "";
  363. if(jsonArrRoot.length == 0) {
  364. result+=startTag(jsonArrRoot, jsonArrObj, attrList, true);
  365. }
  366. else {
  367. for(var arIdx = 0; arIdx < jsonArrRoot.length; arIdx++) {
  368. result+=startTag(jsonArrRoot[arIdx], jsonArrObj, parseJSONAttributes(jsonArrRoot[arIdx]), false);
  369. result+=parseJSONObject(jsonArrRoot[arIdx], getJsonPropertyPath(jsonObjPath,jsonArrObj));
  370. result+=endTag(jsonArrRoot[arIdx],jsonArrObj);
  371. }
  372. }
  373. return result;
  374. }
  375. function parseJSONObject ( jsonObj, jsonObjPath ) {
  376. var result = "";
  377. var elementsCnt = jsonXmlElemCount ( jsonObj );
  378. if(elementsCnt > 0) {
  379. for( var it in jsonObj ) {
  380. if(jsonXmlSpecialElem ( jsonObj, it) || (jsonObjPath!="" && !checkJsonObjPropertiesFilter(jsonObj, it, getJsonPropertyPath(jsonObjPath,it))) )
  381. continue;
  382. var subObj = jsonObj[it];
  383. var attrList = parseJSONAttributes( subObj )
  384. if(subObj == null || subObj == undefined) {
  385. result+=startTag(subObj, it, attrList, true);
  386. }
  387. else
  388. if(subObj instanceof Object) {
  389. if(subObj instanceof Array) {
  390. result+=parseJSONArray( subObj, it, attrList, jsonObjPath );
  391. }
  392. else if(subObj instanceof Date) {
  393. result+=startTag(subObj, it, attrList, false);
  394. result+=subObj.toISOString();
  395. result+=endTag(subObj,it);
  396. }
  397. else {
  398. var subObjElementsCnt = jsonXmlElemCount ( subObj );
  399. if(subObjElementsCnt > 0 || subObj.__text!=null || subObj.__cdata!=null) {
  400. result+=startTag(subObj, it, attrList, false);
  401. result+=parseJSONObject(subObj, getJsonPropertyPath(jsonObjPath,it));
  402. result+=endTag(subObj,it);
  403. }
  404. else {
  405. result+=startTag(subObj, it, attrList, true);
  406. }
  407. }
  408. }
  409. else {
  410. result+=startTag(subObj, it, attrList, false);
  411. result+=parseJSONTextObject(subObj);
  412. result+=endTag(subObj,it);
  413. }
  414. }
  415. }
  416. result+=parseJSONTextObject(jsonObj);
  417. return result;
  418. }
  419. this.parseXmlString = function(xmlDocStr) {
  420. // var isIEParser = window.ActiveXObject || "ActiveXObject" in window;
  421. var isIEParser = false;
  422. if (xmlDocStr === undefined) {
  423. return null;
  424. }
  425. var xmlDoc;
  426. if (DOMParser) {
  427. var parser=new DOMParser();
  428. var parsererrorNS = null;
  429. // IE9+ now is here
  430. if(!isIEParser) {
  431. try {
  432. parsererrorNS = parser.parseFromString("INVALID", "text/xml").getElementsByTagName("parsererror")[0].namespaceURI;
  433. }
  434. catch(err) {
  435. parsererrorNS = null;
  436. }
  437. }
  438. try {
  439. xmlDoc = parser.parseFromString( xmlDocStr, "text/xml" );
  440. if( parsererrorNS!= null && xmlDoc.getElementsByTagNameNS(parsererrorNS, "parsererror").length > 0) {
  441. //throw new Error('Error parsing XML: '+xmlDocStr);
  442. xmlDoc = null;
  443. }
  444. }
  445. catch(err) {
  446. xmlDoc = null;
  447. }
  448. }
  449. else {
  450. // IE :(
  451. if(xmlDocStr.indexOf("<?")==0) {
  452. xmlDocStr = xmlDocStr.substr( xmlDocStr.indexOf("?>") + 2 );
  453. }
  454. xmlDoc=new ActiveXObject("Microsoft.XMLDOM");
  455. xmlDoc.async="false";
  456. xmlDoc.loadXML(xmlDocStr);
  457. }
  458. return xmlDoc;
  459. };
  460. this.asArray = function(prop) {
  461. if (prop === undefined || prop == null)
  462. return [];
  463. else
  464. if(prop instanceof Array)
  465. return prop;
  466. else
  467. return [prop];
  468. };
  469. this.toXmlDateTime = function(dt) {
  470. if(dt instanceof Date)
  471. return dt.toISOString();
  472. else
  473. if(typeof(dt) === 'number' )
  474. return new Date(dt).toISOString();
  475. else
  476. return null;
  477. };
  478. this.asDateTime = function(prop) {
  479. if(typeof(prop) == "string") {
  480. return fromXmlDateTime(prop);
  481. }
  482. else
  483. return prop;
  484. };
  485. this.xml2json = function (xmlDoc) {
  486. return parseDOMChildren ( xmlDoc );
  487. };
  488. this.xml_str2json = function (xmlDocStr) {
  489. var xmlDoc = this.parseXmlString(xmlDocStr);
  490. if(xmlDoc!=null)
  491. return this.xml2json(xmlDoc);
  492. else
  493. return null;
  494. };
  495. this.json2xml_str = function (jsonObj) {
  496. return parseJSONObject ( jsonObj, "" );
  497. };
  498. this.json2xml = function (jsonObj) {
  499. var xmlDocStr = this.json2xml_str (jsonObj);
  500. return this.parseXmlString(xmlDocStr);
  501. };
  502. this.getVersion = function () {
  503. return VERSION;
  504. };
  505. };
  506. var xml2json = function (str) {
  507. if (!str) return null;
  508. var parser = new DOMParser();
  509. var xmlDoc = parser.parseFromString(str, "text/xml");
  510. var x2jsObj = new x2js();
  511. var data = x2jsObj.xml2json(xmlDoc);
  512. if (data.html && data.getElementsByTagName('parsererror').length) {
  513. return null;
  514. } else {
  515. return data;
  516. }
  517. };
  518. var json2xml = function (data) {
  519. var x2jsObj = new x2js();
  520. return x2jsObj.json2xml(data);
  521. };
  522. module.exports = xml2json;