Files
meteor/packages/jsparse/stringify.js
David Glasser 5a5204e3a4 Remove closures around package files. (hint: git blame -w)
The bundler now adds closures around client files and the server adds closures
around server files.
2013-03-19 15:13:53 -07:00

119 lines
3.4 KiB
JavaScript

// 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];
};