Skip to content

Commit bbcb340

Browse files
authored
Merge pull request #172 from Pike/BaseNode.equals
BaseNode.equals support, and other ast goodness from python-fluent
2 parents 1a0cd74 + 0eaa60b commit bbcb340

File tree

5 files changed

+313
-1
lines changed

5 files changed

+313
-1
lines changed

fluent-syntax/src/ast.js

+62-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,69 @@
55
* Annotation.
66
*
77
*/
8-
class BaseNode {
8+
export class BaseNode {
99
constructor() {}
10+
11+
equals(other, ignoredFields = ["span"]) {
12+
const thisKeys = new Set(Object.keys(this));
13+
const otherKeys = new Set(Object.keys(other));
14+
if (ignoredFields) {
15+
for (const fieldName of ignoredFields) {
16+
thisKeys.delete(fieldName);
17+
otherKeys.delete(fieldName);
18+
}
19+
}
20+
if (thisKeys.size !== otherKeys.size) {
21+
return false;
22+
}
23+
for (const fieldName of thisKeys) {
24+
if (!otherKeys.has(fieldName)) {
25+
return false;
26+
}
27+
const thisVal = this[fieldName];
28+
const otherVal = other[fieldName];
29+
if (typeof thisVal !== typeof otherVal) {
30+
return false;
31+
}
32+
if (thisVal instanceof Array) {
33+
if (thisVal.length !== otherVal.length) {
34+
return false;
35+
}
36+
for (let i = 0; i < thisVal.length; ++i) {
37+
if (!scalarsEqual(thisVal[i], otherVal[i], ignoredFields)) {
38+
return false;
39+
}
40+
}
41+
} else if (!scalarsEqual(thisVal, otherVal, ignoredFields)) {
42+
return false;
43+
}
44+
}
45+
return true;
46+
}
47+
48+
clone() {
49+
function visit(value) {
50+
if (value instanceof BaseNode) {
51+
return value.clone();
52+
}
53+
if (Array.isArray(value)) {
54+
return value.map(visit);
55+
}
56+
return value;
57+
}
58+
const clone = Object.create(this.constructor.prototype);
59+
for (const prop of Object.keys(this)) {
60+
clone[prop] = visit(this[prop]);
61+
}
62+
return clone;
63+
}
64+
}
65+
66+
function scalarsEqual(thisVal, otherVal, ignoredFields) {
67+
if (thisVal instanceof BaseNode) {
68+
return thisVal.equals(otherVal, ignoredFields);
69+
}
70+
return thisVal === otherVal;
1071
}
1172

1273
/*

fluent-syntax/src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import FluentSerializer from "./serializer";
33

44
export * from "./ast";
55
export { FluentParser, FluentSerializer };
6+
export * from "./visitor";
67

78
export function parse(source, opts) {
89
const parser = new FluentParser(opts);

fluent-syntax/src/visitor.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { BaseNode } from "./ast";
2+
3+
/*
4+
* Abstract Visitor pattern
5+
*/
6+
export class Visitor {
7+
visit(node) {
8+
if (Array.isArray(node)) {
9+
node.forEach(child => this.visit(child));
10+
return;
11+
}
12+
if (!(node instanceof BaseNode)) {
13+
return;
14+
}
15+
const visit = this[`visit${node.type}`] || this.genericVisit;
16+
visit.call(this, node);
17+
}
18+
19+
genericVisit(node) {
20+
for (const propname of Object.keys(node)) {
21+
this.visit(node[propname]);
22+
}
23+
}
24+
}
25+
26+
/*
27+
* Abstract Transformer pattern
28+
*/
29+
export class Transformer extends Visitor {
30+
visit(node) {
31+
if (!(node instanceof BaseNode)) {
32+
return node;
33+
}
34+
const visit = this[`visit${node.type}`] || this.genericVisit;
35+
return visit.call(this, node);
36+
}
37+
38+
genericVisit(node) {
39+
for (const propname of Object.keys(node)) {
40+
const propvalue = node[propname];
41+
if (Array.isArray(propvalue)) {
42+
const newvals = propvalue
43+
.map(child => this.visit(child))
44+
.filter(newchild => newchild !== undefined);
45+
node[propname] = newvals;
46+
}
47+
if (propvalue instanceof BaseNode) {
48+
const new_val = this.visit(propvalue);
49+
if (new_val === undefined) {
50+
delete node[propname];
51+
} else {
52+
node[propname] = new_val;
53+
}
54+
}
55+
}
56+
return node;
57+
}
58+
}

fluent-syntax/test/ast_test.js

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"use strict";
2+
3+
import assert from "assert";
4+
import { ftl } from "./util";
5+
import { FluentParser } from "../src";
6+
import * as AST from "../src/ast";
7+
8+
suite("BaseNode.equals", function() {
9+
setup(function() {
10+
this.parser = new FluentParser();
11+
});
12+
test("Identifier.equals", function() {
13+
const thisNode = new AST.Identifier("name");
14+
const otherNode = new AST.Identifier("name");
15+
assert.ok(thisNode.clone() instanceof AST.Identifier);
16+
assert.strictEqual(thisNode.equals(otherNode), true);
17+
assert.strictEqual(thisNode.equals(thisNode.clone()), true);
18+
assert.notStrictEqual(thisNode, thisNode.clone());
19+
});
20+
test("Node.type", function() {
21+
const thisNode = new AST.Identifier("name");
22+
const otherNode = new AST.StringLiteral("name");
23+
assert.strictEqual(thisNode.equals(otherNode), false);
24+
});
25+
test("Array children", function() {
26+
const thisNode = new AST.Pattern([
27+
new AST.TextElement("one"),
28+
new AST.TextElement("two"),
29+
new AST.TextElement("three"),
30+
]);
31+
let otherNode = new AST.Pattern([
32+
new AST.TextElement("one"),
33+
new AST.TextElement("two"),
34+
new AST.TextElement("three"),
35+
]);
36+
assert.strictEqual(thisNode.equals(otherNode), true);
37+
});
38+
test("Variant order matters", function() {
39+
const thisRes = this.parser.parse(ftl`
40+
msg = { $val ->
41+
[few] things
42+
[1] one
43+
*[other] default
44+
}
45+
`);
46+
const otherRes = this.parser.parse(ftl`
47+
msg = { $val ->
48+
[few] things
49+
*[other] default
50+
[1] one
51+
}
52+
`);
53+
const thisNode = thisRes.body[0];
54+
const otherNode = otherRes.body[0];
55+
assert.strictEqual(thisNode.equals(otherNode), false);
56+
assert.strictEqual(thisRes.equals(thisRes.clone(), []), true);
57+
assert.notStrictEqual(thisRes, thisRes.clone());
58+
});
59+
test("Attribute order matters", function() {
60+
const thisRes = this.parser.parse(ftl`
61+
msg =
62+
.attr1 = one
63+
.attr2 = two
64+
`);
65+
const otherRes = this.parser.parse(ftl`
66+
msg =
67+
.attr2 = two
68+
.attr1 = one
69+
`);
70+
const thisNode = thisRes.body[0];
71+
const otherNode = otherRes.body[0];
72+
assert.strictEqual(thisNode.equals(otherNode), false);
73+
});
74+
});

fluent-syntax/test/visitor_test.js

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"use strict";
2+
3+
import assert from "assert";
4+
import { ftl } from "./util";
5+
import { FluentParser, Visitor, Transformer } from "../src";
6+
7+
suite("Visitor", function() {
8+
setup(function() {
9+
const parser = new FluentParser();
10+
this.resource = parser.parse(ftl`
11+
one = Message
12+
# Comment
13+
two = Messages
14+
three = Messages with
15+
.an = Attribute
16+
`);
17+
});
18+
test("Mock Visitor", function() {
19+
class MockVisitor extends Visitor {
20+
constructor() {
21+
super();
22+
this.calls = {};
23+
this.pattern_calls = 0;
24+
}
25+
genericVisit(node) {
26+
const nodename = node.type;
27+
if (nodename in this.calls) {
28+
this.calls[nodename]++;
29+
} else {
30+
this.calls[nodename] = 1;
31+
}
32+
super.genericVisit(node);
33+
}
34+
visitPattern(node) {
35+
this.pattern_calls++;
36+
}
37+
}
38+
const mv = new MockVisitor();
39+
mv.visit(this.resource);
40+
assert.strictEqual(mv.pattern_calls, 4);
41+
assert.deepStrictEqual(
42+
mv.calls,
43+
{
44+
'Resource': 1,
45+
'Comment': 1,
46+
'Message': 3,
47+
'Identifier': 4,
48+
'Attribute': 1,
49+
'Span': 10,
50+
}
51+
)
52+
});
53+
test("WordCount", function() {
54+
class VisitorCounter extends Visitor {
55+
constructor() {
56+
super();
57+
this.word_count = 0;
58+
}
59+
genericVisit(node) {
60+
switch (node.type) {
61+
case 'Span':
62+
case 'Annotation':
63+
break;
64+
default:
65+
super.genericVisit(node);
66+
}
67+
}
68+
visitTextElement(node) {
69+
this.word_count += node.value.split(/\s+/).length;
70+
}
71+
}
72+
const vc = new VisitorCounter();
73+
vc.visit(this.resource);
74+
assert.strictEqual(vc.word_count, 5);
75+
})
76+
});
77+
78+
suite("Transformer", function() {
79+
setup(function() {
80+
const parser = new FluentParser();
81+
this.resource = parser.parse(ftl`
82+
one = Message
83+
# Comment
84+
two = Messages
85+
three = Messages with
86+
.an = Attribute
87+
`);
88+
});
89+
test("ReplaceTransformer", function() {
90+
class ReplaceTransformer extends Transformer {
91+
constructor(before, after) {
92+
super();
93+
this.before = before;
94+
this.after = after;
95+
}
96+
genericVisit(node) {
97+
switch (node.type) {
98+
case 'Span':
99+
case 'Annotation':
100+
return node;
101+
break;
102+
default:
103+
return super.genericVisit(node);
104+
}
105+
}
106+
visitTextElement(node) {
107+
node.value = node.value.replace(this.before, this.after);
108+
return node;
109+
}
110+
}
111+
const resource = this.resource.clone()
112+
const transformed = new ReplaceTransformer('Message', 'Term').visit(resource);
113+
assert.notStrictEqual(resource, this.resource);
114+
assert.strictEqual(resource, transformed);
115+
assert.strictEqual(this.resource.equals(transformed), false);
116+
assert.strictEqual(transformed.body[1].value.elements[0].value, 'Terms');
117+
});
118+
});

0 commit comments

Comments
 (0)