583 lines
20 KiB
JavaScript
583 lines
20 KiB
JavaScript
"use strict";
|
|
// Copyright 2021 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.
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.SassNumber = void 0;
|
|
const immutable_1 = require("immutable");
|
|
const utils_1 = require("../utils");
|
|
const index_1 = require("./index");
|
|
const utils_2 = require("./utils");
|
|
// Conversion rates for each unit.
|
|
const conversions = {
|
|
// Length
|
|
in: {
|
|
in: 1,
|
|
cm: 1 / 2.54,
|
|
pc: 1 / 6,
|
|
mm: 1 / 25.4,
|
|
q: 1 / 101.6,
|
|
pt: 1 / 72,
|
|
px: 1 / 96,
|
|
},
|
|
cm: {
|
|
in: 2.54,
|
|
cm: 1,
|
|
pc: 2.54 / 6,
|
|
mm: 1 / 10,
|
|
q: 1 / 40,
|
|
pt: 2.54 / 72,
|
|
px: 2.54 / 96,
|
|
},
|
|
pc: {
|
|
in: 6,
|
|
cm: 6 / 2.54,
|
|
pc: 1,
|
|
mm: 6 / 25.4,
|
|
q: 6 / 101.6,
|
|
pt: 1 / 12,
|
|
px: 1 / 16,
|
|
},
|
|
mm: {
|
|
in: 25.4,
|
|
cm: 10,
|
|
pc: 25.4 / 6,
|
|
mm: 1,
|
|
q: 1 / 4,
|
|
pt: 25.4 / 72,
|
|
px: 25.4 / 96,
|
|
},
|
|
q: {
|
|
in: 101.6,
|
|
cm: 40,
|
|
pc: 101.6 / 6,
|
|
mm: 4,
|
|
q: 1,
|
|
pt: 101.6 / 72,
|
|
px: 101.6 / 96,
|
|
},
|
|
pt: {
|
|
in: 72,
|
|
cm: 72 / 2.54,
|
|
pc: 12,
|
|
mm: 72 / 25.4,
|
|
q: 72 / 101.6,
|
|
pt: 1,
|
|
px: 3 / 4,
|
|
},
|
|
px: {
|
|
in: 96,
|
|
cm: 96 / 2.54,
|
|
pc: 16,
|
|
mm: 96 / 25.4,
|
|
q: 96 / 101.6,
|
|
pt: 4 / 3,
|
|
px: 1,
|
|
},
|
|
// Rotation
|
|
deg: {
|
|
deg: 1,
|
|
grad: 9 / 10,
|
|
rad: 180 / Math.PI,
|
|
turn: 360,
|
|
},
|
|
grad: {
|
|
deg: 10 / 9,
|
|
grad: 1,
|
|
rad: 200 / Math.PI,
|
|
turn: 400,
|
|
},
|
|
rad: {
|
|
deg: Math.PI / 180,
|
|
grad: Math.PI / 200,
|
|
rad: 1,
|
|
turn: 2 * Math.PI,
|
|
},
|
|
turn: {
|
|
deg: 1 / 360,
|
|
grad: 1 / 400,
|
|
rad: 1 / (2 * Math.PI),
|
|
turn: 1,
|
|
},
|
|
// Time
|
|
s: {
|
|
s: 1,
|
|
ms: 1 / 1000,
|
|
},
|
|
ms: {
|
|
s: 1000,
|
|
ms: 1,
|
|
},
|
|
// Frequency
|
|
Hz: { Hz: 1, kHz: 1000 },
|
|
kHz: { Hz: 1 / 1000, kHz: 1 },
|
|
// Pixel density
|
|
dpi: {
|
|
dpi: 1,
|
|
dpcm: 2.54,
|
|
dppx: 96,
|
|
},
|
|
dpcm: {
|
|
dpi: 1 / 2.54,
|
|
dpcm: 1,
|
|
dppx: 96 / 2.54,
|
|
},
|
|
dppx: {
|
|
dpi: 1 / 96,
|
|
dpcm: 2.54 / 96,
|
|
dppx: 1,
|
|
},
|
|
};
|
|
// A map from each human-readable type of unit to the units that belong to that
|
|
// type.
|
|
const unitsByType = {
|
|
length: ['in', 'cm', 'pc', 'mm', 'q', 'pt', 'px'],
|
|
angle: ['deg', 'grad', 'rad', 'turn'],
|
|
time: ['s', 'ms'],
|
|
frequency: ['Hz', 'kHz'],
|
|
'pixel density': ['dpi', 'dpcm', 'dppx'],
|
|
};
|
|
// A map from each unit to its human-readable type.
|
|
const typesByUnit = {};
|
|
for (const [type, units] of Object.entries(unitsByType)) {
|
|
for (const unit of units) {
|
|
typesByUnit[unit] = type;
|
|
}
|
|
}
|
|
/** A SassScript number. */
|
|
class SassNumber extends index_1.Value {
|
|
valueInternal;
|
|
numeratorUnitsInternal;
|
|
denominatorUnitsInternal;
|
|
constructor(value, unitOrOptions) {
|
|
super();
|
|
if (typeof unitOrOptions === 'string') {
|
|
this.valueInternal = value;
|
|
this.numeratorUnitsInternal =
|
|
unitOrOptions === undefined ? (0, immutable_1.List)([]) : (0, immutable_1.List)([unitOrOptions]);
|
|
this.denominatorUnitsInternal = (0, immutable_1.List)([]);
|
|
return;
|
|
}
|
|
let numerators = (0, utils_1.asImmutableList)(unitOrOptions?.numeratorUnits ?? []);
|
|
const unsimplifiedDenominators = unitOrOptions?.denominatorUnits ?? [];
|
|
const denominators = [];
|
|
for (const denominator of unsimplifiedDenominators) {
|
|
let simplifiedAway = false;
|
|
for (const [i, numerator] of numerators.entries()) {
|
|
const factor = conversionFactor(denominator, numerator);
|
|
if (factor === null)
|
|
continue;
|
|
value /= factor;
|
|
numerators = numerators.delete(i);
|
|
simplifiedAway = true;
|
|
break;
|
|
}
|
|
if (!simplifiedAway)
|
|
denominators.push(denominator);
|
|
}
|
|
this.valueInternal = value;
|
|
this.numeratorUnitsInternal = numerators;
|
|
this.denominatorUnitsInternal = (0, immutable_1.List)(denominators);
|
|
}
|
|
/** `this`'s value. */
|
|
get value() {
|
|
return this.valueInternal;
|
|
}
|
|
/** Whether `value` is an integer. */
|
|
get isInt() {
|
|
return (0, utils_2.fuzzyIsInt)(this.value);
|
|
}
|
|
/**
|
|
* If `value` is an integer according to `isInt`, returns `value` rounded to
|
|
* that integer.
|
|
*
|
|
* Otherwise, returns null.
|
|
*/
|
|
get asInt() {
|
|
return (0, utils_2.fuzzyAsInt)(this.value);
|
|
}
|
|
/** `this`'s numerator units. */
|
|
get numeratorUnits() {
|
|
return this.numeratorUnitsInternal;
|
|
}
|
|
/** `this`'s denominator units. */
|
|
get denominatorUnits() {
|
|
return this.denominatorUnitsInternal;
|
|
}
|
|
/** Whether `this` has any units. */
|
|
get hasUnits() {
|
|
return !(this.numeratorUnits.isEmpty() && this.denominatorUnits.isEmpty());
|
|
}
|
|
assertNumber() {
|
|
return this;
|
|
}
|
|
/**
|
|
* If `value` is an integer according to `isInt`, returns it as an integer.
|
|
*
|
|
* Otherwise, throws an error.
|
|
*
|
|
* If `this` came from a function argument, `name` is the argument name
|
|
* (without the `$`) and is used for error reporting.
|
|
*/
|
|
assertInt(name) {
|
|
const int = (0, utils_2.fuzzyAsInt)(this.value);
|
|
if (int === null) {
|
|
throw (0, utils_1.valueError)(`${this} is not an int`, name);
|
|
}
|
|
return int;
|
|
}
|
|
/**
|
|
* If `value` is within `min` and `max`, returns `value`, or if it
|
|
* `fuzzyEquals` `min` or `max`, returns `value` clamped to that value.
|
|
*
|
|
* Otherwise, throws an error.
|
|
*
|
|
* If `this` came from a function argument, `name` is the argument name
|
|
* (without the `$`) and is used for error reporting.
|
|
*/
|
|
assertInRange(min, max, name) {
|
|
const clamped = (0, utils_2.fuzzyInRange)(this.value, min, max);
|
|
if (clamped === null) {
|
|
throw (0, utils_1.valueError)(`${this} must be between ${min} and ${max}`, name);
|
|
}
|
|
return clamped;
|
|
}
|
|
/**
|
|
* If `this` has no units, returns `this`.
|
|
*
|
|
* Otherwise, throws an error.
|
|
*
|
|
* If `this` came from a function argument, `name` is the argument name
|
|
* (without the `$`) and is used for error reporting.
|
|
*/
|
|
assertNoUnits(name) {
|
|
if (this.hasUnits) {
|
|
throw (0, utils_1.valueError)(`Expected ${this} to have no units`, name);
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* If `this` has `unit` as its only unit (and as a numerator), returns `this`.
|
|
*
|
|
* Otherwise, throws an error.
|
|
*
|
|
* If `this` came from a function argument, `name` is the argument name
|
|
* (without the `$`) and is used for error reporting.
|
|
*/
|
|
assertUnit(unit, name) {
|
|
if (!this.hasUnit(unit)) {
|
|
throw (0, utils_1.valueError)(`Expected ${this} to have no unit ${unit}`, name);
|
|
}
|
|
return this;
|
|
}
|
|
/** Whether `this` has `unit` as its only unit (and as a numerator). */
|
|
hasUnit(unit) {
|
|
return (this.denominatorUnits.isEmpty() &&
|
|
this.numeratorUnits.size === 1 &&
|
|
this.numeratorUnits.get(0) === unit);
|
|
}
|
|
/** Whether `this` is compatible with `unit`. */
|
|
compatibleWithUnit(unit) {
|
|
if (!this.denominatorUnits.isEmpty())
|
|
return false;
|
|
if (this.numeratorUnits.size > 1)
|
|
return false;
|
|
const numerator = this.numeratorUnits.get(0);
|
|
return typesByUnit[numerator]
|
|
? typesByUnit[numerator] === typesByUnit[unit]
|
|
: numerator === unit;
|
|
}
|
|
/**
|
|
* Returns a copy of `this`, converted to the units represented by
|
|
* `newNumerators` and `newDenominators`.
|
|
*
|
|
* Throws an error if `this`'s units are incompatible with `newNumerators` and
|
|
* `newDenominators`. Also throws an error if `this` is unitless and either
|
|
* `newNumerators` or `newDenominators` are not empty, or vice-versa.
|
|
*
|
|
* If `this` came from a function argument, `name` is the argument name
|
|
* (without the `$`) and is used for error reporting.
|
|
*/
|
|
convert(newNumerators, newDenominators, name) {
|
|
return new SassNumber(this.convertValue(newNumerators, newDenominators, name), { numeratorUnits: newNumerators, denominatorUnits: newDenominators });
|
|
}
|
|
/**
|
|
* Returns `value`, converted to the units represented by `newNumerators` and
|
|
* `newDenominators`.
|
|
*
|
|
* Throws an error if `this`'s units are incompatible with `newNumerators` and
|
|
* `newDenominators`. Also throws an error if `this` is unitless and either
|
|
* `newNumerators` or `newDenominators` are not empty, or vice-versa.
|
|
*
|
|
* If `this` came from a function argument, `name` is the argument name
|
|
* (without the `$`) and is used for error reporting.
|
|
*/
|
|
convertValue(newNumerators, newDenominators, name) {
|
|
return this.convertOrCoerce({
|
|
coerceUnitless: false,
|
|
newNumeratorUnits: (0, utils_1.asImmutableList)(newNumerators),
|
|
newDenominatorUnits: (0, utils_1.asImmutableList)(newDenominators),
|
|
name,
|
|
});
|
|
}
|
|
/**
|
|
* Returns a copy of `this`, converted to the same units as `other`.
|
|
*
|
|
* Throws an error if `this`'s units are incompatible with `other`'s units, or
|
|
* if either number is unitless but the other is not.
|
|
*
|
|
* If `this` came from a function argument, `name` is the argument name
|
|
* and `otherName` is the argument name for `other` (both without the `$`).
|
|
* They are used for error reporting.
|
|
*/
|
|
convertToMatch(other, name, otherName) {
|
|
return new SassNumber(this.convertValueToMatch(other, name, otherName), {
|
|
numeratorUnits: other.numeratorUnits,
|
|
denominatorUnits: other.denominatorUnits,
|
|
});
|
|
}
|
|
/**
|
|
* Returns `value`, converted to the same units as `other`.
|
|
*
|
|
* Throws an error if `this`'s units are incompatible with `other`'s units, or
|
|
* if either number is unitless but the other is not.
|
|
*
|
|
* If `this` came from a function argument, `name` is the argument name
|
|
* and `otherName` is the argument name for `other` (both without the `$`).
|
|
* They are used for error reporting.
|
|
*/
|
|
convertValueToMatch(other, name, otherName) {
|
|
return this.convertOrCoerce({
|
|
coerceUnitless: false,
|
|
other,
|
|
name,
|
|
otherName,
|
|
});
|
|
}
|
|
/**
|
|
* Returns a copy of `this`, converted to the units represented by
|
|
* `newNumerators` and `newDenominators`.
|
|
*
|
|
* Does *not* throw an error if this number is unitless and either
|
|
* `newNumerators` or `newDenominators` are not empty, or vice-versa. Instead,
|
|
* it treats all unitless numbers as convertible to and from all units
|
|
* without changing the value.
|
|
*
|
|
* Throws an error if `this`'s units are incompatible with `newNumerators` and
|
|
* `newDenominators`.
|
|
*
|
|
* If `this` came from a function argument, `name` is the argument name
|
|
* (without the `$`) and is used for error reporting.
|
|
*/
|
|
coerce(newNumerators, newDenominators, name) {
|
|
return new SassNumber(this.coerceValue(newNumerators, newDenominators, name), { numeratorUnits: newNumerators, denominatorUnits: newDenominators });
|
|
}
|
|
/**
|
|
* Returns `value`, converted to the units represented by `newNumerators` and
|
|
* `newDenominators`.
|
|
*
|
|
* Does *not* throw an error if this number is unitless and either
|
|
* `newNumerators` or `newDenominators` are not empty, or vice-versa. Instead,
|
|
* it treats all unitless numbers as convertible to and from all units
|
|
* without changing the value.
|
|
*
|
|
* Throws an error if `this`'s units are incompatible with `newNumerators` and
|
|
* `newDenominators`.
|
|
*
|
|
* If `this` came from a function argument, `name` is the argument name
|
|
* (without the `$`) and is used for error reporting.
|
|
*/
|
|
coerceValue(newNumerators, newDenominators, name) {
|
|
return this.convertOrCoerce({
|
|
coerceUnitless: true,
|
|
newNumeratorUnits: (0, utils_1.asImmutableList)(newNumerators),
|
|
newDenominatorUnits: (0, utils_1.asImmutableList)(newDenominators),
|
|
name,
|
|
});
|
|
}
|
|
/**
|
|
* Returns a copy of `this`, converted to the same units as `other`.
|
|
*
|
|
* Does *not* throw an error if `this` is unitless and `other` is not, or
|
|
* vice-versa. Instead, it treats all unitless numbers as convertible to and
|
|
* from all units without changing the value.
|
|
*
|
|
* Throws an error if `this`'s units are incompatible with `other`'s units.
|
|
*
|
|
* If `this` came from a function argument, `name` is the argument name
|
|
* and `otherName` is the argument name for `other` (both without the `$`).
|
|
* They are used for error reporting.
|
|
*/
|
|
coerceToMatch(other, name, otherName) {
|
|
return new SassNumber(this.coerceValueToMatch(other, name, otherName), {
|
|
numeratorUnits: other.numeratorUnits,
|
|
denominatorUnits: other.denominatorUnits,
|
|
});
|
|
}
|
|
/**
|
|
* Returns `value`, converted to the same units as `other`.
|
|
*
|
|
* Does *not* throw an error if `this` is unitless and `other` is not, or
|
|
* vice-versa. Instead, it treats all unitless numbers as convertible to and
|
|
* from all units without changing the value.
|
|
*
|
|
* Throws an error if `this`'s units are incompatible with `other`'s units.
|
|
*
|
|
* If `this` came from a function argument, `name` is the argument name
|
|
* and `otherName` is the argument name for `other` (both without the `$`).
|
|
* They are used for error reporting.
|
|
*/
|
|
coerceValueToMatch(other, name, otherName) {
|
|
return this.convertOrCoerce({
|
|
coerceUnitless: true,
|
|
other,
|
|
name,
|
|
otherName,
|
|
});
|
|
}
|
|
equals(other) {
|
|
if (!(other instanceof SassNumber))
|
|
return false;
|
|
try {
|
|
return (0, utils_2.fuzzyEquals)(this.value, other.convertValueToMatch(this));
|
|
}
|
|
catch {
|
|
return false;
|
|
}
|
|
}
|
|
hashCode() {
|
|
const canonicalNumerators = canonicalizeUnits(this.numeratorUnits);
|
|
const canonicalDenominators = canonicalizeUnits(this.denominatorUnits);
|
|
const canonicalValue = this.convertValue(canonicalNumerators, canonicalDenominators);
|
|
return ((0, utils_2.fuzzyHashCode)(canonicalValue) ^
|
|
(0, immutable_1.hash)(canonicalNumerators) ^
|
|
(0, immutable_1.hash)(canonicalDenominators));
|
|
}
|
|
toString() {
|
|
return `${this.value}${unitString(this.numeratorUnits, this.denominatorUnits)}`;
|
|
}
|
|
// Returns the value of converting `number` to new units.
|
|
//
|
|
// The units may be specified as lists of units (`newNumeratorUnits` and
|
|
// `newDenominatorUnits`), or by providng a SassNumber `other` that contains the
|
|
// desired units.
|
|
//
|
|
// Throws an error if `number` is not compatible with the new units. Coercing a
|
|
// unitful number to unitless (or vice-versa) throws an error unless
|
|
// specifically enabled with `coerceUnitless`.
|
|
convertOrCoerce(params) {
|
|
const newNumerators = 'other' in params
|
|
? params.other.numeratorUnits
|
|
: params.newNumeratorUnits;
|
|
const newDenominators = 'other' in params
|
|
? params.other.denominatorUnits
|
|
: params.newDenominatorUnits;
|
|
const compatibilityError = () => {
|
|
if ('other' in params) {
|
|
let message = `${this} and`;
|
|
if (params.otherName) {
|
|
message += ` $${params.otherName}:`;
|
|
}
|
|
message += ` ${params.other} have incompatible units`;
|
|
if (!this.hasUnits || !otherHasUnits) {
|
|
message += " (one has units and the other doesn't)";
|
|
}
|
|
return (0, utils_1.valueError)(message, params.name);
|
|
}
|
|
if (!otherHasUnits) {
|
|
return (0, utils_1.valueError)(`Expected ${this} to have no units.`, params.name);
|
|
}
|
|
// For single numerators, throw a detailed error with info about which unit
|
|
// types would have been acceptable.
|
|
if (newNumerators.size === 1 && newDenominators.isEmpty()) {
|
|
const type = typesByUnit[newNumerators.get(0)];
|
|
if (type) {
|
|
return (0, utils_1.valueError)(`Expected ${this} to have a single ${type} unit (${unitsByType[type].join(', ')}).`, params.name);
|
|
}
|
|
}
|
|
const unitSize = newNumerators.size + newDenominators.size;
|
|
return (0, utils_1.valueError)(`Expected $this to have ${unitSize === 0
|
|
? 'no units'
|
|
: `unit${unitSize > 1 ? 's' : ''} ${unitString(newNumerators, newDenominators)}`}.`, params.name);
|
|
};
|
|
const otherHasUnits = !newNumerators.isEmpty() || !newDenominators.isEmpty();
|
|
if ((this.hasUnits && !otherHasUnits) ||
|
|
(!this.hasUnits && otherHasUnits)) {
|
|
if (params.coerceUnitless)
|
|
return this.value;
|
|
throw compatibilityError();
|
|
}
|
|
if (this.numeratorUnits.equals(newNumerators) &&
|
|
this.denominatorUnits.equals(newDenominators)) {
|
|
return this.value;
|
|
}
|
|
let value = this.value;
|
|
let oldNumerators = this.numeratorUnits;
|
|
for (const newNumerator of newNumerators) {
|
|
const idx = oldNumerators.findIndex(oldNumerator => {
|
|
const factor = conversionFactor(oldNumerator, newNumerator);
|
|
if (factor === null)
|
|
return false;
|
|
value *= factor;
|
|
return true;
|
|
});
|
|
if (idx < 0)
|
|
throw compatibilityError();
|
|
oldNumerators = oldNumerators.delete(idx);
|
|
}
|
|
let oldDenominators = this.denominatorUnits;
|
|
for (const newDenominator of newDenominators) {
|
|
const idx = oldDenominators.findIndex(oldDenominator => {
|
|
const factor = conversionFactor(oldDenominator, newDenominator);
|
|
if (factor === null)
|
|
return false;
|
|
value /= factor;
|
|
return true;
|
|
});
|
|
if (idx < 0)
|
|
throw compatibilityError();
|
|
oldDenominators = oldDenominators.delete(idx);
|
|
}
|
|
if (!oldNumerators.isEmpty() || !oldDenominators.isEmpty()) {
|
|
throw compatibilityError();
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
exports.SassNumber = SassNumber;
|
|
// Returns the conversion factor needed to convert from `fromUnit` to `toUnit`.
|
|
// Returns null if no such factor exists.
|
|
function conversionFactor(fromUnit, toUnit) {
|
|
if (fromUnit === toUnit)
|
|
return 1;
|
|
const factors = conversions[toUnit];
|
|
if (!factors)
|
|
return null;
|
|
return factors[fromUnit] ?? null;
|
|
}
|
|
// Returns a human-readable string representation of `numerators` and
|
|
// `denominators`.
|
|
function unitString(numerators, denominators) {
|
|
if (numerators.isEmpty() && denominators.isEmpty()) {
|
|
return '';
|
|
}
|
|
if (denominators.isEmpty()) {
|
|
return numerators.join('*');
|
|
}
|
|
if (numerators.isEmpty()) {
|
|
return denominators.size === 1
|
|
? `${denominators.get(0)}^-1`
|
|
: `(${denominators.join('*')})^-1`;
|
|
}
|
|
return `${numerators.join('*')}/${denominators.join('*')}`;
|
|
}
|
|
// Converts the `units` list into an equivalent canonical list.
|
|
function canonicalizeUnits(units) {
|
|
return units
|
|
.map(unit => {
|
|
const type = typesByUnit[unit];
|
|
return type ? unitsByType[type][0] : unit;
|
|
})
|
|
.sort();
|
|
}
|
|
//# sourceMappingURL=number.js.map
|