Skip to content

Commit

Permalink
Add Visitor and Transformer APIs from python's fluent.syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
Pike committed Feb 27, 2019
1 parent b7109e7 commit 387bc7e
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 1 deletion.
2 changes: 1 addition & 1 deletion fluent-syntax/src/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Annotation.
*
*/
class BaseNode {
export class BaseNode {
constructor() {}

equals(other, ignoredFields = ["span"]) {
Expand Down
1 change: 1 addition & 0 deletions fluent-syntax/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import FluentSerializer from "./serializer";

export * from "./ast";
export { FluentParser, FluentSerializer };
export * from "./visitor";

export function parse(source, opts) {
const parser = new FluentParser(opts);
Expand Down
60 changes: 60 additions & 0 deletions fluent-syntax/src/visitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { BaseNode } from "./ast";

/*
* Abstract Visitor pattern
*/
export class Visitor {
visit(node) {
if (Array.isArray(node)) {
node.forEach(child => this.visit(child));
return;
}
if (!(node instanceof BaseNode)) {
return;
}
const nodename = node.type;
const visit = this[`visit_${nodename}`] || this.generic_visit;
visit.call(this, node);
}

generic_visit(node) {
for (const propname of Object.keys(node)) {
this.visit(node[propname]);
}
}
}

/*
* Abstract Transformer pattern
*/
export class Transformer extends Visitor {
visit(node) {
if (!(node instanceof BaseNode)) {
return node;
}
const nodename = node.type;
const visit = this[`visit_${nodename}`] || this.generic_visit;
return visit.call(this, node);
}

generic_visit(node) {
for (const propname of Object.keys(node)) {
const propvalue = node[propname];
if (Array.isArray(propvalue)) {
const newvals = propvalue
.map(child => this.visit(child))
.filter(newchild => newchild !== undefined);
node[propname] = newvals;
}
if (propvalue instanceof BaseNode) {
const new_val = this.visit(propvalue);
if (new_val === undefined) {
delete node[propname];
} else {
node[propname] = new_val;
}
}
}
return node;
}
}
118 changes: 118 additions & 0 deletions fluent-syntax/test/visitor_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"use strict";

import assert from "assert";
import { ftl } from "./util";
import { FluentParser, Visitor, Transformer } from "../src";

suite("Visitor", function() {
setup(function() {
const parser = new FluentParser();
this.resource = parser.parse(ftl`
one = Message
# Comment
two = Messages
three = Messages with
.an = Attribute
`);
});
test("Mock Visitor", function() {
class MockVisitor extends Visitor {
constructor() {
super();
this.calls = {};
this.pattern_calls = 0;
}
generic_visit(node) {
const nodename = node.type;
if (nodename in this.calls) {
this.calls[nodename]++;
} else {
this.calls[nodename] = 1;
}
super.generic_visit(node);
}
visit_Pattern(node) {
this.pattern_calls++;
}
}
const mv = new MockVisitor();
mv.visit(this.resource);
assert.strictEqual(mv.pattern_calls, 4);
assert.deepStrictEqual(
mv.calls,
{
'Resource': 1,
'Comment': 1,
'Message': 3,
'Identifier': 4,
'Attribute': 1,
'Span': 10,
}
)
});
test("WordCount", function() {
class VisitorCounter extends Visitor {
constructor() {
super();
this.word_count = 0;
}
generic_visit(node) {
switch (node.type) {
case 'Span':
case 'Annotation':
break;
default:
super.generic_visit(node);
}
}
visit_TextElement(node) {
this.word_count += node.value.split(/\s+/).length;
}
}
const vc = new VisitorCounter();
vc.visit(this.resource);
assert.strictEqual(vc.word_count, 5);
})
});

suite("Transformer", function() {
setup(function() {
const parser = new FluentParser();
this.resource = parser.parse(ftl`
one = Message
# Comment
two = Messages
three = Messages with
.an = Attribute
`);
});
test("ReplaceTransformer", function() {
class ReplaceTransformer extends Transformer {
constructor(before, after) {
super();
this.before = before;
this.after = after;
}
generic_visit(node) {
switch (node.type) {
case 'Span':
case 'Annotation':
return node;
break;
default:
return super.generic_visit(node);
}
}
visit_TextElement(node) {
node.value = node.value.replace(this.before, this.after);
return node;
}
}
const resource = this.resource.clone()
const transformed = new ReplaceTransformer('Message', 'Term').visit(resource);
assert.notStrictEqual(resource, this.resource);
assert.strictEqual(resource, transformed);
assert.strictEqual(this.resource.equals(transformed), false);
assert.strictEqual(transformed.body[1].value.elements[0].value, 'Terms');
});
});

0 comments on commit 387bc7e

Please sign in to comment.