JavaScript 小技 - XML 字串縮排顯示與互動檢視
0 |
這些年在 JavaScript 處理 XML 的機會愈來愈少,但從事古蹟翻新與維護難免遇到。最近想為老系統增加一個編輯及檢視 XML 的小工具,有兩個小需求:
- 想讓 <textarea> 輸入的 XML 內容依標準縮排規則排版
- 提供可以展開、縮合 XML 節點的互動式 XML 檢視器
兩個都算常見的需求,理應有現成可撿,不需要自己造輪子,重點在找到符合自己磁場的版本。內部使用,故瀏覽器僅需支援 Chrome/Edge,不用費心跨瀏覽器問題,感覺輕鬆好多,算是簡單任務。
我喜歡輕巧、簡潔的程式碼,最好只用一小段 JavaScript 函式搞定,依這個標準在 Internet 搜尋一番,整理出一套極簡風格做法。
原以為 JavaScript 應已內建 XML 排版 API 卻撲了空,最後找到一段 Gist 範例,用 RegExp 加 .split().map().join() 不到 20 行搞定排版,深得我心。我有多加 DOMParser + XMLSerializer 前處理,清除標籤自身的換行。程式碼與實測結果如下:
<!DOCTYPE html>
<html>
<body>
<div>
<button onclick="test()">Format XML</button>
</div>
<textarea id="result" rows="8" cols="32"></textarea>
<script>
var xml = `<rows>
<row id="1">ITEM1</row>
<row id="2"
remark="reserved"
>ITEM2</row>
<row id="3" /><row id="4">ITEM4</row>
</rows>`;
document.getElementById('result').value = xml;
function test() {
let res = document.getElementById('result');
res.value = formatXml(res.value);
}
//
function formatXml(xml) {
xml = new XMLSerializer().serializeToString(
new DOMParser().parseFromString(xml, 'text/xml'))
.replace(/>\s{0,}</g, '><');
const PADDING = ' '.repeat(2); // set desired indent size here
const reg = /(>)(<)(\/*)/g;
let pad = 0;
xml = xml.replace(reg, '$1\r\n$2$3');
return xml.split('\r\n').map((node, index) => {
let indent = 0;
if (node.match(/.+<\/\w[^>]*>$/)) {
indent = 0;
} else if (node.match(/^<\/\w/) && pad > 0) {
pad -= 1;
} else if (node.match(/^<\w[^>]*[^\/]>.*$/)) {
indent = 1;
} else {
indent = 0;
}
pad += indent;
return PADDING.repeat(pad - indent) + node;
}).join('\r\n');
}
</script>
</body>
</html>
至於 XML 節點展開收合檢視功能,有些人會選擇用 XSLT 實現,我則找到一個開源 jQuery 小套件 - SimpleXML。
程式含註解約 200 行,嫌要載入 css 及使用 png 不夠輕巧,我將 css 內容放進 js 改為動態注入,換用較小的展開收合圖示並改為 DataUri 不再需要 png 檔,如此只需一個 js 搞定,更符合 KISS 精神,並將修改過的版本命名為 SimplerXML.js:(比簡單更簡單 XD)
(function ($) {
// Additional features that are needed
//
// allow specification of how attributes are shown
// Data Model:
//
// One of the following must be supplied
// xml - the XML object to be shown
// xmlString - the XML string to be shown
//
// Options:
// collapsedText - the text shown when the node is collapsed. Defaults to '...'
$.fn.simpleXML = function (options) {
// This is the easiest way to have default options.
var settings = $.extend({
// These are the defaults.
collapsedText: "...",
}, options);
if (settings.xml == undefined && settings.xmlString == undefined)
throw "No XML to be displayed was supplied";
if (settings.xml != undefined && settings.xmlString != undefined)
throw "Only one of xml and xmlString may be supplied";
var xml = settings.xml;
if (xml == undefined)
xml = $.parseXML(settings.xmlString);
return this.each(function () {
var wrapperNode = document.createElement("span");
$(wrapperNode).addClass("simpleXML");
showNode(wrapperNode, xml, settings);
this.appendChild(wrapperNode);
$(wrapperNode).find(".simpleXML-expanderHeader").click(function () {
var expanderHeader = $(this).closest(".simpleXML-expanderHeader");
var expander = expanderHeader.find(".simpleXML-expander");
var content = expanderHeader.parent().find(".simpleXML-content").first();
var collapsedText = expanderHeader.parent().children(".simpleXML-collapsedText").first();
var closeExpander = expanderHeader.parent().children(".simpleXML-expanderClose").first();
if (expander.hasClass("simpleXML-expander-expanded")) {
// Already Expanded, therefore collapse time...
expander.removeClass("simpleXML-expander-expanded").addClass("simpleXML-expander-collapsed");
collapsedText.attr("style", "display: inline;");
content.attr("style", "display: none;");
closeExpander.attr("style", "display: none");
}
else {
// Time to expand..
expander.addClass("simpleXML-expander-expanded").removeClass("simpleXML-expander-collapsed");
collapsedText.attr("style", "display: none;");
content.attr("style", "");
closeExpander.attr("style", "");
}
});
});
};
let css = `.simpleXML ul
{ list-style: none; margin: 0px; padding-left: 1.2em; }
.simpleXML { font-family: 'Courier New'; }
.simpleXML-comment { color: green; }
.simpleXML-tagHeader { color: blue; }
.simpleXML-cdata { color: gray; }
.simpleXML-tagValue { color: darkred; }
.simpleXML-collapsedText { color: lightgray; }
.simpleXML-attrName { color: red; }
.simpleXML-attrValue { color: blue; }
.simpleXML span.simpleXML-expander, .simpleXML span.simpleXML-expanderClose
{ height: 1em; width: 1em; display: inline-block; }
.simpleXML span.simpleXML-expanderHeader { cursor: pointer; }
.simpleXML span.simpleXML-expanderHeader span.simpleXML-expander {
background-repeat: no-repeat; background-position: center;
position: relative; top: 0.2em;
border: 1px solid lightgray; margin-right: 2px;
}
.simpleXML span.simpleXML-expander-collapsed
{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAGCAYAAAAG5SQMAAAAOUlEQVR42jXKwQkAMAgDwKwqKD4EwQ26sSOkVWjgIIHAzPiCgaqiqnJHZnKICBERHN194O5b9vbLuAVRL+l0YWnZAAAAAElFTkSuQmCCXA==");
}
.simpleXML span.simpleXML-expander-expanded
{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42mWKsQ0AMAzC8ixLlrzQjzmBiEjp0A6WwBCSPgKAXoLkqSot7nN3yMwR7pZ32NzpKkVoDBUxKAAAAABJRU5ErkJggg==");
}`;
if ($('#simpleXmlCss').length == 0) {
$('head').append('<style id="simpleXmlCss">' + css + '</style>');
}
function showNode(parent, xml, settings) {
if (xml.nodeType == 9) {
for (var i = 0; i < xml.childNodes.length; i++)
showNode(parent, xml.childNodes[i], settings);
return;
}
switch (xml.nodeType) {
case 1: // Simple element
var hasChildNodes = xml.childNodes.length > 0;
var expandingNode = hasChildNodes && (xml.childNodes.length > 1 || xml.childNodes[0].nodeType != 3);
var expanderHeader = expandingNode ? makeSpan("", "simpleXML-expanderHeader") : parent;
var expanderSpan = makeSpan("", "simpleXML-expander");
if (expandingNode)
$(expanderSpan).addClass("simpleXML-expander-expanded");
expanderHeader.appendChild(expanderSpan);
expanderHeader.appendChild(makeSpan("<", "simpleXML-tagHeader"));
expanderHeader.appendChild(makeSpan(xml.nodeName, "simpleXML-tagValue"));
if (expandingNode)
parent.appendChild(expanderHeader);
// Handle attributes
var attributes = xml.attributes;
for (var attrIdx = 0; attrIdx < attributes.length; attrIdx++) {
expanderHeader.appendChild(makeSpan(" "));
expanderHeader.appendChild(makeSpan(attributes[attrIdx].name, "simpleXML-attrName"));
expanderHeader.appendChild(makeSpan('="'));
expanderHeader.appendChild(makeSpan(attributes[attrIdx].value, "simpleXML-attrValue"));
expanderHeader.appendChild(makeSpan('"'));
}
// Handle child nodes
if (hasChildNodes) {
parent.appendChild(makeSpan(">", "simpleXML-tagHeader"));
if (expandingNode) {
var ulElement = document.createElement("ul");
for (var i = 0; i < xml.childNodes.length; i++) {
var liElement = document.createElement("li");
showNode(liElement, xml.childNodes[i], settings);
ulElement.appendChild(liElement);
}
var collapsedTextSpan = makeSpan(settings.collapsedText, "simpleXML-collapsedText");
collapsedTextSpan.setAttribute("style", "display: none;");
ulElement.setAttribute("class", "simpleXML-content");
parent.appendChild(collapsedTextSpan);
parent.appendChild(ulElement);
parent.appendChild(makeSpan("", "simpleXML-expanderClose"));
}
else {
parent.appendChild(makeSpan(xml.childNodes[0].nodeValue));
}
// Closing tag
parent.appendChild(makeSpan("</", "simpleXML-tagHeader"));
parent.appendChild(makeSpan(xml.nodeName, "simpleXML-tagValue"));
parent.appendChild(makeSpan(">", "simpleXML-tagHeader"));
} else {
var closingSpan = document.createElement("span");
closingSpan.innerText = "/>";
parent.appendChild(closingSpan);
}
break;
case 3: // text
if (xml.nodeValue.trim() !== "") {
parent.appendChild(makeSpan("", "simpleXML-expander"));
parent.appendChild(makeSpan(xml.nodeValue));
}
break;
case 4: // cdata
parent.appendChild(makeSpan("", "simpleXML-expander"));
parent.appendChild(makeSpan("<![CDATA[", "simpleXML-tagHeader"));
parent.appendChild(makeSpan(xml.nodeValue, "simpleXML-cdata"));
parent.appendChild(makeSpan("]]>", "simpleXML-tagHeader"));
break;
case 8: // comment
parent.appendChild(makeSpan("", "simpleXML-expander"));
parent.appendChild(makeSpan("<!--" + xml.nodeValue + "-->", "simpleXML-comment"));
break;
default:
var item = document.createElement("span");
item.innerText = "" + xml.nodeType + " - " + xml.name;
parent.appendChild(item);
break;
}
function makeSpan(innerText) {
return makeSpan(innerText, undefined);
}
function makeSpan(innerText, classes) {
var span = document.createElement("span");
span.innerText = innerText;
if (classes != undefined)
span.setAttribute("class", classes);
return span;
}
}
}(jQuery));
測試網頁:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="simplerXML.js"></script>
</head>
<body>
<button>12pt</button>
<button>10pt</button>
<button>9pt</button>
<div id="demo">
</div>
<script>
$.get('demo.xml', function (data) {
$('#demo').empty().simpleXML({ xml: data });
});
$('button').click(function () {
$("#demo").css('font-size', $(this).text());
});
</script>
</body>
</html>
Examples XML formatting and interactive node expanding/collapsing with JavaScript.
Comments
Be the first to post a comment