// CSS Grammar // =========== // // 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. // // 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 { function extractOptional(optional, index) { return optional ? optional[index] : null; } function extractList(list, index) { return list.map(function(element) { return element[index]; }); } function buildList(head, tail, index) { return [head].concat(extractList(tail, index)) .filter(function(element) { return element !== null; }); } function buildExpression(head, tail) { return tail.reduce(function(result, element) { return { type: "Expression", operator: element[0], left: result, right: element[1] }; }, head); } } 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*)*)* { return { 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: "ImportRule", href: href, media: media !== null ? media : [] }; } media = MEDIA_SYM S* media:media_list "{" S* rules:ruleset* "}" S* { return { type: "MediaRule", media: media, rules: rules }; } media_list = head:medium tail:("," S* medium)* { return buildList(head, tail, 2); } medium = name:IDENT S* { return name; } page = PAGE_SYM S* selector:pseudo_page? "{" S* declarationsHead:declaration? declarationsTail:(";" S* declaration?)* "}" S* { return { type: "PageRule", selector: selector, declarations: buildList(declarationsHead, declarationsTail, 2) }; } pseudo_page = ":" value:IDENT S* { return { type: "PseudoSelector", value: value }; } operator = "/" S* { return "/"; } / "," S* { return ","; } combinator = "+" S* { return "+"; } / ">" S* { return ">"; } property = name:IDENT S* { return name; } ruleset = selectorsHead:selector selectorsTail:("," S* selector)* "{" S* declarationsHead:declaration? declarationsTail:(";" S* declaration?)* "}" S* { return { type: "RuleSet", selectors: buildList(selectorsHead, selectorsTail, 2), declarations: buildList(declarationsHead, declarationsTail, 2) }; } selector = left:simple_selector S* combinator:combinator right:selector { return { type: "Selector", combinator: combinator, left: left, right: right }; } / left:simple_selector S+ right:selector { return { type: "Selector", combinator: " ", left: left, right: right }; } / selector:simple_selector S* { return selector; } simple_selector = element:element_name qualifiers:(id / class / attrib / pseudo)* { return { type: "SimpleSelector", element: element, qualifiers: qualifiers }; } / qualifiers:(id / class / attrib / pseudo)+ { return { type: "SimpleSelector", element: "*", qualifiers: qualifiers }; } id = id:HASH { return { type: "IDSelector", id: id }; } class = "." class_:IDENT { return { type: "ClassSelector", "class": class_ }; } element_name = IDENT / "*" attrib = "[" S* attribute:IDENT S* operatorAndValue:(("=" / INCLUDES / DASHMATCH) S* (IDENT / STRING) S*)? "]" { return { type: "AttributeSelector", attribute: attribute, operator: extractOptional(operatorAndValue, 0), value: extractOptional(operatorAndValue, 2) }; } pseudo = ":" value:( name:FUNCTION S* params:(IDENT S*)? ")" { return { type: "Function", name: name, params: params !== null ? [params[0]] : [] }; } / IDENT ) { return { type: "PseudoSelector", value: value }; } declaration = name:property ':' S* value:expr prio:prio? { return { type: "Declaration", name: name, value: value, important: prio !== null }; } prio = IMPORTANT_SYM S* expr = head:term tail:(operator? term)* { return buildExpression(head, tail); } term = quantity:(PERCENTAGE / LENGTH / EMS / EXS / ANGLE / TIME / FREQ / NUMBER) S* { return { type: "Quantity", value: quantity.value, unit: quantity.unit }; } / value:STRING S* { return { type: "String", value: value }; } / value:URI S* { return { type: "URI", value: value }; } / function / hexcolor / value:IDENT S* { return { type: "Ident", value: value }; } function = name:FUNCTION S* params:expr ")" S* { return { type: "Function", name: name, params: params }; } hexcolor = value:HASH S* { return { type: "Hexcolor", value: value }; } // ----- G.2 Lexical scanner ----- // Macros h = [0-9a-f]i nonascii = [\x80-\uFFFF] unicode = "\\" digits:$(h h? h? h? h? h?) ("\r\n" / [ \t\r\n\f])? { return String.fromCharCode(parseInt(digits, 16)); } escape = unicode / "\\" ch:[^\r\n\f0-9a-f]i { return ch; } nmstart = [_a-z]i / nonascii / escape nmchar = [_a-z0-9-]i / nonascii / escape string1 = '"' chars:([^\n\r\f\\"] / "\\" nl:nl { return ""; } / escape)* '"' { return chars.join(""); } string2 = "'" chars:([^\n\r\f\\'] / "\\" nl:nl { return ""; } / escape)* "'" { return chars.join(""); } comment = "/*" [^*]* "*"+ ([^/*] [^*]* "*"+)* "/" ident = prefix:$"-"? start:nmstart chars:nmchar* { return prefix + start + chars.join(""); } name = chars:nmchar+ { return chars.join(""); } num = [+-]? ([0-9]* "." [0-9]+ / [0-9]+) ("e" [+-]? [0-9]+)? { return parseFloat(text()); } string = string1 / string2 url = chars:([!#$%&*-\[\]-~] / nonascii / escape)* { return chars.join(""); } s = [ \t\r\n\f]+ w = s? nl = "\n" / "\r\n" / "\r" / "\f" 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 S "whitespace" = comment* s CDO "" = comment* "-->" INCLUDES "~=" = comment* "~=" DASHMATCH "|=" = comment* "|=" STRING "string" = comment* string:string { return string; } IDENT "identifier" = comment* ident:ident { return ident; } HASH "hash" = comment* "#" name:name { return "#" + name; } IMPORT_SYM "@import" = comment* "@" I M P O R T PAGE_SYM "@page" = comment* "@" P A G E MEDIA_SYM "@media" = comment* "@" M E D I A CHARSET_SYM "@charset" = comment* "@charset " // 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 EMS "length" = comment* value:num E M { return { value: value, unit: "em" }; } EXS "length" = comment* value:num E X { return { value: value, unit: "ex" }; } LENGTH "length" = 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* 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* value:num M S_ { return { value: value, unit: "ms" }; } / comment* value:num S_ { return { value: value, unit: "s" }; } FREQ "frequency" = comment* value:num H Z { return { value: value, unit: "hz" }; } / comment* value:num K H Z { return { value: value, unit: "kh" }; } PERCENTAGE "percentage" = comment* value:num "%" { return { value: value, unit: "%" }; } NUMBER "number" = comment* value:num { return { value: value, unit: null }; } URI "uri" = 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; }