ff3cc7930e
This makes extractList identical to the same function in other grammars and makes it so that nulls are dealt with in only one function (until now, they were dealt with both in extractList and buildList). The refactoring should be safe as extractList isn't by itself used in contexts where it can be passed a list containing nulls.
419 lines
11 KiB
JavaScript
419 lines
11 KiB
JavaScript
// 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* "<!--"
|
|
|
|
CDC "-->"
|
|
= 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; }
|