Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 85 additions & 14 deletions src/dom/xml-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,17 +210,72 @@
}
) {
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;
}

Check warning on line 262 in src/dom/xml-functions.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
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;
Expand Down Expand Up @@ -258,15 +313,15 @@
} 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) {
Expand All @@ -281,7 +336,7 @@
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);
}
}

Expand All @@ -292,9 +347,10 @@
* 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[] = [];
Expand Down Expand Up @@ -354,8 +410,21 @@
}
} else {
buffer.push('>');
const indentChildren =
options.indent === true &&
xmlIsIndentEligibleOutputMethod(options) &&
xmlShouldIndentChildren(childNodes);
for (let i = 0; i < childNodes.length; ++i) {
Comment thread
leonelsanchesdasilva marked this conversation as resolved.
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(`</${xmlFullNodeName(node)}>`);
}
Expand All @@ -367,9 +436,10 @@
* 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;
Expand All @@ -382,7 +452,7 @@
}
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);
}
}

Expand All @@ -391,8 +461,9 @@
* @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;
Expand All @@ -405,7 +476,7 @@
}
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);
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/dom/xml-output-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
11 changes: 9 additions & 2 deletions src/xslt/xslt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
99 changes: 55 additions & 44 deletions tests/lmht/html-to-lmht.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1547,19 +1547,20 @@ describe('HTML to LMHT', () => {
</html>
`;

const expectedOutString = `<lmht>` +
`<cabeça>` +
`<meta nome="description" conteúdo="LMHT"/>` +
`<meta nome="keywords" conteúdo="HTML, LMHT, Desenvolvimento, Web"/>` +
`<meta nome="author" conteúdo="Leonel Sanches da Silva"/>` +
`<meta nome="viewport" conteúdo="width=device-width, initial-scale=1.0"/>` +
`<título>About - Simple Blog Template</título>` +
`<recurso destino="css/bootstrap.min.css" tipo="stylesheet"/>` +
`<recurso destino="css/simple-blog-template.css" tipo="stylesheet"/>` +
`</cabeça>` +
`<corpo>` +
`<parágrafo classe="anything">This is a paragraph with a class</parágrafo>` +
`</corpo>` +
// `indent="yes"` is declared in the stylesheet's `<xsl:output>`, so the result is pretty-printed.
const expectedOutString = `<lmht>\n` +
` <cabeça>\n` +
` <meta nome="description" conteúdo="LMHT"/>\n` +
` <meta nome="keywords" conteúdo="HTML, LMHT, Desenvolvimento, Web"/>\n` +
` <meta nome="author" conteúdo="Leonel Sanches da Silva"/>\n` +
` <meta nome="viewport" conteúdo="width=device-width, initial-scale=1.0"/>\n` +
` <título>About - Simple Blog Template</título>\n` +
` <recurso destino="css/bootstrap.min.css" tipo="stylesheet"/>\n` +
` <recurso destino="css/simple-blog-template.css" tipo="stylesheet"/>\n` +
` </cabeça>\n` +
` <corpo>\n` +
` <parágrafo classe="anything">This is a paragraph with a class</parágrafo>\n` +
` </corpo>\n` +
`</lmht>`

const xsltClass = new Xslt({ selfClosingTags: true });
Expand Down Expand Up @@ -1587,17 +1588,26 @@ describe('HTML to LMHT', () => {
</html>
`;

const expectedOutString = `<lmht>`+
`<corpo>`+
`<título1><ligação destino="#">Delégua Blog</ligação></título1>`+
`<navegação>`+
`<lista-simples>`+
`<item-lista><ligação destino="#">Início</ligação></item-lista>`+
`<item-lista><ligação destino="#">Sobre</ligação></item-lista>`+
`<item-lista><ligação destino="#">Contato</ligação></item-lista>`+
`</lista-simples>`+
`</navegação>`+
`</corpo>`+
// `indent="yes"` is declared in the stylesheet's `<xsl:output>`, so the result is pretty-printed.
const expectedOutString = `<lmht>\n`+
` <corpo>\n`+
` <título1>\n`+
` <ligação destino="#">Delégua Blog</ligação>\n`+
` </título1>\n`+
` <navegação>\n`+
` <lista-simples>\n`+
` <item-lista>\n`+
` <ligação destino="#">Início</ligação>\n`+
` </item-lista>\n`+
` <item-lista>\n`+
` <ligação destino="#">Sobre</ligação>\n`+
` </item-lista>\n`+
` <item-lista>\n`+
` <ligação destino="#">Contato</ligação>\n`+
` </item-lista>\n`+
` </lista-simples>\n`+
` </navegação>\n`+
` </corpo>\n`+
`</lmht>`;

const xsltClass = new Xslt({ selfClosingTags: true });
Expand Down Expand Up @@ -1634,26 +1644,27 @@ describe('HTML to LMHT', () => {
</html>
`;

const expectedOutString = `<lmht>`+
`<cabeça>`+
`<meta codificação="UTF-8"/>`+
`<meta nome="viewport" conteúdo="width=device-width, initial-scale=1.0"/>`+
`<título>Blog Simples</título>`+
`</cabeça>`+
`<corpo>`+
`<divisão classe="container main-content">`+
`<divisão classe="post">`+
`<título2>Título da Postagem 1</título2>`+
`<parágrafo>Publicado em 2 de julho de 2024</parágrafo>`+
`<parágrafo>Conteúdo da postagem 1. Este é um exemplo de conteúdo para uma postagem de blog. Você pode adicionar mais postagens conforme necessário.</parágrafo>`+
`</divisão>`+
`<divisão classe="post">`+
`<título2>Título da Postagem 2</título2>`+
`<parágrafo>Publicado em 1 de julho de 2024</parágrafo>`+
`<parágrafo>Conteúdo da postagem 2. Este é outro exemplo de conteúdo para uma postagem de blog.</parágrafo>`+
`</divisão>`+
`</divisão>`+
`</corpo>`+
// `indent="yes"` is declared in the stylesheet's `<xsl:output>`, so the result is pretty-printed.
const expectedOutString = `<lmht>\n`+
` <cabeça>\n`+
` <meta codificação="UTF-8"/>\n`+
` <meta nome="viewport" conteúdo="width=device-width, initial-scale=1.0"/>\n`+
` <título>Blog Simples</título>\n`+
` </cabeça>\n`+
` <corpo>\n`+
` <divisão classe="container main-content">\n`+
` <divisão classe="post">\n`+
` <título2>Título da Postagem 1</título2>\n`+
` <parágrafo>Publicado em 2 de julho de 2024</parágrafo>\n`+
` <parágrafo>Conteúdo da postagem 1. Este é um exemplo de conteúdo para uma postagem de blog. Você pode adicionar mais postagens conforme necessário.</parágrafo>\n`+
` </divisão>\n`+
` <divisão classe="post">\n`+
` <título2>Título da Postagem 2</título2>\n`+
` <parágrafo>Publicado em 1 de julho de 2024</parágrafo>\n`+
` <parágrafo>Conteúdo da postagem 2. Este é outro exemplo de conteúdo para uma postagem de blog.</parágrafo>\n`+
` </divisão>\n`+
` </divisão>\n`+
` </corpo>\n`+
`</lmht>`;

const xsltClass = new Xslt({ selfClosingTags: true });
Expand Down
Loading
Loading