Skip to content

Commit 387bc7e

Browse files
committed
Add Visitor and Transformer APIs from python's fluent.syntax
1 parent b7109e7 commit 387bc7e

File tree

4 files changed

+180
-1
lines changed

4 files changed

+180
-1
lines changed

fluent-syntax/src/ast.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Annotation.
66
*
77
*/
8-
class BaseNode {
8+
export class BaseNode {
99
constructor() {}
1010

1111
equals(other, ignoredFields = ["span"]) {

fluent-syntax/src/index.js

Lines changed: 1 addition & 0 deletions
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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 nodename = node.type;
16+
const visit = this[`visit_${nodename}`] || this.generic_visit;
17+
visit.call(this, node);
18+
}
19+
20+
generic_visit(node) {
21+
for (const propname of Object.keys(node)) {
22+
this.visit(node[propname]);
23+
}
24+
}
25+
}
26+
27+
/*
28+
* Abstract Transformer pattern
29+
*/
30+
export class Transformer extends Visitor {
31+
visit(node) {
32+
if (!(node instanceof BaseNode)) {
33+
return node;
34+
}
35+
const nodename = node.type;
36+
const visit = this[`visit_${nodename}`] || this.generic_visit;
37+
return visit.call(this, node);
38+
}
39+
40+
generic_visit(node) {
41+
for (const propname of Object.keys(node)) {
42+
const propvalue = node[propname];
43+
if (Array.isArray(propvalue)) {
44+
const newvals = propvalue
45+
.map(child => this.visit(child))
46+
.filter(newchild => newchild !== undefined);
47+
node[propname] = newvals;
48+
}
49+
if (propvalue instanceof BaseNode) {
50+
const new_val = this.visit(propvalue);
51+
if (new_val === undefined) {
52+
delete node[propname];
53+
} else {
54+
node[propname] = new_val;
55+
}
56+
}
57+
}
58+
return node;
59+
}
60+
}

fluent-syntax/test/visitor_test.js

Lines changed: 118 additions & 0 deletions
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+
generic_visit(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.generic_visit(node);
33+
}
34+
visit_Pattern(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+
generic_visit(node) {
60+
switch (node.type) {
61+
case 'Span':
62+
case 'Annotation':
63+
break;
64+
default:
65+
super.generic_visit(node);
66+
}
67+
}
68+
visit_TextElement(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+
generic_visit(node) {
97+
switch (node.type) {
98+
case 'Span':
99+
case 'Annotation':
100+
return node;
101+
break;
102+
default:
103+
return super.generic_visit(node);
104+
}
105+
}
106+
visit_TextElement(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)