diff --git a/src/dom/xml-functions.ts b/src/dom/xml-functions.ts index c4926bb..8888457 100644 --- a/src/dom/xml-functions.ts +++ b/src/dom/xml-functions.ts @@ -210,17 +210,72 @@ export function xmlTransformedText( } ) { const buffer: string[] = []; - xmlTransformedTextRecursive(node, buffer, options); + xmlTransformedTextRecursive(node, buffer, options, 0); return buffer.join(''); } +/** + * Two spaces, used as the indentation unit per depth level when + * `options.indent` is enabled. + */ +const INDENT_UNIT = ' '; + +/** + * Whether indentation should be considered at all for the given output method. + * Only `xml`, `xhtml` and `html` are eligible; `text` and `name` outputs are + * never indented (and `json`/`adaptive` never reach this serializer at all). + * @param options XML output options. + */ +function xmlIsIndentEligibleOutputMethod(options: XmlOutputOptions): boolean { + return options.outputMethod === 'xml' || options.outputMethod === 'xhtml' || options.outputMethod === 'html'; +} + +/** + * Whether a node contributes no output to the serialized result. This mirrors + * the skip condition applied to text nodes in `xmlTransformedTextRecursive`: + * a text node produces nothing when it has no value, or when it's + * whitespace-only and wasn't created by `xsl:text`. + * @param node The node to check. + */ +function xmlProducesNoOutput(node: XNode): boolean { + if (node.nodeType !== DOM_TEXT_NODE) { + return false; + } + const isFromXslText = node.fromXslText === true; + return !(node.nodeValue && (isFromXslText || node.nodeValue.trim() !== '')); +} + +/** + * Decides whether a node's direct (non-attribute) children should be + * pretty-printed. Indentation is only safe when every child that actually + * produces output is an element, comment or processing instruction; any + * text/CDATA content that will be serialized is considered "mixed content" + * and is left untouched to avoid altering the value. Ignorable whitespace-only + * text nodes (which are dropped during serialization anyway) don't disqualify + * indentation. + * @param childNodes The non-attribute child nodes of an element. + */ +function xmlShouldIndentChildren(childNodes: XNode[]): boolean { + const significantChildren = childNodes.filter((child) => !xmlProducesNoOutput(child)); + if (significantChildren.length === 0) { + return false; + } + return significantChildren.every( + (child) => + child.nodeType === DOM_ELEMENT_NODE || + child.nodeType === DOM_COMMENT_NODE || + child.nodeType === DOM_PROCESSING_INSTRUCTION_NODE + ); +} + /** * The recursive logic to transform a node in XML text. * @param {XNode} node The node. * @param {string[]} buffer The buffer, that will represent the transformed XML text. * @param {XmlOutputOptions} options XML output options. + * @param {number} depth Current nesting depth, used to compute indentation. */ -function xmlTransformedTextRecursive(node: XNode, buffer: string[], options: XmlOutputOptions) { +function xmlTransformedTextRecursive(node: XNode, buffer: string[], options: XmlOutputOptions, depth: number = 0) { if (node.visited) return; const nodeType = node.nodeType const nodeValue = node.nodeValue; @@ -258,15 +313,15 @@ function xmlTransformedTextRecursive(node: XNode, buffer: string[], options: Xml } else if (nodeType == DOM_ELEMENT_NODE) { if (options.outputMethod === 'text') { // For text output, only extract text content from elements - xmlElementLogicTextOnly(node, buffer, options); + xmlElementLogicTextOnly(node, buffer, options, depth); } else { // If node didn't have a transformed name, but its children // had transformations, children should be present at output. // This is called here "muted logic". if (node.nodeName !== null && node.nodeName !== undefined) { - xmlElementLogicTrivial(node, buffer, options); + xmlElementLogicTrivial(node, buffer, options, depth); } else { - xmlElementLogicMuted(node, buffer, options); + xmlElementLogicMuted(node, buffer, options, depth); } } } else if (nodeType === DOM_DOCUMENT_NODE || nodeType === DOM_DOCUMENT_FRAGMENT_NODE) { @@ -281,7 +336,7 @@ function xmlTransformedTextRecursive(node: XNode, buffer: string[], options: Xml childNodes.sort((a, b) => a.siblingPosition - b.siblingPosition); for (let i = 0; i < childNodes.length; ++i) { - xmlTransformedTextRecursive(childNodes[i], buffer, options); + xmlTransformedTextRecursive(childNodes[i], buffer, options, depth); } } @@ -292,9 +347,10 @@ function xmlTransformedTextRecursive(node: XNode, buffer: string[], options: Xml * XML element output, trivial logic. * @param node The XML node. * @param buffer The XML buffer. - * @param cdata If using CDATA configuration. + * @param options XML output options. + * @param depth Current nesting depth, used to compute indentation. */ -function xmlElementLogicTrivial(node: XNode, buffer: string[], options: XmlOutputOptions) { +function xmlElementLogicTrivial(node: XNode, buffer: string[], options: XmlOutputOptions, depth: number = 0) { buffer.push(`<${xmlFullNodeName(node)}`); let attributes: XNode[] = []; @@ -354,8 +410,21 @@ function xmlElementLogicTrivial(node: XNode, buffer: string[], options: XmlOutpu } } else { buffer.push('>'); + const indentChildren = + options.indent === true && + xmlIsIndentEligibleOutputMethod(options) && + xmlShouldIndentChildren(childNodes); for (let i = 0; i < childNodes.length; ++i) { - xmlTransformedTextRecursive(childNodes[i], buffer, options); + const child = childNodes[i]; + // Skip the indent prefix before nodes that produce no output (e.g. + // ignorable whitespace-only text), so they don't leave blank lines. + if (indentChildren && !xmlProducesNoOutput(child)) { + buffer.push('\n' + INDENT_UNIT.repeat(depth + 1)); + } + xmlTransformedTextRecursive(child, buffer, options, depth + 1); + } + if (indentChildren) { + buffer.push('\n' + INDENT_UNIT.repeat(depth)); } buffer.push(``); } @@ -367,9 +436,10 @@ function xmlElementLogicTrivial(node: XNode, buffer: string[], options: XmlOutpu * children can be printed if they have transformed values. * @param node The XML node. * @param buffer The XML buffer. - * @param cdata If using CDATA configuration. + * @param options XML output options. + * @param depth Current nesting depth, used to compute indentation. */ -function xmlElementLogicMuted(node: XNode, buffer: any[], options: XmlOutputOptions) { +function xmlElementLogicMuted(node: XNode, buffer: any[], options: XmlOutputOptions, depth: number = 0) { let childNodes: XNode[] = []; if (node.firstChild) { let child = node.firstChild; @@ -382,7 +452,7 @@ function xmlElementLogicMuted(node: XNode, buffer: any[], options: XmlOutputOpti } childNodes = childNodes.sort((a, b) => a.siblingPosition - b.siblingPosition); for (let i = 0; i < childNodes.length; ++i) { - xmlTransformedTextRecursive(childNodes[i], buffer, options); + xmlTransformedTextRecursive(childNodes[i], buffer, options, depth); } } @@ -391,8 +461,9 @@ function xmlElementLogicMuted(node: XNode, buffer: any[], options: XmlOutputOpti * @param node The XML node. * @param buffer The output buffer. * @param options XML output options. + * @param depth Current nesting depth (unused in text mode, kept for signature symmetry). */ -function xmlElementLogicTextOnly(node: XNode, buffer: string[], options: XmlOutputOptions) { +function xmlElementLogicTextOnly(node: XNode, buffer: string[], options: XmlOutputOptions, depth: number = 0) { let childNodes: XNode[] = []; if (node.firstChild) { let child = node.firstChild; @@ -405,7 +476,7 @@ function xmlElementLogicTextOnly(node: XNode, buffer: string[], options: XmlOutp } childNodes = childNodes.sort((a, b) => a.siblingPosition - b.siblingPosition); for (let i = 0; i < childNodes.length; ++i) { - xmlTransformedTextRecursive(childNodes[i], buffer, options); + xmlTransformedTextRecursive(childNodes[i], buffer, options, depth); } } diff --git a/src/dom/xml-output-options.ts b/src/dom/xml-output-options.ts index 5e003b1..bd64cbb 100644 --- a/src/dom/xml-output-options.ts +++ b/src/dom/xml-output-options.ts @@ -5,4 +5,11 @@ export type XmlOutputOptions = { outputMethod: 'xml' | 'html' | 'text' | 'name' | 'xhtml'; outputVersion?: string; itemSeparator?: string; + /** + * Whether to pretty-print the output, corresponding to `xsl:output`'s + * `indent` attribute. Only elements whose children are exclusively + * elements, comments and/or processing instructions (i.e. no significant + * text content) are indented, to avoid corrupting mixed content. + */ + indent?: boolean; } diff --git a/src/xpath/lib b/src/xpath/lib index 3dff86f..39bb40d 160000 --- a/src/xpath/lib +++ b/src/xpath/lib @@ -1 +1 @@ -Subproject commit 3dff86fa9190498f07ffed8c075b8ee5b7f4c29e +Subproject commit 39bb40d77bdca21fc4211a124805597ed0f48bba diff --git a/src/xslt/xslt.ts b/src/xslt/xslt.ts index 550169e..876e403 100644 --- a/src/xslt/xslt.ts +++ b/src/xslt/xslt.ts @@ -159,6 +159,7 @@ export class Xslt { outputMethod: 'xml' | 'html' | 'text' | 'name' | 'xhtml' | 'json' | 'adaptive'; outputOmitXmlDeclaration: string; outputVersion: string; + outputIndent: boolean; itemSeparator: string; version: string; firstTemplateRan: boolean; @@ -298,6 +299,7 @@ export class Xslt { this.outputMethod = options.outputMethod || 'xml'; this.outputOmitXmlDeclaration = 'no'; this.outputVersion = ''; + this.outputIndent = false; this.itemSeparator = ''; this.stripSpacePatterns = []; this.preserveSpacePatterns = []; @@ -378,7 +380,8 @@ export class Xslt { selfClosingTags: this.options.selfClosingTags, outputMethod: serializationMethod as 'xml' | 'html' | 'text' | 'xhtml', outputVersion: this.outputVersion, - itemSeparator: this.itemSeparator + itemSeparator: this.itemSeparator, + indent: this.outputIndent }); return transformedOutputXml; @@ -581,6 +584,7 @@ export class Xslt { this.outputMethod = xmlGetAttribute(template, 'method') as 'xml' | 'html' | 'text' | 'name'; this.outputOmitXmlDeclaration = xmlGetAttribute(template, 'omit-xml-declaration'); this.outputVersion = xmlGetAttribute(template, 'version') || ''; + this.outputIndent = xmlGetAttribute(template, 'indent') === 'yes'; this.itemSeparator = xmlGetAttribute(template, 'item-separator') || ''; break; case 'package': @@ -4621,6 +4625,8 @@ export class Xslt { const hrefExpr = xmlGetAttribute(template, 'href') || ''; const methodAttr = xmlGetAttribute(template, 'method') || this.outputMethod || 'xml'; const omitXmlDeclaration = xmlGetAttribute(template, 'omit-xml-declaration') || this.outputOmitXmlDeclaration; + const indentAttr = xmlGetAttribute(template, 'indent'); + const indent = indentAttr ? indentAttr === 'yes' : this.outputIndent; // Evaluate href as attribute value template const href = this.xsltAttributeValue(hrefExpr, context); @@ -4647,7 +4653,8 @@ export class Xslt { selfClosingTags: this.options.selfClosingTags, outputMethod: methodAttr as 'xml' | 'html' | 'text' | 'xhtml', outputVersion: this.outputVersion, - itemSeparator: this.itemSeparator + itemSeparator: this.itemSeparator, + indent }); // Store in result documents map diff --git a/tests/lmht/html-to-lmht.test.tsx b/tests/lmht/html-to-lmht.test.tsx index cb6e616..056a658 100644 --- a/tests/lmht/html-to-lmht.test.tsx +++ b/tests/lmht/html-to-lmht.test.tsx @@ -1547,19 +1547,20 @@ describe('HTML to LMHT', () => { `; - const expectedOutString = `` + - `` + - `` + - `` + - `` + - `` + - `About - Simple Blog Template` + - `` + - `` + - `` + - `` + - `This is a paragraph with a class` + - `` + + // `indent="yes"` is declared in the stylesheet's ``, so the result is pretty-printed. + const expectedOutString = `\n` + + ` \n` + + ` \n` + + ` \n` + + ` \n` + + ` \n` + + ` About - Simple Blog Template\n` + + ` \n` + + ` \n` + + ` \n` + + ` \n` + + ` This is a paragraph with a class\n` + + ` \n` + `` const xsltClass = new Xslt({ selfClosingTags: true }); @@ -1587,17 +1588,26 @@ describe('HTML to LMHT', () => { `; - const expectedOutString = ``+ - ``+ - `Delégua Blog`+ - ``+ - ``+ - `Início`+ - `Sobre`+ - `Contato`+ - ``+ - ``+ - ``+ + // `indent="yes"` is declared in the stylesheet's ``, so the result is pretty-printed. + const expectedOutString = `\n`+ + ` \n`+ + ` \n`+ + ` Delégua Blog\n`+ + ` \n`+ + ` \n`+ + ` \n`+ + ` \n`+ + ` Início\n`+ + ` \n`+ + ` \n`+ + ` Sobre\n`+ + ` \n`+ + ` \n`+ + ` Contato\n`+ + ` \n`+ + ` \n`+ + ` \n`+ + ` \n`+ ``; const xsltClass = new Xslt({ selfClosingTags: true }); @@ -1634,26 +1644,27 @@ describe('HTML to LMHT', () => { `; - const expectedOutString = ``+ - ``+ - ``+ - ``+ - `Blog Simples`+ - ``+ - ``+ - ``+ - ``+ - `Título da Postagem 1`+ - `Publicado em 2 de julho de 2024`+ - `Conteúdo da postagem 1. Este é um exemplo de conteúdo para uma postagem de blog. Você pode adicionar mais postagens conforme necessário.`+ - ``+ - ``+ - `Título da Postagem 2`+ - `Publicado em 1 de julho de 2024`+ - `Conteúdo da postagem 2. Este é outro exemplo de conteúdo para uma postagem de blog.`+ - ``+ - ``+ - ``+ + // `indent="yes"` is declared in the stylesheet's ``, so the result is pretty-printed. + const expectedOutString = `\n`+ + ` \n`+ + ` \n`+ + ` \n`+ + ` Blog Simples\n`+ + ` \n`+ + ` \n`+ + ` \n`+ + ` \n`+ + ` Título da Postagem 1\n`+ + ` Publicado em 2 de julho de 2024\n`+ + ` Conteúdo da postagem 1. Este é um exemplo de conteúdo para uma postagem de blog. Você pode adicionar mais postagens conforme necessário.\n`+ + ` \n`+ + ` \n`+ + ` Título da Postagem 2\n`+ + ` Publicado em 1 de julho de 2024\n`+ + ` Conteúdo da postagem 2. Este é outro exemplo de conteúdo para uma postagem de blog.\n`+ + ` \n`+ + ` \n`+ + ` \n`+ ``; const xsltClass = new Xslt({ selfClosingTags: true }); diff --git a/tests/lmht/lmht.test.tsx b/tests/lmht/lmht.test.tsx index 1f302e1..d1f980a 100644 --- a/tests/lmht/lmht.test.tsx +++ b/tests/lmht/lmht.test.tsx @@ -1588,7 +1588,8 @@ describe('LMHT', () => { ` ); - const expectedOutString = `TesteTeste`; + // `indent="yes"` is declared in the stylesheet's ``, so the result is pretty-printed. + const expectedOutString = `\n \n Teste\n \n Teste\n`; const xsltClass = new Xslt(); const xmlParser = new XmlParser(); @@ -1619,22 +1620,23 @@ describe('LMHT', () => { ` ); + // `indent="yes"` is declared in the stylesheet's ``, so the result is pretty-printed. const expectedOutString = - `` + - `` + - `` + - `` + - `` + - `` + - `Meu blog` + - `` + - `` + - `` + - `
` + - `

Meu primeiro artigo

` + - `

Este é meu primeiro artigo.

` + - `
` + - `` + + `\n` + + ` \n` + + ` \n` + + ` \n` + + ` \n` + + ` \n` + + ` Meu blog\n` + + ` \n` + + ` \n` + + ` \n` + + `
\n` + + `

Meu primeiro artigo

\n` + + `

Este é meu primeiro artigo.

\n` + + `
\n` + + ` \n` + ``; const xsltClass = new Xslt({ selfClosingTags: false }); diff --git a/tests/xslt/choose.test.tsx b/tests/xslt/choose.test.tsx index 77e68dc..2f70756 100644 --- a/tests/xslt/choose.test.tsx +++ b/tests/xslt/choose.test.tsx @@ -42,7 +42,8 @@ describe('xsl:choose', () => { const xml = xmlParser.xmlParse(xmlSource); const xslt = xmlParser.xmlParse(xsltSource); const html = await xsltClass.xsltProcess(xml, xslt); - assert.equal(html, 'NoYes'); + // `indent="yes"` is declared in the stylesheet's ``, so the result is pretty-printed. + assert.equal(html, '\n No\n Yes\n'); }); it('https://github.com/DesignLiquido/xslt-processor/issues/92', async () => { @@ -128,7 +129,8 @@ describe('xsl:choose', () => { const xml = xmlParser.xmlParse(xmlSource); const xslt = xmlParser.xmlParse(xsltSource); const html = await xsltClass.xsltProcess(xml, xslt); - assert.equal(html, ''); + // `indent="yes"` is declared in the stylesheet's ``, so the result is pretty-printed. + assert.equal(html, '\n \n'); }); /** diff --git a/tests/xslt/import.test.tsx b/tests/xslt/import.test.tsx index 70d5107..858625a 100644 --- a/tests/xslt/import.test.tsx +++ b/tests/xslt/import.test.tsx @@ -17,7 +17,31 @@ describe('xsl:import', () => { const xml = xmlParser.xmlParse(xmlSource); const xslt = xmlParser.xmlParse(xsltSource); const resultingXml = await xsltClass.xsltProcess(xml, xslt); - assert.equal(resultingXml, '</head><body><div id="container"><div id="header"><div id="menu"><ul><li><a href="#" class="active">Home</a></li><li><a href="#">about</a></li></ul></div></div></div></body></html>'); + // `indent="yes"` is declared in the stylesheet's `<xsl:output>`, so the result is pretty-printed. + const expected = + '<html>\n' + + ' <head>\n' + + ' <link rel="stylesheet" type="text/css" href="style.css">\n' + + ' <title/>\n' + + ' </head>\n' + + ' <body>\n' + + ' <div id="container">\n' + + ' <div id="header">\n' + + ' <div id="menu">\n' + + ' <ul>\n' + + ' <li>\n' + + ' <a href="#" class="active">Home</a>\n' + + ' </li>\n' + + ' <li>\n' + + ' <a href="#">about</a>\n' + + ' </li>\n' + + ' </ul>\n' + + ' </div>\n' + + ' </div>\n' + + ' </div>\n' + + ' </body>\n' + + '</html>'; + assert.equal(resultingXml, expected); }); it('Not the first child of `<xsl:stylesheet>` or `<xsl:transform>`', async () => { diff --git a/tests/xslt/include.test.tsx b/tests/xslt/include.test.tsx index f381c63..8466372 100644 --- a/tests/xslt/include.test.tsx +++ b/tests/xslt/include.test.tsx @@ -17,6 +17,30 @@ describe('xsl:include', () => { const xml = xmlParser.xmlParse(xmlSource); const xslt = xmlParser.xmlParse(xsltSource); const resultingXml = await xsltClass.xsltProcess(xml, xslt); - assert.equal(resultingXml, '<html><head><link rel="stylesheet" type="text/css" href="style.css"><title/></head><body><div id="container"><div id="header"><div id="menu"><ul><li><a href="#" class="active">Home</a></li><li><a href="#">about</a></li></ul></div></div></div></body></html>'); + // `indent="yes"` is declared in the stylesheet's `<xsl:output>`, so the result is pretty-printed. + const expected = + '<html>\n' + + ' <head>\n' + + ' <link rel="stylesheet" type="text/css" href="style.css">\n' + + ' <title/>\n' + + ' </head>\n' + + ' <body>\n' + + ' <div id="container">\n' + + ' <div id="header">\n' + + ' <div id="menu">\n' + + ' <ul>\n' + + ' <li>\n' + + ' <a href="#" class="active">Home</a>\n' + + ' </li>\n' + + ' <li>\n' + + ' <a href="#">about</a>\n' + + ' </li>\n' + + ' </ul>\n' + + ' </div>\n' + + ' </div>\n' + + ' </div>\n' + + ' </body>\n' + + '</html>'; + assert.equal(resultingXml, expected); }); }); diff --git a/tests/xslt/output-indent.test.ts b/tests/xslt/output-indent.test.ts new file mode 100644 index 0000000..54b2bda --- /dev/null +++ b/tests/xslt/output-indent.test.ts @@ -0,0 +1,134 @@ +import { Xslt } from '../../src/xslt'; +import { XmlParser } from '../../src/dom'; + +describe('xsl:output indent', () => { + // https://github.com/DesignLiquido/xslt-processor/issues/219 + it('pretty-prints the output when indent="yes"', async () => { + const xmlString = `<FOO></FOO>`; + const xsltString = `<?xml version="1.0" encoding="utf-8"?> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="xml" indent="yes"/> + <xsl:template match="FOO"> + <BAR> + <QUX> + </QUX> + </BAR> + </xsl:template> +</xsl:stylesheet>`; + + const xsltClass = new Xslt(); + const xmlParser = new XmlParser(); + const xml = xmlParser.xmlParse(xmlString); + const xslt = xmlParser.xmlParse(xsltString); + const result = await xsltClass.xsltProcess(xml, xslt); + + expect(result).toBe('<BAR>\n <QUX/>\n</BAR>'); + }); + + it('does not indent when indent="no" (default)', async () => { + const xmlString = `<FOO></FOO>`; + const xsltString = `<?xml version="1.0" encoding="utf-8"?> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="xml"/> + <xsl:template match="FOO"> + <BAR> + <QUX> + </QUX> + </BAR> + </xsl:template> +</xsl:stylesheet>`; + + const xsltClass = new Xslt(); + const xmlParser = new XmlParser(); + const xml = xmlParser.xmlParse(xmlString); + const xslt = xmlParser.xmlParse(xsltString); + const result = await xsltClass.xsltProcess(xml, xslt); + + expect(result).toBe('<BAR><QUX/></BAR>'); + }); + + it('does not insert whitespace into elements with mixed (text + element) content', async () => { + const xmlString = `<FOO></FOO>`; + const xsltString = `<?xml version="1.0" encoding="utf-8"?> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="xml" indent="yes"/> + <xsl:template match="FOO"> + <span>Before<b>bold</b>After</span> + </xsl:template> +</xsl:stylesheet>`; + + const xsltClass = new Xslt(); + const xmlParser = new XmlParser(); + const xml = xmlParser.xmlParse(xmlString); + const xslt = xmlParser.xmlParse(xsltString); + const result = await xsltClass.xsltProcess(xml, xslt); + + expect(result).toBe('<span>Before<b>bold</b>After</span>'); + }); + + it('indents nested elements at increasing depths', async () => { + const xmlString = `<FOO></FOO>`; + const xsltString = `<?xml version="1.0" encoding="utf-8"?> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="xml" indent="yes"/> + <xsl:template match="FOO"> + <a><b><c/><d/></b></a> + </xsl:template> +</xsl:stylesheet>`; + + const xsltClass = new Xslt(); + const xmlParser = new XmlParser(); + const xml = xmlParser.xmlParse(xmlString); + const xslt = xmlParser.xmlParse(xsltString); + const result = await xsltClass.xsltProcess(xml, xslt); + + expect(result).toBe('<a>\n <b>\n <c/>\n <d/>\n </b>\n</a>'); + }); + + it('still indents when ignorable whitespace-only text nodes are copied from the source (e.g. via xsl:copy-of)', async () => { + // The source's own pretty-printing whitespace is copied along with the element + // nodes but produces no output (it's whitespace-only and not from xsl:text), + // so it must not disable indentation of the surrounding elements. + const xmlString = `<root><a> + <b/> + <c/> +</a></root>`; + const xsltString = `<?xml version="1.0" encoding="utf-8"?> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="xml" indent="yes"/> + <xsl:template match="/root"> + <xsl:copy-of select="a"/> + </xsl:template> +</xsl:stylesheet>`; + + const xsltClass = new Xslt(); + const xmlParser = new XmlParser(); + const xml = xmlParser.xmlParse(xmlString); + const xslt = xmlParser.xmlParse(xsltString); + const result = await xsltClass.xsltProcess(xml, xslt); + + expect(result).toBe('<a>\n <b/>\n <c/>\n</a>'); + }); + + it('honors indent="yes" set via xsl:result-document, independent of xsl:output', async () => { + const xmlString = `<FOO></FOO>`; + const xsltString = `<?xml version="1.0" encoding="utf-8"?> +<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="xml" indent="no"/> + <xsl:template match="FOO"> + <xsl:result-document href="out.xml" indent="yes"> + <BAR><QUX/></BAR> + </xsl:result-document> + </xsl:template> +</xsl:stylesheet>`; + + const xsltClass = new Xslt(); + const xmlParser = new XmlParser(); + const xml = xmlParser.xmlParse(xmlString); + const xslt = xmlParser.xmlParse(xsltString); + await xsltClass.xsltProcess(xml, xslt); + + const resultDocuments = xsltClass.getResultDocuments(); + expect(resultDocuments.get('out.xml')).toBe('<BAR>\n <QUX/>\n</BAR>'); + }); +}); diff --git a/tests/xslt/xslt.test.tsx b/tests/xslt/xslt.test.tsx index 8673509..53fd75c 100644 --- a/tests/xslt/xslt.test.tsx +++ b/tests/xslt/xslt.test.tsx @@ -40,10 +40,13 @@ describe('xslt', () => { </xsl:stylesheet>` // Needs to be this way. `isomorphic-jsx rewrites `<outputA />` as `<outputA></outputA>`. - const expectedOutString = `<outputUnknown original-name="root">`+ - `<outputA/>`+ - `<outputB>I have text!</outputB>`+ - `</outputUnknown>`; + // `indent="yes"` is declared in the stylesheet's `<xsl:output>`, so the result + // is pretty-printed (2-space indent) since `outputUnknown`'s children are all elements. + const expectedOutString = + `<outputUnknown original-name="root">\n` + + ` <outputA/>\n` + + ` <outputB>I have text!</outputB>\n` + + `</outputUnknown>`; const xsltClass = new Xslt(); const xmlParser = new XmlParser(); @@ -114,13 +117,14 @@ describe('xslt', () => { </xsl:template> </xsl:stylesheet>`; - const expectedOutString = `<outputUnknown original-name="root">` + - `<subnode>Custom text</subnode>` + - `<outputA>`+ - `<yep/>`+ - `</outputA>`+ - `<outputB foo="bar">I have text!</outputB>`+ - `</outputUnknown>`; + const expectedOutString = + `<outputUnknown original-name="root">\n` + + ` <subnode>Custom text</subnode>\n` + + ` <outputA>\n` + + ` <yep/>\n` + + ` </outputA>\n` + + ` <outputB foo="bar">I have text!</outputB>\n` + + `</outputUnknown>`; const xsltClass = new Xslt(); const xmlParser = new XmlParser(); @@ -159,12 +163,13 @@ describe('xslt', () => { </xsl:template> </xsl:stylesheet>`; - const expectedOutString = `<outputUnknown original-name="root">`+ - `<outputA>`+ - `<yep/>`+ - `</outputA>`+ - `<outputB foo="bar">I have text!</outputB>`+ - `</outputUnknown>`; + const expectedOutString = + `<outputUnknown original-name="root">\n` + + ` <outputA>\n` + + ` <yep/>\n` + + ` </outputA>\n` + + ` <outputB foo="bar">I have text!</outputB>\n` + + `</outputUnknown>`; const xsltClass = new Xslt(); const xmlParser = new XmlParser(); @@ -201,10 +206,11 @@ describe('xslt', () => { </xsl:template> </xsl:stylesheet>`; - const expectedOutString = `<outputUnknown original-name="root">`+ - `<outputA/>`+ - `<outputB foo="bar">I have text!</outputB>`+ - `</outputUnknown>`; + const expectedOutString = + `<outputUnknown original-name="root">\n` + + ` <outputA/>\n` + + ` <outputB foo="bar">I have text!</outputB>\n` + + `</outputUnknown>`; const xsltClass = new Xslt(); const xmlParser = new XmlParser(); diff --git a/tsconfig.debug.json b/tsconfig.debug.json index 081ac7c..4020e01 100644 --- a/tsconfig.debug.json +++ b/tsconfig.debug.json @@ -4,6 +4,11 @@ "outDir": "dist", "module": "CommonJS", "target": "ES2023", + "ignoreDeprecations": "6.0", + "strict": false, + "strictNullChecks": false, + "strictPropertyInitialization": false, + "useUnknownInCatchVariables": false, "rootDir": "src", "allowJs": true, "sourceMap": true,