diff --git a/src/css-typed-om/css-keyword-value.js b/src/css-typed-om/css-keyword-value.js new file mode 100644 index 0000000..f88b102 --- /dev/null +++ b/src/css-typed-om/css-keyword-value.js @@ -0,0 +1,11 @@ +export class CSSKeywordValue { + #value; + + constructor(value) { + this.#value = value; + } + + toString() { + return this.#value.toString(); + } +} \ No newline at end of file diff --git a/src/css-typed-om/css-math-invert.js b/src/css-typed-om/css-math-invert.js new file mode 100644 index 0000000..18e3892 --- /dev/null +++ b/src/css-typed-om/css-math-invert.js @@ -0,0 +1,17 @@ +import {invertType} from './procedures'; +import {CSSMathValue} from './css-math-value'; + +export class CSSMathInvert extends CSSMathValue { + constructor(values) { + super([1, arguments[0]], 'invert', 'calc', ' / '); + } + + get value() { + return this.values[1]; + } + + type() { + // The type of a CSSUnitValue is the result of creating a type from its unit internal slot. + return invertType(this.values[1].type()) + } +} \ No newline at end of file diff --git a/src/css-typed-om/css-math-max.js b/src/css-typed-om/css-math-max.js new file mode 100644 index 0000000..df46c4c --- /dev/null +++ b/src/css-typed-om/css-math-max.js @@ -0,0 +1,7 @@ +import {CSSMathValue} from './css-math-value'; + +export class CSSMathMax extends CSSMathValue { + constructor() { + super(arguments, 'max'); + } +} \ No newline at end of file diff --git a/src/css-typed-om/css-math-min.js b/src/css-typed-om/css-math-min.js new file mode 100644 index 0000000..7591f42 --- /dev/null +++ b/src/css-typed-om/css-math-min.js @@ -0,0 +1,7 @@ +import {CSSMathValue} from './css-math-value'; + +export class CSSMathMin extends CSSMathValue { + constructor() { + super(arguments, 'min'); + } +} diff --git a/src/css-typed-om/css-math-negate.js b/src/css-typed-om/css-math-negate.js new file mode 100644 index 0000000..ea56de2 --- /dev/null +++ b/src/css-typed-om/css-math-negate.js @@ -0,0 +1,15 @@ +import {CSSMathValue} from './css-math-value'; + +export class CSSMathNegate extends CSSMathValue { + constructor(values) { + super([arguments[0]], 'negate', '-'); + } + + get value() { + return this.values[0]; + } + + type() { + return this.value.type(); + } +} \ No newline at end of file diff --git a/src/css-typed-om/css-math-product.js b/src/css-typed-om/css-math-product.js new file mode 100644 index 0000000..ef4ad89 --- /dev/null +++ b/src/css-typed-om/css-math-product.js @@ -0,0 +1,17 @@ +import {multiplyTypes, toSum} from './procedures'; +import {CSSMathValue} from './css-math-value'; + +export class CSSMathProduct extends CSSMathValue { + constructor(values) { + super(arguments, 'product', 'calc', ' * '); + } + + toSum(...units) { + return toSum(this, ...units) + } + + type() { + // The type is the result of multiplying the types of each of the items in its values internal slot. + return this.values.map(v => v.type()).reduce(multiplyTypes) + } +} \ No newline at end of file diff --git a/src/css-typed-om/css-math-sum.js b/src/css-typed-om/css-math-sum.js new file mode 100644 index 0000000..f87c509 --- /dev/null +++ b/src/css-typed-om/css-math-sum.js @@ -0,0 +1,7 @@ +import {CSSMathValue} from './css-math-value'; + +export class CSSMathSum extends CSSMathValue { + constructor(values) { + super(arguments, 'sum', 'calc', ' + '); + } +} \ No newline at end of file diff --git a/src/css-typed-om/css-math-value.js b/src/css-typed-om/css-math-value.js new file mode 100644 index 0000000..c969988 --- /dev/null +++ b/src/css-typed-om/css-math-value.js @@ -0,0 +1,41 @@ +import {CSSNumericValue} from './css-numeric-value' +function toCssUnitValue(v) { + if (typeof v === 'number') + return new CSSUnitValue(v, 'number'); + return v; +} + +function toCssNumericArray(values) { + const result = []; + for (let i = 0; i < values.length; i++) { + result[i] = toCssUnitValue(values[i]); + } + return result; +} + +export class CSSMathValue extends CSSNumericValue { + #values; + #operator; + #name; + #delimiter; + + constructor(values, operator, opt_name, opt_delimiter) { + super(); + this.#values = toCssNumericArray(values); + this.#operator = operator; + this.#name = opt_name || operator; + this.#delimiter = opt_delimiter || ', '; + } + + get operator() { + return this.#operator; + } + + get values() { + return this.#values; + } + + toString() { + return `${this.#name}(${this.#values.join(this.#delimiter)})`; + } +} diff --git a/src/css-typed-om/css-numeric-value.js b/src/css-typed-om/css-numeric-value.js new file mode 100644 index 0000000..c2ee65a --- /dev/null +++ b/src/css-typed-om/css-numeric-value.js @@ -0,0 +1,13 @@ +import {parseCSSNumericValue} from './parse-numeric-value'; + +export class CSSNumericValue { + static parse(value) { + if (value instanceof CSSNumericValue) + return value; + + return parseCSSNumericValue(value); + } + + // TODO: Add other methods: add, sub, mul, div, … + // Spec: https://drafts.css-houdini.org/css-typed-om/#numeric-value +} \ No newline at end of file diff --git a/src/css-typed-om/css-unit-value.js b/src/css-typed-om/css-unit-value.js new file mode 100644 index 0000000..89894cd --- /dev/null +++ b/src/css-typed-om/css-unit-value.js @@ -0,0 +1,53 @@ +import {CSSNumericValue} from './css-numeric-value'; +import {createAType, to, toSum} from './procedures'; + +function displayUnit(unit) { + switch (unit) { + case 'percent': + return '%'; + case 'number': + return ''; + default: + return unit.toLowerCase(); + } +} + +export class CSSUnitValue extends CSSNumericValue { + #value; + #unit; + + constructor(value, unit) { + super(); + this.#value = value; + this.#unit = unit; + } + + get value() { + return this.#value; + } + + set value(value) { + this.#value = value; + } + + get unit() { + return this.#unit; + } + + to(unit) { + return to(this, unit); + } + + toSum(...units) { + return toSum(this, ...units); + } + + type() { + // The type of a CSSUnitValue is the result of creating a type from its unit internal slot. + return createAType(this.#unit); + } + + toString() { + return `${this.#value}${displayUnit(this.#unit)}`; + } +} \ No newline at end of file diff --git a/src/css-typed-om/parse-numeric-value.js b/src/css-typed-om/parse-numeric-value.js new file mode 100644 index 0000000..d280648 --- /dev/null +++ b/src/css-typed-om/parse-numeric-value.js @@ -0,0 +1,439 @@ +import {simplifyCalculation} from '../simplify-calculation'; +import { + CommaToken, + DelimToken, + DimensionToken, + FunctionToken, + IdentToken, + LeftCurlyBracketToken, + LeftParenthesisToken, + LeftSquareBracketToken, + NumberToken, + PercentageToken, + RightCurlyBracketToken, + RightParenthesisToken, + RightSquareBracketToken, + WhitespaceToken +} from './tokens'; +import {tokenizeString} from './tokenizer'; +import {createAType} from './procedures.js'; + +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') { + // + // 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') { + // + // 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 , 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 , consume the next input token from input. + while (tokens[0] instanceof WhitespaceToken) { + tokens.shift(); + } + // 3. If the next input token from input is an , 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 , consume the next input token. + while (tokens[0] instanceof WhitespaceToken) { + tokens.shift(); + } + // 6. If the next input token from input is an , 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 , + // 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 , "percent" if num is a , and num’s unit if num is a . + 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 , , , 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 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); +} \ No newline at end of file diff --git a/src/css-typed-om/procedures.js b/src/css-typed-om/procedures.js new file mode 100644 index 0000000..2b89986 --- /dev/null +++ b/src/css-typed-om/procedures.js @@ -0,0 +1,409 @@ +/** + * @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; +} \ No newline at end of file diff --git a/src/css-typed-om/proxy-cssom.js b/src/css-typed-om/proxy-cssom.js new file mode 100644 index 0000000..67bf477 --- /dev/null +++ b/src/css-typed-om/proxy-cssom.js @@ -0,0 +1,96 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import {CSSNumericValue} from './css-numeric-value' +import {CSSUnitValue} from './css-unit-value' +import {CSSMathValue} from './css-math-value' +import {CSSKeywordValue} from './css-keyword-value'; +import {CSSMathSum} from './css-math-sum'; +import {CSSMathProduct} from './css-math-product'; +import {CSSMathNegate} from './css-math-negate'; +import {CSSMathInvert} from './css-math-invert'; +import {CSSMathMax} from './css-math-max'; +import {CSSMathMin} from './css-math-min'; + +export function installCSSOM() { + const cssOMTypes = { + 'CSSNumericValue': CSSNumericValue, + 'CSSMathValue': CSSMathValue, + 'CSSUnitValue': CSSUnitValue, + 'CSSKeywordValue': CSSKeywordValue, + 'CSSMathSum': CSSMathSum, + 'CSSMathProduct': CSSMathProduct, + 'CSSMathNegate': CSSMathNegate, + 'CSSMathInvert': CSSMathInvert, + 'CSSMathMax': CSSMathMax, + 'CSSMathMin': CSSMathMin }; + + if (!window.CSS) { + if (!Reflect.defineProperty(window, 'CSS', { value: {} })) + throw Error(`Error installing CSSOM support`); + } + + if (!window.CSSUnitValue) { + [ + 'number', + 'percent', + // Length units + 'em', + 'ex', + 'px', + 'cm', + 'mm', + 'in', + 'pt', + 'pc', // Picas + 'Q', // Quarter millimeter + 'vw', + 'vh', + 'vmin', + 'vmax', + 'rems', + "ch", + // Angle units + 'deg', + 'rad', + 'grad', + 'turn', + // Time units + 'ms', + 's', + 'Hz', + 'kHz', + // Resolution + 'dppx', + 'dpi', + 'dpcm', + // Other units + "fr" + ].forEach((name) => { + const fn = (value) => { + return new CSSUnitValue(value, name); + }; + if (!Reflect.defineProperty(CSS, name, { value: fn })) + throw Error(`Error installing CSS.${name}`); + }); + } + + for (let [type, value] of Object.entries(cssOMTypes)) { + if (type in window) + continue; + if (!Reflect.defineProperty(window, type, { value })) + throw Error(`Error installing CSSOM support for ${type}`); + + Object.defineProperty(value, 'name', { value: type }); + } +} diff --git a/src/tokenizer.js b/src/css-typed-om/tokenizer.js similarity index 91% rename from src/tokenizer.js rename to src/css-typed-om/tokenizer.js index 273a21b..f9ed780 100644 --- a/src/tokenizer.js +++ b/src/css-typed-om/tokenizer.js @@ -1,125 +1,29 @@ - -export class Token {} - -// The output of tokenization step is a stream of zero or more of the following tokens: , , -// , , , , , , , -// , , , , , , , -// , , <[-token>, <]-token>, <(-token>, <)-token>, <{-token>, and <}-token>. -export class IdentToken extends Token { - value; - constructor(value) { - super(); - this.value = value; - } -} - -export class FunctionToken extends Token { - value; - constructor(value) { - super(); - this.value = value; - } -} - -export class AtKeywordToken extends Token { - value; - constructor(value) { - super(); - this.value = value; - } -} - -export class HashToken extends Token { - type; - value; - constructor(value, type = 'unrestricted') { - super(); - this.value = value; - this.type = type; - } -} - -export class StringToken extends Token { - value; - constructor(value) { - super(); - this.value = value; - } -} - -export class BadStringToken extends Token {} - -export class UrlToken extends Token { - value; - constructor(value) { - super(); - this.value = value; - } -} - -export class BadUrlToken extends Token {} - -export class DelimToken extends Token { - value; - constructor(value) { - super(); - this.value = value; - } -} - -export class NumberToken extends Token { - value; - type; - constructor(value, type = "integer") { - super(); - this.value = value; - this.type = type; - } -} - -export class PercentageToken extends Token { - value; - constructor(value) { - super(); - this.value = value; - } -} - -export class DimensionToken extends Token { - value; - type; - unit; - constructor(value, type, unit) { - super(); - this.value = value; - this.type = type; - this.unit = unit; - } -} - -export class WhitespaceToken extends Token {} - -export class CDOToken extends Token {} - -export class CDCToken extends Token {} - -export class ColonToken extends Token {} - -export class SemicolonToken extends Token {} - -export class CommaToken extends Token {} - -export class LeftSquareBracketToken extends Token {} - -export class RightSquareBracketToken extends Token {} - -export class LeftParenthesisToken extends Token {} - -export class RightParenthesisToken extends Token {} - -export class LeftCurlyBracketToken extends Token {} - -export class RightCurlyBracketToken extends Token {} +import { + AtKeywordToken, + BadStringToken, + BadUrlToken, + CDCToken, + CDOToken, + ColonToken, + CommaToken, + DelimToken, + DimensionToken, + FunctionToken, + HashToken, + IdentToken, + LeftCurlyBracketToken, + LeftParenthesisToken, + LeftSquareBracketToken, + NumberToken, + PercentageToken, + RightCurlyBracketToken, + RightParenthesisToken, + RightSquareBracketToken, + SemicolonToken, + StringToken, + UrlToken, + WhitespaceToken +} from './tokens'; class InputStream { input diff --git a/src/css-typed-om/tokens.js b/src/css-typed-om/tokens.js new file mode 100644 index 0000000..53d61f7 --- /dev/null +++ b/src/css-typed-om/tokens.js @@ -0,0 +1,122 @@ + +export class Token {} + +// The output of tokenization step is a stream of zero or more of the following tokens: , , +// , , , , , , , +// , , , , , , , +// , , <[-token>, <]-token>, <(-token>, <)-token>, <{-token>, and <}-token>. +export class IdentToken extends Token { + value; + constructor(value) { + super(); + this.value = value; + } +} + +export class FunctionToken extends Token { + value; + constructor(value) { + super(); + this.value = value; + } +} + +export class AtKeywordToken extends Token { + value; + constructor(value) { + super(); + this.value = value; + } +} + +export class HashToken extends Token { + type; + value; + constructor(value, type = 'unrestricted') { + super(); + this.value = value; + this.type = type; + } +} + +export class StringToken extends Token { + value; + constructor(value) { + super(); + this.value = value; + } +} + +export class BadStringToken extends Token {} + +export class UrlToken extends Token { + value; + constructor(value) { + super(); + this.value = value; + } +} + +export class BadUrlToken extends Token {} + +export class DelimToken extends Token { + value; + constructor(value) { + super(); + this.value = value; + } +} + +export class NumberToken extends Token { + value; + type; + constructor(value, type = "integer") { + super(); + this.value = value; + this.type = type; + } +} + +export class PercentageToken extends Token { + value; + constructor(value) { + super(); + this.value = value; + } +} + +export class DimensionToken extends Token { + value; + type; + unit; + constructor(value, type, unit) { + super(); + this.value = value; + this.type = type; + this.unit = unit; + } +} + +export class WhitespaceToken extends Token {} + +export class CDOToken extends Token {} + +export class CDCToken extends Token {} + +export class ColonToken extends Token {} + +export class SemicolonToken extends Token {} + +export class CommaToken extends Token {} + +export class LeftSquareBracketToken extends Token {} + +export class RightSquareBracketToken extends Token {} + +export class LeftParenthesisToken extends Token {} + +export class RightParenthesisToken extends Token {} + +export class LeftCurlyBracketToken extends Token {} + +export class RightCurlyBracketToken extends Token {} diff --git a/src/numeric-values.js b/src/numeric-values.js deleted file mode 100644 index 9c212d4..0000000 --- a/src/numeric-values.js +++ /dev/null @@ -1,844 +0,0 @@ -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') { - // - // 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') { - // - // 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 , 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 , consume the next input token from input. - while (tokens[0] instanceof WhitespaceToken) { - tokens.shift(); - } - // 3. If the next input token from input is an , 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 , consume the next input token. - while (tokens[0] instanceof WhitespaceToken) { - tokens.shift(); - } - // 6. If the next input token from input is an , 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 , - // 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 , "percent" if num is a , and num’s unit if num is a . - 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 , , , 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 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); -} \ No newline at end of file diff --git a/src/proxy-cssom.js b/src/proxy-cssom.js index 519d338..e69de29 100644 --- a/src/proxy-cssom.js +++ b/src/proxy-cssom.js @@ -1,260 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -import {createAType, invertType, multiplyTypes, parseCSSNumericValue, to, toSum} from './numeric-values'; -import {simplifyCalculation} from './simplify-calculation'; -import './tokenizer' - -export function installCSSOM() { - // Object for storing details associated with an object which are to be kept - // private. This approach allows the constructed objects to more closely - // resemble their native counterparts when inspected. - let privateDetails = new WeakMap(); - - function displayUnit(unit) { - switch(unit) { - case 'percent': - return '%'; - case 'number': - return ''; - default: - return unit.toLowerCase(); - } - } - - function toCssUnitValue(v) { - if (typeof v === 'number') - return new CSSUnitValue(v, 'number'); - return v; - } - - function toCssNumericArray(values) { - const result = []; - for (let i = 0; i < values.length; i++) { - result[i] = toCssUnitValue(values[i]); - } - return result; - } - - class CSSNumericValue { - static parse(value) { - if (value instanceof CSSNumericValue) return value; - - return simplifyCalculation(parseCSSNumericValue(value), {}); - } - - // TODO: Add other methods: add, sub, mul, div, … - // Spec: https://drafts.css-houdini.org/css-typed-om/#numeric-value - } - - class CSSMathValue extends CSSNumericValue { - constructor(values, operator, opt_name, opt_delimiter) { - super(); - privateDetails.set(this, { - values: toCssNumericArray(values), - operator: operator, - name: opt_name || operator, - delimiter: opt_delimiter || ', ' - }); - } - - get operator() { - return privateDetails.get(this).operator; - } - - get values() { - return privateDetails.get(this).values; - } - - toString() { - const details = privateDetails.get(this); - return `${details.name}(${details.values.join(details.delimiter)})`; - } - } - - const cssOMTypes = { - 'CSSNumericValue': CSSNumericValue, - 'CSSMathValue': CSSMathValue, - 'CSSUnitValue': class extends CSSNumericValue { - constructor(value, unit) { - super(); - privateDetails.set(this, { - value: value, - unit: unit - }); - } - - get value() { - return privateDetails.get(this).value; - } - - set value(value) { - privateDetails.get(this).value = value; - } - - get unit() { - return privateDetails.get(this).unit; - } - - to(unit) { - return to(this, unit) - } - - toSum(...units) { - return toSum(this, ...units) - } - - type() { - const details = privateDetails.get(this) - // The type of a CSSUnitValue is the result of creating a type from its unit internal slot. - return createAType(details.unit) - } - - toString() { - const details = privateDetails.get(this); - return `${details.value}${displayUnit(details.unit)}`; - } - }, - - 'CSSKeywordValue': class { - constructor(value) { - this.value = value; - } - - toString() { - return this.value.toString(); - } - }, - - 'CSSMathSum': class extends CSSMathValue { - constructor(values) { - super(arguments, 'sum', 'calc', ' + '); - } - }, - - 'CSSMathProduct': class extends CSSMathValue { - constructor(values) { - super(arguments, 'product', 'calc', ' * '); - } - - toSum(...units) { - return toSum(this, ...units) - } - - type() { - const values = privateDetails.get(this).values; - // The type is the result of multiplying the types of each of the items in its values internal slot. - return values.map(v => v.type()).reduce(multiplyTypes) - } - }, - - 'CSSMathNegate': class extends CSSMathValue { - constructor(values) { - super([arguments[0]], 'negate', '-'); - } - - get value() { - return privateDetails.get(this).values[0]; - } - - type() { - return this.value.type(); - } - }, - - 'CSSMathInvert': class extends CSSMathValue { - constructor(values) { - super([1, arguments[0]], 'invert', 'calc', ' / '); - } - - get value() { - return privateDetails.get(this).values[1]; - } - - type() { - const details = privateDetails.get(this) - // The type of a CSSUnitValue is the result of creating a type from its unit internal slot. - return invertType(details.values[1].type()) - } - }, - - 'CSSMathMax': class extends CSSMathValue { - constructor() { - super(arguments, 'max'); - } - }, - - 'CSSMathMin': class extends CSSMathValue { - constructor() { - super(arguments, 'min'); - } - } - }; - - if (!window.CSS) { - if (!Reflect.defineProperty(window, 'CSS', { value: {} })) - throw Error(`Error installing CSSOM support`); - } - - if (!window.CSSUnitValue) { - [ - 'number', - 'percent', - // Length units - 'em', - 'ex', - 'px', - 'cm', - 'mm', - 'in', - 'pt', - 'pc', // Picas - 'Q', // Quarter millimeter - 'vw', - 'vh', - 'vmin', - 'vmax', - 'rems', - "ch", - // Angle units - 'deg', - 'rad', - 'grad', - 'turn', - // Time units - 'ms', - 's', - 'Hz', - 'kHz', - // Resolution - 'dppx', - 'dpi', - 'dpcm', - // Other units - "fr" - ].forEach((name) => { - const fn = (value) => { - return new CSSUnitValue(value, name); - }; - if (!Reflect.defineProperty(CSS, name, { value: fn })) - throw Error(`Error installing CSS.${name}`); - }); - } - - for (let [type, value] of Object.entries(cssOMTypes)) { - if (type in window) - continue; - if (!Reflect.defineProperty(window, type, { value })) - throw Error(`Error installing CSSOM support for ${type}`); - } -} diff --git a/src/scroll-timeline-base.js b/src/scroll-timeline-base.js index 2c85492..cb14a90 100644 --- a/src/scroll-timeline-base.js +++ b/src/scroll-timeline-base.js @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {installCSSOM} from "./proxy-cssom.js"; +import {installCSSOM} from "./css-typed-om/proxy-cssom.js"; import {simplifyCalculation} from "./simplify-calculation"; import {normalizeAxis, splitIntoComponentValues} from './utils.js'; diff --git a/test/expected.txt b/test/expected.txt index 003d769..6300dcd 100644 --- a/test/expected.txt +++ b/test/expected.txt @@ -696,6 +696,7 @@ PASS /scroll-animations/scroll-timelines/current-time-root-scroller.html current PASS /scroll-animations/scroll-timelines/current-time-writing-modes.html currentTime handles direction: rtl correctly PASS /scroll-animations/scroll-timelines/current-time-writing-modes.html currentTime handles writing-mode: vertical-rl correctly PASS /scroll-animations/scroll-timelines/current-time-writing-modes.html currentTime handles writing-mode: vertical-lr correctly +PASS /scroll-animations/scroll-timelines/duration.html The duration of a scroll timeline is 100% PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the delay to a positive number PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the delay to a negative number PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the delay of an animation in progress: positive delay that causes the animation to be no longer in-effect @@ -1026,6 +1027,7 @@ PASS /scroll-animations/view-timelines/block-view-timeline-current-time.tentativ FAIL /scroll-animations/view-timelines/block-view-timeline-nested-subject.tentative.html View timeline with subject that is not a direct descendant of the scroll container FAIL /scroll-animations/view-timelines/change-animation-range-updates-play-state.html Changing the animation range updates the play state FAIL /scroll-animations/view-timelines/contain-alignment.html Stability of animated elements aligned to the bounds of a contain region +PASS /scroll-animations/view-timelines/duration.html The duration of a view timeline is 100% PASS /scroll-animations/view-timelines/fieldset-source.html Fieldset is a valid source for a view timeline FAIL /scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html Report specified timeline offsets FAIL /scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html Computed offsets can be outside [0,1] for keyframes with timeline offsets