Reparse media queries at perform-time.

This commit is contained in:
Natalie Weizenbaum 2016-10-28 16:55:56 -07:00
parent 6f6eb79dae
commit 868286911b
11 changed files with 170 additions and 155 deletions

View File

@ -2,42 +2,38 @@
// 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 '../../utils.dart';
import '../node.dart';
import 'value.dart';
import '../../parse/media_query.dart';
/// A plain CSS media query, as used in `@media` and `@import`.
class CssMediaQuery implements AstNode {
class CssMediaQuery {
/// The modifier, probably either "not" or "only".
///
/// This may be `null` if no modifier is in use.
final CssValue<String> modifier;
final String modifier;
/// The media type, for example "screen" or "print".
///
/// This may be `null`. If so, [features] will not be empty.
final CssValue<String> type;
final String type;
/// Feature queries, including parentheses.
final List<CssValue<String>> features;
final List<String> features;
FileSpan get span {
var components = <AstNode>[];
if (modifier != null) components.add(modifier);
if (type != null) components.add(type);
components.addAll(features);
return spanForList(components);
}
/// Parses a media query from [contents].
///
/// If passed, [url] is the name of the file from which [contents] comes.
///
/// Throws a [SassFormatException] if parsing fails.
static List<CssMediaQuery> parseList(String contents, {url}) =>
new MediaQueryParser(contents, url: url).parse();
/// Creates a media query specifies a type and, optionally, features.
CssMediaQuery(this.type, {this.modifier, Iterable<CssValue<String>> features})
CssMediaQuery(this.type, {this.modifier, Iterable<String> features})
: features =
features == null ? const [] : new List.unmodifiable(features);
/// Creates a media query that only specifies features.
CssMediaQuery.condition(Iterable<CssValue<String>> features)
CssMediaQuery.condition(Iterable<String> features)
: modifier = null,
type = null,
features = new List.unmodifiable(features);
@ -45,10 +41,10 @@ class CssMediaQuery implements AstNode {
/// Merges this with [other] to return a query that matches the intersection
/// of both inputs.
CssMediaQuery merge(CssMediaQuery other) {
var ourModifier = this.modifier?.value?.toLowerCase();
var ourType = this.type?.value?.toLowerCase();
var theirModifier = other.modifier?.value?.toLowerCase();
var theirType = other.type?.value?.toLowerCase();
var ourModifier = this.modifier?.toLowerCase();
var ourType = this.type?.toLowerCase();
var theirModifier = other.modifier?.toLowerCase();
var theirType = other.type?.toLowerCase();
if (ourType == null && theirType == null) {
return new CssMediaQuery.condition(
@ -84,4 +80,15 @@ class CssMediaQuery implements AstNode {
modifier: modifier == ourModifier ? this.modifier : other.modifier,
features: features.toList()..addAll(other.features));
}
String toString() {
var buffer = new StringBuffer();
if (modifier != null) buffer.write("$modifier ");
if (type != null) {
buffer.write(type);
if (features.isNotEmpty) buffer.write(" and ");
}
buffer.write(features.join(" and "));
return buffer.toString();
}
}

View File

@ -17,7 +17,8 @@ class CssMediaRule extends CssParentNode {
final FileSpan span;
CssMediaRule(this.queries, this.span) {
CssMediaRule(Iterable<CssMediaQuery> queries, this.span)
: queries = new List.unmodifiable(queries) {
if (queries.isEmpty) {
throw new ArgumentError.value(queries, "queries", "may not be empty.");
}

View File

@ -24,7 +24,6 @@ export 'sass/expression/unary_operation.dart';
export 'sass/expression/value.dart';
export 'sass/expression/variable.dart';
export 'sass/interpolation.dart';
export 'sass/media_query.dart';
export 'sass/node.dart';
export 'sass/statement.dart';
export 'sass/statement/at_root_rule.dart';

View File

@ -1,57 +0,0 @@
// 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 '../../utils.dart';
import '../node.dart';
import 'interpolation.dart';
import 'node.dart';
/// A media query, as used in `@media` and `@import`.
class MediaQuery implements SassNode {
/// The modifier, which is expected (but not required) to evaluate to "not" or
/// "only".
///
/// This may be `null` if no modifier is in use.
final Interpolation modifier;
/// The media type.
///
/// This may be `null`. If so, [features] will not be empty.
final Interpolation type;
/// Feature queries.
final List<Interpolation> features;
FileSpan get span {
var components = <AstNode>[];
if (modifier != null) components.add(modifier);
if (type != null) components.add(type);
components.addAll(features);
return spanForList(components);
}
/// Creates a media query specifies a type and, optionally, features.
MediaQuery(this.type, {this.modifier, Iterable<Interpolation> features})
: features =
features == null ? const [] : new List.unmodifiable(features);
/// Creates a media query that only specifies features.
MediaQuery.condition(Iterable<Interpolation> features,
{this.modifier, this.type})
: features = new List.unmodifiable(features);
String toString() {
var buffer = new StringBuffer();
if (modifier != null) buffer.write("$modifier ");
if (type != null) {
buffer.write(type);
if (features.isNotEmpty) buffer.write(" and ");
}
buffer.write(features.join(" and "));
return buffer.toString();
}
}

View File

@ -5,32 +5,26 @@
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
import '../media_query.dart';
import '../interpolation.dart';
import '../statement.dart';
/// A `@media` rule.
class MediaRule implements Statement {
/// The queries that select what browsers and conditions this rule targets.
/// The query that determines on which platforms the styles will be in effect.
///
/// This is never empty.
final List<MediaQuery> queries;
/// This is only parsed after the interpolation has been resolved.
final Interpolation query;
/// The contents of this rule.
final List<Statement> children;
final FileSpan span;
MediaRule(
Iterable<MediaQuery> queries, Iterable<Statement> children, this.span)
: queries = new List.unmodifiable(queries),
children = new List.unmodifiable(children) {
if (this.queries.isEmpty) {
throw new ArgumentError("queries may not be empty.");
}
}
MediaRule(this.query, Iterable<Statement> children, this.span)
: children = new List.unmodifiable(children);
/*=T*/ accept/*<T>*/(StatementVisitor/*<T>*/ visitor) =>
visitor.visitMediaRule(this);
String toString() => "@media ${queries.join(", ")} {${children.join(" ")}}";
String toString() => "@media $query {${children.join(" ")}}";
}

View File

@ -7,7 +7,6 @@ import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
import '../interpolation.dart';
import '../media_query.dart';
import '../statement.dart';
import '../supports_condition.dart';
@ -24,13 +23,11 @@ class PlainImportRule implements Statement {
/// The media query attached to this import, or `null` if no condition is
/// attached.
final List<MediaQuery> media;
final Interpolation media;
final FileSpan span;
PlainImportRule(this.url, this.span,
{this.supports, Iterable<MediaQuery> media})
: media = media == null ? null : new List.unmodifiable(media);
PlainImportRule(this.url, this.span, {this.supports, this.media});
/*=T*/ accept/*<T>*/(StatementVisitor/*<T>*/ visitor) =>
visitor.visitPlainImportRule(this);

View File

@ -0,0 +1,77 @@
// 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 '../ast/css.dart';
import '../utils.dart';
import 'parser.dart';
/// A parser for `@media` queries.
class MediaQueryParser extends Parser {
MediaQueryParser(String contents, {url}) : super(contents, url: url);
List<CssMediaQuery> parse() {
return wrapSpanFormatException(() {
var queries = <CssMediaQuery>[];
do {
whitespace();
queries.add(_mediaQuery());
} while (scanner.scanChar($comma));
return queries;
});
}
/// Consumes a single media query.
CssMediaQuery _mediaQuery() {
// This is somewhat duplicated in StylesheetParser._mediaQuery.
String modifier;
String type;
if (scanner.peekChar() != $lparen) {
var identifier1 = identifier();
whitespace();
if (!lookingAtIdentifier()) {
// For example, "@media screen {"
return new CssMediaQuery(identifier1);
}
var identifier2 = identifier();
whitespace();
if (equalsIgnoreCase(identifier2, "and")) {
// For example, "@media screen and ..."
type = identifier1;
} else {
modifier = identifier1;
type = identifier2;
if (scanIdentifier("and", ignoreCase: true)) {
// For example, "@media only screen and ..."
whitespace();
} else {
// For example, "@media only screen {"
return new CssMediaQuery(type, modifier: modifier);
}
}
}
// We've consumed either `IDENTIFIER "and"` or
// `IDENTIFIER IDENTIFIER "and"`.
var features = <String>[];
do {
whitespace();
scanner.expectChar($lparen);
features.add("(${declarationValue()})");
scanner.expectChar($rparen);
whitespace();
} while (scanIdentifier("and", ignoreCase: true));
if (type == null) {
return new CssMediaQuery.condition(features);
} else {
return new CssMediaQuery(type, modifier: modifier, features: features);
}
}
}

View File

@ -437,11 +437,13 @@ abstract class Parser {
// See also [ScssParser._lookingAtInterpolatedIdentifier].
var first = scanner.peekChar();
if (first == null) return false;
if (isNameStart(first) || first == $backslash) return true;
if (first != $dash) return false;
var second = scanner.peekChar(1);
return isNameStart(second) || second == $dash || second == $backslash;
return second != null &&
(isNameStart(second) || second == $dash || second == $backslash);
}
/// Consumes an identifier if its name exactly matches [text].

View File

@ -675,7 +675,7 @@ abstract class StylesheetParser extends Parser {
/// Consumes a supports condition and/or a media query after an `@import`.
///
/// Returns `null` if neither type of query can be found.
Tuple2<SupportsCondition, List<MediaQuery>> _tryImportQueries() {
Tuple2<SupportsCondition, Interpolation> _tryImportQueries() {
SupportsCondition supports;
if (scanIdentifier("supports", ignoreCase: true)) {
scanner.expectChar($lparen);
@ -731,8 +731,11 @@ abstract class StylesheetParser extends Parser {
/// Consumes a `@media` rule.
///
/// [start] should point before the `@`.
MediaRule _mediaRule(LineScannerState start) => new MediaRule(
_mediaQueryList(), children(_ruleChild), scanner.spanFrom(start));
MediaRule _mediaRule(LineScannerState start) {
var query = _mediaQueryList();
var children = this.children(_ruleChild);
return new MediaRule(query, children, scanner.spanFrom(start));
}
/// Consumes a mixin declaration.
///
@ -2169,43 +2172,47 @@ abstract class StylesheetParser extends Parser {
// ## Media Queries
/// Consumes a list of media queries.
List<MediaQuery> _mediaQueryList() {
var queries = <MediaQuery>[];
do {
Interpolation _mediaQueryList() {
var start = scanner.state;
var buffer = new InterpolationBuffer();
while (true) {
whitespace();
queries.add(_mediaQuery());
} while (scanner.scanChar($comma));
return queries;
_mediaQuery(buffer);
if (!scanner.scanChar($comma)) break;
buffer.writeCharCode($comma);
buffer.writeCharCode($space);
}
return buffer.interpolation(scanner.spanFrom(start));
}
/// Consumes a single media query.
MediaQuery _mediaQuery() {
Interpolation modifier;
Interpolation type;
void _mediaQuery(InterpolationBuffer buffer) {
// This is somewhat duplicated in MediaQueryParser._mediaQuery.
if (scanner.peekChar() != $lparen) {
var identifier1 = _interpolatedIdentifier();
buffer.addInterpolation(_interpolatedIdentifier());
whitespace();
if (!_lookingAtInterpolatedIdentifier()) {
// For example, "@media screen {"
return new MediaQuery(identifier1);
// For example, "@media screen {".
return;
}
var identifier2 = _interpolatedIdentifier();
buffer.writeCharCode($space);
var identifier = _interpolatedIdentifier();
whitespace();
if (equalsIgnoreCase(identifier2.asPlain, "and")) {
if (equalsIgnoreCase(identifier.asPlain, "and")) {
// For example, "@media screen and ..."
type = identifier1;
buffer.write("and ");
} else {
modifier = identifier1;
type = identifier2;
buffer.addInterpolation(identifier);
if (scanIdentifier("and", ignoreCase: true)) {
// For example, "@media only screen and ..."
whitespace();
buffer.write("and ");
} else {
// For example, "@media only screen {"
return new MediaQuery(type, modifier: modifier);
return;
}
}
}
@ -2213,17 +2220,12 @@ abstract class StylesheetParser extends Parser {
// We've consumed either `IDENTIFIER "and"` or
// `IDENTIFIER IDENTIFIER "and"`.
var features = <Interpolation>[];
do {
while (true) {
whitespace();
features.add(_queryExpression());
buffer.addInterpolation(_queryExpression());
whitespace();
} while (scanIdentifier("and", ignoreCase: true));
if (type == null) {
return new MediaQuery.condition(features);
} else {
return new MediaQuery(type, modifier: modifier, features: features);
if (!scanIdentifier("and", ignoreCase: true)) break;
buffer.write(" and ");
}
}

View File

@ -585,11 +585,11 @@ class _PerformVisitor
"Media rules may not be used within nested declarations.", node.span);
}
var queryIterable = node.queries.map(_visitMediaQuery);
var queries = _mediaQueries == null
? new List<CssMediaQuery>.unmodifiable(queryIterable)
: _mergeMediaQueries(_mediaQueries, queryIterable);
if (queries.isEmpty) return null;
var queries = _visitMediaQueries(node.query);
if (_mediaQueries != null) {
queries = _mergeMediaQueries(_mediaQueries, queries);
if (queries.isEmpty) return null;
}
_withParent(new CssMediaRule(queries, node.span), () {
_withMediaQueries(queries, () {
@ -615,6 +615,12 @@ class _PerformVisitor
return null;
}
/// Evaluates [interpolation] and parses the result as a list of media
/// queries.
List<CssMediaQuery> _visitMediaQueries(Interpolation interpolation) =>
_adjustParseError(interpolation.span,
() => CssMediaQuery.parseList(_performInterpolation(interpolation)));
/// Returns a list of queries that selects for platforms that match both
/// [queries1] and [queries2].
List<CssMediaQuery> _mergeMediaQueries(
@ -624,20 +630,6 @@ class _PerformVisitor
}).where((query) => query != null));
}
/// Evaluates [query] and converts it to a plain CSS query.
CssMediaQuery _visitMediaQuery(MediaQuery query) {
var modifier =
query.modifier == null ? null : _interpolationToValue(query.modifier);
var type = query.type == null ? null : _interpolationToValue(query.type);
var features =
query.features.map((feature) => _interpolationToValue(feature));
if (type == null) return new CssMediaQuery.condition(features);
return new CssMediaQuery(type, modifier: modifier, features: features);
}
Value visitPlainImportRule(PlainImportRule node) {
var url = _interpolationToValue(node.url);
var supports = node.supports;
@ -645,11 +637,12 @@ class _PerformVisitor
? "${supports.name.accept(this).toCssString()}: "
"${supports.value.accept(this).toCssString()})"
: (supports == null ? null : _visitSupportsCondition(supports));
var mediaQuery = node.media == null ? null : _visitMediaQueries(node.media);
_parent.addChild(new CssImport(url, node.span,
supports: resolvedSupports == null
? null
: new CssValue(resolvedSupports, node.supports.span),
media: node?.media?.map(_visitMediaQuery)));
media: mediaQuery));
return null;
}

View File

@ -132,7 +132,7 @@ class _SerializeCssVisitor
void visitMediaRule(CssMediaRule node) {
_writeIndentation();
_buffer.write("@media ");
_writeBetween(node.queries, ", ", visitMediaQuery);
_writeBetween(node.queries, ", ", _visitMediaQuery);
_buffer.writeCharCode($space);
_visitChildren(node.children);
}
@ -149,7 +149,7 @@ class _SerializeCssVisitor
if (node.media != null) {
_buffer.writeCharCode($space);
_writeBetween(node.media, ', ', visitMediaQuery);
_writeBetween(node.media, ', ', _visitMediaQuery);
}
_buffer.writeCharCode($semicolon);
@ -162,14 +162,14 @@ class _SerializeCssVisitor
_visitChildren(node.children);
}
void visitMediaQuery(CssMediaQuery query) {
void _visitMediaQuery(CssMediaQuery query) {
if (query.modifier != null) {
_buffer.write(query.modifier.value);
_buffer.write(query.modifier);
_buffer.writeCharCode($space);
}
if (query.type != null) {
_buffer.write(query.type.value);
_buffer.write(query.type);
if (query.features.isNotEmpty) _buffer.write(" and ");
}