/* Copyright © 2018 Andrew Powell This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. The above copyright notice and this permission notice shall be included in all copies or substantial portions of this Source Code Form. */ const { getTokens } = require('../tokenize'); const { registerWalker } = require('../walker'); const Container = require('./Container'); const Punctuation = require('./Punctuation'); const cssFunctions = [ 'annotation', 'attr', 'blur', 'brightness', 'calc', 'character-variant', 'circle', 'contrast', 'cubic-bezier', 'dir', 'drop-shadow', 'element', 'ellipse', 'grayscale', 'hsl', 'hsla', 'hue-rotate', 'image', 'inset', 'invert', 'lang', 'linear-gradient', 'matrix', 'matrix3d', 'minmax', 'not', 'nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type', 'opacity', 'ornaments', 'perspective', 'polygon', 'radial-gradient', 'rect', 'repeat', 'repeating-linear-gradient', 'repeating-radial-gradient', 'rgb', 'rgba', 'rotate', 'rotatex', 'rotatey', 'rotatez', 'rotate3d', 'saturate', 'scale', 'scalex', 'scaley', 'scalez', 'scale3d', 'sepia', 'skew', 'skewx', 'skewy', 'steps', 'styleset', 'stylistic', 'swash', 'symbols', 'translate', 'translatex', 'translatey', 'translatez', 'translate3d', 'url', 'var' ]; const vendorPrefixes = ['-webkit-', '-moz-', '-ms-', '-o-']; const reFunctions = new RegExp(`^(${vendorPrefixes.join('|')})?(${cssFunctions.join('|')})`, 'i'); const rePunctuation = new RegExp(`^(\\${Punctuation.chars.join('|\\')})`); const reColorFunctions = /^(hsla?|hwb|lab|lch|rgba?)$/i; const reVar = /^var$/i; const reVarPrefix = /^--[^\s]+$/; class Func extends Container { constructor(options = {}) { super(options); this.type = 'func'; this.isColor = false; this.isVar = false; this.name = options.name || ''; if (!this.nodes) { this.nodes = []; } } static test(tokens) { return ( tokens.length > 1 && tokens[0][0] === 'word' && // fixes #91 !rePunctuation.test(tokens[0][1]) && (tokens[1][0] === 'brackets' || tokens[1][0] === '(') ); } static fromTokens(tokens, parser) { const [[, , startLine, startChar]] = tokens; const [name, brackets] = tokens.splice(0, 2); const node = new Func({ name: name[1] }); let foundParens = 0; let expectedParens = 1; let lastToken = brackets; // fixes #92 // eslint-disable-next-line no-useless-escape if (!reFunctions.test(node.name) && !/^[a-zA-Z\-\.]+$/.test(node.name)) { const nameTokens = getTokens(node.name); tokens.unshift(...nameTokens, brackets); parser.back(tokens); return; } parser.init(node, startLine, startChar); parser.current = node; // eslint-disable-line no-param-reassign if (brackets[0] === 'brackets') { expectedParens = brackets[1].match(/[(]/g).length - 1; } const rightTokens = []; // the number of closing parens we should expect, minus one for the closing paren of brackets for (const token of tokens) { if (foundParens < expectedParens) { if (token[1] === ')') { foundParens += 1; } else if (token[1] === '(') { expectedParens += 1; } brackets[1] += token[1]; lastToken = token; } else { rightTokens.push(token); } } if (foundParens !== expectedParens) { parser.unclosedBracket(brackets); } [, node.params] = brackets; const params = brackets[1].slice(1, -1); if (params.length) { let opts = parser.options; if (node.name === 'url') { // any unknown words are likely part of a url. let the consumer scrutinize the result opts = Object.assign({}, parser.options, { ignoreUnknownWords: true }); } opts.parentNode = node; // use a new parser to parse the params of the function. recursion here makes for easier maint // we must require this here due to circular dependency resolution // eslint-disable-next-line global-require const { parse } = require('../'); const root = parse(params, opts); const { nodes: children } = root; // TODO: correct line and character position (should we just pad the input? probably easiest) for (const child of children) { node.push(child); } if (root.raws.after) { node.last.raws.after = root.raws.after; } } parser.end(lastToken); parser.back(rightTokens); const { lastNode } = parser; const { nodes } = node; lastNode.isColor = reColorFunctions.test(lastNode.name); lastNode.isVar = reVar.test(lastNode.name) && nodes.length && reVarPrefix.test(nodes[0].value); } } registerWalker(Func); module.exports = Func;