Skip to content

Instantly share code, notes, and snippets.

@cdata
Created May 6, 2020 17:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cdata/e73dddcefe70b10b3f899bbaee2b6a13 to your computer and use it in GitHub Desktop.
Save cdata/e73dddcefe70b10b3f899bbaee2b6a13 to your computer and use it in GitHub Desktop.
/* @license
* Copyright 2019 Google LLC. All Rights Reserved.
* 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
*
* http://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.
*/
const numberNode = (value, unit) => ({ type: 'number', number: value, unit });
/**
* Given a string representing a comma-separated set of CSS-like expressions,
* parses and returns an array of ASTs that correspond to those expressions.
*
* Currently supported syntax includes:
*
* - functions (top-level and nested)
* - calc() arithmetic operators
* - numbers with units
* - hexidecimal-encoded colors in 3, 6 or 8 digit form
* - idents
*
* All syntax is intended to match the parsing rules and semantics of the actual
* CSS spec as closely as possible.
*
* @see https://www.w3.org/TR/CSS2/
* @see https://www.w3.org/TR/css-values-3/
*/
const parseExpressions = (() => {
const cache = {};
const MAX_PARSE_ITERATIONS = 1000; // Arbitrarily large
return (inputString) => {
const cacheKey = inputString;
if (cacheKey in cache) {
return cache[cacheKey];
}
const expressions = [];
let parseIterations = 0;
while (inputString) {
if (++parseIterations > MAX_PARSE_ITERATIONS) {
// Avoid a potentially infinite loop due to typos:
inputString = '';
break;
}
const expressionParseResult = parseExpression(inputString);
const expression = expressionParseResult.nodes[0];
if (expression == null || expression.terms.length === 0) {
break;
}
expressions.push(expression);
inputString = expressionParseResult.remainingInput;
}
return cache[cacheKey] = expressions;
};
})();
/**
* Parse a single expression. For the purposes of our supported syntax, an
* expression is the set of semantically meaningful terms that appear before the
* next comma, or between the parens of a function invokation.
*/
const parseExpression = (() => {
const IS_IDENT_RE = /^(\-\-|[a-z\u0240-\uffff])/i;
const IS_OPERATOR_RE = /^([\*\+\/]|[\-]\s)/i;
const IS_EXPRESSION_END_RE = /^[\),]/;
const FUNCTION_ARGUMENTS_FIRST_TOKEN = '(';
const HEX_FIRST_TOKEN = '#';
return (inputString) => {
const terms = [];
while (inputString.length) {
inputString = inputString.trim();
if (IS_EXPRESSION_END_RE.test(inputString)) {
break;
}
else if (inputString[0] === FUNCTION_ARGUMENTS_FIRST_TOKEN) {
const { nodes, remainingInput } = parseFunctionArguments(inputString);
inputString = remainingInput;
terms.push({
type: 'function',
name: { type: 'ident', value: 'calc' },
arguments: nodes
});
}
else if (IS_IDENT_RE.test(inputString)) {
const identParseResult = parseIdent(inputString);
const identNode = identParseResult.nodes[0];
inputString = identParseResult.remainingInput;
if (inputString[0] === FUNCTION_ARGUMENTS_FIRST_TOKEN) {
const { nodes, remainingInput } = parseFunctionArguments(inputString);
terms.push({ type: 'function', name: identNode, arguments: nodes });
inputString = remainingInput;
}
else {
terms.push(identNode);
}
}
else if (IS_OPERATOR_RE.test(inputString)) {
// Operators are always a single character, so just pluck them out:
terms.push({ type: 'operator', value: inputString[0] });
inputString = inputString.slice(1);
}
else {
const { nodes, remainingInput } = inputString[0] === HEX_FIRST_TOKEN ?
parseHex(inputString) :
parseNumber(inputString);
// The remaining string may not have had any meaningful content. Exit
// early if this is the case:
if (nodes.length === 0) {
break;
}
terms.push(nodes[0]);
inputString = remainingInput;
}
}
return { nodes: [{ type: 'expression', terms }], remainingInput: inputString };
};
})();
/**
* An ident is something like a function name or the keyword "auto".
*/
const parseIdent = (() => {
const NOT_IDENT_RE = /[^a-z^0-9^_^\-^\u0240-\uffff]/i;
return (inputString) => {
const match = inputString.match(NOT_IDENT_RE);
const ident = match == null ? inputString : inputString.substr(0, match.index);
const remainingInput = match == null ? '' : inputString.substr(match.index);
return { nodes: [{ type: 'ident', value: ident }], remainingInput };
};
})();
/**
* Parses a number. A number value can be expressed with an integer or
* non-integer syntax, and usually includes a unit (but does not strictly
* require one for our purposes).
*/
const parseNumber = (() => {
// @see https://www.w3.org/TR/css-syntax/#number-token-diagram
const VALUE_RE = /[\+\-]?(\d+[\.]\d+|\d+|[\.]\d+)([eE][\+\-]?\d+)?/;
const UNIT_RE = /^[a-z%]+/i;
const ALLOWED_UNITS = /^(m|mm|cm|rad|deg|[%])$/;
return (inputString) => {
const valueMatch = inputString.match(VALUE_RE);
const value = valueMatch == null ? '0' : valueMatch[0];
inputString = value == null ? inputString : inputString.slice(value.length);
const unitMatch = inputString.match(UNIT_RE);
let unit = unitMatch != null && unitMatch[0] !== '' ? unitMatch[0] : null;
const remainingInput = unitMatch == null ? inputString : inputString.slice(unit.length);
if (unit != null && !ALLOWED_UNITS.test(unit)) {
unit = null;
}
return {
nodes: [{
type: 'number',
number: parseFloat(value) || 0,
unit: unit
}],
remainingInput
};
};
})();
/**
* Parses a hexidecimal-encoded color in 3, 6 or 8 digit form.
*/
const parseHex = (() => {
// TODO(cdata): right now we don't actually enforce the number of digits
const HEX_RE = /^[a-f0-9]*/i;
return (inputString) => {
inputString = inputString.slice(1).trim();
const hexMatch = inputString.match(HEX_RE);
const nodes = hexMatch == null ? [] : [{ type: 'hex', value: hexMatch[0] }];
return {
nodes,
remainingInput: hexMatch == null ? inputString :
inputString.slice(hexMatch[0].length)
};
};
})();
/**
* Parses arguments passed to a function invokation (e.g., the expressions
* within a matched set of parens).
*/
const parseFunctionArguments = (inputString) => {
const expressionNodes = [];
// Consume the opening paren
inputString = inputString.slice(1).trim();
while (inputString.length) {
const expressionParseResult = parseExpression(inputString);
expressionNodes.push(expressionParseResult.nodes[0]);
inputString = expressionParseResult.remainingInput.trim();
if (inputString[0] === ',') {
inputString = inputString.slice(1).trim();
}
else if (inputString[0] === ')') {
// Consume the closing paren and stop parsing
inputString = inputString.slice(1);
break;
}
}
return { nodes: expressionNodes, remainingInput: inputString };
};
const $visitedTypes = Symbol('visitedTypes');
/**
* An ASTWalker walks an array of ASTs such as the type produced by
* parseExpressions and invokes a callback for a configured set of nodes that
* the user wishes to "visit" during the walk.
*/
class ASTWalker {
constructor(visitedTypes) {
this[$visitedTypes] = visitedTypes;
}
/**
* Walk the given set of ASTs, and invoke the provided callback for nodes that
* match the filtered set that the ASTWalker was constructed with.
*/
walk(ast, callback) {
const remaining = ast.slice();
while (remaining.length) {
const next = remaining.shift();
if (this[$visitedTypes].indexOf(next.type) > -1) {
callback(next);
}
switch (next.type) {
case 'expression':
remaining.unshift(...next.terms);
break;
case 'function':
remaining.unshift(next.name, ...next.arguments);
break;
}
}
}
}
const ZERO = Object.freeze({ type: 'number', number: 0, unit: null });
/* @license
* Copyright 2019 Google LLC. All Rights Reserved.
* 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
*
* http://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.
*/
/**
* Ensures that a given number is expressed in radians. If the number is already
* in radians, does nothing. If the value is in degrees, converts it to radians.
* If the value has no specified unit, the unit is assumed to be radians. If the
* value is not in radians or degrees, the value is resolved as 0 radians.
*
* Also accepts a second argument that is a default value to use if the input
* numberNode number is NaN or Infinity.
*/
const degreesToRadians = (numberNode$$1, fallbackRadianValue = 0) => {
let { number, unit } = numberNode$$1;
if (!isFinite(number)) {
number = fallbackRadianValue;
unit = 'rad';
}
else if (numberNode$$1.unit === 'rad' || numberNode$$1.unit == null) {
return numberNode$$1;
}
const valueIsDegrees = unit === 'deg' && number != null;
const value = valueIsDegrees ? number : 0;
const radians = value * Math.PI / 180;
return { type: 'number', number: radians, unit: 'rad' };
};
/**
* Ensures that a given number is expressed in degrees. If the number is alrady
* in degrees, does nothing. If the value is in radians or has no specified
* unit, converts it to degrees. If the value is not in radians or degrees, the
* value is resolved as 0 degrees.
*
* Also accepts a second argument that is a default value to use if the input
* numberNode number is NaN or Infinity.
*/
const radiansToDegrees = (numberNode$$1, fallbackDegreeValue = 0) => {
let { number, unit } = numberNode$$1;
if (!isFinite(number)) {
number = fallbackDegreeValue;
unit = 'deg';
}
else if (numberNode$$1.unit === 'deg') {
return numberNode$$1;
}
const valueIsRadians = (unit === null || unit === 'rad') && number != null;
const value = valueIsRadians ? number : 0;
const degrees = value * 180 / Math.PI;
return { type: 'number', number: degrees, unit: 'deg' };
};
/**
* Converts a given length to meters. Currently supported input units are
* meters, centimeters and millimeters.
*
* Also accepts a second argument that is a default value to use if the input
* numberNode number is NaN or Infinity.
*/
const lengthToBaseMeters = (numberNode$$1, fallbackMeterValue = 0) => {
let { number, unit } = numberNode$$1;
if (!isFinite(number)) {
number = fallbackMeterValue;
unit = 'm';
}
else if (numberNode$$1.unit === 'm') {
return numberNode$$1;
}
let scale;
switch (unit) {
default:
scale = 1;
break;
case 'cm':
scale = 1 / 100;
break;
case 'mm':
scale = 1 / 1000;
break;
}
const value = scale * number;
return { type: 'number', number: value, unit: 'm' };
};
/**
* Normalizes the unit of a given input number so that it is expressed in a
* preferred unit. For length nodes, the return value will be expressed in
* meters. For angle nodes, the return value will be expressed in radians.
*
* Also takes a fallback number that is used when the number value is not a
* valid number or when the unit of the given number cannot be normalized.
*/
const normalizeUnit = (() => {
const identity = (node) => node;
const unitNormalizers = {
'rad': identity,
'deg': degreesToRadians,
'm': identity,
'mm': lengthToBaseMeters,
'cm': lengthToBaseMeters
};
return (node, fallback = ZERO) => {
let { number, unit } = node;
if (!isFinite(number)) {
number = fallback.number;
unit = fallback.unit;
}
if (unit == null) {
return node;
}
const normalize = unitNormalizers[unit];
if (normalize == null) {
return fallback;
}
return normalize(node);
};
})();
/* @license
* Copyright 2019 Google LLC. All Rights Reserved.
* 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
*
* http://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.
*/
var _a, _b, _c;
const $evaluate = Symbol('evaluate');
const $lastValue = Symbol('lastValue');
/**
* An Evaluator is used to derive a computed style from part (or all) of a CSS
* expression AST. This construct is particularly useful for complex ASTs
* containing function calls such as calc, var and env. Such styles could be
* costly to re-evaluate on every frame (and in some cases we may try to do
* that). The Evaluator construct allows us to mark sub-trees of the AST as
* constant, so that only the dynamic parts are re-evaluated. It also separates
* one-time AST preparation work from work that necessarily has to happen upon
* each evaluation.
*/
class Evaluator {
constructor() {
this[_a] = null;
}
/**
* An Evaluatable is a NumberNode or an Evaluator that evaluates a NumberNode
* as the result of invoking its evaluate method. This is mainly used to
* ensure that CSS function nodes are cast to the corresponding Evaluators
* that will resolve the result of the function, but is also used to ensure
* that a percentage nested at arbitrary depth in the expression will always
* be evaluated against the correct basis.
*/
static evaluatableFor(node, basis = ZERO) {
if (node instanceof Evaluator) {
return node;
}
if (node.type === 'number') {
if (node.unit === '%') {
return new PercentageEvaluator(node, basis);
}
return node;
}
switch (node.name.value) {
case 'calc':
return new CalcEvaluator(node, basis);
case 'env':
return new EnvEvaluator(node);
}
return ZERO;
}
/**
* If the input is an Evaluator, returns the result of evaluating it.
* Otherwise, returns the input.
*
* This is a helper to aide in resolving a NumberNode without conditionally
* checking if the Evaluatable is an Evaluator everywhere.
*/
static evaluate(evaluatable) {
if (evaluatable instanceof Evaluator) {
return evaluatable.evaluate();
}
return evaluatable;
}
/**
* If the input is an Evaluator, returns the value of its isConstant property.
* Returns true for all other input values.
*/
static isConstant(evaluatable) {
if (evaluatable instanceof Evaluator) {
return evaluatable.isConstant;
}
return true;
}
/**
* This method applies a set of structured intrinsic metadata to an evaluated
* result from a parsed CSS-like string of expressions. Intrinsics provide
* sufficient metadata (e.g., basis values, analogs for keywords) such that
* omitted values in the input string can be backfilled, and keywords can be
* converted to concrete numbers.
*
* The result of applying intrinsics is a tuple of NumberNode values whose
* units match the units used by the basis of the intrinsics.
*
* The following is a high-level description of how intrinsics are applied:
*
* 1. Determine the value of 'auto' for the current term
* 2. If there is no corresponding input value for this term, substitute the
* 'auto' value.
* 3. If the term is an IdentNode, treat it as a keyword and perform the
* appropriate substitution.
* 4. If the term is still null, fallback to the 'auto' value
* 5. If the term is a percentage, apply it to the basis and return that
* value
* 6. Normalize the unit of the term
* 7. If the term's unit does not match the basis unit, return the basis
* value
* 8. Return the term as is
*/
static applyIntrinsics(evaluated, intrinsics) {
const { basis, keywords } = intrinsics;
const { auto } = keywords;
return basis.map((basisNode, index) => {
// Use an auto value if we have it, otherwise the auto value is the basis:
const autoSubstituteNode = auto[index] == null ? basisNode : auto[index];
// If the evaluated nodes do not have a node at the current
// index, fallback to the "auto" substitute right away:
let evaluatedNode = evaluated[index] ? evaluated[index] : autoSubstituteNode;
// Any ident node is considered a keyword:
if (evaluatedNode.type === 'ident') {
const keyword = evaluatedNode.value;
// Substitute any keywords for concrete values first:
if (keyword in keywords) {
evaluatedNode = keywords[keyword][index];
}
}
// If we don't have a NumberNode at this point, fall back to whatever
// is specified for auto:
if (evaluatedNode == null || evaluatedNode.type === 'ident') {
evaluatedNode = autoSubstituteNode;
}
// For percentages, we always apply the percentage to the basis value:
if (evaluatedNode.unit === '%') {
return numberNode(evaluatedNode.number / 100 * basisNode.number, basisNode.unit);
}
// Otherwise, normalize whatever we have:
evaluatedNode = normalizeUnit(evaluatedNode, basisNode);
// If the normalized units do not match, return the basis as a fallback:
if (evaluatedNode.unit !== basisNode.unit) {
return basisNode;
}
// Finally, return the evaluated node with intrinsics applied:
return evaluatedNode;
});
}
/**
* If true, the Evaluator will only evaluate its AST one time. If false, the
* Evaluator will re-evaluate the AST each time that the public evaluate
* method is invoked.
*/
get isConstant() {
return false;
}
/**
* Evaluate the Evaluator and return the result. If the Evaluator is constant,
* the corresponding AST will only be evaluated once, and the result of
* evaluating it the first time will be returned on all subsequent
* evaluations.
*/
evaluate() {
if (!this.isConstant || this[$lastValue] == null) {
this[$lastValue] = this[$evaluate]();
}
return this[$lastValue];
}
}
_a = $lastValue;
const $percentage = Symbol('percentage');
const $basis = Symbol('basis');
/**
* A PercentageEvaluator scales a given basis value by a given percentage value.
* The evaluated result is always considered to be constant.
*/
class PercentageEvaluator extends Evaluator {
constructor(percentage, basis) {
super();
this[$percentage] = percentage;
this[$basis] = basis;
}
get isConstant() {
return true;
}
[$evaluate]() {
return numberNode(this[$percentage].number / 100 * this[$basis].number, this[$basis].unit);
}
}
const $identNode = Symbol('identNode');
/**
* Evaluator for CSS-like env() functions. Currently, only one environment
* variable is accepted as an argument for such functions: window-scroll-y.
*
* The env() Evaluator is explicitly dynamic because it always refers to
* external state that changes as the user scrolls, so it should always be
* re-evaluated to ensure we get the most recent value.
*
* Some important notes about this feature include:
*
* - There is no such thing as a "window-scroll-y" CSS environment variable in
* any stable browser at the time that this comment is being written.
* - The actual CSS env() function accepts a second argument as a fallback for
* the case that the specified first argument isn't set; our syntax does not
* support this second argument.
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/env
*/
class EnvEvaluator extends Evaluator {
constructor(envFunction) {
super();
this[_b] = null;
const identNode = envFunction.arguments.length ? envFunction.arguments[0].terms[0] : null;
if (identNode != null && identNode.type === 'ident') {
this[$identNode] = identNode;
}
}
get isConstant() {
return false;
}
;
[(_b = $identNode, $evaluate)]() {
if (this[$identNode] != null) {
switch (this[$identNode].value) {
case 'window-scroll-y':
const verticalScrollPosition = window.pageYOffset;
const verticalScrollMax = Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);
const scrollY = verticalScrollPosition /
(verticalScrollMax - window.innerHeight) ||
0;
return { type: 'number', number: scrollY, unit: null };
}
}
return ZERO;
}
}
const IS_MULTIPLICATION_RE = /[\*\/]/;
const $evaluator = Symbol('evalutor');
/**
* Evaluator for CSS-like calc() functions. Our implementation of calc()
* evaluation currently support nested function calls, an unlimited number of
* terms, and all four algebraic operators (+, -, * and /).
*
* The Evaluator is marked as constant unless the calc expression contains an
* internal env expression at any depth, in which case it will be marked as
* dynamic.
*
* @see https://www.w3.org/TR/css-values-3/#calc-syntax
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/calc
*/
class CalcEvaluator extends Evaluator {
constructor(calcFunction, basis = ZERO) {
super();
this[_c] = null;
if (calcFunction.arguments.length !== 1) {
return;
}
const terms = calcFunction.arguments[0].terms.slice();
const secondOrderTerms = [];
while (terms.length) {
const term = terms.shift();
if (secondOrderTerms.length > 0) {
const previousTerm = secondOrderTerms[secondOrderTerms.length - 1];
if (previousTerm.type === 'operator' &&
IS_MULTIPLICATION_RE.test(previousTerm.value)) {
const operator = secondOrderTerms.pop();
const leftValue = secondOrderTerms.pop();
if (leftValue == null) {
return;
}
secondOrderTerms.push(new OperatorEvaluator(operator, Evaluator.evaluatableFor(leftValue, basis), Evaluator.evaluatableFor(term, basis)));
continue;
}
}
secondOrderTerms.push(term.type === 'operator' ? term :
Evaluator.evaluatableFor(term, basis));
}
while (secondOrderTerms.length > 2) {
const [left, operator, right] = secondOrderTerms.splice(0, 3);
if (operator.type !== 'operator') {
return;
}
secondOrderTerms.unshift(new OperatorEvaluator(operator, Evaluator.evaluatableFor(left, basis), Evaluator.evaluatableFor(right, basis)));
}
// There should only be one combined evaluator at this point:
if (secondOrderTerms.length === 1) {
this[$evaluator] = secondOrderTerms[0];
}
}
get isConstant() {
return this[$evaluator] == null || Evaluator.isConstant(this[$evaluator]);
}
[(_c = $evaluator, $evaluate)]() {
return this[$evaluator] != null ? Evaluator.evaluate(this[$evaluator]) :
ZERO;
}
}
const $operator = Symbol('operator');
const $left = Symbol('left');
const $right = Symbol('right');
/**
* An Evaluator for the operators found inside CSS calc() functions.
* The evaluator accepts an operator and left/right operands. The operands can
* be any valid expression term typically allowed inside a CSS calc function.
*
* As detail of this implementation, the only supported unit types are angles
* expressed as radians or degrees, and lengths expressed as meters, centimeters
* or millimeters.
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/calc
*/
class OperatorEvaluator extends Evaluator {
constructor(operator, left, right) {
super();
this[$operator] = operator;
this[$left] = left;
this[$right] = right;
}
get isConstant() {
return Evaluator.isConstant(this[$left]) &&
Evaluator.isConstant(this[$right]);
}
[$evaluate]() {
const leftNode = normalizeUnit(Evaluator.evaluate(this[$left]));
const rightNode = normalizeUnit(Evaluator.evaluate(this[$right]));
const { number: leftValue, unit: leftUnit } = leftNode;
const { number: rightValue, unit: rightUnit } = rightNode;
// Disallow operations for mismatched normalized units e.g., m and rad:
if (rightUnit != null && leftUnit != null && rightUnit != leftUnit) {
return ZERO;
}
// NOTE(cdata): rules for calc type checking are defined here
// https://drafts.csswg.org/css-values-3/#calc-type-checking
// This is a simplification and may not hold up once we begin to support
// additional unit types:
const unit = leftUnit || rightUnit;
let value;
switch (this[$operator].value) {
case '+':
value = leftValue + rightValue;
break;
case '-':
value = leftValue - rightValue;
break;
case '/':
value = leftValue / rightValue;
break;
case '*':
value = leftValue * rightValue;
break;
default:
return ZERO;
}
return { type: 'number', number: value, unit };
}
}
const $evaluatables = Symbol('evaluatables');
const $intrinsics = Symbol('intrinsics');
/**
* A VectorEvaluator evaluates a series of numeric terms that usually represent
* a data structure such as a multi-dimensional vector or a spherical
*
* The form of the evaluator's result is determined by the Intrinsics that are
* given to it when it is constructed. For example, spherical intrinsics would
* establish two angle terms and a length term, so the result of evaluating the
* evaluator that is configured with spherical intrinsics is a three element
* array where the first two elements represent angles in radians and the third
* element representing a length in meters.
*/
class StyleEvaluator extends Evaluator {
constructor(expressions, intrinsics) {
super();
this[$intrinsics] = intrinsics;
const firstExpression = expressions[0];
const terms = firstExpression != null ? firstExpression.terms : [];
this[$evaluatables] =
intrinsics.basis.map((basisNode, index) => {
const term = terms[index];
if (term == null) {
return { type: 'ident', value: 'auto' };
}
if (term.type === 'ident') {
return term;
}
return Evaluator.evaluatableFor(term, basisNode);
});
}
get isConstant() {
for (const evaluatable of this[$evaluatables]) {
if (!Evaluator.isConstant(evaluatable)) {
return false;
}
}
return true;
}
[$evaluate]() {
const evaluated = this[$evaluatables].map(evaluatable => Evaluator.evaluate(evaluatable));
return Evaluator.applyIntrinsics(evaluated, this[$intrinsics])
.map(numberNode$$1 => numberNode$$1.number);
}
}
/* @license
* Copyright 2019 Google LLC. All Rights Reserved.
* 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
*
* http://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.
*/
var _a$1, _b$1, _c$1, _d;
const $instances = Symbol('instances');
const $activateListener = Symbol('activateListener');
const $deactivateListener = Symbol('deactivateListener');
const $notifyInstances = Symbol('notifyInstances');
const $notify = Symbol('notify');
const $scrollCallback = Symbol('callback');
/**
* This internal helper is intended to work as a reference-counting manager of
* scroll event listeners. Only one scroll listener is ever registered for all
* instances of the class, and when the last ScrollObserver "disconnects", that
* event listener is removed. This spares us from thrashing
* the {add,remove}EventListener API (the binding cost of these methods has been
* known to show up in performance anlyses) as well as potential memory leaks.
*/
class ScrollObserver {
constructor(callback) {
this[$scrollCallback] = callback;
}
static [$notifyInstances]() {
for (const instance of ScrollObserver[$instances]) {
instance[$notify]();
}
}
static [(_a$1 = $instances, $activateListener)]() {
window.addEventListener('scroll', this[$notifyInstances], { passive: true });
}
static [$deactivateListener]() {
window.removeEventListener('scroll', this[$notifyInstances]);
}
/**
* Listen for scroll events. The configured callback (passed to the
* constructor) will be invoked for subsequent global scroll events.
*/
observe() {
if (ScrollObserver[$instances].size === 0) {
ScrollObserver[$activateListener]();
}
ScrollObserver[$instances].add(this);
}
/**
* Stop listening for scroll events.
*/
disconnect() {
ScrollObserver[$instances].delete(this);
if (ScrollObserver[$instances].size === 0) {
ScrollObserver[$deactivateListener]();
}
}
[$notify]() {
this[$scrollCallback]();
}
;
}
ScrollObserver[_a$1] = new Set();
const $computeStyleCallback = Symbol('computeStyleCallback');
const $astWalker = Symbol('astWalker');
const $dependencies = Symbol('dependencies');
const $scrollHandler = Symbol('scrollHandler');
const $onScroll = Symbol('onScroll');
/**
* The StyleEffector is configured with a callback that will be invoked at the
* optimal time that some array of CSS expression ASTs ought to be evaluated.
*
* For example, our CSS-like expression syntax supports usage of the env()
* function to incorporate the current top-level scroll position into a CSS
* expression: env(window-scroll-y).
*
* This "environment variable" will change dynamically as the user scrolls the
* page. If an AST contains such a usage of env(), we would have to evaluate the
* AST on every frame in order to be sure that the computed style stays up to
* date.
*
* The StyleEffector spares us from evaluating the expressions on every frame by
* correlating specific parts of an AST with observers of the external effects
* that they refer to (if any). So, if the AST contains env(window-scroll-y),
* the StyleEffector manages the lifetime of a global scroll event listener and
* notifies the user at the optimal time to evaluate the computed style.
*/
class StyleEffector {
constructor(callback) {
this[_b$1] = {};
this[_c$1] = new ASTWalker(['function']);
this[_d] = () => this[$onScroll]();
this[$computeStyleCallback] = callback;
}
/**
* Sets the expressions that govern when the StyleEffector callback will be
* invoked.
*/
observeEffectsFor(ast) {
const newDependencies = {};
const oldDependencies = this[$dependencies];
this[$astWalker].walk(ast, functionNode => {
const { name } = functionNode;
const firstArgument = functionNode.arguments[0];
const firstTerm = firstArgument.terms[0];
if (name.value !== 'env' || firstTerm == null ||
firstTerm.type !== 'ident') {
return;
}
switch (firstTerm.value) {
case 'window-scroll-y':
if (newDependencies['window-scroll'] == null) {
const observer = 'window-scroll' in oldDependencies ?
oldDependencies['window-scroll'] :
new ScrollObserver(this[$scrollHandler]);
observer.observe();
delete oldDependencies['window-scroll'];
newDependencies['window-scroll'] = observer;
}
break;
}
});
for (const environmentState in oldDependencies) {
const observer = oldDependencies[environmentState];
observer.disconnect();
}
this[$dependencies] = newDependencies;
}
/**
* Disposes of the StyleEffector by disconnecting all observers of external
* effects.
*/
dispose() {
for (const environmentState in this[$dependencies]) {
const observer = this[$dependencies][environmentState];
observer.disconnect();
}
}
[(_b$1 = $dependencies, _c$1 = $astWalker, _d = $scrollHandler, $onScroll)]() {
this[$computeStyleCallback]({ relatedState: 'window-scroll' });
}
}
/* @license
* Copyright 2019 Google LLC. All Rights Reserved.
* 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
*
* http://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.
*/
/**
* The @style decorator is responsible for coordinating the conversion of a
* CSS-like string property value into numbers that can be applied to
* lower-level constructs. It also can optionally manage the lifecycle of a
* StyleEffector which allows automatic updates for styles that use env() or
* var() functions.
*
* The decorator is configured with Intrinsics and the property key for a
* method that handles updates. The named update handler is invoked with the
* result of parsing and evaluating the raw property string value. The format of
* the evaluated result is derived from the basis of the configured Intrinsics,
* and is always an array of numbers of fixed length.
*
* NOTE: This decorator depends on the property updating mechanism defined by
* UpdatingElement as exported by the lit-element module. That means it *must*
* be used in conjunction with the @property decorator, or equivalent
* JavaScript.
*
* Supported configurations are:
*
* - `intrinsics`: An Intrinsics struct that describes how to interpret a
* serialized style attribute. For more detail on intrinsics see
* ./styles/evaluators.ts
* - `updateHandler`: A string or Symbol that is the key of a method to be
* invoked with the result of parsing and evaluating a serialized style string.
* - `observeEffects`: Optional, if set to true then styles that use env() will
* cause their update handlers to be invoked every time the corresponding
* environment variable changes (even if the style attribute itself remains
* static).
*/
const style = (config) => {
const observeEffects = config.observeEffects || false;
const getIntrinsics = config.intrinsics instanceof Function ?
config.intrinsics :
(() => config.intrinsics);
return (proto, propertyName) => {
const originalUpdated = proto.updated;
const originalConnectedCallback = proto.connectedCallback;
const originalDisconnectedCallback = proto.disconnectedCallback;
const $styleEffector = Symbol(`${propertyName}StyleEffector`);
const $styleEvaluator = Symbol(`${propertyName}StyleEvaluator`);
const $updateEvaluator = Symbol(`${propertyName}UpdateEvaluator`);
const $evaluateAndSync = Symbol(`${propertyName}EvaluateAndSync`);
Object.defineProperties(proto, {
[$styleEffector]: { value: null, writable: true },
[$styleEvaluator]: { value: null, writable: true },
[$updateEvaluator]: {
value: function () {
const ast = parseExpressions(this[propertyName]);
this[$styleEvaluator] =
new StyleEvaluator(ast, getIntrinsics(this));
if (this[$styleEffector] == null && observeEffects) {
this[$styleEffector] =
new StyleEffector(() => this[$evaluateAndSync]());
}
if (this[$styleEffector] != null) {
this[$styleEffector].observeEffectsFor(ast);
}
}
},
[$evaluateAndSync]: {
value: function () {
if (this[$styleEvaluator] == null) {
return;
}
const result = this[$styleEvaluator].evaluate();
// @see https://github.com/microsoft/TypeScript/pull/30769
// @see https://github.com/Microsoft/TypeScript/issues/1863
this[config.updateHandler](result);
}
},
updated: {
value: function (changedProperties) {
// Always invoke updates to styles first. This gives a class that
// uses this decorator the opportunity to override the effect, or
// respond to it, in its own implementation of `updated`.
if (changedProperties.has(propertyName)) {
this[$updateEvaluator]();
this[$evaluateAndSync]();
}
originalUpdated.call(this, changedProperties);
}
},
connectedCallback: {
value: function () {
originalConnectedCallback.call(this);
this.requestUpdate(propertyName, this[propertyName]);
}
},
disconnectedCallback: {
value: function () {
originalDisconnectedCallback.call(this);
if (this[$styleEffector] != null) {
this[$styleEffector].dispose();
this[$styleEffector] = null;
}
}
}
});
};
};
export { style, numberNode, parseExpressions, ASTWalker, ZERO, Evaluator, PercentageEvaluator, EnvEvaluator, CalcEvaluator, OperatorEvaluator, StyleEvaluator, degreesToRadians, radiansToDegrees, lengthToBaseMeters, normalizeUnit };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment