mirror of
https://github.com/danog/dart-sass.git
synced 2025-01-22 22:02:00 +01:00
Support keyframes.
This commit is contained in:
parent
271f899d3b
commit
18cc8d3f66
@ -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';
|
||||
|
27
lib/src/ast/css/keyframe_block.dart
Normal file
27
lib/src/ast/css/keyframe_block.dart
Normal 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);
|
||||
}
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
73
lib/src/parse/keyframe_selector.dart
Normal file
73
lib/src/parse/keyframe_selector.dart
Normal 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();
|
||||
}
|
||||
}
|
@ -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) =>
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user