scroll-timeline-polyfill
Version:
A polyfill for scroll-driven animations on the web via ScrollTimeline
844 lines (786 loc) • 31.4 kB
JavaScript
import {
CommaToken,
DelimToken,
DimensionToken,
FunctionToken, IdentToken,
LeftCurlyBracketToken,
LeftParenthesisToken,
LeftSquareBracketToken,
NumberToken,
PercentageToken, RightCurlyBracketToken,
RightParenthesisToken, RightSquareBracketToken,
Token,
tokenizeString,
WhitespaceToken
} from './tokenizer';
import {simplifyCalculation} from './simplify-calculation';
/**
* @typedef {{[string]: integer}} UnitMap
* @typedef {[number, UnitMap]} SumValueItem
* @typedef {SumValueItem[]} SumValue
* @typedef {null} Failure
* @typedef {{[string]: integer} & {percentHint: string | undefined}} Type
* @typedef {{type: 'ADDITION'}|{type: 'MULTIPLICATION'}|{type: 'NEGATE'}|{type: 'INVERT'}} ASTNode
*/
const failure = null;
const baseTypes = ["percent", "length", "angle", "time", "frequency", "resolution", "flex"];
const unitGroups = {
// https://www.w3.org/TR/css-values-4/#font-relative-lengths
fontRelativeLengths: {
units: new Set(["em", "rem", "ex", "rex", "cap", "rcap", "ch", "rch", "ic", "ric", "lh", "rlh"])
},
// https://www.w3.org/TR/css-values-4/#viewport-relative-lengths
viewportRelativeLengths: {
units: new Set(
["vw", "lvw", "svw", "dvw", "vh", "lvh", "svh", "dvh", "vi", "lvi", "svi", "dvi", "vb", "lvb", "svb", "dvb",
"vmin", "lvmin", "svmin", "dvmin", "vmax", "lvmax", "svmax", "dvmax"])
},
// https://www.w3.org/TR/css-values-4/#absolute-lengths
absoluteLengths: {
units: new Set(["cm", "mm", "Q", "in", "pt", "pc", "px"]),
compatible: true,
canonicalUnit: "px",
ratios: {
"cm": 96 / 2.54, "mm": (96 / 2.54) / 10, "Q": (96 / 2.54) / 40, "in": 96, "pc": 96 / 6, "pt": 96 / 72, "px": 1
}
},
// https://www.w3.org/TR/css-values-4/#angles
angle: {
units: new Set(["deg", "grad", "rad", "turn"]),
compatible: true,
canonicalUnit: "deg",
ratios: {
"deg": 1, "grad": 360 / 400, "rad": 180 / Math.PI, "turn": 360
}
},
// https://www.w3.org/TR/css-values-4/#time
time: {
units: new Set(["s", "ms"]),
compatible: true,
canonicalUnit: "s",
ratios: {
"s": 1, "ms": 1 / 1000
}
},
// https://www.w3.org/TR/css-values-4/#frequency
frequency: {
units: new Set(["hz", "khz"]),
compatible: true,
canonicalUnit: "hz",
ratios: {
"hz": 1, "khz": 1000
}
},
// https://www.w3.org/TR/css-values-4/#resolution
resolution: {
units: new Set(["dpi", "dpcm", "dppx"]),
compatible: true,
canonicalUnit: "dppx",
ratios: {
"dpi": 1 / 96, "dpcm": 2.54 / 96, "dppx": 1
}
}
};
const unitToCompatibleUnitsMap = new Map();
for (const group of Object.values(unitGroups)) {
if (!group.compatible) {
continue;
}
for (const unit of group.units) {
unitToCompatibleUnitsMap.set(unit, group);
}
}
export function getSetOfCompatibleUnits(unit) {
return unitToCompatibleUnitsMap.get(unit);
}
/**
* Implementation of `product of two unit maps` from css-typed-om-1:
* https://www.w3.org/TR/css-typed-om-1/#product-of-two-unit-maps
*
* @param {UnitMap} units1 map of units (strings) to powers (integers)
* @param {UnitMap} units2 map of units (strings) to powers (integers)
* @return {UnitMap} map of units (strings) to powers (integers)
*/
function productOfTwoUnitMaps(units1, units2) {
// 1. Let result be a copy of units1.
const result = {...units1};
// 2. For each unit → power in units2:
for (const unit of Object.keys(units2)) {
if (result[unit]) {
// 1. If result[unit] exists, increment result[unit] by power.
result[unit] += units2[unit];
} else {
// 2. Otherwise, set result[unit] to power.
result[unit] = units2[unit];
}
}
// 3. Return result.
return result;
}
/**
* Implementation of `create a type` from css-typed-om-1:
* https://www.w3.org/TR/css-typed-om-1/#create-a-type
*
* @param {string} unit
* @return {Type|Failure}
*/
export function createAType(unit) {
if (unit === "number") {
return {};
} else if (unit === "percent") {
return {"percent": 1};
} else if (unitGroups.absoluteLengths.units.has(unit) || unitGroups.fontRelativeLengths.units.has(unit) ||
unitGroups.viewportRelativeLengths.units.has(unit)) {
return {"length": 1};
} else if (unitGroups.angle.units.has(unit)) {
return {"angle": 1};
} else if (unitGroups.time.units.has(unit)) {
return {"time": 1};
} else if (unitGroups.frequency.units.has(unit)) {
return {"frequency": 1};
} else if (unitGroups.resolution.units.has(unit)) {
return {"resolution": 1};
} else if (unit === "fr") {
return {"flex": 1};
} else {
return failure;
}
}
/**
* Partial implementation of `create a sum value` from css-typed-om-1:
* https://www.w3.org/TR/css-typed-om-1/#create-a-sum-value
*
* Supports CSSUnitValue, CSSMathProduct and CSSMathInvert with a CSSUnitValue value.
* Other types are not supported, and will throw an error.
*
* @param {CSSNumericValue} cssNumericValue
* @return {SumValue} Abstract representation of a CSSNumericValue as a sum of numbers with (possibly complex) units
*/
export function createSumValue(cssNumericValue) {
if (cssNumericValue instanceof CSSUnitValue) {
let {unit, value} = cssNumericValue;
// Let unit be the value of this’s unit internal slot, and value be the value of this’s value internal slot.
// If unit is a member of a set of compatible units, and is not the set’s canonical unit,
// multiply value by the conversion ratio between unit and the canonical unit, and change unit to the canonical unit.
const compatibleUnits = getSetOfCompatibleUnits(cssNumericValue.unit);
if (compatibleUnits && unit !== compatibleUnits.canonicalUnit) {
value *= compatibleUnits.ratios[unit];
unit = compatibleUnits.canonicalUnit;
}
if (unit === "number") {
// If unit is "number", return «(value, «[ ]»)».
return [[value, {}]];
} else {
// Otherwise, return «(value, «[unit → 1]»)».
return [[value, {[unit]: 1}]];
}
} else if (cssNumericValue instanceof CSSMathInvert) {
if (!(cssNumericValue.value instanceof CSSUnitValue)) {
// Limit implementation to CSSMathInvert of CSSUnitValue
throw new Error("Not implemented");
}
// 1. Let values be the result of creating a sum value from this’s value internal slot.
const values = createSumValue(cssNumericValue.value);
// 2. If values is failure, return failure.
if (values === failure) {
return failure;
}
// 3. If the length of values is more than one, return failure.
if (values.length > 1) {
return failure;
}
// 4. Invert (find the reciprocal of) the value of the item in values, and negate the value of each entry in its unit map.
const item = values[0];
const tempUnionMap = {};
for (const [unit, power] of Object.entries(item[1])) {
tempUnionMap[unit] = -1 * power;
}
values[0] = [1 / item[0], tempUnionMap];
// 5. Return values.
return values;
} else if (cssNumericValue instanceof CSSMathProduct) {
// 1. Let values initially be the sum value «(1, «[ ]»)». (I.e. what you’d get from 1.)
let values = [[1, {}]];
// 2. For each item in this’s values internal slot:
for (const item of cssNumericValue.values) {
// 1. Let new values be the result of creating a sum value from item. Let temp initially be an empty list.
const newValues = createSumValue(item);
const temp = [];
// 2. If new values is failure, return failure.
if (newValues === failure) {
return failure;
}
// 3. For each item1 in values:
for (const item1 of values) {
// 1. For each item2 in new values:
for (const item2 of newValues) {
// 1. Let item be a tuple with its value set to the product of the values of item1 and item2, and its unit
// map set to the product of the unit maps of item1 and item2, with all entries with a zero value removed.
// 2. Append item to temp.
temp.push([item1[0] * item2[0], productOfTwoUnitMaps(item1[1], item2[1])]);
}
}
// 4. Set values to temp.
values = temp;
}
// Return values.
return values;
} else {
throw new Error("Not implemented");
}
}
/**
* Implementation of `to(unit)` for CSSNumericValue from css-typed-om-1:
* https://www.w3.org/TR/css-typed-om-1/#dom-cssnumericvalue-to
*
* Converts an existing CSSNumeric value into another with the specified unit, if possible.
*
* @param {CSSNumericValue} cssNumericValue value to convert
* @param {string} unit
* @return {CSSUnitValue}
*/
export function to(cssNumericValue, unit) {
// Let type be the result of creating a type from unit. If type is failure, throw a SyntaxError.
const type = createAType(unit);
if (type === failure) {
throw new SyntaxError("The string did not match the expected pattern.");
}
// Let sum be the result of creating a sum value from this.
const sumValue = createSumValue(cssNumericValue);
// If sum is failure, throw a TypeError.
if (!sumValue) {
throw new TypeError();
}
// If sum has more than one item, throw a TypeError.
if (sumValue.length > 1) {
throw new TypeError("Sum has more than one item");
}
// Otherwise, let item be the result of creating a CSSUnitValue
// from the sole item in sum, then converting it to unit.
const item = convertCSSUnitValue(createCSSUnitValue(sumValue[0]), unit);
// If item is failure, throw a TypeError.
if (item === failure) {
throw new TypeError();
}
// Return item.
return item;
}
/**
* Implementation of `create a CSSUnitValue from a sum value item` from css-typed-om-1:
* https://www.w3.org/TR/css-typed-om-1/#create-a-cssunitvalue-from-a-sum-value-item
*
* @param {SumValueItem} sumValueItem a tuple of a value, and a unit map
* @return {CSSUnitValue|Failure}
*/
export function createCSSUnitValue(sumValueItem) {
const [value, unitMap] = sumValueItem;
// When asked to create a CSSUnitValue from a sum value item item, perform the following steps:
// If item has more than one entry in its unit map, return failure.
const entries = Object.entries(unitMap);
if (entries.length > 1) {
return failure;
}
// If item has no entries in its unit map, return a new CSSUnitValue whose unit internal slot is set to "number",
// and whose value internal slot is set to item’s value.
if (entries.length === 0) {
return new CSSUnitValue(value, "number");
}
// Otherwise, item has a single entry in its unit map. If that entry’s value is anything other than 1, return failure.
const entry = entries[0];
if (entry[1] !== 1) {
return failure;
}
// Otherwise, return a new CSSUnitValue whose unit internal slot is set to that entry’s key, and whose value internal slot is set to item’s value.
else {
return new CSSUnitValue(value, entry[0]);
}
}
/**
* Implementation of `convert a CSSUnitValue` from css-typed-om-1:
* https://www.w3.org/TR/css-typed-om-1/#convert-a-cssunitvalue
* @param {CSSUnitValue} cssUnitValue
* @param {string} unit
* @return {CSSUnitValue|Failure}
*/
export function convertCSSUnitValue(cssUnitValue, unit) {
// Let old unit be the value of this’s unit internal slot, and old value be the value of this’s value internal slot.
const oldUnit = cssUnitValue.unit;
const oldValue = cssUnitValue.value;
// If old unit and unit are not compatible units, return failure.
const oldCompatibleUnitGroup = getSetOfCompatibleUnits(oldUnit);
const compatibleUnitGroup = getSetOfCompatibleUnits(unit);
if (!compatibleUnitGroup || oldCompatibleUnitGroup !== compatibleUnitGroup) {
return failure;
}
// Return a new CSSUnitValue whose unit internal slot is set to unit, and whose value internal slot is set to
// old value multiplied by the conversation ratio between old unit and unit.
return new CSSUnitValue(oldValue * compatibleUnitGroup.ratios[oldUnit] / compatibleUnitGroup.ratios[unit], unit);
}
/**
* Partial implementation of `toSum(...units)`:
* https://www.w3.org/TR/css-typed-om-1/#dom-cssnumericvalue-tosum
*
* The implementation is restricted to conversion without units.
* It simplifies a CSSNumericValue into a minimal sum of CSSUnitValues.
* Will throw an error if called with units.
*
* @param {CSSNumericValue} cssNumericValue value to convert to a CSSMathSum
* @param {string[]} units Not supported in this implementation
* @return {CSSMathSum}
*/
export function toSum(cssNumericValue, ...units) {
// The toSum(...units) method converts an existing CSSNumericValue this into a CSSMathSum of only CSSUnitValues
// with the specified units, if possible. (It’s like to(), but allows the result to have multiple units in it.)
// If called without any units, it just simplifies this into a minimal sum of CSSUnitValues.
// When called, it must perform the following steps:
//
// For each unit in units, if the result of creating a type from unit is failure, throw a SyntaxError.
//
if (units && units.length) {
// Only unitless method calls are implemented in this polyfill
throw new Error("Not implemented");
}
// Let sum be the result of creating a sum value from this. If sum is failure, throw a TypeError.
const sum = createSumValue(cssNumericValue);
// Let values be the result of creating a CSSUnitValue for each item in sum. If any item of values is failure,
// throw a TypeError.
const values = sum.map(item => createCSSUnitValue(item));
if (values.some(value => value === failure)) {
throw new TypeError("Type error");
}
// If units is empty, sort values in code point order according to the unit internal slot of its items,
// then return a new CSSMathSum object whose values internal slot is set to values.
return new CSSMathSum(...values);
}
/**
* Implementation of `invert a type` from css-typed-om-1 Editors Draft:
* https://drafts.css-houdini.org/css-typed-om/
*
* @param {Type} type
* @return {Type}
*/
export function invertType(type) {
// To invert a type type, perform the following steps:
// Let result be a new type with an initially empty ordered map and an initially null percent hint
// For each unit → exponent of type, set result[unit] to (-1 * exponent).
// Return result.
const result = {};
for (const baseType of baseTypes) {
result[baseType] = -1 * type[baseType];
}
return result;
}
/**
* Implementation of `multiply two types` from css-typed-om-1 Editor's Draft:
* https://drafts.css-houdini.org/css-typed-om/#cssnumericvalue-multiply-two-types
*
* @param {Type} type1 a map of base types to integers and an associated percent hint
* @param {Type} type2 a map of base types to integers and an associated percent hint
* @return {Type|Failure}
*/
export function multiplyTypes(type1, type2) {
if (type1.percentHint && type2.percentHint && type1.percentHint !== type2.percentHint) {
return failure;
}
const finalType = {
...type1, percentHint: type1.percentHint ?? type2.percentHint,
};
for (const baseType of baseTypes) {
if (!type2[baseType]) {
continue;
}
finalType[baseType] ??= 0;
finalType[baseType] += type2[baseType];
}
return finalType;
}
class CSSFunction {
name;
values;
constructor(name, values) {
this.name = name;
this.values = values;
}
}
class CSSSimpleBlock {
value;
associatedToken;
constructor(value, associatedToken) {
this.value = value;
this.associatedToken = associatedToken;
}
}
/**
* Normalize into a token stream
* https://www.w3.org/TR/css-syntax-3/#normalize-into-a-token-stream
*/
function normalizeIntoTokenStream(input) {
// If input is a list of CSS tokens, return input.
// If input is a list of CSS component values, return input.
if (Array.isArray(input)) {
return input;
}
// If input is a string, then filter code points from input, tokenize the result, and return the final result.
if (typeof input === 'string') {
return tokenizeString(input);
}
// Assert: Only the preceding types should be passed as input.
throw new TypeError(`Invalid input type ${typeof input}`)
}
/**
* Consume a function
* https://www.w3.org/TR/css-syntax-3/#consume-a-function
* @param {FunctionToken} token
* @param {Token[]} tokens
*/
function consumeFunction(token, tokens) {
// Create a function with its name equal to the value of the current input token and with its value initially set to an empty list.
const func = new CSSFunction(token.value, []);
// Repeatedly consume the next input token and process it as follows:
while(true) {
const nextToken = tokens.shift();
if (nextToken instanceof RightParenthesisToken) {
// <)-token>
// Return the function.
return func;
} else if (typeof nextToken === 'undefined') {
// <EOF-token>
// This is a parse error. Return the function.
return func;
} else {
// anything else
// Reconsume the current input token. Consume a component value and append the returned value to the function’s value.
tokens.unshift(nextToken);
func.values.push(consumeComponentValue(tokens));
}
}
}
/**
* Consume a simple block
* https://www.w3.org/TR/css-syntax-3/#consume-simple-block
* @param {Token[]} tokens
* @param {LeftCurlyBracketToken | LeftParenthesisToken | LeftSquareBracketToken} currentInputToken
*/
function consumeSimpleBlock(tokens, currentInputToken) {
// The ending token is the mirror variant of the current input token. (E.g. if it was called with <[-token>, the ending token is <]-token>.)
let endingTokenConstructor ;
if (currentInputToken instanceof LeftCurlyBracketToken) {
endingTokenConstructor = RightCurlyBracketToken;
} else if (currentInputToken instanceof LeftParenthesisToken) {
endingTokenConstructor = RightParenthesisToken;
} else if (currentInputToken instanceof LeftSquareBracketToken) {
endingTokenConstructor = RightSquareBracketToken;
} else {
return undefined;
}
// Create a simple block with its associated token set to the current input token and with its value initially set to an empty list.
const simpleBlock = new CSSSimpleBlock([], currentInputToken);
// Repeatedly consume the next input token and process it as follows:
while (true) {
const token = tokens.shift();
if (token instanceof endingTokenConstructor) {
// ending token
// Return the block.
return simpleBlock;
} else if (typeof token === 'undefined') {
// <EOF-token>
// This is a parse error. Return the block.
return simpleBlock;
} else {
// anything else
// Reconsume the current input token. Consume a component value and append it to the value of the block.
tokens.unshift(token);
simpleBlock.value.push(consumeComponentValue(tokens));
}
}
}
/**
* Consume a component value
* https://www.w3.org/TR/css-syntax-3/#consume-a-component-value
* @param {Token[]} tokens
*/
function consumeComponentValue(tokens) {
const syntaxError = null;
// Consume the next input token.
const token = tokens.shift();
if (token instanceof LeftCurlyBracketToken || token instanceof LeftSquareBracketToken || token instanceof LeftParenthesisToken) {
// If the current input token is a <{-token>, <[-token>, or <(-token>, consume a simple block and return it.
return consumeSimpleBlock(tokens, token);
} else if (token instanceof FunctionToken) {
// Otherwise, if the current input token is a <function-token>, consume a function and return it.
return consumeFunction(token, tokens);
} else {
// Otherwise, return the current input token.
return token;
}
}
/**
* Parse a component value
* https://www.w3.org/TR/css-syntax-3/#parse-component-value
* @param {string} input
*/
function parseComponentValue(input) {
const syntaxError = null;
// To parse a component value from input:
// 1. Normalize input, and set input to the result.
const tokens = normalizeIntoTokenStream(input);
// 2. While the next input token from input is a <whitespace-token>, consume the next input token from input.
while (tokens[0] instanceof WhitespaceToken) {
tokens.shift();
}
// 3. If the next input token from input is an <EOF-token>, return a syntax error.
if (typeof tokens[0] === 'undefined') {
return syntaxError;
}
// 4. Consume a component value from input and let value be the return value.
const returnValue = consumeComponentValue(tokens);
// 5. While the next input token from input is a <whitespace-token>, consume the next input token.
while (tokens[0] instanceof WhitespaceToken) {
tokens.shift();
}
// 6. If the next input token from input is an <EOF-token>, return value. Otherwise, return a syntax error.
if (typeof tokens[0] === 'undefined') {
return returnValue;
} else {
return syntaxError;
}
}
function precedence(token) {
if (token instanceof LeftParenthesisToken || token instanceof RightParenthesisToken) {
return 6;
} else if (token instanceof DelimToken) {
const value = token.value;
switch (value) {
case '*':
return 4;
case '/':
return 4;
case '+':
return 2;
case '-':
return 2;
}
}
}
function last(items) {
return items[items.length - 1];
}
function toNAryAstNode(operatorToken, first, second) {
// Treat subtraction as instead being addition, with the RHS argument instead wrapped in a special "negate" node.
// Treat division as instead being multiplication, with the RHS argument instead wrapped in a special "invert" node.
const type = ['+','-'].includes(operatorToken.value) ? 'ADDITION' : 'MULTIPLICATION';
const firstValues = first.type === type ? first.values : [first];
const secondValues = second.type === type ? second.values : [second];
if (operatorToken.value === '-') {
secondValues[0] = {type: 'NEGATE', value: secondValues[0]};
} else if (operatorToken.value === '/') {
secondValues[0] = {type: 'INVERT', value: secondValues[0]};
}
return {type, values: [...firstValues, ...secondValues]};
}
/**
* Convert expression to AST using the Shunting Yard Algorithm
* https://en.wikipedia.org/wiki/Shunting_yard_algorithm
* @param {(Token | CSSFunction)[]} tokens
* @return {null}
*/
function convertTokensToAST(tokens) {
const operatorStack = [];
const tree = [];
while (tokens.length) {
const token = tokens.shift();
if (token instanceof NumberToken || token instanceof DimensionToken || token instanceof PercentageToken ||
token instanceof CSSFunction || token instanceof CSSSimpleBlock || token instanceof IdentToken) {
tree.push(token);
} else if (token instanceof DelimToken && ['*', '/', '+', '-'].includes(token.value)) {
while (operatorStack.length &&
!(last(operatorStack) instanceof LeftParenthesisToken) &&
precedence(last(operatorStack)) > precedence(token)) {
const o2 = operatorStack.pop();
const second = tree.pop();
const first = tree.pop();
tree.push(toNAryAstNode(o2, first, second));
}
operatorStack.push(token);
} else if (token instanceof LeftParenthesisToken) {
operatorStack.push(token);
} else if (token instanceof RightParenthesisToken) {
if (!operatorStack.length) {
return null;
}
while (!(last(operatorStack) instanceof LeftParenthesisToken) ) {
const o2 = operatorStack.pop();
const second = tree.pop();
const first = tree.pop();
tree.push(toNAryAstNode(o2, first, second));
}
if (!(last(operatorStack) instanceof LeftParenthesisToken)) {
return null;
}
operatorStack.pop();
} else if (token instanceof WhitespaceToken) {
// Consume token
} else {
return null;
}
}
while(operatorStack.length) {
if (last(operatorStack) instanceof LeftParenthesisToken) {
return null;
}
const o2 = operatorStack.pop()
const second = tree.pop();
const first = tree.pop();
tree.push(toNAryAstNode(o2, first, second));
}
return tree[0];
}
/**
* Step 4 of `reify a math expression`
* https://drafts.css-houdini.org/css-typed-om/#reify-a-math-expression
*
* 4. Recursively transform the expression tree into objects, as follows:
*
* @param {ASTNode} node
* @return {CSSMathNegate|CSSMathProduct|CSSMathMin|CSSMathMax|CSSMathSum|CSSNumericValue|CSSUnitValue|CSSMathInvert}
*/
function transformToCSSNumericValue(node) {
if (node.type === 'ADDITION') {
// addition node
// becomes a new CSSMathSum object, with its values internal slot set to its list of arguments
return new CSSMathSum(...node.values.map(value => transformToCSSNumericValue(value)));
} else if (node.type === 'MULTIPLICATION') {
// multiplication node
// becomes a new CSSMathProduct object, with its values internal slot set to its list of arguments
return new CSSMathProduct(...node.values.map(value => transformToCSSNumericValue(value)));
} else if (node.type === 'NEGATE') {
// negate node
// becomes a new CSSMathNegate object, with its value internal slot set to its argument
return new CSSMathNegate(transformToCSSNumericValue(node.value));
} else if (node.type === 'INVERT') {
// invert node
// becomes a new CSSMathInvert object, with its value internal slot set to its argument
return new CSSMathInvert(transformToCSSNumericValue(node.value));
} else {
// leaf node
// reified as appropriate
if (node instanceof CSSSimpleBlock) {
return reifyMathExpression(new CSSFunction('calc', node.value));
} else if (node instanceof IdentToken) {
if (node.value === 'e') {
return new CSSUnitValue(Math.E, 'number');
} else if (node.value === 'pi') {
return new CSSUnitValue(Math.PI, 'number');
} else {
throw new SyntaxError('Invalid math expression')
}
} else {
return reifyNumericValue(node);
}
}
}
/**
* Reify a math expression
* https://drafts.css-houdini.org/css-typed-om/#reify-a-math-expression
* @param {CSSFunction} num
*/
function reifyMathExpression(num) {
// TODO: handle `clamp()` and possibly other math functions
// 1. If num is a min() or max() expression:
if (num.name === 'min' || num.name === 'max')
{
// Let values be the result of reifying the arguments to the expression, treating each argument as if it were the contents of a calc() expression.
const values = num.values
.filter(value => !(value instanceof WhitespaceToken || value instanceof CommaToken))
// TODO: Update when we have clarification on where simplify a calculation should be run:
// https://github.com/w3c/csswg-drafts/issues/9870
.map(value => simplifyCalculation(reifyMathExpression(new CSSFunction('calc', value))));
// Return a new CSSMathMin or CSSMathMax object, respectively, with its values internal slot set to values.
return num.name === 'min' ? new CSSMathMin(...values) : new CSSMathMax(...values);
}
// 2. Assert: Otherwise, num is a calc().
if (num.name !== 'calc') {
return null;
}
// 3. Turn num’s argument into an expression tree using standard PEMDAS precedence rules, with the following exceptions/clarification:
//
// Treat subtraction as instead being addition, with the RHS argument instead wrapped in a special "negate" node.
// Treat division as instead being multiplication, with the RHS argument instead wrapped in a special "invert" node.
// Addition and multiplication are N-ary; each node can have any number of arguments.
// If an expression has only a single value in it, and no operation, treat it as an addition node with the single argument.
const root = convertTokensToAST([...num.values]);
// 4. Recursively transform the expression tree into objects
const numericValue = transformToCSSNumericValue(root);
let simplifiedValue;
try {
// TODO: Update when we have clarification on where simplify a calculation should be run:
// https://github.com/w3c/csswg-drafts/issues/9870
simplifiedValue = simplifyCalculation(numericValue);
} catch (e) {
// Use insertRule to trigger native SyntaxError on TypeError
(new CSSStyleSheet()).insertRule('error', 0);
}
if (simplifiedValue instanceof CSSUnitValue) {
return new CSSMathSum(simplifiedValue);
} else {
return simplifiedValue;
}
}
/**
* Reify a numeric value
* https://drafts.css-houdini.org/css-typed-om/#reify-a-numeric-value
* @param num
*/
function reifyNumericValue(num) {
// If an internal representation contains a var() reference, then it is reified by reifying a list of component values,
// regardless of what property it is for.
// TODO: handle `var()` function
// If num is a math function, reify a math expression from num and return the result.
if (num instanceof CSSFunction && ['calc', 'min', 'max', 'clamp'].includes(num.name)) {
return reifyMathExpression(num);
}
// If num is the unitless value 0 and num is a <dimension>,
// return a new CSSUnitValue with its value internal slot set to 0, and its unit internal slot set to "px".
if (num instanceof NumberToken && num.value === 0 && !num.unit) {
return new CSSUnitValue(0, 'px');
}
// Return a new CSSUnitValue with its value internal slot set to the numeric value of num, and its unit internal slot
// set to "number" if num is a <number>, "percent" if num is a <percentage>, and num’s unit if num is a <dimension>.
if (num instanceof NumberToken) {
return new CSSUnitValue(num.value, 'number');
} else if (num instanceof PercentageToken) {
return new CSSUnitValue(num.value, 'percent');
} else if (num instanceof DimensionToken) {
return new CSSUnitValue(num.value, num.unit);
}
}
/**
* Implementation of the parse(cssText) method.
* https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-parse
* @param {string} cssText
* @return {CSSMathMin|CSSMathMax|CSSMathSum|CSSMathProduct|CSSMathNegate|CSSMathInvert|CSSUnitValue}
*/
export function parseCSSNumericValue(cssText) {
// Parse a component value from cssText and let result be the result.
// If result is a syntax error, throw a SyntaxError and abort this algorithm.
const result = parseComponentValue(cssText);
if (result === null) {
// Use insertRule to trigger native SyntaxError
(new CSSStyleSheet()).insertRule('error', 0);
}
// If result is not a <number-token>, <percentage-token>, <dimension-token>, or a math function, throw a SyntaxError and abort this algorithm.
if (!(result instanceof NumberToken || result instanceof PercentageToken || result instanceof DimensionToken || result instanceof CSSFunction)) {
// Use insertRule to trigger native SyntaxError
(new CSSStyleSheet()).insertRule('error', 0);
}
// If result is a <dimension-token> and creating a type from result’s unit returns failure, throw a SyntaxError and abort this algorithm.
if (result instanceof DimensionToken) {
const type = createAType(result.unit);
if (type === null) {
// Use insertRule to trigger native SyntaxError
(new CSSStyleSheet()).insertRule('error', 0);
}
}
// Reify a numeric value result, and return the result.
return reifyNumericValue(result);
}