Skip to content

Commit 814995d

Browse files
Resolving CDATA issue reported in #95:
- `output` node should only be expected for fragments; - While transforming source elements, attributes can be 1) created by a previous transformation, and 2) previous attributes could be renamed.
1 parent a74ec4b commit 814995d

File tree

7 files changed

+213
-114
lines changed

7 files changed

+213
-114
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ node_modules/
88

99
package-lock.json
1010
.history/
11+
12+
# Strange bug with debugging a unit test causes a stop in `async_hooks`.
13+
test-without-jest.ts

.vscode/launch.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@
1919
"skipFiles": ["<node_internals>/**", "node_modules/**"],
2020
"console": "integratedTerminal",
2121
"internalConsoleOptions": "neverOpen"
22+
},
23+
{
24+
"name": "Launch TS file",
25+
"type": "node",
26+
"request": "launch",
27+
"runtimeArgs": [
28+
"-r",
29+
"ts-node/register"
30+
],
31+
"args": [
32+
"${file}"
33+
],
34+
"console": "integratedTerminal",
35+
"internalConsoleOptions": "neverOpen"
2236
}
2337
]
2438
}

jest.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { Config } from '@jest/types';
22
export default async (): Promise<Config.InitialOptions> => ({
33
verbose: true,
4-
modulePathIgnorePatterns: ['<rootDir>/dist/'],
4+
modulePathIgnorePatterns: [
5+
'<rootDir>/dist/'
6+
],
57
testEnvironment: 'node',
68
coverageReporters: ['json-summary', 'lcov', 'text', 'text-summary'],
79
displayName: {

src/xslt/xslt.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,10 @@ export class Xslt {
262262
node.transformedNodeName = name;
263263

264264
domAppendTransformedChild(context.outputNodeList[context.outputPosition], node);
265+
// The element becomes the output node of the source node.
266+
context.nodeList[context.position].outputNode = node;
265267
const clonedContext = context.clone(undefined, [node], undefined, 0);
266-
await this.xsltChildNodes(clonedContext, template, node);
268+
await this.xsltChildNodes(clonedContext, template);
267269
break;
268270
case 'fallback':
269271
throw new Error(`not implemented: ${template.localName}`);
@@ -353,7 +355,7 @@ export class Xslt {
353355
node = domCreateTransformedTextNode(this.outputDocument, value);
354356
node.siblingPosition = context.nodeList[context.position].siblingPosition;
355357

356-
if (output.nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
358+
if (output && output.nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
357359
output.appendTransformedChild(node);
358360
} else {
359361
context.outputNodeList[context.outputPosition].appendTransformedChild(node);
@@ -471,7 +473,7 @@ export class Xslt {
471473
await this.xsltChildNodes(context, template, documentFragment);
472474
const value = xmlValue2(documentFragment);
473475

474-
if (output.nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
476+
if (output && output.nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
475477
domSetTransformedAttribute(output, name, value);
476478
} else {
477479
let sourceNode = context.nodeList[context.position];
@@ -507,15 +509,19 @@ export class Xslt {
507509

508510
// If the parent transformation is something like `xsl:element`, we should
509511
// add a copy of the attribute to this element.
510-
domSetTransformedAttribute(output, name, value);
512+
domSetTransformedAttribute(outputNode, name, value);
513+
514+
if (sourceNode.nodeType === DOM_ATTRIBUTE_NODE) {
515+
sourceNode.transformedNodeType = DOM_ATTRIBUTE_NODE;
516+
sourceNode.transformedNodeName = name;
517+
sourceNode.transformedNodeValue = value;
518+
}
511519

512520
// Some operations start by the tag attributes, and not by the tag itself.
513521
// When this is the case, the output node is not set yet, so
514522
// we add the transformed attributes into the original tag.
515523
if (parentSourceNode && parentSourceNode.outputNode) {
516524
domSetTransformedAttribute(parentSourceNode.outputNode, name, value);
517-
} else {
518-
domSetTransformedAttribute(parentSourceNode, name, value);
519525
}
520526
}
521527
}
@@ -527,7 +533,7 @@ export class Xslt {
527533
* @param template The template.
528534
* @param output The output. Only used if there's no corresponding output node already defined.
529535
*/
530-
protected async xsltChoose(context: ExprContext, template: XNode, output: XNode) {
536+
protected async xsltChoose(context: ExprContext, template: XNode, output?: XNode) {
531537
for (const childNode of template.childNodes) {
532538
if (childNode.nodeType !== DOM_ELEMENT_NODE) {
533539
continue;
@@ -536,13 +542,11 @@ export class Xslt {
536542
if (this.isXsltElement(childNode, 'when')) {
537543
const test = xmlGetAttribute(childNode, 'test');
538544
if (this.xPath.xPathEval(test, context).booleanValue()) {
539-
const outputNode = context.outputNodeList[context.outputPosition] || output;
540-
await this.xsltChildNodes(context, childNode, outputNode);
545+
await this.xsltChildNodes(context, childNode, output);
541546
break;
542547
}
543548
} else if (this.isXsltElement(childNode, 'otherwise')) {
544-
const outputNode = context.outputNodeList[context.outputPosition] || output;
545-
await this.xsltChildNodes(context, childNode, outputNode);
549+
await this.xsltChildNodes(context, childNode, output);
546550
break;
547551
}
548552
}
@@ -740,9 +744,9 @@ export class Xslt {
740744

741745
const nonAttributeChildren = template.childNodes.filter((n) => n.nodeType !== DOM_ATTRIBUTE_NODE);
742746
if (nonAttributeChildren.length > 0) {
743-
const root = domCreateDocumentFragment(template.ownerDocument);
744-
await this.xsltChildNodes(context, template, root);
745-
value = new NodeSetValue([root]);
747+
const fragment = domCreateDocumentFragment(template.ownerDocument);
748+
await this.xsltChildNodes(context, template, fragment);
749+
value = new NodeSetValue([fragment]);
746750
} else if (select) {
747751
value = this.xPath.xPathEval(select, context);
748752
} else {
@@ -785,7 +789,7 @@ export class Xslt {
785789
* @param output The output.
786790
*/
787791
private commonLogicTextNode(context: ExprContext, template: XNode, output: XNode) {
788-
if (output.nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
792+
if (output && output.nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
789793
let node = domCreateTransformedTextNode(this.outputDocument, template.nodeValue);
790794
domAppendTransformedChild(output, node);
791795
} else {
@@ -843,7 +847,16 @@ export class Xslt {
843847
newNode.transformedLocalName = template.localName;
844848

845849
// The node can have transformed attributes from previous transformations.
846-
const transformedAttributes = node.transformedChildNodes.filter((n) => n.nodeType === DOM_ATTRIBUTE_NODE);
850+
// Case 1: attributes that were created by a transformation without a source attribute.
851+
const transformedChildNodes = node.transformedChildNodes.filter((n) => n.nodeType === DOM_ATTRIBUTE_NODE);
852+
for (const previouslyTransformedAttribute of transformedChildNodes) {
853+
const name = previouslyTransformedAttribute.transformedNodeName;
854+
const value = previouslyTransformedAttribute.transformedNodeValue;
855+
domSetTransformedAttribute(newNode, name, value);
856+
}
857+
858+
// Case 2: attributes that existed as a source attribute and were transformed.
859+
const transformedAttributes = node.childNodes.filter((n) => n.nodeType === DOM_ATTRIBUTE_NODE && n.transformedNodeName)
847860
for (const previouslyTransformedAttribute of transformedAttributes) {
848861
const name = previouslyTransformedAttribute.transformedNodeName;
849862
const value = previouslyTransformedAttribute.transformedNodeValue;
@@ -864,7 +877,7 @@ export class Xslt {
864877
outputNode.transformedChildNodes.length - 1,
865878
++elementContext.outputDepth
866879
);
867-
await this.xsltChildNodes(clonedContext, template, output);
880+
await this.xsltChildNodes(clonedContext, template);
868881
} else {
869882
// This applies also to the DOCUMENT_NODE of the XSL stylesheet,
870883
// so we don't have to treat it specially.

tests/lmht/html-to-lmht.test.tsx

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,28 +1523,28 @@ describe('HTML to LMHT', () => {
15231523
const xmlString =
15241524
'<!DOCTYPE html>' +
15251525
`<html lang="en">
1526-
<head>
1527-
<meta name="description" content="LMHT">
1528-
<meta name="keywords" content="HTML, LMHT, Desenvolvimento, Web">
1529-
<meta name="author" content="Leonel Sanches da Silva">
1530-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
1531-
<!-- <meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content=""><meta name="author" content=""> -->
1532-
<title>About - Simple Blog Template</title>
1533-
<!-- Bootstrap Core CSS -->
1534-
<link href="css/bootstrap.min.css" rel="stylesheet">
1535-
<!-- Custom CSS -->
1536-
<link href="css/simple-blog-template.css" rel="stylesheet">
1537-
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
1538-
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
1539-
<!--[if lt IE 9]>
1540-
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
1541-
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
1542-
<![endif]-->
1543-
</head>
1544-
<body>
1545-
<p class="anything">This is a paragraph with a class</p>
1546-
</body>
1547-
</html>
1526+
<head>
1527+
<meta name="description" content="LMHT">
1528+
<meta name="keywords" content="HTML, LMHT, Desenvolvimento, Web">
1529+
<meta name="author" content="Leonel Sanches da Silva">
1530+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
1531+
<!-- <meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content=""><meta name="author" content=""> -->
1532+
<title>About - Simple Blog Template</title>
1533+
<!-- Bootstrap Core CSS -->
1534+
<link href="css/bootstrap.min.css" rel="stylesheet">
1535+
<!-- Custom CSS -->
1536+
<link href="css/simple-blog-template.css" rel="stylesheet">
1537+
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
1538+
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
1539+
<!--[if lt IE 9]>
1540+
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
1541+
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
1542+
<![endif]-->
1543+
</head>
1544+
<body>
1545+
<p class="anything">This is a paragraph with a class</p>
1546+
</body>
1547+
</html>
15481548
`;
15491549

15501550
const expectedOutString = `<lmht>` +

tests/xslt/for-each.test.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { XmlParser } from '../../src/dom';
2+
import { Xslt } from '../../src/xslt';
3+
4+
import assert from 'assert';
5+
6+
describe('xsl:for-each', () => {
7+
const xmlString = (
8+
`<all>
9+
<item pos="2">A</item>
10+
<item pos="3">B</item>
11+
<item pos="1">C</item>
12+
</all>`
13+
);
14+
15+
it('handles for-each sort', async () => {
16+
const xsltForEachSort = (
17+
`<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
18+
<xsl:template match="/">
19+
<xsl:for-each select="//item">
20+
<xsl:sort select="@pos" />
21+
<xsl:value-of select="." />
22+
</xsl:for-each>
23+
</xsl:template>
24+
</xsl:stylesheet>`
25+
);
26+
27+
const xsltClass = new Xslt();
28+
const xmlParser = new XmlParser();
29+
const xml = xmlParser.xmlParse(xmlString);
30+
const xslt = xmlParser.xmlParse(xsltForEachSort);
31+
const html = await xsltClass.xsltProcess(xml, xslt);
32+
assert.equal(html, 'CAB');
33+
});
34+
35+
it('handles for-each sort ascending', async () => {
36+
const xsltForEachSortAscending = (
37+
`<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
38+
<xsl:template match="/">
39+
<xsl:for-each select="//item">
40+
<xsl:sort select="." order="ascending" />
41+
<xsl:value-of select="." />
42+
</xsl:for-each>
43+
</xsl:template>
44+
</xsl:stylesheet>`
45+
);
46+
47+
const xsltClass = new Xslt();
48+
const xmlParser = new XmlParser();
49+
const xml = xmlParser.xmlParse(xmlString);
50+
const xslt = xmlParser.xmlParse(xsltForEachSortAscending);
51+
const html = await xsltClass.xsltProcess(xml, xslt);
52+
assert.equal(html, 'ABC');
53+
});
54+
55+
it('handles for-each sort descending', async () => {
56+
const xsltForEachSortDescending = (
57+
`<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
58+
<xsl:template match="/">
59+
<xsl:for-each select="//item">
60+
<xsl:sort select="." order="descending" />
61+
<xsl:value-of select="." />
62+
</xsl:for-each>
63+
</xsl:template>
64+
</xsl:stylesheet>`
65+
);
66+
67+
const xsltClass = new Xslt();
68+
const xmlParser = new XmlParser();
69+
const xml = xmlParser.xmlParse(xmlString);
70+
const xslt = xmlParser.xmlParse(xsltForEachSortDescending);
71+
const html = await xsltClass.xsltProcess(xml, xslt);
72+
assert.equal(html, 'CBA');
73+
});
74+
});

0 commit comments

Comments
 (0)