1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * Parser for a simple grammar that describes a tree structure using a function-
7 * like "a(b(c,d))" syntax. Original intended usage: to have browsertests
8 * specify an arbitrary tree of iframes, loaded from various sites, without
9 * having to write a .html page for each level or do crazy feats of data: url
10 * escaping. But there's nothing really iframe-specific here. See below for some
11 * examples of the grammar and the parser output.
13 * @example <caption>Basic syntax: an identifier followed by arg list.</caption>
14 * TreeParserUtil.parse('abc ()'); // returns { value: 'abc', children: [] }
16 * @example <caption>The arg list is optional. Dots are legal in ids.</caption>
17 * TreeParserUtil.parse('b.com'); // returns { value: 'b.com', children: [] }
19 * @example <caption>Commas separate children in the arg list.</caption>
20 * // returns { value: 'b', children: [
21 * // { value: 'c', children: [] },
22 * // { value: 'd', children: [] }
24 * TreeParserUtil.parse('b (c, d)';
26 * @example <caption>Children can have children, and so on.</caption>
27 * // returns { value: 'e', children: [
28 * // { value: 'f', children: [
29 * // { value: 'g', children: [
30 * // { value: 'h', children: [] },
31 * // { value: 'i', children: [
32 * // { value: 'j', children: [] }
37 * TreeParserUtil.parse('e(f(g(h(),i(j))))';
39 * @example <caption>flatten() converts a [sub]tree back to a string.</caption>
40 * var tree = TreeParserUtil.parse('b.com (c.com(e.com), d.com)');
41 * TreeParserUtil.flatten(tree.children[0]); // returns 'c.com(e.com())'
43 var TreeParserUtil = (function() {
47 * Parses an input string into a tree. See class comment for examples.
48 * @returns A tree of the form {value: <string>, children: <Array.<tree>>}.
50 function parse(input) {
51 var tokenStream = lex(input);
53 var result = takeIdAndChild(tokenStream);
54 if (tokenStream.length != 0)
55 throw new Error('Expected end of stream, but found "' +
56 tokenStream[0] + '".')
61 * Inverse of |parse|. Converts a parsed tree object into a string. Can be
62 * used to forward a subtree as an argument to a nested document.
64 function flatten(tree) {
65 return tree.value + '(' + tree.children.map(flatten).join(',') + ')';
69 * Lexer function to convert an input string into a token stream. Splits the
70 * input along whitespace, parens and commas. Whitespace is removed, while
71 * parens and commas are preserved as standalone tokens.
73 * @param {string} input The input string.
74 * @return {Array.<string>} The resulting token stream.
77 return input.split(/(\s+|\(|\)|,)/).reduce(
78 function (resultArray, token) {
79 var trimmed = token.trim();
81 resultArray.push(trimmed);
89 * Consumes from the stream an identifier and optional child list, returning
90 * its parsed representation.
92 function takeIdAndChild(tokenStream) {
93 return { value: takeIdentifier(tokenStream),
94 children: takeChildList(tokenStream) };
98 * Consumes from the stream an identifier, returning its value (as a string).
100 function takeIdentifier(tokenStream) {
101 if (tokenStream.length == 0)
102 throw new Error('Expected an identifier, but found end-of-stream.');
103 var token = tokenStream.shift();
104 if (!token.match(/[a-zA-Z0-9.-]+/))
105 throw new Error('Expected an identifier, but found "' + token + '".');
110 * Consumes an optional child list from the token stream, returning a list of
111 * the parsed children.
113 function takeChildList(tokenStream) {
114 // Remove the next token from the stream if it matches |token|.
115 function tryToEatA(token) {
116 if (tokenStream[0] == token) {
123 // Bare identifier case, as in 'b' in the input '(a (b, c))'
127 // Empty list case, as in 'b' in the input 'a (b (), c)'.
128 if (tryToEatA(')')) {
132 // List with at least one entry.
133 var result = [ takeIdAndChild(tokenStream) ];
135 // Additional entries allowed with commas.
136 while (tryToEatA(',')) {
137 result.push(takeIdAndChild(tokenStream));
141 if (tryToEatA(')')) {
144 if (tokenStream.length == 0)
145 throw new Error('Expected ")" or ",", but found end-of-stream.');
146 throw new Error('Expected ")" or ",", but found "' + tokenStream[0] + '".');