Skip to content

Commit 3119382

Browse files
generate-id (#57)
* Using `xsl:output` tag. * - Implementing `generate-id()` XPath method; - Each node now has a random id; - Using `cyrb53` method to generate the id based on the node id.
1 parent 90399b6 commit 3119382

File tree

8 files changed

+159
-27
lines changed

8 files changed

+159
-27
lines changed

.eslintrc.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ module.exports = {
8585
'no-array-constructor': 'error',
8686
'no-async-promise-executor': 'error',
8787
'no-await-in-loop': 'error',
88-
'no-bitwise': 'error',
8988
'no-buffer-constructor': 'error',
9089
'no-caller': 'error',
9190
'no-case-declarations': 'off',
@@ -115,7 +114,6 @@ module.exports = {
115114
'no-loop-func': 'error',
116115
'no-magic-numbers': 'off',
117116
'no-misleading-character-class': 'error',
118-
'no-mixed-operators': 'error',
119117
'no-mixed-requires': 'error',
120118
'no-multi-assign': 'error',
121119
'no-multi-spaces': 'error',

src/dom/functions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export function xmlParse(xml: any) {
128128
} else {
129129
// VersionInfo is missing, or unknown version number.
130130
// TODO : Fallback to XML 1.0 or XML 1.1, or just return null?
131-
throw 'VersionInfo is missing, or unknown version number.';
131+
throw new Error('VersionInfo is missing, or unknown version number.');
132132
}
133133
} else {
134134
// When an XML declaration is missing it's an XML 1.0 document.

src/dom/xnode.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { XDocument } from './xdocument';
99

1010
// operate on native DOM nodes.
1111
export class XNode {
12+
id: number;
1213
attributes: XNode[];
1314
childNodes: XNode[];
1415
nodeType: any;
@@ -45,6 +46,7 @@ export class XNode {
4546
static _unusedXNodes: any[] = [];
4647

4748
constructor(type: any, name: any, opt_value: any, opt_owner: any, opt_namespace?: any) {
49+
this.id = Math.random() * (Number.MAX_SAFE_INTEGER - 1) + 1;
4850
this.attributes = [];
4951
this.childNodes = [];
5052
this.transformedAttributes = [];

src/xpath/expressions/function-call-expr.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,49 @@ export class FunctionCallExpr extends Expression {
1111
name: any;
1212
args: any[];
1313

14+
private cyrb53(str: string, seed = 0) {
15+
let h1 = 0xdeadbeef ^ seed;
16+
let h2 = 0x41c6ce57 ^ seed;
17+
18+
for(let i = 0, ch: any; i < str.length; i++) {
19+
ch = str.charCodeAt(i);
20+
h1 = Math.imul(h1 ^ ch, 2654435761);
21+
h2 = Math.imul(h2 ^ ch, 1597334677);
22+
}
23+
24+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
25+
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
26+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
27+
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
28+
29+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
30+
}
31+
1432
xpathfunctions = {
15-
last(ctx) {
33+
last(ctx: ExprContext) {
1634
assert(this.args.length == 0);
1735
// NOTE(mesch): XPath position starts at 1.
1836
return new NumberValue(ctx.contextSize());
1937
},
2038

21-
position(ctx) {
39+
position(ctx: ExprContext) {
2240
assert(this.args.length == 0);
2341
// NOTE(mesch): XPath position starts at 1.
2442
return new NumberValue(ctx.position + 1);
2543
},
2644

27-
count(ctx) {
45+
count(ctx: ExprContext) {
2846
assert(this.args.length == 1);
2947
const v = this.args[0].evaluate(ctx);
3048
return new NumberValue(v.nodeSetValue().length);
3149
},
3250

33-
'generate-id'(_ctx) {
34-
throw 'not implmented yet: XPath function generate-id()';
51+
'generate-id'(_ctx: ExprContext) {
52+
return new StringValue(
53+
'A' + this.cyrb53(
54+
JSON.stringify(_ctx.nodelist[_ctx.position].id)
55+
)
56+
);
3557
},
3658

3759
id(ctx: ExprContext) {
@@ -382,7 +404,7 @@ export class FunctionCallExpr extends Expression {
382404
this.args = [];
383405
}
384406

385-
appendArg(arg) {
407+
appendArg(arg: any) {
386408
this.args.push(arg);
387409
}
388410

src/xpath/expressions/step-expr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export class StepExpr extends Expression {
112112
nodelist.push(n);
113113
}
114114
} else if (this.axis == xpathAxis.NAMESPACE) {
115-
throw 'not implemented: axis namespace';
115+
throw new Error('not implemented: axis namespace');
116116
} else if (this.axis == xpathAxis.PARENT) {
117117
if (input.parentNode) {
118118
nodelist.push(input.parentNode);

src/xslt.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
} from './constants';
4242
import { Expression } from './xpath/expressions/expression';
4343
import { StringValue, NodeSetValue } from './xpath/values';
44+
import { LocationExpr } from './xpath/expressions';
4445

4546
/**
4647
* The main class for XSL-T processing. The implementation is NOT
@@ -68,9 +69,13 @@ import { StringValue, NodeSetValue } from './xpath/values';
6869
*/
6970
export class Xslt {
7071
xPath: XPath;
72+
outputMethod: string;
73+
outputOmitXmlDeclaration: string;
7174

7275
constructor() {
7376
this.xPath = new XPath();
77+
this.outputMethod = "xml";
78+
this.outputOmitXmlDeclaration = "no";
7479
}
7580

7681
/**
@@ -273,10 +278,8 @@ export class Xslt {
273278
case 'otherwise':
274279
throw `error if here: ${template.localName}`;
275280
case 'output':
276-
// Ignored. -- Since we operate on the DOM, and all further use
277-
// of the output of the XSL transformation is determined by the
278-
// browser that we run in, this parameter is not applicable to
279-
// this implementation.
281+
this.outputMethod = xmlGetAttribute(template, 'method');
282+
this.outputOmitXmlDeclaration = xmlGetAttribute(template, 'omit-xml-declaration');
280283
break;
281284
case 'preserve-space':
282285
throw `not implemented: ${template.localName}`;
@@ -656,32 +659,41 @@ export class Xslt {
656659
protected xsltMatch(match: string, context: ExprContext) {
657660
const expr = this.xPath.xPathParse(match);
658661

659-
if (expr.steps.length <= 0) {
662+
if (expr instanceof LocationExpr) {
663+
return this.xsltLocationExpressionMatch(match, expr, context);
664+
}
665+
666+
// TODO: Other expressions
667+
return true;
668+
}
669+
670+
private xsltLocationExpressionMatch(match: string, expression: LocationExpr, context: ExprContext) {
671+
if (expression === undefined || expression.steps === undefined || expression.steps.length <= 0) {
660672
throw new Error('Error resolving XSLT match: Location Expression should have steps.');
661673
}
662674

663-
const firstStep = expr.steps[0];
675+
const firstStep = expression.steps[0];
664676

665677
// Shortcut for the most common case.
666678
if (
667-
expr.steps &&
668-
!expr.absolute &&
669-
expr.steps.length == 1 &&
679+
expression.steps &&
680+
!expression.absolute &&
681+
expression.steps.length == 1 &&
670682
firstStep.axis == 'child' &&
671683
firstStep.predicate.length === 0
672684
) {
673685
return firstStep.nodetest.evaluate(context).booleanValue();
674686
}
675687

676-
if (expr.absolute && firstStep.axis !== 'self') {
688+
if (expression.absolute && firstStep.axis !== 'self') {
677689
// TODO: `xPathCollectDescendants()`?
678690
const levels = match.split('/');
679691
if (levels.length > 1) {
680-
return this.absoluteXsltMatch(levels, expr, context);
692+
return this.absoluteXsltMatch(levels, expression, context);
681693
}
682694
}
683695

684-
return this.relativeXsltMatch(expr, context);
696+
return this.relativeXsltMatch(expression, context);
685697
}
686698

687699
/**

tests/xpath/functions.test.tsx

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/* eslint-disable no-undef */
2+
3+
// Copyright 2023 Design Liquido
4+
// All Rights Reserved.
5+
6+
import assert from 'assert';
7+
8+
import { dom } from 'isomorphic-jsx';
9+
import React from 'react';
10+
import { xmlParse } from '../../src/dom';
11+
import { Xslt } from '../../src/xslt';
12+
13+
// Just touching the `dom`, otherwise Babel prunes the import.
14+
console.log(dom);
15+
16+
describe('XPath Functions', () => {
17+
it('generate-id, trivial', () => {
18+
const xml = xmlParse(<root></root>);
19+
const xsltDefinition = xmlParse(
20+
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
21+
<xsl:template match="/">
22+
<xsl:attribute name="uid">
23+
<xsl:value-of select="generate-id(.)"/>
24+
</xsl:attribute>
25+
</xsl:template>
26+
</xsl:stylesheet>
27+
);
28+
29+
const xsltClass = new Xslt();
30+
31+
const outXmlString = xsltClass.xsltProcess(
32+
xml,
33+
xsltDefinition
34+
);
35+
36+
assert.ok(outXmlString);
37+
});
38+
39+
it.skip('generate-id, complete', () => {
40+
const xsltDefinition = xmlParse(
41+
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
42+
<xsl:output method="xml" omit-xml-declaration="yes"/>
43+
44+
<xsl:template match="chapter | sect1 | sect2">
45+
<xsl:copy>
46+
<xsl:attribute name="uid">
47+
<xsl:value-of select="generate-id(.)"/>
48+
</xsl:attribute>
49+
<xsl:apply-templates select="@*|node()"/>
50+
</xsl:copy>
51+
</xsl:template>
52+
53+
<xsl:template match="@*|node()">
54+
<xsl:copy>
55+
<xsl:apply-templates select="@*|node()"/>
56+
</xsl:copy>
57+
</xsl:template>
58+
59+
</xsl:stylesheet>
60+
);
61+
62+
const xml = xmlParse(
63+
<chapter>
64+
<para>Then with expanded wings he steers his flight</para>
65+
<figure>
66+
<title>"Incumbent on the Dusky Air"</title>
67+
<graphic fileref="pic1.jpg"/>
68+
</figure>
69+
<para>Aloft, incumbent on the dusky Air</para>
70+
<sect1>
71+
<para>That felt unusual weight, till on dry Land</para>
72+
<figure>
73+
<title>"He Lights"</title>
74+
<graphic fileref="pic2.jpg"/>
75+
</figure>
76+
<para>He lights, if it were Land that ever burned</para>
77+
<sect2>
78+
<para>With solid, as the Lake with liquid fire</para>
79+
<figure>
80+
<title>"The Lake with Liquid Fire"</title>
81+
<graphic fileref="pic3.jpg"/>
82+
</figure>
83+
</sect2>
84+
</sect1>
85+
</chapter>
86+
);
87+
88+
const xsltClass = new Xslt();
89+
90+
const outXmlString = xsltClass.xsltProcess(
91+
xml,
92+
xsltDefinition
93+
);
94+
95+
console.log(outXmlString)
96+
assert.ok(!outXmlString);
97+
});
98+
});

tests/xpath.test.tsx renamed to tests/xpath/xpath.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ import assert from 'assert';
1616
import { dom } from 'isomorphic-jsx';
1717
import React from 'react';
1818

19-
import { ExprContext, XPath } from '../src/xpath';
20-
import { xmlParse, xmlValue } from '../src/dom';
21-
import { BooleanValue } from '../src/xpath/values/boolean-value';
22-
import { NumberValue } from '../src/xpath/values/number-value';
23-
import { StringValue } from '../src/xpath/values/string-value';
19+
import { ExprContext, XPath } from '../../src/xpath';
20+
import { xmlParse, xmlValue } from '../../src/dom';
21+
import { BooleanValue } from '../../src/xpath/values/boolean-value';
22+
import { NumberValue } from '../../src/xpath/values/number-value';
23+
import { StringValue } from '../../src/xpath/values/string-value';
2424

2525
// Just touching the `dom`, otherwise Babel prunes the import.
2626
console.log(dom);

0 commit comments

Comments
 (0)