UNPKG

@google/model-viewer

Version:

Easily display interactive 3D models on the web and in AR!

237 lines 9.38 kB
/* @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. */ export 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/ */ export 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. */ export 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; } } } } export const ZERO = Object.freeze({ type: 'number', number: 0, unit: null }); //# sourceMappingURL=parsers.js.map