From 18cc8d3f664a49000425f682f6d6faa8866b03b0 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 20 Oct 2016 23:07:23 -0700 Subject: [PATCH] Support keyframes. --- lib/src/ast/css.dart | 1 + lib/src/ast/css/keyframe_block.dart | 27 +++++++++ lib/src/ast/sass/at_root_query.dart | 6 +- lib/src/ast/sass/statement/at_rule.dart | 10 +++- lib/src/parse/at_root_query.dart | 2 +- lib/src/parse/keyframe_selector.dart | 73 +++++++++++++++++++++++++ lib/src/util/character.dart | 3 +- lib/src/visitor/interface/css.dart | 1 + lib/src/visitor/perform.dart | 46 ++++++++++++++-- lib/src/visitor/serialize.dart | 7 +++ 10 files changed, 164 insertions(+), 12 deletions(-) create mode 100644 lib/src/ast/css/keyframe_block.dart create mode 100644 lib/src/parse/keyframe_selector.dart diff --git a/lib/src/ast/css.dart b/lib/src/ast/css.dart index 5eb32dd1..62fe2714 100644 --- a/lib/src/ast/css.dart +++ b/lib/src/ast/css.dart @@ -6,6 +6,7 @@ export 'css/at_rule.dart'; export 'css/comment.dart'; export 'css/declaration.dart'; export 'css/import.dart'; +export 'css/keyframe_block.dart'; export 'css/media_query.dart'; export 'css/media_rule.dart'; export 'css/node.dart'; diff --git a/lib/src/ast/css/keyframe_block.dart b/lib/src/ast/css/keyframe_block.dart new file mode 100644 index 00000000..dc9c6955 --- /dev/null +++ b/lib/src/ast/css/keyframe_block.dart @@ -0,0 +1,27 @@ +// Copyright 2016 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:source_span/source_span.dart'; + +import '../../visitor/interface/css.dart'; +import 'node.dart'; +import 'value.dart'; + +/// A block within a `@keyframes` rule. +/// +/// For example, `10% {opacity: 0.5}`. +class CssKeyframeBlock extends CssParentNode { + /// The selector for this block. + final CssValue> selector; + + final FileSpan span; + + CssKeyframeBlock(this.selector, this.span); + + /*=T*/ accept/**/(CssVisitor/**/ visitor) => + visitor.visitKeyframeBlock(this); + + CssKeyframeBlock copyWithoutChildren() => + new CssKeyframeBlock(selector, span); +} diff --git a/lib/src/ast/sass/at_root_query.dart b/lib/src/ast/sass/at_root_query.dart index 7c48f7be..818ecb46 100644 --- a/lib/src/ast/sass/at_root_query.dart +++ b/lib/src/ast/sass/at_root_query.dart @@ -30,7 +30,7 @@ class AtRootQuery { /// Whether this excludes `@media` rules. /// /// Note that this takes [include] into account. - bool get excludesMedia => _all ? !include : _excludesName("media"); + bool get excludesMedia => _all ? !include : excludesName("media"); /// Whether this excludes style rules. /// @@ -61,11 +61,11 @@ class AtRootQuery { bool excludes(CssParentNode node) { if (_all) return !include; if (_rule && node is CssStyleRule) return !include; - return _excludesName(_nameFor(node)); + return excludesName(_nameFor(node)); } /// Returns whether [this] excludes a node with the given [name]. - bool _excludesName(String name) => names.contains(name) != include; + bool excludesName(String name) => names.contains(name) != include; /// Returns the at-rule name for [node], or `null` if it's not an at-rule. String _nameFor(CssParentNode node) { diff --git a/lib/src/ast/sass/statement/at_rule.dart b/lib/src/ast/sass/statement/at_rule.dart index cc1a2caf..01c67de0 100644 --- a/lib/src/ast/sass/statement/at_rule.dart +++ b/lib/src/ast/sass/statement/at_rule.dart @@ -4,6 +4,7 @@ import 'package:source_span/source_span.dart'; +import '../../../utils.dart'; import '../../../visitor/interface/statement.dart'; import '../interpolation.dart'; import '../statement.dart'; @@ -13,6 +14,9 @@ class AtRule implements Statement { /// The name of this rule. final String name; + /// Like [name], but without any vendor prefixes. + final String normalizedName; + /// The value of this rule. final Interpolation value; @@ -24,8 +28,10 @@ class AtRule implements Statement { final FileSpan span; - AtRule(this.name, this.span, {this.value, Iterable children}) - : children = children == null ? null : new List.unmodifiable(children); + AtRule(String name, this.span, {this.value, Iterable children}) + : name = name, + normalizedName = unvendor(name), + children = children == null ? null : new List.unmodifiable(children); /*=T*/ accept/**/(StatementVisitor/**/ visitor) => visitor.visitAtRule(this); diff --git a/lib/src/parse/at_root_query.dart b/lib/src/parse/at_root_query.dart index ca8914ab..c1769b96 100644 --- a/lib/src/parse/at_root_query.dart +++ b/lib/src/parse/at_root_query.dart @@ -16,7 +16,7 @@ class AtRootQueryParser extends Parser { scanner.expectChar($lparen); whitespace(); var include = scanIdentifier("with"); - if (!include) expectIdentifier("without"); + if (!include) expectIdentifier("without", name: '"with" or "without"'); whitespace(); scanner.expectChar($colon); whitespace(); diff --git a/lib/src/parse/keyframe_selector.dart b/lib/src/parse/keyframe_selector.dart new file mode 100644 index 00000000..6af94351 --- /dev/null +++ b/lib/src/parse/keyframe_selector.dart @@ -0,0 +1,73 @@ +// Copyright 2016 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:charcode/charcode.dart'; + +import '../util/character.dart'; +import 'parser.dart'; + +/// A parser for `@keyframes` block selectors. +class KeyframeSelectorParser extends Parser { + KeyframeSelectorParser(String contents, {url}) : super(contents, url: url); + + List parse() { + return wrapSpanFormatException(() { + whitespace(); + + var selectors = []; + do { + if (lookingAtIdentifier()) { + if (scanIdentifier("from")) { + selectors.add("from"); + } else { + expectIdentifier("to", name: '"to" or "from"'); + selectors.add("to"); + } + } else { + selectors.add(_percentage()); + } + whitespace(); + } while (scanner.scanChar($comma)); + + return selectors; + }); + } + + String _percentage() { + var buffer = new StringBuffer(); + if (scanner.scanChar($plus)) buffer.writeCharCode($plus); + + var second = scanner.peekChar(); + if (!isDigit(second) && second != $dot) { + scanner.error("Expected number."); + } + + while (isDigit(scanner.peekChar())) { + buffer.writeCharCode(scanner.readChar()); + } + + if (scanner.peekChar() == $dot) { + buffer.writeCharCode(scanner.readChar()); + + while (isDigit(scanner.peekChar())) { + buffer.writeCharCode(scanner.readChar()); + } + } + + if (scanIdentifier("e", ignoreCase: true)) { + buffer.write(scanner.readChar()); + var next = scanner.peekChar(); + if (next == $plus || next == $minus) buffer.write(scanner.readChar()); + if (!isDigit(scanner.peekChar())) scanner.error("Expected digit."); + + while (isDigit(scanner.peekChar())) { + buffer.writeCharCode(scanner.readChar()); + } + } + + scanner.expectChar($percent); + buffer.writeCharCode($percent); + return buffer.toString(); + } +} diff --git a/lib/src/util/character.dart b/lib/src/util/character.dart index 0c5dea95..91f1340c 100644 --- a/lib/src/util/character.dart +++ b/lib/src/util/character.dart @@ -28,7 +28,8 @@ bool isAlphabetic(int character) => (character >= $A && character <= $Z); /// Returns whether [character] is a number. -bool isDigit(int character) => character >= $0 && character <= $9; +bool isDigit(int character) => + character != null && character >= $0 && character <= $9; /// Returns whether [character] is legal as the start of a Sass identifier. bool isNameStart(int character) => diff --git a/lib/src/visitor/interface/css.dart b/lib/src/visitor/interface/css.dart index 2c2455b2..3c26bffb 100644 --- a/lib/src/visitor/interface/css.dart +++ b/lib/src/visitor/interface/css.dart @@ -12,6 +12,7 @@ abstract class CssVisitor { T visitComment(CssComment node); T visitDeclaration(CssDeclaration node); T visitImport(CssImport node); + T visitKeyframeBlock(CssKeyframeBlock node); T visitMediaRule(CssMediaRule node); T visitStyleRule(CssStyleRule node); T visitStylesheet(CssStylesheet node); diff --git a/lib/src/visitor/perform.dart b/lib/src/visitor/perform.dart index 82b9a0fb..ac1ece5f 100644 --- a/lib/src/visitor/perform.dart +++ b/lib/src/visitor/perform.dart @@ -17,6 +17,7 @@ import '../environment.dart'; import '../exception.dart'; import '../extend/extender.dart'; import '../io.dart'; +import '../parse/keyframe_selector.dart'; import '../utils.dart'; import '../value.dart'; import 'interface/statement.dart'; @@ -82,6 +83,9 @@ class _PerformVisitor /// Whether we're currently building the output of an unknown at rule. var _inUnknownAtRule = false; + /// Whether we're currently building the output of a `@keyframes` rule. + var _inKeyframes = false; + /// The resolved URLs for each [ImportRule] that's been seen so far. /// /// This is cached in case the same file is imported multiple times, and thus @@ -228,14 +232,24 @@ class _PerformVisitor var innerScope = scope; scope = (callback) => _withMediaQueries(null, () => innerScope(callback)); } + + if (_inKeyframes && query.excludesName('keyframes')) { + var innerScope = scope; + scope = (callback) { + var wasInKeyframes = _inKeyframes; + _inKeyframes = false; + innerScope(callback); + _inKeyframes = wasInKeyframes; + }; + } + if (_inUnknownAtRule && !included.any((parent) => parent is CssAtRule)) { var innerScope = scope; scope = (callback) { var wasInUnknownAtRule = _inUnknownAtRule; _inUnknownAtRule = false; - var result = innerScope(callback); + innerScope(callback); _inUnknownAtRule = wasInUnknownAtRule; - return result; }; } @@ -270,7 +284,7 @@ class _PerformVisitor } Value visitDeclaration(Declaration node) { - if (_selector == null && !_inUnknownAtRule) { + if (_selector == null && !_inUnknownAtRule && !_inKeyframes) { throw _exception( "Declarations may only be used within style rules.", node.span); } @@ -357,8 +371,6 @@ class _PerformVisitor "At-rules may not be used within nested declarations.", node.span); } - var wasInUnknownAtRule = _inUnknownAtRule; - _inUnknownAtRule = true; var value = node.value == null ? null : _interpolationToValue(node.value, trim: true); @@ -369,6 +381,14 @@ class _PerformVisitor return null; } + var wasInKeyframes = _inKeyframes; + var wasInUnknownAtRule = _inUnknownAtRule; + if (node.normalizedName == 'keyframes') { + _inKeyframes = true; + } else { + _inUnknownAtRule = true; + } + _withParent(new CssAtRule(node.name, node.span, value: value), () { if (_selector == null) { for (var child in node.children) { @@ -388,6 +408,7 @@ class _PerformVisitor }, through: (node) => node is CssStyleRule); _inUnknownAtRule = wasInUnknownAtRule; + _inKeyframes = wasInKeyframes; return null; } @@ -601,6 +622,21 @@ class _PerformVisitor } var selectorText = _interpolationToValue(node.selector, trim: true); + if (_inKeyframes) { + var parsedSelector = _adjustParseError(node.selector.span, + () => new KeyframeSelectorParser(selectorText.value).parse()); + var rule = new CssKeyframeBlock( + new CssValue( + new List.unmodifiable(parsedSelector), node.selector.span), + node.span); + _withParent(rule, () { + for (var child in node.children) { + child.accept(this); + } + }, through: (node) => node is CssStyleRule); + return null; + } + var parsedSelector = _adjustParseError( node.selector.span, () => new SelectorList.parse(selectorText.value)); parsedSelector = _addExceptionSpan(node.selector.span, diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index f163ca7a..57f2e007 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -144,6 +144,13 @@ class _SerializeCssVisitor _buffer.writeCharCode($semicolon); } + void visitKeyframeBlock(CssKeyframeBlock node) { + _writeIndentation(); + _writeBetween(node.selector.value, ", ", _buffer.write); + _buffer.writeCharCode($space); + _visitChildren(node.children); + } + void visitMediaQuery(CssMediaQuery query) { if (query.modifier != null) { _buffer.write(query.modifier.value);