UNPKG

joicomponents

Version:

Design patterns to build native web components

291 lines (263 loc) 9.24 kB
//should we add "/gi" ? /* needs to be split up into a series of value tokens and comma tokens. Then, this big list will be split up into a set of smaller lists on the comma tokens. So, we can get a flat list, or a list of comma separated lists. This is simplest to always return as a list of comma separated lists, kinda. Then, to get the value of each token, we have to access it as either a primitive word, a number value, or a function. (The #color is a shortcut for an rgb function, but this is simple and not very relevant.) The real problem is parsing the CSS function. The function consists of a function name, then a "(" and then a list of comma separated function expressions, and then ")". what can be a function name? what can be a function expression? the function expression can be either a: 1. quoted string 2. another function expression 3. primitive word, or number value 4. calc expression (boolean expression): 10px + 20%vh width >= 200px */ /** CSS Value token list: ******************** 0. <space> \s+ 1. <word> [-]*[a-zA-Z_][a-zA-Z_0-9-]* "-", "--", "---", etc. and "---3d" are not legal css variable names; "--_" and "---d3" are legal 2. <number> [+-]?(\d+.\d+|.\d+|\d+)(e[+-]?\d+)? 4. <operator> >=|<=|==|[#\(\),<>/*+%-] "==", "<=", ">=", "#", "+", "-", "*", "/", ">", "<", "%", "(", ",", ")" 5. <singlequote> '((\\\\|\\'|[^'\n])*)' 6. <doublequote> "((\\\\|\\"|[^"\n])*)" 7. <hashcolor> #[0-9a-fA-F]+ 8. any other finding would be an error .+ * @type {RegExp} */ const tokenizer = /(\s+)|([+-]?\d*\.?\d+(e[+-]?\d+)?)|([-]*[a-zA-Z_][a-zA-Z_0-9-]*)|#([0-9a-fA-F]+)|(>=|<=|==|[(),/<>+*%-])|'((\\\\|\\'|[^'\n])*)'|"((\\\\|\\"|[^"\n])*)"|(.+)/g; export class CssValueTokenizer { constructor(str) { tokenizer.lastIndex = 0; this._input = str.trim(); this._next = undefined; this._nextNext = undefined; this._active = true; } hasNext() { return this._active; } _nextToken() { if (!this._active) return null; const token = tokenizer.exec(this._input); if (token === null) { this._active = false; return null; } if (token[11]) throwSyntaxError("Illegal token: ", token); return token; } next() { if (this._next === undefined) return this._nextToken(); let n = this._next; this._next = this._nextNext; this._nextNext = undefined; return n; } lookAhead() { if (this._next === undefined) this._next = this._nextToken(); return this._next; } lookAheadAhead() { if (this._nextNext === undefined) { this.lookAhead(); this._nextNext = this._nextToken() } return this._nextNext; } } function throwSyntaxError(msg, token) { throw new SyntaxError(msg + "\n " + token.input + "\n " + new Array(token.index + 1).join(" ") + "^"); } /** * CSS Value BNF: ************* CssValue ::= ValueList ("," ValueList)* ValueList ::= <space>? Value (<space> Value)* <space>? //if the next thing is a ",", return, "error"=> error, the rest is a value Value ::= Function | Primitive Function ::= <word> "(" <space>? ExpressionList <space>? ")" ExpressionList ::= Expression (<space>* "," <space>* Expression)* //if the next thing is a ",", return, "error"=> error, the rest is a value Expression ::= Operation | Value Operation ::= Value <space> <operator> <space> Expression Primitive ::= <word> | NumberUnit | <hashColor> | <quote> NumberUnit ::= <number> ("%" | <word>)? */ export function parseCssValue(str) { const tokens = new CssValueTokenizer(str); let result = []; while (tokens.hasNext()) { result.push(parseSpaceSeparatedValueList(tokens)); if (!tokens.hasNext()) return result; let next = tokens.next(); if (next && next[0] !== ",") throw new SyntaxError("not a list of values nor a comma, but: " + next[0]); } return result; } function parseSpaceSeparatedValueList(tokens) { let result = []; for (let next = tokens.lookAhead(); next; next = tokens.lookAhead()) { if (next && next[1] /*isSpace*/) { tokens.next(); continue; } if (next[0] === ",") return result; result.push(parseValue(tokens)); } return result; } function parseValue(tokens) { const lookAheadAhead = tokens.lookAheadAhead(); if (lookAheadAhead && lookAheadAhead[0] === "(") { const name = tokens.next()[0]; tokens.next(); //skip the "(" const children = parseCssExpressionList(tokens); return {type: "function", name, children}; } return parsePrimitive(tokens); } function parseCssExpressionList(tokens) { let result = []; for (let next = tokens.lookAhead(); next; next = tokens.lookAhead()) { if (next[1]) //isSpace tokens.next(); else { result.push(parseExpression(tokens)); next = tokens.next(); if (next[1]) //isSpace next = tokens.next(); if (next[0] === ",") continue; if (next[0] === ")") return result; throwSyntaxError("Illegal CSS value expression list:", next); } } } function parseExpression(tokens) { const left = parseValue(tokens); const operator = getOperator(tokens); if (operator) { return { type: "operation", left, operator, right: parseExpression(tokens) }; } return left; } function getOperator(tokens) { const space = tokens.lookAhead(); if (!space || !space[1]) //next is not space return undefined; tokens.next(); //skip space const op = tokens.lookAhead(); if (!op || !op[6]) //nextNext is not an operator return undefined; const operator = tokens.next()[0]; const space2 = tokens.next(); if (!space2[1]) //there is no space after the operator throwSyntaxError("Css value operator must be surrounded by space:", space2); return operator; } function parsePrimitive(tokens) { const next = tokens.next(); if (next[5]) { const l =next[5].length; if (l === 3 || l === 4 || l === 6 || l === 8) //todo loosen the parser here a bit? return {type: "#", value: next[5]}; throwSyntaxError("Illegal #color: ", next); } if (next[11]) throwSyntaxError("Illegal Primitive: ", next); if (next[4] /*isWord*/) return {type: "word", value: next[0]}; if (next[7] /*isSingleQuote*/) return {type: "quote", value: next[0], text: next[7]}; if (next[9] /*isDoubleQuote*/) return {type: "quote", value: next[0], text: next[9]}; if (next[2] /*isNumber*/) { let lookAhead = tokens.lookAhead(); if (lookAhead && (lookAhead[4] /*isWord*/ || lookAhead[0] === "%")) return {type: "number", unit: tokens.next()[0], value: next[0]}; return {type: "number", value: next[0]}; } throwSyntaxError("Illegal CSS primitive value: ", next); } /** * * interpret CSS value functions * **/ export function interpretCssValue(value) { function hsl(name, args) { return convertHslToRgb(args); } function colorcalc(name, args) { //identify the operator in the args, and then interpret it } function calc(name, args) { let printOfTheOperatorArgs = "do + 12px - of + all + args"; return "calc(" +printOfTheOperatorArgs+ ")"; } function hash(name, str) { if (str.length === 3) return [parseInt(str[0], 16) * 16, parseInt(str[1], 16) * 16, parseInt(str[2], 16) * 16]; if (str.length === 6) return [parseInt(str[0] + str[1], 16), parseInt(str[2] + str[3], 16), parseInt(str[4] + str[5], 16)]; let printOfTheOperatorArgs = "do + 12px - of + all + args"; return "calc(" +printOfTheOperatorArgs+ ")"; } function __defaultInterpretation(name, args) { return name + "(" + args.join(",") + ")"; } if (value.type === "#"){ } if (value.type === "function") { let args = value.children.map(child => interpretCssValue(child)); let name = value.name === "#" ? "hash" : value.name; let fn = this[value.name]; if (!fn instanceof Function) fn = this[__defaultInterpretation]; return fn(name, args); } if (value.type === "operator") return value; if (value.type === "number") return value.value + (value.unit || ""); if (value.type === "word") return getColorWordValue(value.value) || value.value; return value.value; } export function getRgbValue(_obj) { if (_obj.type === "function" && _obj.unit === "rgb") return _obj.children.map(number => parseInt(number.value)); if (_obj.type === "#") { const str = _obj.value; if (str.length === 3) return [parseInt(str[0], 16) * 16, parseInt(str[1], 16) * 16, parseInt(str[2], 16) * 16]; if (str.length === 6) return [parseInt(str[0] + str[1], 16), parseInt(str[2] + str[3], 16), parseInt(str[4] + str[5], 16)]; } if (_obj.type === "function" && _obj.unit === "hsl") return convertHslToRgb(_obj); if (_obj.type === "function" && _obj.unit === "colorcalc") { return interpretColorCalc(_obj); } return undefined; }