From 023f4504e888ac1d908394358cd6219812d67018 Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Wed, 1 Jan 2025 02:29:19 -0500 Subject: [PATCH 1/2] implement math functions #49 --- .github/workflows/node.yml | 2 +- CHANGELOG.md | 4 + README.md | 3 +- dist/index-umd-web.js | 602 +++++++++++++++++--- dist/index.cjs | 602 +++++++++++++++++--- dist/index.d.ts | 19 +- dist/lib/ast/features/calc.js | 90 ++- dist/lib/ast/features/inlinecssvariables.js | 4 +- dist/lib/ast/features/prefix.js | 5 - dist/lib/ast/features/shorthand.js | 1 + dist/lib/ast/math/expression.js | 315 +++++++++- dist/lib/ast/math/math.js | 8 +- dist/lib/ast/minify.js | 1 + dist/lib/ast/walk.js | 91 ++- dist/lib/parser/declaration/list.js | 1 + dist/lib/parser/declaration/map.js | 1 + dist/lib/parser/declaration/set.js | 1 + dist/lib/parser/parse.js | 13 +- dist/lib/parser/tokenize.js | 1 + dist/lib/parser/utils/type.js | 11 +- dist/lib/renderer/color/a98rgb.js | 1 + dist/lib/renderer/color/color.js | 1 + dist/lib/renderer/color/colormix.js | 5 +- dist/lib/renderer/color/hex.js | 1 + dist/lib/renderer/color/hsl.js | 1 + dist/lib/renderer/color/hwb.js | 1 + dist/lib/renderer/color/lab.js | 1 + dist/lib/renderer/color/lch.js | 1 + dist/lib/renderer/color/oklab.js | 1 + dist/lib/renderer/color/oklch.js | 1 + dist/lib/renderer/color/p3.js | 1 + dist/lib/renderer/color/rec2020.js | 1 + dist/lib/renderer/color/relativecolor.js | 79 ++- dist/lib/renderer/color/rgb.js | 1 + dist/lib/renderer/color/srgb.js | 1 + dist/lib/renderer/color/utils/components.js | 1 + dist/lib/renderer/color/utils/constants.js | 5 +- dist/lib/renderer/color/xyz.js | 1 + dist/lib/renderer/color/xyzd50.js | 1 + dist/lib/renderer/render.js | 15 +- dist/lib/syntax/syntax.js | 7 +- dist/lib/validation/parser/parse.js | 1 + dist/lib/validation/selector.js | 1 + jsr.json | 2 +- package.json | 26 +- src/@types/index.d.ts | 1 + src/@types/walker.d.ts | 8 +- src/lib/ast/features/calc.ts | 118 +++- src/lib/ast/features/inlinecssvariables.ts | 6 +- src/lib/ast/features/prefix.ts | 7 - src/lib/ast/math/expression.ts | 454 ++++++++++++++- src/lib/ast/math/math.ts | 10 + src/lib/ast/walk.ts | 120 +++- src/lib/parser/parse.ts | 16 +- src/lib/parser/utils/type.ts | 9 +- src/lib/renderer/color/relativecolor.ts | 104 ++-- src/lib/renderer/color/utils/constants.ts | 2 + src/lib/renderer/render.ts | 17 +- src/lib/syntax/syntax.ts | 5 +- test/specs/code/calc.js | 291 +++++++++- test/specs/code/color.js | 57 +- test/specs/code/vars.js | 12 +- 62 files changed, 2708 insertions(+), 461 deletions(-) diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 5279784f..4a9d208d 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [ 18.x, 20.x ] + node-version: [ 18.x, 20.x, 22.x ] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fdcf2c8..121f8ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +# v0.8.0 + +- [x] evaluate math functions: calc, clamp, min, max, round, mod, rem, sin, cos, tan, asin, acos, atan, atan2, pow, sqrt, hypot, log, exp, abs, sign #49 + # v0.7.1 - [x] fix nesting rules expansion #45 diff --git a/README.md b/README.md index b9ac06ac..c6095b12 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ $ deno add @tbela99/css-parser - convert nested css rules to legacy syntax - generate sourcemap - compute css shorthands. see supported properties list below -- evaluate calc() +- evaluate math functions: calc(), clamp(), min(), max(), round(), mod(), rem(), sin(), cos(), tan(), asin(), acos(), atan(), atan2(), pow(), sqrt(), hypot(), log(), exp(), abs(), sign #49 - inline css variables - remove duplicate properties - flatten @import rules @@ -177,6 +177,7 @@ Include ParseOptions and RenderOptions - minify: boolean, optional. default to _true_. minify css output. - withParents: boolean, optional. render this node and its parents. +- removeEmpty: boolean, optional. remove empty rule lists from the ast. - expandNestingRules: boolean, optional. expand nesting rules. - preserveLicense: boolean, force preserving comments starting with '/\*!' when minify is enabled. - removeComments: boolean, remove comments in generated css. diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index 6c9e83b8..864798b8 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -194,6 +194,8 @@ b: [0, 0.4] } }; + // https://www.w3.org/TR/css-values-4/#math-function + const mathFuncs = ['calc', 'clamp', 'min', 'max', 'round', 'mod', 'rem', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'pow', 'sqrt', 'hypot', 'log', 'exp', 'abs', 'sign']; const colorFuncColorSpace = ['srgb', 'srgb-linear', 'display-p3', 'prophoto-rgb', 'a98-rgb', 'rec2020', 'xyz', 'xyz-d65', 'xyz-d50']; ({ typ: exports.EnumToken.IdenTokenType, val: 'none' }); const D50 = [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585]; @@ -2520,6 +2522,12 @@ r: { typ: exports.EnumToken.NumberTokenType, val: reduceNumber(a2[1]) } }; } + function rem(...a) { + if (a.some((i) => !Number.isInteger(i))) { + return a.reduce((a, b) => Math.max(a, String(b).split('.')[1]?.length ?? 0), 0); + } + return 0; + } function simplify(a, b) { const g = gcd(a, b); return g > 1 ? [a / g, b / g] : [a, b]; @@ -2531,15 +2539,45 @@ */ function evaluate(tokens) { let nodes; + if (tokens.length == 1 && tokens[0].typ == exports.EnumToken.FunctionTokenType && tokens[0].val != 'calc' && mathFuncs.includes(tokens[0].val)) { + const chi = tokens[0].chi.reduce((acc, t) => { + if (acc.length == 0 || t.typ == exports.EnumToken.CommaTokenType) { + acc.push([]); + } + if ([exports.EnumToken.WhitespaceTokenType, exports.EnumToken.CommaTokenType, exports.EnumToken.CommaTokenType].includes(t.typ)) { + return acc; + } + acc.at(-1).push(t); + return acc; + }, []); + for (let i = 0; i < chi.length; i++) { + chi[i] = evaluate(chi[i]); + } + tokens[0].chi = chi.reduce((acc, t) => { + if (acc.length > 0) { + acc.push({ typ: exports.EnumToken.CommaTokenType }); + } + acc.push(...t); + return acc; + }); + return evaluateFunc(tokens[0]); + } try { nodes = inlineExpression(evaluateExpression(buildExpression(tokens))); } catch (e) { - // console.error({tokens}); - // console.error(e); return tokens; } if (nodes.length <= 1) { + // @ts-ignore + if (nodes.length == 1 && nodes[0].typ == exports.EnumToken.IdenTokenType && typeof Math[nodes[0].val.toUpperCase()] == 'number') { + return [{ + ...nodes[0], + // @ts-ignore + val: '' + Math[nodes[0].val.toUpperCase()], + typ: exports.EnumToken.NumberTokenType + }]; + } return nodes; } const map = new Map; @@ -2594,9 +2632,33 @@ l, r }; - if (!isScalarToken(l) || !isScalarToken(r)) { + if (!isScalarToken(l) || !isScalarToken(r) || (l.typ == r.typ && 'unit' in l && 'unit' in r && l.unit != r.unit)) { return defaultReturn; } + if (l.typ == exports.EnumToken.FunctionTokenType) { + const val = evaluateFunc(l); + if (val.length == 1) { + l = val[0]; + } + else { + return defaultReturn; + } + } + if (r.typ == exports.EnumToken.FunctionTokenType) { + const val = evaluateFunc(r); + if (val.length == 1) { + r = val[0]; + } + else { + return defaultReturn; + } + } + if (l.typ == exports.EnumToken.FunctionTokenType) { + const val = evaluateFunc(l); + if (val.length == 1) { + l = val[0]; + } + } if ((op == exports.EnumToken.Add || op == exports.EnumToken.Sub)) { // @ts-ignore if (l.typ != r.typ) { @@ -2608,11 +2670,13 @@ ![exports.EnumToken.NumberTokenType, exports.EnumToken.PercentageTokenType].includes(r.typ)) { return defaultReturn; } - const typ = l.typ == exports.EnumToken.NumberTokenType ? r.typ : (r.typ == exports.EnumToken.NumberTokenType ? l.typ : (l.typ == exports.EnumToken.PercentageTokenType ? r.typ : l.typ)); - // @ts-ignore - let v1 = typeof l.val == 'string' ? +l.val : l.val; + let typ = l.typ == exports.EnumToken.NumberTokenType ? r.typ : (r.typ == exports.EnumToken.NumberTokenType ? l.typ : (l.typ == exports.EnumToken.PercentageTokenType ? r.typ : l.typ)); // @ts-ignore - let v2 = typeof r.val == 'string' ? +r.val : r.val; + let v1 = getValue$1(l); + let v2 = getValue$1(r); + if (v1 == null || v2 == null) { + return defaultReturn; + } if (op == exports.EnumToken.Mul) { if (l.typ != exports.EnumToken.NumberTokenType && r.typ != exports.EnumToken.NumberTokenType) { if (typeof v1 == 'number' && l.typ == exports.EnumToken.PercentageTokenType) { @@ -2633,11 +2697,239 @@ } // @ts-ignore const val = compute(v1, v2, op); - return { + // typ = typeof val == 'number' ? EnumToken.NumberTokenType : EnumToken.FractionTokenType; + const token = { ...(l.typ == exports.EnumToken.NumberTokenType ? r : l), typ, val: typeof val == 'number' ? reduceNumber(val) : val }; + if (token.typ == exports.EnumToken.IdenTokenType) { + // @ts-ignore + token.typ = exports.EnumToken.NumberTokenType; + } + return token; + } + function getValue$1(t) { + let v1; + if (t.typ == exports.EnumToken.FunctionTokenType) { + v1 = evaluateFunc(t); + if (v1.length != 1 || v1[0].typ == exports.EnumToken.BinaryExpressionTokenType) { + return null; + } + t = v1[0]; + } + if (t.typ == exports.EnumToken.IdenTokenType) { + // @ts-ignore + return Math[t.val.toUpperCase()]; + } + if (t.val.typ == exports.EnumToken.FractionTokenType) { + // @ts-ignore + return t.val.l.val / t.val.r.val; + } + // @ts-ignore + return t.typ == exports.EnumToken.FractionTokenType ? t.l.val / t.r.val : +t.val; + } + function evaluateFunc(token) { + const values = token.chi.slice(); + switch (token.val) { + case 'abs': + case 'sin': + case 'cos': + case 'tan': + case 'asin': + case 'acos': + case 'atan': + case 'sign': + case 'sqrt': + case 'exp': { + const value = evaluate(values); + if (value.length != 1 || (value[0].typ != exports.EnumToken.NumberTokenType && value[0].typ != exports.EnumToken.FractionTokenType) || (value[0].typ == exports.EnumToken.FractionTokenType && (+value[0].r.val == 0 || !Number.isFinite(+value[0].l.val) || !Number.isFinite(+value[0].r.val)))) { + return value; + } + // @ts-ignore + let val = value[0].typ == exports.EnumToken.NumberTokenType ? +value[0].val : value[0].l.val / value[0].r.val; + return [{ + typ: exports.EnumToken.NumberTokenType, + val: '' + Math[token.val](val) + }]; + } + case 'hypot': { + const chi = values.filter(t => ![exports.EnumToken.WhitespaceTokenType, exports.EnumToken.CommentTokenType, exports.EnumToken.CommaTokenType].includes(t.typ)); + let all = []; + let ref = chi[0]; + let value = 0; + if (![exports.EnumToken.NumberTokenType, exports.EnumToken.PercentageTokenType].includes(ref.typ) && !('unit' in ref)) { + return [token]; + } + for (let i = 0; i < chi.length; i++) { + // @ts-ignore + if (chi[i].typ != ref.typ || ('unit' in chi[i] && 'unit' in ref && chi[i].unit != ref.unit)) { + return [token]; + } + // @ts-ignore + const val = getValue$1(chi[i]); + if (val == null) { + return [token]; + } + all.push(val); + value += val * val; + } + return [ + { + ...ref, + val: Math.sqrt(value).toFixed(rem(...all)) + } + ]; + } + case 'atan2': + case 'pow': + case 'rem': + case 'mod': { + const chi = values.filter(t => ![exports.EnumToken.WhitespaceTokenType, exports.EnumToken.CommentTokenType].includes(t.typ)); + if (chi.length != 3 || chi[1].typ != exports.EnumToken.CommaTokenType) { + return [token]; + } + if (token.val == 'pow' && (chi[0].typ != exports.EnumToken.NumberTokenType || chi[2].typ != exports.EnumToken.NumberTokenType)) { + return [token]; + } + if (['rem', 'mod'].includes(token.val) && + (chi[0].typ != chi[2].typ) || ('unit' in chi[0] && 'unit' in chi[2] && + chi[0].unit != chi[2].unit)) { + return [token]; + } + // https://developer.mozilla.org/en-US/docs/Web/CSS/mod + const v1 = evaluate([chi[0]]); + const v2 = evaluate([chi[2]]); + const types = [exports.EnumToken.PercentageTokenType, exports.EnumToken.DimensionTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.NumberTokenType, exports.EnumToken.LengthTokenType, exports.EnumToken.TimeTokenType, exports.EnumToken.FrequencyTokenType, exports.EnumToken.ResolutionTokenType]; + if (v1.length != 1 || v2.length != 1 || !types.includes(v1[0].typ) || !types.includes(v2[0].typ) || v1[0].unit != v2[0].unit) { + return [token]; + } + // @ts-ignore + const val1 = getValue$1(v1[0]); + // @ts-ignore + const val2 = getValue$1(v2[0]); + if (val1 == null || val2 == null || (v1[0].typ != v2[0].typ && val1 != 0 && val2 != 0)) { + return [token]; + } + if (token.val == 'rem') { + if (val2 == 0) { + return [token]; + } + return [ + { + ...v1[0], + val: (val1 % val2).toFixed(rem(val1, val2)) + } + ]; + } + if (token.val == 'pow') { + return [ + { + ...v1[0], + val: String(Math.pow(val1, val2)) + } + ]; + } + if (token.val == 'atan2') { + return [ + { + ...{}, ...v1[0], + val: String(Math.atan2(val1, val2)) + } + ]; + } + return [ + { + ...v1[0], + val: String(val2 == 0 ? val1 : val1 - (Math.floor(val1 / val2) * val2)) + } + ]; + } + case 'clamp': + token.chi = values; + return [token]; + case 'log': + case 'round': + case 'min': + case 'max': + { + const strategy = token.val == 'round' && values[0]?.typ == exports.EnumToken.IdenTokenType ? values.shift().val : null; + const valuesMap = new Map; + for (const curr of values) { + if (curr.typ == exports.EnumToken.CommaTokenType || curr.typ == exports.EnumToken.WhitespaceTokenType || curr.typ == exports.EnumToken.CommentTokenType) { + continue; + } + const result = evaluate([curr]); + if (result.length != 1 || result[0].typ == exports.EnumToken.FunctionTokenType) { + return [token]; + } + const key = result[0].typ + ('unit' in result[0] ? result[0].unit : ''); + if (!valuesMap.has(key)) { + valuesMap.set(key, []); + } + valuesMap.get(key).push(result[0]); + } + if (valuesMap.size == 1) { + const values = valuesMap.values().next().value; + console.error({ values }); + if (token.val == 'log') { + if (values[0].typ != exports.EnumToken.NumberTokenType || values.length > 2) { + return [token]; + } + const val1 = getValue$1(values[0]); + const val2 = values.length == 2 ? getValue$1(values[1]) : null; + if (values.length == 1) { + return [ + { + ...values[0], + val: String(Math.log(val1)) + } + ]; + } + return [ + { + ...values[0], + val: String(Math.log(val1) / Math.log(val2)) + } + ]; + } + if (token.val == 'min' || token.val == 'max') { + let val = getValue$1(values[0]); + let val2 = val; + let ret = values[0]; + for (const curr of values.slice(1)) { + val2 = getValue$1(curr); + if (val2 < val && token.val == 'min') { + val = val2; + ret = curr; + } + else if (val2 > val && token.val == 'max') { + val = val2; + ret = curr; + } + } + return [ret]; + } + if (token.val == 'round') { + let val = getValue$1(values[0]); + let val2 = getValue$1(values[1]); + if (Number.isNaN(val) || Number.isNaN(val2)) { + return [token]; + } + if (strategy == null || strategy == 'down') { + val = val - (val % val2); + } + else { + val = strategy == 'to-zero' ? Math.trunc(val / val2) * val2 : (strategy == 'nearest' ? Math.round(val / val2) * val2 : Math.ceil(val / val2) * val2); + } + // @ts-ignore + return [{ ...values[0], val: String(val) }]; + } + } + } + return [token]; + } + return [token]; } /** * convert BinaryExpression into an array @@ -2678,7 +2970,11 @@ return doEvaluate(token.l, token.r, token.op); } function isScalarToken(token) { - return 'unit' in token || [exports.EnumToken.NumberTokenType, exports.EnumToken.FractionTokenType, exports.EnumToken.PercentageTokenType].includes(token.typ); + return 'unit' in token || + (token.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(token.val)) || + // @ts-ignore + (token.typ == exports.EnumToken.IdenTokenType && typeof Math[token.val.toUpperCase()] == 'number') || + [exports.EnumToken.NumberTokenType, exports.EnumToken.FractionTokenType, exports.EnumToken.PercentageTokenType].includes(token.typ); } /** * @@ -2787,7 +3083,7 @@ val: '1' } : aExp) }; - return computeComponentValue(keys, values); + return computeComponentValue(keys, converted, values); } function getValue(t, converted, component) { if (t == null) { @@ -2806,7 +3102,7 @@ } return t; } - function computeComponentValue(expr, values) { + function computeComponentValue(expr, converted, values) { for (const object of [values, expr]) { if ('h' in object) { // normalize hue @@ -2847,34 +3143,29 @@ expr[key] = values[exp.val]; } } - else if (exp.typ == exports.EnumToken.FunctionTokenType && exp.val == 'calc') { - for (let { value, parent } of walkValues(exp.chi)) { - if (value.typ == exports.EnumToken.IdenTokenType) { - if (!(value.val in values)) { + else if (exp.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(exp.val)) { + for (let { value, parent } of walkValues(exp.chi, exp)) { + if (parent == null) { + parent = exp; + } + if (value.typ == exports.EnumToken.PercentageTokenType) { + replaceValue(parent, value, getValue(value, converted, key)); + } + else if (value.typ == exports.EnumToken.IdenTokenType) { + // @ts-ignore + if (!(value.val in values || typeof Math[value.val.toUpperCase()] == 'number')) { return null; } - if (parent == null) { - parent = exp; - } - if (parent.typ == exports.EnumToken.BinaryExpressionTokenType) { - if (parent.l == value) { - parent.l = values[value.val]; - } - else { - parent.r = values[value.val]; - } - } - else { - for (let i = 0; i < parent.chi.length; i++) { - if (parent.chi[i] == value) { - parent.chi.splice(i, 1, values[value.val]); - break; - } - } - } + // @ts-ignore + replaceValue(parent, value, values[value.val] ?? { + typ: exports.EnumToken.NumberTokenType, + // @ts-ignore + val: '' + Math[value.val.toUpperCase()] + // @ts-ignore + }); } } - const result = evaluate(exp.chi); + const result = exp.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(exp.val) && exp.val != 'calc' ? evaluateFunc(exp) : evaluate(exp.chi); if (result.length == 1 && result[0].typ != exports.EnumToken.BinaryExpressionTokenType) { expr[key] = result[0]; } @@ -2885,6 +3176,34 @@ } return expr; } + function replaceValue(parent, value, newValue) { + if (parent.typ == exports.EnumToken.BinaryExpressionTokenType) { + if (parent.l == value) { + parent.l = newValue; + } + else { + parent.r = newValue; + } + } + else { + for (let i = 0; i < parent.chi.length; i++) { + if (parent.chi[i] == value) { + parent.chi.splice(i, 1, newValue); + break; + } + if (parent.chi[i].typ == exports.EnumToken.BinaryExpressionTokenType) { + if (parent.chi[i].l == value) { + parent.chi[i].l = newValue; + break; + } + else if (parent.chi[i].r == value) { + parent.chi[i].r = newValue; + break; + } + } + } + } + } // from https://github.com/Rich-Harris/vlq/tree/master // credit: Rich Harris @@ -3012,6 +3331,7 @@ ...(options.minify ?? true ? { indent: '', newLine: '', + removeEmpty: true, removeComments: true } : { indent: ' ', @@ -3160,6 +3480,9 @@ } return `${css}${options.newLine}${indentSub}${str}`; }, ''); + if (options.removeEmpty && children === '') { + return ''; + } if (children.endsWith(';')) { children = children.slice(0, -1); } @@ -3170,6 +3493,7 @@ case exports.EnumToken.InvalidRuleTokenType: return ''; default: + // return renderToken(data as Token, options, cache, reducer, errors); throw new Error(`render: unexpected token ${JSON.stringify(data, null, 1)}`); } return ''; @@ -3361,12 +3685,11 @@ case exports.EnumToken.TimelineFunctionTokenType: case exports.EnumToken.GridTemplateFuncTokenType: if (token.typ == exports.EnumToken.FunctionTokenType && - token.val == 'calc' && + mathFuncs.includes(token.val) && token.chi.length == 1 && - token.chi[0].typ != exports.EnumToken.BinaryExpressionTokenType && - token.chi[0].typ != exports.EnumToken.FractionTokenType && + ![exports.EnumToken.BinaryExpressionTokenType, exports.EnumToken.FractionTokenType, exports.EnumToken.IdenTokenType].includes(token.chi[0].typ) && + // @ts-ignore token.chi[0].val?.typ != exports.EnumToken.FractionTokenType) { - // calc(200px) => 200px return token.chi.reduce((acc, curr) => acc + renderToken(curr, options, cache, reducer), ''); } // @ts-ignore @@ -3653,7 +3976,7 @@ return false; } } - if (children[i].typ == exports.EnumToken.FunctionTokenType && !['calc'].includes(children[i].val)) { + if (children[i].typ == exports.EnumToken.FunctionTokenType && !mathFuncs.includes(children[i].val)) { return false; } } @@ -3748,7 +4071,7 @@ } continue; } - if (v.typ == exports.EnumToken.FunctionTokenType && (v.val == 'calc' || v.val == 'var' || colorsFunc.includes(v.val))) { + if (v.typ == exports.EnumToken.FunctionTokenType && (mathFuncs.includes(v.val) || v.val == 'var' || colorsFunc.includes(v.val))) { continue; } if (![exports.EnumToken.ColorTokenType, exports.EnumToken.IdenTokenType, exports.EnumToken.NumberTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.PercentageTokenType, exports.EnumToken.CommaTokenType, exports.EnumToken.WhitespaceTokenType, exports.EnumToken.LiteralTokenType].includes(v.typ)) { @@ -5495,8 +5818,6 @@ Object.freeze(config$3); const getConfig$1 = () => config$3; - // https://www.w3.org/TR/css-values-4/#math-function - const funcList = ['clamp', 'calc']; function matchType(val, properties) { if (val.typ == exports.EnumToken.IdenTokenType && properties.keywords.includes(val.val) || // @ts-ignore @@ -5512,8 +5833,8 @@ }); } if (val.typ == exports.EnumToken.FunctionTokenType) { - if (funcList.includes(val.val)) { - return val.chi.every(((t) => [exports.EnumToken.LiteralTokenType, exports.EnumToken.CommaTokenType, exports.EnumToken.WhitespaceTokenType, exports.EnumToken.StartParensTokenType, exports.EnumToken.EndParensTokenType].includes(t.typ) || matchType(t, properties))); + if (mathFuncs.includes(val.val)) { + return val.chi.every(((t) => [exports.EnumToken.Add, exports.EnumToken.Mul, exports.EnumToken.Div, exports.EnumToken.Sub, exports.EnumToken.LiteralTokenType, exports.EnumToken.CommaTokenType, exports.EnumToken.WhitespaceTokenType, exports.EnumToken.DimensionTokenType, exports.EnumToken.NumberTokenType, exports.EnumToken.LengthTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.PercentageTokenType, exports.EnumToken.ResolutionTokenType, exports.EnumToken.TimeTokenType, exports.EnumToken.BinaryExpressionTokenType].includes(t.typ) || matchType(t, properties))); } // match type defined like function 'symbols()', 'url()', 'attr()' etc. // return properties.types.includes((val).val + '()') @@ -60216,9 +60537,6 @@ })); } function getTokenType(val, hint) { - // if (val === '' && hint == null) { - // throw new Error('empty string?'); - // } if (hint != null) { return enumTokenHints.has(hint) ? { typ: hint } : { typ: hint, val }; } @@ -60396,11 +60714,7 @@ if (t.typ == exports.EnumToken.WhitespaceTokenType && ((i == 0 || i + 1 == tokens.length || [exports.EnumToken.CommaTokenType, exports.EnumToken.GteTokenType, exports.EnumToken.LteTokenType, exports.EnumToken.ColumnCombinatorTokenType].includes(tokens[i + 1].typ)) || - (i > 0 && - // tokens[i + 1]?.typ != Literal || - // funcLike.includes(tokens[i - 1].typ) && - // !['var', 'calc'].includes((tokens[i - 1]).val)))) && - trimWhiteSpace.includes(tokens[i - 1].typ)))) { + (i > 0 && trimWhiteSpace.includes(tokens[i - 1].typ)))) { tokens.splice(i--, 1); continue; } @@ -60593,7 +60907,7 @@ // @ts-ignore parseTokens(t.chi, options); } - if (t.typ == exports.EnumToken.FunctionTokenType && t.val == 'calc') { + if (t.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(t.val)) { for (const { value, parent } of walkValues(t.chi)) { if (value.typ == exports.EnumToken.WhitespaceTokenType) { const p = (parent ?? t); @@ -60749,6 +61063,11 @@ return true; } + var WalkerValueEvent; + (function (WalkerValueEvent) { + WalkerValueEvent[WalkerValueEvent["Enter"] = 0] = "Enter"; + WalkerValueEvent[WalkerValueEvent["Leave"] = 1] = "Leave"; + })(WalkerValueEvent || (WalkerValueEvent = {})); function* walk(node, filter) { const parents = [node]; const root = node; @@ -60778,37 +61097,89 @@ } } function* walkValues(values, root = null, filter) { + // const set = new Set(); const stack = values.slice(); const map = new Map; - let value; let previous = null; - while ((value = stack.shift())) { + // let parent: FunctionToken | ParensToken | BinaryExpressionToken | null = null; + if (filter != null && typeof filter == 'function') { + filter = { + event: WalkerValueEvent.Enter, + fn: filter + }; + } + else if (filter == null) { + filter = { + event: WalkerValueEvent.Enter + }; + } + while (stack.length > 0) { + let value = stack.shift(); let option = null; - if (filter != null) { - option = filter(value); - if (option === 'ignore') { - continue; - } - if (option === 'stop') { - break; + if (filter.fn != null && filter.event == WalkerValueEvent.Enter) { + const isValid = filter.type == null || value.typ == filter.type || + (Array.isArray(filter.type) && filter.type.includes(value.typ)) || + (typeof filter.type == 'function' && filter.type(value)); + if (isValid) { + option = filter.fn(value, map.get(value) ?? root, WalkerValueEvent.Enter); + if (option === 'ignore') { + continue; + } + if (option === 'stop') { + break; + } + // @ts-ignore + if (option != null && typeof option == 'object' && 'typ' in option) { + map.set(option, map.get(value) ?? root); + } } } // @ts-ignore - if (option !== 'children') { - // @ts-ignore - yield { value, parent: map.get(value), previousValue: previous, nextValue: stack[0] ?? null, root }; + if (filter.event == WalkerValueEvent.Enter && option !== 'children') { + yield { + value, + parent: map.get(value) ?? root, + previousValue: previous, + nextValue: stack[0] ?? null, + // @ts-ignore + root: root ?? null + }; } if (option !== 'ignore-children' && 'chi' in value) { - for (const child of value.chi.slice()) { + const sliced = value.chi.slice(); + for (const child of sliced) { map.set(child, value); } - stack.unshift(...value.chi); + stack.unshift(...sliced); } else if (value.typ == exports.EnumToken.BinaryExpressionTokenType) { - map.set(value.l, value); - map.set(value.r, value); + map.set(value.l, map.get(value) ?? root); + map.set(value.r, map.get(value) ?? root); stack.unshift(value.l, value.r); } + if (filter.event == WalkerValueEvent.Leave && filter.fn != null) { + const isValid = filter.type == null || value.typ == filter.type || + (Array.isArray(filter.type) && filter.type.includes(value.typ)) || + (typeof filter.type == 'function' && filter.type(value)); + if (isValid) { + option = filter.fn(value, map.get(value), WalkerValueEvent.Leave); + // @ts-ignore + if (option != null && 'typ' in option) { + map.set(option, map.get(value) ?? root); + } + } + } + // @ts-ignore + if (filter.event == WalkerValueEvent.Leave && option !== 'children') { + yield { + value, + parent: map.get(value) ?? root, + previousValue: previous, + nextValue: stack[0] ?? null, + // @ts-ignore + root: root ?? null + }; + } previous = value; } } @@ -61140,7 +61511,6 @@ case ValidationTokenEnum.Comma: break; case ValidationTokenEnum.Keyword: - console.error(matches[i], token); if (token.typ == exports.EnumToken.IdenTokenType && token.val == matches[i].val) { return token; } @@ -61179,10 +61549,6 @@ return result; } break; - // default: - // - // console.error(token, matches[i]); - // throw new Error('bar bar'); } } return null; @@ -61295,7 +61661,8 @@ i = parent.chi?.length ?? 0; while (i--) { if (parent.chi[i].typ == exports.EnumToken.DeclarationNodeType && parent.chi[i].nam == info.node.nam) { - parent.chi.splice(i, 1); + // @ts-ignore + parent.chi.splice(i++, 1, { typ: exports.EnumToken.CommentTokenType, val: `/* ${info.node.nam}: ${info.node.val.reduce((acc, curr) => acc + renderToken(curr), '')} */` }); } } if (parent.chi?.length == 0 && 'parent' in parent) { @@ -62273,30 +62640,91 @@ continue; } const set = new Set; - for (const { value, parent } of walkValues(node.val)) { - if (value != null && value.typ == exports.EnumToken.FunctionTokenType && value.val == 'calc') { - if (!set.has(parent)) { + for (const { value, parent } of walkValues(node.val, node, { + event: WalkerValueEvent.Enter, + fn(node, parent, event) { + if (parent != null && + parent.typ == exports.EnumToken.DeclarationNodeType && + parent.val.length == 1 && + node.typ == exports.EnumToken.FunctionTokenType && + mathFuncs.includes(node.val) && + node.chi.length == 1 && + node.chi[0].typ == exports.EnumToken.IdenTokenType) { + return 'ignore'; + } + if ((node.typ == exports.EnumToken.FunctionTokenType && node.val == 'var') || (!mathFuncs.includes(parent.val) && [exports.EnumToken.ColorTokenType, exports.EnumToken.DeclarationNodeType, exports.EnumToken.RuleNodeType, exports.EnumToken.AtRuleNodeType, exports.EnumToken.StyleSheetNodeType].includes(parent?.typ))) { + return null; + } + const slice = (node.typ == exports.EnumToken.FunctionTokenType ? node.chi : (node.typ == exports.EnumToken.DeclarationNodeType ? node.val : node.chi))?.slice(); + if (slice != null && node.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(node.val)) { + // @ts-ignore + const cp = (node.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(node.val) && node.val != 'calc' ? [node] : (node.typ == exports.EnumToken.DeclarationNodeType ? node.val : node.chi)).slice(); + const values = evaluate(cp); + const key = 'chi' in node ? 'chi' : 'val'; + const str1 = renderToken({ ...node, [key]: slice }); + const str2 = renderToken(node); // values.reduce((acc: string, curr: Token): string => acc + renderToken(curr), ''); + if (str1.length <= str2.length) { + // @ts-ignore + node[key] = slice; + } + else { + // @ts-ignore + node[key] = values; + } + return 'ignore'; + } + return null; + } + })) { + if (value != null && value.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(value.val)) { + if (!set.has(value)) { set.add(value); - value.chi = evaluate(value.chi); - if (value.chi.length == 1 && value.chi[0].typ != exports.EnumToken.BinaryExpressionTokenType) { - if (parent != null) { + if (parent != null) { + // @ts-ignore + const cp = value.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(value.val) && value.val != 'calc' ? [value] : (value.typ == exports.EnumToken.DeclarationNodeType ? value.val : value.chi); + const values = evaluate(cp); + // @ts-ignore + const children = parent.typ == exports.EnumToken.DeclarationNodeType ? parent.val : parent.chi; + if (values.length == 1 && values[0].typ != exports.EnumToken.BinaryExpressionTokenType) { if (parent.typ == exports.EnumToken.BinaryExpressionTokenType) { if (parent.l == value) { - parent.l = value.chi[0]; + parent.l = values[0]; } else { - parent.r = value.chi[0]; + parent.r = values[0]; } } else { - for (let i = 0; i < parent.chi.length; i++) { - if (parent.chi[i] == value) { - parent.chi.splice(i, 1, value.chi[0]); + for (let i = 0; i < children.length; i++) { + if (children[i] == value) { + // @ts-ignore + children.splice(i, 1, !(parent.typ == exports.EnumToken.FunctionTokenType && parent.val == 'calc') && typeof values[0].val != 'string' ? { + typ: exports.EnumToken.FunctionTokenType, + val: 'calc', + chi: values + } : values[0]); break; } } } } + else { + for (let i = 0; i < children.length; i++) { + if (children[i] == value) { + if (parent.typ == exports.EnumToken.FunctionTokenType && parent.val == 'calc') { + children.splice(i, 1, ...values); + } + else { + children.splice(i, 1, { + typ: exports.EnumToken.FunctionTokenType, + val: 'calc', + chi: values + }); + } + break; + } + } + } } } } diff --git a/dist/index.cjs b/dist/index.cjs index 787b2584..16a7cae2 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -193,6 +193,8 @@ const colorRange = { b: [0, 0.4] } }; +// https://www.w3.org/TR/css-values-4/#math-function +const mathFuncs = ['calc', 'clamp', 'min', 'max', 'round', 'mod', 'rem', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'pow', 'sqrt', 'hypot', 'log', 'exp', 'abs', 'sign']; const colorFuncColorSpace = ['srgb', 'srgb-linear', 'display-p3', 'prophoto-rgb', 'a98-rgb', 'rec2020', 'xyz', 'xyz-d65', 'xyz-d50']; ({ typ: exports.EnumToken.IdenTokenType, val: 'none' }); const D50 = [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585]; @@ -2519,6 +2521,12 @@ function compute(a, b, op) { r: { typ: exports.EnumToken.NumberTokenType, val: reduceNumber(a2[1]) } }; } +function rem(...a) { + if (a.some((i) => !Number.isInteger(i))) { + return a.reduce((a, b) => Math.max(a, String(b).split('.')[1]?.length ?? 0), 0); + } + return 0; +} function simplify(a, b) { const g = gcd(a, b); return g > 1 ? [a / g, b / g] : [a, b]; @@ -2530,15 +2538,45 @@ function simplify(a, b) { */ function evaluate(tokens) { let nodes; + if (tokens.length == 1 && tokens[0].typ == exports.EnumToken.FunctionTokenType && tokens[0].val != 'calc' && mathFuncs.includes(tokens[0].val)) { + const chi = tokens[0].chi.reduce((acc, t) => { + if (acc.length == 0 || t.typ == exports.EnumToken.CommaTokenType) { + acc.push([]); + } + if ([exports.EnumToken.WhitespaceTokenType, exports.EnumToken.CommaTokenType, exports.EnumToken.CommaTokenType].includes(t.typ)) { + return acc; + } + acc.at(-1).push(t); + return acc; + }, []); + for (let i = 0; i < chi.length; i++) { + chi[i] = evaluate(chi[i]); + } + tokens[0].chi = chi.reduce((acc, t) => { + if (acc.length > 0) { + acc.push({ typ: exports.EnumToken.CommaTokenType }); + } + acc.push(...t); + return acc; + }); + return evaluateFunc(tokens[0]); + } try { nodes = inlineExpression(evaluateExpression(buildExpression(tokens))); } catch (e) { - // console.error({tokens}); - // console.error(e); return tokens; } if (nodes.length <= 1) { + // @ts-ignore + if (nodes.length == 1 && nodes[0].typ == exports.EnumToken.IdenTokenType && typeof Math[nodes[0].val.toUpperCase()] == 'number') { + return [{ + ...nodes[0], + // @ts-ignore + val: '' + Math[nodes[0].val.toUpperCase()], + typ: exports.EnumToken.NumberTokenType + }]; + } return nodes; } const map = new Map; @@ -2593,9 +2631,33 @@ function doEvaluate(l, r, op) { l, r }; - if (!isScalarToken(l) || !isScalarToken(r)) { + if (!isScalarToken(l) || !isScalarToken(r) || (l.typ == r.typ && 'unit' in l && 'unit' in r && l.unit != r.unit)) { return defaultReturn; } + if (l.typ == exports.EnumToken.FunctionTokenType) { + const val = evaluateFunc(l); + if (val.length == 1) { + l = val[0]; + } + else { + return defaultReturn; + } + } + if (r.typ == exports.EnumToken.FunctionTokenType) { + const val = evaluateFunc(r); + if (val.length == 1) { + r = val[0]; + } + else { + return defaultReturn; + } + } + if (l.typ == exports.EnumToken.FunctionTokenType) { + const val = evaluateFunc(l); + if (val.length == 1) { + l = val[0]; + } + } if ((op == exports.EnumToken.Add || op == exports.EnumToken.Sub)) { // @ts-ignore if (l.typ != r.typ) { @@ -2607,11 +2669,13 @@ function doEvaluate(l, r, op) { ![exports.EnumToken.NumberTokenType, exports.EnumToken.PercentageTokenType].includes(r.typ)) { return defaultReturn; } - const typ = l.typ == exports.EnumToken.NumberTokenType ? r.typ : (r.typ == exports.EnumToken.NumberTokenType ? l.typ : (l.typ == exports.EnumToken.PercentageTokenType ? r.typ : l.typ)); - // @ts-ignore - let v1 = typeof l.val == 'string' ? +l.val : l.val; + let typ = l.typ == exports.EnumToken.NumberTokenType ? r.typ : (r.typ == exports.EnumToken.NumberTokenType ? l.typ : (l.typ == exports.EnumToken.PercentageTokenType ? r.typ : l.typ)); // @ts-ignore - let v2 = typeof r.val == 'string' ? +r.val : r.val; + let v1 = getValue$1(l); + let v2 = getValue$1(r); + if (v1 == null || v2 == null) { + return defaultReturn; + } if (op == exports.EnumToken.Mul) { if (l.typ != exports.EnumToken.NumberTokenType && r.typ != exports.EnumToken.NumberTokenType) { if (typeof v1 == 'number' && l.typ == exports.EnumToken.PercentageTokenType) { @@ -2632,11 +2696,239 @@ function doEvaluate(l, r, op) { } // @ts-ignore const val = compute(v1, v2, op); - return { + // typ = typeof val == 'number' ? EnumToken.NumberTokenType : EnumToken.FractionTokenType; + const token = { ...(l.typ == exports.EnumToken.NumberTokenType ? r : l), typ, val: typeof val == 'number' ? reduceNumber(val) : val }; + if (token.typ == exports.EnumToken.IdenTokenType) { + // @ts-ignore + token.typ = exports.EnumToken.NumberTokenType; + } + return token; +} +function getValue$1(t) { + let v1; + if (t.typ == exports.EnumToken.FunctionTokenType) { + v1 = evaluateFunc(t); + if (v1.length != 1 || v1[0].typ == exports.EnumToken.BinaryExpressionTokenType) { + return null; + } + t = v1[0]; + } + if (t.typ == exports.EnumToken.IdenTokenType) { + // @ts-ignore + return Math[t.val.toUpperCase()]; + } + if (t.val.typ == exports.EnumToken.FractionTokenType) { + // @ts-ignore + return t.val.l.val / t.val.r.val; + } + // @ts-ignore + return t.typ == exports.EnumToken.FractionTokenType ? t.l.val / t.r.val : +t.val; +} +function evaluateFunc(token) { + const values = token.chi.slice(); + switch (token.val) { + case 'abs': + case 'sin': + case 'cos': + case 'tan': + case 'asin': + case 'acos': + case 'atan': + case 'sign': + case 'sqrt': + case 'exp': { + const value = evaluate(values); + if (value.length != 1 || (value[0].typ != exports.EnumToken.NumberTokenType && value[0].typ != exports.EnumToken.FractionTokenType) || (value[0].typ == exports.EnumToken.FractionTokenType && (+value[0].r.val == 0 || !Number.isFinite(+value[0].l.val) || !Number.isFinite(+value[0].r.val)))) { + return value; + } + // @ts-ignore + let val = value[0].typ == exports.EnumToken.NumberTokenType ? +value[0].val : value[0].l.val / value[0].r.val; + return [{ + typ: exports.EnumToken.NumberTokenType, + val: '' + Math[token.val](val) + }]; + } + case 'hypot': { + const chi = values.filter(t => ![exports.EnumToken.WhitespaceTokenType, exports.EnumToken.CommentTokenType, exports.EnumToken.CommaTokenType].includes(t.typ)); + let all = []; + let ref = chi[0]; + let value = 0; + if (![exports.EnumToken.NumberTokenType, exports.EnumToken.PercentageTokenType].includes(ref.typ) && !('unit' in ref)) { + return [token]; + } + for (let i = 0; i < chi.length; i++) { + // @ts-ignore + if (chi[i].typ != ref.typ || ('unit' in chi[i] && 'unit' in ref && chi[i].unit != ref.unit)) { + return [token]; + } + // @ts-ignore + const val = getValue$1(chi[i]); + if (val == null) { + return [token]; + } + all.push(val); + value += val * val; + } + return [ + { + ...ref, + val: Math.sqrt(value).toFixed(rem(...all)) + } + ]; + } + case 'atan2': + case 'pow': + case 'rem': + case 'mod': { + const chi = values.filter(t => ![exports.EnumToken.WhitespaceTokenType, exports.EnumToken.CommentTokenType].includes(t.typ)); + if (chi.length != 3 || chi[1].typ != exports.EnumToken.CommaTokenType) { + return [token]; + } + if (token.val == 'pow' && (chi[0].typ != exports.EnumToken.NumberTokenType || chi[2].typ != exports.EnumToken.NumberTokenType)) { + return [token]; + } + if (['rem', 'mod'].includes(token.val) && + (chi[0].typ != chi[2].typ) || ('unit' in chi[0] && 'unit' in chi[2] && + chi[0].unit != chi[2].unit)) { + return [token]; + } + // https://developer.mozilla.org/en-US/docs/Web/CSS/mod + const v1 = evaluate([chi[0]]); + const v2 = evaluate([chi[2]]); + const types = [exports.EnumToken.PercentageTokenType, exports.EnumToken.DimensionTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.NumberTokenType, exports.EnumToken.LengthTokenType, exports.EnumToken.TimeTokenType, exports.EnumToken.FrequencyTokenType, exports.EnumToken.ResolutionTokenType]; + if (v1.length != 1 || v2.length != 1 || !types.includes(v1[0].typ) || !types.includes(v2[0].typ) || v1[0].unit != v2[0].unit) { + return [token]; + } + // @ts-ignore + const val1 = getValue$1(v1[0]); + // @ts-ignore + const val2 = getValue$1(v2[0]); + if (val1 == null || val2 == null || (v1[0].typ != v2[0].typ && val1 != 0 && val2 != 0)) { + return [token]; + } + if (token.val == 'rem') { + if (val2 == 0) { + return [token]; + } + return [ + { + ...v1[0], + val: (val1 % val2).toFixed(rem(val1, val2)) + } + ]; + } + if (token.val == 'pow') { + return [ + { + ...v1[0], + val: String(Math.pow(val1, val2)) + } + ]; + } + if (token.val == 'atan2') { + return [ + { + ...{}, ...v1[0], + val: String(Math.atan2(val1, val2)) + } + ]; + } + return [ + { + ...v1[0], + val: String(val2 == 0 ? val1 : val1 - (Math.floor(val1 / val2) * val2)) + } + ]; + } + case 'clamp': + token.chi = values; + return [token]; + case 'log': + case 'round': + case 'min': + case 'max': + { + const strategy = token.val == 'round' && values[0]?.typ == exports.EnumToken.IdenTokenType ? values.shift().val : null; + const valuesMap = new Map; + for (const curr of values) { + if (curr.typ == exports.EnumToken.CommaTokenType || curr.typ == exports.EnumToken.WhitespaceTokenType || curr.typ == exports.EnumToken.CommentTokenType) { + continue; + } + const result = evaluate([curr]); + if (result.length != 1 || result[0].typ == exports.EnumToken.FunctionTokenType) { + return [token]; + } + const key = result[0].typ + ('unit' in result[0] ? result[0].unit : ''); + if (!valuesMap.has(key)) { + valuesMap.set(key, []); + } + valuesMap.get(key).push(result[0]); + } + if (valuesMap.size == 1) { + const values = valuesMap.values().next().value; + console.error({ values }); + if (token.val == 'log') { + if (values[0].typ != exports.EnumToken.NumberTokenType || values.length > 2) { + return [token]; + } + const val1 = getValue$1(values[0]); + const val2 = values.length == 2 ? getValue$1(values[1]) : null; + if (values.length == 1) { + return [ + { + ...values[0], + val: String(Math.log(val1)) + } + ]; + } + return [ + { + ...values[0], + val: String(Math.log(val1) / Math.log(val2)) + } + ]; + } + if (token.val == 'min' || token.val == 'max') { + let val = getValue$1(values[0]); + let val2 = val; + let ret = values[0]; + for (const curr of values.slice(1)) { + val2 = getValue$1(curr); + if (val2 < val && token.val == 'min') { + val = val2; + ret = curr; + } + else if (val2 > val && token.val == 'max') { + val = val2; + ret = curr; + } + } + return [ret]; + } + if (token.val == 'round') { + let val = getValue$1(values[0]); + let val2 = getValue$1(values[1]); + if (Number.isNaN(val) || Number.isNaN(val2)) { + return [token]; + } + if (strategy == null || strategy == 'down') { + val = val - (val % val2); + } + else { + val = strategy == 'to-zero' ? Math.trunc(val / val2) * val2 : (strategy == 'nearest' ? Math.round(val / val2) * val2 : Math.ceil(val / val2) * val2); + } + // @ts-ignore + return [{ ...values[0], val: String(val) }]; + } + } + } + return [token]; + } + return [token]; } /** * convert BinaryExpression into an array @@ -2677,7 +2969,11 @@ function evaluateExpression(token) { return doEvaluate(token.l, token.r, token.op); } function isScalarToken(token) { - return 'unit' in token || [exports.EnumToken.NumberTokenType, exports.EnumToken.FractionTokenType, exports.EnumToken.PercentageTokenType].includes(token.typ); + return 'unit' in token || + (token.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(token.val)) || + // @ts-ignore + (token.typ == exports.EnumToken.IdenTokenType && typeof Math[token.val.toUpperCase()] == 'number') || + [exports.EnumToken.NumberTokenType, exports.EnumToken.FractionTokenType, exports.EnumToken.PercentageTokenType].includes(token.typ); } /** * @@ -2786,7 +3082,7 @@ function parseRelativeColor(relativeKeys, original, rExp, gExp, bExp, aExp) { val: '1' } : aExp) }; - return computeComponentValue(keys, values); + return computeComponentValue(keys, converted, values); } function getValue(t, converted, component) { if (t == null) { @@ -2805,7 +3101,7 @@ function getValue(t, converted, component) { } return t; } -function computeComponentValue(expr, values) { +function computeComponentValue(expr, converted, values) { for (const object of [values, expr]) { if ('h' in object) { // normalize hue @@ -2846,34 +3142,29 @@ function computeComponentValue(expr, values) { expr[key] = values[exp.val]; } } - else if (exp.typ == exports.EnumToken.FunctionTokenType && exp.val == 'calc') { - for (let { value, parent } of walkValues(exp.chi)) { - if (value.typ == exports.EnumToken.IdenTokenType) { - if (!(value.val in values)) { + else if (exp.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(exp.val)) { + for (let { value, parent } of walkValues(exp.chi, exp)) { + if (parent == null) { + parent = exp; + } + if (value.typ == exports.EnumToken.PercentageTokenType) { + replaceValue(parent, value, getValue(value, converted, key)); + } + else if (value.typ == exports.EnumToken.IdenTokenType) { + // @ts-ignore + if (!(value.val in values || typeof Math[value.val.toUpperCase()] == 'number')) { return null; } - if (parent == null) { - parent = exp; - } - if (parent.typ == exports.EnumToken.BinaryExpressionTokenType) { - if (parent.l == value) { - parent.l = values[value.val]; - } - else { - parent.r = values[value.val]; - } - } - else { - for (let i = 0; i < parent.chi.length; i++) { - if (parent.chi[i] == value) { - parent.chi.splice(i, 1, values[value.val]); - break; - } - } - } + // @ts-ignore + replaceValue(parent, value, values[value.val] ?? { + typ: exports.EnumToken.NumberTokenType, + // @ts-ignore + val: '' + Math[value.val.toUpperCase()] + // @ts-ignore + }); } } - const result = evaluate(exp.chi); + const result = exp.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(exp.val) && exp.val != 'calc' ? evaluateFunc(exp) : evaluate(exp.chi); if (result.length == 1 && result[0].typ != exports.EnumToken.BinaryExpressionTokenType) { expr[key] = result[0]; } @@ -2884,6 +3175,34 @@ function computeComponentValue(expr, values) { } return expr; } +function replaceValue(parent, value, newValue) { + if (parent.typ == exports.EnumToken.BinaryExpressionTokenType) { + if (parent.l == value) { + parent.l = newValue; + } + else { + parent.r = newValue; + } + } + else { + for (let i = 0; i < parent.chi.length; i++) { + if (parent.chi[i] == value) { + parent.chi.splice(i, 1, newValue); + break; + } + if (parent.chi[i].typ == exports.EnumToken.BinaryExpressionTokenType) { + if (parent.chi[i].l == value) { + parent.chi[i].l = newValue; + break; + } + else if (parent.chi[i].r == value) { + parent.chi[i].r = newValue; + break; + } + } + } + } +} // from https://github.com/Rich-Harris/vlq/tree/master // credit: Rich Harris @@ -3011,6 +3330,7 @@ function doRender(data, options = {}) { ...(options.minify ?? true ? { indent: '', newLine: '', + removeEmpty: true, removeComments: true } : { indent: ' ', @@ -3159,6 +3479,9 @@ function renderAstNode(data, options, sourcemap, position, errors, reducer, cach } return `${css}${options.newLine}${indentSub}${str}`; }, ''); + if (options.removeEmpty && children === '') { + return ''; + } if (children.endsWith(';')) { children = children.slice(0, -1); } @@ -3169,6 +3492,7 @@ function renderAstNode(data, options, sourcemap, position, errors, reducer, cach case exports.EnumToken.InvalidRuleTokenType: return ''; default: + // return renderToken(data as Token, options, cache, reducer, errors); throw new Error(`render: unexpected token ${JSON.stringify(data, null, 1)}`); } return ''; @@ -3360,12 +3684,11 @@ function renderToken(token, options = {}, cache = Object.create(null), reducer, case exports.EnumToken.TimelineFunctionTokenType: case exports.EnumToken.GridTemplateFuncTokenType: if (token.typ == exports.EnumToken.FunctionTokenType && - token.val == 'calc' && + mathFuncs.includes(token.val) && token.chi.length == 1 && - token.chi[0].typ != exports.EnumToken.BinaryExpressionTokenType && - token.chi[0].typ != exports.EnumToken.FractionTokenType && + ![exports.EnumToken.BinaryExpressionTokenType, exports.EnumToken.FractionTokenType, exports.EnumToken.IdenTokenType].includes(token.chi[0].typ) && + // @ts-ignore token.chi[0].val?.typ != exports.EnumToken.FractionTokenType) { - // calc(200px) => 200px return token.chi.reduce((acc, curr) => acc + renderToken(curr, options, cache, reducer), ''); } // @ts-ignore @@ -3652,7 +3975,7 @@ function isColor(token) { return false; } } - if (children[i].typ == exports.EnumToken.FunctionTokenType && !['calc'].includes(children[i].val)) { + if (children[i].typ == exports.EnumToken.FunctionTokenType && !mathFuncs.includes(children[i].val)) { return false; } } @@ -3747,7 +4070,7 @@ function isColor(token) { } continue; } - if (v.typ == exports.EnumToken.FunctionTokenType && (v.val == 'calc' || v.val == 'var' || colorsFunc.includes(v.val))) { + if (v.typ == exports.EnumToken.FunctionTokenType && (mathFuncs.includes(v.val) || v.val == 'var' || colorsFunc.includes(v.val))) { continue; } if (![exports.EnumToken.ColorTokenType, exports.EnumToken.IdenTokenType, exports.EnumToken.NumberTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.PercentageTokenType, exports.EnumToken.CommaTokenType, exports.EnumToken.WhitespaceTokenType, exports.EnumToken.LiteralTokenType].includes(v.typ)) { @@ -5494,8 +5817,6 @@ var config$3 = { Object.freeze(config$3); const getConfig$1 = () => config$3; -// https://www.w3.org/TR/css-values-4/#math-function -const funcList = ['clamp', 'calc']; function matchType(val, properties) { if (val.typ == exports.EnumToken.IdenTokenType && properties.keywords.includes(val.val) || // @ts-ignore @@ -5511,8 +5832,8 @@ function matchType(val, properties) { }); } if (val.typ == exports.EnumToken.FunctionTokenType) { - if (funcList.includes(val.val)) { - return val.chi.every(((t) => [exports.EnumToken.LiteralTokenType, exports.EnumToken.CommaTokenType, exports.EnumToken.WhitespaceTokenType, exports.EnumToken.StartParensTokenType, exports.EnumToken.EndParensTokenType].includes(t.typ) || matchType(t, properties))); + if (mathFuncs.includes(val.val)) { + return val.chi.every(((t) => [exports.EnumToken.Add, exports.EnumToken.Mul, exports.EnumToken.Div, exports.EnumToken.Sub, exports.EnumToken.LiteralTokenType, exports.EnumToken.CommaTokenType, exports.EnumToken.WhitespaceTokenType, exports.EnumToken.DimensionTokenType, exports.EnumToken.NumberTokenType, exports.EnumToken.LengthTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.PercentageTokenType, exports.EnumToken.ResolutionTokenType, exports.EnumToken.TimeTokenType, exports.EnumToken.BinaryExpressionTokenType].includes(t.typ) || matchType(t, properties))); } // match type defined like function 'symbols()', 'url()', 'attr()' etc. // return properties.types.includes((val).val + '()') @@ -60215,9 +60536,6 @@ function parseString(src, options = { location: false }) { })); } function getTokenType(val, hint) { - // if (val === '' && hint == null) { - // throw new Error('empty string?'); - // } if (hint != null) { return enumTokenHints.has(hint) ? { typ: hint } : { typ: hint, val }; } @@ -60395,11 +60713,7 @@ function parseTokens(tokens, options = {}) { if (t.typ == exports.EnumToken.WhitespaceTokenType && ((i == 0 || i + 1 == tokens.length || [exports.EnumToken.CommaTokenType, exports.EnumToken.GteTokenType, exports.EnumToken.LteTokenType, exports.EnumToken.ColumnCombinatorTokenType].includes(tokens[i + 1].typ)) || - (i > 0 && - // tokens[i + 1]?.typ != Literal || - // funcLike.includes(tokens[i - 1].typ) && - // !['var', 'calc'].includes((tokens[i - 1]).val)))) && - trimWhiteSpace.includes(tokens[i - 1].typ)))) { + (i > 0 && trimWhiteSpace.includes(tokens[i - 1].typ)))) { tokens.splice(i--, 1); continue; } @@ -60592,7 +60906,7 @@ function parseTokens(tokens, options = {}) { // @ts-ignore parseTokens(t.chi, options); } - if (t.typ == exports.EnumToken.FunctionTokenType && t.val == 'calc') { + if (t.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(t.val)) { for (const { value, parent } of walkValues(t.chi)) { if (value.typ == exports.EnumToken.WhitespaceTokenType) { const p = (parent ?? t); @@ -60748,6 +61062,11 @@ function eq(a, b) { return true; } +var WalkerValueEvent; +(function (WalkerValueEvent) { + WalkerValueEvent[WalkerValueEvent["Enter"] = 0] = "Enter"; + WalkerValueEvent[WalkerValueEvent["Leave"] = 1] = "Leave"; +})(WalkerValueEvent || (WalkerValueEvent = {})); function* walk(node, filter) { const parents = [node]; const root = node; @@ -60777,37 +61096,89 @@ function* walk(node, filter) { } } function* walkValues(values, root = null, filter) { + // const set = new Set(); const stack = values.slice(); const map = new Map; - let value; let previous = null; - while ((value = stack.shift())) { + // let parent: FunctionToken | ParensToken | BinaryExpressionToken | null = null; + if (filter != null && typeof filter == 'function') { + filter = { + event: WalkerValueEvent.Enter, + fn: filter + }; + } + else if (filter == null) { + filter = { + event: WalkerValueEvent.Enter + }; + } + while (stack.length > 0) { + let value = stack.shift(); let option = null; - if (filter != null) { - option = filter(value); - if (option === 'ignore') { - continue; - } - if (option === 'stop') { - break; + if (filter.fn != null && filter.event == WalkerValueEvent.Enter) { + const isValid = filter.type == null || value.typ == filter.type || + (Array.isArray(filter.type) && filter.type.includes(value.typ)) || + (typeof filter.type == 'function' && filter.type(value)); + if (isValid) { + option = filter.fn(value, map.get(value) ?? root, WalkerValueEvent.Enter); + if (option === 'ignore') { + continue; + } + if (option === 'stop') { + break; + } + // @ts-ignore + if (option != null && typeof option == 'object' && 'typ' in option) { + map.set(option, map.get(value) ?? root); + } } } // @ts-ignore - if (option !== 'children') { - // @ts-ignore - yield { value, parent: map.get(value), previousValue: previous, nextValue: stack[0] ?? null, root }; + if (filter.event == WalkerValueEvent.Enter && option !== 'children') { + yield { + value, + parent: map.get(value) ?? root, + previousValue: previous, + nextValue: stack[0] ?? null, + // @ts-ignore + root: root ?? null + }; } if (option !== 'ignore-children' && 'chi' in value) { - for (const child of value.chi.slice()) { + const sliced = value.chi.slice(); + for (const child of sliced) { map.set(child, value); } - stack.unshift(...value.chi); + stack.unshift(...sliced); } else if (value.typ == exports.EnumToken.BinaryExpressionTokenType) { - map.set(value.l, value); - map.set(value.r, value); + map.set(value.l, map.get(value) ?? root); + map.set(value.r, map.get(value) ?? root); stack.unshift(value.l, value.r); } + if (filter.event == WalkerValueEvent.Leave && filter.fn != null) { + const isValid = filter.type == null || value.typ == filter.type || + (Array.isArray(filter.type) && filter.type.includes(value.typ)) || + (typeof filter.type == 'function' && filter.type(value)); + if (isValid) { + option = filter.fn(value, map.get(value), WalkerValueEvent.Leave); + // @ts-ignore + if (option != null && 'typ' in option) { + map.set(option, map.get(value) ?? root); + } + } + } + // @ts-ignore + if (filter.event == WalkerValueEvent.Leave && option !== 'children') { + yield { + value, + parent: map.get(value) ?? root, + previousValue: previous, + nextValue: stack[0] ?? null, + // @ts-ignore + root: root ?? null + }; + } previous = value; } } @@ -61139,7 +61510,6 @@ function matchToken(token, matches) { case ValidationTokenEnum.Comma: break; case ValidationTokenEnum.Keyword: - console.error(matches[i], token); if (token.typ == exports.EnumToken.IdenTokenType && token.val == matches[i].val) { return token; } @@ -61178,10 +61548,6 @@ function matchToken(token, matches) { return result; } break; - // default: - // - // console.error(token, matches[i]); - // throw new Error('bar bar'); } } return null; @@ -61294,7 +61660,8 @@ class InlineCssVariablesFeature { i = parent.chi?.length ?? 0; while (i--) { if (parent.chi[i].typ == exports.EnumToken.DeclarationNodeType && parent.chi[i].nam == info.node.nam) { - parent.chi.splice(i, 1); + // @ts-ignore + parent.chi.splice(i++, 1, { typ: exports.EnumToken.CommentTokenType, val: `/* ${info.node.nam}: ${info.node.val.reduce((acc, curr) => acc + renderToken(curr), '')} */` }); } } if (parent.chi?.length == 0 && 'parent' in parent) { @@ -62272,30 +62639,91 @@ class ComputeCalcExpressionFeature { continue; } const set = new Set; - for (const { value, parent } of walkValues(node.val)) { - if (value != null && value.typ == exports.EnumToken.FunctionTokenType && value.val == 'calc') { - if (!set.has(parent)) { + for (const { value, parent } of walkValues(node.val, node, { + event: WalkerValueEvent.Enter, + fn(node, parent, event) { + if (parent != null && + parent.typ == exports.EnumToken.DeclarationNodeType && + parent.val.length == 1 && + node.typ == exports.EnumToken.FunctionTokenType && + mathFuncs.includes(node.val) && + node.chi.length == 1 && + node.chi[0].typ == exports.EnumToken.IdenTokenType) { + return 'ignore'; + } + if ((node.typ == exports.EnumToken.FunctionTokenType && node.val == 'var') || (!mathFuncs.includes(parent.val) && [exports.EnumToken.ColorTokenType, exports.EnumToken.DeclarationNodeType, exports.EnumToken.RuleNodeType, exports.EnumToken.AtRuleNodeType, exports.EnumToken.StyleSheetNodeType].includes(parent?.typ))) { + return null; + } + const slice = (node.typ == exports.EnumToken.FunctionTokenType ? node.chi : (node.typ == exports.EnumToken.DeclarationNodeType ? node.val : node.chi))?.slice(); + if (slice != null && node.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(node.val)) { + // @ts-ignore + const cp = (node.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(node.val) && node.val != 'calc' ? [node] : (node.typ == exports.EnumToken.DeclarationNodeType ? node.val : node.chi)).slice(); + const values = evaluate(cp); + const key = 'chi' in node ? 'chi' : 'val'; + const str1 = renderToken({ ...node, [key]: slice }); + const str2 = renderToken(node); // values.reduce((acc: string, curr: Token): string => acc + renderToken(curr), ''); + if (str1.length <= str2.length) { + // @ts-ignore + node[key] = slice; + } + else { + // @ts-ignore + node[key] = values; + } + return 'ignore'; + } + return null; + } + })) { + if (value != null && value.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(value.val)) { + if (!set.has(value)) { set.add(value); - value.chi = evaluate(value.chi); - if (value.chi.length == 1 && value.chi[0].typ != exports.EnumToken.BinaryExpressionTokenType) { - if (parent != null) { + if (parent != null) { + // @ts-ignore + const cp = value.typ == exports.EnumToken.FunctionTokenType && mathFuncs.includes(value.val) && value.val != 'calc' ? [value] : (value.typ == exports.EnumToken.DeclarationNodeType ? value.val : value.chi); + const values = evaluate(cp); + // @ts-ignore + const children = parent.typ == exports.EnumToken.DeclarationNodeType ? parent.val : parent.chi; + if (values.length == 1 && values[0].typ != exports.EnumToken.BinaryExpressionTokenType) { if (parent.typ == exports.EnumToken.BinaryExpressionTokenType) { if (parent.l == value) { - parent.l = value.chi[0]; + parent.l = values[0]; } else { - parent.r = value.chi[0]; + parent.r = values[0]; } } else { - for (let i = 0; i < parent.chi.length; i++) { - if (parent.chi[i] == value) { - parent.chi.splice(i, 1, value.chi[0]); + for (let i = 0; i < children.length; i++) { + if (children[i] == value) { + // @ts-ignore + children.splice(i, 1, !(parent.typ == exports.EnumToken.FunctionTokenType && parent.val == 'calc') && typeof values[0].val != 'string' ? { + typ: exports.EnumToken.FunctionTokenType, + val: 'calc', + chi: values + } : values[0]); break; } } } } + else { + for (let i = 0; i < children.length; i++) { + if (children[i] == value) { + if (parent.typ == exports.EnumToken.FunctionTokenType && parent.val == 'calc') { + children.splice(i, 1, ...values); + } + else { + children.splice(i, 1, { + typ: exports.EnumToken.FunctionTokenType, + val: 'calc', + chi: values + }); + } + break; + } + } + } } } } diff --git a/dist/index.d.ts b/dist/index.d.ts index 44dc6df7..7705fe11 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -115,8 +115,16 @@ declare function minify(ast: AstNode, options?: ParserOptions | MinifyOptions, r [key: string]: any; }): AstNode; +declare enum WalkerValueEvent$1 { + Enter = 0, + Leave = 1 +} declare function walk(node: AstNode, filter?: WalkerFilter): Generator; -declare function walkValues(values: Token[], root?: AstNode | null, filter?: WalkerValueFilter): Generator; +declare function walkValues(values: Token[], root?: AstNode | Token | null, filter?: WalkerValueFilter | { + event: WalkerValueEvent$1; + fn?: WalkerValueFilter; + type?: EnumToken | EnumToken[] | ((token: Token) => boolean); +}): Generator; declare function expand(ast: AstNode): AstNode; @@ -826,7 +834,7 @@ declare class SourceMap { toJSON(): SourceMapObject; } -export declare type WalkerOption = 'ignore' | 'stop' | 'children' | 'ignore-children' | null; +export declare type WalkerOption = 'ignore' | 'stop' | 'children' | 'ignore-children' | Token | null; /** * returned value: * - 'ignore': ignore this node and its children @@ -843,18 +851,18 @@ export declare type WalkerFilter = (node: AstNode) => WalkerOption; * - 'children': walk the children and ignore the node itself * - 'ignore-children': walk the node and ignore children */ -export declare type WalkerValueFilter = (node: Token) => WalkerOption; +export declare type WalkerValueFilter = (node: AstNode | Token, parent: FunctionToken | ParensToken | BinaryExpressionToken, event?: WalkerValueEvent) => WalkerOption | null; export declare interface WalkResult { node: AstNode; parent?: AstRuleList; - root?: AstRuleList; + root?: AstNode; } export declare interface WalkAttributesResult { value: Token; previousValue: Token | null; - nextValue: AstNode | null; + nextValue: Token | null; root?: AstNode; parent: FunctionToken | ParensToken | BinaryExpressionToken | null; } @@ -941,6 +949,7 @@ export declare interface ResolvedPath { export declare interface RenderOptions { minify?: boolean; + removeEmpty?: boolean; expandNestingRules?: boolean; preserveLicense?: boolean; sourcemap?: boolean; diff --git a/dist/lib/ast/features/calc.js b/dist/lib/ast/features/calc.js index aae10b69..b62ba8ea 100644 --- a/dist/lib/ast/features/calc.js +++ b/dist/lib/ast/features/calc.js @@ -1,6 +1,11 @@ import { EnumToken } from '../types.js'; -import { walkValues } from '../walk.js'; +import { walkValues, WalkerValueEvent } from '../walk.js'; import { evaluate } from '../math/expression.js'; +import { mathFuncs } from '../../renderer/color/utils/constants.js'; +import '../minify.js'; +import '../../parser/parse.js'; +import { renderToken } from '../../renderer/render.js'; +import '../../parser/utils/config.js'; class ComputeCalcExpressionFeature { static get ordering() { @@ -27,30 +32,91 @@ class ComputeCalcExpressionFeature { continue; } const set = new Set; - for (const { value, parent } of walkValues(node.val)) { - if (value != null && value.typ == EnumToken.FunctionTokenType && value.val == 'calc') { - if (!set.has(parent)) { + for (const { value, parent } of walkValues(node.val, node, { + event: WalkerValueEvent.Enter, + fn(node, parent, event) { + if (parent != null && + parent.typ == EnumToken.DeclarationNodeType && + parent.val.length == 1 && + node.typ == EnumToken.FunctionTokenType && + mathFuncs.includes(node.val) && + node.chi.length == 1 && + node.chi[0].typ == EnumToken.IdenTokenType) { + return 'ignore'; + } + if ((node.typ == EnumToken.FunctionTokenType && node.val == 'var') || (!mathFuncs.includes(parent.val) && [EnumToken.ColorTokenType, EnumToken.DeclarationNodeType, EnumToken.RuleNodeType, EnumToken.AtRuleNodeType, EnumToken.StyleSheetNodeType].includes(parent?.typ))) { + return null; + } + const slice = (node.typ == EnumToken.FunctionTokenType ? node.chi : (node.typ == EnumToken.DeclarationNodeType ? node.val : node.chi))?.slice(); + if (slice != null && node.typ == EnumToken.FunctionTokenType && mathFuncs.includes(node.val)) { + // @ts-ignore + const cp = (node.typ == EnumToken.FunctionTokenType && mathFuncs.includes(node.val) && node.val != 'calc' ? [node] : (node.typ == EnumToken.DeclarationNodeType ? node.val : node.chi)).slice(); + const values = evaluate(cp); + const key = 'chi' in node ? 'chi' : 'val'; + const str1 = renderToken({ ...node, [key]: slice }); + const str2 = renderToken(node); // values.reduce((acc: string, curr: Token): string => acc + renderToken(curr), ''); + if (str1.length <= str2.length) { + // @ts-ignore + node[key] = slice; + } + else { + // @ts-ignore + node[key] = values; + } + return 'ignore'; + } + return null; + } + })) { + if (value != null && value.typ == EnumToken.FunctionTokenType && mathFuncs.includes(value.val)) { + if (!set.has(value)) { set.add(value); - value.chi = evaluate(value.chi); - if (value.chi.length == 1 && value.chi[0].typ != EnumToken.BinaryExpressionTokenType) { - if (parent != null) { + if (parent != null) { + // @ts-ignore + const cp = value.typ == EnumToken.FunctionTokenType && mathFuncs.includes(value.val) && value.val != 'calc' ? [value] : (value.typ == EnumToken.DeclarationNodeType ? value.val : value.chi); + const values = evaluate(cp); + // @ts-ignore + const children = parent.typ == EnumToken.DeclarationNodeType ? parent.val : parent.chi; + if (values.length == 1 && values[0].typ != EnumToken.BinaryExpressionTokenType) { if (parent.typ == EnumToken.BinaryExpressionTokenType) { if (parent.l == value) { - parent.l = value.chi[0]; + parent.l = values[0]; } else { - parent.r = value.chi[0]; + parent.r = values[0]; } } else { - for (let i = 0; i < parent.chi.length; i++) { - if (parent.chi[i] == value) { - parent.chi.splice(i, 1, value.chi[0]); + for (let i = 0; i < children.length; i++) { + if (children[i] == value) { + // @ts-ignore + children.splice(i, 1, !(parent.typ == EnumToken.FunctionTokenType && parent.val == 'calc') && typeof values[0].val != 'string' ? { + typ: EnumToken.FunctionTokenType, + val: 'calc', + chi: values + } : values[0]); break; } } } } + else { + for (let i = 0; i < children.length; i++) { + if (children[i] == value) { + if (parent.typ == EnumToken.FunctionTokenType && parent.val == 'calc') { + children.splice(i, 1, ...values); + } + else { + children.splice(i, 1, { + typ: EnumToken.FunctionTokenType, + val: 'calc', + chi: values + }); + } + break; + } + } + } } } } diff --git a/dist/lib/ast/features/inlinecssvariables.js b/dist/lib/ast/features/inlinecssvariables.js index 91edfc1b..ca965b6b 100644 --- a/dist/lib/ast/features/inlinecssvariables.js +++ b/dist/lib/ast/features/inlinecssvariables.js @@ -1,5 +1,6 @@ import { EnumToken } from '../types.js'; import { walkValues } from '../walk.js'; +import { renderToken } from '../../renderer/render.js'; function replace(node, variableScope) { for (const { value, parent: parentValue } of walkValues(node.val)) { @@ -108,7 +109,8 @@ class InlineCssVariablesFeature { i = parent.chi?.length ?? 0; while (i--) { if (parent.chi[i].typ == EnumToken.DeclarationNodeType && parent.chi[i].nam == info.node.nam) { - parent.chi.splice(i, 1); + // @ts-ignore + parent.chi.splice(i++, 1, { typ: EnumToken.CommentTokenType, val: `/* ${info.node.nam}: ${info.node.val.reduce((acc, curr) => acc + renderToken(curr), '')} */` }); } } if (parent.chi?.length == 0 && 'parent' in parent) { diff --git a/dist/lib/ast/features/prefix.js b/dist/lib/ast/features/prefix.js index a4d4abbc..3448de37 100644 --- a/dist/lib/ast/features/prefix.js +++ b/dist/lib/ast/features/prefix.js @@ -67,7 +67,6 @@ function matchToken(token, matches) { case ValidationTokenEnum.Comma: break; case ValidationTokenEnum.Keyword: - console.error(matches[i], token); if (token.typ == EnumToken.IdenTokenType && token.val == matches[i].val) { return token; } @@ -106,10 +105,6 @@ function matchToken(token, matches) { return result; } break; - // default: - // - // console.error(token, matches[i]); - // throw new Error('bar bar'); } } return null; diff --git a/dist/lib/ast/features/shorthand.js b/dist/lib/ast/features/shorthand.js index 24459781..c7911f97 100644 --- a/dist/lib/ast/features/shorthand.js +++ b/dist/lib/ast/features/shorthand.js @@ -1,6 +1,7 @@ import { PropertyList } from '../../parser/declaration/list.js'; import { EnumToken } from '../types.js'; import '../minify.js'; +import '../walk.js'; import '../../parser/parse.js'; import '../../renderer/color/utils/constants.js'; import '../../renderer/sourcemap/lib/encode.js'; diff --git a/dist/lib/ast/math/expression.js b/dist/lib/ast/math/expression.js index 2d169599..96124363 100644 --- a/dist/lib/ast/math/expression.js +++ b/dist/lib/ast/math/expression.js @@ -1,6 +1,11 @@ import { EnumToken } from '../types.js'; -import { compute } from './math.js'; +import { compute, rem } from './math.js'; import { reduceNumber } from '../../renderer/render.js'; +import { mathFuncs } from '../../renderer/color/utils/constants.js'; +import '../minify.js'; +import '../walk.js'; +import '../../parser/parse.js'; +import '../../parser/utils/config.js'; /** * evaluate an array of tokens @@ -8,15 +13,45 @@ import { reduceNumber } from '../../renderer/render.js'; */ function evaluate(tokens) { let nodes; + if (tokens.length == 1 && tokens[0].typ == EnumToken.FunctionTokenType && tokens[0].val != 'calc' && mathFuncs.includes(tokens[0].val)) { + const chi = tokens[0].chi.reduce((acc, t) => { + if (acc.length == 0 || t.typ == EnumToken.CommaTokenType) { + acc.push([]); + } + if ([EnumToken.WhitespaceTokenType, EnumToken.CommaTokenType, EnumToken.CommaTokenType].includes(t.typ)) { + return acc; + } + acc.at(-1).push(t); + return acc; + }, []); + for (let i = 0; i < chi.length; i++) { + chi[i] = evaluate(chi[i]); + } + tokens[0].chi = chi.reduce((acc, t) => { + if (acc.length > 0) { + acc.push({ typ: EnumToken.CommaTokenType }); + } + acc.push(...t); + return acc; + }); + return evaluateFunc(tokens[0]); + } try { nodes = inlineExpression(evaluateExpression(buildExpression(tokens))); } catch (e) { - // console.error({tokens}); - // console.error(e); return tokens; } if (nodes.length <= 1) { + // @ts-ignore + if (nodes.length == 1 && nodes[0].typ == EnumToken.IdenTokenType && typeof Math[nodes[0].val.toUpperCase()] == 'number') { + return [{ + ...nodes[0], + // @ts-ignore + val: '' + Math[nodes[0].val.toUpperCase()], + typ: EnumToken.NumberTokenType + }]; + } return nodes; } const map = new Map; @@ -71,9 +106,33 @@ function doEvaluate(l, r, op) { l, r }; - if (!isScalarToken(l) || !isScalarToken(r)) { + if (!isScalarToken(l) || !isScalarToken(r) || (l.typ == r.typ && 'unit' in l && 'unit' in r && l.unit != r.unit)) { return defaultReturn; } + if (l.typ == EnumToken.FunctionTokenType) { + const val = evaluateFunc(l); + if (val.length == 1) { + l = val[0]; + } + else { + return defaultReturn; + } + } + if (r.typ == EnumToken.FunctionTokenType) { + const val = evaluateFunc(r); + if (val.length == 1) { + r = val[0]; + } + else { + return defaultReturn; + } + } + if (l.typ == EnumToken.FunctionTokenType) { + const val = evaluateFunc(l); + if (val.length == 1) { + l = val[0]; + } + } if ((op == EnumToken.Add || op == EnumToken.Sub)) { // @ts-ignore if (l.typ != r.typ) { @@ -85,11 +144,13 @@ function doEvaluate(l, r, op) { ![EnumToken.NumberTokenType, EnumToken.PercentageTokenType].includes(r.typ)) { return defaultReturn; } - const typ = l.typ == EnumToken.NumberTokenType ? r.typ : (r.typ == EnumToken.NumberTokenType ? l.typ : (l.typ == EnumToken.PercentageTokenType ? r.typ : l.typ)); - // @ts-ignore - let v1 = typeof l.val == 'string' ? +l.val : l.val; + let typ = l.typ == EnumToken.NumberTokenType ? r.typ : (r.typ == EnumToken.NumberTokenType ? l.typ : (l.typ == EnumToken.PercentageTokenType ? r.typ : l.typ)); // @ts-ignore - let v2 = typeof r.val == 'string' ? +r.val : r.val; + let v1 = getValue(l); + let v2 = getValue(r); + if (v1 == null || v2 == null) { + return defaultReturn; + } if (op == EnumToken.Mul) { if (l.typ != EnumToken.NumberTokenType && r.typ != EnumToken.NumberTokenType) { if (typeof v1 == 'number' && l.typ == EnumToken.PercentageTokenType) { @@ -110,11 +171,239 @@ function doEvaluate(l, r, op) { } // @ts-ignore const val = compute(v1, v2, op); - return { + // typ = typeof val == 'number' ? EnumToken.NumberTokenType : EnumToken.FractionTokenType; + const token = { ...(l.typ == EnumToken.NumberTokenType ? r : l), typ, val: typeof val == 'number' ? reduceNumber(val) : val }; + if (token.typ == EnumToken.IdenTokenType) { + // @ts-ignore + token.typ = EnumToken.NumberTokenType; + } + return token; +} +function getValue(t) { + let v1; + if (t.typ == EnumToken.FunctionTokenType) { + v1 = evaluateFunc(t); + if (v1.length != 1 || v1[0].typ == EnumToken.BinaryExpressionTokenType) { + return null; + } + t = v1[0]; + } + if (t.typ == EnumToken.IdenTokenType) { + // @ts-ignore + return Math[t.val.toUpperCase()]; + } + if (t.val.typ == EnumToken.FractionTokenType) { + // @ts-ignore + return t.val.l.val / t.val.r.val; + } + // @ts-ignore + return t.typ == EnumToken.FractionTokenType ? t.l.val / t.r.val : +t.val; +} +function evaluateFunc(token) { + const values = token.chi.slice(); + switch (token.val) { + case 'abs': + case 'sin': + case 'cos': + case 'tan': + case 'asin': + case 'acos': + case 'atan': + case 'sign': + case 'sqrt': + case 'exp': { + const value = evaluate(values); + if (value.length != 1 || (value[0].typ != EnumToken.NumberTokenType && value[0].typ != EnumToken.FractionTokenType) || (value[0].typ == EnumToken.FractionTokenType && (+value[0].r.val == 0 || !Number.isFinite(+value[0].l.val) || !Number.isFinite(+value[0].r.val)))) { + return value; + } + // @ts-ignore + let val = value[0].typ == EnumToken.NumberTokenType ? +value[0].val : value[0].l.val / value[0].r.val; + return [{ + typ: EnumToken.NumberTokenType, + val: '' + Math[token.val](val) + }]; + } + case 'hypot': { + const chi = values.filter(t => ![EnumToken.WhitespaceTokenType, EnumToken.CommentTokenType, EnumToken.CommaTokenType].includes(t.typ)); + let all = []; + let ref = chi[0]; + let value = 0; + if (![EnumToken.NumberTokenType, EnumToken.PercentageTokenType].includes(ref.typ) && !('unit' in ref)) { + return [token]; + } + for (let i = 0; i < chi.length; i++) { + // @ts-ignore + if (chi[i].typ != ref.typ || ('unit' in chi[i] && 'unit' in ref && chi[i].unit != ref.unit)) { + return [token]; + } + // @ts-ignore + const val = getValue(chi[i]); + if (val == null) { + return [token]; + } + all.push(val); + value += val * val; + } + return [ + { + ...ref, + val: Math.sqrt(value).toFixed(rem(...all)) + } + ]; + } + case 'atan2': + case 'pow': + case 'rem': + case 'mod': { + const chi = values.filter(t => ![EnumToken.WhitespaceTokenType, EnumToken.CommentTokenType].includes(t.typ)); + if (chi.length != 3 || chi[1].typ != EnumToken.CommaTokenType) { + return [token]; + } + if (token.val == 'pow' && (chi[0].typ != EnumToken.NumberTokenType || chi[2].typ != EnumToken.NumberTokenType)) { + return [token]; + } + if (['rem', 'mod'].includes(token.val) && + (chi[0].typ != chi[2].typ) || ('unit' in chi[0] && 'unit' in chi[2] && + chi[0].unit != chi[2].unit)) { + return [token]; + } + // https://developer.mozilla.org/en-US/docs/Web/CSS/mod + const v1 = evaluate([chi[0]]); + const v2 = evaluate([chi[2]]); + const types = [EnumToken.PercentageTokenType, EnumToken.DimensionTokenType, EnumToken.AngleTokenType, EnumToken.NumberTokenType, EnumToken.LengthTokenType, EnumToken.TimeTokenType, EnumToken.FrequencyTokenType, EnumToken.ResolutionTokenType]; + if (v1.length != 1 || v2.length != 1 || !types.includes(v1[0].typ) || !types.includes(v2[0].typ) || v1[0].unit != v2[0].unit) { + return [token]; + } + // @ts-ignore + const val1 = getValue(v1[0]); + // @ts-ignore + const val2 = getValue(v2[0]); + if (val1 == null || val2 == null || (v1[0].typ != v2[0].typ && val1 != 0 && val2 != 0)) { + return [token]; + } + if (token.val == 'rem') { + if (val2 == 0) { + return [token]; + } + return [ + { + ...v1[0], + val: (val1 % val2).toFixed(rem(val1, val2)) + } + ]; + } + if (token.val == 'pow') { + return [ + { + ...v1[0], + val: String(Math.pow(val1, val2)) + } + ]; + } + if (token.val == 'atan2') { + return [ + { + ...{}, ...v1[0], + val: String(Math.atan2(val1, val2)) + } + ]; + } + return [ + { + ...v1[0], + val: String(val2 == 0 ? val1 : val1 - (Math.floor(val1 / val2) * val2)) + } + ]; + } + case 'clamp': + token.chi = values; + return [token]; + case 'log': + case 'round': + case 'min': + case 'max': + { + const strategy = token.val == 'round' && values[0]?.typ == EnumToken.IdenTokenType ? values.shift().val : null; + const valuesMap = new Map; + for (const curr of values) { + if (curr.typ == EnumToken.CommaTokenType || curr.typ == EnumToken.WhitespaceTokenType || curr.typ == EnumToken.CommentTokenType) { + continue; + } + const result = evaluate([curr]); + if (result.length != 1 || result[0].typ == EnumToken.FunctionTokenType) { + return [token]; + } + const key = result[0].typ + ('unit' in result[0] ? result[0].unit : ''); + if (!valuesMap.has(key)) { + valuesMap.set(key, []); + } + valuesMap.get(key).push(result[0]); + } + if (valuesMap.size == 1) { + const values = valuesMap.values().next().value; + console.error({ values }); + if (token.val == 'log') { + if (values[0].typ != EnumToken.NumberTokenType || values.length > 2) { + return [token]; + } + const val1 = getValue(values[0]); + const val2 = values.length == 2 ? getValue(values[1]) : null; + if (values.length == 1) { + return [ + { + ...values[0], + val: String(Math.log(val1)) + } + ]; + } + return [ + { + ...values[0], + val: String(Math.log(val1) / Math.log(val2)) + } + ]; + } + if (token.val == 'min' || token.val == 'max') { + let val = getValue(values[0]); + let val2 = val; + let ret = values[0]; + for (const curr of values.slice(1)) { + val2 = getValue(curr); + if (val2 < val && token.val == 'min') { + val = val2; + ret = curr; + } + else if (val2 > val && token.val == 'max') { + val = val2; + ret = curr; + } + } + return [ret]; + } + if (token.val == 'round') { + let val = getValue(values[0]); + let val2 = getValue(values[1]); + if (Number.isNaN(val) || Number.isNaN(val2)) { + return [token]; + } + if (strategy == null || strategy == 'down') { + val = val - (val % val2); + } + else { + val = strategy == 'to-zero' ? Math.trunc(val / val2) * val2 : (strategy == 'nearest' ? Math.round(val / val2) * val2 : Math.ceil(val / val2) * val2); + } + // @ts-ignore + return [{ ...values[0], val: String(val) }]; + } + } + } + return [token]; + } + return [token]; } /** * convert BinaryExpression into an array @@ -155,7 +444,11 @@ function evaluateExpression(token) { return doEvaluate(token.l, token.r, token.op); } function isScalarToken(token) { - return 'unit' in token || [EnumToken.NumberTokenType, EnumToken.FractionTokenType, EnumToken.PercentageTokenType].includes(token.typ); + return 'unit' in token || + (token.typ == EnumToken.FunctionTokenType && mathFuncs.includes(token.val)) || + // @ts-ignore + (token.typ == EnumToken.IdenTokenType && typeof Math[token.val.toUpperCase()] == 'number') || + [EnumToken.NumberTokenType, EnumToken.FractionTokenType, EnumToken.PercentageTokenType].includes(token.typ); } /** * @@ -225,4 +518,4 @@ function factor(tokens, ops) { return tokens; } -export { evaluate }; +export { evaluate, evaluateFunc }; diff --git a/dist/lib/ast/math/math.js b/dist/lib/ast/math/math.js index 4eb91767..5bc758ce 100644 --- a/dist/lib/ast/math/math.js +++ b/dist/lib/ast/math/math.js @@ -87,9 +87,15 @@ function compute(a, b, op) { r: { typ: EnumToken.NumberTokenType, val: reduceNumber(a2[1]) } }; } +function rem(...a) { + if (a.some((i) => !Number.isInteger(i))) { + return a.reduce((a, b) => Math.max(a, String(b).split('.')[1]?.length ?? 0), 0); + } + return 0; +} function simplify(a, b) { const g = gcd(a, b); return g > 1 ? [a / g, b / g] : [a, b]; } -export { compute, gcd, simplify }; +export { compute, gcd, rem, simplify }; diff --git a/dist/lib/ast/minify.js b/dist/lib/ast/minify.js index d2e7753d..1f894b8f 100644 --- a/dist/lib/ast/minify.js +++ b/dist/lib/ast/minify.js @@ -4,6 +4,7 @@ import { walkValues } from './walk.js'; import { replaceCompound } from './expand.js'; import { isWhiteSpace, isIdent, isFunction, isIdentStart } from '../syntax/syntax.js'; import '../parser/utils/config.js'; +import '../renderer/color/utils/constants.js'; import { eq } from '../parser/utils/eq.js'; import { renderToken, doRender } from '../renderer/render.js'; import * as index from './features/index.js'; diff --git a/dist/lib/ast/walk.js b/dist/lib/ast/walk.js index 7f0295c7..d2936358 100644 --- a/dist/lib/ast/walk.js +++ b/dist/lib/ast/walk.js @@ -1,5 +1,10 @@ import { EnumToken } from './types.js'; +var WalkerValueEvent; +(function (WalkerValueEvent) { + WalkerValueEvent[WalkerValueEvent["Enter"] = 0] = "Enter"; + WalkerValueEvent[WalkerValueEvent["Leave"] = 1] = "Leave"; +})(WalkerValueEvent || (WalkerValueEvent = {})); function* walk(node, filter) { const parents = [node]; const root = node; @@ -29,39 +34,91 @@ function* walk(node, filter) { } } function* walkValues(values, root = null, filter) { + // const set = new Set(); const stack = values.slice(); const map = new Map; - let value; let previous = null; - while ((value = stack.shift())) { + // let parent: FunctionToken | ParensToken | BinaryExpressionToken | null = null; + if (filter != null && typeof filter == 'function') { + filter = { + event: WalkerValueEvent.Enter, + fn: filter + }; + } + else if (filter == null) { + filter = { + event: WalkerValueEvent.Enter + }; + } + while (stack.length > 0) { + let value = stack.shift(); let option = null; - if (filter != null) { - option = filter(value); - if (option === 'ignore') { - continue; - } - if (option === 'stop') { - break; + if (filter.fn != null && filter.event == WalkerValueEvent.Enter) { + const isValid = filter.type == null || value.typ == filter.type || + (Array.isArray(filter.type) && filter.type.includes(value.typ)) || + (typeof filter.type == 'function' && filter.type(value)); + if (isValid) { + option = filter.fn(value, map.get(value) ?? root, WalkerValueEvent.Enter); + if (option === 'ignore') { + continue; + } + if (option === 'stop') { + break; + } + // @ts-ignore + if (option != null && typeof option == 'object' && 'typ' in option) { + map.set(option, map.get(value) ?? root); + } } } // @ts-ignore - if (option !== 'children') { - // @ts-ignore - yield { value, parent: map.get(value), previousValue: previous, nextValue: stack[0] ?? null, root }; + if (filter.event == WalkerValueEvent.Enter && option !== 'children') { + yield { + value, + parent: map.get(value) ?? root, + previousValue: previous, + nextValue: stack[0] ?? null, + // @ts-ignore + root: root ?? null + }; } if (option !== 'ignore-children' && 'chi' in value) { - for (const child of value.chi.slice()) { + const sliced = value.chi.slice(); + for (const child of sliced) { map.set(child, value); } - stack.unshift(...value.chi); + stack.unshift(...sliced); } else if (value.typ == EnumToken.BinaryExpressionTokenType) { - map.set(value.l, value); - map.set(value.r, value); + map.set(value.l, map.get(value) ?? root); + map.set(value.r, map.get(value) ?? root); stack.unshift(value.l, value.r); } + if (filter.event == WalkerValueEvent.Leave && filter.fn != null) { + const isValid = filter.type == null || value.typ == filter.type || + (Array.isArray(filter.type) && filter.type.includes(value.typ)) || + (typeof filter.type == 'function' && filter.type(value)); + if (isValid) { + option = filter.fn(value, map.get(value), WalkerValueEvent.Leave); + // @ts-ignore + if (option != null && 'typ' in option) { + map.set(option, map.get(value) ?? root); + } + } + } + // @ts-ignore + if (filter.event == WalkerValueEvent.Leave && option !== 'children') { + yield { + value, + parent: map.get(value) ?? root, + previousValue: previous, + nextValue: stack[0] ?? null, + // @ts-ignore + root: root ?? null + }; + } previous = value; } } -export { walk, walkValues }; +export { WalkerValueEvent, walk, walkValues }; diff --git a/dist/lib/parser/declaration/list.js b/dist/lib/parser/declaration/list.js index 774ea2f7..13e7b475 100644 --- a/dist/lib/parser/declaration/list.js +++ b/dist/lib/parser/declaration/list.js @@ -2,6 +2,7 @@ import { PropertySet } from './set.js'; import { getConfig } from '../utils/config.js'; import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import { parseString } from '../parse.js'; import '../../renderer/color/utils/constants.js'; import '../../renderer/sourcemap/lib/encode.js'; diff --git a/dist/lib/parser/declaration/map.js b/dist/lib/parser/declaration/map.js index cff5a6d2..960d32d8 100644 --- a/dist/lib/parser/declaration/map.js +++ b/dist/lib/parser/declaration/map.js @@ -3,6 +3,7 @@ import { getConfig } from '../utils/config.js'; import { matchType } from '../utils/type.js'; import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import { parseString } from '../parse.js'; import { renderToken } from '../../renderer/render.js'; import '../../renderer/color/utils/constants.js'; diff --git a/dist/lib/parser/declaration/set.js b/dist/lib/parser/declaration/set.js index fe15aa37..f687b19b 100644 --- a/dist/lib/parser/declaration/set.js +++ b/dist/lib/parser/declaration/set.js @@ -1,6 +1,7 @@ import { eq } from '../utils/eq.js'; import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../parse.js'; import { isLength } from '../../syntax/syntax.js'; import '../utils/config.js'; diff --git a/dist/lib/parser/parse.js b/dist/lib/parser/parse.js index b81bb044..d0a28622 100644 --- a/dist/lib/parser/parse.js +++ b/dist/lib/parser/parse.js @@ -4,9 +4,9 @@ import { EnumToken, funcLike, ValidationLevel } from '../ast/types.js'; import { minify, definedPropertySettings, combinators } from '../ast/minify.js'; import { walkValues, walk } from '../ast/walk.js'; import { expand } from '../ast/expand.js'; +import { COLORS_NAMES, systemColors, deprecatedSystemColors, mathFuncs } from '../renderer/color/utils/constants.js'; import { parseDeclaration } from './utils/declaration.js'; import { renderToken } from '../renderer/render.js'; -import { COLORS_NAMES, systemColors, deprecatedSystemColors } from '../renderer/color/utils/constants.js'; import { tokenize } from './tokenize.js'; import { validateSelector } from '../validation/selector.js'; @@ -764,9 +764,6 @@ function parseString(src, options = { location: false }) { })); } function getTokenType(val, hint) { - // if (val === '' && hint == null) { - // throw new Error('empty string?'); - // } if (hint != null) { return enumTokenHints.has(hint) ? { typ: hint } : { typ: hint, val }; } @@ -944,11 +941,7 @@ function parseTokens(tokens, options = {}) { if (t.typ == EnumToken.WhitespaceTokenType && ((i == 0 || i + 1 == tokens.length || [EnumToken.CommaTokenType, EnumToken.GteTokenType, EnumToken.LteTokenType, EnumToken.ColumnCombinatorTokenType].includes(tokens[i + 1].typ)) || - (i > 0 && - // tokens[i + 1]?.typ != Literal || - // funcLike.includes(tokens[i - 1].typ) && - // !['var', 'calc'].includes((tokens[i - 1]).val)))) && - trimWhiteSpace.includes(tokens[i - 1].typ)))) { + (i > 0 && trimWhiteSpace.includes(tokens[i - 1].typ)))) { tokens.splice(i--, 1); continue; } @@ -1141,7 +1134,7 @@ function parseTokens(tokens, options = {}) { // @ts-ignore parseTokens(t.chi, options); } - if (t.typ == EnumToken.FunctionTokenType && t.val == 'calc') { + if (t.typ == EnumToken.FunctionTokenType && mathFuncs.includes(t.val)) { for (const { value, parent } of walkValues(t.chi)) { if (value.typ == EnumToken.WhitespaceTokenType) { const p = (parent ?? t); diff --git a/dist/lib/parser/tokenize.js b/dist/lib/parser/tokenize.js index c24d6aaa..d73ce61e 100644 --- a/dist/lib/parser/tokenize.js +++ b/dist/lib/parser/tokenize.js @@ -1,5 +1,6 @@ import { EnumToken } from '../ast/types.js'; import '../ast/minify.js'; +import '../ast/walk.js'; import './parse.js'; import { isWhiteSpace, isNewLine, isDigit, isNonPrintable } from '../syntax/syntax.js'; import './utils/config.js'; diff --git a/dist/lib/parser/utils/type.js b/dist/lib/parser/utils/type.js index 7dbcfa1b..48cf0c31 100644 --- a/dist/lib/parser/utils/type.js +++ b/dist/lib/parser/utils/type.js @@ -1,12 +1,11 @@ import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../parse.js'; -import '../../renderer/color/utils/constants.js'; +import { mathFuncs } from '../../renderer/color/utils/constants.js'; import '../../renderer/sourcemap/lib/encode.js'; import './config.js'; -// https://www.w3.org/TR/css-values-4/#math-function -const funcList = ['clamp', 'calc']; function matchType(val, properties) { if (val.typ == EnumToken.IdenTokenType && properties.keywords.includes(val.val) || // @ts-ignore @@ -22,8 +21,8 @@ function matchType(val, properties) { }); } if (val.typ == EnumToken.FunctionTokenType) { - if (funcList.includes(val.val)) { - return val.chi.every(((t) => [EnumToken.LiteralTokenType, EnumToken.CommaTokenType, EnumToken.WhitespaceTokenType, EnumToken.StartParensTokenType, EnumToken.EndParensTokenType].includes(t.typ) || matchType(t, properties))); + if (mathFuncs.includes(val.val)) { + return val.chi.every(((t) => [EnumToken.Add, EnumToken.Mul, EnumToken.Div, EnumToken.Sub, EnumToken.LiteralTokenType, EnumToken.CommaTokenType, EnumToken.WhitespaceTokenType, EnumToken.DimensionTokenType, EnumToken.NumberTokenType, EnumToken.LengthTokenType, EnumToken.AngleTokenType, EnumToken.PercentageTokenType, EnumToken.ResolutionTokenType, EnumToken.TimeTokenType, EnumToken.BinaryExpressionTokenType].includes(t.typ) || matchType(t, properties))); } // match type defined like function 'symbols()', 'url()', 'attr()' etc. // return properties.types.includes((val).val + '()') @@ -31,4 +30,4 @@ function matchType(val, properties) { return false; } -export { funcList, matchType }; +export { matchType }; diff --git a/dist/lib/renderer/color/a98rgb.js b/dist/lib/renderer/color/a98rgb.js index 64622405..41d37415 100644 --- a/dist/lib/renderer/color/a98rgb.js +++ b/dist/lib/renderer/color/a98rgb.js @@ -3,6 +3,7 @@ import { multiplyMatrices } from './utils/matrix.js'; import './utils/constants.js'; import '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { srgb2xyz } from './xyz.js'; import '../sourcemap/lib/encode.js'; diff --git a/dist/lib/renderer/color/color.js b/dist/lib/renderer/color/color.js index f6664e48..f4298178 100644 --- a/dist/lib/renderer/color/color.js +++ b/dist/lib/renderer/color/color.js @@ -1,5 +1,6 @@ import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { srgb2rgb, lch2rgb, lab2rgb, oklch2rgb, oklab2rgb, hwb2rgb, hsl2rgb, hex2rgb } from './rgb.js'; import { srgb2hsl, lch2hsl, lab2hsl, oklch2hsl, oklab2hsl, hwb2hsl, hex2hsl, rgb2hsl } from './hsl.js'; diff --git a/dist/lib/renderer/color/colormix.js b/dist/lib/renderer/color/colormix.js index 20f59fe3..a000b430 100644 --- a/dist/lib/renderer/color/colormix.js +++ b/dist/lib/renderer/color/colormix.js @@ -1,12 +1,13 @@ import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { isRectangularOrthogonalColorspace, isPolarColorspace } from '../../syntax/syntax.js'; import '../../parser/utils/config.js'; -import { getNumber } from './color.js'; -import { srgb2rgb } from './rgb.js'; import './utils/constants.js'; import { getComponents } from './utils/components.js'; +import { getNumber } from './color.js'; +import { srgb2rgb } from './rgb.js'; import { srgb2hwb } from './hwb.js'; import { srgb2hsl } from './hsl.js'; import { srgbvalues, srgb2lsrgbvalues } from './srgb.js'; diff --git a/dist/lib/renderer/color/hex.js b/dist/lib/renderer/color/hex.js index 2465b0a2..bc8deac8 100644 --- a/dist/lib/renderer/color/hex.js +++ b/dist/lib/renderer/color/hex.js @@ -1,5 +1,6 @@ import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { getNumber, minmax } from './color.js'; import { hsl2rgb, hwb2rgb, cmyk2rgb, oklab2rgb, oklch2rgb, lab2rgb, lch2rgb } from './rgb.js'; diff --git a/dist/lib/renderer/color/hsl.js b/dist/lib/renderer/color/hsl.js index 80e7558c..ab59f9d2 100644 --- a/dist/lib/renderer/color/hsl.js +++ b/dist/lib/renderer/color/hsl.js @@ -5,6 +5,7 @@ import './utils/constants.js'; import { getComponents } from './utils/components.js'; import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { hslvalues } from './srgb.js'; import '../sourcemap/lib/encode.js'; diff --git a/dist/lib/renderer/color/hwb.js b/dist/lib/renderer/color/hwb.js index 52dcbb20..f28a0293 100644 --- a/dist/lib/renderer/color/hwb.js +++ b/dist/lib/renderer/color/hwb.js @@ -4,6 +4,7 @@ import { getComponents } from './utils/components.js'; import { getNumber, getAngle } from './color.js'; import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { lab2srgb, lch2srgb, oklab2srgb, oklch2srgb } from './srgb.js'; import '../sourcemap/lib/encode.js'; diff --git a/dist/lib/renderer/color/lab.js b/dist/lib/renderer/color/lab.js index 67c13a00..d62ff00a 100644 --- a/dist/lib/renderer/color/lab.js +++ b/dist/lib/renderer/color/lab.js @@ -7,6 +7,7 @@ import { OKLab_to_XYZ, getOKLABComponents } from './oklab.js'; import { getNumber } from './color.js'; import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import '../sourcemap/lib/encode.js'; import '../../parser/utils/config.js'; diff --git a/dist/lib/renderer/color/lch.js b/dist/lib/renderer/color/lch.js index a16a0871..eeed9c9c 100644 --- a/dist/lib/renderer/color/lch.js +++ b/dist/lib/renderer/color/lch.js @@ -3,6 +3,7 @@ import { getComponents } from './utils/components.js'; import { getNumber, getAngle } from './color.js'; import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { srgb2lab, xyz2lab, hex2lab, rgb2lab, hsl2lab, hwb2lab, getLABComponents, oklab2lab, oklch2lab } from './lab.js'; import '../sourcemap/lib/encode.js'; diff --git a/dist/lib/renderer/color/oklab.js b/dist/lib/renderer/color/oklab.js index ffb1f4ba..7eb36f41 100644 --- a/dist/lib/renderer/color/oklab.js +++ b/dist/lib/renderer/color/oklab.js @@ -5,6 +5,7 @@ import { srgb2lsrgbvalues, hex2srgb, rgb2srgb, hsl2srgb, hwb2srgb, lab2srgb, lch import { getNumber } from './color.js'; import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { lch2labvalues } from './lab.js'; import { getOKLCHComponents } from './oklch.js'; diff --git a/dist/lib/renderer/color/oklch.js b/dist/lib/renderer/color/oklch.js index a6719c86..20bf9b56 100644 --- a/dist/lib/renderer/color/oklch.js +++ b/dist/lib/renderer/color/oklch.js @@ -3,6 +3,7 @@ import { getComponents } from './utils/components.js'; import { getNumber, getAngle } from './color.js'; import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { lab2lchvalues } from './lch.js'; import { srgb2oklab, hex2oklab, rgb2oklab, hsl2oklab, hwb2oklab, lab2oklab, lch2oklab, getOKLABComponents } from './oklab.js'; diff --git a/dist/lib/renderer/color/p3.js b/dist/lib/renderer/color/p3.js index 57404781..91e4d58f 100644 --- a/dist/lib/renderer/color/p3.js +++ b/dist/lib/renderer/color/p3.js @@ -3,6 +3,7 @@ import { multiplyMatrices } from './utils/matrix.js'; import './utils/constants.js'; import '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { srgb2xyz } from './xyz.js'; import '../sourcemap/lib/encode.js'; diff --git a/dist/lib/renderer/color/rec2020.js b/dist/lib/renderer/color/rec2020.js index f99e077e..31b2dfcf 100644 --- a/dist/lib/renderer/color/rec2020.js +++ b/dist/lib/renderer/color/rec2020.js @@ -3,6 +3,7 @@ import { multiplyMatrices } from './utils/matrix.js'; import './utils/constants.js'; import '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { srgb2xyz } from './xyz.js'; import '../sourcemap/lib/encode.js'; diff --git a/dist/lib/renderer/color/relativecolor.js b/dist/lib/renderer/color/relativecolor.js index e7f47fd1..1137e110 100644 --- a/dist/lib/renderer/color/relativecolor.js +++ b/dist/lib/renderer/color/relativecolor.js @@ -4,8 +4,8 @@ import '../../ast/minify.js'; import { walkValues } from '../../ast/walk.js'; import '../../parser/parse.js'; import { reduceNumber } from '../render.js'; -import { colorRange } from './utils/constants.js'; -import { evaluate } from '../../ast/math/expression.js'; +import { colorRange, mathFuncs } from './utils/constants.js'; +import { evaluateFunc, evaluate } from '../../ast/math/expression.js'; import '../../parser/utils/config.js'; function parseRelativeColor(relativeKeys, original, rExp, gExp, bExp, aExp) { @@ -47,7 +47,7 @@ function parseRelativeColor(relativeKeys, original, rExp, gExp, bExp, aExp) { val: '1' } : aExp) }; - return computeComponentValue(keys, values); + return computeComponentValue(keys, converted, values); } function getValue(t, converted, component) { if (t == null) { @@ -66,7 +66,7 @@ function getValue(t, converted, component) { } return t; } -function computeComponentValue(expr, values) { +function computeComponentValue(expr, converted, values) { for (const object of [values, expr]) { if ('h' in object) { // normalize hue @@ -107,34 +107,29 @@ function computeComponentValue(expr, values) { expr[key] = values[exp.val]; } } - else if (exp.typ == EnumToken.FunctionTokenType && exp.val == 'calc') { - for (let { value, parent } of walkValues(exp.chi)) { - if (value.typ == EnumToken.IdenTokenType) { - if (!(value.val in values)) { + else if (exp.typ == EnumToken.FunctionTokenType && mathFuncs.includes(exp.val)) { + for (let { value, parent } of walkValues(exp.chi, exp)) { + if (parent == null) { + parent = exp; + } + if (value.typ == EnumToken.PercentageTokenType) { + replaceValue(parent, value, getValue(value, converted, key)); + } + else if (value.typ == EnumToken.IdenTokenType) { + // @ts-ignore + if (!(value.val in values || typeof Math[value.val.toUpperCase()] == 'number')) { return null; } - if (parent == null) { - parent = exp; - } - if (parent.typ == EnumToken.BinaryExpressionTokenType) { - if (parent.l == value) { - parent.l = values[value.val]; - } - else { - parent.r = values[value.val]; - } - } - else { - for (let i = 0; i < parent.chi.length; i++) { - if (parent.chi[i] == value) { - parent.chi.splice(i, 1, values[value.val]); - break; - } - } - } + // @ts-ignore + replaceValue(parent, value, values[value.val] ?? { + typ: EnumToken.NumberTokenType, + // @ts-ignore + val: '' + Math[value.val.toUpperCase()] + // @ts-ignore + }); } } - const result = evaluate(exp.chi); + const result = exp.typ == EnumToken.FunctionTokenType && mathFuncs.includes(exp.val) && exp.val != 'calc' ? evaluateFunc(exp) : evaluate(exp.chi); if (result.length == 1 && result[0].typ != EnumToken.BinaryExpressionTokenType) { expr[key] = result[0]; } @@ -145,5 +140,33 @@ function computeComponentValue(expr, values) { } return expr; } +function replaceValue(parent, value, newValue) { + if (parent.typ == EnumToken.BinaryExpressionTokenType) { + if (parent.l == value) { + parent.l = newValue; + } + else { + parent.r = newValue; + } + } + else { + for (let i = 0; i < parent.chi.length; i++) { + if (parent.chi[i] == value) { + parent.chi.splice(i, 1, newValue); + break; + } + if (parent.chi[i].typ == EnumToken.BinaryExpressionTokenType) { + if (parent.chi[i].l == value) { + parent.chi[i].l = newValue; + break; + } + else if (parent.chi[i].r == value) { + parent.chi[i].r = newValue; + break; + } + } + } + } +} export { parseRelativeColor }; diff --git a/dist/lib/renderer/color/rgb.js b/dist/lib/renderer/color/rgb.js index 38dd7aa3..2189731c 100644 --- a/dist/lib/renderer/color/rgb.js +++ b/dist/lib/renderer/color/rgb.js @@ -2,6 +2,7 @@ import { minmax } from './color.js'; import { COLORS_NAMES } from './utils/constants.js'; import '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { expandHexValue } from './hex.js'; import { hwb2srgb, hslvalues, hsl2srgbvalues, cmyk2srgb, oklab2srgb, oklch2srgb, lab2srgb, lch2srgb } from './srgb.js'; diff --git a/dist/lib/renderer/color/srgb.js b/dist/lib/renderer/color/srgb.js index 1e597510..46d525bc 100644 --- a/dist/lib/renderer/color/srgb.js +++ b/dist/lib/renderer/color/srgb.js @@ -3,6 +3,7 @@ import { getComponents } from './utils/components.js'; import { color2srgbvalues, getNumber, getAngle } from './color.js'; import { EnumToken } from '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { expandHexValue } from './hex.js'; import { lch2labvalues, getLABComponents, Lab_to_sRGB } from './lab.js'; diff --git a/dist/lib/renderer/color/utils/components.js b/dist/lib/renderer/color/utils/components.js index cb222eb5..ae522d1e 100644 --- a/dist/lib/renderer/color/utils/components.js +++ b/dist/lib/renderer/color/utils/components.js @@ -1,5 +1,6 @@ import { EnumToken } from '../../../ast/types.js'; import '../../../ast/minify.js'; +import '../../../ast/walk.js'; import '../../../parser/parse.js'; import { COLORS_NAMES } from './constants.js'; import { expandHexValue } from '../hex.js'; diff --git a/dist/lib/renderer/color/utils/constants.js b/dist/lib/renderer/color/utils/constants.js index c1edbc49..7f23ff46 100644 --- a/dist/lib/renderer/color/utils/constants.js +++ b/dist/lib/renderer/color/utils/constants.js @@ -1,5 +1,6 @@ import { EnumToken } from '../../../ast/types.js'; import '../../../ast/minify.js'; +import '../../../ast/walk.js'; import '../../../parser/parse.js'; import '../../sourcemap/lib/encode.js'; import '../../../parser/utils/config.js'; @@ -26,6 +27,8 @@ const colorRange = { b: [0, 0.4] } }; +// https://www.w3.org/TR/css-values-4/#math-function +const mathFuncs = ['calc', 'clamp', 'min', 'max', 'round', 'mod', 'rem', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'pow', 'sqrt', 'hypot', 'log', 'exp', 'abs', 'sign']; const colorFuncColorSpace = ['srgb', 'srgb-linear', 'display-p3', 'prophoto-rgb', 'a98-rgb', 'rec2020', 'xyz', 'xyz-d65', 'xyz-d50']; ({ typ: EnumToken.IdenTokenType, val: 'none' }); const D50 = [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585]; @@ -193,4 +196,4 @@ const NAMES_COLORS = Object.seal(Object.entries(COLORS_NAMES).reduce((acc, [key, return acc; }, Object.create(null))); -export { COLORS_NAMES, D50, NAMES_COLORS, colorFuncColorSpace, colorRange, deprecatedSystemColors, e, k, systemColors }; +export { COLORS_NAMES, D50, NAMES_COLORS, colorFuncColorSpace, colorRange, deprecatedSystemColors, e, k, mathFuncs, systemColors }; diff --git a/dist/lib/renderer/color/xyz.js b/dist/lib/renderer/color/xyz.js index cc371a28..df61779e 100644 --- a/dist/lib/renderer/color/xyz.js +++ b/dist/lib/renderer/color/xyz.js @@ -2,6 +2,7 @@ import { multiplyMatrices } from './utils/matrix.js'; import './utils/constants.js'; import '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { lsrgb2srgbvalues, srgb2lsrgbvalues } from './srgb.js'; import '../sourcemap/lib/encode.js'; diff --git a/dist/lib/renderer/color/xyzd50.js b/dist/lib/renderer/color/xyzd50.js index 5dcdee0b..ac436db6 100644 --- a/dist/lib/renderer/color/xyzd50.js +++ b/dist/lib/renderer/color/xyzd50.js @@ -2,6 +2,7 @@ import { multiplyMatrices } from './utils/matrix.js'; import './utils/constants.js'; import '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import { xyz2lab } from './lab.js'; import { lab2lchvalues } from './lch.js'; diff --git a/dist/lib/renderer/render.js b/dist/lib/renderer/render.js index 90d4dd26..615c0026 100644 --- a/dist/lib/renderer/render.js +++ b/dist/lib/renderer/render.js @@ -1,9 +1,10 @@ import { getAngle, color2srgbvalues, clamp } from './color/color.js'; -import { colorFuncColorSpace, COLORS_NAMES } from './color/utils/constants.js'; +import { mathFuncs, colorFuncColorSpace, COLORS_NAMES } from './color/utils/constants.js'; import { getComponents } from './color/utils/components.js'; import { reduceHexValue, srgb2hexvalues, rgb2hex, hsl2hex, hwb2hex, cmyk2hex, oklab2hex, oklch2hex, lab2hex, lch2hex } from './color/hex.js'; import { EnumToken } from '../ast/types.js'; import '../ast/minify.js'; +import '../ast/walk.js'; import { expand } from '../ast/expand.js'; import { colorMix } from './color/colormix.js'; import { parseRelativeColor } from './color/relativecolor.js'; @@ -45,6 +46,7 @@ function doRender(data, options = {}) { ...(options.minify ?? true ? { indent: '', newLine: '', + removeEmpty: true, removeComments: true } : { indent: ' ', @@ -193,6 +195,9 @@ function renderAstNode(data, options, sourcemap, position, errors, reducer, cach } return `${css}${options.newLine}${indentSub}${str}`; }, ''); + if (options.removeEmpty && children === '') { + return ''; + } if (children.endsWith(';')) { children = children.slice(0, -1); } @@ -203,6 +208,7 @@ function renderAstNode(data, options, sourcemap, position, errors, reducer, cach case EnumToken.InvalidRuleTokenType: return ''; default: + // return renderToken(data as Token, options, cache, reducer, errors); throw new Error(`render: unexpected token ${JSON.stringify(data, null, 1)}`); } return ''; @@ -394,12 +400,11 @@ function renderToken(token, options = {}, cache = Object.create(null), reducer, case EnumToken.TimelineFunctionTokenType: case EnumToken.GridTemplateFuncTokenType: if (token.typ == EnumToken.FunctionTokenType && - token.val == 'calc' && + mathFuncs.includes(token.val) && token.chi.length == 1 && - token.chi[0].typ != EnumToken.BinaryExpressionTokenType && - token.chi[0].typ != EnumToken.FractionTokenType && + ![EnumToken.BinaryExpressionTokenType, EnumToken.FractionTokenType, EnumToken.IdenTokenType].includes(token.chi[0].typ) && + // @ts-ignore token.chi[0].val?.typ != EnumToken.FractionTokenType) { - // calc(200px) => 200px return token.chi.reduce((acc, curr) => acc + renderToken(curr, options, cache, reducer), ''); } // @ts-ignore diff --git a/dist/lib/syntax/syntax.js b/dist/lib/syntax/syntax.js index b9cae3af..0a2c05db 100644 --- a/dist/lib/syntax/syntax.js +++ b/dist/lib/syntax/syntax.js @@ -1,9 +1,10 @@ import { colorsFunc } from '../renderer/render.js'; import { EnumToken } from '../ast/types.js'; import '../ast/minify.js'; +import '../ast/walk.js'; import '../parser/parse.js'; +import { COLORS_NAMES, mathFuncs } from '../renderer/color/utils/constants.js'; import '../parser/utils/config.js'; -import { COLORS_NAMES } from '../renderer/color/utils/constants.js'; // https://www.w3.org/TR/CSS21/syndata.html#syntax // https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-ident-token @@ -89,7 +90,7 @@ function isColor(token) { return false; } } - if (children[i].typ == EnumToken.FunctionTokenType && !['calc'].includes(children[i].val)) { + if (children[i].typ == EnumToken.FunctionTokenType && !mathFuncs.includes(children[i].val)) { return false; } } @@ -184,7 +185,7 @@ function isColor(token) { } continue; } - if (v.typ == EnumToken.FunctionTokenType && (v.val == 'calc' || v.val == 'var' || colorsFunc.includes(v.val))) { + if (v.typ == EnumToken.FunctionTokenType && (mathFuncs.includes(v.val) || v.val == 'var' || colorsFunc.includes(v.val))) { continue; } if (![EnumToken.ColorTokenType, EnumToken.IdenTokenType, EnumToken.NumberTokenType, EnumToken.AngleTokenType, EnumToken.PercentageTokenType, EnumToken.CommaTokenType, EnumToken.WhitespaceTokenType, EnumToken.LiteralTokenType].includes(v.typ)) { diff --git a/dist/lib/validation/parser/parse.js b/dist/lib/validation/parser/parse.js index da346e53..cb29d082 100644 --- a/dist/lib/validation/parser/parse.js +++ b/dist/lib/validation/parser/parse.js @@ -1,6 +1,7 @@ import { ValidationTokenEnum } from './types.js'; import '../../ast/types.js'; import '../../ast/minify.js'; +import '../../ast/walk.js'; import '../../parser/parse.js'; import '../../parser/utils/config.js'; import '../../renderer/color/utils/constants.js'; diff --git a/dist/lib/validation/selector.js b/dist/lib/validation/selector.js index a40663b9..93ac99ca 100644 --- a/dist/lib/validation/selector.js +++ b/dist/lib/validation/selector.js @@ -1,5 +1,6 @@ import { EnumToken, ValidationLevel } from '../ast/types.js'; import '../ast/minify.js'; +import '../ast/walk.js'; import '../parser/parse.js'; import '../renderer/color/utils/constants.js'; import '../renderer/sourcemap/lib/encode.js'; diff --git a/jsr.json b/jsr.json index 54ab24ce..6aa28d72 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@tbela99/css-parser", - "version": "0.7.1", + "version": "0.8.0", "publish": { "include": [ "src", diff --git a/package.json b/package.json index 40361d3a..28609c10 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tbela99/css-parser", "description": "CSS parser for node and the browser", - "version": "0.7.1", + "version": "0.8.0", "exports": { ".": "./dist/node/index.js", "./umd": "./dist/index-umd-web.js", @@ -49,21 +49,21 @@ "homepage": "https://github.com/tbela99/css-parser#readme", "devDependencies": { "@esm-bundle/chai": "^4.3.4-fix.0", - "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.6", - "@types/chai": "^4.3.19", - "@types/mocha": "^10.0.8", - "@types/node": "^22.5.5", - "@types/web": "^0.0.163", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-typescript": "^12.1.2", + "@types/chai": "^5.0.1", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.2", + "@types/web": "^0.0.187", "@web/test-runner": "^0.19.0", "@web/test-runner-playwright": "^0.11.0", - "c8": "^10.1.2", - "mocha": "^10.7.3", - "playwright": "^1.47.1", - "rollup": "^4.21.3", + "c8": "^10.1.3", + "mocha": "^11.0.1", + "playwright": "^1.49.1", + "rollup": "^4.29.1", "rollup-plugin-dts": "^6.1.1", - "tslib": "^2.7.0" + "tslib": "^2.8.1" } } diff --git a/src/@types/index.d.ts b/src/@types/index.d.ts index 025c4d9b..16f36472 100644 --- a/src/@types/index.d.ts +++ b/src/@types/index.d.ts @@ -93,6 +93,7 @@ export declare interface ResolvedPath { export declare interface RenderOptions { minify?: boolean; + removeEmpty?: boolean; expandNestingRules?: boolean; preserveLicense?: boolean; sourcemap?: boolean; diff --git a/src/@types/walker.d.ts b/src/@types/walker.d.ts index 3f7e4847..9ae513de 100644 --- a/src/@types/walker.d.ts +++ b/src/@types/walker.d.ts @@ -1,7 +1,7 @@ import {AstNode, AstRuleList} from "./ast.d.ts"; import {BinaryExpressionToken, FunctionToken, ParensToken, Token} from "./token.d.ts"; -export declare type WalkerOption = 'ignore' | 'stop' | 'children' | 'ignore-children' | null; +export declare type WalkerOption = 'ignore' | 'stop' | 'children' | 'ignore-children' | Token | null; /** * returned value: * - 'ignore': ignore this node and its children @@ -18,18 +18,18 @@ export declare type WalkerFilter = (node: AstNode) => WalkerOption; * - 'children': walk the children and ignore the node itself * - 'ignore-children': walk the node and ignore children */ -export declare type WalkerValueFilter = (node: Token) => WalkerOption; +export declare type WalkerValueFilter = (node: AstNode | Token, parent: FunctionToken | ParensToken | BinaryExpressionToken, event?: WalkerValueEvent) => WalkerOption | null; export declare interface WalkResult { node: AstNode; parent?: AstRuleList; - root?: AstRuleList; + root?: AstNode; } export declare interface WalkAttributesResult { value: Token; previousValue: Token | null; - nextValue: AstNode | null; + nextValue: Token | null; root?: AstNode; parent: FunctionToken | ParensToken | BinaryExpressionToken | null; } \ No newline at end of file diff --git a/src/lib/ast/features/calc.ts b/src/lib/ast/features/calc.ts index 974bc993..32e1f042 100644 --- a/src/lib/ast/features/calc.ts +++ b/src/lib/ast/features/calc.ts @@ -1,14 +1,21 @@ import type { AstAtRule, AstDeclaration, + AstNode, AstRule, + BinaryExpressionToken, FunctionToken, MinifyOptions, - Token -} from "../../../@types"; + NumberToken, + ParensToken, + Token, + WalkerOption +} from "../../../@types/index.d.ts"; import {EnumToken} from "../types"; -import {walkValues} from "../walk"; +import {WalkerValueEvent, walkValues} from "../walk"; import {evaluate} from "../math"; +import {mathFuncs} from "../../renderer/color/utils"; +import {renderToken} from "../../renderer"; export class ComputeCalcExpressionFeature { @@ -50,39 +57,122 @@ export class ComputeCalcExpressionFeature { const set: Set = new Set; - for (const {value, parent} of walkValues((node).val)) { + for (const {value, parent} of walkValues((node).val, node, { - if (value != null && value.typ == EnumToken.FunctionTokenType && value.val == 'calc') { + event: WalkerValueEvent.Enter, + fn(node: AstNode | Token, parent: AstNode | FunctionToken | ParensToken | BinaryExpressionToken, event?: WalkerValueEvent): WalkerOption | null { - if (!set.has(parent)) { + if (parent != null && + (parent as AstDeclaration).typ == EnumToken.DeclarationNodeType && + (parent as AstDeclaration).val.length == 1 && + node.typ == EnumToken.FunctionTokenType && + mathFuncs.includes((node as FunctionToken).val) && + (node as FunctionToken).chi.length == 1 && + (node as FunctionToken).chi[0].typ == EnumToken.IdenTokenType) { + + return 'ignore' + } + + if ((node.typ == EnumToken.FunctionTokenType && (node as FunctionToken).val == 'var') || (!mathFuncs.includes((parent as FunctionToken).val) && [EnumToken.ColorTokenType, EnumToken.DeclarationNodeType, EnumToken.RuleNodeType, EnumToken.AtRuleNodeType, EnumToken.StyleSheetNodeType].includes(parent?.typ))) { + + return null; + } + + const slice: Token[] = (node.typ == EnumToken.FunctionTokenType ? (node as FunctionToken).chi : (node.typ == EnumToken.DeclarationNodeType ? (node).val : (node as FunctionToken).chi))?.slice(); + + if (slice != null && node.typ == EnumToken.FunctionTokenType && mathFuncs.includes((node as FunctionToken).val)) { + + // @ts-ignore + const cp: Token[] = (node.typ == EnumToken.FunctionTokenType && mathFuncs.includes(node.val) && node.val != 'calc' ? [node] : (node.typ == EnumToken.DeclarationNodeType ? (node).val : node.chi)).slice(); + const values: Token[] = evaluate(cp); + + const key = 'chi' in node ? 'chi' : 'val'; + + const str1: string = renderToken({...node, [key]: slice} as Token); + const str2: string = renderToken(node as Token); // values.reduce((acc: string, curr: Token): string => acc + renderToken(curr), ''); + + if (str1.length <= str2.length) { + + // @ts-ignore + node[key] = slice; + } else { + + // @ts-ignore + node[key] = values; + } + + return 'ignore'; + } + + return null; + } + } + )) { + + if (value != null && value.typ == EnumToken.FunctionTokenType && mathFuncs.includes(value.val)) { + + if (!set.has(value)) { set.add(value); - value.chi = evaluate(value.chi); - if (value.chi.length == 1 && value.chi[0].typ != EnumToken.BinaryExpressionTokenType) { + if (parent != null) { - if (parent != null) { + // @ts-ignore + const cp: Token[] = value.typ == EnumToken.FunctionTokenType && mathFuncs.includes(value.val) && value.val != 'calc' ? [value] : (value.typ == EnumToken.DeclarationNodeType ? (value).val : value.chi); + const values: Token[] = evaluate(cp); + + // @ts-ignore + const children: Token[] = parent.typ == EnumToken.DeclarationNodeType ? (parent).val : parent.chi; + + if (values.length == 1 && values[0].typ != EnumToken.BinaryExpressionTokenType) { if (parent.typ == EnumToken.BinaryExpressionTokenType) { if (parent.l == value) { - parent.l = value.chi[0]; + parent.l = values[0]; } else { - parent.r = value.chi[0]; + parent.r = values[0]; } } else { - for (let i = 0; i < parent.chi.length; i++) { + for (let i = 0; i < children.length; i++) { - if (parent.chi[i] == value) { + if (children[i] == value) { - parent.chi.splice(i, 1, value.chi[0]); + // @ts-ignore + children.splice(i, 1, !(parent.typ == EnumToken.FunctionTokenType && parent.val == 'calc') && typeof (values[0] as NumberToken).val != 'string' ? { + typ: EnumToken.FunctionTokenType, + val: 'calc', + chi: values + } : values[0]); break; } } } + + } else { + + for (let i = 0; i < children.length; i++) { + + if (children[i] == value) { + + if (parent.typ == EnumToken.FunctionTokenType && parent.val == 'calc') { + + children.splice(i, 1, ...values); + } else { + + children.splice(i, 1, { + typ: EnumToken.FunctionTokenType, + val: 'calc', + chi: values + }); + } + + break; + } + } } } } diff --git a/src/lib/ast/features/inlinecssvariables.ts b/src/lib/ast/features/inlinecssvariables.ts index df77a220..87168cf9 100644 --- a/src/lib/ast/features/inlinecssvariables.ts +++ b/src/lib/ast/features/inlinecssvariables.ts @@ -2,13 +2,14 @@ import type { AstAtRule, AstComment, AstDeclaration, AstRule, AstRuleList, - AstRuleStyleSheet, + AstRuleStyleSheet, CommentToken, FunctionToken, MinifyOptions, ParserOptions, VariableScopeInfo } from "../../../@types"; import {EnumToken} from "../types"; import {walkValues} from "../walk"; +import {renderToken} from "../../renderer"; function replace(node: AstDeclaration | AstRule | AstComment | AstRuleList, variableScope: Map) { @@ -178,7 +179,8 @@ export class InlineCssVariablesFeature { if (((parent.chi)[i]).typ == EnumToken.DeclarationNodeType && ((parent.chi)[i]).nam == info.node.nam) { - (parent.chi).splice(i, 1); + // @ts-ignore + (parent.chi).splice(i++, 1, {typ: EnumToken.CommentTokenType, val: `/* ${info.node.nam}: ${info.node.val.reduce((acc, curr) => acc + renderToken(curr), '')} */`} as CommentToken ); } } diff --git a/src/lib/ast/features/prefix.ts b/src/lib/ast/features/prefix.ts index 66ae7727..ec4fdfe2 100644 --- a/src/lib/ast/features/prefix.ts +++ b/src/lib/ast/features/prefix.ts @@ -110,7 +110,6 @@ function matchToken(token: Token, matches: ValidationToken[]): null | Token { case ValidationTokenEnum.Keyword: - console.error(matches[i], token); if (token.typ == EnumToken.IdenTokenType && token.val == (matches[i] as ValidationKeywordToken).val) { return token; @@ -172,12 +171,6 @@ function matchToken(token: Token, matches: ValidationToken[]): null | Token { } break; - - // default: - // - // console.error(token, matches[i]); - // throw new Error('bar bar'); - } } diff --git a/src/lib/ast/math/expression.ts b/src/lib/ast/math/expression.ts index ae8c375b..89494fee 100644 --- a/src/lib/ast/math/expression.ts +++ b/src/lib/ast/math/expression.ts @@ -1,15 +1,24 @@ import type { + AngleToken, BinaryExpressionNode, BinaryExpressionToken, + DimensionToken, FractionToken, + FrequencyToken, FunctionToken, + IdentToken, + LengthToken, LiteralToken, + NumberToken, ParensToken, + ResolutionToken, + TimeToken, Token } from "../../../@types/index.d.ts"; import {EnumToken} from "../types"; -import {compute} from "./math"; +import {compute, rem} from "./math"; import {reduceNumber} from "../../renderer"; +import {mathFuncs} from "../../renderer/color/utils"; /** * evaluate an array of tokens @@ -19,20 +28,65 @@ export function evaluate(tokens: Token[]): Token[] { let nodes: Token[]; - try { + if (tokens.length == 1 && tokens[0].typ == EnumToken.FunctionTokenType && (tokens[0]).val != 'calc' && mathFuncs.includes((tokens[0]).val)) { - nodes = inlineExpression(evaluateExpression(buildExpression(tokens))); + const chi: Token[][] = tokens[0].chi.reduce((acc: Token[][], t: Token): Token[][] => { + + if (acc.length == 0 || t.typ == EnumToken.CommaTokenType) { + + acc.push([]); + } + + if ([EnumToken.WhitespaceTokenType, EnumToken.CommaTokenType, EnumToken.CommaTokenType].includes(t.typ)) { + + return acc; + } + + acc.at(-1)!.push(t); + return acc; + }, [] as Token[][]); + + for (let i = 0; i < chi.length; i++) { + + chi[i] = evaluate(chi[i]); + } + + tokens[0].chi = chi.reduce((acc: Token[], t: Token[]): Token[] => { + + if (acc.length > 0) { + + acc.push({typ: EnumToken.CommaTokenType}); + } + + acc.push(...t); + + return acc; + }); + + return evaluateFunc(tokens[0]); } - catch (e) { + try { + + nodes = inlineExpression(evaluateExpression(buildExpression(tokens))); + } catch (e) { - // console.error({tokens}); - // console.error(e); return tokens; } if (nodes.length <= 1) { + // @ts-ignore + if (nodes.length == 1 && nodes[0].typ == EnumToken.IdenTokenType && typeof Math[(nodes[0]).val.toUpperCase()] == 'number') { + + return [{ + ...nodes[0], + // @ts-ignore + val: ('' + Math[(nodes[0]).val.toUpperCase()] as number), + typ: EnumToken.NumberTokenType + }]; + } + return nodes; } @@ -110,11 +164,47 @@ function doEvaluate(l: Token, r: Token, op: EnumToken.Add | EnumToken.Sub | Enum r }; - if (!isScalarToken(l) || !isScalarToken(r)) { + if (!isScalarToken(l) || !isScalarToken(r) || (l.typ == r.typ && 'unit' in l && 'unit' in r && l.unit != r.unit)) { return defaultReturn; } + if (l.typ == EnumToken.FunctionTokenType) { + + const val: Token[] = evaluateFunc(l); + + if (val.length == 1) { + + l = val[0]; + } else { + + return defaultReturn; + } + } + + if (r.typ == EnumToken.FunctionTokenType) { + + const val = evaluateFunc(r); + + if (val.length == 1) { + + r = val[0]; + } else { + + return defaultReturn; + } + } + + if (l.typ == EnumToken.FunctionTokenType) { + + const val = evaluateFunc(l); + + if (val.length == 1) { + + l = val[0]; + } + } + if ((op == EnumToken.Add || op == EnumToken.Sub)) { // @ts-ignore @@ -130,12 +220,16 @@ function doEvaluate(l: Token, r: Token, op: EnumToken.Add | EnumToken.Sub | Enum return defaultReturn; } - const typ: EnumToken = l.typ == EnumToken.NumberTokenType ? r.typ : (r.typ == EnumToken.NumberTokenType ? l.typ : (l.typ == EnumToken.PercentageTokenType ? r.typ : l.typ)); + let typ: EnumToken = l.typ == EnumToken.NumberTokenType ? r.typ : (r.typ == EnumToken.NumberTokenType ? l.typ : (l.typ == EnumToken.PercentageTokenType ? r.typ : l.typ)); // @ts-ignore - let v1 = typeof l.val == 'string' ? +l.val : l.val; - // @ts-ignore - let v2 = typeof r.val == 'string' ? +r.val : r.val; + let v1: number | Token | null = getValue(l); + let v2: number | Token | null = getValue(r as NumberToken | IdentToken | FunctionToken); + + if (v1 == null || v2 == null) { + + return defaultReturn; + } if (op == EnumToken.Mul) { @@ -161,12 +255,334 @@ function doEvaluate(l: Token, r: Token, op: EnumToken.Add | EnumToken.Sub | Enum // @ts-ignore const val: number | FractionToken = compute(v1, v2, op); - - return { + // typ = typeof val == 'number' ? EnumToken.NumberTokenType : EnumToken.FractionTokenType; + const token = { ...(l.typ == EnumToken.NumberTokenType ? r : l), typ, val: typeof val == 'number' ? reduceNumber(val) : val - }; + } as Token; + + if (token.typ == EnumToken.IdenTokenType) { + + // @ts-ignore + token.typ = EnumToken.NumberTokenType; + } + + return token; +} + +function getValue(t: NumberToken | IdentToken | FunctionToken): number | null { + + let v1: number | FractionToken | Token[]; + + if (t.typ == EnumToken.FunctionTokenType) { + + v1 = evaluateFunc(t); + + if (v1.length != 1 || v1[0].typ == EnumToken.BinaryExpressionTokenType) { + + return null; + } + + t = v1[0] as NumberToken | IdentToken; + } + + if (t.typ == EnumToken.IdenTokenType) { + + // @ts-ignore + return Math[(t as IdentToken).val.toUpperCase()] as number; + } + + if ((t.val as FractionToken).typ == EnumToken.FractionTokenType) { + + // @ts-ignore + return (t.val as FractionToken).l.val / (t.val as FractionToken).r.val; + } + + // @ts-ignore + return t.typ == EnumToken.FractionTokenType ? (t as FractionToken).l.val / (t as FractionToken).r.val : +t.val; +} + +export function evaluateFunc(token: FunctionToken): Token[] { + + const values: Token[] = token.chi.slice(); + + switch (token.val) { + + case 'abs': + case 'sin': + case 'cos': + case 'tan': + case 'asin': + case 'acos': + case 'atan': + case 'sign': + case 'sqrt': + case 'exp': { + + const value: Token[] = evaluate(values); + + if (value.length != 1 || (value[0].typ != EnumToken.NumberTokenType && value[0].typ != EnumToken.FractionTokenType) || (value[0].typ == EnumToken.FractionTokenType && (+(value[0] as FractionToken).r.val == 0 || !Number.isFinite(+(value[0] as FractionToken).l.val) || !Number.isFinite(+(value[0] as FractionToken).r.val)))) { + + return value; + } + + // @ts-ignore + let val: number = value[0].typ == EnumToken.NumberTokenType ? +value[0].val : (value[0] as FractionToken).l.val / (value[0] as FractionToken).r.val; + + return [{ + typ: EnumToken.NumberTokenType, + val: '' + Math[token.val](val) + }]; + } + + case 'hypot': { + + const chi = values.filter(t => ![EnumToken.WhitespaceTokenType, EnumToken.CommentTokenType, EnumToken.CommaTokenType].includes(t.typ)); + + let all: number[] = []; + let ref = chi[0]; + let value: number = 0; + + if (![EnumToken.NumberTokenType, EnumToken.PercentageTokenType].includes(ref.typ) && !('unit' in ref)) { + + return [token]; + } + + for (let i = 0; i < chi.length; i++) { + + // @ts-ignore + if (chi[i].typ != ref.typ || ('unit' in chi[i] && 'unit' in ref && chi[i].unit != ref.unit)) { + + return [token]; + } + + // @ts-ignore + const val = getValue(chi[i] as DimensionToken | NumberToken) as number; + + if (val == null) { + + return [token]; + } + + all.push(val); + value += val * val; + } + + return [ + { + ...ref, + val: Math.sqrt(value).toFixed(rem(...all)) + } as DimensionToken | AngleToken | NumberToken | LengthToken | TimeToken | FrequencyToken | ResolutionToken]; + } + + case 'atan2': + case 'pow': + case 'rem': + case 'mod': { + + const chi = values.filter(t => ![EnumToken.WhitespaceTokenType, EnumToken.CommentTokenType].includes(t.typ)); + + if (chi.length != 3 || chi[1].typ != EnumToken.CommaTokenType) { + + return [token]; + } + + if (token.val == 'pow' && (chi[0].typ != EnumToken.NumberTokenType || chi[2].typ != EnumToken.NumberTokenType)) { + + return [token]; + } + + if (['rem', 'mod'].includes(token.val) && + ( + chi[0].typ != chi[2].typ) || ( + 'unit' in chi[0] && 'unit' in chi[2] && + chi[0].unit != chi[2].unit + )) { + + return [token]; + } + + // https://developer.mozilla.org/en-US/docs/Web/CSS/mod + const v1: Token[] = evaluate([chi[0]]); + const v2: Token[] = evaluate([chi[2]]); + const types: EnumToken[] = [EnumToken.PercentageTokenType, EnumToken.DimensionTokenType, EnumToken.AngleTokenType, EnumToken.NumberTokenType, EnumToken.LengthTokenType, EnumToken.TimeTokenType, EnumToken.FrequencyTokenType, EnumToken.ResolutionTokenType]; + + if (v1.length != 1 || v2.length != 1 || !types.includes(v1[0].typ) || !types.includes(v2[0].typ) || (v1[0] as DimensionToken).unit != (v2[0] as DimensionToken).unit) { + + return [token]; + } + + // @ts-ignore + const val1 = getValue(v1[0] as Token) as number; + // @ts-ignore + const val2 = getValue(v2[0] as Token) as number; + + if (val1 == null || val2 == null || (v1[0].typ != v2[0].typ && val1 != 0 && val2 != 0)) { + + return [token]; + } + + if (token.val == 'rem') { + + if (val2 == 0) { + + return [token]; + } + + return [ + { + ...v1[0], + val: (val1 % val2).toFixed(rem(val1, val2)) + } as DimensionToken | AngleToken | NumberToken | LengthToken | TimeToken | FrequencyToken | ResolutionToken]; + } + + if (token.val == 'pow') { + + return [ + { + ...v1[0], + val: String(Math.pow(val1, val2)) + } as DimensionToken | AngleToken | NumberToken | LengthToken | TimeToken | FrequencyToken | ResolutionToken]; + } + + if (token.val == 'atan2') { + + return [ + { + ...{}, ...v1[0], + val: String(Math.atan2(val1, val2)) + } as DimensionToken | AngleToken | NumberToken | LengthToken | TimeToken | FrequencyToken | ResolutionToken]; + } + + return [ + { + ...v1[0], + val: String(val2 == 0 ? val1 : val1 - (Math.floor(val1 / val2) * val2)) + } as DimensionToken | AngleToken | NumberToken | LengthToken | TimeToken | FrequencyToken | ResolutionToken]; + } + + case 'clamp': + + token.chi = values; + + return [token]; + + case 'log': + case 'round': + case 'min': + case 'max': { + + const strategy = token.val == 'round' && values[0]?.typ == EnumToken.IdenTokenType ? (values.shift() as IdentToken).val : null; + const valuesMap = new Map; + + for (const curr of values) { + + if (curr.typ == EnumToken.CommaTokenType || curr.typ == EnumToken.WhitespaceTokenType || curr.typ == EnumToken.CommentTokenType) { + + continue; + } + + const result: Token[] = evaluate([curr]); + + if (result.length != 1 || result[0].typ == EnumToken.FunctionTokenType) { + + return [token]; + } + + const key: string = result[0].typ + ('unit' in result[0] ? result[0].unit : ''); + + if (!valuesMap.has(key)) { + + valuesMap.set(key, []); + } + + valuesMap.get(key)!.push(result[0]); + } + + if (valuesMap.size == 1) { + + const values = valuesMap.values().next().value as Token[]; + + if (token.val == 'log') { + + if (values[0].typ != EnumToken.NumberTokenType || values.length > 2) { + + return [token]; + } + + const val1 = getValue(values[0] as NumberToken) as number; + const val2 = values.length == 2 ? getValue(values[1] as NumberToken) as number : null; + + if (values.length == 1) { + + return [ + { + ...values[0], + val: String(Math.log(val1)) + } as DimensionToken | AngleToken | NumberToken | LengthToken | TimeToken | FrequencyToken | ResolutionToken]; + } + + return [ + { + ...values[0], + val: String(Math.log(val1) / Math.log(val2 as number)) + } as DimensionToken | AngleToken | NumberToken | LengthToken | TimeToken | FrequencyToken | ResolutionToken]; + } + + if (token.val == 'min' || token.val == 'max') { + + let val = getValue(values[0] as NumberToken) as number; + let val2: number = val; + let ret = values[0]; + + for (const curr of values.slice(1)) { + + val2 = getValue(curr as NumberToken) as number; + + if (val2 < val && token.val == 'min') { + + val = val2; + ret = curr; + } else if (val2 > val && token.val == 'max') { + + val = val2; + ret = curr; + + } + } + + return [ret]; + } + + if (token.val == 'round') { + + let val = getValue(values[0] as NumberToken) as number; + let val2 = getValue(values[1] as NumberToken) as number; + + if (Number.isNaN(val) || Number.isNaN(val2)) { + + return [token]; + } + + if (strategy == null || strategy == 'down') { + + val = val - (val % val2); + } else { + + val = strategy == 'to-zero' ? Math.trunc(val / val2) * val2 : (strategy == 'nearest' ? Math.round(val / val2) * val2 : Math.ceil(val / val2) * val2); + } + + // @ts-ignore + return [{...values[0], val: String(val)}]; + } + } + } + + return [token]; + } + + return [token]; } /** @@ -189,9 +605,7 @@ function inlineExpression(token: Token): Token[] { result.push(...inlineExpression(token.l), {typ: token.op}, ...inlineExpression(token.r)); } - } - - else { + } else { result.push(token); } @@ -225,7 +639,11 @@ function evaluateExpression(token: Token): Token { function isScalarToken(token: Token): boolean { - return 'unit' in token || [EnumToken.NumberTokenType, EnumToken.FractionTokenType, EnumToken.PercentageTokenType].includes(token.typ); + return 'unit' in token || + (token.typ == EnumToken.FunctionTokenType && mathFuncs.includes((token as FunctionToken).val)) || + // @ts-ignore + (token.typ == EnumToken.IdenTokenType && typeof Math[(token as IdentToken).val.toUpperCase()] == 'number') || + [EnumToken.NumberTokenType, EnumToken.FractionTokenType, EnumToken.PercentageTokenType].includes(token.typ); } /** diff --git a/src/lib/ast/math/math.ts b/src/lib/ast/math/math.ts index 97e7ff45..9bb3023c 100644 --- a/src/lib/ast/math/math.ts +++ b/src/lib/ast/math/math.ts @@ -127,6 +127,16 @@ export function compute(a: number | FractionToken, b: number | FractionToken, op }; } +export function rem(...a: number[]): number { + + if (a.some((i) => !Number.isInteger(i))) { + + return a.reduce((a, b) => Math.max(a, String(b).split('.')[1]?.length ?? 0), 0); + } + + return 0; +} + export function simplify(a: number, b: number): [number, number] { const g: number = gcd(a, b); diff --git a/src/lib/ast/walk.ts b/src/lib/ast/walk.ts index 1ac9b0e7..ce183747 100644 --- a/src/lib/ast/walk.ts +++ b/src/lib/ast/walk.ts @@ -1,6 +1,7 @@ import type { AstNode, - AstRuleList, BinaryExpressionToken, + AstRuleList, + BinaryExpressionToken, FunctionToken, ParensToken, Token, @@ -11,6 +12,12 @@ import type { WalkResult } from "../../@types/index.d.ts"; import {EnumToken} from "./types"; +import {renderToken} from "../renderer"; + +export enum WalkerValueEvent { + Enter, + Leave +} export function* walk(node: AstNode, filter?: WalkerFilter): Generator { @@ -57,57 +64,128 @@ export function* walk(node: AstNode, filter?: WalkerFilter): Generator { +export function* walkValues(values: Token[], root: AstNode | Token | null = null, filter?: WalkerValueFilter | { + event: WalkerValueEvent, + fn?: WalkerValueFilter, + type?: EnumToken | EnumToken[] | ((token: Token) => boolean) +}): Generator { + // const set = new Set(); const stack: Token[] = values.slice(); const map: Map = new Map; - let value: Token; let previous: Token | null = null; + // let parent: FunctionToken | ParensToken | BinaryExpressionToken | null = null; + + if (filter != null && typeof filter == 'function') { + + filter = { + event: WalkerValueEvent.Enter, + fn: filter + } + + } else if (filter == null) { + + filter = { + event: WalkerValueEvent.Enter + } + } - while ((value = stack.shift())) { + while (stack.length > 0) { + let value: Token = stack.shift(); let option: WalkerOption = null; - if (filter != null) { + if (filter.fn != null && filter.event == WalkerValueEvent.Enter) { - option = filter(value); + const isValid: boolean = filter.type == null || value.typ == filter.type || + (Array.isArray(filter.type) && filter.type.includes(value.typ)) || + (typeof filter.type == 'function' && filter.type(value)); - if (option === 'ignore') { + if (isValid) { - continue; - } + option = filter.fn(value, map.get(value) ?? root, WalkerValueEvent.Enter); - if (option === 'stop') { + if (option === 'ignore') { - break; + continue; + } + + if (option === 'stop') { + + break; + } + + // @ts-ignore + if (option != null && typeof option == 'object' && 'typ' in option) { + + map.set(option, map.get(value) ?? root as FunctionToken | ParensToken); + } } } // @ts-ignore - if (option !== 'children') { - - // @ts-ignore - yield {value, parent: map.get(value), previousValue: previous, nextValue: stack[0] ?? null, root}; + if (filter.event == WalkerValueEvent.Enter && option !== 'children') { + + yield { + value, + parent: map.get(value) ?? root, + previousValue: previous, + nextValue: stack[0] ?? null, + // @ts-ignore + root: root ?? null + }; } if (option !== 'ignore-children' && 'chi' in value) { - for (const child of (value).chi.slice()) { + const sliced = (value).chi.slice(); + + for (const child of sliced) { map.set(child, value); } - stack.unshift(...(value).chi); - } + stack.unshift(...sliced); + } else if (value.typ == EnumToken.BinaryExpressionTokenType) { - else if (value.typ == EnumToken.BinaryExpressionTokenType) { + map.set(value.l, map.get(value) ?? root as FunctionToken | ParensToken); + map.set(value.r, map.get(value) ?? root as FunctionToken | ParensToken); - map.set(value.l, value); - map.set(value.r, value); stack.unshift(value.l, value.r); } + if (filter.event == WalkerValueEvent.Leave && filter.fn != null) { + + const isValid: boolean = filter.type == null || value.typ == filter.type || + (Array.isArray(filter.type) && filter.type.includes(value.typ)) || + (typeof filter.type == 'function' && filter.type(value)); + + if (isValid) { + + option = filter.fn(value, map.get(value), WalkerValueEvent.Leave); + + // @ts-ignore + if (option != null && 'typ' in option) { + + map.set(option, map.get(value) ?? root as FunctionToken | ParensToken); + } + } + } + + // @ts-ignore + if (filter.event == WalkerValueEvent.Leave && option !== 'children') { + + yield { + value, + parent: map.get(value) ?? root, + previousValue: previous, + nextValue: stack[0] ?? null, + // @ts-ignore + root: root ?? null + }; + } + previous = value; } } \ No newline at end of file diff --git a/src/lib/parser/parse.ts b/src/lib/parser/parse.ts index 11894f17..66a74604 100644 --- a/src/lib/parser/parse.ts +++ b/src/lib/parser/parse.ts @@ -94,7 +94,7 @@ import type { UrlToken, WhitespaceToken } from "../../@types"; -import {deprecatedSystemColors, systemColors} from "../renderer/color/utils"; +import {deprecatedSystemColors, mathFuncs, systemColors} from "../renderer/color/utils"; import {validateSelector} from "../validation/selector"; export const urlTokenMatcher: RegExp = /^(["']?)[a-zA-Z0-9_/.-][a-zA-Z0-9_/:.#?-]+(\1)$/; @@ -1112,10 +1112,6 @@ export function parseString(src: string, options: { location: boolean } = {locat function getTokenType(val: string, hint?: EnumToken): Token { - // if (val === '' && hint == null) { - // throw new Error('empty string?'); - // } - if (hint != null) { return enumTokenHints.has(hint) ? {typ: hint} : {typ: hint, val}; @@ -1343,11 +1339,7 @@ export function parseTokens(tokens: Token[], options: ParseTokenOptions = {}): T if (t.typ == EnumToken.WhitespaceTokenType && ((i == 0 || i + 1 == tokens.length || [EnumToken.CommaTokenType, EnumToken.GteTokenType, EnumToken.LteTokenType, EnumToken.ColumnCombinatorTokenType].includes(tokens[i + 1].typ)) || - (i > 0 && - // tokens[i + 1]?.typ != Literal || - // funcLike.includes(tokens[i - 1].typ) && - // !['var', 'calc'].includes((tokens[i - 1]).val)))) && - trimWhiteSpace.includes(tokens[i - 1].typ)))) { + (i > 0 && trimWhiteSpace.includes(tokens[i - 1].typ)))) { tokens.splice(i--, 1); continue; @@ -1356,7 +1348,9 @@ export function parseTokens(tokens: Token[], options: ParseTokenOptions = {}): T if (t.typ == EnumToken.ColonTokenType) { const typ: EnumToken = tokens[i + 1]?.typ; + if (typ != null) { + if (typ == EnumToken.FunctionTokenType) { (tokens[i + 1]).val = ':' + ((tokens[i + 1]).val in webkitPseudoAliasMap ? webkitPseudoAliasMap[(tokens[i + 1]).val] : (tokens[i + 1]).val); @@ -1603,7 +1597,7 @@ export function parseTokens(tokens: Token[], options: ParseTokenOptions = {}): T parseTokens(t.chi, options); } - if (t.typ == EnumToken.FunctionTokenType && (t).val == 'calc') { + if (t.typ == EnumToken.FunctionTokenType && mathFuncs.includes((t).val)) { for (const {value, parent} of walkValues((t).chi)) { diff --git a/src/lib/parser/utils/type.ts b/src/lib/parser/utils/type.ts index 0cbc884d..1697c83c 100644 --- a/src/lib/parser/utils/type.ts +++ b/src/lib/parser/utils/type.ts @@ -1,8 +1,6 @@ import {EnumToken} from "../../ast"; import type {IdentToken, PropertyMapType, Token} from "../../../@types/index.d.ts"; - -// https://www.w3.org/TR/css-values-4/#math-function -export const funcList: string[] = ['clamp', 'calc']; +import {mathFuncs} from "../../renderer/color/utils"; export function matchType(val: Token, properties: PropertyMapType): boolean { @@ -26,8 +24,9 @@ export function matchType(val: Token, properties: PropertyMapType): boolean { if (val.typ == EnumToken.FunctionTokenType) { - if (funcList.includes(val.val)) { - return val.chi.every(((t: Token) => [EnumToken.LiteralTokenType, EnumToken.CommaTokenType, EnumToken.WhitespaceTokenType, EnumToken.StartParensTokenType, EnumToken.EndParensTokenType].includes(t.typ) || matchType(t, properties))); + if (mathFuncs.includes(val.val)) { + + return val.chi.every(((t: Token) => [EnumToken.Add,EnumToken.Mul,EnumToken.Div,EnumToken.Sub,EnumToken.LiteralTokenType, EnumToken.CommaTokenType, EnumToken.WhitespaceTokenType, EnumToken.DimensionTokenType, EnumToken.NumberTokenType, EnumToken.LengthTokenType, EnumToken.AngleTokenType, EnumToken.PercentageTokenType, EnumToken.ResolutionTokenType, EnumToken.TimeTokenType, EnumToken.BinaryExpressionTokenType].includes(t.typ) || matchType(t, properties))); } // match type defined like function 'symbols()', 'url()', 'attr()' etc. diff --git a/src/lib/renderer/color/relativecolor.ts b/src/lib/renderer/color/relativecolor.ts index 2cfc6d78..818899e8 100644 --- a/src/lib/renderer/color/relativecolor.ts +++ b/src/lib/renderer/color/relativecolor.ts @@ -1,9 +1,18 @@ -import type {ColorToken, PercentageToken, Token} from "../../../@types/index.d.ts"; +import type { + BinaryExpressionToken, + ColorToken, + FunctionToken, + IdentToken, + ParensToken, + PercentageToken, + Token +} from "../../../@types/index.d.ts"; import {convert, getNumber} from "./color"; import {EnumToken, walkValues} from "../../ast"; import {reduceNumber} from "../render"; -import {evaluate} from "../../ast/math"; -import {colorRange} from "./utils"; +import {evaluate, evaluateFunc} from "../../ast/math"; +import {colorRange, mathFuncs} from "./utils"; +import {eq} from "../../parser/utils/eq"; type RGBKeyType = 'r' | 'g' | 'b' | 'alpha'; type HSLKeyType = 'h' | 's' | 'l' | 'alpha'; @@ -72,7 +81,7 @@ export function parseRelativeColor(relativeKeys: string, original: ColorToken, r } : aExp) }; - return computeComponentValue(keys, values); + return computeComponentValue(keys, converted, values); } function getValue(t: Token, converted: ColorToken, component: string): Token { @@ -101,7 +110,7 @@ function getValue(t: Token, converted: ColorToken, component: string): Token { return t; } -function computeComponentValue(expr: Record, values: Record): Record | null { +function computeComponentValue(expr: Record, converted: ColorToken, values: Record): Record | null { for (const object of [values, expr]) { @@ -154,46 +163,37 @@ function computeComponentValue(expr: Record, values: expr[key] = values[exp.val]; } - } else if (exp.typ == EnumToken.FunctionTokenType && exp.val == 'calc') { + } else if (exp.typ == EnumToken.FunctionTokenType && mathFuncs.includes(exp.val)) { - for (let {value, parent} of walkValues(exp.chi)) { + for (let {value, parent} of walkValues(exp.chi, exp)) { - if (value.typ == EnumToken.IdenTokenType) { + if (parent == null) { - if (!(value.val in values)) { - - return null; - } - - if (parent == null) { - - parent = exp; - } - - if (parent.typ == EnumToken.BinaryExpressionTokenType) { - - if (parent.l == value) { - - parent.l = values[value.val]; - } else { + parent = exp; + } - parent.r = values[value.val]; - } - } else { + if (value.typ == EnumToken.PercentageTokenType) { - for (let i = 0; i < parent.chi.length; i++) { + replaceValue(parent as BinaryExpressionToken | FunctionToken | ParensToken, value, getValue(value, converted, key)); + } else if (value.typ == EnumToken.IdenTokenType) { - if (parent.chi[i] == value) { + // @ts-ignore + if (!(value.val in values || typeof Math[(value as IdentToken).val.toUpperCase()] == 'number')) { - parent.chi.splice(i, 1, values[value.val]); - break; - } - } + return null; } + + // @ts-ignore + replaceValue(parent as BinaryExpressionToken | FunctionToken | ParensToken, value, values[value.val] ?? { + typ: EnumToken.NumberTokenType, + // @ts-ignore + val: '' + Math[(value as IdentToken).val.toUpperCase()] + // @ts-ignore + } as Token); } } - const result: Token[] = evaluate(exp.chi); + const result: Token[] = exp.typ == EnumToken.FunctionTokenType && mathFuncs.includes(exp.val) && exp.val != 'calc' ? evaluateFunc(exp) : evaluate(exp.chi); if (result.length == 1 && result[0].typ != EnumToken.BinaryExpressionTokenType) { @@ -207,3 +207,39 @@ function computeComponentValue(expr: Record, values: return >expr; } + +function replaceValue(parent: FunctionToken | ParensToken | BinaryExpressionToken, value: Token, newValue: Token) { + if (parent.typ == EnumToken.BinaryExpressionTokenType) { + + if (parent.l == value) { + + parent.l = newValue; + } else { + + parent.r = newValue; + } + } else { + + for (let i = 0; i < parent.chi.length; i++) { + + if (parent.chi[i] == value) { + + parent.chi.splice(i, 1, newValue); + break; + } + + if (parent.chi[i].typ == EnumToken.BinaryExpressionTokenType) { + + if ((parent.chi[i] as BinaryExpressionToken).l == value) { + + (parent.chi[i] as BinaryExpressionToken).l = newValue; + break; + } else if ((parent.chi[i] as BinaryExpressionToken).r == value) { + + (parent.chi[i] as BinaryExpressionToken).r = newValue; + break + } + } + } + } +} diff --git a/src/lib/renderer/color/utils/constants.ts b/src/lib/renderer/color/utils/constants.ts index fa781530..bab2e6b5 100644 --- a/src/lib/renderer/color/utils/constants.ts +++ b/src/lib/renderer/color/utils/constants.ts @@ -29,6 +29,8 @@ export const colorRange = { } } +// https://www.w3.org/TR/css-values-4/#math-function +export const mathFuncs = ['calc', 'clamp', 'min', 'max', 'round', 'mod', 'rem', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'pow', 'sqrt', 'hypot', 'log', 'exp', 'abs', 'sign']; export const colorFuncColorSpace: ColorSpace[] = ['srgb', 'srgb-linear', 'display-p3', 'prophoto-rgb', 'a98-rgb', 'rec2020', 'xyz', 'xyz-d65', 'xyz-d50']; export const powerlessColorComponent: IdentToken = {typ: EnumToken.IdenTokenType, val: 'none'}; diff --git a/src/lib/renderer/render.ts b/src/lib/renderer/render.ts index aede6331..634f4d12 100644 --- a/src/lib/renderer/render.ts +++ b/src/lib/renderer/render.ts @@ -42,7 +42,7 @@ import { } from "./color"; import {EnumToken, expand} from "../ast"; import {SourceMap} from "./sourcemap"; -import {colorFuncColorSpace, getComponents} from "./color/utils"; +import {colorFuncColorSpace, getComponents, mathFuncs} from "./color/utils"; import {isColor, isNewLine} from "../syntax"; export const colorsFunc: string[] = ['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'device-cmyk', 'color-mix', 'color', 'oklab', 'lab', 'oklch', 'lch', 'light-dark']; @@ -100,6 +100,7 @@ export function doRender(data: AstNode, options: RenderOptions = {}): RenderResu ...(options.minify ?? true ? { indent: '', newLine: '', + removeEmpty: true, removeComments: true } : { indent: ' ', @@ -323,6 +324,11 @@ function renderAstNode(data: AstNode, options: RenderOptions, sourcemap: SourceM return `${css}${options.newLine}${indentSub}${str}`; }, ''); + if (options.removeEmpty && children === '') { + + return ''; + } + if (children.endsWith(';')) { children = children.slice(0, -1); @@ -341,6 +347,7 @@ function renderAstNode(data: AstNode, options: RenderOptions, sourcemap: SourceM default: + // return renderToken(data as Token, options, cache, reducer, errors); throw new Error(`render: unexpected token ${JSON.stringify(data, null, 1)}`); } @@ -509,7 +516,6 @@ export function renderToken(token: Token, options: RenderOptions = {}, cache: { // @ts-ignore const color: ColorToken = chi[1]; - const components: Record = >parseRelativeColor(token.val == 'color' ? (chi[offset]).val : token.val, color, chi[offset + 1], chi[offset + 2], chi[offset + 3], chi[offset + 4]); if (components != null) { @@ -634,13 +640,12 @@ export function renderToken(token: Token, options: RenderOptions = {}, cache: { if ( token.typ == EnumToken.FunctionTokenType && - token.val == 'calc' && + mathFuncs.includes(token.val) && token.chi.length == 1 && - token.chi[0].typ != EnumToken.BinaryExpressionTokenType && - token.chi[0].typ != EnumToken.FractionTokenType && + ![EnumToken.BinaryExpressionTokenType, EnumToken.FractionTokenType, EnumToken.IdenTokenType].includes(token.chi[0].typ) && + // @ts-ignore ((token.chi[0]).val)?.typ != EnumToken.FractionTokenType) { - // calc(200px) => 200px return token.chi.reduce((acc: string, curr: Token) => acc + renderToken(curr, options, cache, reducer), '') } diff --git a/src/lib/syntax/syntax.ts b/src/lib/syntax/syntax.ts index f58b780e..241da55e 100644 --- a/src/lib/syntax/syntax.ts +++ b/src/lib/syntax/syntax.ts @@ -14,6 +14,7 @@ import type { Token } from "../../@types"; import {EnumToken} from "../ast"; +import {mathFuncs} from "../renderer/color/utils"; // '\\' const REVERSE_SOLIDUS = 0x5c; @@ -146,7 +147,7 @@ export function isColor(token: Token): boolean { } } - if (children[i].typ == EnumToken.FunctionTokenType && !['calc'].includes((children[i]).val)) { + if (children[i].typ == EnumToken.FunctionTokenType && !mathFuncs.includes((children[i]).val)) { return false; } @@ -284,7 +285,7 @@ export function isColor(token: Token): boolean { continue; } - if (v.typ == EnumToken.FunctionTokenType && (v.val == 'calc' || v.val == 'var' || colorsFunc.includes(v.val))) { + if (v.typ == EnumToken.FunctionTokenType && (mathFuncs.includes(v.val) || v.val == 'var' || colorsFunc.includes(v.val))) { continue; } diff --git a/test/specs/code/calc.js b/test/specs/code/calc.js index 63ed9e48..d7034835 100644 --- a/test/specs/code/calc.js +++ b/test/specs/code/calc.js @@ -1,12 +1,11 @@ -export function run(describe, expect, transform) { +export function run(describe, expect, transform, parse, render) { describe('calc expression', function () { - it('calc #1', function () { - const css = ` -`; + it('calc() #1', function () { + return transform(` .foo { width: calc(100px * 2); @@ -15,9 +14,8 @@ export function run(describe, expect, transform) { `).then(result => expect(result.code).equals(`.foo{width:200px;height:calc(75.37% - 763.5px)}`)); }); - it('calc #2', function () { - const css = ` -`; + it('calc() #2', function () { + return transform(`.foo { height: calc(200% / 6 + 2%/3); width: calc(3.5rem + calc(var(--bs-border-width) * 2)); @@ -25,18 +23,16 @@ export function run(describe, expect, transform) { `).then(result => expect(result.code).equals(`.foo{height:34%;width:calc(3.5rem + var(--bs-border-width)*2)}`)); }); - it('calc #3', function () { - const css = ` -`; + it('calc() #3', function () { + return transform(`.foo { bottom:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)) } `).then(result => expect(result.code).equals(`.foo{bottom:calc(-1*var(--bs-popover-arrow-height) - var(--bs-popover-border-width))}`)); }); - it('calc #4', function () { - const css = ` -`; + it('calc() #4', function () { + return transform(` :root { @@ -48,9 +44,8 @@ export function run(describe, expect, transform) { `, {inlineCssVariables: true}).then(result => expect(result.code).equals(`.foo-bar{width:25px}`)); }); - it('calc #5', function () { - const css = ` -`; + it('calc() #5', function () { + return transform(` :root { @@ -63,9 +58,8 @@ export function run(describe, expect, transform) { `, {inlineCssVariables: true}).then(result => expect(result.code).equals(`.foo-bar{width:12px;height:25%}`)); }); - it('calc #6', function () { - const css = ` -`; + it('calc() #6', function () { + return transform(` :root { @@ -78,9 +72,8 @@ export function run(describe, expect, transform) { `).then(result => expect(result.code).equals(`:root{--preferred-width:20px}.foo-bar{width:calc((var(--preferred-width) + 1px)/3 + 5px);height:25%}`)); }); - it('calc #7', function () { - const css = ` -`; + it('calc() #7', function () { + return transform(` .foo { @@ -89,9 +82,8 @@ export function run(describe, expect, transform) { `).then(result => expect(result.code).equals(`.foo{height:14px}`)); }); - it('calc #8', function () { - const css = ` -`; + it('calc() #8', function () { + return transform(` .foo { @@ -100,9 +92,8 @@ export function run(describe, expect, transform) { `).then(result => expect(result.code).equals(`.foo{height:calc(13px - 5%)}`)); }); - it('calc #9', function () { - const css = ` -`; + it('calc() #9', function () { + return transform(` .foo { @@ -111,9 +102,8 @@ export function run(describe, expect, transform) { `).then(result => expect(result.code).equals(`.foo{height:calc(40px/3)}`)); }); - it('calc #10', function () { - const css = ` -`; + it('calc() #10', function () { + return transform(` .foo { @@ -122,6 +112,245 @@ height: calc(80% * 50%); } `).then(result => expect(result.code).equals(`.foo{width:1px;height:40%}`)); }); + + it('calc() #11', function () { + + return transform(` + +a { + +width: calc(100px * sin(pi / 4)) + +`).then(result => expect(result.code).equals(`a{width:70.71067811865474px}`)); + }); + + it('mod() #12', function () { + + return transform(` + +.foo{ + margin: mod(29vmin, 6vmin); +} +`).then(result => expect(result.code).equals(`.foo{margin:5vmin}`)); + }); + + it('round() #13', function () { + + return transform(` + +.foo{ + margin: round(up, calc(100px * sin(pi / 4)), 5.5px); +} +`).then(result => expect(result.code).equals(`.foo{margin:71.5px}`)); + }); + + it('round() #14', function () { + + return transform(` + +.foo{ + margin: round(down, calc(100px * sin(pi / 4)), 5.5px); +} +`).then(result => expect(result.code).equals(`.foo{margin:66px}`)); + }); + + it('round() #15', function () { + + return transform(` + +.foo{ + margin: round(nearest, calc(100px * sin(pi / 4)), 5.5px); +} +`).then(result => expect(result.code).equals(`.foo{margin:71.5px}`)); + }); + + it('round() #16', function () { + + return transform(` + +.foo{ + margin: round(to-zero, calc(100px * sin(pi / 4)), 5.5px); +} +`).then(result => expect(result.code).equals(`.foo{margin:66px}`)); + }); + + it('min()/max() #17', function () { + + return transform(` + +.foo{ + +height: min(calc(100px * sin(pi / 4)), 5.5px); +width: max(calc(100px * sin(pi / 2)), 5.5px); +} +`).then(result => expect(result.code).equals(`.foo{height:5.5px;width:100px}`)); + }); + + it('rem() #18', function () { + + return transform(` + +.foo{ + +scale: rem(10 * 2, 1.7); +} +`).then(result => expect(result.code).equals(`.foo{scale:1.3}`)); + }); + + it('pow() #19', function () { + + return transform(` + +.foo{ + + width: calc(10px * pow(5, 3)); +} +`).then(result => expect(result.code).equals(`.foo{width:1250px}`)); + }); + + it('pow() #20', function () { + + return parse(` + +:root { + --size-0: 100px; + --size-1: hypot(var(--size-0)); + --size-2: hypot(var(--size-0), var(--size-0)); + ); + --size-3: hypot( + calc(var(--size-0) * 1.5), + calc(var(--size-0) * 2) +} +.one { + width: var(--size-1); + height: var(--size-1); +} +.two { + width: var(--size-2); + height: var(--size-2); +} +.three { + width: var(--size-3); + height: var(--size-3); +} + +`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`:root { + /* --size-0: 100px */ + /* --size-1: 100px */ + /* --size-2: 141px */ + /* --size-3: 250px */ +} +.one { + width: 100px; + height: 100px +} +.two { + width: 141px; + height: 141px +} +.three { + width: 250px; + height: 250px +}`)); + }); + + it('pow() #21', function () { + + return parse(` + +a { + +-moz-transform: rotate(atan2(1rem, -0.5rem)); +height: rotate(atan2(pi, 45)); +line-height: calc(pi); +transform: rotate(atan2(e, 30)); +line-height: calc(pi); +} +`).then(result => expect(render(result.ast, {minify: false}).code).equals(`a { + -moz-transform: rotate(atan2(1rem,-.5rem)); + height: rotate(atan2(pi,45)); + line-height: calc(pi); + transform: rotate(atan2(e,30)) +}`)); + }); + + it('pow() #22', function () { + + return parse(` + +a { + +width: calc(100px * log(8, 2)); +} +`).then(result => expect(render(result.ast, {minify: false}).code).equals(`a { + width: 300px +}`)); + }); + + it('pow() #23', function () { + + return parse(` + +a { + +width: calc(100px * log(625, 5)); +} +`).then(result => expect(render(result.ast, {minify: false}).code).equals(`a { + width: 400px +}`)); + }); + + it('pow() #24', function () { + + return parse(` + +a { + +width: calc(100px * log(625, 5)); +} +`).then(result => expect(render(result.ast, {minify: false}).code).equals(`a { + width: 400px +}`)); + }); + + it('pow() #25', function () { + + return parse(` + +a { + +width: calc(100px * exp(-1));} +} +`).then(result => expect(render(result.ast, {minify: false}).code).equals(`a { + width: 36.787944117144235px +}`)); + }); + + it('pow() #26', function () { + + return parse(` + +a { + +width: calc(2px *abs(-1);} +} +`).then(result => expect(render(result.ast, {minify: false}).code).equals(`a { + width: 2px +}`)); + }); + + it('pow() #27', function () { + + return parse(` + +a { + +width: calc(-2px *sign(-1);} +} +`).then(result => expect(render(result.ast, {minify: false}).code).equals(`a { + width: 2px +}`)); + }); }); } \ No newline at end of file diff --git a/test/specs/code/color.js b/test/specs/code/color.js index 13c238f4..a291a406 100644 --- a/test/specs/code/color.js +++ b/test/specs/code/color.js @@ -234,7 +234,10 @@ color: hsl(from green calc(h * 2) s l / calc(alpha / 2)) `, { inlineCssVariables: true - }).then(result => expect(render(result.ast, {minify: false}).code).equals(`._19_u :focus { + }).then(result => expect(render(result.ast, {minify: false}).code).equals(`:root { + /* --color: green */ +} +._19_u :focus { color: navy }`)); }); @@ -251,7 +254,10 @@ color: hsl(from green calc(h * 2) s l / calc(alpha / 2)) `, { inlineCssVariables: true - }).then(result => expect(render(result.ast, {minify: false}).code).equals(`.selector { + }).then(result => expect(render(result.ast, {minify: false}).code).equals(`:root { + /* --color: 255 0 0 */ +} +.selector { background-color: #ff000080 }`)); }); @@ -1100,7 +1106,10 @@ html { --color: green; } .foo { --darker-accent: lch(from var(--color) calc(l / 2) c h); } -`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`.foo { +`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`html { + /* --color: green */ +} +.foo { --darker-accent: #004500 }`)); }); @@ -1111,7 +1120,10 @@ html { --base: oklch(52.6% 0.115 44.6deg) } .summary { background: oklch(from var(--base) l c calc(h + 90)); } -`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`.summary { +`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`html { + /* --base: oklch(52.6% .115 44.6deg) */ +} +.summary { background: #4d792f }`)); }); @@ -1125,7 +1137,11 @@ html { .foo { background: var(--darker-accent); } -`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`.foo { +`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`html { + /* --color: green */ + /* --darker-accent: lch(from green calc(l/2) c h) */ +} +.foo { background: #004500 }`)); }); @@ -1156,7 +1172,10 @@ html { --bluegreen: oklab(54.3% -22.5% -5%); } .overlay { background: oklab(from var(--bluegreen) calc(1.0 - l) calc(a * 0.8) b); } -`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`.overlay { +`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`html { + /* --bluegreen: oklab(54.3% -22.5% -5%) */ +} +.overlay { background: #0c6464 }`)); }); @@ -1201,7 +1220,11 @@ color: light-dark(rgb(0 0 0), rgb(255 255 255)); .c { color: light-dark(var(--dark), var(--light)); } -`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`/* Named color values */ +`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`:root { + /* --light: #fff */ + /* --dark: #000 */ +} +/* Named color values */ .a { color: light-dark(#000,#fff) } @@ -1227,6 +1250,26 @@ color: light-dark(rgb(0 0 0), rgb(255 255 255)); /* Use a border instead, since box-shadow is forced to 'none' in forced-colors mode */ border: ButtonBorder solid 2px +}`)); + }); + + it('percentage in calc() #128', function () { + return parse(` + +a {color:lch(from slateblue calc(l + 10%) c h) ; +`).then(result => expect(render(result.ast, {minify: false}).code).equals(`a { + color: #8673ea +}`)); + }); + + it('percentage in calc() #129', function () { + return parse(` + +a { +color: lch(from slateblue calc(l * sin(pi / 4)) c h); +; +`).then(result => expect(render(result.ast, {minify: false}).code).equals(`a { + color: #453ba9 }`)); }); } \ No newline at end of file diff --git a/test/specs/code/vars.js b/test/specs/code/vars.js index f9e85bea..42fb38fc 100644 --- a/test/specs/code/vars.js +++ b/test/specs/code/vars.js @@ -52,7 +52,12 @@ export function run(describe, expect, transform, parse, render, dirname, readFil animation-delay: calc(var(--animate-delay)*4) } -`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`.animate__animated.animate__repeat-1 { +`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`:root { + /* --animate-duration: 1s */ + /* --animate-delay: 1s */ + /* --animate-repeat: 1 */ +} +.animate__animated.animate__repeat-1 { -webkit-animation-iteration-count: 1; animation-iteration-count: 1 } @@ -87,7 +92,10 @@ html { --color: green; } --darker-accent: lch(from var(--color) calc(l / 2) c h); } -`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`.foo { +`, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`html { + /* --color: green */ +} +.foo { --darker-accent: #004500 }`)); }); From d7c6f1d9dce8b21e5c5cefa1584e41b88d889d65 Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Wed, 1 Jan 2025 02:33:57 -0500 Subject: [PATCH 2/2] add changed file #49 --- dist/lib/validation/declaration.js | 71 ++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 dist/lib/validation/declaration.js diff --git a/dist/lib/validation/declaration.js b/dist/lib/validation/declaration.js new file mode 100644 index 00000000..ddea64b7 --- /dev/null +++ b/dist/lib/validation/declaration.js @@ -0,0 +1,71 @@ +import { EnumToken, ValidationLevel } from '../ast/types.js'; +import '../ast/minify.js'; +import '../parser/parse.js'; +import '../renderer/color/utils/constants.js'; +import '../renderer/sourcemap/lib/encode.js'; +import '../parser/utils/config.js'; +import { getParsedSyntax, getSyntaxConfig } from './config.js'; +import { validateSyntax } from './syntax.js'; + +function validateDeclaration(declaration, options, root) { + const config = getSyntaxConfig(); + let name = declaration.nam; + if (!(name in config.declarations) && !(name in config.syntaxes)) { + if (name[0] == '-') { + const match = /^-([a-z]+)-(.*)$/.exec(name); + if (match != null) { + name = match[2]; + } + } + } + // root is at-rule - check if declaration allowed + if (root?.typ == EnumToken.AtRuleNodeType) { + const syntax = getParsedSyntax("atRules" /* ValidationSyntaxGroupEnum.AtRules */, '@' + root.nam)?.[0]; + if (syntax != null) { + if (!('chi' in syntax)) { + return { + valid: ValidationLevel.Drop, + node: declaration, + syntax, + error: 'declaration not allowed here' + }; + } + if (name.startsWith('--')) { + return { + valid: ValidationLevel.Valid, + node: declaration, + syntax: null, + error: '' + }; + } + if (!(name in config.declarations) && !(name in config.syntaxes)) { + return { + valid: ValidationLevel.Drop, + node: declaration, + syntax: null, + error: `unknown declaration "${declaration.nam}"` + }; + } + return validateSyntax(syntax.chi, [declaration], root, options); + } + } + if (name.startsWith('--')) { + return { + valid: ValidationLevel.Valid, + node: declaration, + syntax: null, + error: '' + }; + } + if (!(name in config.declarations) && !(name in config.syntaxes)) { + return { + valid: ValidationLevel.Drop, + node: declaration, + syntax: null, + error: `unknown declaration "${declaration.nam}"` + }; + } + return validateSyntax(getParsedSyntax("declarations" /* ValidationSyntaxGroupEnum.Declarations */, name), declaration.val); +} + +export { validateDeclaration };