Index: pkg/csslib/lib/parser.dart |
diff --git a/pkg/csslib/lib/parser.dart b/pkg/csslib/lib/parser.dart |
index 9f7b3ff1de6992ca3436d664e45a148d9afb8a5d..fd5a0c2bc6c0e9f9ea73478a5ab0179bfeaf09f8 100644 |
--- a/pkg/csslib/lib/parser.dart |
+++ b/pkg/csslib/lib/parser.dart |
@@ -45,21 +45,25 @@ bool get isChecked => messages.options.checked; |
// TODO(terry): Remove nested name parameter. |
/** Parse and analyze the CSS file. */ |
-StyleSheet compile(var input, |
- {List errors, List options, bool nested: true, bool polyfill: false}) { |
+StyleSheet compile(var input, {List errors, List options, bool nested: true, |
+ bool polyfill: false, List<StyleSheet> includes: null}) { |
+ if (includes == null) { |
+ includes = []; |
+ } |
+ |
var source = _inputAsString(input); |
_createMessages(errors: errors, options: options); |
var file = new SourceFile.text(null, source); |
- var tree = new Parser(file, source).parse(); |
+ var tree = new _Parser(file, source).parse(); |
analyze([tree], errors: errors, options: options); |
if (polyfill) { |
var processCss = new PolyFill(messages, true); |
- processCss.process(tree); |
+ processCss.process(tree, includes: includes); |
} |
return tree; |
@@ -83,7 +87,7 @@ StyleSheet parse(var input, {List errors, List options}) { |
var file = new SourceFile.text(null, source); |
- return new Parser(file, source).parse(); |
+ return new _Parser(file, source).parse(); |
} |
/** |
@@ -98,7 +102,7 @@ StyleSheet selector(var input, {List errors}) { |
var file = new SourceFile.text(null, source); |
- return new Parser(file, source).parseSelector(); |
+ return new _Parser(file, source).parseSelector(); |
} |
String _inputAsString(var input) { |
@@ -132,8 +136,20 @@ String _inputAsString(var input) { |
return source; |
} |
-/** A simple recursive descent parser for CSS. */ |
+// TODO(terry): Consider removing this class when all usages can be eliminated |
+// or replaced with compile API. |
+/** Public parsing interface for csslib. */ |
class Parser { |
+ final _Parser _parser; |
+ |
+ Parser(SourceFile file, String text, {int start: 0, String baseUrl}) : |
+ _parser = new _Parser(file, text, start: start, baseUrl: baseUrl); |
+ |
+ StyleSheet parse() => _parser.parse(); |
+} |
+ |
+/** A simple recursive descent parser for CSS. */ |
+class _Parser { |
Tokenizer tokenizer; |
/** Base url of CSS file. */ |
@@ -148,7 +164,7 @@ class Parser { |
Token _previousToken; |
Token _peekToken; |
- Parser(SourceFile file, String text, {int start: 0, String baseUrl}) |
+ _Parser(SourceFile file, String text, {int start: 0, String baseUrl}) |
: this.file = file, |
_baseUrl = baseUrl, |
tokenizer = new Tokenizer(file, text, true, start) { |
@@ -429,60 +445,28 @@ class Parser { |
} |
} |
- // Directive grammar: |
- // |
- // import: '@import' [string | URI] media_list? |
- // media: '@media' media_query_list '{' ruleset '}' |
- // page: '@page' [':' IDENT]? '{' declarations '}' |
- // stylet: '@stylet' IDENT '{' ruleset '}' |
- // media_query_list: IDENT [',' IDENT] |
- // keyframes: '@-webkit-keyframes ...' (see grammar below). |
- // font_face: '@font-face' '{' declarations '}' |
- // namespace: '@namespace name url("xmlns") |
- // host: '@host '{' ruleset '}' |
+ /** |
+ * Directive grammar: |
+ * |
+ * import: '@import' [string | URI] media_list? |
+ * media: '@media' media_query_list '{' ruleset '}' |
+ * page: '@page' [':' IDENT]? '{' declarations '}' |
+ * stylet: '@stylet' IDENT '{' ruleset '}' |
+ * media_query_list: IDENT [',' IDENT] |
+ * keyframes: '@-webkit-keyframes ...' (see grammar below). |
+ * font_face: '@font-face' '{' declarations '}' |
+ * namespace: '@namespace name url("xmlns") |
+ * host: '@host '{' ruleset '}' |
+ * mixin: '@mixin name [(args,...)] '{' declarations/ruleset '}' |
+ * include: '@include name [(@arg,@arg1)] |
+ * '@include name [(@arg...)] |
+ * content '@content' |
+ */ |
processDirective() { |
int start = _peekToken.start; |
- var tokId = _peek(); |
- // Handle case for @ directive (where there's a whitespace between the @ |
- // sign and the directive name. Technically, it's not valid grammar but |
- // a number of CSS tests test for whitespace between @ and name. |
- if (tokId == TokenKind.AT) { |
- Token tok = _next(); |
- tokId = _peek(); |
- if (_peekIdentifier()) { |
- // Is it a directive? |
- var directive = _peekToken.text; |
- var directiveLen = directive.length; |
- tokId = TokenKind.matchDirectives(directive, 0, directiveLen); |
- if (tokId == -1) { |
- tokId = TokenKind.matchMarginDirectives(directive, 0, directiveLen); |
- } |
- } |
- |
- if (tokId == -1) { |
- if (messages.options.lessSupport) { |
- // Less compatibility: |
- // @name: value; => var-name: value; (VarDefinition) |
- // property: @name; => property: var(name); (VarUsage) |
- var name; |
- if (_peekIdentifier()) { |
- name = identifier(); |
- } |
- |
- _eat(TokenKind.COLON); |
- |
- Expressions exprs = processExpr(); |
- |
- var span = _makeSpan(start); |
- return new VarDefinitionDirective( |
- new VarDefinition(name, exprs, span), span); |
- } else if (isChecked) { |
- _error('unexpected directive @$_peekToken', _peekToken.span); |
- } |
- } |
- } |
- |
+ var tokId = processVariableOrDirective(); |
+ if (tokId is VarDefinitionDirective) return tokId; |
switch (tokId) { |
case TokenKind.DIRECTIVE_IMPORT: |
_next(); |
@@ -748,7 +732,237 @@ class Parser { |
return new NamespaceDirective(prefix != null ? prefix.name : '', |
namespaceUri, _makeSpan(start)); |
+ |
+ case TokenKind.DIRECTIVE_MIXIN: |
+ return processMixin(start); |
+ |
+ case TokenKind.DIRECTIVE_INCLUDE: |
+ return processInclude( _makeSpan(start)); |
+ |
+ case TokenKind.DIRECTIVE_CONTENT: |
+ // TODO(terry): TBD |
+ _warning("@content not implemented.", _makeSpan(start)); |
+ return; |
+ } |
+ } |
+ |
+ /** |
+ * Parse the mixin beginning token offset [start]. Returns a [MixinDefinition] |
+ * node. |
+ * |
+ * Mixin grammar: |
+ * |
+ * @mixin IDENT [(args,...)] '{' |
+ * [ruleset | property | directive]* |
+ * '}' |
+ */ |
+ MixinDefinition processMixin(int start) { |
+ _next(); |
+ |
+ var name = identifier(); |
+ |
+ List<VarDefinitionDirective> params = []; |
+ // Any parameters? |
+ if (_maybeEat(TokenKind.LPAREN)) { |
+ var mustHaveParam = false; |
+ var keepGoing = true; |
+ while (keepGoing) { |
+ var varDef = processVariableOrDirective(mixinParameter: true); |
+ if (varDef is VarDefinitionDirective || varDef is VarDefinition) { |
+ params.add(varDef); |
+ } else if (mustHaveParam) { |
+ _warning("Expecting parameter", _makeSpan(_peekToken.start)); |
+ keepGoing = false; |
+ } |
+ if (_maybeEat(TokenKind.COMMA)) { |
+ mustHaveParam = true; |
+ continue; |
+ } |
+ keepGoing = !_maybeEat(TokenKind.RPAREN); |
+ } |
+ } |
+ |
+ _eat(TokenKind.LBRACE); |
+ |
+ List<TreeNode> productions = []; |
+ List<TreeNode> declarations = []; |
+ var mixinDirective; |
+ |
+ start = _peekToken.start; |
+ while (!_maybeEat(TokenKind.END_OF_FILE)) { |
+ var directive = processDirective(); |
+ if (directive != null) { |
+ productions.add(directive); |
+ continue; |
+ } |
+ |
+ var declGroup = processDeclarations(checkBrace: false); |
+ var decls = []; |
+ if (declGroup.declarations.any((decl) { |
+ return decl is Declaration && |
+ decl is! IncludeMixinAtDeclaration; |
+ })) { |
+ var newDecls = []; |
+ productions.forEach((include) { |
+ // If declGroup has items that are declarations then we assume |
+ // this mixin is a declaration mixin not a top-level mixin. |
+ if (include is IncludeDirective) { |
+ newDecls.add(new IncludeMixinAtDeclaration(include, |
+ include.span)); |
+ } else { |
+ _warning("Error mixing of top-level vs declarations mixins", |
+ _makeSpan(include)); |
+ } |
+ }); |
+ declGroup.declarations.insertAll(0, newDecls); |
+ productions = []; |
+ } else { |
+ // Declarations are just @includes make it a list of productions |
+ // not a declaration group (anything else is a ruleset). Make it a |
+ // list of productions, not a declaration group. |
+ for (var decl in declGroup.declarations) { |
+ productions.add(decl is IncludeMixinAtDeclaration ? |
+ decl.include : decl); |
+ }; |
+ declGroup.declarations.clear(); |
+ } |
+ |
+ if (declGroup.declarations.isNotEmpty) { |
+ if (productions.isEmpty) { |
+ mixinDirective = new MixinDeclarationDirective(name.name, params, |
+ false, declGroup, _makeSpan(start)); |
+ break; |
+ } else { |
+ for (var decl in declGroup.declarations) { |
+ productions.add(decl is IncludeMixinAtDeclaration ? |
+ decl.include : decl); |
+ } |
+ } |
+ } else { |
+ mixinDirective = new MixinRulesetDirective(name.name, params, |
+ false, productions, _makeSpan(start)); |
+ break; |
+ } |
+ } |
+ |
+ if (productions.isNotEmpty) { |
+ mixinDirective = new MixinRulesetDirective(name.name, params, |
+ false, productions, _makeSpan(start)); |
+ } |
+ |
+ _eat(TokenKind.RBRACE); |
+ |
+ return mixinDirective; |
+ } |
+ |
+ /** |
+ * Returns a VarDefinitionDirective or VarDefinition if a varaible otherwise |
+ * return the token id of a directive or -1 if neither. |
+ */ |
+ processVariableOrDirective({bool mixinParameter: false}) { |
+ int start = _peekToken.start; |
+ |
+ var tokId = _peek(); |
+ // Handle case for @ directive (where there's a whitespace between the @ |
+ // sign and the directive name. Technically, it's not valid grammar but |
+ // a number of CSS tests test for whitespace between @ and name. |
+ if (tokId == TokenKind.AT) { |
+ Token tok = _next(); |
+ tokId = _peek(); |
+ if (_peekIdentifier()) { |
+ // Is it a directive? |
+ var directive = _peekToken.text; |
+ var directiveLen = directive.length; |
+ tokId = TokenKind.matchDirectives(directive, 0, directiveLen); |
+ if (tokId == -1) { |
+ tokId = TokenKind.matchMarginDirectives(directive, 0, directiveLen); |
+ } |
+ } |
+ |
+ if (tokId == -1) { |
+ if (messages.options.lessSupport) { |
+ // Less compatibility: |
+ // @name: value; => var-name: value; (VarDefinition) |
+ // property: @name; => property: var(name); (VarUsage) |
+ var name; |
+ if (_peekIdentifier()) { |
+ name = identifier(); |
+ } |
+ |
+ Expressions exprs; |
+ if (mixinParameter && _maybeEat(TokenKind.COLON)) { |
+ exprs = processExpr(); |
+ } else if (!mixinParameter) { |
+ _eat(TokenKind.COLON); |
+ exprs = processExpr(); |
+ } |
+ |
+ var span = _makeSpan(start); |
+ return new VarDefinitionDirective( |
+ new VarDefinition(name, exprs, span), span); |
+ } else if (isChecked) { |
+ _error('unexpected directive @$_peekToken', _peekToken.span); |
+ } |
+ } |
+ } else if (mixinParameter && _peekToken.kind == TokenKind.VAR_DEFINITION) { |
+ _next(); |
+ var definedName; |
+ if (_peekIdentifier()) definedName = identifier(); |
+ |
+ Expressions exprs; |
+ if (_maybeEat(TokenKind.COLON)) { |
+ exprs = processExpr(); |
+ } |
+ |
+ return new VarDefinition(definedName, exprs, _makeSpan(start)); |
} |
+ |
+ return tokId; |
+ } |
+ |
+ IncludeDirective processInclude(Span span, {bool eatSemiColon: true}) { |
+ /* Stylet grammar: |
+ * |
+ * @include IDENT [(args,...)]; |
+ */ |
+ _next(); |
+ |
+ var name; |
+ if (_peekIdentifier()) { |
+ name = identifier(); |
+ } |
+ |
+ var params = []; |
+ |
+ // Any parameters? Parameters can be multiple terms per argument e.g., |
+ // 3px solid yellow, green is two parameters: |
+ // 1. 3px solid yellow |
+ // 2. green |
+ // the first has 3 terms and the second has 1 term. |
+ if (_maybeEat(TokenKind.LPAREN)) { |
+ var terms = []; |
+ var expr; |
+ var keepGoing = true; |
+ while (keepGoing && (expr = processTerm()) != null) { |
+ // VarUsage is returns as a list |
+ terms.add(expr is List ? expr[0] : expr); |
+ keepGoing = !_peekKind(TokenKind.RPAREN); |
+ if (keepGoing) { |
+ if (_maybeEat(TokenKind.COMMA)) { |
+ params.add(terms); |
+ terms = []; |
+ } |
+ } |
+ } |
+ params.add(terms); |
+ _maybeEat(TokenKind.RPAREN); |
+ } |
+ |
+ if (eatSemiColon) { |
+ _eat(TokenKind.SEMICOLON); |
+ } |
+ |
+ return new IncludeDirective(name.name, params, span); |
} |
RuleSet processRuleSet([SelectorGroup selectorGroup]) { |
@@ -809,7 +1023,7 @@ class Parser { |
} |
} |
- processDeclarations({bool checkBrace: true}) { |
+ DeclarationGroup processDeclarations({bool checkBrace: true}) { |
int start = _peekToken.start; |
if (checkBrace) _eat(TokenKind.LBRACE); |
@@ -1165,56 +1379,7 @@ class Parser { |
return new ClassSelector(id, _makeSpan(start)); |
case TokenKind.COLON: |
// :pseudo-class ::pseudo-element |
- // TODO(terry): '::' should be token. |
- _eat(TokenKind.COLON); |
- bool pseudoElement = _maybeEat(TokenKind.COLON); |
- |
- // TODO(terry): If no identifier specified consider optimizing out the |
- // : or :: and making this a normal selector. For now, |
- // create an empty pseudoName. |
- var pseudoName; |
- if (_peekIdentifier()) { |
- pseudoName = identifier(); |
- } else { |
- return null; |
- } |
- |
- // Functional pseudo? |
- if (_maybeEat(TokenKind.LPAREN)) { |
- if (!pseudoElement && pseudoName.name.toLowerCase() == 'not') { |
- // Negation : ':NOT(' S* negation_arg S* ')' |
- var negArg = simpleSelector(); |
- |
- _eat(TokenKind.RPAREN); |
- return new NegationSelector(negArg, _makeSpan(start)); |
- } else { |
- // Handle function expression. |
- var span = _makeSpan(start); |
- var expr = processSelectorExpression(); |
- |
- // Used during selector look-a-head if not a SelectorExpression is |
- // bad. |
- if (expr is! SelectorExpression) { |
- _errorExpected("CSS expression"); |
- return null; |
- } |
- |
- _eat(TokenKind.RPAREN); |
- return (pseudoElement) ? |
- new PseudoElementFunctionSelector(pseudoName, expr, span) : |
- new PseudoClassFunctionSelector(pseudoName, expr, span); |
- } |
- } |
- |
- // TODO(terry): Need to handle specific pseudo class/element name and |
- // backward compatible names that are : as well as :: as well as |
- // parameters. Current, spec uses :: for pseudo-element and : for |
- // pseudo-class. However, CSS2.1 allows for : to specify old |
- // pseudo-elements (:first-line, :first-letter, :before and :after) any |
- // new pseudo-elements defined would require a ::. |
- return pseudoElement ? |
- new PseudoElementSelector(pseudoName, _makeSpan(start)) : |
- new PseudoClassSelector(pseudoName, _makeSpan(start)); |
+ return processPseudoSelector(start); |
case TokenKind.LBRACK: |
return processAttribute(); |
case TokenKind.DOUBLE: |
@@ -1225,6 +1390,60 @@ class Parser { |
} |
} |
+ processPseudoSelector(int start) { |
+ // :pseudo-class ::pseudo-element |
+ // TODO(terry): '::' should be token. |
+ _eat(TokenKind.COLON); |
+ bool pseudoElement = _maybeEat(TokenKind.COLON); |
+ |
+ // TODO(terry): If no identifier specified consider optimizing out the |
+ // : or :: and making this a normal selector. For now, |
+ // create an empty pseudoName. |
+ var pseudoName; |
+ if (_peekIdentifier()) { |
+ pseudoName = identifier(); |
+ } else { |
+ return null; |
+ } |
+ |
+ // Functional pseudo? |
+ if (_maybeEat(TokenKind.LPAREN)) { |
+ if (!pseudoElement && pseudoName.name.toLowerCase() == 'not') { |
+ // Negation : ':NOT(' S* negation_arg S* ')' |
+ var negArg = simpleSelector(); |
+ |
+ _eat(TokenKind.RPAREN); |
+ return new NegationSelector(negArg, _makeSpan(start)); |
+ } else { |
+ // Handle function expression. |
+ var span = _makeSpan(start); |
+ var expr = processSelectorExpression(); |
+ |
+ // Used during selector look-a-head if not a SelectorExpression is |
+ // bad. |
+ if (expr is! SelectorExpression) { |
+ _errorExpected("CSS expression"); |
+ return null; |
+ } |
+ |
+ _eat(TokenKind.RPAREN); |
+ return (pseudoElement) ? |
+ new PseudoElementFunctionSelector(pseudoName, expr, span) : |
+ new PseudoClassFunctionSelector(pseudoName, expr, span); |
+ } |
+ } |
+ |
+ // TODO(terry): Need to handle specific pseudo class/element name and |
+ // backward compatible names that are : as well as :: as well as |
+ // parameters. Current, spec uses :: for pseudo-element and : for |
+ // pseudo-class. However, CSS2.1 allows for : to specify old |
+ // pseudo-elements (:first-line, :first-letter, :before and :after) any |
+ // new pseudo-elements defined would require a ::. |
+ return pseudoElement ? |
+ new PseudoElementSelector(pseudoName, _makeSpan(start)) : |
+ new PseudoClassSelector(pseudoName, _makeSpan(start)); |
+ } |
+ |
/** |
* In CSS3, the expressions are identifiers, strings, or of the form "an+b". |
* |
@@ -1411,6 +1630,32 @@ class Parser { |
Expressions exprs = processExpr(); |
decl = new VarDefinition(definedName, exprs, _makeSpan(start)); |
+ } else if (_peekToken.kind == TokenKind.DIRECTIVE_INCLUDE) { |
+ // @include mixinName in the declaration area. |
+ var span = _makeSpan(start); |
+ var include = processInclude(span, eatSemiColon: false); |
+ decl = new IncludeMixinAtDeclaration(include, span); |
+ } else if (_peekToken.kind == TokenKind.DIRECTIVE_EXTEND) { |
+ List<SimpleSelectorSequence> simpleSequences = []; |
+ |
+ _next(); |
+ var span = _makeSpan(start); |
+ var selector = simpleSelector(); |
+ if (selector == null) { |
+ _warning("@extends expecting simple selector name", span); |
+ } else { |
+ simpleSequences.add(selector); |
+ } |
+ if (_peekKind(TokenKind.COLON)) { |
+ var pseudoSelector = processPseudoSelector(_peekToken.start); |
+ if (pseudoSelector is PseudoElementSelector || |
+ pseudoSelector is PseudoClassSelector) { |
+ simpleSequences.add(pseudoSelector); |
+ } else { |
+ _warning("not a valid selector", span); |
+ } |
+ } |
+ decl = new ExtendDeclaration(simpleSequences, span); |
} |
return decl; |
@@ -1791,7 +2036,13 @@ class Parser { |
} |
if (expr != null) { |
- expressions.add(expr); |
+ if (expr is List) { |
+ expr.forEach((exprItem) { |
+ expressions.add(exprItem); |
+ }); |
+ } else { |
+ expressions.add(expr); |
+ } |
} else { |
keepGoing = false; |
} |
@@ -1985,7 +2236,9 @@ class Parser { |
} |
var param = expr.expressions[0]; |
- return new VarUsage(param.text, [], _makeSpan(start)); |
+ var varUsage = new VarUsage(param.text, [], _makeSpan(start)); |
+ expr.expressions[0] = varUsage; |
+ return expr.expressions; |
} |
break; |
} |