Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SassCalculation #237

Merged
merged 14 commits into from
Jul 19, 2023
157 changes: 157 additions & 0 deletions lib/src/value/calculations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import {hash, List, ValueObject} from 'immutable';

import {Value} from './index';
import {SassNumber} from './number';
import {SassString} from './string';

export type CalculationValue =
| SassNumber
| SassCalculation
| SassString
| CalculationOperation
| CalculationInterpolation;

type CalculationValueIterable = CalculationValue[] | List<CalculationValue>;

function checkUnquotedString(value: CalculationValue): void {
if (value instanceof SassString && value.hasQuotes) {
throw new Error(`Expected ${value} to be an unquoted string.`);
}
}
jerivas marked this conversation as resolved.
Show resolved Hide resolved

export class SassCalculation extends Value {
readonly name: string;
readonly arguments: List<CalculationValue>;

private constructor(name: string, args: CalculationValueIterable) {
super();
this.name = name;
this.arguments = List(args);
}

static calc(argument: CalculationValue): SassCalculation {
checkUnquotedString(argument);
return new SassCalculation('calc', [argument]);
}

static min(args: CalculationValueIterable): SassCalculation {
args.forEach(checkUnquotedString);
return new SassCalculation('min', args);
}

static max(args: CalculationValueIterable): SassCalculation {
args.forEach(checkUnquotedString);
return new SassCalculation('max', args);
}

static clamp(
min: CalculationValue,
value?: CalculationValue,
max?: CalculationValue
): SassCalculation {
if (value === undefined && max === undefined) {
let minString: string;
if (min instanceof SassString) {
minString = min.text;
} else if (min instanceof CalculationInterpolation) {
minString = min.value;
} else {
throw new Error(
'`value` and `max` are both undefined, but `min` is not a SassString or CalculationInterpolation.'
);
}
const values = minString.split(',').map(s => {
const parsed = parseFloat(s.trim());
return isNaN(parsed) ? undefined : new SassNumber(parsed);
});
const error = new Error(
`Expected \`min\` to be a comma-separated list of numbers, got \`${min}\``
);
jerivas marked this conversation as resolved.
Show resolved Hide resolved
if (values[0] === undefined) {
throw error;
} else if (values.length === 2) {
[min, value] = values;
} else if (values.length === 3) {
[min, value, max] = values;
} else {
throw error;
}
} else if (value === undefined && max !== undefined) {
throw new Error('`value` is undefined but `max` is defined.');
}
const args = [min];
if (value !== undefined) args.push(value);
if (max !== undefined) args.push(max);
args.forEach(checkUnquotedString);
return new SassCalculation('clamp', args);
}

assertCalculation(): SassCalculation {
return this;
}

equals(other: Value): boolean {
return (
other instanceof SassCalculation &&
this.name === other.name &&
this.arguments.equals(other.arguments)
);
}

hashCode(): number {
return hash(this.name) ^ this.arguments.hashCode();
}

toString(): string {
return `${this.name}(${this.arguments.join(', ')})`;
}
}

export type CalculationOperator = '+' | '-' | '*' | '/';

export class CalculationOperation implements ValueObject {
readonly operator: CalculationOperator;
readonly left: CalculationValue;
readonly right: CalculationValue;

constructor(
operator: CalculationOperator,
left: CalculationValue,
right: CalculationValue
jerivas marked this conversation as resolved.
Show resolved Hide resolved
) {
this.operator = operator;
this.left = left;
this.right = right;
}

equals(other: Value): boolean {
return (
other instanceof CalculationOperation &&
this.operator === other.operator &&
this.left === other.left &&
this.right === other.right
);
}

hashCode(): number {
return hash(this.operator) ^ hash(this.left) ^ hash(this.right);
}
}

export class CalculationInterpolation implements ValueObject {
readonly value: string;

constructor(value: string) {
this.value = value;
}

equals(other: Value): boolean {
return (
other instanceof CalculationInterpolation && this.value === other.value
);
}

hashCode(): number {
return hash(this.value);
}
}
11 changes: 11 additions & 0 deletions lib/src/value/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {SassMap} from './map';
import {SassNumber} from './number';
import {SassString} from './string';
import {valueError} from '../utils';
import {SassCalculation} from './calculations';

/**
* A SassScript value.
Expand Down Expand Up @@ -106,6 +107,16 @@ export abstract class Value implements ValueObject {
throw valueError(`${this} is not a boolean`, name);
}

/**
* Casts `this` to `SassCalculation`; throws if `this` isn't a calculation.
*
* If `this` came from a function argument, `name` is the argument name
* (without the `$`) and is used for error reporting.
*/
assertCalculation(name?: string): SassCalculation {
throw valueError(`${this} is not a calculation`, name);
}

/**
* Casts `this` to `SassColor`; throws if `this` isn't a color.
*
Expand Down