mirror of
https://github.com/danog/dart-sass.git
synced 2024-12-03 10:08:01 +01:00
Reparse media queries at perform-time.
This commit is contained in:
parent
6f6eb79dae
commit
868286911b
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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.");
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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(" ")}}";
|
||||
}
|
||||
|
@ -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);
|
||||
|
77
lib/src/parse/media_query.dart
Normal file
77
lib/src/parse/media_query.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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].
|
||||
|
@ -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 ");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 ");
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user