Transformador XSLT
Aplica una hoja de estilos. Con respaldo en WebAssembly.
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 en el panel izquierdo y una hoja de estilos en el derecho, y la transformación se ejecuta en esta pestaña. El resultado vuelve como texto, con el motor que lo produjo y el tiempo que tardó encima de la salida. Ambas entradas se comprueban primero en cuanto a buena formación, así que un ángulo que falte en la hoja de estilos se informa como tal y no como un fallo de compilación.
Recurres a ella cuando una hoja de estilos no hace lo que esperas y quieres un ciclo rápido: una transformación heredada de alguien que se fue, un patrón de coincidencia que no sabes si dispara, una plantilla que produce un archivo vacío con una entrada y no con otra.
La herramienta tiene la forma que tiene porque el XSLT de los navegadores se está retirando con un calendario publicado. Usa el procesador nativo mientras haya uno y recurre a una compilación WebAssembly de libxslt cuando no lo haya, y dice qué motor se ejecutó.
Chrome está retirando XSLT, y las fechas están publicadas
Google presentó una intención de obsoletar y eliminar en 2025. El razonamiento: la instrucción de procesamiento xml-stylesheet aparece en aproximadamente una carga de página de cada ocho mil, mientras que libxslt arrastra un largo historial de seguridad de memoria y perdió a su mantenedor de siempre en junio de 2025. Mozilla y Apple han manifestado su apoyo. El calendario tiene fechas:
Firefox y Safari no han anunciado eliminación propia, así que XSLTProcessor sigue funcionando ahí por ahora. Firefox usa su propio procesador TransforMiiX en vez de libxslt, y por eso un puñado de hojas de estilos ya se comportan distinto entre navegadores.
Esta página sondea si hay un procesador nativo que funcione ejecutando una transformación de una línea en lugar de comprobar si existe el constructor: uno que existe y falla en silencio es peor que uno ausente. Si el sondeo falla, carga una compilación WebAssembly de libxslt, el polyfill al que apunta la propia guía de migración de Chrome.
- Chrome 143, 2 de diciembre de 2025: obsolescencia oficial, avisos en consola y en Lighthouse.
- Chrome 145, diciembre de 2025: XSLT desactivado por defecto en Canary, Dev y Beta.
- Chrome 146, 10 de marzo de 2026: vía de escape por política empresarial.
- Chrome 152, 25 de agosto de 2026: se abre la prueba de origen.
- Chrome 158, 17 de noviembre de 2026: XSLT deja de funcionar en estable, salvo bajo la prueba o la política.
- Chrome 176, 17 de agosto de 2027: ambas vías caducan. XSLTProcessor y la instrucción de procesamiento xml-stylesheet desaparecen. La forma type="text/css" de esa instrucción no se ve afectada.
Las plantillas son reglas, no un guion
Una hoja de estilos es un conjunto de reglas de plantilla, cada una con un patrón de coincidencia. El procesador arranca en la raíz del documento, encuentra la plantilla que mejor coincide, la ejecuta, y donde esa plantilla dice xsl:apply-templates repite el proceso para los hijos seleccionados. Nada se ejecuta en el orden en que lo escribiste.
xsl:apply-templates devuelve el control al conjunto de reglas: cada nodo seleccionado encuentra su propia plantilla, así que hijos heterogéneos reciben sus propias reglas en lugar de una cadena de ramas xsl:choose. xsl:for-each itera en línea, aplicando un solo cuerpo a cada nodo seleccionado, lo que se lee mejor para una lista plana de filas. Ambos cambian el nodo de contexto, de donde viene la mayoría de los avisos de una expresión select que de pronto está mal.
XSLT también tiene reglas integradas que tú no escribiste: la de los elementos aplica plantillas a sus hijos, y la de los nodos de texto copia el texto tal cual. Esa pareja es la razón de que una hoja de estilos cuyos patrones no coinciden con nada siga produciendo una página de texto todo pegado.
Cuando la transformación no produce nada
Un resultado vacío es el segundo problema de XSLT más reportado después del texto todo pegado, y tiene una causa dominante: los espacios de nombres. Una hoja de estilos escrita para XML sin espacio de nombres no coincidirá con elementos que estén en uno, aunque los nombres se vean idénticos en ambos archivos. Si tu raíz de origen lleva xmlns="http://example.com/orders", <xsl:template match="order"> pide {}order mientras el documento tiene {http://example.com/orders}order. No dispara ninguna plantilla, y XSLT no considera eso un error.
Declara el espacio de nombres en la hoja de estilos con el prefijo que prefieras y úsalo en cada match y cada select. XSLT 1.0 comparte la limitación de XPath 1.0, así que el prefijo no es opcional, y xpath-default-namespace no ayuda porque es de XSLT 2.0 y los navegadores lo ignoran.
Una hoja de estilos que declara version="2.0" o "3.0" se señala antes de ejecutarse, porque ambos motores son 1.0 y las construcciones posteriores pueden fallar en silencio, dando una salida sutilmente equivocada en vez de ausente. Y si la raíz de la hoja de estilos no está en http://www.w3.org/1999/XSL/Transform, no funciona nada en absoluto: ese URI es idéntico para las tres versiones de XSLT, y uno mal escrito compila a una hoja de estilos sin ninguna regla dentro.
Lo que ningún motor XSLT de navegador puede hacer
Solo XSLT 1.0. Ningún navegador ha distribuido nunca 2.0 ni 3.0, y libxslt tampoco los implementa, así que el respaldo WebAssembly no sube el techo. Eso descarta xsl:function, xsl:for-each-group (la agrupación sigue siendo la técnica muenchiana con key()), xsl:analyze-string y xsl:result-document. Saxon-JS es la única vía a 2.0 o 3.0 en un navegador.
Los recursos externos son el otro límite duro. document(), xsl:include y xsl:import de URLs remotas se rigen por la política del mismo origen, así que una hoja de estilos que traiga una tabla de consulta por HTTP fallará. Aquí nada descarga por ti y no hay proxy. Unas cuantas lagunas propias de cada motor también pillan a la gente:
- disable-output-escaping no está implementado en Firefox: TransforMiiX produce un árbol en lugar de una cadena serializada, y la especificación permite al procesador recuperarse tratando el atributo como no. Usa una referencia de carácter numérica como   en su lugar.
- xsl:namespace-alias no está soportado en Firefox, como tampoco el eje namespace:: de XPath.
- xsl:output es en gran medida orientativo cuando el procesador se maneja desde script, porque el resultado es un DOM: indent, encoding y las propiedades de doctype tienen poco efecto. Esta página lee de él method para decidir entre texto y marcado serializado.
- La cobertura de EXSLT difiere. libxslt, usado por Chrome, Safari y el respaldo WebAssembly, implementa casi todo. Firefox implementa un subconjunto.
- transformToDocument y transformToFragment devuelven null al fallar en lugar de lanzar, y el motivo real solo aparece en la consola.
Hacer esto desde código
Una hoja de estilos es entrada ejecutable. Todos los motores de abajo pueden leer archivos, abrir conexiones de red o escribir en disco mediante document(), xsl:import o una función de extensión, así que cada ejemplo lo desactiva. Trata una hoja de estilos que no escribiste como un script que no escribiste.
// Native first, WebAssembly fallback. A probe, not a feature check:
// after Chrome 158 the constructor is gone, but a constructor that
// exists and fails is the case that actually misleads you.
function nativeXsltWorks() {
if (typeof XSLTProcessor === 'undefined') return false;
try {
const p = new DOMParser();
const xsl = p.parseFromString(
'<xsl:stylesheet version="1.0" ' +
'xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' +
'<xsl:template match="/"><ok/></xsl:template></xsl:stylesheet>',
'application/xml');
const proc = new XSLTProcessor();
proc.importStylesheet(xsl);
const out = proc.transformToDocument(
p.parseFromString('<a/>', 'application/xml'));
return out?.documentElement?.nodeName === 'ok';
} catch {
return false;
}
}
async function transform(xmlText, xslText) {
// xslt-polyfill is libxslt compiled to WebAssembly. Importing it
// installs a replacement XSLTProcessor on globalThis.
if (!nativeXsltWorks()) await import('xslt-polyfill');
const p = new DOMParser();
const xml = p.parseFromString(xmlText, 'application/xml');
const xsl = p.parseFromString(xslText, 'application/xml');
if (xml.querySelector('parsererror') || xsl.querySelector('parsererror')) {
throw new Error('one of the two inputs is not well-formed');
}
const proc = new XSLTProcessor();
proc.importStylesheet(xsl);
proc.setParameter(null, 'lang', 'en'); // (namespaceURI, name, value)
// Returns null rather than throwing. Do not skip this check.
const out = proc.transformToDocument(xml);
if (!out) throw new Error('no template matched the document root');
return new XMLSerializer().serializeToString(out);
}from lxml import etree
# lxml wraps libxslt, the same engine Chrome and Safari use.
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
xml = etree.fromstring(xml_bytes, parser)
xsl = etree.fromstring(xsl_bytes, parser)
# DENY_ALL blocks document(), xsl:include, xsl:import and every EXSLT
# file or network operation. Without it a stylesheet is a file-read
# primitive that runs whatever its author wrote.
transform = etree.XSLT(xsl, access_control=etree.XSLTAccessControl.DENY_ALL)
try:
result = transform(xml, lang="'en'") # params are XPath: quote strings twice
except etree.XSLTApplyError as e:
raise SystemExit(f'transform failed: {e}')
# xsl:message output and warnings land here, not in the result.
for entry in transform.error_log:
print(f'{entry.line}: {entry.message}')
print(str(result)) # honours xsl:output method, encoding and indentimport javax.xml.XMLConstants;
import javax.xml.transform.*;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
TransformerFactory factory = TransformerFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// An empty string means "no protocol is permitted", which blocks
// document(), xsl:import and xsl:include of anything outside the
// stylesheet you handed in. Neither is restricted by default.
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
// Without a listener, compilation warnings go to stderr and are lost.
factory.setErrorListener(new ErrorListener() {
public void warning(TransformerException e) {
System.err.println("warning: " + e.getMessageAndLocation());
}
public void error(TransformerException e) throws TransformerException {
throw e;
}
public void fatalError(TransformerException e) throws TransformerException {
throw e;
}
});
// Templates is compiled once and is thread-safe; Transformer is not.
Templates templates = factory.newTemplates(new StreamSource(stylesheetStream));
Transformer transformer = templates.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setParameter("lang", "en");
transformer.transform(new StreamSource(xmlStream), new StreamResult(System.out));
// The JDK's built-in processor is Xalan, XSLT 1.0. For 2.0 or 3.0 put
// Saxon-HE on the classpath; it registers itself as the factory.using System.Xml;
using System.Xml.Xsl;
var readerSettings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
};
var transform = new XslCompiledTransform();
// XsltSettings.Default disables both the document() function and
// embedded <msxsl:script>. XsltSettings.TrustedXslt enables both and
// should never be used on a stylesheet from outside your codebase.
// The null resolver blocks xsl:import and xsl:include as well.
using (var xslReader = XmlReader.Create(stylesheetPath, readerSettings))
{
transform.Load(xslReader, XsltSettings.Default, null);
}
var args = new XsltArgumentList();
args.AddParam("lang", "", "en");
using var xmlReader = XmlReader.Create(inputPath, readerSettings);
using var writer = XmlWriter.Create(Console.Out, transform.OutputSettings);
transform.Transform(xmlReader, args, writer);
// XslCompiledTransform is XSLT 1.0. Microsoft never shipped 2.0 for
// .NET; Saxonica's Saxon-HE has a .NET build if you need it.<?php
// Requires ext-xsl, which wraps libxslt.
$xml = new DOMDocument();
$xml->loadXML($xmlSource, LIBXML_NONET);
$xsl = new DOMDocument();
$xsl->loadXML($xslSource, LIBXML_NONET);
$proc = new XSLTProcessor();
// Block every filesystem and network operation a stylesheet can reach
// for. Only the write prefs are blocked by default.
$proc->setSecurityPrefs(
XSL_SECPREF_READ_FILE
| XSL_SECPREF_WRITE_FILE
| XSL_SECPREF_CREATE_DIRECTORY
| XSL_SECPREF_READ_NETWORK
| XSL_SECPREF_WRITE_NETWORK
);
$proc->importStylesheet($xsl);
$proc->setParameter('', 'lang', 'en');
$result = $proc->transformToXml($xml);
if ($result === false) {
fwrite(STDERR, "transformation failed\n");
exit(1);
}
echo $result;# xsltproc ships with libxslt: the same engine Chrome and Safari use,
# and the same one this page falls back to in WebAssembly.
# --nonet stops it fetching anything the stylesheet references.
xsltproc --nonet style.xsl doc.xml > out.html
# --stringparam passes a string. --param takes an XPath expression, so
# a literal string needs its own quotes inside the shell quotes.
xsltproc --nonet --stringparam lang en style.xsl doc.xml
xsltproc --nonet --param year "'2026'" style.xsl doc.xml
# xsl:message output goes to stderr, so separate it before piping.
xsltproc --nonet style.xsl doc.xml 2> messages.log | xmllint --format -
# XSLT 2.0 and 3.0 need Saxon. libxslt will never implement them.
java -cp saxon-he.jar net.sf.saxon.Transform \
-s:doc.xml -xsl:style.xsl -o:out.html lang=enLa sintaxis de parámetros difiere en cada uno de estos, y no de forma cosmética: en lxml, en xsltproc --param y en Saxon, el valor de un parámetro es una expresión XPath, así que la cadena en debe escribirse "'en'" o se lee como un paso de ruta que selecciona elementos llamados en. setParameter en JavaScript y XsltArgumentList en .NET aceptan cadenas simples.
Preguntas frecuentes
¿Dejará de funcionar esta herramienta cuando Chrome elimine XSLT?
No. Chrome 158, el 17 de noviembre de 2026, hace que XSLT deje de funcionar en estable, y Chrome 176 el 17 de agosto de 2027 elimina XSLTProcessor y la instrucción de procesamiento xml-stylesheet del todo. Una herramienta construida solo sobre el procesador nativo se rompería en esas fechas.
Esta página ejecuta antes una transformación de sondeo de una línea. Mientras tu navegador tenga un motor nativo que funcione, es el que se usa; cuando el sondeo falla, se carga en su lugar una compilación WebAssembly de libxslt y el panel dice qué motor se ejecutó. El respaldo se descarga solo cuando hace falta, así que quienes usan Firefox y Safari nunca lo bajan.
¿Puedo ejecutar XSLT 2.0 o 3.0 aquí?
No, y ningún navegador puede tampoco. Chrome y Safari usan libxslt, Firefox usa su propio TransforMiiX, y los tres implementan 1.0. El respaldo WebAssembly también es libxslt, así que no cambia el techo.
Si tu hoja de estilos declara version="2.0" o "3.0", esta página avisa antes de ejecutar y no después, porque un procesador 1.0 no rechaza de plano una hoja 2.0: ejecuta lo que reconoce y maneja mal el resto sin decir nada. Saxon es la respuesta práctica: Saxon-JS en un navegador y Saxon-HE en la JVM o .NET.
¿Por qué mi hoja de estilos produce un resultado vacío?
Casi siempre por un desajuste de espacio de nombres. Si tu origen declara un espacio de nombres por defecto en su raíz, como xmlns="http://example.com/orders", todo elemento sin prefijo dentro pertenece a ese espacio. Una plantilla escrita como <xsl:template match="order"> pide un elemento order sin espacio de nombres, así que no dispara ninguna regla y la salida sale vacía. XSLT no trata eso como un error.
Declara el espacio de nombres en la hoja de estilos con un prefijo, digamos xmlns:o="http://example.com/orders", y luego escribe match="o:order" y select="o:line" en todas partes. Dos causas más que descartar: una raíz de hoja de estilos que no está en http://www.w3.org/1999/XSL/Transform, y ninguna plantilla que coincida con la raíz del documento.
¿Se suben mi XML y mi hoja de estilos?
Ninguno de los dos. Ambos paneles se quedan en esta pestaña: la comprobación de buena formación corre en un Web Worker, y la transformación corre o en el procesador integrado de tu navegador o en un módulo WebAssembly cargado en la página. Aquí no hay servidor que los reciba.
Esa es una pregunta más aguda para XSLT que para la mayoría de herramientas, porque la hoja de estilos suele ser la mitad valiosa: una transformación que mapea el esquema de un socio sobre el tuyo interno codifica reglas de negocio, nombres de campo y a veces endpoints. Una consecuencia de ejecutar en local es que la salida se muestra como texto y nunca se inserta en la página como marcado, ya que una hoja de estilos puede generar elementos script.
¿Por qué mi salida contiene todo el texto de mi documento sin marcado?
Has chocado con las reglas de plantilla integradas de XSLT. La especificación define valores por defecto que existen los escribas o no: la regla para elementos aplica plantillas a sus hijos, y la regla para nodos de texto copia el texto directamente.
Así que cuando ninguna de tus plantillas coincide, el procesador no se detiene. Recorre todo el árbol usando esos valores por defecto e imprime cada nodo de texto que encuentra, incluida la sangría del origen. La causa suele ser un espacio de nombres. El diagnóstico rápido es añadir <xsl:template match="text()"/>, que suprime el valor por defecto del texto: si la salida se queda vacía, ninguna regla tuya estaba disparando.
¿Cuál es la diferencia entre apply-templates y for-each?
xsl:for-each itera en línea. Selecciona una lista de nodos y ejecuta su cuerpo una vez por nodo en orden de documento, y ese cuerpo es fijo, así que todos los nodos se tratan igual. Se lee como un bucle y encaja con una lista plana y homogénea, como convertir cada elemento row en una fila de tabla.
xsl:apply-templates devuelve el control al conjunto de reglas, dejando que cada nodo seleccionado encuentre la plantilla que mejor le encaja, así que distintos tipos de elemento reciben distinto trato sin condicionales. Además acepta un atributo mode, así que el mismo nodo puede procesarse dos veces bajo reglas distintas, una para un índice y otra para el cuerpo. for-each no tiene equivalente.
¿Puede la hoja de estilos cargar otro archivo con document() o xsl:import?
Aquí no. document(), xsl:include y xsl:import resuelven todos una URL, y en un navegador esas cargas están sujetas a la política del mismo origen, así que cualquier cosa remota queda bloqueada. Esta página tampoco tiene proxy de descarga, y es deliberado: hacer de proxy significaría que el documento de destino, y la URL misma, pasan por un servidor que controla este sitio.
Incrusta el segundo documento en tu origen y selecciónalo con XPath normal, mueve la tabla de consulta a un parámetro de la hoja de estilos, o ejecuta la transformación en local con xsltproc, donde document() resuelve contra tu sistema de archivos.