XML フォーマッター
インデントして整える。混在内容はそのまま保持します。
すべてこのタブ内で実行されます。貼り付けた内容がアップロード・記録・送信されることはありません。 ネットワークパネルを開いて確認する.
上に文書を貼り付けると、入力しながらインデントし直されます。スペース 2 個、4 個、またはタブを選べ、要素が 3 個・4 個・6 個以上の属性を持つときは属性を個別の行に送り出せます。結果は入力の隣にある読み取り専用のペインに現れます。アップロードはありません。パーサーもフォーマッターもこのタブで動く JavaScript で、作業中もネットワークパネルは空のままです。
フォーマッターが要るのは、その XML を別の何かが吐き出したときです。ログから 40,000 文字の 1 行として取り出した SOAP レスポンス、デプロイツールが書き換えた設定ファイル、機械生成のサイトマップ。これは最速の整形式チェックでもあります。フォーマッターは、解析できないものをインデントできないからです。
ここが違うのは混在内容の扱いです。ある要素がテキストと子要素の両方を持つとき、その間の空白はデータであり、それを「整えた」フォーマッターは文書を書き換えてしまっています。そうした部分木はバイト単位でそのまま複製します。オンラインのフォーマッターの多くはこれをしません。いくつかはパーサーですらなく、山かっこに対する文字列操作です。
整形が変えてよいものは何か
XML 文書の中で本当に無意味なものは 1 つだけです。要素のみを内容とするモデルにおける、要素と要素のあいだの空白です。フォーマッターはそれを削除し、自前のものを生成してかまいません。それ以外はすべて内容であり、安全な考え方は、そうでないと証明されるまですべてのバイトを内容として扱うことです。
ですから、兄弟要素のあいだにある空白だけのテキストノードは破棄され、あなたのインデント設定から生成し直されます。それ以外には手を触れません。属性値は書かれたとおりに出力します。再エスケープすると & が & になってしまいますし、&companyName; のような DTD 宣言済み実体を参照する値は DTD なしには解釈できないからです。元の引用符も保ちます。単一引用符で書かれた値は二重引用符を合法的に含みうるからです。
テキストの内部が折り返されることはありません。テキストのみを持つ要素の先頭と末尾の空白だけが取り除かれるので、<price> 42.00 </price> は <price>42.00</price> になりますが、<note>スペース 2 個</note> は 2 個のまま残ります。
混在内容と、多くのフォーマッターが壊れる理由
要素が混在内容を持つとは、その子にテキストとマークアップの両方が含まれることです。<p>Hello <b>world</b>!</p> のように。"Hello" のあとの空白は文書中の 1 文字であり、</b> のあとの感嘆符も同じです。子をそれぞれ独立した行にインデントして置けば、テキストノードを連結する側は違う文字列を受け取ります。それは整形ではなく、静かな破壊です。
すべての要素は直列化の前に検査されます。空でないテキストや CDATA セクションと並んで要素の子を持っている場合、その部分木は元のソースからそのまま複製され、内部にはどの規則も適用されません。この取引は意図的なものです。混在した部分木は、たとえ見苦しい体裁でも到着したときのまま残します。代わりの選択肢は「間違えること」だからです。
これは珍しい事例ではありません。CMS のエクスポートに含まれる XHTML 断片、DocBook や DITA、スキーマ内の xs:documentation、インラインマークアップを含む RSS の description。あなたの文書にそれらがなければ、この配慮は何のコストにもなりません。あるなら、それがすべてです。
- 混在しているのでそのまま複製:<line>合計: <amount>9.99</amount>(税抜)</line>。
- 混在していないので自由にインデント:<order><id>1</id><status>open</status></order>。
インデントと、SOAP・XSD のための属性折り返し
スペース 2 個が既定なのは、多くの XML ツールがそう出力するからです。スペース 4 個があるのは、多くの業務コードベースがそれで統一しているからです。タブがあるのは、.editorconfig でそう決めているリポジトリがあるからであり、またタブは 1 バイト、スペース 4 個は 4 バイトだからです。
SOAP と XSD にとって効いてくるのは属性の設定です。SOAP エンベロープのルートは名前空間宣言を 5 つ抱えるのが普通ですし、xs:element 宣言は name、type、minOccurs、maxOccurs、nillable、default を持ちます。1 行にすれば誰も読まない 200 文字です。しきい値を 3・4・6 のいずれかにすれば、それ以上の属性を持つ要素は 1 行 1 属性になり、小さい要素は 1 行のままです。
このツールがしないこと
整形式でない文書は整形しません。入力はそのまま返され、代わりにすべてのエラーが行・列・直し方つきで並びます。もう 2 つ、はっきり書いておくべき限界があります。xml:space="preserve" は特別扱いしないので、それを持つテキストのみの要素でも先頭と末尾の空白は取り除かれます。また <a>text<!-- なぜ -->more</a> のようにコメントを挟んだテキストは、この判定では混在内容になりません。コメントは要素ではないからです。結果としてインデントし直され、テキストに空白が加わります。どちらも狭い条件ですが、どちらも実在します。そして黙ってそうするツールなら、あなたに教えてはくれません。
整形は冪等です。同じ設定で出力をもう一度通しても同じバイト列になるので、pre-commit フックでも安全に使えます。1 MB の文書で約 200 ミリ秒、5 MB で 1 秒弱です。上限は 20 MB で、これは方針ではなくメモリーの制約です。
コードで XML を整形する
実際に XML を処理する言語での同じ操作です。どのサンプルも安全に解析します。Java、PHP、Python の標準ライブラリは既定で外部実体を解決してしまうからです。コメントには、各ライブラリがどこで混在内容を組み替えてしまうかを記してあります。
// Browsers ship a parser and a serialiser but no pretty printer. This walker
// indents only elements whose children are all elements: touching anything
// else would rewrite mixed content.
function indentXml(source, unit = ' ') {
const doc = new DOMParser().parseFromString(source, 'application/xml');
if (doc.querySelector('parsererror')) {
throw new Error(doc.querySelector('parsererror').textContent.trim());
}
const walk = (el, depth) => {
const kids = [...el.childNodes];
const elementOnly =
kids.some((n) => n.nodeType === 1) &&
kids.every((n) => n.nodeType !== 3 || !n.nodeValue.trim());
if (!elementOnly) return; // mixed or text-only: leave the subtree alone
for (const n of kids) if (n.nodeType === 3) el.removeChild(n);
for (const child of [...el.children]) {
el.insertBefore(doc.createTextNode('\n' + unit.repeat(depth + 1)), child);
walk(child, depth + 1);
}
el.appendChild(doc.createTextNode('\n' + unit.repeat(depth)));
};
walk(doc.documentElement, 0);
return new XMLSerializer().serializeToString(doc);
}
// Browsers never resolve external entities, so XXE is not reachable here.
// Internal entity expansion is, so cap the input size before parsing.# ElementTree.indent (3.9+) only adds whitespace where an element has no
# non-whitespace text, so mixed content survives. It does drop comments,
# because the default parser never builds them.
import xml.etree.ElementTree as ET
from defusedxml.ElementTree import fromstring
root = fromstring(source) # safe: no entity expansion, no network
ET.indent(root, space=' ') # four spaces
print(ET.tostring(root, encoding='unicode'))
# lxml keeps comments and processing instructions, and etree.indent applies
# the same mixed-content rule:
#
# from lxml import etree
# parser = etree.XMLParser(resolve_entities=False, no_network=True,
# load_dtd=False, huge_tree=False)
# tree = etree.fromstring(source.encode(), parser)
# etree.indent(tree, space=' ')
# print(etree.tostring(tree, encoding='unicode'))
#
# Do not add remove_blank_text=True unless the document has a DTD. Without
# one, lxml guesses which blank text nodes are ignorable.import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.*;
import org.w3c.dom.*;
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
Document doc = dbf.newDocumentBuilder().parse(new java.io.File("in.xml"));
// The serialiser adds indentation on top of the whitespace already in the
// tree, so an already-indented file gets deeper on every run. Remove the
// blank text nodes first. The second predicate keeps the blanks that sit
// inside mixed content, where a sibling text node carries real characters.
XPath xpath = XPathFactory.newInstance().newXPath();
NodeList blanks = (NodeList) xpath.evaluate(
"//text()[not(normalize-space())][not(../text()[normalize-space()])]",
doc, XPathConstants.NODESET);
for (int i = 0; i < blanks.getLength(); i++) {
Node n = blanks.item(i);
n.getParentNode().removeChild(n);
}
TransformerFactory tf = TransformerFactory.newInstance();
tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
Transformer t = tf.newTransformer();
t.setOutputProperty(OutputKeys.INDENT, "yes");
t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
t.transform(new DOMSource(doc), new StreamResult(System.out));using System.Text;
using System.Xml;
using System.Xml.Linq;
// XDocument.Parse discards whitespace-only text nodes by default, which is
// what you want for element-only content. On mixed content it also removes
// the space in <p>a <b>x</b> <i>y</i></p>, so pass
// LoadOptions.PreserveWhitespace when the document carries prose.
var doc = XDocument.Parse(source);
// DtdProcessing is Prohibit by default for the reader XDocument builds, so
// external entities are never fetched. Say it out loud when you construct
// the reader yourself.
var settings = new XmlWriterSettings
{
Indent = true,
IndentChars = " ",
OmitXmlDeclaration = false,
};
var output = new StringBuilder();
using (var writer = XmlWriter.Create(output, settings))
{
doc.Save(writer);
}
Console.WriteLine(output.ToString());
// XmlWriter stops indenting an element once character data has been written
// into it, so it will not reflow mixed content it is given.<?php
$doc = new DOMDocument();
// Both flags must be set before loading. preserveWhiteSpace = false makes
// libxml2 drop blank text nodes; without a DTD it applies a heuristic, and
// that heuristic keeps blanks whose siblings carry real text, which is what
// protects mixed content. Run it on a copy and diff the first time.
$doc->preserveWhiteSpace = false;
$doc->formatOutput = true;
libxml_use_internal_errors(true);
if (!$doc->loadXML($source, LIBXML_NONET)) {
foreach (libxml_get_errors() as $e) {
fprintf(STDERR, "XML error at line %d, column %d: %s\n",
$e->line, $e->column, trim($e->message));
}
libxml_clear_errors();
exit(1);
}
echo $doc->saveXML();# xmllint is part of libxml2 and is almost certainly already installed.
# --nonet stops it fetching a DTD the document references.
xmllint --format --nonet document.xml
# The indent unit comes from an environment variable, not a flag:
XMLLINT_INDENT=' ' xmllint --format --nonet document.xml
# Rewrite in place:
xmllint --format --nonet --output document.xml document.xml
# xmllint refuses to format a document that is not well-formed: it prints the
# first error and exits non-zero, leaving the output file untouched.
# libxml2 will not indent an element that has a text child, which is the same
# mixed-content rule this page applies.共通する型に注目してください。libxml2 も、.NET の XmlWriter も、Python の ET.indent も、文字データを持つ要素のインデントを拒みます。うまくいかないレシピは、先に空白のテキストノードを見境なく取り除くものです。これは正規表現で組み立てたフォーマッターに共通する欠陥でもあります。正規表現には内容モデルが見えないからです。
よくある質問
整形すると XML はアップロードされますか。
いいえ。パーサーもフォーマッターも、このタブの中の Web Worker で動く JavaScript です。送信先となるサーバー側の仕組みも、エディターにアクセスできる解析ツールも、サードパーティのスクリプトもありません。
開発者ツールを開いてネットワークタブに切り替え、文書を整形してみてください。ページ自身のアセットが一度読み込まれ、そのあとは何も起きません。ここでそれが重要なのは、整形が最も必要な文書こそ本番のログから取り出したものだからです。入力は再読み込みで失われないようこのブラウザーの localStorage に保存され、クリアで消せます。
整形するとデータは変わりますか。
データは変わりません。属性値は実体参照も元の引用符も含め、書かれたまま複製されます。CDATA がエスケープ済みテキストに変換されることはありません。コメント、処理命令、内部 DTD サブセットも残り、混在内容の中のテキストはバイト単位でそのままです。
文字が取り除かれる場所が 1 か所だけあります。テキストのみを含む要素の先頭と末尾の空白です。テキストノードの途中にある空白がまとめられることはありません。この除去は xml:space="preserve" を持つ要素にも適用されるので、それに依存しているなら確認してください。
2 個ではなく 4 個のスペースやタブで整形できますか。
できます。インデントの設定にはスペース 2 個、4 個、タブがあり、選んだものは文書全体に適用されます。属性が個別の行に折り返されるときの追加レベルにも同じ設定が使われます。
出力先に合わせて選んでください。.editorconfig のあるリポジトリに置かれるファイルなら、それに合わせます。どこかに埋め込むためサイズが重要なら、タブは 1 レベルあたり 4 バイトではなく 1 バイトです。
整形したのに文書がそのまま返ってきたのはなぜですか。
整形式ではなかったからです。整形には解析が必要なので、入力はそのまま返し、代わりにエラーを行・列・そこに何を書くべきかとともに並べます。
よくある原因は、URL のクエリ文字列にある生のアンパサンド、閉じられていないタグ、開始タグと名前が一致しない終了タグ、断片を連結したことで生じたルート要素 2 つです。xmllint も同じ振る舞いをするので、「xmllint がファイルを整形してくれない」という検索がこれほど多いのです。
CDATA セクションやコメントはどうなりますか。
CDATA セクションはそのまま通ります。区切り記号は残り、その間のバイトはエスケープも除去も再インデントもされません。使っているフォーマッターでは必ず確認してください。CDATA をエスケープ済みテキストに変換するのを既定にしているツールもあり、その場合、返ってくる文書は貼り付けたものとは別物です。
コメントは既定で保持され、削除用のチェックボックスがあります。押す前に一考を。XSLT や Maven、Ant のファイルでは、コメントが誰かの書き残した唯一の説明であることがよくあります。
どのくらいの大きさの文書を整形できますか。
20 MB までです。1 MB の文書はおよそ 200 ミリ秒、5 MB は 1 秒弱で整形され、Web Worker で実行されるのでエディターは固まりません。
この上限があるのは、すべてがこのタブで動いているからです。大きなファイルを渡せるサーバーはなく、20 MB を超えると解析木が数百 MB を占め、ブラウザーは応答しなくなります。それ以上のものには、手元のマシンの xmllint --format が同じ混在内容の規則を適用してくれます。