Formateador XML

Indenta, alinea y ordena. El contenido mixto se conserva exacto.

Entrada
Salida
En esperaPega un documento para comprobarlo. La validación se ejecuta mientras escribes.

Todo se ejecuta en esta pestaña. Nada de lo que pegues se sube, se registra ni se envía a ningún sitio. Abre el panel de red y compruébalo.

Pega un documento arriba y se reindenta mientras escribes. Dos espacios, cuatro espacios o un tabulador, con los atributos empujados a sus propias líneas cuando un elemento lleva tres, cuatro o seis de ellos. El resultado aparece en el panel de solo lectura junto a tu entrada. No se sube nada: el analizador y el formateador son JavaScript ejecutándose en esta pestaña, y tu panel de red sigue vacío mientras trabajas.

Recurres a un formateador cuando el XML lo ha producido otra cosa: una respuesta SOAP sacada de un log como una única línea de 40.000 caracteres, una configuración que reescribió una herramienta de despliegue, un sitemap generado por una máquina. Además es la comprobación de buena formación más rápida que existe, porque un formateador no puede indentar lo que no puede analizar.

Lo que aquí es distinto es el contenido mixto. Cuando un elemento contiene a la vez texto y elementos hijos, el espacio en blanco entre ellos es dato, y un formateador que lo "ordena" ha cambiado el documento. Esos subárboles se reproducen byte a byte. La mayoría de los formateadores online no hacen esto; varios son manipulación de cadenas sobre corchetes angulares en lugar de un analizador.

Qué se le permite cambiar al pretty printing

Solo una cosa en un documento XML es realmente insignificante: el espacio en blanco entre elementos en un modelo de contenido de solo elementos. Un formateador puede borrarlo y generar el suyo. Todo lo demás es contenido, y el enfoque seguro es tratar cada byte como contenido hasta que se demuestre lo contrario.

Así que los nodos de texto compuestos solo de espacio en blanco entre elementos hermanos se descartan y se regeneran a partir de tu ajuste de sangría, y nada más se toca. Los valores de atributo se emiten tal como estaban escritos, porque volver a escapar convertiría & en & y un valor que referencia una entidad declarada en la DTD, como &companyName;, no se puede decodificar sin la DTD. También se conserva el carácter de comilla original, ya que un valor escrito con comillas simples puede contener legalmente una comilla doble.

El texto nunca se reflowea por dentro. Solo se recorta el espacio inicial y final de un elemento que contiene solo texto, así que <price> 42.00 </price> pasa a ser <price>42.00</price> mientras que <note>dos espacios</note> conserva los dos.

El contenido mixto, y por qué rompe casi todos los formateadores

Un elemento tiene contenido mixto cuando entre sus hijos hay texto y marcado a la vez: <p>Hola <b>mundo</b>!</p>. El espacio después de "Hola" es un carácter del documento, y el signo de exclamación tras </b> también. Pon cada hijo en su propia línea indentada y un consumidor que concatene los nodos de texto obtendrá una cadena distinta. Eso no es ordenar, es corromper en silencio.

Cada elemento se comprueba antes de serializarse. Si contiene elementos hijos junto a texto no vacío o una sección CDATA, el subárbol se copia de tu fuente sin aplicar ninguna regla dentro. La contrapartida es deliberada: un subárbol mixto conserva la disposición con la que llegó, aunque sea fea, porque la alternativa es equivocarse.

No es un caso exótico: fragmentos XHTML en una exportación de un CMS, DocBook y DITA, xs:documentation dentro de un esquema, una description de RSS con marcado en línea. Si tu documento no tiene nada de eso, no te cuesta nada. Si lo tiene, es lo único que importa.

  • Mixto, así que se reproduce literalmente: <line>Total: <amount>9,99</amount> sin IVA</line>.
  • No mixto, así que se reindenta con libertad: <order><id>1</id><status>open</status></order>.

Sangría y salto de atributos para SOAP y XSD

Dos espacios es el valor por defecto porque es lo que emite la mayoría del utillaje XML. Cuatro espacios existe porque un montón de bases de código empresariales lo estandarizaron. El tabulador existe porque algunos repositorios lo dicen en su .editorconfig, y porque un tabulador es un byte donde cuatro espacios son cuatro.

El control de atributos es el que importa para SOAP y XSD. La raíz de un envelope SOAP lleva habitualmente cinco declaraciones de espacio de nombres, y una declaración xs:element lleva name, type, minOccurs, maxOccurs, nillable y default: en una sola línea eso son 200 caracteres que nadie va a leer. Pon el umbral en tres, cuatro o seis y cualquier elemento que llegue a ese número recibe un atributo por línea, mientras que los elementos más pequeños se quedan en una sola.

Lo que no va a hacer

Un documento que no está bien formado no se formatea: recuperas tu entrada sin cambios y cada error listado con línea, columna y solución. Merece la pena decir claramente otros dos límites. A xml:space="preserve" no se le da un trato especial, así que a un elemento de solo texto que lo lleve se le sigue recortando el espacio inicial y final. Y el texto entremezclado con comentarios, como en <a>texto<!-- por qué -->más</a>, no es contenido mixto según esta comprobación porque un comentario no es un elemento, así que se reindenta y el texto gana espacio en blanco. Los dos son casos estrechos, los dos son reales, y ninguno es algo que una herramienta que lo hiciera en silencio te contaría.

El formateo es idempotente: pasar la salida otra vez con los mismos ajustes produce los mismos bytes, así que es seguro en un hook de pre-commit. Un documento de 1 MB tarda unos 200 milisegundos y 5 MB algo menos de un segundo. El techo son 20 MB, un límite de memoria más que una política.

Formatear XML desde código

La misma operación en los lenguajes que realmente procesan XML. Cada ejemplo analiza de forma segura, porque los valores por defecto de Java, PHP y la biblioteca estándar de Python resuelven entidades externas, y los comentarios señalan dónde cada biblioteca reordena el contenido mixto.

// 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.

Fíjate en el patrón: libxml2, el XmlWriter de .NET y el ET.indent de Python se niegan todos a indentar un elemento que contiene datos de carácter. Las recetas que salen mal son las que primero eliminan los nodos de texto en blanco de forma indiscriminada, que es también el defecto de cualquier formateador construido sobre una expresión regular, ya que una expresión regular no puede ver un modelo de contenido.

Preguntas frecuentes

¿Se sube mi XML cuando lo formateo?

No. El analizador y el formateador son JavaScript ejecutándose dentro de esta pestaña, en un Web Worker. No hay ningún componente de servidor al que enviar nada, ni analítica con acceso al editor, ni scripts de terceros.

Abre tus herramientas de desarrollo, ve a la pestaña Red y formatea un documento: la página carga sus propios recursos una vez y después nada más. Aquí eso importa porque los documentos que más necesitan reformatearse son justo los que se sacan de logs de producción. Tu entrada se guarda en el localStorage de este navegador para que un refresco no la pierda, y Limpiar la elimina.

¿El formateo cambia mis datos?

Los datos no. Los valores de atributo se copian tal cual, incluidas las referencias a entidades y el carácter de comilla original. El CDATA nunca se convierte en texto escapado. Los comentarios, las instrucciones de procesamiento y el subconjunto interno de la DTD sobreviven, y el texto dentro de contenido mixto se reproduce byte a byte.

Hay un sitio donde sí se quitan caracteres: el espacio inicial y final de un elemento que contiene solo texto. El espacio en medio de un nodo de texto nunca se colapsa. Ese recorte también se aplica a un elemento que lleve xml:space="preserve", así que revísalos si dependes de ellos.

¿Puedo formatear con 4 espacios o tabuladores en vez de 2?

Sí. El control de sangría ofrece dos espacios, cuatro espacios y tabulador, y la elección se aplica a todo el documento, incluido el nivel extra que se usa cuando los atributos saltan a sus propias líneas.

Elige según el destino: si el archivo vive en un repositorio con .editorconfig, ajústate a eso. Si el tamaño importa porque el documento se va a incrustar en algún sitio, un tabulador es un byte por nivel en vez de cuatro.

¿Por qué el formateador me ha devuelto el documento sin cambios?

Porque no estaba bien formado. Formatear requiere analizar, así que la entrada se devuelve intacta y en su lugar se listan los errores, con la línea, la columna y qué escribir ahí.

Los sospechosos habituales son un ampersand suelto en la cadena de consulta de una URL, una etiqueta sin cerrar, una etiqueta de cierre cuyo nombre no coincide con el de apertura, y dos elementos raíz por concatenar fragmentos. xmllint se comporta igual, y por eso "xmllint no me formatea el archivo" es una búsqueda tan común.

¿Qué pasa con las secciones CDATA y los comentarios?

Las secciones CDATA pasan intactas: los delimitadores se quedan y los bytes de en medio no se escapan, ni se recortan, ni se reindentan. Comprueba esto en cualquier formateador que uses, porque convertir CDATA en texto escapado es una opción que algunas herramientas toman por defecto, y entonces el documento que recuperas no es el que pegaste.

Los comentarios se conservan por defecto, con una casilla para quitarlos. Piénsalo antes de marcarla: en archivos de XSLT, Maven y Ant suelen ser la única explicación que alguien dejó escrita.

¿Qué tamaño de documento puede formatear?

Hasta 20 MB. Un documento de 1 MB se formatea en unos 200 milisegundos y 5 MB en algo menos de un segundo, en un Web Worker para que el editor no se congele.

El límite existe porque todo corre en esta pestaña: no hay servidor al que pasarle un archivo grande, y pasados los 20 MB el árbol de análisis puede ocupar varios cientos de megabytes y el navegador deja de responder. Para algo más grande, xmllint --format en tu propia máquina aplica la misma regla de contenido mixto.

Herramientas relacionadas

Lecturas de referencia

Errores que esto resuelve