Formatador XML

Identa, alinha e organiza. O conteúdo misto fica intacto.

Entrada
Saída
AguardandoCole um documento para verificá-lo. A validação roda enquanto você digita.

Tudo roda nesta aba. Nada do que você colar é enviado, registrado ou transmitido para lugar nenhum. Abra o painel de rede e confira.

Cole um documento acima e ele é reindentado enquanto você digita. Dois espaços, quatro espaços ou uma tabulação, com os atributos empurrados para linhas próprias assim que um elemento carrega três, quatro ou seis deles. O resultado aparece no painel somente leitura ao lado da sua entrada. Nada é enviado: o analisador e o formatador são JavaScript rodando nesta aba, e o seu painel de rede continua vazio enquanto você trabalha.

Você recorre a um formatador quando o XML foi produzido por outra coisa: uma resposta SOAP tirada de um log como uma única linha de 40.000 caracteres, uma configuração reescrita por uma ferramenta de deploy, um sitemap gerado por máquina. É também a verificação de boa formação mais rápida que existe, porque um formatador não consegue indentar o que não consegue analisar.

O que é diferente aqui é o conteúdo misto. Quando um elemento contém texto e elementos filhos ao mesmo tempo, o espaço em branco entre eles é dado, e um formatador que "arruma" isso alterou o documento. Essas subárvores são reproduzidas byte a byte. A maioria dos formatadores online não faz isso; vários são manipulação de string sobre sinais de menor e maior, e não um analisador.

O que a formatação tem permissão para mudar

Só uma coisa num documento XML é genuinamente insignificante: o espaço em branco entre elementos num modelo de conteúdo formado apenas por elementos. Um formatador pode apagá-lo e gerar o seu próprio. Todo o resto é conteúdo, e a abordagem segura é tratar cada byte como conteúdo até que se prove o contrário.

Assim, os nós de texto compostos só de espaço em branco entre elementos irmãos são descartados e regenerados a partir da sua configuração de indentação, e nada mais é tocado. Os valores de atributo são emitidos como foram escritos, porque re-escapar transformaria & em &, e um valor que referencia uma entidade declarada na DTD, como &companyName;, não pode ser decodificado sem a DTD. O caractere de aspas original também é mantido, já que um valor escrito com aspas simples pode legalmente conter aspas duplas.

O texto nunca é requebrado internamente. Só o espaço em branco no começo e no fim de um elemento que contém apenas texto é removido, então <price> 42.00 </price> vira <price>42.00</price>, enquanto <note>dois espaços</note> mantém os dois.

Conteúdo misto, e por que ele quebra a maioria dos formatadores

Um elemento tem conteúdo misto quando seus filhos incluem texto e marcação ao mesmo tempo: <p>Olá <b>mundo</b>!</p>. O espaço depois de "Olá" é um caractere do documento, e o ponto de exclamação depois de </b> também. Coloque cada filho em sua própria linha indentada e um consumidor que concatena os nós de texto recebe uma string diferente. Isso não é arrumar, é corromper em silêncio.

Cada elemento é verificado antes de ser serializado. Se ele tiver elementos filhos ao lado de texto não vazio ou de uma seção CDATA, a subárvore é copiada da sua fonte sem nenhuma regra aplicada lá dentro. A troca é deliberada: uma subárvore mista mantém o layout com que chegou, mesmo feio, porque a alternativa é estar errado.

Não é um caso exótico: fragmentos XHTML numa exportação de CMS, DocBook e DITA, xs:documentation dentro de um esquema, uma description de RSS com marcação embutida. Se o seu documento não tem nada disso, não custa nada. Se tem, é o jogo inteiro.

  • Misto, então reproduzido literalmente: <line>Total: <amount>9,99</amount> sem impostos</line>.
  • Não misto, então reindentado livremente: <order><id>1</id><status>open</status></order>.

Indentação e quebra de atributos para SOAP e XSD

Dois espaços é o padrão porque é o que a maioria das ferramentas XML emite. Quatro espaços existe porque muitas bases de código corporativas padronizaram nisso. A tabulação existe porque alguns repositórios determinam isso no .editorconfig, e porque uma tabulação é um byte onde quatro espaços são quatro.

O controle de atributos é o que importa para SOAP e XSD. A raiz de um envelope SOAP costuma carregar cinco declarações de namespace, e uma declaração xs:element carrega name, type, minOccurs, maxOccurs, nillable e default: em uma linha isso dá 200 caracteres que ninguém vai ler. Coloque o limiar em três, quatro ou seis e qualquer elemento que chegue lá recebe um atributo por linha, enquanto elementos menores continuam em uma linha só.

O que ele não vai fazer

Um documento que não está bem formado não é formatado: você recebe sua entrada de volta sem mudanças e cada erro listado com linha, coluna e correção. Vale dizer com clareza mais dois limites. xml:space="preserve" não recebe tratamento especial, então um elemento só de texto que o carregue ainda tem o espaço do começo e do fim removido. E texto intercalado com comentários, como em <a>texto<!-- por quê -->mais</a>, não é conteúdo misto por este teste, porque um comentário não é um elemento: ele é reindentado e o texto ganha espaço em branco. Os dois são casos estreitos, os dois são reais, e nenhum é algo que uma ferramenta que fizesse isso caladamente contaria a você.

A formatação é idempotente: passar a saída de novo com as mesmas opções produz os mesmos bytes, então é seguro num hook de pre-commit. Um documento de 1 MB leva cerca de 200 milissegundos e 5 MB pouco menos de um segundo. O teto é 20 MB, um limite de memória e não uma política.

Formatando XML em código

A mesma operação nas linguagens que realmente processam XML. Cada exemplo faz o parsing de forma segura, porque os padrões de Java, PHP e da biblioteca padrão do Python resolvem entidades externas, e os comentários marcam onde cada biblioteca reorganiza o conteúdo 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.

Repare no padrão: libxml2, o XmlWriter do .NET e o ET.indent do Python todos se recusam a indentar um elemento que contém dados de caractere. As receitas que dão errado são as que primeiro removem os nós de texto em branco indiscriminadamente, que é também a falha de qualquer formatador construído sobre expressão regular, já que uma expressão regular não enxerga um modelo de conteúdo.

Perguntas frequentes

Meu XML é enviado quando eu formato?

Não. O analisador e o formatador são JavaScript rodando dentro desta aba, num Web Worker. Não há componente de servidor para o qual mandar nada, nem analytics com acesso ao editor, nem scripts de terceiros.

Abra as ferramentas de desenvolvedor, vá para a aba Rede e formate um documento: a página carrega os próprios recursos uma vez e depois mais nada. Isso importa aqui porque os documentos que mais precisam de reformatação são justamente os tirados de logs de produção. Sua entrada fica no localStorage deste navegador para que um recarregamento não a perca, e Limpar a remove.

A formatação muda meus dados?

Os dados não. Os valores de atributo são copiados como foram escritos, incluindo referências de entidade e o caractere de aspas original. CDATA nunca é convertido em texto escapado. Comentários, instruções de processamento e o subconjunto interno da DTD sobrevivem, e o texto dentro de conteúdo misto é reproduzido byte a byte.

Há um lugar em que caracteres são removidos: o espaço em branco no começo e no fim de um elemento que contém apenas texto. Espaço no meio de um nó de texto nunca é colapsado. Esse corte também se aplica a um elemento com xml:space="preserve", então verifique esses se você depende deles.

Posso formatar com 4 espaços ou tabulações em vez de 2?

Pode. O controle de indentação oferece dois espaços, quatro espaços e tabulação, e a escolha vale para o documento inteiro, incluindo o nível extra usado quando os atributos quebram para linhas próprias.

Escolha conforme o destino: se o arquivo vive num repositório com .editorconfig, siga aquilo. Se o tamanho importa porque o documento vai ser embutido em algum lugar, uma tabulação é um byte por nível em vez de quatro.

Por que o formatador devolveu meu documento sem mudanças?

Porque ele não estava bem formado. Formatar exige um parsing, então a entrada volta intacta e os erros são listados no lugar, com a linha, a coluna e o que escrever ali.

Os suspeitos de sempre são um & solto numa query string de URL, uma tag não fechada, uma tag de fechamento cujo nome não bate com o de abertura, e dois elementos raiz vindos de fragmentos concatenados. O xmllint se comporta do mesmo jeito, e é por isso que "xmllint não formata meu arquivo" é uma busca tão comum.

O que acontece com seções CDATA e comentários?

Seções CDATA passam intactas: os delimitadores ficam e os bytes entre eles não são escapados, cortados nem reindentados. Confira isso em qualquer formatador que você use, porque converter CDATA em texto escapado é uma opção que algumas ferramentas tomam por padrão, e aí o documento que volta não é o que você colou.

Comentários são mantidos por padrão, com uma caixa para removê-los. Pense antes de marcar: em arquivos de XSLT, Maven e Ant eles costumam ser a única explicação que alguém deixou escrita.

Que tamanho de documento ele consegue formatar?

Até 20 MB. Um documento de 1 MB é formatado em cerca de 200 milissegundos e 5 MB em pouco menos de um segundo, num Web Worker para o editor não congelar.

O limite existe porque tudo roda nesta aba: não há servidor para receber um arquivo grande, e passando de 20 MB a árvore de análise pode ocupar várias centenas de megabytes e o navegador para de responder. Para algo maior, o xmllint --format na sua própria máquina aplica a mesma regra de conteúdo misto.

Ferramentas relacionadas

Leitura complementar

Erros que isto resolve