diff --git a/lib/src/ast/sass/expression/number.dart b/lib/src/ast/sass/expression/number.dart index 8b201bb1..57c98dce 100644 --- a/lib/src/ast/sass/expression/number.dart +++ b/lib/src/ast/sass/expression/number.dart @@ -15,9 +15,13 @@ class NumberExpression implements Expression { /// The number's unit, or `null`. final String unit; + /// Whether the number produced should retain its original representation. + final bool hasOriginal; + final FileSpan span; - NumberExpression(this.value, this.span, {this.unit}); + NumberExpression(this.value, this.span, {this.unit, bool original: false}) + : hasOriginal = original; /*=T*/ accept/**/(ExpressionVisitor/**/ visitor) => visitor.visitNumberExpression(this); diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 0d018bb9..81221a4a 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -45,6 +45,9 @@ abstract class StylesheetParser extends Parser { /// or `@each`. var _inControlDirective = false; + /// Whether the parser is currently within a parenthesized expression. + var _inParentheses = false; + StylesheetParser(String contents, {url}) : super(contents, url: url); // ## Statements @@ -1229,38 +1232,44 @@ abstract class StylesheetParser extends Parser { /// Consumes a parenthesized expression. Expression _parentheses() { - var start = scanner.state; - scanner.expectChar($lparen); - whitespace(); - if (!_lookingAtExpression()) { + try { + _inParentheses = true; + var start = scanner.state; + scanner.expectChar($lparen); + whitespace(); + if (!_lookingAtExpression()) { + scanner.expectChar($rparen); + return new ListExpression([], ListSeparator.undecided, + span: scanner.spanFrom(start)); + } + + var first = _expressionUntilComma(); + if (scanner.scanChar($colon)) { + whitespace(); + _inParentheses = false; + return _map(first, start); + } + + if (!scanner.scanChar($comma)) { + scanner.expectChar($rparen); + return first; + } + whitespace(); + + var expressions = [first]; + while (true) { + if (!_lookingAtExpression()) break; + expressions.add(_expressionUntilComma()); + if (!scanner.scanChar($comma)) break; + whitespace(); + } + scanner.expectChar($rparen); - return new ListExpression([], ListSeparator.undecided, + return new ListExpression(expressions, ListSeparator.comma, span: scanner.spanFrom(start)); + } finally { + _inParentheses = false; } - - var first = _expressionUntilComma(); - if (scanner.scanChar($colon)) { - whitespace(); - return _map(first, start); - } - - if (!scanner.scanChar($comma)) { - scanner.expectChar($rparen); - return first; - } - whitespace(); - - var expressions = [first]; - while (true) { - if (!_lookingAtExpression()) break; - expressions.add(_expressionUntilComma()); - if (!scanner.scanChar($comma)) break; - whitespace(); - } - - scanner.expectChar($rparen); - return new ListExpression(expressions, ListSeparator.comma, - span: scanner.spanFrom(start)); } /// Consumes a map expression. @@ -1444,7 +1453,7 @@ abstract class StylesheetParser extends Parser { } return new NumberExpression(sign * number, scanner.spanFrom(start), - unit: unit); + unit: unit, original: !_inParentheses); } /// Consumes a variable expression. diff --git a/lib/src/value.dart b/lib/src/value.dart index 43375702..f4565c0e 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -281,6 +281,11 @@ abstract class Value { /// The SassScript unary `not` operation. Value unaryNot() => sassFalse; + /// Returns a copy of [this] without [SassNumber.original] set. + /// + /// If this isn't a [SassNumber], returns it as-is. + Value withoutOriginal() => this; + /// Returns a valid CSS representation of [this]. /// /// Throws a [SassScriptException] if [this] can't be represented in plain diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index 94f966f6..787cb097 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -161,6 +161,28 @@ class SassNumber extends Value { /// This number's denominator units. final List denominatorUnits; + /// Whether this should be represented as a slash-separated series of numbers. + /// + /// This is `true` if and only if [original] contains a slash. + bool get isSlashSeparated => _hasOriginal && _original != null; + + /// The original representation of the number. + /// + /// This is used to preserve slash-separated numbers in some contexts. + String get original { + if (_original != null) return _original; + if (_hasOriginal) return toCssString(); + return _original; + } + + final String _original; + + /// Whether this number still has its original representation. + /// + /// This is separate from [_original] so that we can avoid eagerly converting + /// all number literals to strings. + final bool _hasOriginal; + /// Whether [this] has any units. bool get hasUnits => numeratorUnits.isNotEmpty || denominatorUnits.isNotEmpty; @@ -181,14 +203,27 @@ class SassNumber extends Value { String get unitString => hasUnits ? _unitString(numeratorUnits, denominatorUnits) : ''; - /// Returns a number, optionally with a single numerator unit. + /// Creates a number, optionally with a single numerator unit. /// /// This matches the numbers that can be written as literals. /// [SassNumber.withUnits] can be used to construct more complex units. - SassNumber(num value, [String unit]) - : this.withUnits(value, numeratorUnits: unit == null ? null : [unit]); + SassNumber(this.value, [String unit]) + : numeratorUnits = + unit == null ? const [] : new List.unmodifiable([unit]), + denominatorUnits = const [], + _original = null, + _hasOriginal = false; - /// Returns a number with full [numeratorUnits] and [denominatorUnits]. + /// Like [new SassNumber], but sets [original] based on the string + /// representation of the number. + SassNumber.withOriginal(this.value, [String unit]) + : numeratorUnits = + unit == null ? const [] : new List.unmodifiable([unit]), + denominatorUnits = const [], + _original = null, + _hasOriginal = true; + + /// Creates a number with full [numeratorUnits] and [denominatorUnits]. SassNumber.withUnits(this.value, {Iterable numeratorUnits, Iterable denominatorUnits}) : numeratorUnits = numeratorUnits == null @@ -196,11 +231,28 @@ class SassNumber extends Value { : new List.unmodifiable(numeratorUnits), denominatorUnits = denominatorUnits == null ? const [] - : new List.unmodifiable(denominatorUnits); + : new List.unmodifiable(denominatorUnits), + _original = null, + _hasOriginal = false; + + SassNumber._(this.value, this.numeratorUnits, this.denominatorUnits, + [String original]) + : _original = original, + _hasOriginal = original != null; /*=T*/ accept/**/(ValueVisitor/**/ visitor) => visitor.visitNumber(this); + /// Returns a copy of [this] without [original] set. + SassNumber withoutOriginal() { + if (!_hasOriginal) return this; + return new SassNumber._(value, numeratorUnits, denominatorUnits); + } + + /// Returns a copy of [this] with [this.original] set to [original]. + SassNumber _withOriginal(String original) => + new SassNumber._(value, numeratorUnits, denominatorUnits, original); + SassNumber assertNumber([String name]) => this; /// Returns [value] as an [int], if it's an integer value according to @@ -403,8 +455,10 @@ class SassNumber extends Value { Value dividedBy(Value other) { if (other is SassNumber) { - return _multiplyUnits(this.value / other.value, this.numeratorUnits, + var result = _multiplyUnits(this.value / other.value, this.numeratorUnits, this.denominatorUnits, other.denominatorUnits, other.numeratorUnits); + if (!this._hasOriginal || !other._hasOriginal) return result; + return result._withOriginal("${this.original}/${other.original}"); } if (other is! SassColor) super.dividedBy(other); throw new SassScriptException('Undefined operation "$this / $other".'); diff --git a/lib/src/visitor/perform.dart b/lib/src/visitor/perform.dart index 1790b0b9..d616e004 100644 --- a/lib/src/visitor/perform.dart +++ b/lib/src/visitor/perform.dart @@ -622,7 +622,8 @@ class _PerformVisitor implements StatementVisitor, ExpressionVisitor { } void visitVariableDeclaration(VariableDeclaration node) { - _environment.setVariable(node.name, node.expression.accept(this), + _environment.setVariable( + node.name, node.expression.accept(this).withoutOriginal(), global: node.isGlobal); } @@ -732,8 +733,9 @@ class _PerformVisitor implements StatementVisitor, ExpressionVisitor { SassNull visitNullExpression(NullExpression node) => sassNull; - SassNumber visitNumberExpression(NumberExpression node) => - new SassNumber(node.value, node.unit); + SassNumber visitNumberExpression(NumberExpression node) => node.hasOriginal + ? new SassNumber.withOriginal(node.value, node.unit) + : new SassNumber(node.value, node.unit); SassColor visitColorExpression(ColorExpression node) => node.value; @@ -761,7 +763,7 @@ class _PerformVisitor implements StatementVisitor, ExpressionVisitor { var function = _environment.getFunction(plainName); if (function != null) { if (function is BuiltInCallable) { - return _runBuiltInCallable(node, function); + return _runBuiltInCallable(node, function).withoutOriginal(); } else if (function is UserDefinedCallable) { return _runUserDefinedCallable(node, function, () { for (var statement in function.declaration.children) { @@ -771,7 +773,7 @@ class _PerformVisitor implements StatementVisitor, ExpressionVisitor { throw _exception("Function finished without @return.", function.declaration.span); - }); + }).withoutOriginal(); } else { return null; } diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 731434ac..b1ac1d76 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -353,6 +353,11 @@ class _SerializeCssVisitor } void visitNumber(SassNumber value) { + if (value.isSlashSeparated) { + _buffer.write(value.original); + return; + } + _writeNumber(value.value); if (!_inspect) {