Emit compressed output

This commit is contained in:
Natalie Weizenbaum 2018-01-14 16:45:33 -08:00
parent 8d7964e9be
commit ac0ab00120
10 changed files with 500 additions and 77 deletions

View File

@ -1,5 +1,7 @@
## 1.0.0-beta.5
* Add a `compressed` output style.
* Emit a warning when `&&` is used, since it's probably not what the user means.
* `round()` now returns the correct results for negative numbers that should
@ -24,6 +26,14 @@
`sassTrue`, `sassFalse`, and `sassNull` values, which represent Sass values
that may be passed into or returned from custom functions.
* Expose the `OutputStyle` enum, and add a `style` parameter to `compile()`,
`compleString()`, `compileAsync()`, and `compileStringAsync()` that allows
users to control the output style.
### Node JS API
* Support the `"compressed"` value for the `outputStyle` option.
## 1.0.0-beta.4
* Support unquoted imports in the indented syntax.

View File

@ -24,12 +24,6 @@ class CssAtRule extends CssParentNode {
final FileSpan span;
/// An unknown at-rule is never invisible.
///
/// Because we don't know the semantics of unknown rules, we can't guarantee
/// that (for example) `@foo {}` isn't meaningful.
bool get isInvisible => false;
CssAtRule(this.name, this.span, {bool childless: false, this.value})
: isChildless = childless;

View File

@ -2,6 +2,7 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:charcode/charcode.dart';
import 'package:source_span/source_span.dart';
import '../../visitor/interface/css.dart';
@ -16,6 +17,10 @@ class CssComment extends CssNode {
final FileSpan span;
/// Whether this comment starts with `/*!` and so should be preserved even in
/// compressed mode.
bool get isPreserved => text.codeUnitAt(2) == $exclamation;
CssComment(this.text, this.span);
T accept<T>(CssVisitor<T> visitor) => visitor.visitComment(this);

View File

@ -20,6 +20,9 @@ class CssMediaQuery {
/// Feature queries, including parentheses.
final List<String> features;
/// Whether this media query only specifies features.
bool get isCondition => modifier == null && type == null;
/// Parses a media query from [contents].
///
/// If passed, [url] is the name of the file from which [contents] comes.

View File

@ -7,6 +7,8 @@ import 'dart:collection';
import '../../visitor/interface/css.dart';
import '../../visitor/serialize.dart';
import '../node.dart';
import 'at_rule.dart';
import 'style_rule.dart';
/// A statement in a plain CSS syntax tree.
abstract class CssNode extends AstNode {
@ -19,9 +21,6 @@ abstract class CssNode extends AstNode {
/// This makes [remove] more efficient.
int _indexInParent;
/// If `true`, this node should not be emitted to CSS.
bool get isInvisible => false;
/// Whether this was generated from the last node in a nested Sass tree that
/// got flattened during evaluation.
var isGroupEnd = false;
@ -32,11 +31,31 @@ abstract class CssNode extends AstNode {
var siblings = _parent.children;
for (var i = _indexInParent + 1; i < siblings.length; i++) {
var sibling = siblings[i];
if (!sibling.isInvisible) return true;
if (!_isInvisible(sibling)) return true;
}
return false;
}
/// Returns whether [node] is invisible for the purposes of
/// [hasFollowingSibling].
///
/// This can return a false negative for a comment node in compressed mode,
/// since the AST doesn't know the output style, but that's an extremely
/// narrow edge case so we don't worry about it.
bool _isInvisible(CssNode node) {
if (node is CssParentNode) {
// An unknown at-rule is never invisible. Because we don't know the
// semantics of unknown rules, we can't guarantee that (for example)
// `@foo {}` isn't meaningful.
if (node is CssAtRule) return false;
if (node is CssStyleRule && node.selector.value.isInvisible) return true;
return node.children.every(_isInvisible);
} else {
return false;
}
}
/// Calls the appropriate visit method on [visitor].
T accept<T>(CssVisitor<T> visitor);
@ -66,16 +85,12 @@ abstract class CssParentNode extends CssNode {
final List<CssNode> children;
final List<CssNode> _children;
/// By default, a parent node is invisible if it's empty or if all its
/// children are invisible.
bool get isInvisible {
if (_isInvisible == null) {
_isInvisible = children.every((child) => child.isInvisible);
}
return _isInvisible;
}
bool _isInvisible;
/// Whether the rule has no children and should be emitted without curly
/// braces.
///
/// This implies `children.isEmpty`, but the reverse is not truefor a rule
/// like `@foo {}`, [children] is empty but [isChildless] is `false`.
bool get isChildless => false;
CssParentNode() : this._([]);

View File

@ -23,14 +23,6 @@ class CssStyleRule extends CssParentNode {
final FileSpan span;
/// A style rule is invisible if it's empty, if all its children are
/// invisible, or if every complex selector in [selector] contains a
/// placeholder.
bool get isInvisible {
if (super.isInvisible) return true;
return selector.value.isInvisible;
}
/// Creates a new [CssStyleRule].
///
/// If [originalSelector] isn't passed, it defaults to [selector.value].

View File

@ -781,8 +781,8 @@ class _EvaluateVisitor
var url = await _interpolationToValue(import.url);
var supports = import.supports;
var resolvedSupports = supports is SupportsDeclaration
? "(${await _evaluateToCss(supports.name)}: "
"${await _evaluateToCss(supports.value)})"
? "${await _evaluateToCss(supports.name)}: "
"${await _evaluateToCss(supports.value)}"
: (supports == null ? null : await _visitSupportsCondition(supports));
var mediaQuery =
import.media == null ? null : await _visitMediaQueries(import.media);

View File

@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/synchronize.dart for details.
//
// Checksum: 5179f301f1c56e3690e6de5aa89be5c6ad03f6ff
// Checksum: d4ffc2a9cc2c3e8e202705c44e03c7acb7822356
import 'dart:math' as math;
@ -775,8 +775,8 @@ class _EvaluateVisitor
var url = _interpolationToValue(import.url);
var supports = import.supports;
var resolvedSupports = supports is SupportsDeclaration
? "(${_evaluateToCss(supports.name)}: "
"${_evaluateToCss(supports.value)})"
? "${_evaluateToCss(supports.name)}: "
"${_evaluateToCss(supports.value)}"
: (supports == null ? null : _visitSupportsCondition(supports));
var mediaQuery =
import.media == null ? null : _visitMediaQueries(import.media);

View File

@ -44,7 +44,11 @@ String serialize(CssNode node,
node.accept(visitor);
var result = visitor._buffer.toString();
if (result.codeUnits.any((codeUnit) => codeUnit > 0x7F)) {
result = '@charset "UTF-8";\n$result';
if (style == OutputStyle.compressed) {
result = '\uFEFF$result';
} else {
result = '@charset "UTF-8";\n$result';
}
}
return result;
}
@ -83,6 +87,9 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
/// The current indentation of the CSS output.
var _indentation = 0;
/// The style of CSS to generate.
final OutputStyle _style;
/// Whether we're emitting an unambiguous representation of the source
/// structure, as opposed to valid CSS.
final bool _inspect;
@ -99,6 +106,9 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
/// The characters to use for a line feed.
final LineFeed _lineFeed;
/// Whether we're emitting compressed output.
bool get _isCompressed => _style == OutputStyle.compressed;
_SerializeVisitor(
{OutputStyle style,
bool inspect: false,
@ -106,7 +116,8 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
bool useSpaces: true,
int indentWidth,
LineFeed lineFeed})
: _inspect = inspect,
: _style = style ?? OutputStyle.expanded,
_inspect = inspect,
_quote = quote,
_indentCharacter = useSpaces ? $space : $tab,
_indentWidth = indentWidth ?? 2,
@ -121,16 +132,24 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
if (_isInvisible(child)) continue;
if (previous != null) {
_buffer.write(_lineFeed.text);
if (previous.isGroupEnd) _buffer.write(_lineFeed.text);
if (_requiresSemicolon(previous)) _buffer.writeCharCode($semicolon);
_writeLineFeed();
if (previous.isGroupEnd) _writeLineFeed();
}
previous = child;
child.accept(this);
}
if (previous != null && _requiresSemicolon(previous) && !_isCompressed) {
_buffer.writeCharCode($semicolon);
}
}
void visitComment(CssComment node) {
// Preserve comments that start with `/*!`.
if (_isCompressed && !node.isPreserved) return;
var minimumIndentation = _minimumIndentation(node.text);
assert(minimumIndentation != -1);
if (minimumIndentation == null) {
@ -157,44 +176,66 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
_buffer.write(node.value.value);
}
if (node.isChildless) {
_buffer.writeCharCode($semicolon);
} else {
_buffer.writeCharCode($space);
if (!node.isChildless) {
_writeOptionalSpace();
_visitChildren(node.children);
}
}
void visitMediaRule(CssMediaRule node) {
_writeIndentation();
_buffer.write("@media ");
_writeBetween(node.queries, ", ", _visitMediaQuery);
_buffer.writeCharCode($space);
_buffer.write("@media");
if (!_isCompressed || !node.queries.first.isCondition) {
_buffer.writeCharCode($space);
}
_writeBetween(node.queries, _commaSeparator, _visitMediaQuery);
_writeOptionalSpace();
_visitChildren(node.children);
}
void visitImport(CssImport node) {
_writeIndentation();
_buffer.write("@import ");
_buffer.write(node.url.value);
_buffer.write("@import");
_writeOptionalSpace();
_writeImportUrl(node.url.value);
if (node.supports != null) {
_buffer.writeCharCode($space);
_writeOptionalSpace();
_buffer.write(node.supports.value);
}
if (node.media != null) {
_buffer.writeCharCode($space);
_writeBetween(node.media, ', ', _visitMediaQuery);
_writeOptionalSpace();
_writeBetween(node.media, _commaSeparator, _visitMediaQuery);
}
}
/// Writes [url], which is an import's URL, to the buffer.
void _writeImportUrl(String url) {
if (!_isCompressed || url.codeUnitAt(0) != $u) {
_buffer.write(url);
return;
}
_buffer.writeCharCode($semicolon);
// If this is url(...), remove the surrounding function. This is terser and
// it allows us to remove whitespace between `@import` and the URL.
var urlContents = url.substring(4, url.length - 1);
var maybeQuote = urlContents.codeUnitAt(0);
if (maybeQuote == $single_quote || maybeQuote == $double_quote) {
_buffer.write(urlContents);
} else {
// If the URL didn't contain quotes, write them manually.
_visitQuotedString(urlContents);
}
}
void visitKeyframeBlock(CssKeyframeBlock node) {
_writeIndentation();
_writeBetween(node.selector.value, ", ", _buffer.write);
_buffer.writeCharCode($space);
_writeBetween(node.selector.value, _commaSeparator, _buffer.write);
_writeOptionalSpace();
_visitChildren(node.children);
}
@ -206,24 +247,33 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
if (query.type != null) {
_buffer.write(query.type);
if (query.features.isNotEmpty) _buffer.write(" and ");
if (query.features.isNotEmpty) {
_buffer.write(" and");
_writeOptionalSpace();
}
}
_writeBetween(query.features, " and ", _buffer.write);
_writeBetween(
query.features, _isCompressed ? "and" : " and ", _buffer.write);
}
void visitStyleRule(CssStyleRule node) {
_writeIndentation();
node.selector.value.accept(this);
_buffer.writeCharCode($space);
_writeOptionalSpace();
_visitChildren(node.children);
}
void visitSupportsRule(CssSupportsRule node) {
_writeIndentation();
_buffer.write("@supports ");
_buffer.write("@supports");
if (!(_isCompressed && node.condition.value.codeUnitAt(0) == $lparen)) {
_buffer.writeCharCode($space);
}
_buffer.write(node.condition.value);
_buffer.writeCharCode($space);
_writeOptionalSpace();
_visitChildren(node.children);
}
@ -231,29 +281,50 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
_writeIndentation();
_buffer.write(node.name.value);
_buffer.writeCharCode($colon);
if (_shouldReindentValue(node)) {
_writeReindentedValue(node);
if (_isParsedCustomProperty(node)) {
if (_isCompressed) {
_writeFoldedValue(node);
} else {
_writeReindentedValue(node);
}
} else {
_buffer.writeCharCode($space);
_writeOptionalSpace();
_visitValue(node.value);
}
_buffer.writeCharCode($semicolon);
}
/// Returns whether [node]'s value should be re-indented when being written to
/// the stylesheet.
/// Returns whether [node] is a custom property that was parsed as a custom
/// property (rather than being dynamically generated, as in `#{--foo}: ...`).
///
/// We only re-indent custom property values that were parsed as custom
/// properties, which we detect as unquoted strings. It's possible to have
/// false positives here, since someone could write `#{--foo}: unquoted`, but
/// that's unlikely enough that we can spare the extra time a no-op
/// reindenting will take.
bool _shouldReindentValue(CssDeclaration node) {
bool _isParsedCustomProperty(CssDeclaration node) {
if (!node.name.value.startsWith("--")) return false;
var value = node.value.value;
return value is SassString && !value.hasQuotes;
}
/// Emits the value of [node], with all newlines followed by whitespace
void _writeFoldedValue(CssDeclaration node) {
var scanner = new StringScanner((node.value.value as SassString).text);
while (!scanner.isDone) {
var next = scanner.readChar();
if (next != $lf) {
_buffer.writeCharCode(next);
continue;
}
_buffer.writeCharCode($space);
while (isWhitespace(scanner.peekChar())) {
scanner.readChar();
}
}
}
/// Emits the value of [node], re-indented relative to the current indentation.
void _writeReindentedValue(CssDeclaration node) {
var value = (node.value.value as SassString).text;
@ -365,6 +436,26 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
void visitBoolean(SassBoolean value) => _buffer.write(value.value.toString());
void visitColor(SassColor value) {
// In compressed mode, emit colors in the shortest representation possible.
if (_isCompressed && fuzzyEquals(value.alpha, 1)) {
var name = namesByColor[value];
var hexLength = _canUseShortHex(value) ? 4 : 7;
if (name.length <= hexLength) {
_buffer.write(name);
} else if (_canUseShortHex(value)) {
_buffer.writeCharCode($hash);
_buffer.writeCharCode(hexCharFor(value.red & 0xF));
_buffer.writeCharCode(hexCharFor(value.green & 0xF));
_buffer.writeCharCode(hexCharFor(value.blue & 0xF));
} else {
_buffer.writeCharCode($hash);
_writeHexComponent(value.red);
_writeHexComponent(value.green);
_writeHexComponent(value.blue);
}
return;
}
if (value.original != null) {
_buffer.write(value.original);
} else if (namesByColor.containsKey(value) &&
@ -372,18 +463,35 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
// around an IE bug. See sass/sass#1782.
!fuzzyEquals(value.alpha, 0)) {
_buffer.write(namesByColor[value]);
} else if (value.alpha == 1) {
} else if (fuzzyEquals(value.alpha, 1)) {
_buffer.writeCharCode($hash);
_writeHexComponent(value.red);
_writeHexComponent(value.green);
_writeHexComponent(value.blue);
} else {
_buffer.write("rgba(${value.red}, ${value.green}, ${value.blue}, ");
_buffer
..write("rgba(${value.red}")
..write(_commaSeparator)
..write(value.green)
..write(_commaSeparator)
..write(value.blue)
..write(_commaSeparator);
_writeNumber(value.alpha);
_buffer.writeCharCode($rparen);
}
}
/// Returns whether [color]'s hex pair representation is symmetrical (e.g.
/// `FF`).
bool _isSymmetricalHex(int color) => color & 0xF == color >> 4;
/// Returns whether [color] can be represented as a short hexadecimal color
/// (e.g. `#fff`).
bool _canUseShortHex(SassColor color) =>
_isSymmetricalHex(color.red) &&
_isSymmetricalHex(color.green) &&
_isSymmetricalHex(color.blue);
/// Emits [color] as a hex character pair.
void _writeHexComponent(int color) {
assert(color < 0x100);
@ -421,7 +529,7 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
_inspect
? value.contents
: value.contents.where((element) => !element.isBlank),
value.separator == ListSeparator.space ? " " : ", ",
value.separator == ListSeparator.space ? " " : _commaSeparator,
_inspect
? (element) {
var needsParens = _elementNeedsParens(value.separator, element);
@ -487,6 +595,12 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
return;
}
// 0 is valid for any unit context.
if (_isCompressed && fuzzyEquals(value.value, 0)) {
_buffer.writeCharCode($0);
return;
}
_writeNumber(value.value);
if (!_inspect) {
@ -516,6 +630,7 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
var text = number.toString();
if (text.contains("e")) text = _removeExponent(text);
if (_isCompressed && text.codeUnitAt(0) == $0) text = text.substring(1);
// Any double that doesn't contain "e" and is less than
// `SassNumber.precision + 2` digits long is guaranteed to be safe to emit
@ -802,9 +917,9 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
} else {
_buffer.writeCharCode($comma);
if (complex.lineBreak) {
_buffer.write(_lineFeed.text);
_writeLineFeed();
} else {
_buffer.writeCharCode($space);
_writeOptionalSpace();
}
}
visitComplexSelector(complex);
@ -865,30 +980,51 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
return;
}
_buffer.write(_lineFeed.text);
_writeLineFeed();
CssNode previous;
_indent(() {
CssNode previous;
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (_isInvisible(child)) continue;
if (previous != null) {
_buffer.write(_lineFeed.text);
if (previous.isGroupEnd) _buffer.write(_lineFeed.text);
if (_requiresSemicolon(previous)) _buffer.writeCharCode($semicolon);
_writeLineFeed();
if (previous.isGroupEnd) _writeLineFeed();
}
previous = child;
child.accept(this);
}
});
_buffer.write(_lineFeed.text);
if (_requiresSemicolon(previous) && !_isCompressed) {
_buffer.writeCharCode($semicolon);
}
_writeLineFeed();
_writeIndentation();
_buffer.writeCharCode($rbrace);
}
/// Whether [node] requires a semicolon to be written after it.
bool _requiresSemicolon(CssNode node) =>
node is CssParentNode ? node.isChildless : node is! CssComment;
/// Writes a line feed, unless this emitting compressed CSS.
void _writeLineFeed() {
if (!_isCompressed) _buffer.write(_lineFeed.text);
}
/// Writes a space unless [_style] is [OutputStyle.compressed].
void _writeOptionalSpace() {
if (!_isCompressed) _buffer.writeCharCode($space);
}
/// Writes indentation based on [_indentation].
void _writeIndentation() =>
_writeTimes(_indentCharacter, _indentation * _indentWidth);
void _writeIndentation() {
if (_isCompressed) return;
_writeTimes(_indentCharacter, _indentation * _indentWidth);
}
/// Writes [char] to [_buffer] with [times] repetitions.
void _writeTimes(int char, int times) {
@ -912,6 +1048,9 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
}
}
/// Returns a comma used to separate values in lists.
String get _commaSeparator => _isCompressed ? "," : ", ";
/// Runs [callback] with indentation increased one level.
void _indent(void callback()) {
_indentation++;
@ -920,7 +1059,21 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
}
/// Returns whether [node] is considered invisible.
bool _isInvisible(CssNode node) => !_inspect && node.isInvisible;
bool _isInvisible(CssNode node) {
if (_inspect) return false;
if (_isCompressed && node is CssComment && !node.isPreserved) return true;
if (node is CssParentNode) {
// An unknown at-rule is never invisible. Because we don't know the
// semantics of unknown rules, we can't guarantee that (for example)
// `@foo {}` isn't meaningful.
if (node is CssAtRule) return false;
if (node is CssStyleRule && node.selector.value.isInvisible) return true;
return node.children.every(_isInvisible);
} else {
return false;
}
}
/// Returns whether [text] is a valid identifier.
bool _isIdentifier(String text) {

251
test/compressed_test.dart Normal file
View File

@ -0,0 +1,251 @@
// Copyright 2018 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:test/test.dart';
import 'package:sass/sass.dart';
void main() {
group("in style rules", () {
test("removes unnecessary whitespace and semicolons", () {
expect(_compile("a {x: y}"), equals("a{x:y}"));
});
group("for selectors", () {
test("preserves whitespace where necessary", () {
expect(_compile("a b .c {x: y}"), equals("a b .c{x:y}"));
});
test("removes whitespace after commas", () {
expect(_compile("a, b, .c {x: y}"), equals("a,b,.c{x:y}"));
});
test("doesn't preserve newlines", () {
expect(_compile("a,\nb,\n.c {x: y}"), equals("a,b,.c{x:y}"));
});
group("in prefixed pseudos", () {
test("preserves whitespace", () {
expect(_compile("a:nth-child(2n of b) {x: y}"),
equals("a:nth-child(2n of b){x:y}"));
});
test("removes whitespace after commas", () {
expect(_compile("a:nth-child(2n of b, c) {x: y}"),
equals("a:nth-child(2n of b,c){x:y}"));
});
});
});
group("for declarations", () {
test("preserves semicolons when necessary", () {
expect(_compile("a {q: r; s: t}"), equals("a{q:r;s:t}"));
});
group("of custom properties", () {
test("folds whitespace for multiline properties", () {
expect(_compile("""
a {
--foo: {
q: r;
b {
s: t;
}
}
}
"""), equals("a{--foo: { q: r; b { s: t; } } }"));
});
test("folds whitespace for single-line properties", () {
expect(_compile("""
a {
--foo: a b\t\tc;
}
"""), equals("a{--foo: a b\tc}"));
});
test("preserves semicolons when necessary", () {
expect(_compile("""
a {
--foo: {
a: b;
};
--bar: x y;
--baz: q r;
}
"""), equals("a{--foo: { a: b; };--bar: x y;--baz: q r}"));
});
});
});
});
group("values:", () {
group("numbers", () {
test("omit the leading 0", () {
expect(_compile("a {b: 0.123}"), equals("a{b:.123}"));
expect(_compile("a {b: 0.123px}"), equals("a{b:.123px}"));
});
test("omit units for 0 values", () {
expect(_compile("a {b: 0px}"), equals("a{b:0}"));
expect(_compile("a {b: 0.000000000000001px}"), equals("a{b:0}"));
});
});
group("lists", () {
test("don't include spaces after commas", () {
expect(_compile("a {b: x, y, z}"), equals("a{b:x,y,z}"));
});
test("do include spaces when space-separated", () {
expect(_compile("a {b: x y z}"), equals("a{b:x y z}"));
});
});
group("colors", () {
test("use names when they're shortest", () {
expect(_compile("a {b: #f00}"), equals("a{b:red}"));
});
test("use terse hex when it's shortest", () {
expect(_compile("a {b: white}"), equals("a{b:#fff}"));
});
test("use verbose hex when it's shortest", () {
expect(_compile("a {b: darkgoldenrod}"), equals("a{b:#b8860b}"));
});
test("use rgba() when necessary", () {
expect(_compile("a {b: rgba(255, 0, 0, 0.5)}"),
equals("a{b:rgba(255,0,0,.5)}"));
});
});
});
group("the top level", () {
test("removes whitespace and semicolons between at-rules", () {
expect(_compile("@foo; @bar; @baz;"), equals("@foo;@bar;@baz"));
});
test("removes whitespace between style rules", () {
expect(_compile("a {b: c} x {y: z}"), equals("a{b:c}x{y:z}"));
});
});
group("@supports", () {
test("removes whitespace around the condition", () {
expect(_compile("@supports (display: flex) {a {b: c}}"),
equals("@supports(display: flex){a{b:c}}"));
});
test("preserves whitespace before the condition if necessary", () {
expect(_compile("@supports not (display: flex) {a {b: c}}"),
equals("@supports not (display: flex){a{b:c}}"));
});
});
group("@media", () {
test("removes whitespace around the query", () {
expect(_compile("@media (min-width: 900px) {a {b: c}}"),
equals("@media(min-width: 900px){a{b:c}}"));
});
test("preserves whitespace before the query if necessary", () {
expect(_compile("@media screen {a {b: c}}"),
equals("@media screen{a{b:c}}"));
});
test("preserves whitespace before the query if necessary", () {
expect(_compile("@media screen {a {b: c}}"),
equals("@media screen{a{b:c}}"));
});
test('removes whitespace around "and"', () {
expect(
_compile("""
@media screen and (min-width: 900px) and (max-width: 100px) {
a {b: c}
}
"""),
equals("@media screen and(min-width: 900px)and(max-width: 100px)"
"{a{b:c}}"));
});
test("preserves whitespace around the modifier", () {
expect(_compile("@media only screen {a {b: c}}"),
equals("@media only screen{a{b:c}}"));
});
});
group("@keyframes", () {
test("removes whitespace after the selector", () {
expect(_compile("@keyframes a {from {a: b}}"),
equals("@keyframes a{from{a:b}}"));
});
test("removes whitespace after commas", () {
expect(_compile("@keyframes a {from, to {a: b}}"),
equals("@keyframes a{from,to{a:b}}"));
});
});
group("@import", () {
test("removes whitespace before the URL", () {
expect(_compile('@import "foo.css";'), equals('@import"foo.css"'));
});
test("converts a url() to a string", () {
expect(_compile('@import url(foo.css);'), equals('@import"foo.css"'));
expect(_compile('@import url("foo.css");'), equals('@import"foo.css"'));
});
test("removes whitespace before a media query", () {
expect(_compile('@import "foo.css" screen;'),
equals('@import"foo.css"screen'));
});
test("removes whitespace before a supports condition", () {
expect(_compile('@import "foo.css" supports(display: flex);'),
equals('@import"foo.css"supports(display: flex)'));
});
});
group("comments", () {
test("are removed", () {
expect(_compile("/* foo bar */"), isEmpty);
expect(_compile("""
a {
b: c;
/* foo bar */
d: e;
}
"""), equals("a{b:c;d:e}"));
});
test("remove their parents if they're the only contents", () {
expect(_compile("a {/* foo bar */}"), isEmpty);
expect(_compile("""
a {
/* foo bar */
/* baz bang */
}
"""), isEmpty);
});
test("are preserved with /*!", () {
expect(_compile("/*! foo bar */"), equals("/*! foo bar */"));
expect(
_compile("/*! foo */\n/*! bar */"), equals("/*! foo *//*! bar */"));
expect(_compile("""
a {
/*! foo bar */
}
"""), equals("a{/*! foo bar */}"));
});
});
}
/// Like [compileString], but always produces compressed output.
String _compile(String source) =>
compileString(source, style: OutputStyle.compressed);