Support keyframes.

This commit is contained in:
Natalie Weizenbaum 2016-10-20 23:07:23 -07:00
parent 271f899d3b
commit 18cc8d3f66
10 changed files with 164 additions and 12 deletions

View File

@ -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';

View File

@ -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<List<String>> selector;
final FileSpan span;
CssKeyframeBlock(this.selector, this.span);
/*=T*/ accept/*<T>*/(CssVisitor/*<T>*/ visitor) =>
visitor.visitKeyframeBlock(this);
CssKeyframeBlock copyWithoutChildren() =>
new CssKeyframeBlock(selector, span);
}

View File

@ -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) {

View File

@ -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<Statement> children})
: children = children == null ? null : new List.unmodifiable(children);
AtRule(String name, this.span, {this.value, Iterable<Statement> children})
: name = name,
normalizedName = unvendor(name),
children = children == null ? null : new List.unmodifiable(children);
/*=T*/ accept/*<T>*/(StatementVisitor/*<T>*/ visitor) =>
visitor.visitAtRule(this);

View File

@ -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();

View File

@ -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<String> parse() {
return wrapSpanFormatException(() {
whitespace();
var selectors = <String>[];
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();
}
}

View File

@ -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) =>

View File

@ -12,6 +12,7 @@ abstract class CssVisitor<T> {
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);

View File

@ -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,

View File

@ -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);