Skip to content

Commit 386a869

Browse files
Bug fix reported in DesignLiquido/lmht-js#3.
1 parent aecf8b2 commit 386a869

File tree

7 files changed

+208
-158
lines changed

7 files changed

+208
-158
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.git/
2+
.idea/
23

34
coverage/
45
demo/

.vscode/launch.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"--runInBand",
1616
"--testTimeout=100000000"
1717
],
18+
"smartStep": true,
1819
"skipFiles": ["<node_internals>/**", "node_modules/**"],
1920
"console": "integratedTerminal",
2021
"internalConsoleOptions": "neverOpen"

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@
6666
"rollup-plugin-node-resolve": "^4.0.0",
6767
"rollup-plugin-terser": "^4.0.3",
6868
"ts-jest": "^29.1.5",
69-
"ts-node": "^10.9.1",
70-
"typescript": "^5.5.3"
69+
"ts-node": "^10.9.2",
70+
"typescript": "^4.9.5"
7171
},
7272
"dependencies": {
7373
"he": "^1.2.0",

src/xpath/match-resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class MatchResolver {
4242
if (expression.absolute) {
4343
// If expression is absolute and the axis of first step is self,
4444
// the match starts by the #document node (for instance, `<xsl:template match="/">`).
45-
// Otherwise (axis === 'child'), the match stasts on the first
45+
// Otherwise (axis === 'child'), the match starts on the first
4646
// child of #document node.
4747
const firstStep = expression.steps[0];
4848
if (firstStep.axis === 'self') {

src/xslt/xslt.ts

Lines changed: 154 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export class Xslt {
133133
}
134134

135135
await this.xsltProcessContext(expressionContext, stylesheet, this.outputDocument);
136-
const transformedOutputXml = xmlTransformedText(outputDocument, {
136+
const transformedOutputXml: string = xmlTransformedText(outputDocument, {
137137
cData: this.options.cData,
138138
escape: this.options.escape,
139139
selfClosingTags: this.options.selfClosingTags,
@@ -160,8 +160,6 @@ export class Xslt {
160160
select: any,
161161
value: any,
162162
nodes: any,
163-
mode: any,
164-
templates: any,
165163
paramContext: any,
166164
commentData: any,
167165
commentNode: any,
@@ -172,129 +170,10 @@ export class Xslt {
172170
case 'apply-imports':
173171
throw new Error(`not implemented: ${template.localName}`);
174172
case 'apply-templates':
175-
select = xmlGetAttribute(template, 'select');
176-
if (select) {
177-
nodes = this.xPath.xPathEval(select, context).nodeSetValue();
178-
} else {
179-
nodes = context.nodeList[context.position].childNodes;
180-
}
181-
182-
// TODO: Check why apply-templates was sorting and filing parameters
183-
// automatically.
184-
/* this.xsltWithParam(sortContext, template);
185-
this.xsltSort(sortContext, template); */
186-
187-
mode = xmlGetAttribute(template, 'mode');
188-
top = template.ownerDocument.documentElement;
189-
190-
templates = [];
191-
for (let element of top.childNodes.filter(
192-
(c: XNode) => c.nodeType == DOM_ELEMENT_NODE && this.isXsltElement(c, 'template')
193-
)) {
194-
// Actual template should be executed.
195-
// `<xsl:apply-templates>` should have an ancestor `<xsl:template>`
196-
// for comparison.
197-
const templateAncestor = template.getAncestorByLocalName('template');
198-
if (templateAncestor === undefined) {
199-
continue;
200-
}
201-
202-
if (templateAncestor.id === element.id) {
203-
continue;
204-
}
205-
206-
if (!mode || element.getAttributeValue('mode') === mode) {
207-
templates.push(element);
208-
}
209-
}
210-
211-
const modifiedContext = context.clone(nodes);
212-
for (let i = 0; i < templates.length; ++i) {
213-
for (let j = 0; j < modifiedContext.contextSize(); ++j) {
214-
// If the current node is text, there's no need to test all the templates
215-
// against it. Just appending it to its parent is fine.
216-
if (modifiedContext.nodeList[j].nodeType === DOM_TEXT_NODE) {
217-
const textNodeContext = context.clone(
218-
[modifiedContext.nodeList[j]],
219-
undefined,
220-
0,
221-
undefined
222-
);
223-
// TODO: verify if it is okay to pass the own text node as template.
224-
this.commonLogicTextNode(textNodeContext, modifiedContext.nodeList[j], output);
225-
} else {
226-
const clonedContext = modifiedContext.clone(
227-
[modifiedContext.nodeList[j]],
228-
undefined,
229-
0,
230-
undefined
231-
);
232-
clonedContext.inApplyTemplates = true;
233-
// The output depth should be restarted, since
234-
// another template is being applied from this point.
235-
clonedContext.outputDepth = 0;
236-
await this.xsltProcessContext(clonedContext, templates[i], output);
237-
}
238-
}
239-
}
240-
173+
await this.xsltApplyTemplates(context, template, output);
241174
break;
242175
case 'attribute':
243-
nameExpr = xmlGetAttribute(template, 'name');
244-
name = this.xsltAttributeValue(nameExpr, context);
245-
246-
const documentFragment = domCreateDocumentFragment(this.outputDocument);
247-
await this.xsltChildNodes(context, template, documentFragment);
248-
value = xmlValue2(documentFragment);
249-
250-
if (output.nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
251-
domSetTransformedAttribute(output, name, value);
252-
} else {
253-
let sourceNode = context.nodeList[context.position];
254-
let parentSourceNode = sourceNode.parentNode;
255-
let outputNode = sourceNode.outputNode;
256-
257-
// At this point, the output node should exist.
258-
// If not, a new node is created.
259-
if (outputNode === null || outputNode === undefined) {
260-
outputNode = new XNode(
261-
sourceNode.nodeType,
262-
sourceNode.nodeName,
263-
sourceNode.nodeValue,
264-
context.outputNodeList[context.outputPosition],
265-
sourceNode.namespaceUri
266-
);
267-
sourceNode.outputNode = outputNode;
268-
}
269-
270-
// Corner case:
271-
// It can happen here that we don't have the root node set.
272-
// In this case we need to append a copy of the root
273-
// source node to receive the attribute.
274-
if (outputNode.localName === '#document') {
275-
const sourceRootNode = context.root.childNodes[0];
276-
const newRootNode = domCreateElement(this.outputDocument, sourceRootNode.nodeName);
277-
newRootNode.transformedNodeName = sourceRootNode.nodeName;
278-
newRootNode.transformedLocalName = sourceRootNode.localName;
279-
domAppendTransformedChild(outputNode, newRootNode);
280-
outputNode = newRootNode;
281-
parentSourceNode = newRootNode;
282-
}
283-
284-
// If the parent transformation is something like `xsl:element`, we should
285-
// add a copy of the attribute to this element.
286-
domSetTransformedAttribute(output, name, value);
287-
288-
// Some operations start by the tag attributes, and not by the tag itself.
289-
// When this is the case, the output node is not set yet, so
290-
// we add the transformed attributes into the original tag.
291-
if (parentSourceNode && parentSourceNode.outputNode) {
292-
domSetTransformedAttribute(parentSourceNode.outputNode, name, value);
293-
} else {
294-
domSetTransformedAttribute(parentSourceNode, name, value);
295-
}
296-
}
297-
176+
await this.xsltAttribute(context, template, output);
298177
break;
299178
case 'attribute-set':
300179
throw new Error(`not implemented: ${template.localName}`);
@@ -494,6 +373,153 @@ export class Xslt {
494373
}
495374
}
496375

376+
/**
377+
* Implements `xsl:apply-templates`.
378+
* @param context The Expression Context.
379+
* @param template The template.
380+
* @param output The output. Only used if there's no corresponding output node already defined.
381+
* @protected
382+
*/
383+
protected async xsltApplyTemplates(context: ExprContext, template: XNode, output?: XNode) {
384+
const getAllTemplates = (top: XNode, template: XNode, mode: string | null) => {
385+
let templates = [];
386+
for (let element of top.childNodes.filter(
387+
(c: XNode) => c.nodeType == DOM_ELEMENT_NODE && this.isXsltElement(c, 'template')
388+
)) {
389+
// TODO: Remember why this logic was here.
390+
// In the past the idea was to avoid executing the same matcher repeatedly,
391+
// but this proved to be a *terrible* idea some time later.
392+
// Will keep this code for a few more versions, then remove it.
393+
/* const templateAncestor = template.getAncestorByLocalName('template');
394+
if (templateAncestor === undefined) {
395+
continue;
396+
}
397+
398+
if (templateAncestor.id === element.id) {
399+
continue;
400+
} */
401+
402+
if (!mode || element.getAttributeValue('mode') === mode) {
403+
templates.push(element);
404+
}
405+
}
406+
407+
return templates;
408+
}
409+
410+
const select = xmlGetAttribute(template, 'select');
411+
let nodes: XNode[] = [];
412+
if (select) {
413+
nodes = this.xPath.xPathEval(select, context).nodeSetValue();
414+
} else {
415+
nodes = context.nodeList[context.position].childNodes;
416+
}
417+
418+
// TODO: Check why apply-templates was sorting and filing parameters
419+
// automatically.
420+
/* this.xsltWithParam(sortContext, template);
421+
this.xsltSort(sortContext, template); */
422+
423+
const mode: string | null = xmlGetAttribute(template, 'mode');
424+
const top = template.ownerDocument.documentElement;
425+
426+
const templates = getAllTemplates(top, template, mode);
427+
428+
const modifiedContext = context.clone(nodes);
429+
for (let i = 0; i < templates.length; ++i) {
430+
for (let j = 0; j < modifiedContext.contextSize(); ++j) {
431+
// If the current node is text, there's no need to test all the templates
432+
// against it. Just appending it to its parent is fine.
433+
if (modifiedContext.nodeList[j].nodeType === DOM_TEXT_NODE) {
434+
const textNodeContext = context.clone(
435+
[modifiedContext.nodeList[j]],
436+
undefined,
437+
0,
438+
undefined
439+
);
440+
// TODO: verify if it is okay to pass the own text node as template.
441+
this.commonLogicTextNode(textNodeContext, modifiedContext.nodeList[j], output);
442+
} else {
443+
const clonedContext = modifiedContext.clone(
444+
[modifiedContext.nodeList[j]],
445+
undefined,
446+
0,
447+
undefined
448+
);
449+
clonedContext.inApplyTemplates = true;
450+
// The output depth should be restarted, since
451+
// another template is being applied from this point.
452+
clonedContext.outputDepth = 0;
453+
await this.xsltProcessContext(clonedContext, templates[i], output);
454+
}
455+
}
456+
}
457+
}
458+
459+
/**
460+
* Implements `xsl:attribute`.
461+
* @param context The Expression Context.
462+
* @param template The template.
463+
* @param output The output. Only used if there's no corresponding output node already defined.
464+
* @protected
465+
*/
466+
protected async xsltAttribute(context: ExprContext, template: XNode, output?: XNode) {
467+
const nameExpr = xmlGetAttribute(template, 'name');
468+
const name = this.xsltAttributeValue(nameExpr, context);
469+
470+
const documentFragment = domCreateDocumentFragment(this.outputDocument);
471+
await this.xsltChildNodes(context, template, documentFragment);
472+
const value = xmlValue2(documentFragment);
473+
474+
if (output.nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
475+
domSetTransformedAttribute(output, name, value);
476+
} else {
477+
let sourceNode = context.nodeList[context.position];
478+
let parentSourceNode = sourceNode.parentNode;
479+
let outputNode = sourceNode.outputNode;
480+
481+
// At this point, the output node should exist.
482+
// If not, a new node is created.
483+
if (outputNode === null || outputNode === undefined) {
484+
outputNode = new XNode(
485+
sourceNode.nodeType,
486+
sourceNode.nodeName,
487+
sourceNode.nodeValue,
488+
context.outputNodeList[context.outputPosition],
489+
sourceNode.namespaceUri
490+
);
491+
sourceNode.outputNode = outputNode;
492+
}
493+
494+
// Corner case:
495+
// It can happen here that we don't have the root node set.
496+
// In this case we need to append a copy of the root
497+
// source node to receive the attribute.
498+
if (outputNode.localName === '#document') {
499+
const sourceRootNode = context.root.childNodes[0];
500+
const newRootNode = domCreateElement(this.outputDocument, sourceRootNode.nodeName);
501+
newRootNode.transformedNodeName = sourceRootNode.nodeName;
502+
newRootNode.transformedLocalName = sourceRootNode.localName;
503+
domAppendTransformedChild(outputNode, newRootNode);
504+
outputNode = newRootNode;
505+
parentSourceNode = newRootNode;
506+
}
507+
508+
// If the parent transformation is something like `xsl:element`, we should
509+
// add a copy of the attribute to this element.
510+
domSetTransformedAttribute(output, name, value);
511+
512+
// Some operations start by the tag attributes, and not by the tag itself.
513+
// When this is the case, the output node is not set yet, so
514+
// we add the transformed attributes into the original tag.
515+
if (parentSourceNode && parentSourceNode.outputNode) {
516+
domSetTransformedAttribute(parentSourceNode.outputNode, name, value);
517+
} else {
518+
domSetTransformedAttribute(parentSourceNode, name, value);
519+
}
520+
}
521+
}
522+
497523
/**
498524
* Implements `xsl:choose`, its child nodes `xsl:when`, and
499525
* `xsl:otherwise`.
@@ -579,7 +605,7 @@ export class Xslt {
579605

580606
/**
581607
* Implements `xsl:for-each`.
582-
* @param input The Expression Context.
608+
* @param context The Expression Context.
583609
* @param template The template.
584610
* @param output The output.
585611
*/
@@ -609,7 +635,7 @@ export class Xslt {
609635

610636
/**
611637
* Implements `xsl:include`.
612-
* @param input The Expression Context.
638+
* @param context The Expression Context.
613639
* @param template The template.
614640
* @param output The output.
615641
*/
@@ -756,6 +782,7 @@ export class Xslt {
756782
* - `xsltProcessContext`, `apply-templates` operation, when the current node is text.
757783
* @param context The Expression Context.
758784
* @param template The template, that contains the node value to be written.
785+
* @param output The output.
759786
*/
760787
private commonLogicTextNode(context: ExprContext, template: XNode, output: XNode) {
761788
if (output.nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
@@ -882,7 +909,7 @@ export class Xslt {
882909
return false;
883910
}
884911

885-
protected xsltAttribute(attributeName: string, context: ExprContext): XNode {
912+
protected findAttributeInContext(attributeName: string, context: ExprContext): XNode {
886913
return context.nodeList[context.position].childNodes.find(
887914
(a: XNode) => a.nodeType === DOM_ATTRIBUTE_NODE && a.nodeName === attributeName
888915
);

0 commit comments

Comments
 (0)