Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add types #155

Merged
merged 3 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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__"
},
Expand All @@ -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": {
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import transform from './lib/transform';

/**
* @param {{precision?: number | false,
* preserve?: boolean,
* warnWhenCannotResolve?: boolean,
* mediaQueries?: boolean,
* selectors?: boolean}} opts
*/
ludofischer marked this conversation as resolved.
Show resolved Hide resolved
function pluginCreator(opts) {
const options = Object.assign({
precision: 5,
Expand All @@ -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);
}
Expand Down
12 changes: 10 additions & 2 deletions src/lib/convertUnit.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @type {{[key:string]: {[key:string]: number}}}
*/
const conversions = {
// Absolute length units
'px': {
Expand Down Expand Up @@ -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();
Expand All @@ -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);
ludofischer marked this conversation as resolved.
Show resolved Hide resolved

return Math.round(converted * precision) / precision;
}
Expand Down
92 changes: 70 additions & 22 deletions src/lib/reducer.js
Original file line number Diff line number Diff line change
@@ -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) {
ludofischer marked this conversation as resolved.
Show resolved Hide resolved
switch (node.type) {
case 'LengthValue':
case 'AngleValue':
case 'TimeValue':
Expand All @@ -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 = '+';
Expand All @@ -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;
Expand All @@ -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))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential bug? I don't see why firstZeroItem could not be undefined. Without this case TypeScript complains every time you get an element out of collected.

Copy link
Collaborator

@alexander-akait alexander-akait Jan 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, but firstZeroItem can be undefined, we should add check firstNonZeroItem && firstNonZeroItem.preOperator === '-', but it is regarding how ts do checks, because logic is fine here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've replaced the cast with checking if firstZeroItem is truthy before unshifting it. It makes no sense no add an undefined or null item to the array anyway.

}

// 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 = '+';
}
Expand All @@ -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;
}

Expand All @@ -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;
}
Expand Down Expand Up @@ -169,7 +201,9 @@ function applyNumberDivision(node, divisor) {
}
}
}

/**
* @param {import('../parser').MathExpression} node
*/
function reduceMultiplicationExpression(node) {
// (expr) * number
if (node.right.type === 'Number') {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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':
Expand All @@ -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)) {
Expand All @@ -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);
ludofischer marked this conversation as resolved.
Show resolved Hide resolved
case "*":
return reduceMultiplicationExpression(node, precision);
return reduceMultiplicationExpression(node);
}

return node;
Expand Down
23 changes: 20 additions & 3 deletions src/lib/stringifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -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,
Expand Down
Loading