forked from meteor/meteor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstringify.js
118 lines (105 loc) · 3.44 KB
/
stringify.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// The "tree string" format is a simple format for representing syntax trees.
//
// For example, the parse of `x++;` is written as:
// "program(expressionStmnt(postfix(identifier(x) ++) ;))"
//
// A Node is written as "name(item1 item2 item3)", with additional whitespace
// allowed anywhere between the name, parentheses, and items.
//
// Tokens don't need to be escaped unless they contain '(', ')', whitespace, or
// backticks, or are empty. If they do, they can be written enclosed in backticks.
// To escape a backtick within backticks, double it.
//
// `stringify` generates "canonical" tree strings, which have no extra escaping
// or whitespace, just one space between items in a Node.
ParseNode.prototype.stringify = function () {
return ParseNode.stringify(this);
};
var backtickEscape = function (str) {
if (/[\s()`]/.test(str))
return '`' + str.replace(/`/g, '``') + '`';
else if (! str)
return '``';
else
return str;
};
var backtickUnescape = function (str) {
if (str.charAt(0) === '`') {
if (str.length === 1 || str.slice(-1) !== '`')
throw new Error("Mismatched ` in " + str);
if (str.length === 2)
str = '';
else
str = str.slice(1, -1).replace(/``/g, '`');
}
return str;
};
ParseNode.stringify = function (tree) {
if (tree instanceof ParseNode) {
var str = backtickEscape(tree.name);
str += '(';
var escapedChildren = [];
for(var i = 0, N = tree.children.length; i < N; i++)
escapedChildren.push(ParseNode.stringify(tree.children[i]));
str += escapedChildren.join(' ');
str += ')';
return str;
}
// Treat a token object or string as a token.
if (typeof tree.text === 'function')
tree = tree.text();
else if (typeof tree.text === 'string')
tree = tree.text;
return backtickEscape(String(tree));
};
ParseNode.unstringify = function (str) {
var lexemes = str.match(/\(|\)|`([^`]||``)*`|`|[^\s()`]+/g) || [];
var N = lexemes.length;
var state = {
i: 0,
getParseError: function (expecting) {
throw new Error("unstringify: Expecting " + expecting +", found " +
(lexemes[this.i] || "end of string"));
},
peek: function () { return lexemes[this.i]; },
advance: function () { this.i++; }
};
var paren = function (chr) {
return new Parser(chr, function (t) {
if (t.peek() !== chr)
return null;
t.advance();
return chr;
});
};
var EMPTY_STRING = [""];
var token = new Parser('token', function (t) {
var txt = t.peek();
if (!txt || txt.charAt(0) === '(' || txt.charAt(0) === ')')
return null;
t.advance();
// can't return falsy value from successful parser
return backtickUnescape(txt) || EMPTY_STRING;
});
// Make "item" lazy so it can be recursive.
var item = Parsers.lazy('token', function () { return item; });
// Parse a single node or token.
item = Parsers.mapResult(
Parsers.seq(token,
Parsers.opt(Parsers.seq(
paren('('), Parsers.opt(Parsers.list(item)), paren(')')))),
function (v) {
for(var i = 0, N = v.length; i < N; i++)
if (v[i] === EMPTY_STRING)
v[i] = "";
if (v.length === 1)
// token
return v[0];
// node. exclude parens
return new ParseNode(v[0], v.slice(2, -1));
});
var endOfString = new Parser("end of string", function (t) {
return t.i === N ? [] : null;
});
return Parsers.seq(item, endOfString).parseRequired(state)[0];
};