Store numbers' original values.

This commit is contained in:
Natalie Weizenbaum 2016-10-17 16:50:20 -07:00
parent d5aa9e7f82
commit 8969fc75f2
6 changed files with 121 additions and 42 deletions

View File

@ -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/*<T>*/(ExpressionVisitor/*<T>*/ visitor) =>
visitor.visitNumberExpression(this);

View File

@ -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.

View File

@ -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

View File

@ -161,6 +161,28 @@ class SassNumber extends Value {
/// This number's denominator units.
final List<String> 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<String> numeratorUnits, Iterable<String> 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/*<T>*/(ValueVisitor/*<T>*/ 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".');

View File

@ -622,7 +622,8 @@ class _PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
}
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<Value> {
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<Value> {
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<Value> {
throw _exception("Function finished without @return.",
function.declaration.span);
});
}).withoutOriginal();
} else {
return null;
}

View File

@ -353,6 +353,11 @@ class _SerializeCssVisitor
}
void visitNumber(SassNumber value) {
if (value.isSlashSeparated) {
_buffer.write(value.original);
return;
}
_writeNumber(value.value);
if (!_inspect) {