@google/model-viewer
Version: 
Easily display interactive 3D models on the web and in AR!
237 lines • 9.38 kB
JavaScript
/* @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