--- title: JXON slug: Archive/JXON translation_of: Archive/JXON ---
JXON (无损 JavaScript XML对象注释) 是一个通用名称,通过它定义使用 XML的JavaScript对象的表示。 这种转换没有真正的标准,但一些公约开始出现在网络上。在某些情况下,必须从JavaScript解释器中读取XML文档的全部内容(例如用于Web应用程序语言或设置XML文档)。在这些情况下,JXON可能是最实用的方法。
在本文中,我们将演示如何将解析的XML Document(即Document的一个实例)转换为JavaScript Object树(即Object的嵌套实例树),反之亦然,用一些不同的算法。首先阅读XML介绍文章会比较有帮助。
如果您想要一个完整的双向JXON库(在JSON全局对象上建模),请跳至专用段落(但请阅读关于const语句兼容性的注释)。
现在想象你有这个示例XML文档:
<?xml version="1.0"?> <!DOCTYPE catalog SYSTEM "catalog.dtd"> <catalog> <product description="Cardigan Sweater"> <catalog_item gender="Men's"> <item_number>QWZ5671</item_number> <price>39.95</price> <size description="Medium"> <color_swatch image="red_cardigan.jpg">Red</color_swatch> <color_swatch image="burgundy_cardigan.jpg">Burgundy</color_swatch> </size> <size description="Large"> <color_swatch image="red_cardigan.jpg">Red</color_swatch> <color_swatch image="burgundy_cardigan.jpg">Burgundy</color_swatch> </size> </catalog_item> <catalog_item gender="Women's"> <item_number>RRX9856</item_number> <discount_until>Dec 25, 1995</discount_until> <price>42.50</price> <size description="Medium"> <color_swatch image="black_cardigan.jpg">Black</color_swatch> </size> </catalog_item> </product> <script type="text/javascript"><![CDATA[function matchwo(a,b) { if (a < b && a < 0) { return 1; } else { return 0; } }]]></script> </catalog>
首先,按照“如何创建DOM树”文章中的描述,创建一个类似前面示例的DOM树。如果您已经有使用XMLHttpRequest的DOM树,请跳到下一段。
yourRequest.responseXML
属性来获取已解析的XML文档。不要使用yourRequest.responseText
!这里提出的算法(参见:#1,#2,#3,#4)将只考虑以下类型的节点及其属性:
Document
(只作为函数参数),DocumentFragment
(只作为函数参数),Element
,Text
(从不作为函数参数),CDATASection
(从不作为函数参数),Attr
(从不作为函数参数)。对于JavaScript的使用来说,这是一个很好的标准化的妥协,因为XML文档的所有信息都包含在这些节点类型中。所有其他信息(如处理指令,模式,注释等)都将丢失。这种算法仍然被认为是无损的,因为丢失的是元信息而不是信息。
为了避免冲突,节点和属性名称的表示不区分大小写(总是以小写形式呈现),所以使用JavaScript设置的对象的本地属性名称必须总是具有某种大小写(即至少有一个大写字母在他们的名字),如你可以看到下面。
下面的算法有些基于Parker公约(版本0.4),它规定了标签名称到对象属性名称的转换以及每个标签(纯文本解析)的所有收集 text content
的 typeof
的识别。但有一些分歧(所以,可以说我们遵循我们的惯例)。而且,对于设想的节点,所有算法都是同样无损的。
我们认为第三种算法是最具代表性和实用性的JXON解析算法。
现在让我们将doc
(DOM树)序列化为一个JavaScript对象树(您可以阅读关于使用对象以及Javascript如何面向对象的更多信息)。我们可以使用几种算法将其内容转换为Javascript对象树。
这个简单的递归构造函数将一个XML DOM树转换成一个JavaScript对象树。每个元素的文本内容都存储在keyValue
属性中,而nodeAttributes
(如果存在)列在子对象keyAttributes
下。构造函数的参数可以是整个XML Document
,DocumentFragment
或简单的Element
节点。
/*\ |*| |*| JXON Snippet #1 - Mozilla Developer Network |*| |*| https://developer.mozilla.org/en-US/docs/JXON |*| https://developer.mozilla.org/User:fusionchess |*| |*| This framework is released under the GNU Public License, version 3 or later. |*| http://www.gnu.org/licenses/gpl-3.0-standalone.html |*| \*/ function parseText (sValue) { if (/^\s*$/.test(sValue)) { return null; } if (/^(?:true|false)$/i.test(sValue)) { return sValue.toLowerCase() === "true"; } if (isFinite(sValue)) { return parseFloat(sValue); } if (isFinite(Date.parse(sValue))) { return new Date(sValue); } return sValue; } function JXONTree (oXMLParent) { var nAttrLen = 0, nLength = 0, sCollectedTxt = ""; if (oXMLParent.hasChildNodes()) { for (var oNode, sProp, vContent, nItem = 0; nItem < oXMLParent.childNodes.length; nItem++) { oNode = oXMLParent.childNodes.item(nItem); if ((oNode.nodeType - 1 | 1) === 3) { sCollectedTxt += oNode.nodeType === 3 ? oNode.nodeValue.trim() : oNode.nodeValue; } // nodeType is "Text" (3) or "CDATASection" (4) else if (oNode.nodeType === 1 && !oNode.prefix) { // nodeType is "Element" (1) sProp = oNode.nodeName.toLowerCase(); vContent = new JXONTree(oNode); if (this.hasOwnProperty(sProp)) { if (this[sProp].constructor !== Array) { this[sProp] = [this[sProp]]; } this[sProp].push(vContent); } else { this[sProp] = vContent; nLength++; } } } this.keyValue = parseText(sCollectedTxt); } else { this.keyValue = null; } if (oParentNode.hasAttributes && oXMLParent.hasAttributes()) { var oAttrib; this.keyAttributes = {}; for (nAttrLen; nAttrLen < oXMLParent.attributes.length; nAttrLen++) { oAttrib = oXMLParent.attributes.item(nAttrLen); this.keyAttributes[oAttrib.name.toLowerCase()] = parseText(oAttrib.value.trim()); } } /* * Optional properties... this.keyLength = nLength; this.attributesLength = nAttrLen; // this.DOMNode = oXMLParent; */ /* Object.freeze(this); */ } /* * Optional methods... Uncomment the optional properties first! JXONTree.prototype.valueOf = function () { return this.keyValue; }; JXONTree.prototype.toString = function () { return String(this.keyValue); }; JXONTree.prototype.getItem = function (nItem) { if (nLength === 0) { return null; } var nCount = 0; for (var sKey in this) { if (nCount === nItem) { return this[sKey]; } nCount++; } return null; }; JXONTree.prototype.getAttribute = function (nAttrId) { if (nAttrLen === 0 || nAttrId + 1 > nAttrLen) { return null; } var nAttr = 0; for (var sAttrName in this.keyAttributes) { if (nAttr === nAttrId) { return this.keyAttributes[sAttrName]; } nAttr++; } return null; }; JXONTree.prototype.hasChildren = function () { return this.keyLength > 0; }; */ var myObject = new JXONTree(doc); // we got our javascript object! try: alert(JSON.stringify(myObject));
/* Object.freeze(this); */
。 Object.freeze()
方法防止将新属性添加到该属性中,防止现有属性被删除,并防止现有属性或其可枚举性,可配置性或可写性发生更改。本质上,对象树是有效的不可变的。
用这个算法我们的例子变成:
{ "catalog": { "product": { "catalog_item": [{ "item_number": { "keyValue": "QWZ5671" }, "price": { "keyValue": 39.95 }, "size": [{ "color_swatch": [{ "keyValue": "Red", "keyAttributes": { "image": "red_cardigan.jpg" } }, { "keyValue": "Burgundy", "keyAttributes": { "image": "burgundy_cardigan.jpg" } }], "keyValue": null, "keyAttributes": { "description": "Medium" } }, { "color_swatch": [{ "keyValue": "Red", "keyAttributes": { "image": "red_cardigan.jpg" } }, { "keyValue": "Burgundy", "keyAttributes": { "image": "burgundy_cardigan.jpg" } }], "purchased": { "keyValue": null }, "keyValue": null, "keyAttributes": { "description": "Large" } }], "keyValue": null, "keyAttributes": { "gender": "Men's" } }, { "item_number": { "keyValue": "RRX9856" }, "discount_until": { "keyValue": new Date(1995, 11, 25) }, "price": { "keyValue": 42.5 }, "size": { "color_swatch": { "keyValue": "Black", "keyAttributes": { "image": "black_cardigan.jpg" } }, "keyValue": null, "keyAttributes": { "description": "Medium" } }, "keyValue": null, "keyAttributes": { "gender": "Women's" } }], "keyValue": null, "keyAttributes": { "description": "Cardigan Sweater" } }, "script": { "keyValue": "function matchwo(a,b) {\n if (a < b && a < 0) { return 1; }\n else { return 0; }\n}", "keyAttributes": { "type": "text/javascript" } }, "keyValue": null }, "keyValue": null }
如果您部分了解XML文档的结构,则可以使用这种技术。
这里是另一个更简单的转换方法,其中nodeAttributes
列在子节点的同一对象下,但带有“@”前缀(由BadgerFish Convention提出)。如上所述,文本内容存储在keyValue
属性中。构造函数的参数可以是整个XML Document
,一个DocumentFragment
或简单的Element
节点。
/*\ |*| |*| JXON Snippet #2 - Mozilla Developer Network |*| |*| https://developer.mozilla.org/en-US/docs/JXON |*| https://developer.mozilla.org/User:fusionchess |*| |*| This framework is released under the GNU Public License, version 3 or later. |*| http://www.gnu.org/licenses/gpl-3.0-standalone.html |*| \*/ function parseText (sValue) { if (/^\s*$/.test(sValue)) { return null; } if (/^(?:true|false)$/i.test(sValue)) { return sValue.toLowerCase() === "true"; } if (isFinite(sValue)) { return parseFloat(sValue); } if (isFinite(Date.parse(sValue))) { return new Date(sValue); } return sValue; } function JXONTree (oXMLParent) { if (oXMLParent.hasChildNodes()) { var sCollectedTxt = ""; for (var oNode, sProp, vContent, nItem = 0; nItem < oXMLParent.childNodes.length; nItem++) { oNode = oXMLParent.childNodes.item(nItem); if ((oNode.nodeType - 1 | 1) === 3) { sCollectedTxt += oNode.nodeType === 3 ? oNode.nodeValue.trim() : oNode.nodeValue; } else if (oNode.nodeType === 1 && !oNode.prefix) { sProp = oNode.nodeName.toLowerCase(); vContent = new JXONTree(oNode); if (this.hasOwnProperty(sProp)) { if (this[sProp].constructor !== Array) { this[sProp] = [this[sProp]]; } this[sProp].push(vContent); } else { this[sProp] = vContent; } } } if (sCollectedTxt) { this.keyValue = parseText(sCollectedTxt); } } if (oParentNode.hasAttributes && oXMLParent.hasAttributes()) { var oAttrib; for (var nAttrib = 0; nAttrib < oXMLParent.attributes.length; nAttrib++) { oAttrib = oXMLParent.attributes.item(nAttrib); this["@" + oAttrib.name.toLowerCase()] = parseText(oAttrib.value.trim()); } } /* Object.freeze(this); */ } var myObject = new JXONTree(doc); // we got our javascript object! try: alert(JSON.stringify(myObject));
/* Object.freeze(this); */
。 Object.freeze()
方法防止将新属性添加到该属性中,防止现有属性被删除,并防止现有属性或其可枚举性,可配置性或可写性发生更改。本质上,对象树是有效的不可变的。用这个算法我们的例子变成:
{ "catalog": { "product": { "catalog_item": [{ "item_number": { "keyValue": "QWZ5671" }, "price": { "keyValue": 39.95 }, "size": [{ "color_swatch": [{ "keyValue": "Red", "@image": "red_cardigan.jpg" }, { "keyValue": "Burgundy", "@image": "burgundy_cardigan.jpg" }], "@description": "Medium" }, { "color_swatch": [{ "keyValue": "Red", "@image": "red_cardigan.jpg" }, { "keyValue": "Burgundy", "@image": "burgundy_cardigan.jpg" }], "@description": "Large" }], "@gender": "Men's" }, { "item_number": { "keyValue": "RRX9856" }, "discount_until": { "keyValue": new Date(1995, 11, 25) }, "price": { "keyValue": 42.5 }, "size": { "color_swatch": { "keyValue": "Black", "@image": "black_cardigan.jpg" }, "@description": "Medium" }, "@gender": "Women's" }], "@description": "Cardigan Sweater" }, "script": { "keyValue": "function matchwo(a,b) {\n if (a < b && a < 0) { return 1; }\n else { return 0; }\n}", "@type": "text/javascript" } } }
如果您部分了解XML文档的结构,则可以使用这种技术。
这是另一种转换方法。这个算法是最接近Parker约定的。除了不包含除Text
或CDATASection
以外的其他可识别节点的节点不被视为对象,而是直接作为布尔值,字符串,数字或Date
对象(请参阅Parker约定)。空节点(即不包含其他Element
节点,Text
节点,CDATASection
节点或Attr
节点)的默认值为true
(请参阅代码注意事项)。另外,这次我们使用一个函数来代替构造函数。函数的参数可以是整个XML Document
,一个DocumentFragment
,或者只是一个 Element
节点。根据BadgerFish公约的建议,nodeAttributes
具有“@”前缀。在很多情况下,这是最实用的转换方法。
/*\ |*| |*| JXON Snippet #3 - Mozilla Developer Network |*| |*| https://developer.mozilla.org/en-US/docs/JXON |*| https://developer.mozilla.org/User:fusionchess |*| |*| This framework is released under the GNU Public License, version 3 or later. |*| http://www.gnu.org/licenses/gpl-3.0-standalone.html |*| \*/ function parseText (sValue) { if (/^\s*$/.test(sValue)) { return null; } if (/^(?:true|false)$/i.test(sValue)) { return sValue.toLowerCase() === "true"; } if (isFinite(sValue)) { return parseFloat(sValue); } if (isFinite(Date.parse(sValue))) { return new Date(sValue); } return sValue; } function getJXONTree (oXMLParent) { var vResult = /* put here the default value for empty nodes! */ true, nLength = 0, sCollectedTxt = ""; if (oXMLParent.hasAttributes && oXMLParent.hasAttributes()) { vResult = {}; for (nLength; nLength < oXMLParent.attributes.length; nLength++) { oAttrib = oXMLParent.attributes.item(nLength); vResult["@" + oAttrib.name.toLowerCase()] = parseText(oAttrib.value.trim()); } } if (oXMLParent.hasChildNodes()) { for (var oNode, sProp, vContent, nItem = 0; nItem < oXMLParent.childNodes.length; nItem++) { oNode = oXMLParent.childNodes.item(nItem); if (oNode.nodeType === 4) { sCollectedTxt += oNode.nodeValue; } /* nodeType is "CDATASection" (4) */ else if (oNode.nodeType === 3) { sCollectedTxt += oNode.nodeValue.trim(); } /* nodeType is "Text" (3) */ else if (oNode.nodeType === 1 && !oNode.prefix) { /* nodeType is "Element" (1) */ if (nLength === 0) { vResult = {}; } sProp = oNode.nodeName.toLowerCase(); vContent = getJXONTree(oNode); if (vResult.hasOwnProperty(sProp)) { if (vResult[sProp].constructor !== Array) { vResult[sProp] = [vResult[sProp]]; } vResult[sProp].push(vContent); } else { vResult[sProp] = vContent; nLength++; } } } } if (sCollectedTxt) { nLength > 0 ? vResult.keyValue = parseText(sCollectedTxt) : vResult = parseText(sCollectedTxt); } /* if (nLength > 0) { Object.freeze(vResult); } */ return vResult; } var myObject = getJXONTree(doc); // we got our javascript object! try: alert(JSON.stringify(myObject));
/* Object.freeze(this); */
。 Object.freeze()
方法防止将新属性添加到该属性中,防止现有属性被删除,并防止现有属性或其可枚举性,可配置性或可写性发生更改。本质上,对象树是有效的不可变的。用这个算法我们的例子变成:
{ "catalog": { "product": { "@description": "Cardigan Sweater", "catalog_item": [{ "@gender": "Men's", "item_number": "QWZ5671", "price": 39.95, "size": [{ "@description": "Medium", "color_swatch": [{ "@image": "red_cardigan.jpg", "keyValue": "Red" }, { "@image": "burgundy_cardigan.jpg", "keyValue": "Burgundy" }] }, { "@description": "Large", "color_swatch": [{ "@image": "red_cardigan.jpg", "keyValue": "Red" }, { "@image": "burgundy_cardigan.jpg", "keyValue": "Burgundy" }] }] }, { "@gender": "Women's", "item_number": "RRX9856", "discount_until": new Date(1995, 11, 25), "price": 42.5, "size": { "@description": "Medium", "color_swatch": { "@image": "black_cardigan.jpg", "keyValue": "Black" } } }] }, "script": { "@type": "text/javascript", "keyValue": "function matchwo(a,b) {\n if (a < b && a < 0) { return 1; }\n else { return 0; }\n}" } } }
如果您知道XML文档的结构,这是推荐的技术。
以下是另一种可以实现的转换方法。它也非常接近Parker约定。使用此算法,包含同一级别中的其他子元素,文本或CDATASection
节点的所有元素节点都将被视为Boolean
,Number
, String
,或Date
构造函数的实例。因此,任何子元素节点(如果存在)将嵌套在这些类型的对象中。
For example:
<employee type="usher">John Smith</employee> <manager>Lisa Carlucci</manager>
becomes
var myObject = { "employee": new String("John Smith"), "manager": "Lisa Carlucci" }; myObject.employee["@type"] = "usher"; // test alert(myObject.manager); // "Lisa Carlucci" alert(myObject.employee["@type"]); // "usher" alert(myObject.employee); // "John Smith"
对于第三种算法,不包含除Text或CDATASection
之外的其他可识别节点的节点不被视为对象,而是直接作为Boolean
,Number
(原始值), String
,或Date
对象;而空节点(即,不包含其他Element
节点,Text
节点,CDATASection
节点或Attr
节点)具有默认值true
。至于第三个算法,它不是使用构造函数,而是一个函数。该函数的参数可以是整个XML文档,一个DocumentFragment
或简单的Element
节点。根据BadgerFish公约的建议,nodeAttributes
具有“@”前缀。
/*\ |*| |*| JXON Snippet #4 - Mozilla Developer Network |*| |*| https://developer.mozilla.org/en-US/docs/JXON |*| https://developer.mozilla.org/User:fusionchess |*| |*| This framework is released under the GNU Public License, version 3 or later. |*| http://www.gnu.org/licenses/gpl-3.0-standalone.html |*| \*/ function parseText (sValue) { if (/^\s*$/.test(sValue)) { return null; } if (/^(?:true|false)$/i.test(sValue)) { return sValue.toLowerCase() === "true"; } if (isFinite(sValue)) { return parseFloat(sValue); } if (isFinite(Date.parse(sValue))) { return new Date(sValue); } return sValue; } function objectify (vValue) { if (vValue === null) { return new (function() { this.toString = function() { return "null"; } this.valueOf = function() { return null; } })(); } return vValue instanceof Object ? vValue : new vValue.constructor(vValue); } var aTmpEls = []; // loaded element nodes cache function getJXONTree (oXMLParent) { var sProp, vContent, vResult, nLength = 0, nLevelStart = aTmpEls.length, nChildren = oXMLParent.hasChildNodes() ? oXMLParent.childNodes.length : 0, sCollectedTxt = ""; for (var oNode, nItem = 0; nItem < nChildren; nItem++) { oNode = oXMLParent.childNodes.item(nItem); if (oNode.nodeType === 4) { sCollectedTxt += oNode.nodeValue; } /* nodeType is "CDATASection" (4) */ else if (oNode.nodeType === 3) { sCollectedTxt += oNode.nodeValue.trim(); } /* nodeType is "Text" (3) */ else if (oNode.nodeType === 1 && !oNode.prefix) { aTmpEls.push(oNode); } /* nodeType is "Element" (1) */ } var nLevelEnd = aTmpEls.length, vBuiltVal = parseText(sCollectedTxt); if (oParentNode.hasAttributes && oXMLParent.hasAttributes()) { vResult = objectify(vBuiltVal); for (nLength; nLength < oXMLParent.attributes.length; nLength++) { oAttrib = oXMLParent.attributes.item(nLength); vResult["@" + oAttrib.name.toLowerCase()] = parseText(oAttrib.value.trim()); } } else if (nLevelEnd > nLevelStart) { vResult = objectify(vBuiltVal); } for (var nElId = nLevelStart; nElId < nLevelEnd; nElId++) { sProp = aTmpEls[nElId].nodeName.toLowerCase(); vContent = getJXONTree(aTmpEls[nElId]); if (vResult.hasOwnProperty(sProp)) { if (vResult[sProp].constructor !== Array) { vResult[sProp] = [vResult[sProp]]; } vResult[sProp].push(vContent); } else { vResult[sProp] = vContent; nLength++; } } aTmpEls.length = nLevelStart; if (nLength === 0) { vResult = sCollectedTxt ? vBuiltVal : /* put here the default value for empty nodes: */ true; } /* else { Object.freeze(vResult); } */ return vResult; } var myObject = getJXONTree(doc); alert(myObject.catalog.product.catalog_item[1].size.color_swatch["@image"]); // "black_cardigan.jpg" alert(myObject.catalog.product.catalog_item[1].size.color_swatch); // "Black" !
/* Object.freeze(this); */
。 Object.freeze()
方法防止将新属性添加到该属性中,防止现有属性被删除,并防止现有属性或其可枚举性,可配置性或可写性发生更改。本质上,对象树是有效的不可变的。如果您知道XML文档的结构,这是一种可能的技术。
为了从JavaScript对象树开始构建一个新的XML文档,可以将这里提出的算法颠倒过来。为了简单,我们将在这里提出一个例子,在一个单一的方法,代表了所有我们的算法的反演。
/*\ |*| |*| JXON Snippet #5 - Mozilla Developer Network |*| |*| https://developer.mozilla.org/en-US/docs/JXON |*| https://developer.mozilla.org/User:fusionchess |*| |*| This framework is released under the GNU Public License, version 3 or later. |*| http://www.gnu.org/licenses/gpl-3.0-standalone.html |*| \*/ function createXML (oObjTree) { function loadObjTree (oParentEl, oParentObj) { var vValue, oChild; if (oParentObj.constructor === String || oParentObj.constructor === Number || oParentObj.constructor === Boolean) { oParentEl.appendChild(oNewDoc.createTextNode(oParentObj.toString())); /* verbosity level is 0 or 1 */ if (oParentObj === oParentObj.valueOf()) { return; } } else if (oParentObj.constructor === Date) { oParentEl.appendChild(oNewDoc.createTextNode(oParentObj.toGMTString())); } for (var sName in oParentObj) { if (isFinite(sName)) { continue; } /* verbosity level is 0 */ vValue = oParentObj[sName]; if (sName === "keyValue") { if (vValue !== null && vValue !== true) { oParentEl.appendChild(oNewDoc.createTextNode(vValue.constructor === Date ? vValue.toGMTString() : String(vValue))); } } else if (sName === "keyAttributes") { /* verbosity level is 3 */ for (var sAttrib in vValue) { oParentEl.setAttribute(sAttrib, vValue[sAttrib]); } } else if (sName.charAt(0) === "@") { oParentEl.setAttribute(sName.slice(1), vValue); } else if (vValue.constructor === Array) { for (var nItem = 0; nItem < vValue.length; nItem++) { oChild = oNewDoc.createElement(sName); loadObjTree(oChild, vValue[nItem]); oParentEl.appendChild(oChild); } } else { oChild = oNewDoc.createElement(sName); if (vValue instanceof Object) { loadObjTree(oChild, vValue); } else if (vValue !== null && vValue !== true) { oChild.appendChild(oNewDoc.createTextNode(vValue.toString())); } oParentEl.appendChild(oChild); } } } const oNewDoc = document.implementation.createDocument("", "", null); loadObjTree(oNewDoc, oObjTree); return oNewDoc; } var newDoc = createXML(myObject); // we got our Document instance! try: alert((new XMLSerializer()).serializeToString(newDoc));
Date
实例(如果存在的话)通过toGMTString()
方法转换为 Strings 。没有什么禁止使用任何其他转换方法。此外,具有true
值的树的所有属性都将被转换为没有文本节点的空元素(请参阅 Code considerations)。如果要自动创建XML文档,这是一个很好的解决方案。但是,如果要重新构建以前转换为JSON的XML文档,这是一个不错的选择。虽然双向转换是非常忠实的(除了CDATASection
节点,它们将被转换成文本节点),但这个过程是不必要的成本。事实上,如果您的目标是编辑XML文档,强烈建议您使用它而不是创建一个新的。
上面列出的用于将XML文档转换为JSON(通常称为“JXON算法”)的功能或多或少地基于Parker公约(尤其是关于将标签名称转换为对象属性名称,识别所有收集到的每个标签的文本内容以及单独的Text
和/或CDATASection
节点吸收为原始值)。It is called “Parker Convention” in opposition to “BadgerFish Convention”, after the comic Parker & Badger by Cuadrado. See also: BadgerFish Convention.
以下是来自 xml2json-xslt项目网站的“TransformingRules”页面的Parker Convention文章(版本0.4)的转录。
本公约是为了规范从XSLT到 JSON 的转换而编写的,所以它的一部分对于JavaScript来说是徒劳的。
The root element will be absorbed, for there is only one:
<root>test</root>
becomes
"test"
Element names become object properties:
<root><name>Xml</name><encoding>ASCII</encoding></root>
becomes
{ "name": "Xml", "encoding": "ASCII" }
Numbers are recognized (integers and decimals):
<root><age>12</age><height>1.73</height></root>
becomes
{ "age": 12, "height": 1.73 }
Booleans are recognized case insensitive:
<root><checked>True</checked><answer>FALSE</answer></root>
becomes
{ "checked": true, "answer": false }
Strings are escaped:
<root>Quote: " New-line: </root>
becomes
"Quote: \" New-line:\n"
Empty elements will become null:
<root><nil/><empty></empty></root>
becomes
{ "nil": null, "empty": null }
If all sibling elements have the same name, they become an array
<root><item>1</item><item>2</item><item>three</item></root>
becomes
[1, 2, "three"]
Mixed mode text-nodes, comments and attributes get absorbed:
<root version="1.0">testing<!--comment--><element test="true">1</element></root>
becomes
{ "element": true }
Namespaces get absorbed, and prefixes will just be part of the property name:
<root xmlns:ding="http://zanstra.com/ding"><ding:dong>binnen</ding:dong></root>
becomes
{ "ding:dong" : "binnen" }
true
而不是null
- 请参阅Code considerations)。第5点由JavaScript方法JSON.stringify()
自动管理。关于第9点,我们选择忽略所有有前缀的节点;你可以通过从我们的算法中删除字符串&& !oNode.prefix
来包含它们(参见 Code considerations))。This is the same as the JSON translation, but with these extras:
Property names are only escaped when necessary
<root><while>true</while><wend>false</wend><only-if/></root>
becomes
{ "while": true, wend: false, "only-if": null }
Within a string, closing elements "</" are escaped as "<\/"
<root><![CDATA[<script>alert("YES");</script>]]></root>
becomes
{ script: "<script>alert(\"YES\")<\/script>" }
Dates are created as new Date
objects
<root>2006-12-25</root>
becomes
new Date(2006, 12 - 1, 25)
Attributes and comments are shown as comments (for testing purposes):
<!--testing--><root><test version="1.0">123</test></root>
becomes
/* testing */ { test /* @version = "1.0" */ : 123}
A bit of indentation is done, to keep things legible
JSON.stringify()
进行管理。我们以第三种算法作为最具代表性的JXON解析算法。单个结构化XML元素可能有八种不同的配置:
The following table shows the corresponding conversion patterns between XML and JSON according to the third algorithm.
Case | XML | JSON | Javascript access |
---|---|---|---|
1 | <animal /> |
"animal": true |
myObject.animal |
2 | <animal>Deka</animal> |
"animal": "Deka" |
myObject.animal |
3 | <animal name="Deka" /> |
"animal": {"@name": "Deka"} |
myObject.animal["@name"] |
4 | <animal name="Deka">is my cat</animal> |
"animal": { "@name": "Deka", "keyValue": "is my cat" } |
myObject.animal["@name"] , myObject.animal.keyValue |
5 | <animal> <dog>Charlie</dog> <cat>Deka</cat> </animal> |
"animal": { "dog": "Charlie", "cat": "Deka" } |
myObject.animal.dog , myObject.animal.cat |
6 | <animal> <dog>Charlie</dog> <dog>Mad Max</dog> </animal> |
"animal": { "dog": ["Charlie", "Mad Max"] } |
myObject.animal.dog[0] , myObject.animal.dog[1] |
7 | <animal> in my house <dog>Charlie</dog> </animal> |
"animal": { "keyValue": "in my house", "dog": "Charlie" } |
myObject.animal.keyValue , myObject.animal.dog |
8 | <animal> in my ho <dog>Charlie</dog> use </animal> |
"animal": { "keyValue": "in my house", "dog": "Charlie" } |
myObject.animal.keyValue , myObject.animal.dog |
In these examples we chose to use a property named keyValue
for the text content. The lack of standards for XML to JSON conversion leads developers to choose a variety of property names for the text content of XML Element
nodes that also contain other child nodes. Sometimes a property called $
is used. Other times a property called #text
is used (however, a name like this isn't a good choice, since the text content of a node can be parsed into a non-string value by our algorithms during the conversion). In the algorithms proposed here, you can easily change this name, depending on your needs.
The choice of using a true
value instead of a null
value to represent empty nodes is due to the fact that when in an XML document there is an empty node the reason is often to express a Boolean
, as in this case:
<car> <type>Ferrari</type> <bought /> </car>
If the value were null
it would be more cumbersome to launch a code like this:
if (myObject.car.bought) { // do something }
CDATASection
nodes which contain nothing but white spaces (precisely: /^\s+$/
) will be parsed as null
.The fourth algorithm represents a special case of conversion. As you can see, the generated JavaScript Object tree is not stringifyable. It is very practical for internal JavaScript access, but don't use it if you want to transfer the tree via JSON string (as for Worker
messages, for example).
We chose to ignore nodes which have a prefix (for example: <ding:dong>binnen</ding:dong>
), due to their special case (they are often used in order to represents an XML Schema, which is meta-information concerning how to organize the information of the document, reserved for the XML parser). You can include them removing the string && !oNode.prefix
from our algorithms (by doing so the whole tag will become the property name: { "ding:dong": "binnen" }
).
An important consideration is that, when using the third or the fourth algorithm, an XML Document
can be used to create any type of JavaScript object. For example, If you want to create an object like the following:
{ "myboolean": true, "myarray": ["Cinema", "Hot dogs", false], "myobject": { "nickname": "Jack", "registration_date": new Date(1995, 11, 25), "privileged_user": true }, "mynumber": 99, "mytext": "Hello World!" }
you must just create an XML document with the following structure:
<myboolean>true</myboolean> <myarray>Cinema</myarray> <myarray>Hot dogs</myarray> <myarray>false</myarray> <myobject> <nickname>Jack</nickname> <registration_date>Dec 25, 1995</registration_date> <privileged_user /> </myobject> <mynumber>99</mynumber> <mytext>Hello World!</mytext>
This example also shows how the ideal JXON document is an XML document designed specifically to be converted in JSON format, though our algorithms work fine with any kind of XML document.
Now we can create a more complete, bidirectional, JXON library based on all our algorithms (see: #1, #2, #3, #4, reverse). Its usage is modeled on the JSON
native object. Before implementing it in a working environment, please read the note about the const
statement compatibility. The following code is also available on GitHub.
"use strict"; /*\ |*| |*| JXON framework - Copyleft 2011 by Mozilla Developer Network |*| |*| Revision #3 - October 31th, 2016 |*| |*| https://developer.mozilla.org/en-US/docs/JXON |*| https://developer.mozilla.org/User:fusionchess |*| https://github.com/madmurphy/jxon.js |*| |*| This framework is released under the GNU Public License, version 3 or later. |*| http://www.gnu.org/licenses/gpl-3.0-standalone.html |*| \*/ const JXON = new (function () { function parseText (sValue) { if (rIsNull.test(sValue)) { return null; } if (rIsBool.test(sValue)) { return sValue.toLowerCase() === "true"; } if (isFinite(sValue)) { return parseFloat(sValue); } if (isFinite(Date.parse(sValue))) { return new Date(sValue); } return sValue; } function EmptyTree () {} EmptyTree.prototype.toString = function () { return "null"; }; EmptyTree.prototype.valueOf = function () { return null; }; function objectify (vVal) { return vVal === null ? new EmptyTree() : vVal instanceof Object ? vVal : new vVal.constructor(vVal); } function createObjTree (oParentNode, nVerb, bFreeze, bNesteAttr) { const nLevelStart = aCache.length, bChildren = oParentNode.hasChildNodes(), bAttributes = oParentNode.hasAttributes && oParentNode.hasAttributes(), bHighVerb = Boolean(nVerb & 2); var sProp, vContent, nLength = 0, sCollectedTxt = "", vResult = bHighVerb ? {} : /* put here the default value for empty nodes: */ true; if (bChildren) { for (var oNode, nItem = 0; nItem < oParentNode.childNodes.length; nItem++) { oNode = oParentNode.childNodes.item(nItem); if (oNode.nodeType === 4) { sCollectedTxt += oNode.nodeValue; } /* nodeType is "CDATASection" (4) */ else if (oNode.nodeType === 3) { sCollectedTxt += oNode.nodeValue.trim(); } /* nodeType is "Text" (3) */ else if (oNode.nodeType === 1 && !oNode.prefix) { aCache.push(oNode); } /* nodeType is "Element" (1) */ } } const nLevelEnd = aCache.length, vBuiltVal = parseText(sCollectedTxt); if (!bHighVerb && (bChildren || bAttributes)) { vResult = nVerb === 0 ? objectify(vBuiltVal) : {}; } for (var nElId = nLevelStart; nElId < nLevelEnd; nElId++) { sProp = aCache[nElId].nodeName.toLowerCase(); vContent = createObjTree(aCache[nElId], nVerb, bFreeze, bNesteAttr); if (vResult.hasOwnProperty(sProp)) { if (vResult[sProp].constructor !== Array) { vResult[sProp] = [vResult[sProp]]; } vResult[sProp].push(vContent); } else { vResult[sProp] = vContent; nLength++; } } if (bAttributes) { const nAttrLen = oParentNode.attributes.length, sAPrefix = bNesteAttr ? "" : sAttrsPref, oAttrParent = bNesteAttr ? {} : vResult; for (var oAttrib, nAttrib = 0; nAttrib < nAttrLen; nLength++, nAttrib++) { oAttrib = oParentNode.attributes.item(nAttrib); oAttrParent[sAPrefix + oAttrib.name.toLowerCase()] = parseText(oAttrib.value.trim()); } if (bNesteAttr) { if (bFreeze) { Object.freeze(oAttrParent); } vResult[sAttrProp] = oAttrParent; nLength -= nAttrLen - 1; } } if (nVerb === 3 || (nVerb === 2 || nVerb === 1 && nLength > 0) && sCollectedTxt) { vResult[sValProp] = vBuiltVal; } else if (!bHighVerb && nLength === 0 && sCollectedTxt) { vResult = vBuiltVal; } if (bFreeze && (bHighVerb || nLength > 0)) { Object.freeze(vResult); } aCache.length = nLevelStart; return vResult; } function loadObjTree (oXMLDoc, oParentEl, oParentObj) { var vValue, oChild; if (oParentObj.constructor === String || oParentObj.constructor === Number || oParentObj.constructor === Boolean) { oParentEl.appendChild(oXMLDoc.createTextNode(oParentObj.toString())); /* verbosity level is 0 or 1 */ if (oParentObj === oParentObj.valueOf()) { return; } } else if (oParentObj.constructor === Date) { oParentEl.appendChild(oXMLDoc.createTextNode(oParentObj.toGMTString())); } for (var sName in oParentObj) { vValue = oParentObj[sName]; if (isFinite(sName) || vValue instanceof Function) { continue; } /* verbosity level is 0 */ if (sName === sValProp) { if (vValue !== null && vValue !== true) { oParentEl.appendChild(oXMLDoc.createTextNode(vValue.constructor === Date ? vValue.toGMTString() : String(vValue))); } } else if (sName === sAttrProp) { /* verbosity level is 3 */ for (var sAttrib in vValue) { oParentEl.setAttribute(sAttrib, vValue[sAttrib]); } } else if (sName.charAt(0) === sAttrsPref) { oParentEl.setAttribute(sName.slice(1), vValue); } else if (vValue.constructor === Array) { for (var nItem = 0; nItem < vValue.length; nItem++) { oChild = oXMLDoc.createElement(sName); loadObjTree(oXMLDoc, oChild, vValue[nItem]); oParentEl.appendChild(oChild); } } else { oChild = oXMLDoc.createElement(sName); if (vValue instanceof Object) { loadObjTree(oXMLDoc, oChild, vValue); } else if (vValue !== null && vValue !== true) { oChild.appendChild(oXMLDoc.createTextNode(vValue.toString())); } oParentEl.appendChild(oChild); } } } /* Uncomment the following code if you want to enable the .appendJXON() method for *all* the "element" objects! */ /* Element.prototype.appendJXON = function (oObjTree) { loadObjTree(document, this, oObjTree); return this; }; */ this.build = function (oXMLParent, nVerbosity /* optional */, bFreeze /* optional */, bNesteAttributes /* optional */) { const nVerbMask = arguments.length > 1 && typeof nVerbosity === "number" ? nVerbosity & 3 : /* put here the default verbosity level: */ 1; return createObjTree(oXMLParent, nVerbMask, bFreeze || false, arguments.length > 3 ? bNesteAttributes : nVerbMask === 3); }; this.unbuild = function (oObjTree, sNamespaceURI /* optional */, sQualifiedName /* optional */, oDocumentType /* optional */) { const oNewDoc = document.implementation.createDocument(sNamespaceURI || null, sQualifiedName || "", oDocumentType || null); loadObjTree(oNewDoc, oNewDoc, oObjTree); return oNewDoc; }; const sValProp = "keyValue", sAttrProp = "keyAttributes", sAttrsPref = "@", /* you can customize these values */ aCache = [], rIsNull = /^\s*$/, rIsBool = /^(?:true|false)$/i; })();
const
(constant statement) is not part of ECMAScript 5. It is supported in Firefox & Chrome (V8) and partially supported in Opera 9+ and Safari. It is not supported in Internet Explorer 6-9, or in the preview of Internet Explorer 10. const
is going to be defined by ECMAScript 6, but with different semantics. Similar to variables declared with the let
statement, constants declared with const
will be block-scoped. We used it only for didactic purpose. If you want a full browser compatibility of this library, please replace all the const
statements with the var
statements.The obtained non-native JXON
global object will have two methods:
Method | Description |
---|---|
JXON.build(document[, verbosity[, freeze[, nesteAttributes]]]) |
Returns a JavaScript Object based on the given XML Document. |
JXON.unbuild(objTree[, namespaceURI[, qualifiedNameStr[, documentType]]]) |
Returns an XML Document based on the given JavaScript Object . |
These methods are inverses of each other. So, you can work with the JXON
object by inserting the previous code at the beginning of your scripts. If you are not interested in a bidirectional conversion, don't use it, use only one of our algotithm instead.
Sample usage:
var myObject = JXON.build(doc); // we got our javascript object! try: alert(JSON.stringify(myObject)); var newDoc = JXON.unbuild(myObject); // we got our Document instance! try: alert((new XMLSerializer()).serializeToString(newDoc));
…the same thing using AJAX:
function reqListener () { var myObject = JXON.build(this.responseXML); // we got our javascript object! alert(JSON.stringify(myObject)); var newDoc = JXON.unbuild(myObject); // we got our Document instance! alert((new XMLSerializer()).serializeToString(newDoc)); }; var oReq = new XMLHttpRequest(); oReq.onload = reqListener; oReq.open("get", "example.xml", true); oReq.send();
JXON.build(document[, verbosity[, freeze[, nesteAttributes]]])
Returns a JavaScript Object
based on the given XML Document.
document
verbosity
Optional0
to 3
. It is almost equivalent to our algorithms from #4 to #1 (default value is 1
, which is equivalent to the algorithm #3).freeze
Optionalfalse
).nesteAttributes
OptionalnodeAttributes
must be nested into a child-object named keyAttributes
or not (default value is false
for verbosity levels from 0
to 2
; true
for verbosity level 3
).JXON.unbuild(objTree[, namespaceURI[, qualifiedNameStr[, documentType]]])
Returns an XML Document based on the given JavaScript Object
.
objTree
namespaceURI
OptionalDOMString
containing the namespace URI of the document to be created, or null
if the document doesn't belong to one.qualifiedNameStr
OptionalDOMString
containing the qualified name, that is an optional prefix and colon plus the local root element name, of the document to be created.documentType
OptionalDocumentType
of the document to be created. It defaults to null
.Element.prototype
objectIf you want to enable the .appendJXON()
method for all the native element
objects, you can uncomment the following code from the JXON library:
/* Uncomment the following code if you want to enable the .appendJXON() method for *all* the "element" objects! */ /* Element.prototype.appendJXON = function (oObjTree) { loadObjTree(document, this, oObjTree); return this; }; */
Imagine you want to populate the following HTMLElement
through JSON:
<div id="form_container"></div>
Then, the following code:
document.getElementById("form_container").appendJXON({ "form": { "script": { "@type": "text/javascript", "keyValue": "\n function numbersOnly (oToCheckField, oKeyEvent) {\n return oKeyEvent.charCode === 0 || /\\d/.test(String.fromCharCode(oKeyEvent.charCode));\n }\n" }, "input": [{ "@type": "hidden", "@name": "instId", "@value": 1234 }, { "@type": "hidden", "@name": "currency", "@value": "GBP" }, { "@type": "hidden", "@name": "amount", "@value": 0 }, { "@type": "hidden", "@name": "name", "@value": "CAPTURED" }], "table": { "tr": [{ "th": { "@style": "text-align: right;", "keyValue": "Product:" }, "td": { "span": [{ "input": { "@type": "radio", "@name": "nome", "@id": "rel_tshirt", "@value": "tshirt" }, "label": { "@for": "rel_tshirt", "keyValue": "T-Shirt" }, "@class": "product" }, { "input": { "@type": "radio", "@name": "nome", "@id": "rel_trousers", "@value": "trousers" }, "label": { "@for": "rel_trousers", "keyValue": "Trousers" }, "@class": "product" }, { "input": { "@type": "radio", "@name": "nome", "@id": "rel_pullover", "@value": "pullover" }, "label": { "@for": "rel_pullover", "keyValue": "Pullover" }, "@class": "product" }] } }, { "th": { "@style": "text-align: right;", "keyValue": "Quantity:" }, "td": { "input": { "@type": "text", "@name": "myInput", "@onkeypress": "return numbersOnly(this, event);", "@onpaste": "return false;" } } }] }, "p": { "input": { "@type": "submit", "@value": "Purchase!" } }, "@action": "https://secure-test.worldpay.com/wcc/purchase", "@name": "BuyForm", "@method": "POST" } });
will populate the previous element in the following way:
<div id="form_container"> <form action="https://secure-test.worldpay.com/wcc/purchase" name="BuyForm" method="POST"> <script type="text/javascript"> function numbersOnly(oToCheckField, oKeyEvent) { return oKeyEvent.charCode === 0 || /\d/.test(String.fromCharCode(oKeyEvent.charCode)); } </script> <input type="hidden" name="instId" value="1234" /> <input type="hidden" name="currency" value="GBP" /> <input type="hidden" name="amount" value="0" /> <input type="hidden" name="name" value="CAPTURED" /> <table> <tr> <th style="text-align: right;">Product:</th> <td><span class="product"><input type="radio" name="nome" id="rel_tshirt" value="tshirt"/><label for="rel_tshirt">T-Shirt</label></span><span class="product"><input type="radio" name="nome" id="rel_trousers" value="trousers"/><label for="rel_trousers">Trousers</label></span><span class="product"><input type="radio" name="nome" id="rel_pullover" value="pullover"/><label for="rel_pullover">Pullover</label></span> </td> </tr> <tr> <th style="text-align: right;">Quantity:</th> <td> <input type="text" name="myInput" onkeypress="return numbersOnly(this, event);" onpaste="return false;" /> </td> </tr> </table> <p> <input type="submit" value="Purchase!" /> </p> </form> </div>
/* The structure of my document */ var oMyHTMLStruct = { "html": { "head": { "meta": { "@http-equiv": "Content-Type", "@content": "text/html; charset=UTF-8" }, "title": "My HTML Document", "script": { "@type": "text/javascript", "keyValue": "alert(\"Welcome!\");" }, "style": "p:first-letter {\n font: italic bold 30px Georgia, serif;\n}" }, "body": { "h1": "My HTML Document", "p": "Hello world!!" } } }; /* Create the document */ var oMyHTMLDoc = JXON.unbuild(oMyHTMLStruct, "http://www.w3.org/1999/xhtml");
…And here is the output of alert((new XMLSerializer()).serializeToString(oMyHTMLDoc))
:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>My HTML Document</title> <script type="text/javascript"> alert("Welcome!"); </script> <style> p:first-letter { font: italic bold 30px Georgia, serif; } </style> </head> <body> <h1>My HTML Document</h1> <p>Hello world!!</p> </body> </html>
<p>She <strong>loves</strong> you. And definitely <strong>hates</strong> me.</p>would determine a result like the following:
<p><strong>loves</strong><strong>hates</strong>Sheyou. And definitelyme.</p>As you can see in this special case, the whole information is preserved, the ordering of the elements is not.
The JXON.build()
method summarizes all our four ways of conversion (see: #1, #2, #3, #4). The result is therefore the same of our four algorithms, depending on the level of verbosity utilised. As above, optional properties and methods (commented in the example) of the first algorithm (verbosity level: 3) are not included.
The JXON.unbuild()
method utilises our reverse algorithm.
Therefore, all code considerations remain the same.