2 Copyright 2011-2013 Abdulla Abdurakhmanov
\r
3 Original sources are available at https://code.google.com/p/x2js/
\r
5 Licensed under the Apache License, Version 2.0 (the "License");
\r
6 you may not use this file except in compliance with the License.
\r
7 You may obtain a copy of the License at
\r
9 http://www.apache.org/licenses/LICENSE-2.0
\r
11 Unless required by applicable law or agreed to in writing, software
\r
12 distributed under the License is distributed on an "AS IS" BASIS,
\r
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\r
14 See the License for the specific language governing permissions and
\r
15 limitations under the License.
\r
18 function X2JS(config) {
\r
21 var VERSION = "1.1.5";
\r
23 config = config || {};
\r
24 initConfigDefaults();
\r
25 initRequiredPolyfills();
\r
27 function initConfigDefaults() {
\r
28 if(config.escapeMode === undefined) {
\r
29 config.escapeMode = true;
\r
31 config.attributePrefix = config.attributePrefix || "_";
\r
32 config.arrayAccessForm = config.arrayAccessForm || "none";
\r
33 config.emptyNodeForm = config.emptyNodeForm || "text";
\r
34 if(config.enableToStringFunc === undefined) {
\r
35 config.enableToStringFunc = true;
\r
37 config.arrayAccessFormPaths = config.arrayAccessFormPaths || [];
\r
38 if(config.skipEmptyTextNodesForObj === undefined) {
\r
39 config.skipEmptyTextNodesForObj = true;
\r
41 if(config.stripWhitespaces === undefined) {
\r
42 config.stripWhitespaces = true;
\r
44 config.datetimeAccessFormPaths = config.datetimeAccessFormPaths || [];
\r
47 var DOMNodeTypes = {
\r
50 CDATA_SECTION_NODE : 4,
\r
55 function initRequiredPolyfills() {
\r
56 function pad(number) {
\r
57 var r = String(number);
\r
58 if ( r.length === 1 ) {
\r
64 if(typeof String.prototype.trim !== 'function') {
\r
65 String.prototype.trim = function() {
\r
66 return this.replace(/^\s+|^\n+|(\s|\n)+$/g, '');
\r
69 if(typeof Date.prototype.toISOString !== 'function') {
\r
70 // Implementation from http://stackoverflow.com/questions/2573521/how-do-i-output-an-iso-8601-formatted-string-in-javascript
\r
71 Date.prototype.toISOString = function() {
\r
72 return this.getUTCFullYear()
\r
73 + '-' + pad( this.getUTCMonth() + 1 )
\r
74 + '-' + pad( this.getUTCDate() )
\r
75 + 'T' + pad( this.getUTCHours() )
\r
76 + ':' + pad( this.getUTCMinutes() )
\r
77 + ':' + pad( this.getUTCSeconds() )
\r
78 + '.' + String( (this.getUTCMilliseconds()/1000).toFixed(3) ).slice( 2, 5 )
\r
84 function getNodeLocalName( node ) {
\r
85 var nodeLocalName = node.localName;
\r
86 if(nodeLocalName == null) // Yeah, this is IE!!
\r
87 nodeLocalName = node.baseName;
\r
88 if(nodeLocalName == null || nodeLocalName=="") // =="" is IE too
\r
89 nodeLocalName = node.nodeName;
\r
90 return nodeLocalName;
\r
93 function getNodePrefix(node) {
\r
97 function escapeXmlChars(str) {
\r
98 if(typeof(str) == "string")
\r
99 return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/');
\r
104 function unescapeXmlChars(str) {
\r
105 return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(///g, '\/');
\r
108 function toArrayAccessForm(obj, childName, path) {
\r
109 switch(config.arrayAccessForm) {
\r
111 if(!(obj[childName] instanceof Array))
\r
112 obj[childName+"_asArray"] = [obj[childName]];
\r
114 obj[childName+"_asArray"] = obj[childName];
\r
120 if(!(obj[childName] instanceof Array) && config.arrayAccessFormPaths.length > 0) {
\r
122 for(; idx < config.arrayAccessFormPaths.length; idx++) {
\r
123 var arrayPath = config.arrayAccessFormPaths[idx];
\r
124 if( typeof arrayPath === "string" ) {
\r
125 if(arrayPath == path)
\r
129 if( arrayPath instanceof RegExp) {
\r
130 if(arrayPath.test(path))
\r
134 if( typeof arrayPath === "function") {
\r
135 if(arrayPath(obj, childName, path))
\r
139 if(idx!=config.arrayAccessFormPaths.length) {
\r
140 obj[childName] = [obj[childName]];
\r
145 function fromXmlDateTime(prop) {
\r
146 // Implementation based up on http://stackoverflow.com/questions/8178598/xml-datetime-to-javascript-date-object
\r
147 // Improved to support full spec and optional parts
\r
148 var bits = prop.split(/[-T:+Z]/g);
\r
150 var d = new Date(bits[0], bits[1]-1, bits[2]);
\r
151 var secondBits = bits[5].split("\.");
\r
152 d.setHours(bits[3], bits[4], secondBits[0]);
\r
153 if(secondBits.length>1)
\r
154 d.setMilliseconds(secondBits[1]);
\r
156 // Get supplied time zone offset in minutes
\r
157 if(bits[6] && bits[7]) {
\r
158 var offsetMinutes = bits[6] * 60 + Number(bits[7]);
\r
159 var sign = /\d\d-\d\d:\d\d$/.test(prop)? '-' : '+';
\r
162 offsetMinutes = 0 + (sign == '-'? -1 * offsetMinutes : offsetMinutes);
\r
164 // Apply offset and local timezone
\r
165 d.setMinutes(d.getMinutes() - offsetMinutes - d.getTimezoneOffset())
\r
168 if(prop.indexOf("Z", prop.length - 1) !== -1) {
\r
169 d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()));
\r
172 // d is now a local time equivalent to the supplied time
\r
176 function checkFromXmlDateTimePaths(value, childName, fullPath) {
\r
177 if(config.datetimeAccessFormPaths.length > 0) {
\r
178 var path = fullPath.split("\.#")[0];
\r
180 for(; idx < config.datetimeAccessFormPaths.length; idx++) {
\r
181 var dtPath = config.datetimeAccessFormPaths[idx];
\r
182 if( typeof dtPath === "string" ) {
\r
187 if( dtPath instanceof RegExp) {
\r
188 if(dtPath.test(path))
\r
192 if( typeof dtPath === "function") {
\r
193 if(dtPath(obj, childName, path))
\r
197 if(idx!=config.datetimeAccessFormPaths.length) {
\r
198 return fromXmlDateTime(value);
\r
207 function parseDOMChildren( node, path ) {
\r
208 if(node.nodeType == DOMNodeTypes.DOCUMENT_NODE) {
\r
209 var result = new Object;
\r
210 var nodeChildren = node.childNodes;
\r
211 // Alternative for firstElementChild which is not supported in some environments
\r
212 for(var cidx=0; cidx <nodeChildren.length; cidx++) {
\r
213 var child = nodeChildren.item(cidx);
\r
214 if(child.nodeType == DOMNodeTypes.ELEMENT_NODE) {
\r
215 var childName = getNodeLocalName(child);
\r
216 result[childName] = parseDOMChildren(child, childName);
\r
222 if(node.nodeType == DOMNodeTypes.ELEMENT_NODE) {
\r
223 var result = new Object;
\r
226 var nodeChildren = node.childNodes;
\r
229 for(var cidx=0; cidx <nodeChildren.length; cidx++) {
\r
230 var child = nodeChildren.item(cidx); // nodeChildren[cidx];
\r
231 var childName = getNodeLocalName(child);
\r
233 if(child.nodeType!= DOMNodeTypes.COMMENT_NODE) {
\r
235 if(result[childName] == null) {
\r
236 result[childName] = parseDOMChildren(child, path+"."+childName);
\r
237 toArrayAccessForm(result, childName, path+"."+childName);
\r
240 if(result[childName] != null) {
\r
241 if( !(result[childName] instanceof Array)) {
\r
242 result[childName] = [result[childName]];
\r
243 toArrayAccessForm(result, childName, path+"."+childName);
\r
246 (result[childName])[result[childName].length] = parseDOMChildren(child, path+"."+childName);
\r
252 for(var aidx=0; aidx <node.attributes.length; aidx++) {
\r
253 var attr = node.attributes.item(aidx); // [aidx];
\r
255 result[config.attributePrefix+attr.name]=attr.value;
\r
258 // Node namespace prefix
\r
259 var nodePrefix = getNodePrefix(node);
\r
260 if(nodePrefix!=null && nodePrefix!="") {
\r
262 result.__prefix=nodePrefix;
\r
265 if(result["#text"]!=null) {
\r
266 result.__text = result["#text"];
\r
267 if(result.__text instanceof Array) {
\r
268 result.__text = result.__text.join("\n");
\r
270 if(config.escapeMode)
\r
271 result.__text = unescapeXmlChars(result.__text);
\r
272 if(config.stripWhitespaces)
\r
273 result.__text = result.__text.trim();
\r
274 delete result["#text"];
\r
275 if(config.arrayAccessForm=="property")
\r
276 delete result["#text_asArray"];
\r
277 result.__text = checkFromXmlDateTimePaths(result.__text, childName, path+"."+childName);
\r
279 if(result["#cdata-section"]!=null) {
\r
280 result.__cdata = result["#cdata-section"];
\r
281 delete result["#cdata-section"];
\r
282 if(config.arrayAccessForm=="property")
\r
283 delete result["#cdata-section_asArray"];
\r
286 if( result.__cnt == 1 && result.__text!=null ) {
\r
287 result = result.__text;
\r
290 if( result.__cnt == 0 && config.emptyNodeForm=="text" ) {
\r
294 if ( result.__cnt > 1 && result.__text!=null && config.skipEmptyTextNodesForObj) {
\r
295 if( (config.stripWhitespaces && result.__text=="") || (result.__text.trim()=="")) {
\r
296 delete result.__text;
\r
299 delete result.__cnt;
\r
301 if( config.enableToStringFunc && (result.__text!=null || result.__cdata!=null )) {
\r
302 result.toString = function() {
\r
303 return (this.__text!=null? this.__text:'')+( this.__cdata!=null ? this.__cdata:'');
\r
310 if(node.nodeType == DOMNodeTypes.TEXT_NODE || node.nodeType == DOMNodeTypes.CDATA_SECTION_NODE) {
\r
311 return node.nodeValue;
\r
315 function startTag(jsonObj, element, attrList, closed) {
\r
316 var resultStr = "<"+ ( (jsonObj!=null && jsonObj.__prefix!=null)? (jsonObj.__prefix+":"):"") + element;
\r
317 if(attrList!=null) {
\r
318 for(var aidx = 0; aidx < attrList.length; aidx++) {
\r
319 var attrName = attrList[aidx];
\r
320 var attrVal = jsonObj[attrName];
\r
321 if(config.escapeMode)
\r
322 attrVal=escapeXmlChars(attrVal);
\r
323 resultStr+=" "+attrName.substr(config.attributePrefix.length)+"='"+attrVal+"'";
\r
333 function endTag(jsonObj,elementName) {
\r
334 return "</"+ (jsonObj.__prefix!=null? (jsonObj.__prefix+":"):"")+elementName+">";
\r
337 function endsWith(str, suffix) {
\r
338 return str.indexOf(suffix, str.length - suffix.length) !== -1;
\r
341 function jsonXmlSpecialElem ( jsonObj, jsonObjField ) {
\r
342 if((config.arrayAccessForm=="property" && endsWith(jsonObjField.toString(),("_asArray")))
\r
343 || jsonObjField.toString().indexOf(config.attributePrefix)==0
\r
344 || jsonObjField.toString().indexOf("__")==0
\r
345 || (jsonObj[jsonObjField] instanceof Function) )
\r
351 function jsonXmlElemCount ( jsonObj ) {
\r
352 var elementsCnt = 0;
\r
353 if(jsonObj instanceof Object ) {
\r
354 for( var it in jsonObj ) {
\r
355 if(jsonXmlSpecialElem ( jsonObj, it) )
\r
360 return elementsCnt;
\r
363 function parseJSONAttributes ( jsonObj ) {
\r
365 if(jsonObj instanceof Object ) {
\r
366 for( var ait in jsonObj ) {
\r
367 if(ait.toString().indexOf("__")== -1 && ait.toString().indexOf(config.attributePrefix)==0) {
\r
368 attrList.push(ait);
\r
375 function parseJSONTextAttrs ( jsonTxtObj ) {
\r
378 if(jsonTxtObj.__cdata!=null) {
\r
379 result+="<![CDATA["+jsonTxtObj.__cdata+"]]>";
\r
382 if(jsonTxtObj.__text!=null) {
\r
383 if(config.escapeMode)
\r
384 result+=escapeXmlChars(jsonTxtObj.__text);
\r
386 result+=jsonTxtObj.__text;
\r
391 function parseJSONTextObject ( jsonTxtObj ) {
\r
394 if( jsonTxtObj instanceof Object ) {
\r
395 result+=parseJSONTextAttrs ( jsonTxtObj );
\r
398 if(jsonTxtObj!=null) {
\r
399 if(config.escapeMode)
\r
400 result+=escapeXmlChars(jsonTxtObj);
\r
402 result+=jsonTxtObj;
\r
408 function parseJSONArray ( jsonArrRoot, jsonArrObj, attrList ) {
\r
410 if(jsonArrRoot.length == 0) {
\r
411 result+=startTag(jsonArrRoot, jsonArrObj, attrList, true);
\r
414 for(var arIdx = 0; arIdx < jsonArrRoot.length; arIdx++) {
\r
415 result+=startTag(jsonArrRoot[arIdx], jsonArrObj, parseJSONAttributes(jsonArrRoot[arIdx]), false);
\r
416 result+=parseJSONObject(jsonArrRoot[arIdx]);
\r
417 result+=endTag(jsonArrRoot[arIdx],jsonArrObj);
\r
423 function parseJSONObject ( jsonObj ) {
\r
426 var elementsCnt = jsonXmlElemCount ( jsonObj );
\r
428 if(elementsCnt > 0) {
\r
429 for( var it in jsonObj ) {
\r
431 if(jsonXmlSpecialElem ( jsonObj, it) )
\r
434 var subObj = jsonObj[it];
\r
436 var attrList = parseJSONAttributes( subObj )
\r
438 if(subObj == null || subObj == undefined) {
\r
439 result+=startTag(subObj, it, attrList, true);
\r
442 if(subObj instanceof Object) {
\r
444 if(subObj instanceof Array) {
\r
445 result+=parseJSONArray( subObj, it, attrList );
\r
447 else if(subObj instanceof Date) {
\r
448 result+=startTag(subObj, it, attrList, false);
\r
449 result+=subObj.toISOString();
\r
450 result+=endTag(subObj,it);
\r
453 var subObjElementsCnt = jsonXmlElemCount ( subObj );
\r
454 if(subObjElementsCnt > 0 || subObj.__text!=null || subObj.__cdata!=null) {
\r
455 result+=startTag(subObj, it, attrList, false);
\r
456 result+=parseJSONObject(subObj);
\r
457 result+=endTag(subObj,it);
\r
460 result+=startTag(subObj, it, attrList, true);
\r
465 result+=startTag(subObj, it, attrList, false);
\r
466 result+=parseJSONTextObject(subObj);
\r
467 result+=endTag(subObj,it);
\r
471 result+=parseJSONTextObject(jsonObj);
\r
476 this.parseXmlString = function(xmlDocStr) {
\r
477 var isIEParser = window.ActiveXObject || "ActiveXObject" in window;
\r
478 if (xmlDocStr === undefined) {
\r
482 if (window.DOMParser) {
\r
483 var parser=new window.DOMParser();
\r
484 var parsererrorNS = null;
\r
485 // IE9+ now is here
\r
488 parsererrorNS = parser.parseFromString("INVALID", "text/xml").childNodes[0].namespaceURI;
\r
491 parsererrorNS = null;
\r
495 xmlDoc = parser.parseFromString( xmlDocStr, "text/xml" );
\r
496 if( parsererrorNS!= null && xmlDoc.getElementsByTagNameNS(parsererrorNS, "parsererror").length > 0) {
\r
497 //throw new Error('Error parsing XML: '+xmlDocStr);
\r
507 if(xmlDocStr.indexOf("<?")==0) {
\r
508 xmlDocStr = xmlDocStr.substr( xmlDocStr.indexOf("?>") + 2 );
\r
510 xmlDoc=new ActiveXObject("Microsoft.XMLDOM");
\r
511 xmlDoc.async="false";
\r
512 xmlDoc.loadXML(xmlDocStr);
\r
517 this.asArray = function(prop) {
\r
518 if(prop instanceof Array)
\r
524 this.toXmlDateTime = function(dt) {
\r
525 if(dt instanceof Date)
\r
526 return dt.toISOString();
\r
528 if(typeof(dt) === 'number' )
\r
529 return new Date(dt).toISOString();
\r
534 this.asDateTime = function(prop) {
\r
535 if(typeof(prop) == "string") {
\r
536 return fromXmlDateTime(prop);
\r
542 this.xml2json = function (xmlDoc) {
\r
543 return parseDOMChildren ( xmlDoc );
\r
546 this.xml_str2json = function (xmlDocStr) {
\r
547 var xmlDoc = this.parseXmlString(xmlDocStr);
\r
549 return this.xml2json(xmlDoc);
\r
554 this.json2xml_str = function (jsonObj) {
\r
555 return parseJSONObject ( jsonObj );
\r
558 this.json2xml = function (jsonObj) {
\r
559 var xmlDocStr = this.json2xml_str (jsonObj);
\r
560 return this.parseXmlString(xmlDocStr);
\r
563 this.getVersion = function () {
\r