Formattatore XML
Indenta, allinea e ordina. Il contenuto misto resta intatto.
Tutto viene eseguito in questa scheda. Nulla di ciò che incolli viene caricato, registrato o inviato da qualche parte. Apri il pannello di rete e verifica.
Incolla un documento qui sopra e viene re-indentato mentre scrivi. Due spazi, quattro spazi o una tabulazione, con gli attributi spinti su righe proprie quando un elemento ne porta tre, quattro o sei. Il risultato compare nel riquadro di sola lettura accanto al tuo input. Nulla viene caricato: il parser e il formattatore sono JavaScript che gira in questa scheda, e il tuo pannello di rete resta vuoto mentre lavori.
Si ricorre a un formattatore quando l’XML l’ha prodotto qualcos’altro: una risposta SOAP estratta da un log come un’unica riga di 40.000 caratteri, una configurazione riscritta da uno strumento di deploy, una sitemap generata da una macchina. È anche il controllo di buona formazione più rapido che esista, perché un formattatore non può indentare ciò che non riesce ad analizzare.
Qui la differenza è il contenuto misto. Quando un elemento contiene sia testo sia elementi figli, lo spazio bianco tra loro è un dato, e un formattatore che lo mette in ordine ha modificato il documento. Quei sottoalberi vengono riprodotti byte per byte. La maggior parte dei formattatori online non lo fa; parecchi sono manipolazione di stringhe sulle parentesi angolari invece che un parser.
Che cosa può cambiare la formattazione
Una sola cosa in un documento XML è davvero insignificante: lo spazio bianco fra elementi in un modello di contenuto fatto di soli elementi. Un formattatore può cancellarlo e generarne di proprio. Tutto il resto è contenuto, e l’approccio sicuro è trattare ogni byte come contenuto finché non si dimostra il contrario.
Perciò i nodi di testo fatti di soli spazi fra elementi fratelli vengono scartati e rigenerati dalla tua impostazione di indentazione, e nient’altro viene toccato. I valori degli attributi sono emessi così come sono scritti, perché rifare l’escape trasformerebbe & in &, e un valore che richiama un’entità dichiarata nella DTD come &companyName; non può essere risolto senza la DTD. Anche il carattere di virgoletta originale resta, dato che un valore scritto fra apici singoli può legittimamente contenere una virgoletta doppia.
Il testo non viene mai riflusso al suo interno. Solo lo spazio iniziale e finale di un elemento che contiene esclusivamente testo viene tolto, così <price> 42.00 </price> diventa <price>42.00</price> mentre <note>due spazi</note> li mantiene entrambi.
Il contenuto misto, e perché rompe quasi tutti i formattatori
Un elemento ha contenuto misto quando fra i suoi figli ci sono sia testo sia markup: <p>Ciao <b>mondo</b>!</p>. Lo spazio dopo "Ciao" è un carattere del documento, e il punto esclamativo dopo </b> pure. Metti ogni figlio su una riga indentata a sé e chi concatena i nodi di testo ottiene una stringa diversa. Non è mettere in ordine, è corrompere in silenzio.
Ogni elemento viene controllato prima della serializzazione. Se contiene elementi figli accanto a testo non vuoto o a una sezione CDATA, il sottoalbero viene copiato dal tuo sorgente senza applicare al suo interno alcuna regola. Il compromesso è voluto: un sottoalbero misto conserva la disposizione con cui è arrivato, anche se brutta, perché l’alternativa è sbagliare.
Non è un caso esotico: frammenti XHTML in un export da CMS, DocBook e DITA, xs:documentation dentro uno schema, una description RSS con markup in linea. Se il tuo documento non ne contiene, non ti costa nulla. Se ne contiene, è tutto ciò che conta.
- Misto, quindi riprodotto alla lettera: <line>Totale: <amount>9,99</amount> IVA esclusa</line>.
- Non misto, quindi re-indentato liberamente: <order><id>1</id><status>open</status></order>.
Indentazione e a capo degli attributi per SOAP e XSD
Due spazi è il valore predefinito perché è ciò che emette la maggior parte degli strumenti XML. Quattro spazi esiste perché parecchie codebase aziendali si sono standardizzate su quello. La tabulazione esiste perché alcuni repository lo impongono nel loro .editorconfig, e perché una tabulazione è un byte dove quattro spazi sono quattro.
Il controllo degli attributi è quello che conta per SOAP e XSD. La radice di una envelope SOAP porta abitualmente cinque dichiarazioni di namespace, e una dichiarazione xs:element porta name, type, minOccurs, maxOccurs, nillable e default: su una riga sola sono 200 caratteri che nessuno leggerà. Metti la soglia a tre, quattro o sei e ogni elemento che la raggiunge riceve un attributo per riga, mentre gli elementi più piccoli restano su una riga.
Che cosa non farà
Un documento che non è ben formato non viene formattato: ti torna indietro l’input invariato e ogni errore elencato con riga, colonna e rimedio. Vale la pena dire chiaramente altri due limiti. xml:space="preserve" non riceve alcun trattamento speciale, quindi a un elemento di solo testo che lo porta viene comunque tolto lo spazio iniziale e finale. E il testo intervallato da commenti, come in <a>testo<!-- perché -->altro</a>, per questo test non è contenuto misto, perché un commento non è un elemento: viene quindi re-indentato e il testo guadagna spazi. Sono due casi stretti, sono entrambi reali, e nessuno dei due è qualcosa che uno strumento che lo facesse in silenzio ti direbbe.
La formattazione è idempotente: ripassare l’output con le stesse impostazioni produce gli stessi byte, quindi è sicura in un hook di pre-commit. Un documento da 1 MB impiega circa 200 millisecondi e 5 MB poco meno di un secondo. Il tetto è 20 MB, un limite di memoria più che una scelta di policy.
Formattare XML da codice
La stessa operazione nei linguaggi che l’XML lo elaborano davvero. Ogni esempio analizza in modo sicuro, perché le impostazioni predefinite di Java, PHP e della libreria standard di Python risolvono le entità esterne, e i commenti segnano dove ciascuna libreria rimaneggia il contenuto misto.
// 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.Nota lo schema: libxml2, l’XmlWriter di .NET e l’ET.indent di Python si rifiutano tutti di indentare un elemento che contiene dati carattere. Le ricette che vanno storte sono quelle che prima eliminano indiscriminatamente i nodi di testo bianchi, che è anche il difetto di qualunque formattatore costruito su un’espressione regolare, dato che un’espressione regolare non vede un modello di contenuto.
Domande frequenti
Il mio XML viene caricato quando lo formatto?
No. Il parser e il formattatore sono JavaScript eseguito dentro questa scheda, in un Web Worker. Non c’è alcun componente lato server a cui mandare qualcosa, niente analytics con accesso all’editor e nessuno script di terze parti.
Apri gli strumenti per sviluppatori, passa alla scheda Rete e formatta un documento: la pagina carica le proprie risorse una volta e poi più nulla. Qui conta, perché i documenti che più hanno bisogno di essere riformattati sono quelli tirati fuori dai log di produzione. Quello che scrivi resta nel localStorage di questo browser, così un ricaricamento non lo perde, e Svuota lo rimuove.
La formattazione cambia i miei dati?
I dati no. I valori degli attributi sono copiati così come scritti, riferimenti a entità e virgoletta originale inclusi. Il CDATA non viene mai convertito in testo con escape. Commenti, istruzioni di elaborazione e sottoinsieme DTD interno sopravvivono, e il testo dentro il contenuto misto è riprodotto byte per byte.
C’è un punto in cui dei caratteri vengono rimossi: lo spazio iniziale e finale di un elemento che contiene solo testo. Lo spazio in mezzo a un nodo di testo non viene mai compresso. Quel taglio vale anche per un elemento con xml:space="preserve", quindi controllali se ci fai affidamento.
Posso formattare con 4 spazi o con le tabulazioni invece di 2?
Sì. Il controllo dell’indentazione offre due spazi, quattro spazi e tabulazione, e la scelta vale per tutto il documento, compreso il livello aggiuntivo usato quando gli attributi vanno a capo su righe proprie.
Scegli in base alla destinazione: se il file vive in un repository con un .editorconfig, allineati a quello. Se la dimensione conta perché il documento va incorporato da qualche parte, una tabulazione è un byte per livello invece di quattro.
Perché il formattatore mi ha restituito il documento invariato?
Perché non era ben formato. Formattare richiede un’analisi, quindi l’input viene restituito intatto e al suo posto vengono elencati gli errori, con riga, colonna e che cosa scriverci.
I soliti sospetti sono una e commerciale nuda in una query string, un tag non chiuso, un tag di chiusura il cui nome non corrisponde a quello di apertura, e due elementi radice nati da frammenti concatenati. xmllint si comporta allo stesso modo, ed è per questo che «xmllint non mi formatta il file» è una ricerca così frequente.
Che fine fanno le sezioni CDATA e i commenti?
Le sezioni CDATA passano intatte: i delimitatori restano e i byte in mezzo non vengono né sottoposti a escape, né tagliati, né re-indentati. Verificalo su qualunque formattatore usi, perché convertire il CDATA in testo con escape è un’opzione che alcuni strumenti prendono per impostazione predefinita, e il documento che riottieni non è più quello che avevi incollato.
I commenti sono mantenuti per impostazione predefinita, con una casella per rimuoverli. Pensaci prima di spuntarla: nei file XSLT, Maven e Ant sono spesso l’unica spiegazione che qualcuno abbia messo per iscritto.
Quanto può essere grande il documento da formattare?
Fino a 20 MB. Un documento da 1 MB si formatta in circa 200 millisecondi e 5 MB in poco meno di un secondo, in un Web Worker così l’editor non si blocca.
Il limite esiste perché tutto gira in questa scheda: non c’è un server a cui passare un file grande, e oltre i 20 MB l’albero di parsing può occupare diverse centinaia di megabyte e il browser smette di rispondere. Per qualcosa di più grande, xmllint --format sulla tua macchina applica la stessa regola sul contenuto misto.