XML-Formatierer
Einrücken, ausrichten, aufräumen. Gemischter Inhalt bleibt exakt.
Alles läuft in diesem Tab. Nichts, was Sie einfügen, wird hochgeladen, protokolliert oder irgendwohin gesendet. Öffnen Sie Ihr Netzwerkpanel und prüfen Sie es.
Fügen Sie oben ein Dokument ein, und es wird während der Eingabe neu eingerückt. Zwei Leerzeichen, vier Leerzeichen oder ein Tabulator, wobei Attribute auf eigene Zeilen wandern, sobald ein Element drei, vier oder sechs davon trägt. Das Ergebnis erscheint im schreibgeschützten Bereich neben Ihrer Eingabe. Nichts wird hochgeladen: Parser und Formatierer sind JavaScript in diesem Tab, und Ihr Netzwerkpanel bleibt leer, während Sie arbeiten.
Man greift zum Formatierer, wenn das XML von etwas anderem erzeugt wurde: eine SOAP-Antwort, die als eine einzige Zeile mit 40.000 Zeichen aus einem Log kommt, eine Konfiguration, die ein Deployment-Werkzeug umgeschrieben hat, eine maschinell erzeugte Sitemap. Es ist zugleich die schnellste Wohlgeformtheitsprüfung überhaupt, denn ein Formatierer kann nicht einrücken, was er nicht parsen kann.
Anders ist hier der Umgang mit gemischtem Inhalt. Wenn ein Element sowohl Text als auch Kindelemente enthält, ist der Leerraum dazwischen Teil der Daten, und ein Formatierer, der ihn aufräumt, hat das Dokument verändert. Solche Teilbäume werden Byte für Byte übernommen. Die meisten Online-Formatierer tun das nicht; einige sind Stringmanipulation über spitzen Klammern statt eines Parsers.
Was Pretty Printing ändern darf
Nur eines ist in einem XML-Dokument wirklich unbedeutend: der Leerraum zwischen Elementen in einem Inhaltsmodell, das nur Elemente zulässt. Den darf ein Formatierer löschen und selbst neu erzeugen. Alles andere ist Inhalt, und der sichere Ansatz ist, jedes Byte als Inhalt zu behandeln, bis das Gegenteil bewiesen ist.
Deshalb werden reine Leerraum-Textknoten zwischen Geschwisterelementen verworfen und aus Ihrer Einrückungseinstellung neu erzeugt, und sonst wird nichts angefasst. Attributwerte werden ausgegeben, wie sie geschrieben sind, denn erneutes Maskieren würde & zu & machen, und ein Wert, der eine in der DTD deklarierte Entity wie &companyName; referenziert, lässt sich ohne die DTD nicht auflösen. Auch das ursprüngliche Anführungszeichen bleibt, weil ein in einfachen Anführungszeichen geschriebener Wert legal ein doppeltes enthalten darf.
Text wird nie intern umgebrochen. Nur der führende und abschließende Leerraum eines Elements, das ausschließlich Text enthält, wird entfernt: <price> 42.00 </price> wird zu <price>42.00</price>, während <note>zwei Leerzeichen</note> beide behält.
Gemischter Inhalt, und warum er die meisten Formatierer bricht
Ein Element hat gemischten Inhalt, wenn seine Kinder Text und Markup zugleich umfassen: <p>Hallo <b>Welt</b>!</p>. Das Leerzeichen nach „Hallo“ ist ein Zeichen im Dokument, und das Ausrufezeichen nach </b> ebenso. Setzt man jedes Kind auf eine eigene eingerückte Zeile, bekommt ein Konsument, der die Textknoten aneinanderhängt, eine andere Zeichenkette. Das ist kein Aufräumen, das ist stille Beschädigung.
Jedes Element wird vor dem Serialisieren geprüft. Enthält es Kindelemente neben nicht leerem Text oder einem CDATA-Abschnitt, wird der Teilbaum unverändert aus Ihrer Quelle kopiert, ohne dass darin eine Regel greift. Der Kompromiss ist gewollt: Ein gemischter Teilbaum behält das Layout, mit dem er kam, auch ein hässliches – die Alternative wäre, falsch zu liegen.
Das ist kein exotischer Fall: XHTML-Fragmente in einem CMS-Export, DocBook und DITA, xs:documentation innerhalb eines Schemas, eine RSS-description mit Inline-Markup. Hat Ihr Dokument nichts davon, kostet es Sie nichts. Hat es das, ist es der ganze Punkt.
- Gemischt, also wortgetreu übernommen: <line>Summe: <amount>9,99</amount> zzgl. MwSt.</line>.
- Nicht gemischt, also frei neu eingerückt: <order><id>1</id><status>open</status></order>.
Einrückung und Attributumbruch für SOAP und XSD
Zwei Leerzeichen sind der Standard, weil das meiste XML-Werkzeug genau das ausgibt. Vier Leerzeichen gibt es, weil sich viele Unternehmens-Codebasen darauf festgelegt haben. Den Tabulator gibt es, weil manche Repositories das in ihrer .editorconfig so verlangen, und weil ein Tabulator ein Byte ist, wo vier Leerzeichen vier sind.
Die Attributeinstellung ist die, auf die es bei SOAP und XSD ankommt. Die Wurzel einer SOAP-Envelope trägt routinemäßig fünf Namensraumdeklarationen, und eine xs:element-Deklaration trägt name, type, minOccurs, maxOccurs, nillable und default: in einer Zeile sind das 200 Zeichen, die niemand liest. Setzen Sie die Schwelle auf drei, vier oder sechs, und jedes Element ab diesem Wert bekommt ein Attribut pro Zeile, während kleinere Elemente einzeilig bleiben.
Was es nicht tun wird
Ein Dokument, das nicht wohlgeformt ist, wird nicht formatiert: Sie bekommen Ihre Eingabe unverändert zurück und jeden Fehler mit Zeile, Spalte und Lösung aufgelistet. Zwei weitere Grenzen gehören klar gesagt. xml:space="preserve" wird nicht besonders behandelt, einem reinen Textelement damit wird der führende und abschließende Leerraum trotzdem entfernt. Und Text, der mit Kommentaren durchsetzt ist, wie in <a>Text<!-- warum -->mehr</a>, ist nach dieser Prüfung kein gemischter Inhalt, weil ein Kommentar kein Element ist: Er wird also neu eingerückt und der Text bekommt Leerraum dazu. Beides sind enge Fälle, beides ist real, und beides würde Ihnen ein Werkzeug, das es still täte, nicht sagen.
Das Formatieren ist idempotent: Die Ausgabe mit denselben Einstellungen erneut hindurchzuschicken liefert dieselben Bytes, es ist also in einem Pre-Commit-Hook unbedenklich. Ein 1-MB-Dokument dauert etwa 200 Millisekunden, 5 MB knapp unter einer Sekunde. Die Obergrenze liegt bei 20 MB – eine Speichergrenze, keine Richtlinie.
XML in Code formatieren
Dieselbe Operation in den Sprachen, die tatsächlich XML verarbeiten. Jedes Beispiel parst sicher, weil die Standardeinstellungen in Java, PHP und Pythons Standardbibliothek externe Entities auflösen, und die Kommentare markieren, wo die jeweilige Bibliothek gemischten Inhalt umbricht.
// 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.Achten Sie auf das Muster: libxml2, der XmlWriter von .NET und Pythons ET.indent weigern sich alle, ein Element mit Zeichendaten einzurücken. Schiefgehen die Rezepte, die zuerst wahllos Leerraum-Textknoten entfernen – derselbe Fehler steckt in jedem Formatierer, der auf einem regulären Ausdruck aufbaut, denn ein regulärer Ausdruck sieht kein Inhaltsmodell.
Häufige Fragen
Wird mein XML beim Formatieren hochgeladen?
Nein. Parser und Formatierer sind JavaScript, das in diesem Tab in einem Web Worker läuft. Es gibt keine serverseitige Komponente, an die sich etwas senden ließe, keine Analytics mit Zugriff auf den Editor und keine Skripte Dritter.
Öffnen Sie die Entwicklerwerkzeuge, wechseln Sie auf den Netzwerk-Tab und formatieren Sie ein Dokument: Die Seite lädt ihre eigenen Assets einmal und danach nichts mehr. Das zählt hier, weil gerade die Dokumente, die am dringendsten neu formatiert werden müssen, aus Produktionslogs stammen. Ihre Eingabe bleibt im localStorage dieses Browsers, damit ein Neuladen sie nicht verliert; „Leeren“ entfernt sie.
Verändert das Formatieren meine Daten?
Die Daten nicht. Attributwerte werden übernommen, wie sie geschrieben sind, samt Entity-Referenzen und ursprünglichem Anführungszeichen. CDATA wird nie in maskierten Text umgewandelt. Kommentare, Verarbeitungsanweisungen und die interne DTD-Teilmenge überleben, und Text innerhalb gemischten Inhalts wird Byte für Byte reproduziert.
An einer Stelle werden Zeichen entfernt: beim führenden und abschließenden Leerraum eines Elements, das nur Text enthält. Leerraum mitten in einem Textknoten wird nie zusammengefasst. Dieses Kürzen trifft auch ein Element mit xml:space="preserve" – prüfen Sie diese, wenn Sie sich darauf verlassen.
Kann ich mit 4 Leerzeichen oder Tabulatoren statt 2 formatieren?
Ja. Die Einrückungsauswahl bietet zwei Leerzeichen, vier Leerzeichen und Tabulator, und die Wahl gilt für das ganze Dokument, auch für die zusätzliche Ebene, wenn Attribute auf eigene Zeilen umbrechen.
Richten Sie sich nach dem Ziel: Liegt die Datei in einem Repository mit .editorconfig, folgen Sie dem. Zählt die Größe, weil das Dokument irgendwo eingebettet wird, ist ein Tabulator ein Byte pro Ebene statt vier.
Warum kam mein Dokument unverändert zurück?
Weil es nicht wohlgeformt war. Formatieren setzt Parsen voraus, also wird die Eingabe unangetastet zurückgegeben und stattdessen werden die Fehler aufgelistet, mit Zeile, Spalte und dem, was dort stehen sollte.
Die üblichen Verdächtigen: ein rohes Und-Zeichen im Querystring einer URL, ein nicht geschlossenes Tag, ein schließendes Tag, dessen Name nicht zum öffnenden passt, und zwei Wurzelelemente aus aneinandergehängten Fragmenten. xmllint verhält sich genauso, weshalb „xmllint formatiert meine Datei nicht“ eine so häufige Suche ist.
Was passiert mit CDATA-Abschnitten und Kommentaren?
CDATA-Abschnitte gehen unangetastet durch: Die Begrenzer bleiben, und die Bytes dazwischen werden weder maskiert noch gekürzt noch neu eingerückt. Prüfen Sie das bei jedem Formatierer, den Sie benutzen, denn CDATA in maskierten Text umzuwandeln ist eine Option, die manche Werkzeuge standardmäßig wählen – das zurückgegebene Dokument ist dann nicht mehr das eingefügte.
Kommentare bleiben standardmäßig erhalten, mit einer Checkbox zum Entfernen. Überlegen Sie, bevor Sie sie setzen: In XSLT-, Maven- und Ant-Dateien sind sie häufig die einzige Erklärung, die jemand aufgeschrieben hat.
Wie große Dokumente kann es formatieren?
Bis 20 MB. Ein 1-MB-Dokument wird in rund 200 Millisekunden formatiert, 5 MB in knapp unter einer Sekunde, und zwar in einem Web Worker, damit der Editor nicht einfriert.
Die Grenze existiert, weil alles in diesem Tab läuft: Es gibt keinen Server, dem sich eine große Datei übergeben ließe, und jenseits von 20 MB kann der Parse-Baum mehrere hundert Megabyte belegen und der Browser reagiert nicht mehr. Für alles Größere wendet xmllint --format auf Ihrem eigenen Rechner dieselbe Regel für gemischten Inhalt an.