From 200534597620cc818b9cd24b032c661fd770e138 Mon Sep 17 00:00:00 2001 From: David Majda Date: Sun, 23 Mar 2014 13:29:54 +0100 Subject: [PATCH] Complete rewrite of the CSS example grammar This is a complete rewrite of the CSS example grammar. It is now based on CSS 2.1 *including the errata* and the generated parser builds a nicer syntax tree. There is also a number of cleanups, formatting changes, naming changes, and bug fixes. Beside this, the rewrite reflects how I write grammars today (as opposed to few years ago) and what style I would recommend to others. --- examples/css.pegjs | 480 +++++++++++++++++---------------------------- 1 file changed, 177 insertions(+), 303 deletions(-) diff --git a/examples/css.pegjs b/examples/css.pegjs index 45e21b9..a246e2f 100644 --- a/examples/css.pegjs +++ b/examples/css.pegjs @@ -1,48 +1,83 @@ /* - * CSS parser based on the grammar described at http://www.w3.org/TR/CSS2/grammar.html. + * CSS Grammar + * =========== * - * The parser builds a tree representing the parsed CSS, composed of basic - * JavaScript values, arrays and objects (basically JSON). It can be easily - * used by various CSS processors, transformers, etc. + * Based on grammar from CSS 2.1 specification [1] (including the errata [2]). + * Generated parser builds a syntax tree composed of nested JavaScript objects, + * vaguely inspired by CSS DOM [3]. The CSS DOM itself wasn't used as it is not + * expressive enough (e.g. selectors are reflected as text, not structured + * objects) and somewhat cumbersome. * - * Note that the parser does not handle errors in CSS according to the - * specification -- many errors which it should recover from (e.g. malformed - * declarations or unexpected end of stylesheet) are simply fatal. This is a - * result of straightforward rewrite of the CSS grammar to PEG.js and it should - * be fixed sometimes. + * Limitations: + * + * * Many errors which should be recovered from according to the specification + * (e.g. malformed declarations or unexpected end of stylesheet) are fatal. + * This is a result of straightforward rewrite of the CSS grammar to PEG.js. + * + * [1] http://www.w3.org/TR/2011/REC-CSS2-20110607 + * [2] http://www.w3.org/Style/css2-updates/REC-CSS2-20110607-errata.html + * [3] http://www.w3.org/TR/DOM-Level-2-Style/css.html */ -/* ===== Syntactical Elements ===== */ +{ + function extractOptional(optional, index) { + return optional ? optional[index] : null; + } + + function extractList(list, index) { + var result = [], i; + + for (i = 0; i < list.length; i++) { + if (list[i][index] !== null) { + result.push(list[i][index]); + } + } + + return result; + } + + function buildList(first, rest, index) { + return (first !== null ? [first] : []).concat(extractList(rest, index)); + } + + function buildExpression(first, rest) { + var result = first, i; + + for (i = 0; i < rest.length; i++) { + result = { + type: "Expression", + operator: rest[i][0], + left: result, + right: rest[i][1] + }; + } + + return result; + } +} start = stylesheet:stylesheet comment* { return stylesheet; } +/* ----- G.1 Grammar ----- */ + stylesheet = charset:(CHARSET_SYM STRING ";")? (S / CDO / CDC)* imports:(import (CDO S* / CDC S*)*)* - rules:((ruleset / media / page) (CDO S* / CDC S*)*)* { - var importsConverted = []; - for (var i = 0; i < imports.length; i++) { - importsConverted.push(imports[i][0]); - } - - var rulesConverted = []; - for (i = 0; i < rules.length; i++) { - rulesConverted.push(rules[i][0]); - } - + rules:((ruleset / media / page) (CDO S* / CDC S*)*)* + { return { - type: "stylesheet", - charset: charset !== null ? charset[1] : null, - imports: importsConverted, - rules: rulesConverted + type: "StyleSheet", + charset: extractOptional(charset, 1), + imports: extractList(imports, 0), + rules: extractList(rules, 0) }; } import = IMPORT_SYM S* href:(STRING / URI) S* media:media_list? ";" S* { return { - type: "import_rule", + type: "ImportRule", href: href, media: media !== null ? media : [] }; @@ -51,46 +86,34 @@ import media = MEDIA_SYM S* media:media_list "{" S* rules:ruleset* "}" S* { return { - type: "media_rule", + type: "MediaRule", media: media, rules: rules }; } media_list - = head:medium tail:("," S* medium)* { - var result = [head]; - for (var i = 0; i < tail.length; i++) { - result.push(tail[i][2]); - } - return result; - } + = first:medium rest:("," S* medium)* { return buildList(first, rest, 2); } medium - = ident:IDENT S* { return ident; } + = name:IDENT S* { return name; } page - = PAGE_SYM S* qualifier:pseudo_page? + = PAGE_SYM S* selector:pseudo_page? "{" S* - declarationsHead:declaration? - declarationsTail:(";" S* declaration?)* - "}" S* { - var declarations = declarationsHead !== null ? [declarationsHead] : []; - for (var i = 0; i < declarationsTail.length; i++) { - if (declarationsTail[i][2] !== null) { - declarations.push(declarationsTail[i][2]); - } - } - + declarationsFirst:declaration? + declarationsRest:(";" S* declaration?)* + "}" S* + { return { - type: "page_rule", - qualifier: qualifier, - declarations: declarations + type: "PageRule", + selector: selector, + declarations: buildList(declarationsFirst, declarationsRest, 2) }; } pseudo_page - = ":" ident:IDENT S* { return ident; } + = ":" value:IDENT S* { return { type: "PseudoSelector", value: value }; } operator = "/" S* { return "/"; } @@ -100,51 +123,36 @@ combinator = "+" S* { return "+"; } / ">" S* { return ">"; } -unary_operator - = "+" - / "-" - property - = ident:IDENT S* { return ident; } + = name:IDENT S* { return name; } ruleset - = selectorsHead:selector - selectorsTail:("," S* selector)* + = selectorsFirst:selector + selectorsRest:("," S* selector)* "{" S* - declarationsHead:declaration? - declarationsTail:(";" S* declaration?)* - "}" S* { - var selectors = [selectorsHead]; - for (var i = 0; i < selectorsTail.length; i++) { - selectors.push(selectorsTail[i][2]); - } - - var declarations = declarationsHead !== null ? [declarationsHead] : []; - for (i = 0; i < declarationsTail.length; i++) { - if (declarationsTail[i][2] !== null) { - declarations.push(declarationsTail[i][2]); - } - } - + declarationsFirst:declaration? + declarationsRest:(";" S* declaration?)* + "}" S* + { return { - type: "ruleset", - selectors: selectors, - declarations: declarations + type: "RuleSet", + selectors: buildList(selectorsFirst, selectorsRest, 2), + declarations: buildList(declarationsFirst, declarationsRest, 2) }; } selector = left:simple_selector S* combinator:combinator right:selector { return { - type: "selector", + type: "Selector", combinator: combinator, left: left, right: right }; } - / left:simple_selector S* right:selector { + / left:simple_selector S+ right:selector { return { - type: "selector", + type: "Selector", combinator: " ", left: left, right: right @@ -153,51 +161,42 @@ selector / selector:simple_selector S* { return selector; } simple_selector - = element:element_name - qualifiers:( - id:HASH { return { type: "ID selector", id: id.substr(1) }; } - / class - / attrib - / pseudo - )* { + = element:element_name qualifiers:(id / class / attrib / pseudo)* { return { - type: "simple_selector", + type: "SimpleSelector", element: element, qualifiers: qualifiers }; } - / qualifiers:( - id:HASH { return { type: "ID selector", id: id.substr(1) }; } - / class - / attrib - / pseudo - )+ { + / qualifiers:(id / class / attrib / pseudo)+ { return { - type: "simple_selector", + type: "SimpleSelector", element: "*", qualifiers: qualifiers }; } +id + = id:HASH { return { type: "IDSelector", id: id }; } + class - = "." class_:IDENT { return { type: "class_selector", "class": class_ }; } + = "." class_:IDENT { return { type: "ClassSelector", class: class_ }; } element_name - = IDENT / '*' + = IDENT + / "*" attrib = "[" S* attribute:IDENT S* - operatorAndValue:( - ('=' / INCLUDES / DASHMATCH) S* - (IDENT / STRING) S* - )? - "]" { + operatorAndValue:(("=" / INCLUDES / DASHMATCH) S* (IDENT / STRING) S*)? + "]" + { return { - type: "attribute_selector", + type: "AttributeSelector", attribute: attribute, - operator: operatorAndValue !== null ? operatorAndValue[0] : null, - value: operatorAndValue !== null ? operatorAndValue[2] : null + operator: extractOptional(operatorAndValue, 0), + value: extractOptional(operatorAndValue, 2) }; } @@ -206,31 +205,22 @@ pseudo value:( name:FUNCTION S* params:(IDENT S*)? ")" { return { - type: "function", + type: "Function", name: name, params: params !== null ? [params[0]] : [] }; } / IDENT - ) { - /* - * The returned object has somewhat vague property names and values because - * the rule matches both pseudo-classes and pseudo-elements (they look the - * same at the syntactic level). - */ - return { - type: "pseudo_selector", - value: value - }; - } + ) + { return { type: "PseudoSelector", value: value }; } declaration - = property:property ":" S* expression:expr important:prio? { + = name:property ':' S* value:expr prio:prio? { return { - type: "declaration", - property: property, - expression: expression, - important: important !== null ? true : false + type: "Declaration", + name: name, + value: value, + important: prio !== null }; } @@ -238,63 +228,41 @@ prio = IMPORTANT_SYM S* expr - = head:term tail:(operator? term)* { - var result = head; - for (var i = 0; i < tail.length; i++) { - result = { - type: "expression", - operator: tail[i][0], - left: result, - right: tail[i][1] - }; - } - return result; - } + = first:term rest:(operator? term)* { return buildExpression(first, rest); } term - = operator:unary_operator? - value:( - EMS S* - / EXS S* - / LENGTH S* - / ANGLE S* - / TIME S* - / FREQ S* - / PERCENTAGE S* - / NUMBER S* - ) { + = quantity:(PERCENTAGE / LENGTH / EMS / EXS / ANGLE / TIME / FREQ / NUMBER) + S* + { return { - type: "value", - value: (operator !== null ? operator : "") + value[0] + type: "Quantity", + value: quantity.value, + unit: quantity.unit }; } - / value:URI S* { return { type: "uri", value: value }; } + / value:STRING S* { return { type: "String", value: value }; } + / value:URI S* { return { type: "URI", value: value }; } / function / hexcolor - / value:STRING S* { return { type: "string", value: value }; } - / value:IDENT S* { return { type: "ident", value: value }; } + / value:IDENT S* { return { type: "Ident", value: value }; } function = name:FUNCTION S* params:expr ")" S* { - return { - type: "function", - name: name, - params: params - }; + return { type: "Function", name: name, params: params }; } hexcolor - = value:HASH S* { return { type: "hexcolor", value: value}; } + = value:HASH S* { return { type: "Hexcolor", value: value }; } -/* ===== Lexical Elements ===== */ +/* ----- G.2 Lexical scanner ----- */ /* Macros */ h - = [0-9a-fA-F] + = [0-9a-f]i nonascii - = [\x80-\xFF] + = [\x80-\uFFFF] unicode = "\\" digits:$(h h? h? h? h? h?) ("\r\n" / [ \t\r\n\f])? { @@ -303,31 +271,25 @@ unicode escape = unicode - / "\\" char_:[^\r\n\f0-9a-fA-F] { return char_; } + / "\\" ch:[^\r\n\f0-9a-f]i { return ch; } nmstart - = [_a-zA-Z] + = [_a-z]i / nonascii / escape nmchar - = [_a-zA-Z0-9-] + = [_a-z0-9-]i / nonascii / escape -integer - = parts:$[0-9]+ { return parts; } - -float - = parts:$([0-9]* "." [0-9]+) { return parts; } - string1 - = '"' chars:([^\n\r\f\\"] / "\\" nl:nl { return nl } / escape)* '"' { + = '"' chars:([^\n\r\f\\"] / "\\" nl:nl { return ""; } / escape)* '"' { return chars.join(""); } string2 - = "'" chars:([^\n\r\f\\'] / "\\" nl:nl { return nl } / escape)* "'" { + = "'" chars:([^\n\r\f\\'] / "\\" nl:nl { return ""; } / escape)* "'" { return chars.join(""); } @@ -335,23 +297,24 @@ comment = "/*" [^*]* "*"+ ([^/*] [^*]* "*"+)* "/" ident - = dash:"-"? nmstart:nmstart nmchars:nmchar* { - return (dash !== null ? dash : "") + nmstart + nmchars.join(""); + = prefix:$"-"? start:nmstart chars:nmchar* { + return prefix + start + chars.join(""); } name - = nmchars:nmchar+ { return nmchars.join(""); } + = chars:nmchar+ { return chars.join(""); } num - = float - / integer + = [+-]? ([0-9]+ / [0-9]* "." [0-9]+) ("e" [+-]? [0-9]+)? { + return parseFloat(text()); + } string = string1 / string2 url - = chars:([!#$%&*-~] / nonascii / escape)* { return chars.join(""); } + = chars:([!#$%&*-\[\]-~] / nonascii / escape)* { return chars.join(""); } s = [ \t\r\n\f]+ @@ -365,115 +328,25 @@ nl / "\r" / "\f" -A - = [aA] - / "\\" "0"? "0"? "0"? "0"? "41" ("\r\n" / [ \t\r\n\f])? { return "A"; } - / "\\" "0"? "0"? "0"? "0"? "61" ("\r\n" / [ \t\r\n\f])? { return "a"; } - -C - = [cC] - / "\\" "0"? "0"? "0"? "0"? "43" ("\r\n" / [ \t\r\n\f])? { return "C"; } - / "\\" "0"? "0"? "0"? "0"? "63" ("\r\n" / [ \t\r\n\f])? { return "c"; } - -D - = [dD] - / "\\" "0"? "0"? "0"? "0"? "44" ("\r\n" / [ \t\r\n\f])? { return "D"; } - / "\\" "0"? "0"? "0"? "0"? "64" ("\r\n" / [ \t\r\n\f])? { return "d"; } - -E - = [eE] - / "\\" "0"? "0"? "0"? "0"? "45" ("\r\n" / [ \t\r\n\f])? { return "E"; } - / "\\" "0"? "0"? "0"? "0"? "65" ("\r\n" / [ \t\r\n\f])? { return "e"; } - -G - = [gG] - / "\\" "0"? "0"? "0"? "0"? "47" ("\r\n" / [ \t\r\n\f])? { return "G"; } - / "\\" "0"? "0"? "0"? "0"? "67" ("\r\n" / [ \t\r\n\f])? { return "g"; } - / "\\" char_:[gG] { return char_; } - -H - = h:[hH] - / "\\" "0"? "0"? "0"? "0"? "48" ("\r\n" / [ \t\r\n\f])? { return "H"; } - / "\\" "0"? "0"? "0"? "0"? "68" ("\r\n" / [ \t\r\n\f])? { return "h"; } - / "\\" char_:[hH] { return char_; } - -I - = i:[iI] - / "\\" "0"? "0"? "0"? "0"? "49" ("\r\n" / [ \t\r\n\f])? { return "I"; } - / "\\" "0"? "0"? "0"? "0"? "69" ("\r\n" / [ \t\r\n\f])? { return "i"; } - / "\\" char_:[iI] { return char_; } - -K - = [kK] - / "\\" "0"? "0"? "0"? "0"? "4" [bB] ("\r\n" / [ \t\r\n\f])? { return "K"; } - / "\\" "0"? "0"? "0"? "0"? "6" [bB] ("\r\n" / [ \t\r\n\f])? { return "k"; } - / "\\" char_:[kK] { return char_; } - -L - = [lL] - / "\\" "0"? "0"? "0"? "0"? "4" [cC] ("\r\n" / [ \t\r\n\f])? { return "L"; } - / "\\" "0"? "0"? "0"? "0"? "6" [cC] ("\r\n" / [ \t\r\n\f])? { return "l"; } - / "\\" char_:[lL] { return char_; } - -M - = [mM] - / "\\" "0"? "0"? "0"? "0"? "4" [dD] ("\r\n" / [ \t\r\n\f])? { return "M"; } - / "\\" "0"? "0"? "0"? "0"? "6" [dD] ("\r\n" / [ \t\r\n\f])? { return "m"; } - / "\\" char_:[mM] { return char_; } - -N - = [nN] - / "\\" "0"? "0"? "0"? "0"? "4" [eE] ("\r\n" / [ \t\r\n\f])? { return "N"; } - / "\\" "0"? "0"? "0"? "0"? "6" [eE] ("\r\n" / [ \t\r\n\f])? { return "n"; } - / "\\" char_:[nN] { return char_; } - -O - = [oO] - / "\\" "0"? "0"? "0"? "0"? "4" [fF] ("\r\n" / [ \t\r\n\f])? { return "O"; } - / "\\" "0"? "0"? "0"? "0"? "6" [fF] ("\r\n" / [ \t\r\n\f])? { return "o"; } - / "\\" char_:[oO] { return char_; } - -P - = [pP] - / "\\" "0"? "0"? "0"? "0"? "50" ("\r\n" / [ \t\r\n\f])? { return "P"; } - / "\\" "0"? "0"? "0"? "0"? "70" ("\r\n" / [ \t\r\n\f])? { return "p"; } - / "\\" char_:[pP] { return char_; } - -R - = [rR] - / "\\" "0"? "0"? "0"? "0"? "52" ("\r\n" / [ \t\r\n\f])? { return "R"; } - / "\\" "0"? "0"? "0"? "0"? "72" ("\r\n" / [ \t\r\n\f])? { return "r"; } - / "\\" char_:[rR] { return char_; } - -S_ - = [sS] - / "\\" "0"? "0"? "0"? "0"? "53" ("\r\n" / [ \t\r\n\f])? { return "S"; } - / "\\" "0"? "0"? "0"? "0"? "73" ("\r\n" / [ \t\r\n\f])? { return "s"; } - / "\\" char_:[sS] { return char_; } - -T - = [tT] - / "\\" "0"? "0"? "0"? "0"? "54" ("\r\n" / [ \t\r\n\f])? { return "T"; } - / "\\" "0"? "0"? "0"? "0"? "74" ("\r\n" / [ \t\r\n\f])? { return "t"; } - / "\\" char_:[tT] { return char_; } - -U - = [uU] - / "\\" "0"? "0"? "0"? "0"? "55" ("\r\n" / [ \t\r\n\f])? { return "U"; } - / "\\" "0"? "0"? "0"? "0"? "75" ("\r\n" / [ \t\r\n\f])? { return "u"; } - / "\\" char_:[uU] { return char_; } - -X - = [xX] - / "\\" "0"? "0"? "0"? "0"? "58" ("\r\n" / [ \t\r\n\f])? { return "X"; } - / "\\" "0"? "0"? "0"? "0"? "78" ("\r\n" / [ \t\r\n\f])? { return "x"; } - / "\\" char_:[xX] { return char_; } - -Z - = [zZ] - / "\\" "0"? "0"? "0"? "0"? "5" [aA] ("\r\n" / [ \t\r\n\f])? { return "Z"; } - / "\\" "0"? "0"? "0"? "0"? "7" [aA] ("\r\n" / [ \t\r\n\f])? { return "z"; } - / "\\" char_:[zZ] { return char_; } +A = "a"i / "\\" "0"? "0"? "0"? "0"? [\x41\x61] ("\r\n" / [ \t\r\n\f])? { return "a"; } +C = "c"i / "\\" "0"? "0"? "0"? "0"? [\x43\x63] ("\r\n" / [ \t\r\n\f])? { return "c"; } +D = "d"i / "\\" "0"? "0"? "0"? "0"? [\x44\x64] ("\r\n" / [ \t\r\n\f])? { return "d"; } +E = "e"i / "\\" "0"? "0"? "0"? "0"? [\x45\x65] ("\r\n" / [ \t\r\n\f])? { return "e"; } +G = "g"i / "\\" "0"? "0"? "0"? "0"? [\x47\x67] ("\r\n" / [ \t\r\n\f])? / "\\g"i { return "g"; } +H = "h"i / "\\" "0"? "0"? "0"? "0"? [\x48\x68] ("\r\n" / [ \t\r\n\f])? / "\\h"i { return "h"; } +I = "i"i / "\\" "0"? "0"? "0"? "0"? [\x49\x69] ("\r\n" / [ \t\r\n\f])? / "\\i"i { return "i"; } +K = "k"i / "\\" "0"? "0"? "0"? "0"? [\x4b\x6b] ("\r\n" / [ \t\r\n\f])? / "\\k"i { return "k"; } +L = "l"i / "\\" "0"? "0"? "0"? "0"? [\x4c\x6c] ("\r\n" / [ \t\r\n\f])? / "\\l"i { return "l"; } +M = "m"i / "\\" "0"? "0"? "0"? "0"? [\x4d\x6d] ("\r\n" / [ \t\r\n\f])? / "\\m"i { return "m"; } +N = "n"i / "\\" "0"? "0"? "0"? "0"? [\x4e\x6e] ("\r\n" / [ \t\r\n\f])? / "\\n"i { return "n"; } +O = "o"i / "\\" "0"? "0"? "0"? "0"? [\x4f\x6f] ("\r\n" / [ \t\r\n\f])? / "\\o"i { return "o"; } +P = "p"i / "\\" "0"? "0"? "0"? "0"? [\x50\x70] ("\r\n" / [ \t\r\n\f])? / "\\p"i { return "p"; } +R = "r"i / "\\" "0"? "0"? "0"? "0"? [\x52\x72] ("\r\n" / [ \t\r\n\f])? / "\\r"i { return "r"; } +S_ = "s"i / "\\" "0"? "0"? "0"? "0"? [\x53\x73] ("\r\n" / [ \t\r\n\f])? / "\\s"i { return "s"; } +T = "t"i / "\\" "0"? "0"? "0"? "0"? [\x54\x74] ("\r\n" / [ \t\r\n\f])? / "\\t"i { return "t"; } +U = "u"i / "\\" "0"? "0"? "0"? "0"? [\x55\x75] ("\r\n" / [ \t\r\n\f])? / "\\u"i { return "u"; } +X = "x"i / "\\" "0"? "0"? "0"? "0"? [\x58\x78] ("\r\n" / [ \t\r\n\f])? / "\\x"i { return "x"; } +Z = "z"i / "\\" "0"? "0"? "0"? "0"? [\x5a\x7a] ("\r\n" / [ \t\r\n\f])? / "\\z"i { return "z"; } /* Tokens */ @@ -513,45 +386,46 @@ MEDIA_SYM "@media" CHARSET_SYM "@charset" = comment* "@charset " -/* Note: We replace "w" with "s" here to avoid infinite recursion. */ +/* We use |s| instead of |w| here to avoid infinite recursion. */ IMPORTANT_SYM "!important" - = comment* "!" (s / comment)* I M P O R T A N T { return "!important"; } + = comment* "!" (s / comment)* I M P O R T A N T EMS "length" - = comment* num:num e:E m:M { return num + e + m; } + = comment* value:num E M { return { value: value, unit: "em" }; } EXS "length" - = comment* num:num e:E x:X { return num + e + x; } + = comment* value:num E X { return { value: value, unit: "ex" }; } LENGTH "length" - = comment* num:num unit:(P X / C M / M M / I N / P T / P C) { - return num + unit.join(""); - } + = comment* value:num P X { return { value: value, unit: "px" }; } + / comment* value:num C M { return { value: value, unit: "cm" }; } + / comment* value:num M M { return { value: value, unit: "mm" }; } + / comment* value:num I N { return { value: value, unit: "in" }; } + / comment* value:num P T { return { value: value, unit: "pt" }; } + / comment* value:num P C { return { value: value, unit: "pc" }; } ANGLE "angle" - = comment* num:num unit:(D E G / R A D / G R A D) { - return num + unit.join(""); - } + = comment* value:num D E G { return { value: value, unit: "deg" }; } + / comment* value:num R A D { return { value: value, unit: "rad" }; } + / comment* value:num G R A D { return { value: value, unit: "grad" }; } TIME "time" - = comment* num:num unit:(m:M s:S_ { return m + s; } / S_) { - return num + unit; - } + = comment* value:num M S_ { return { value: value, unit: "ms" }; } + / comment* value:num S_ { return { value: value, unit: "s" }; } FREQ "frequency" - = comment* num:num unit:(H Z / K H Z) { return num + unit.join(""); } - -DIMENSION "dimension" - = comment* num:num unit:ident { return num + unit; } + = comment* value:num H Z { return { value: value, unit: "hz" }; } + / comment* value:num K H Z { return { value: value, unit: "kh" }; } PERCENTAGE "percentage" - = comment* parts:$(num "%") { return parts; } + = comment* value:num "%" { return { value: value, unit: "%" }; } NUMBER "number" - = comment* num:num { return num; } + = comment* value:num { return { value: value, unit: null }; } URI "uri" - = comment* U R L "(" w value:(string / url) w ")" { return value; } + = comment* U R L "("i w url:string w ")" { return url; } + / comment* U R L "("i w url:url w ")" { return url; } FUNCTION "function" = comment* name:ident "(" { return name; }