使用JavaScript美化XML

170

我有一个字符串,表示未缩进的XML,我想将其格式化为漂亮的形式。例如:

<root><node/></root>

应该变成:

<root>
  <node/>
</root>

语法高亮不是必需的。为了解决这个问题,我首先将XML转换为添加回车和空格,然后使用pre标签输出XML。为了添加新行和空格,我编写了以下函数:

function formatXml(xml) {
    var formatted = '';
    var reg = /(>)(<)(\/*)/g;
    xml = xml.replace(reg, '$1\r\n$2$3');
    var pad = 0;
    jQuery.each(xml.split('\r\n'), function(index, node) {
        var indent = 0;
        if (node.match( /.+<\/\w[^>]*>$/ )) {
            indent = 0;
        } else if (node.match( /^<\/\w/ )) {
            if (pad != 0) {
                pad -= 1;
            }
        } else if (node.match( /^<\w[^>]*[^\/]>.*$/ )) {
            indent = 1;
        } else {
            indent = 0;
        }

        var padding = '';
        for (var i = 0; i < pad; i++) {
            padding += '  ';
        }

        formatted += padding + node + '\r\n';
        pad += indent;
    });

    return formatted;
}
我随后这样调用函数:
jQuery('pre.formatted-xml').text(formatXml('<root><node1/></root>'));

我觉得这个方法对我来说完美地运行良好,但是在编写前一个函数时,我认为一定有更好的方法。所以我的问题是,你知道有没有更好的方法能够在HTML页面中漂亮地打印给定的XML字符串吗?欢迎使用任何JavaScript框架和/或插件来完成此任务。我唯一的要求是在客户端上完成。


3
如果想要得到漂亮的HTML输出(类似于IE XML显示),请查看XPath Visualizer中使用的XSLT转换。您可以在以下网址下载XPath Visualizer:http://www.huttar.net/dimitre/XPV/TopXML-XPV.html - Dimitre Novatchev
//.+</\w[^>]*>$/ - 在一些 JavaScript 引擎中,对于具有“长属性值”的节点,去掉正则表达式中的“+”可以提高代码运行速度。请注意,这是一个针对编程的内容翻译,不要进行解释。 - 4esn0k
22个回答

73

这可以使用本地JavaScript工具完成,不需要第三方库,扩展@Dimitre Novatchev的答案:

var prettifyXml = function(sourceXml)
{
    var xmlDoc = new DOMParser().parseFromString(sourceXml, 'application/xml');
    var xsltDoc = new DOMParser().parseFromString([
        // describes how we want to modify the XML - indent everything
        '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform">',
        '  <xsl:strip-space elements="*"/>',
        '  <xsl:template match="para[content-style][not(text())]">', // change to just text() to strip space in text nodes
        '    <xsl:value-of select="normalize-space(.)"/>',
        '  </xsl:template>',
        '  <xsl:template match="node()|@*">',
        '    <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>',
        '  </xsl:template>',
        '  <xsl:output indent="yes"/>',
        '</xsl:stylesheet>',
    ].join('\n'), 'application/xml');

    var xsltProcessor = new XSLTProcessor();    
    xsltProcessor.importStylesheet(xsltDoc);
    var resultDoc = xsltProcessor.transformToDocument(xmlDoc);
    var resultXml = new XMLSerializer().serializeToString(resultDoc);
    return resultXml;
};

console.log(prettifyXml('<root><node/></root>'));

输出:

<root>
  <node/>
</root>

JSFiddle

请注意,正如@jat255所指出的那样,在Firefox中不支持使用<xsl:output indent="yes"/>进行漂亮的打印。它似乎只能在Chrome、Opera和其他基于Webkit的浏览器中工作。


非常好的答案,但不幸的是,Internet Explorer 再次搞砸了派对。 - Waruyama
很好,它只在输入的XML是单行时才有效...如果您不关心文本节点中的多行,在调用prettify之前,请调用private makeSingleLine(txt: string): string { let s = txt.trim().replace(new RegExp("\r", "g"), "\n"); let angles = ["<", ">"]; let empty = [" ", "\t", "\n"]; while (s.includes(" <") || s.includes("\t<") || s.includes("\n<") || s.includes("> ") || s.includes(">\t") || s.includes(">/n")) { angles.forEach(an => { empty.forEach(em => { s = s.replace(new RegExp(em + an, "g"), an); }); }); } return s.replace(new RegExp("\n", "g"), " "); } - Sasha Bond
10
我遇到了一个错误,但是这个错误没有任何提示信息。在使用 Firefox 浏览器时,在 fiddle 中也会出现这个问题。 - Tomáš Zato
2
这在Firefox中对我也不起作用,出现了一个空白错误。 - jat255
8
这个问题讨论在这里:https://dev59.com/K63la4cB1Zd3GeqPTd_4。显然,Firefox需要一个xsl版本规范,但是无论如何都没有关系,因为Mozilla实现不尊重任何“xsl:output”标记,所以你无论如何都不会得到漂亮的格式化。 - jat255

61

根据问题的文本,我得到的印象是期望一个字符串结果而不是HTML格式的结果。

如果是这样,实现最简单的方法是使用相同转换<xsl:output indent="yes"/>指令来处理XML文档:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

    <xsl:template match="node()|@*">
      <xsl:copy>
        <xsl:apply-templates select="node()|@*"/>
      </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

当将此转换应用于提供的XML文档时:

<root><node/></root>

大多数XSLT处理器(.NET XslCompiledTransform、Saxon 6.5.4和Saxon 9.0.0.2、AltovaXML)都会产生所需的结果:

<root>
  <node />
</root>

4
看起来这是一个很好的解决方案。有没有跨浏览器的方式在JavaScript中应用这个转换?我没有可依赖的服务器端脚本。 - Darin Dimitrov
8
“不工作”的意思是什么?“Chrome”是什么?我从未听说过这样的XSLT处理器。此外,如果您查看答案的日期,当时还不存在Chrome浏览器。 - Dimitre Novatchev
3
请注意,这个问题(以及我的回答)是为了获得漂亮的XML字符串(文本),而不是HTML。难怪这样的字符串在浏览器中无法显示。如果想要一个花哨的HTML输出(如IE XML显示),请参考XPath Visualizer中使用的XSLT转换。您可以在http://huttar.net/dimitre/XPV/TopXML-XPV.html下载XPath Visualizer。您可能需要稍微调整代码(例如删除用于折叠/展开节点的javascript扩展函数),但是生成的HTML应该可以正常显示。 - Dimitre Novatchev
2
原始问题要求使用JavaScript的方法。如何使用JavaScript使此答案起作用? - JohnK
2
JohnK,2008年回答这个问题时,在IE中人们从JavaScript启动XSLT转换 - 调用MSXML3。现在他们仍然可以这样做,尽管随IE11提供的XSLT处理器是MSXML6。所有其他浏览器都具有类似的功能,尽管它们具有不同的内置XSLT处理器。这就是为什么原始提问者从未提出这样的问题的原因。 - Dimitre Novatchev
显示剩余10条评论

43

当我有类似要求时,我发现了这个帖子,但是我将原帖的代码简化如下:

function formatXml(xml, tab) { // tab = optional indent value, default is tab (\t)
    var formatted = '', indent= '';
    tab = tab || '\t';
    xml.split(/>\s*</).forEach(function(node) {
        if (node.match( /^\/\w/ )) indent = indent.substring(tab.length); // decrease indent by one 'tab'
        formatted += indent + '<' + node + '>\r\n';
        if (node.match( /^<?\w[^>]*[^\/]$/ )) indent += tab;              // increase indent
    });
    return formatted.substring(1, formatted.length-3);
}

对我来说可行!


1
我尝试了一些xsltProcessor的答案,它们在我的浏览器中都可以百分之百地工作。但我认为这个答案很好且简单易于单元测试 - XSLT不是Node.js的一部分,而我在Jest测试期间使用的是Node.js,我不想仅为单元测试安装它。另外,我在https://developer.mozilla.org/en-US/docs/Web/API/XSLTProcessor 上读到 - 此功能非标准,并且没有遵循标准轨道。不要在面向Web的生产站点上使用它:它将无法为每个用户工作。实现之间可能也存在大量不兼容性,并且行为可能会在未来发生变化。 - k1eran
顺便说一句,ESLint 告诉我有一个不必要的转义符号,我的 IDE 自动更正为 (/^<?\w[^>]*[^/]$/)) - k1eran
1
当标签只有一个字母时,例如 <a>/^<?\w[^>]*[^\/]$/ 会失败。建议使用 /^<?\w([^>/]*|[^>]*[^/])$/ - sam hocevar
3
建议使用这个编辑器(https://jsfiddle.net/fbn5j7ya/),它的速度几乎是原来的两倍。函数式循环和正则表达式都比较慢(https://jsbench.me/zhkm1virni/3)。 - milahu
@milahu:你的回答非常好。请单独发布为一个答案!另外:你应该在你的代码中添加一个小文档:要获得压缩的XML,请传递:tab =“”和nl =“”。 - kevinarpe

33

对 efnx clckclcks 的 JavaScript 函数进行了轻微修改。我将格式从空格改为制表符,但最重要的是,我允许文本保持在一行上:

var formatXml = this.formatXml = function (xml) {
        var reg = /(>)\s*(<)(\/*)/g; // updated Mar 30, 2015
        var wsexp = / *(.*) +\n/g;
        var contexp = /(<.+>)(.+\n)/g;
        xml = xml.replace(reg, '$1\n$2$3').replace(wsexp, '$1\n').replace(contexp, '$1\n$2');
        var pad = 0;
        var formatted = '';
        var lines = xml.split('\n');
        var indent = 0;
        var lastType = 'other';
        // 4 types of tags - single, closing, opening, other (text, doctype, comment) - 4*4 = 16 transitions 
        var transitions = {
            'single->single': 0,
            'single->closing': -1,
            'single->opening': 0,
            'single->other': 0,
            'closing->single': 0,
            'closing->closing': -1,
            'closing->opening': 0,
            'closing->other': 0,
            'opening->single': 1,
            'opening->closing': 0,
            'opening->opening': 1,
            'opening->other': 1,
            'other->single': 0,
            'other->closing': -1,
            'other->opening': 0,
            'other->other': 0
        };

        for (var i = 0; i < lines.length; i++) {
            var ln = lines[i];

            // Luca Viggiani 2017-07-03: handle optional <?xml ... ?> declaration
            if (ln.match(/\s*<\?xml/)) {
                formatted += ln + "\n";
                continue;
            }
            // ---

            var single = Boolean(ln.match(/<.+\/>/)); // is this line a single tag? ex. <br />
            var closing = Boolean(ln.match(/<\/.+>/)); // is this a closing tag? ex. </a>
            var opening = Boolean(ln.match(/<[^!].*>/)); // is this even a tag (that's not <!something>)
            var type = single ? 'single' : closing ? 'closing' : opening ? 'opening' : 'other';
            var fromTo = lastType + '->' + type;
            lastType = type;
            var padding = '';

            indent += transitions[fromTo];
            for (var j = 0; j < indent; j++) {
                padding += '\t';
            }
            if (fromTo == 'opening->closing')
                formatted = formatted.substr(0, formatted.length - 1) + ln + '\n'; // substr removes line break (\n) from prev loop
            else
                formatted += padding + ln + '\n';
        }

        return formatted;
    };

请问您能否更新您的函数,以考虑下面Chuan Ma的评论?这对我很有帮助。谢谢。编辑:我已经自己完成了。 - Louis LC
1
嗨,我稍微改进了一下你的函数,以便正确处理XML文本开头的可选<?xml ... ?>声明。 - lviggiani

17

个人而言,我使用google-code-prettify以及以下的函数:

prettyPrintOne('<root><node1><root>', 'xml')

3
抱歉,您需要缩进XML代码,而Google Code Prettify仅为代码着色。 - Touv
1
结合 prettify 和类似于 https://dev59.com/qnVC5IYBdhLWcg3w9F89 的东西。 - Chris
3
结合 http://code.google.com/p/vkbeautify/ 的缩进功能,这是一个不错的组合。 - Vdex
已从Google Code迁移到GitHub。新链接:https://github.com/google/code-prettify - mUser1990

9

或者如果你只是想要另一个JavaScript函数来完成它,我已经对Darin的函数进行了大量修改:

var formatXml = this.formatXml = function (xml) {
    var reg = /(>)(<)(\/*)/g;
    var wsexp = / *(.*) +\n/g;
    var contexp = /(<.+>)(.+\n)/g;
    xml = xml.replace(reg, '$1\n$2$3').replace(wsexp, '$1\n').replace(contexp, '$1\n$2');
    var pad = 0;
    var formatted = '';
    var lines = xml.split('\n');
    var indent = 0;
    var lastType = 'other';
    // 4 types of tags - single, closing, opening, other (text, doctype, comment) - 4*4 = 16 transitions 
    var transitions = {
        'single->single'    : 0,
        'single->closing'   : -1,
        'single->opening'   : 0,
        'single->other'     : 0,
        'closing->single'   : 0,
        'closing->closing'  : -1,
        'closing->opening'  : 0,
        'closing->other'    : 0,
        'opening->single'   : 1,
        'opening->closing'  : 0, 
        'opening->opening'  : 1,
        'opening->other'    : 1,
        'other->single'     : 0,
        'other->closing'    : -1,
        'other->opening'    : 0,
        'other->other'      : 0
    };

    for (var i=0; i < lines.length; i++) {
        var ln = lines[i];
        var single = Boolean(ln.match(/<.+\/>/)); // is this line a single tag? ex. <br />
        var closing = Boolean(ln.match(/<\/.+>/)); // is this a closing tag? ex. </a>
        var opening = Boolean(ln.match(/<[^!].*>/)); // is this even a tag (that's not <!something>)
        var type = single ? 'single' : closing ? 'closing' : opening ? 'opening' : 'other';
        var fromTo = lastType + '->' + type;
        lastType = type;
        var padding = '';

        indent += transitions[fromTo];
        for (var j = 0; j < indent; j++) {
            padding += '    ';
        }

        formatted += padding + ln + '\n';
    }

    return formatted;
};

6
所有这里提供的JavaScript函数都不能用于XML文档中,当该文档在结束标记“>”和开始标记“<”之间存在未指定的空格时。要修复它们,您只需替换函数中的第一行即可。
var reg = /(>)(<)(\/*)/g;

by

var reg = /(>)\s*(<)(\/*)/g;

4
您可以使用xml-beautify来获得格式良好的XML。
var prettyXmlText = new XmlBeautify().beautify(xmlText, 
                    {indent: "  ",useSelfClosingElement: true});

缩进:类似空格的缩进模式。

useSelfClosingElement:true => 当元素为空时使用自闭合元素。

JSFiddle

原始内容(之前)

<?xml version="1.0" encoding="utf-8"?><example version="2.0">
  <head><title>Original aTitle</title></head>
  <body info="none" ></body>
</example>

美化后

<?xml version="1.0" encoding="utf-8"?>
<example version="2.0">
  <head>
    <title>Original aTitle</title>
  </head>
  <body info="none" />
</example>

4
创建一个桩节点(document.createElement('div') - 或使用您的库等效物),通过innerHTML将其填充为XML字符串,并针对根元素/或者如果没有根,则使用桩元素调用简单递归函数。该函数将为所有子节点调用自身。
您可以在此过程中进行语法高亮显示,确保标记是格式良好的(当通过innerHTML附加时,浏览器会自动完成此操作)等。这不会产生太多代码,而且可能足够快。

4
听起来像是一个惊人、优雅的解决方案概要。那么实现呢? - JohnK

4

prettydiff是一个非常好的工具。这里有更多关于使用的信息:https://dev59.com/3XjZa4cB1Zd3GeqPitfi#30648547 - bob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接