mirror of
https://github.com/danog/dart-sass.git
synced 2024-11-27 04:34:59 +01:00
Emit compressed output
This commit is contained in:
parent
8d7964e9be
commit
ac0ab00120
10
CHANGELOG.md
10
CHANGELOG.md
@ -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.
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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 true—for a rule
|
||||
/// like `@foo {}`, [children] is empty but [isChildless] is `false`.
|
||||
bool get isChildless => false;
|
||||
|
||||
CssParentNode() : this._([]);
|
||||
|
||||
|
@ -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].
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
251
test/compressed_test.dart
Normal 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);
|
Loading…
Reference in New Issue
Block a user