From 30ab635a6968fe1d6d825fda104549c0f297924a Mon Sep 17 00:00:00 2001 From: Ludovico Fischer Date: Sun, 2 Jan 2022 20:54:50 +0100 Subject: [PATCH 1/3] feat: add types --- package.json | 7 ++- pnpm-lock.yaml | 8 ++++ src/index.js | 12 ++++- src/lib/convertUnit.js | 12 ++++- src/lib/reducer.js | 92 +++++++++++++++++++++++++++++--------- src/lib/stringifier.js | 23 ++++++++-- src/lib/transform.js | 31 ++++++++++--- src/parser.d.ts | 46 +++++++++++++++++++ tsconfig.json | 18 ++++++++ types/index.d.ts | 27 +++++++++++ types/lib/convertUnit.d.ts | 8 ++++ types/lib/reducer.d.ts | 10 +++++ types/lib/stringifier.d.ts | 14 ++++++ types/lib/transform.d.ts | 6 +++ 14 files changed, 279 insertions(+), 35 deletions(-) create mode 100644 src/parser.d.ts create mode 100644 tsconfig.json create mode 100644 types/index.d.ts create mode 100644 types/lib/convertUnit.d.ts create mode 100644 types/lib/reducer.d.ts create mode 100644 types/lib/stringifier.d.ts create mode 100644 types/lib/transform.d.ts diff --git a/package.json b/package.json index e709d3d..eb08081 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,16 @@ "calc" ], "main": "dist/index.js", + "types": "types/index.d.ts", "files": [ "dist", + "types", "LICENSE" ], "scripts": { - "prepare": "pnpm run build", + "prepare": "pnpm run build && tsc", "build": "rimraf dist && babel src --out-dir dist --ignore src/__tests__/**/*.js && jison src/parser.jison -o dist/parser.js", - "lint": "eslint src", + "lint": "eslint src && tsc", "pretest": "pnpm run build", "test": "uvu -r @babel/register src/__tests__" }, @@ -44,6 +46,7 @@ "jison-gho": "^0.6.1-216", "postcss": "^8.2.2", "rimraf": "^3.0.2", + "typescript": "^4.5.4", "uvu": "^0.5.2" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1c6387..29b8521 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,7 @@ specifiers: postcss-selector-parser: ^6.0.2 postcss-value-parser: ^4.0.2 rimraf: ^3.0.2 + typescript: ^4.5.4 uvu: ^0.5.2 dependencies: @@ -30,6 +31,7 @@ devDependencies: jison-gho: 0.6.1-216 postcss: 8.4.5 rimraf: 3.0.2 + typescript: 4.5.4 uvu: 0.5.2 packages: @@ -2016,6 +2018,12 @@ packages: engines: {node: '>=10'} dev: true + /typescript/4.5.4: + resolution: {integrity: sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + /unbox-primitive/1.0.1: resolution: {integrity: sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==} dependencies: diff --git a/src/index.js b/src/index.js index 04be391..00173b7 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,12 @@ import transform from './lib/transform'; +/** + * @param {{precision?: number | false, + * preserve?: boolean, + * warnWhenCannotResolve?: boolean, + * mediaQueries?: boolean, + * selectors?: boolean}} opts + */ function pluginCreator(opts) { const options = Object.assign({ precision: 5, @@ -11,10 +18,13 @@ function pluginCreator(opts) { return { postcssPlugin: 'postcss-calc', + /** + * @param {import('postcss').Root} css + * @param {{result: import('postcss').Result}} helpers + */ OnceExit(css, { result }) { css.walk(node => { const { type } = node; - if (type === 'decl') { transform(node, "value", options, result); } diff --git a/src/lib/convertUnit.js b/src/lib/convertUnit.js index be8573d..e8ab948 100644 --- a/src/lib/convertUnit.js +++ b/src/lib/convertUnit.js @@ -1,3 +1,6 @@ +/** + * @type {{[key:string]: {[key:string]: number}}} + */ const conversions = { // Absolute length units 'px': { @@ -123,7 +126,12 @@ const conversions = { 'dppx': 1 } }; - +/** + * @param {number} value + * @param {string} sourceUnit + * @param {string} targetUnit + * @param {number|false} precision + */ function convertUnit(value, sourceUnit, targetUnit, precision) { const sourceUnitNormalized = sourceUnit.toLowerCase(); const targetUnitNormalized = targetUnit.toLowerCase(); @@ -139,7 +147,7 @@ function convertUnit(value, sourceUnit, targetUnit, precision) { const converted = conversions[targetUnitNormalized][sourceUnitNormalized] * value; if (precision !== false) { - precision = Math.pow(10, parseInt(precision) || 5); + precision = Math.pow(10, Math.ceil(precision) || 5); return Math.round(converted * precision) / precision; } diff --git a/src/lib/reducer.js b/src/lib/reducer.js index 65a4bcf..7851f91 100644 --- a/src/lib/reducer.js +++ b/src/lib/reducer.js @@ -1,7 +1,11 @@ import convertUnit from "./convertUnit"; -function isValueType(type) { - switch (type) { +/** + * @param {import('../parser').CalcNode} node + * @return {node is import('../parser').ValueExpression} + */ +function isValueType(node) { + switch (node.type) { case 'LengthValue': case 'AngleValue': case 'TimeValue': @@ -22,22 +26,38 @@ function isValueType(type) { return false; } +/** @param {'-'|'+'} operator */ function flip(operator) { return operator === '+' ? '-' : '+'; } +/** + * @param {string} operator + * @returns {operator is '+'|'-'} + */ function isAddSubOperator(operator) { return operator === '+' || operator === '-'; } +/** + * @typedef {{preOperator: '+'|'-', node: import('../parser').CalcNode}} Collectible +*/ + +/** + * @param {'+'|'-'} preOperator + * @param {import('../parser').CalcNode} node + * @param {Collectible[]} collected + * @param {number} precision + */ function collectAddSubItems(preOperator, node, collected, precision) { if (!isAddSubOperator(preOperator)) { throw new Error(`invalid operator ${preOperator}`); } - const type = node.type; - if (isValueType(type)) { - const itemIndex = collected.findIndex(x => x.node.type === type); + if (isValueType(node)) { + const itemIndex = collected.findIndex(x => x.node.type === node.type); if (itemIndex >= 0) { if (node.value === 0) { return; } - const {left: reducedNode, right: current} = convertNodesUnits(collected[itemIndex].node, node, precision) + // can cast because of the criterion used to find itemIndex + const otherValueNode = /** @type import('../parser').ValueExpression*/(collected[itemIndex].node); + const {left: reducedNode, right: current} = convertNodesUnits(otherValueNode, node, precision) if (collected[itemIndex].preOperator === '-') { collected[itemIndex].preOperator = '+'; @@ -64,7 +84,7 @@ function collectAddSubItems(preOperator, node, collected, precision) { collected.push({node, preOperator: flip(preOperator)}); } } - } else if (type === "MathExpression") { + } else if (node.type === "MathExpression") { if (isAddSubOperator(node.operator)) { collectAddSubItems(preOperator, node.left, collected, precision); const collectRightOperator = preOperator === '-' ? flip(node.operator) : node.operator; @@ -85,27 +105,31 @@ function collectAddSubItems(preOperator, node, collected, precision) { } } - +/** + * @param {import('../parser').CalcNode} node + * @param {number} precision + */ function reduceAddSubExpression(node, precision) { + /** @type Collectible[] */ const collected = []; collectAddSubItems('+', node, collected, precision); - const withoutZeroItem = collected.filter((item) => !(isValueType(item.node.type) && item.node.value === 0)); + const withoutZeroItem = collected.filter((item) => !(isValueType(item.node) && item.node.value === 0)); const firstNonZeroItem = withoutZeroItem[0]; // could be undefined // prevent producing "calc(-var(--a))" or "calc()" // which is invalid css if (!firstNonZeroItem || firstNonZeroItem.preOperator === '-' && - !isValueType(firstNonZeroItem.node.type)) { + !isValueType(firstNonZeroItem.node)) { const firstZeroItem = collected.find((item) => - isValueType(item.node.type) && item.node.value === 0); - withoutZeroItem.unshift(firstZeroItem) + isValueType(item.node) && item.node.value === 0); + withoutZeroItem.unshift(/** @type Collectible*/(firstZeroItem)) } // make sure the preOperator of the first item is + if (withoutZeroItem[0].preOperator === '-' && - isValueType(withoutZeroItem[0].node.type)) { + isValueType(withoutZeroItem[0].node)) { withoutZeroItem[0].node.value *= -1; withoutZeroItem[0].preOperator = '+'; } @@ -122,9 +146,11 @@ function reduceAddSubExpression(node, precision) { return root; } - +/** + * @param {import('../parser').MathExpression} node + */ function reduceDivisionExpression(node) { - if (!isValueType(node.right.type)) { + if (!isValueType(node.right)) { return node; } @@ -135,12 +161,18 @@ function reduceDivisionExpression(node) { return applyNumberDivision(node.left, node.right.value) } -// apply (expr) / number +/** + * apply (expr) / number + * + * @param {import('../parser').CalcNode} node + * @param {number} divisor + * @return {import('../parser').CalcNode} +*/ function applyNumberDivision(node, divisor) { if (divisor === 0) { throw new Error('Cannot divide by zero'); } - if (isValueType(node.type)) { + if (isValueType(node)) { node.value /= divisor; return node; } @@ -169,7 +201,9 @@ function applyNumberDivision(node, divisor) { } } } - +/** + * @param {import('../parser').MathExpression} node + */ function reduceMultiplicationExpression(node) { // (expr) * number if (node.right.type === 'Number') { @@ -182,9 +216,14 @@ function reduceMultiplicationExpression(node) { return node; } -// apply (expr) / number +/** + * apply (expr) * number + * @param {number} multiplier + * @param {import('../parser').CalcNode} node + * @return {import('../parser').CalcNode} + */ function applyNumberMultiplication(node, multiplier) { - if (isValueType(node.type)) { + if (isValueType(node)) { node.value *= multiplier; return node; } @@ -214,6 +253,11 @@ function applyNumberMultiplication(node, multiplier) { } } +/** + * @param {import('../parser').ValueExpression} left + * @param {import('../parser').ValueExpression} right + * @param {number} precision + */ function convertNodesUnits(left, right, precision) { switch (left.type) { case 'LengthValue': @@ -237,6 +281,10 @@ function convertNodesUnits(left, right, precision) { } } +/** + * @param {import('../parser').CalcNode} node + * @param {number} precision + */ function reduce(node, precision) { if (node.type === "MathExpression") { if (isAddSubOperator(node.operator)) { @@ -247,9 +295,9 @@ function reduce(node, precision) { node.right = reduce(node.right, precision); switch (node.operator) { case "/": - return reduceDivisionExpression(node, precision); + return reduceDivisionExpression(node); case "*": - return reduceMultiplicationExpression(node, precision); + return reduceMultiplicationExpression(node); } return node; diff --git a/src/lib/stringifier.js b/src/lib/stringifier.js index 40f92f5..3dda7a4 100644 --- a/src/lib/stringifier.js +++ b/src/lib/stringifier.js @@ -5,6 +5,10 @@ const order = { "-": 1, }; +/** + * @param {number} value + * @param {number | false} prec + */ function round(value, prec) { if (prec !== false) { const precision = Math.pow(10, prec); @@ -13,12 +17,15 @@ function round(value, prec) { return value; } +/** + * @param {number | false} prec + * @param {import('../parser').CalcNode} node + */ function stringify(node, prec) { switch (node.type) { case "MathExpression": { const {left, right, operator: op} = node; let str = ""; - if (left.type === 'MathExpression' && order[op] < order[left.operator]) { str += `(${stringify(left, prec)})`; } else { @@ -36,14 +43,24 @@ function stringify(node, prec) { return str; } case 'Number': - return round(node.value, prec); + return round(node.value, prec).toString(); case 'Function': - return node.value; + return node.value.toString(); default: return round(node.value, prec) + node.unit; } } +/** + * @param {string} calc + * @param {import('../parser').CalcNode} node + * @param {string} originalValue + * @param {{precision: number | false, warnWhenCannotResolve: boolean}} options + * @param {import("postcss").Result} result + * @param {import("postcss").ChildNode} item + * + * @returns {string} + */ export default function ( calc, node, diff --git a/src/lib/transform.js b/src/lib/transform.js index f19122c..ae61357 100644 --- a/src/lib/transform.js +++ b/src/lib/transform.js @@ -9,12 +9,18 @@ import stringifier from "./stringifier"; const MATCH_CALC = /((?:-(moz|webkit)-)?calc)/i; +/** + * @param {string} value + * @param {{precision: number, warnWhenCannotResolve: boolean}} options + * @param {import("postcss").Result} result + * @param {import("postcss").ChildNode} item + */ function transformValue(value, options, result, item) { return valueParser(value) .walk(node => { // skip anything which isn't a calc() function if (node.type !== "function" || !MATCH_CALC.test(node.value)) { - return node; + return; } // stringify calc expression and produce an AST @@ -26,7 +32,7 @@ function transformValue(value, options, result, item) { const reducedAst = reducer(ast, options.precision); // stringify AST and write it back - node.type = "word"; + (/** @type {valueParser.Node} */(node)).type = "word"; node.value = stringifier( node.value, reducedAst, @@ -40,7 +46,12 @@ function transformValue(value, options, result, item) { }) .toString(); } - +/** + * @param {import("postcss-selector-parser").Selectors} value + * @param {{precision: number, warnWhenCannotResolve: boolean}} options + * @param {import("postcss").Result} result + * @param {import("postcss").ChildNode} item + */ function transformSelector(value, options, result, item) { return selectorParser(selectors => { selectors.walk(node => { @@ -61,6 +72,13 @@ function transformSelector(value, options, result, item) { }).processSync(value); } + +/** + * @param {any} node + * @param {{precision: number, preserve: boolean, warnWhenCannotResolve: boolean}} options + * @param {'value'|'params'|'selector'} property + * @param {import("postcss").Result} result + */ export default (node, property, options, result) => { let value = node[property]; @@ -70,8 +88,11 @@ export default (node, property, options, result) => { ? transformSelector(node[property], options, result, node) : transformValue(node[property], options, result, node); } catch (error) { - result.warn(error.message, { node }); - + if (error instanceof Error) { + result.warn(error.message, { node }); + } else { + result.warn('Error', { node }); + } return; } diff --git a/src/parser.d.ts b/src/parser.d.ts new file mode 100644 index 0000000..ede1661 --- /dev/null +++ b/src/parser.d.ts @@ -0,0 +1,46 @@ +export interface MathExpression { + type: 'MathExpression'; + right: CalcNode; + left: CalcNode; + operator: '*' | '+' | '-' | '/'; +} + +export interface DimensionExpression { + type: + | 'LengthValue' + | 'AngleValue' + | 'TimeValue' + | 'FrequencyValue' + | 'PercentageValue' + | 'ResolutionValue' + | 'EmValue' + | 'ExValue' + | 'ChValue' + | 'RemValue' + | 'VhValue' + | 'VwValue' + | 'VminValue' + | 'VmaxValue'; + value: number; + unit: string; +} + +export interface NumberExpression { + type: 'Number'; + value: number; +} + +export interface FunctionExpression { + type: 'Function'; + value: string; +} + +export type ValueExpression = DimensionExpression | NumberExpression; + +export type CalcNode = MathExpression | ValueExpression | FunctionExpression; + +export interface Parser { + parse: (arg: string) => CalcNode; +} + +export const parser: Parser; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8b0b4b9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "commonjs", + "allowJs": true, + "checkJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "noEmitOnError": true, + "newLine": "lf", + "outDir": "types", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + }, + "include": ["src/*"] +} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..e7cf5f7 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,27 @@ +export default pluginCreator; +/** + * @param {{precision?: number | false, + * preserve?: boolean, + * warnWhenCannotResolve?: boolean, + * mediaQueries?: boolean, + * selectors?: boolean}} opts + */ +declare function pluginCreator(opts: { + precision?: number | false; + preserve?: boolean; + warnWhenCannotResolve?: boolean; + mediaQueries?: boolean; + selectors?: boolean; +}): { + postcssPlugin: string; + /** + * @param {import('postcss').Root} css + * @param {{result: import('postcss').Result}} helpers + */ + OnceExit(css: import('postcss').Root, { result }: { + result: import('postcss').Result; + }): void; +}; +declare namespace pluginCreator { + const postcss: boolean; +} diff --git a/types/lib/convertUnit.d.ts b/types/lib/convertUnit.d.ts new file mode 100644 index 0000000..4e37402 --- /dev/null +++ b/types/lib/convertUnit.d.ts @@ -0,0 +1,8 @@ +export default convertUnit; +/** + * @param {number} value + * @param {string} sourceUnit + * @param {string} targetUnit + * @param {number|false} precision + */ +declare function convertUnit(value: number, sourceUnit: string, targetUnit: string, precision: number | false): number; diff --git a/types/lib/reducer.d.ts b/types/lib/reducer.d.ts new file mode 100644 index 0000000..d84aee9 --- /dev/null +++ b/types/lib/reducer.d.ts @@ -0,0 +1,10 @@ +export default reduce; +export type Collectible = { + preOperator: '+' | '-'; + node: import('../parser').CalcNode; +}; +/** + * @param {import('../parser').CalcNode} node + * @param {number} precision + */ +declare function reduce(node: import('../parser').CalcNode, precision: number): import("../parser").MathExpression | import("../parser").DimensionExpression | import("../parser").NumberExpression | import("../parser").FunctionExpression; diff --git a/types/lib/stringifier.d.ts b/types/lib/stringifier.d.ts new file mode 100644 index 0000000..45e7252 --- /dev/null +++ b/types/lib/stringifier.d.ts @@ -0,0 +1,14 @@ +/** + * @param {string} calc + * @param {import('../parser').CalcNode} node + * @param {string} originalValue + * @param {{precision: number | false, warnWhenCannotResolve: boolean}} options + * @param {import("postcss").Result} result + * @param {import("postcss").ChildNode} item + * + * @returns {string} + */ +export default function _default(calc: string, node: import('../parser').CalcNode, originalValue: string, options: { + precision: number | false; + warnWhenCannotResolve: boolean; +}, result: import("postcss").Result, item: import("postcss").ChildNode): string; diff --git a/types/lib/transform.d.ts b/types/lib/transform.d.ts new file mode 100644 index 0000000..84df13b --- /dev/null +++ b/types/lib/transform.d.ts @@ -0,0 +1,6 @@ +declare function _default(node: any, property: 'value' | 'params' | 'selector', options: { + precision: number; + preserve: boolean; + warnWhenCannotResolve: boolean; +}, result: import("postcss").Result): void; +export default _default; From 7238d28f749cf0fc552719dd9654937edfbba5f7 Mon Sep 17 00:00:00 2001 From: Ludovico Fischer Date: Thu, 6 Jan 2022 16:57:59 +0100 Subject: [PATCH 2/3] feat: introduce plugin options type --- src/index.js | 6 ++++-- types/index.d.ts | 21 ++++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/index.js b/src/index.js index 00173b7..d08915a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,13 @@ import transform from './lib/transform'; /** - * @param {{precision?: number | false, + * @typedef {{precision?: number | false, * preserve?: boolean, * warnWhenCannotResolve?: boolean, * mediaQueries?: boolean, - * selectors?: boolean}} opts + * selectors?: boolean}} PostCssCalcOptions + * + * @param {PostCssCalcOptions} opts */ function pluginCreator(opts) { const options = Object.assign({ diff --git a/types/index.d.ts b/types/index.d.ts index e7cf5f7..ef5b678 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,18 +1,21 @@ export default pluginCreator; -/** - * @param {{precision?: number | false, - * preserve?: boolean, - * warnWhenCannotResolve?: boolean, - * mediaQueries?: boolean, - * selectors?: boolean}} opts - */ -declare function pluginCreator(opts: { +export type PostCssCalcOptions = { precision?: number | false; preserve?: boolean; warnWhenCannotResolve?: boolean; mediaQueries?: boolean; selectors?: boolean; -}): { +}; +/** + * @typedef {{precision?: number | false, + * preserve?: boolean, + * warnWhenCannotResolve?: boolean, + * mediaQueries?: boolean, + * selectors?: boolean}} PostCssCalcOptions + * + * @param {PostCssCalcOptions} opts + */ +declare function pluginCreator(opts: PostCssCalcOptions): { postcssPlugin: string; /** * @param {import('postcss').Root} css From 9f8cd91e6ba00b51567dcbdcb042d04a20bb1378 Mon Sep 17 00:00:00 2001 From: Ludovico Fischer Date: Thu, 6 Jan 2022 18:33:12 +0100 Subject: [PATCH 3/3] fix: check that firstZeroItem is not undefined --- src/lib/reducer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/reducer.js b/src/lib/reducer.js index 7851f91..7ad3f6e 100644 --- a/src/lib/reducer.js +++ b/src/lib/reducer.js @@ -124,7 +124,9 @@ function reduceAddSubExpression(node, precision) { !isValueType(firstNonZeroItem.node)) { const firstZeroItem = collected.find((item) => isValueType(item.node) && item.node.value === 0); - withoutZeroItem.unshift(/** @type Collectible*/(firstZeroItem)) + if (firstZeroItem) { + withoutZeroItem.unshift(firstZeroItem) + } } // make sure the preOperator of the first item is +