first commit

This commit is contained in:
2026-09-08 20:28:54 +08:00
commit 2ada8d3d5a
37380 changed files with 4886169 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 Maxime Thirouin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+153
View File
@@ -0,0 +1,153 @@
# PostCSS Calc [<img src="https://postcss.github.io/postcss/logo.svg" alt="PostCSS" width="90" height="90" align="right">][PostCSS]
[![NPM Version][npm-img]][npm-url]
[![Support Chat][git-img]][git-url]
[PostCSS Calc] lets you reduce `calc()` references whenever it's possible.
When multiple units are mixed together in the same expression, the `calc()`
statement is left as is, to fallback to the [W3C calc() implementation].
## Installation
```bash
npm install postcss-calc
```
## Usage
```js
// dependencies
var fs = require("fs")
var postcss = require("postcss")
var calc = require("postcss-calc")
// css to be processed
var css = fs.readFileSync("input.css", "utf8")
// process css
var output = postcss()
.use(calc())
.process(css)
.css
```
Using this `input.css`:
```css
h1 {
font-size: calc(16px * 2);
height: calc(100px - 2em);
width: calc(2*var(--base-width));
margin-bottom: calc(16px * 1.5);
}
```
you will get:
```css
h1 {
font-size: 32px;
height: calc(100px - 2em);
width: calc(2*var(--base-width));
margin-bottom: 24px
}
```
Checkout [tests] for more examples.
### Options
#### `precision` (default: `5`)
Allow you to define the precision for decimal numbers.
```js
var out = postcss()
.use(calc({precision: 10}))
.process(css)
.css
```
#### `preserve` (default: `false`)
Allow you to preserve calc() usage in output so browsers will handle decimal
precision themselves.
```js
var out = postcss()
.use(calc({preserve: true}))
.process(css)
.css
```
#### `warnWhenCannotResolve` (default: `false`)
Adds warnings when calc() are not reduced to a single value.
```js
var out = postcss()
.use(calc({warnWhenCannotResolve: true}))
.process(css)
.css
```
#### `mediaQueries` (default: `false`)
Allows calc() usage as part of media query declarations.
```js
var out = postcss()
.use(calc({mediaQueries: true}))
.process(css)
.css
```
#### `selectors` (default: `false`)
Allows calc() usage as part of selectors.
```js
var out = postcss()
.use(calc({selectors: true}))
.process(css)
.css
```
Example:
```css
div[data-size="calc(3*3)"] {
width: 100px;
}
```
---
## Related PostCSS plugins
To replace the value of CSS custom properties at build time, try [PostCSS Custom Properties].
## Contributing
Work on a branch, install dev-dependencies, respect coding style & run tests
before submitting a bug fix or a feature.
```bash
git clone git@github.com:postcss/postcss-calc.git
git checkout -b patch-1
npm install
npm test
```
## [Changelog](CHANGELOG.md)
## [License](LICENSE)
[git-img]: https://img.shields.io/badge/support-chat-blue.svg
[git-url]: https://gitter.im/postcss/postcss
[npm-img]: https://img.shields.io/npm/v/postcss-calc.svg
[npm-url]: https://www.npmjs.com/package/postcss-calc
[PostCSS]: https://github.com/postcss
[PostCSS Calc]: https://github.com/postcss/postcss-calc
[PostCSS Custom Properties]: https://github.com/postcss/postcss-custom-properties
[tests]: src/__tests__/index.js
[W3C calc() implementation]: https://www.w3.org/TR/css3-values/#calc-notation
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) Bogdan Chadkin <trysound@yandex.ru>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
+263
View File
@@ -0,0 +1,263 @@
# postcss-value-parser
[![Travis CI](https://travis-ci.org/TrySound/postcss-value-parser.svg)](https://travis-ci.org/TrySound/postcss-value-parser)
Transforms CSS declaration values and at-rule parameters into a tree of nodes, and provides a simple traversal API.
## Usage
```js
var valueParser = require('postcss-value-parser');
var cssBackgroundValue = 'url(foo.png) no-repeat 40px 73%';
var parsedValue = valueParser(cssBackgroundValue);
// parsedValue exposes an API described below,
// e.g. parsedValue.walk(..), parsedValue.toString(), etc.
```
For example, parsing the value `rgba(233, 45, 66, .5)` will return the following:
```js
{
nodes: [
{
type: 'function',
value: 'rgba',
before: '',
after: '',
nodes: [
{ type: 'word', value: '233' },
{ type: 'div', value: ',', before: '', after: ' ' },
{ type: 'word', value: '45' },
{ type: 'div', value: ',', before: '', after: ' ' },
{ type: 'word', value: '66' },
{ type: 'div', value: ',', before: ' ', after: '' },
{ type: 'word', value: '.5' }
]
}
]
}
```
If you wanted to convert each `rgba()` value in `sourceCSS` to a hex value, you could do so like this:
```js
var valueParser = require('postcss-value-parser');
var parsed = valueParser(sourceCSS);
// walk() will visit all the of the nodes in the tree,
// invoking the callback for each.
parsed.walk(function (node) {
// Since we only want to transform rgba() values,
// we can ignore anything else.
if (node.type !== 'function' && node.value !== 'rgba') return;
// We can make an array of the rgba() arguments to feed to a
// convertToHex() function
var color = node.nodes.filter(function (node) {
return node.type === 'word';
}).map(function (node) {
return Number(node.value);
}); // [233, 45, 66, .5]
// Now we will transform the existing rgba() function node
// into a word node with the hex value
node.type = 'word';
node.value = convertToHex(color);
})
parsed.toString(); // #E92D42
```
## Nodes
Each node is an object with these common properties:
- **type**: The type of node (`word`, `string`, `div`, `space`, `comment`, or `function`).
Each type is documented below.
- **value**: Each node has a `value` property; but what exactly `value` means
is specific to the node type. Details are documented for each type below.
- **sourceIndex**: The starting index of the node within the original source
string. For example, given the source string `10px 20px`, the `word` node
whose value is `20px` will have a `sourceIndex` of `5`.
### word
The catch-all node type that includes keywords (e.g. `no-repeat`),
quantities (e.g. `20px`, `75%`, `1.5`), and hex colors (e.g. `#e6e6e6`).
Node-specific properties:
- **value**: The "word" itself.
### string
A quoted string value, e.g. `"something"` in `content: "something";`.
Node-specific properties:
- **value**: The text content of the string.
- **quote**: The quotation mark surrounding the string, either `"` or `'`.
- **unclosed**: `true` if the string was not closed properly. e.g. `"unclosed string `.
### div
A divider, for example
- `,` in `animation-duration: 1s, 2s, 3s`
- `/` in `border-radius: 10px / 23px`
- `:` in `(min-width: 700px)`
Node-specific properties:
- **value**: The divider character. Either `,`, `/`, or `:` (see examples above).
- **before**: Whitespace before the divider.
- **after**: Whitespace after the divider.
### space
Whitespace used as a separator, e.g. ` ` occurring twice in `border: 1px solid black;`.
Node-specific properties:
- **value**: The whitespace itself.
### comment
A CSS comment starts with `/*` and ends with `*/`
Node-specific properties:
- **value**: The comment value without `/*` and `*/`
- **unclosed**: `true` if the comment was not closed properly. e.g. `/* comment without an end `.
### function
A CSS function, e.g. `rgb(0,0,0)` or `url(foo.bar)`.
Function nodes have nodes nested within them: the function arguments.
Additional properties:
- **value**: The name of the function, e.g. `rgb` in `rgb(0,0,0)`.
- **before**: Whitespace after the opening parenthesis and before the first argument,
e.g. ` ` in `rgb( 0,0,0)`.
- **after**: Whitespace before the closing parenthesis and after the last argument,
e.g. ` ` in `rgb(0,0,0 )`.
- **nodes**: More nodes representing the arguments to the function.
- **unclosed**: `true` if the parentheses was not closed properly. e.g. `( unclosed-function `.
Media features surrounded by parentheses are considered functions with an
empty value. For example, `(min-width: 700px)` parses to these nodes:
```js
[
{
type: 'function', value: '', before: '', after: '',
nodes: [
{ type: 'word', value: 'min-width' },
{ type: 'div', value: ':', before: '', after: ' ' },
{ type: 'word', value: '700px' }
]
}
]
```
`url()` functions can be parsed a little bit differently depending on
whether the first character in the argument is a quotation mark.
`url( /gfx/img/bg.jpg )` parses to:
```js
{ type: 'function', sourceIndex: 0, value: 'url', before: ' ', after: ' ', nodes: [
{ type: 'word', sourceIndex: 5, value: '/gfx/img/bg.jpg' }
] }
```
`url( "/gfx/img/bg.jpg" )`, on the other hand, parses to:
```js
{ type: 'function', sourceIndex: 0, value: 'url', before: ' ', after: ' ', nodes: [
type: 'string', sourceIndex: 5, quote: '"', value: '/gfx/img/bg.jpg' },
] }
```
### unicode-range
The unicode-range CSS descriptor sets the specific range of characters to be
used from a font defined by @font-face and made available
for use on the current page (`unicode-range: U+0025-00FF`).
Node-specific properties:
- **value**: The "unicode-range" itself.
## API
```
var valueParser = require('postcss-value-parser');
```
### valueParser.unit(quantity)
Parses `quantity`, distinguishing the number from the unit. Returns an object like the following:
```js
// Given 2rem
{
number: '2',
unit: 'rem'
}
```
If the `quantity` argument cannot be parsed as a number, returns `false`.
*This function does not parse complete values*: you cannot pass it `1px solid black` and expect `px` as
the unit. Instead, you should pass it single quantities only. Parse `1px solid black`, then pass it
the stringified `1px` node (a `word` node) to parse the number and unit.
### valueParser.stringify(nodes[, custom])
Stringifies a node or array of nodes.
The `custom` function is called for each `node`; return a string to override the default behaviour.
### valueParser.walk(nodes, callback[, bubble])
Walks each provided node, recursively walking all descendent nodes within functions.
Returning `false` in the `callback` will prevent traversal of descendent nodes (within functions).
You can use this feature to for shallow iteration, walking over only the *immediate* children.
*Note: This only applies if `bubble` is `false` (which is the default).*
By default, the tree is walked from the outermost node inwards.
To reverse the direction, pass `true` for the `bubble` argument.
The `callback` is invoked with three arguments: `callback(node, index, nodes)`.
- `node`: The current node.
- `index`: The index of the current node.
- `nodes`: The complete nodes array passed to `walk()`.
Returns the `valueParser` instance.
### var parsed = valueParser(value)
Returns the parsed node tree.
### parsed.nodes
The array of nodes.
### parsed.toString()
Stringifies the node tree.
### parsed.walk(callback[, bubble])
Walks each node inside `parsed.nodes`. See the documentation for `valueParser.walk()` above.
# License
MIT © [Bogdan Chadkin](mailto:trysound@yandex.ru)
@@ -0,0 +1,177 @@
declare namespace postcssValueParser {
interface BaseNode {
/**
* The offset, inclusive, inside the CSS value at which the node starts.
*/
sourceIndex: number;
/**
* The offset, exclusive, inside the CSS value at which the node ends.
*/
sourceEndIndex: number;
/**
* The node's characteristic value
*/
value: string;
}
interface ClosableNode {
/**
* Whether the parsed CSS value ended before the node was properly closed
*/
unclosed?: true;
}
interface AdjacentAwareNode {
/**
* The token at the start of the node
*/
before: string;
/**
* The token at the end of the node
*/
after: string;
}
interface CommentNode extends BaseNode, ClosableNode {
type: "comment";
}
interface DivNode extends BaseNode, AdjacentAwareNode {
type: "div";
}
interface FunctionNode extends BaseNode, ClosableNode, AdjacentAwareNode {
type: "function";
/**
* Nodes inside the function
*/
nodes: Node[];
}
interface SpaceNode extends BaseNode {
type: "space";
}
interface StringNode extends BaseNode, ClosableNode {
type: "string";
/**
* The quote type delimiting the string
*/
quote: '"' | "'";
}
interface UnicodeRangeNode extends BaseNode {
type: "unicode-range";
}
interface WordNode extends BaseNode {
type: "word";
}
/**
* Any node parsed from a CSS value
*/
type Node =
| CommentNode
| DivNode
| FunctionNode
| SpaceNode
| StringNode
| UnicodeRangeNode
| WordNode;
interface CustomStringifierCallback {
/**
* @param node The node to stringify
* @returns The serialized CSS representation of the node
*/
(nodes: Node): string | undefined;
}
interface WalkCallback {
/**
* @param node The currently visited node
* @param index The index of the node in the series of parsed nodes
* @param nodes The series of parsed nodes
* @returns Returning `false` will prevent traversal of descendant nodes (only applies if `bubble` was set to `true` in the `walk()` call)
*/
(node: Node, index: number, nodes: Node[]): void | boolean;
}
/**
* A CSS dimension, decomposed into its numeric and unit parts
*/
interface Dimension {
number: string;
unit: string;
}
/**
* A wrapper around a parsed CSS value that allows for inspecting and walking nodes
*/
interface ParsedValue {
/**
* The series of parsed nodes
*/
nodes: Node[];
/**
* Walk all parsed nodes, applying a callback
*
* @param callback A visitor callback that will be executed for each node
* @param bubble When set to `true`, walking will be done inside-out instead of outside-in
*/
walk(callback: WalkCallback, bubble?: boolean): this;
}
interface ValueParser {
/**
* Decompose a CSS dimension into its numeric and unit part
*
* @param value The dimension to decompose
* @returns An object representing `number` and `unit` part of the dimension or `false` if the decomposing fails
*/
unit(value: string): Dimension | false;
/**
* Serialize a series of nodes into a CSS value
*
* @param nodes The nodes to stringify
* @param custom A custom stringifier callback
* @returns The generated CSS value
*/
stringify(nodes: Node | Node[], custom?: CustomStringifierCallback): string;
/**
* Walk a series of nodes, applying a callback
*
* @param nodes The nodes to walk
* @param callback A visitor callback that will be executed for each node
* @param bubble When set to `true`, walking will be done inside-out instead of outside-in
*/
walk(nodes: Node[], callback: WalkCallback, bubble?: boolean): void;
/**
* Parse a CSS value into a series of nodes to operate on
*
* @param value The value to parse
*/
new (value: string): ParsedValue;
/**
* Parse a CSS value into a series of nodes to operate on
*
* @param value The value to parse
*/
(value: string): ParsedValue;
}
}
declare const postcssValueParser: postcssValueParser.ValueParser;
export = postcssValueParser;
@@ -0,0 +1,28 @@
var parse = require("./parse");
var walk = require("./walk");
var stringify = require("./stringify");
function ValueParser(value) {
if (this instanceof ValueParser) {
this.nodes = parse(value);
return this;
}
return new ValueParser(value);
}
ValueParser.prototype.toString = function() {
return Array.isArray(this.nodes) ? stringify(this.nodes) : "";
};
ValueParser.prototype.walk = function(cb, bubble) {
walk(this.nodes, cb, bubble);
return this;
};
ValueParser.unit = require("./unit");
ValueParser.walk = walk;
ValueParser.stringify = stringify;
module.exports = ValueParser;
@@ -0,0 +1,321 @@
var openParentheses = "(".charCodeAt(0);
var closeParentheses = ")".charCodeAt(0);
var singleQuote = "'".charCodeAt(0);
var doubleQuote = '"'.charCodeAt(0);
var backslash = "\\".charCodeAt(0);
var slash = "/".charCodeAt(0);
var comma = ",".charCodeAt(0);
var colon = ":".charCodeAt(0);
var star = "*".charCodeAt(0);
var uLower = "u".charCodeAt(0);
var uUpper = "U".charCodeAt(0);
var plus = "+".charCodeAt(0);
var isUnicodeRange = /^[a-f0-9?-]+$/i;
module.exports = function(input) {
var tokens = [];
var value = input;
var next,
quote,
prev,
token,
escape,
escapePos,
whitespacePos,
parenthesesOpenPos;
var pos = 0;
var code = value.charCodeAt(pos);
var max = value.length;
var stack = [{ nodes: tokens }];
var balanced = 0;
var parent;
var name = "";
var before = "";
var after = "";
while (pos < max) {
// Whitespaces
if (code <= 32) {
next = pos;
do {
next += 1;
code = value.charCodeAt(next);
} while (code <= 32);
token = value.slice(pos, next);
prev = tokens[tokens.length - 1];
if (code === closeParentheses && balanced) {
after = token;
} else if (prev && prev.type === "div") {
prev.after = token;
prev.sourceEndIndex += token.length;
} else if (
code === comma ||
code === colon ||
(code === slash &&
value.charCodeAt(next + 1) !== star &&
(!parent ||
(parent && parent.type === "function" && parent.value !== "calc")))
) {
before = token;
} else {
tokens.push({
type: "space",
sourceIndex: pos,
sourceEndIndex: next,
value: token
});
}
pos = next;
// Quotes
} else if (code === singleQuote || code === doubleQuote) {
next = pos;
quote = code === singleQuote ? "'" : '"';
token = {
type: "string",
sourceIndex: pos,
quote: quote
};
do {
escape = false;
next = value.indexOf(quote, next + 1);
if (~next) {
escapePos = next;
while (value.charCodeAt(escapePos - 1) === backslash) {
escapePos -= 1;
escape = !escape;
}
} else {
value += quote;
next = value.length - 1;
token.unclosed = true;
}
} while (escape);
token.value = value.slice(pos + 1, next);
token.sourceEndIndex = token.unclosed ? next : next + 1;
tokens.push(token);
pos = next + 1;
code = value.charCodeAt(pos);
// Comments
} else if (code === slash && value.charCodeAt(pos + 1) === star) {
next = value.indexOf("*/", pos);
token = {
type: "comment",
sourceIndex: pos,
sourceEndIndex: next + 2
};
if (next === -1) {
token.unclosed = true;
next = value.length;
token.sourceEndIndex = next;
}
token.value = value.slice(pos + 2, next);
tokens.push(token);
pos = next + 2;
code = value.charCodeAt(pos);
// Operation within calc
} else if (
(code === slash || code === star) &&
parent &&
parent.type === "function" &&
parent.value === "calc"
) {
token = value[pos];
tokens.push({
type: "word",
sourceIndex: pos - before.length,
sourceEndIndex: pos + token.length,
value: token
});
pos += 1;
code = value.charCodeAt(pos);
// Dividers
} else if (code === slash || code === comma || code === colon) {
token = value[pos];
tokens.push({
type: "div",
sourceIndex: pos - before.length,
sourceEndIndex: pos + token.length,
value: token,
before: before,
after: ""
});
before = "";
pos += 1;
code = value.charCodeAt(pos);
// Open parentheses
} else if (openParentheses === code) {
// Whitespaces after open parentheses
next = pos;
do {
next += 1;
code = value.charCodeAt(next);
} while (code <= 32);
parenthesesOpenPos = pos;
token = {
type: "function",
sourceIndex: pos - name.length,
value: name,
before: value.slice(parenthesesOpenPos + 1, next)
};
pos = next;
if (name === "url" && code !== singleQuote && code !== doubleQuote) {
next -= 1;
do {
escape = false;
next = value.indexOf(")", next + 1);
if (~next) {
escapePos = next;
while (value.charCodeAt(escapePos - 1) === backslash) {
escapePos -= 1;
escape = !escape;
}
} else {
value += ")";
next = value.length - 1;
token.unclosed = true;
}
} while (escape);
// Whitespaces before closed
whitespacePos = next;
do {
whitespacePos -= 1;
code = value.charCodeAt(whitespacePos);
} while (code <= 32);
if (parenthesesOpenPos < whitespacePos) {
if (pos !== whitespacePos + 1) {
token.nodes = [
{
type: "word",
sourceIndex: pos,
sourceEndIndex: whitespacePos + 1,
value: value.slice(pos, whitespacePos + 1)
}
];
} else {
token.nodes = [];
}
if (token.unclosed && whitespacePos + 1 !== next) {
token.after = "";
token.nodes.push({
type: "space",
sourceIndex: whitespacePos + 1,
sourceEndIndex: next,
value: value.slice(whitespacePos + 1, next)
});
} else {
token.after = value.slice(whitespacePos + 1, next);
token.sourceEndIndex = next;
}
} else {
token.after = "";
token.nodes = [];
}
pos = next + 1;
token.sourceEndIndex = token.unclosed ? next : pos;
code = value.charCodeAt(pos);
tokens.push(token);
} else {
balanced += 1;
token.after = "";
token.sourceEndIndex = pos + 1;
tokens.push(token);
stack.push(token);
tokens = token.nodes = [];
parent = token;
}
name = "";
// Close parentheses
} else if (closeParentheses === code && balanced) {
pos += 1;
code = value.charCodeAt(pos);
parent.after = after;
parent.sourceEndIndex += after.length;
after = "";
balanced -= 1;
stack[stack.length - 1].sourceEndIndex = pos;
stack.pop();
parent = stack[balanced];
tokens = parent.nodes;
// Words
} else {
next = pos;
do {
if (code === backslash) {
next += 1;
}
next += 1;
code = value.charCodeAt(next);
} while (
next < max &&
!(
code <= 32 ||
code === singleQuote ||
code === doubleQuote ||
code === comma ||
code === colon ||
code === slash ||
code === openParentheses ||
(code === star &&
parent &&
parent.type === "function" &&
parent.value === "calc") ||
(code === slash &&
parent.type === "function" &&
parent.value === "calc") ||
(code === closeParentheses && balanced)
)
);
token = value.slice(pos, next);
if (openParentheses === code) {
name = token;
} else if (
(uLower === token.charCodeAt(0) || uUpper === token.charCodeAt(0)) &&
plus === token.charCodeAt(1) &&
isUnicodeRange.test(token.slice(2))
) {
tokens.push({
type: "unicode-range",
sourceIndex: pos,
sourceEndIndex: next,
value: token
});
} else {
tokens.push({
type: "word",
sourceIndex: pos,
sourceEndIndex: next,
value: token
});
}
pos = next;
}
}
for (pos = stack.length - 1; pos; pos -= 1) {
stack[pos].unclosed = true;
stack[pos].sourceEndIndex = value.length;
}
return stack[0].nodes;
};
@@ -0,0 +1,48 @@
function stringifyNode(node, custom) {
var type = node.type;
var value = node.value;
var buf;
var customResult;
if (custom && (customResult = custom(node)) !== undefined) {
return customResult;
} else if (type === "word" || type === "space") {
return value;
} else if (type === "string") {
buf = node.quote || "";
return buf + value + (node.unclosed ? "" : buf);
} else if (type === "comment") {
return "/*" + value + (node.unclosed ? "" : "*/");
} else if (type === "div") {
return (node.before || "") + value + (node.after || "");
} else if (Array.isArray(node.nodes)) {
buf = stringify(node.nodes, custom);
if (type !== "function") {
return buf;
}
return (
value +
"(" +
(node.before || "") +
buf +
(node.after || "") +
(node.unclosed ? "" : ")")
);
}
return value;
}
function stringify(nodes, custom) {
var result, i;
if (Array.isArray(nodes)) {
result = "";
for (i = nodes.length - 1; ~i; i -= 1) {
result = stringifyNode(nodes[i], custom) + result;
}
return result;
}
return stringifyNode(nodes, custom);
}
module.exports = stringify;
+120
View File
@@ -0,0 +1,120 @@
var minus = "-".charCodeAt(0);
var plus = "+".charCodeAt(0);
var dot = ".".charCodeAt(0);
var exp = "e".charCodeAt(0);
var EXP = "E".charCodeAt(0);
// Check if three code points would start a number
// https://www.w3.org/TR/css-syntax-3/#starts-with-a-number
function likeNumber(value) {
var code = value.charCodeAt(0);
var nextCode;
if (code === plus || code === minus) {
nextCode = value.charCodeAt(1);
if (nextCode >= 48 && nextCode <= 57) {
return true;
}
var nextNextCode = value.charCodeAt(2);
if (nextCode === dot && nextNextCode >= 48 && nextNextCode <= 57) {
return true;
}
return false;
}
if (code === dot) {
nextCode = value.charCodeAt(1);
if (nextCode >= 48 && nextCode <= 57) {
return true;
}
return false;
}
if (code >= 48 && code <= 57) {
return true;
}
return false;
}
// Consume a number
// https://www.w3.org/TR/css-syntax-3/#consume-number
module.exports = function(value) {
var pos = 0;
var length = value.length;
var code;
var nextCode;
var nextNextCode;
if (length === 0 || !likeNumber(value)) {
return false;
}
code = value.charCodeAt(pos);
if (code === plus || code === minus) {
pos++;
}
while (pos < length) {
code = value.charCodeAt(pos);
if (code < 48 || code > 57) {
break;
}
pos += 1;
}
code = value.charCodeAt(pos);
nextCode = value.charCodeAt(pos + 1);
if (code === dot && nextCode >= 48 && nextCode <= 57) {
pos += 2;
while (pos < length) {
code = value.charCodeAt(pos);
if (code < 48 || code > 57) {
break;
}
pos += 1;
}
}
code = value.charCodeAt(pos);
nextCode = value.charCodeAt(pos + 1);
nextNextCode = value.charCodeAt(pos + 2);
if (
(code === exp || code === EXP) &&
((nextCode >= 48 && nextCode <= 57) ||
((nextCode === plus || nextCode === minus) &&
nextNextCode >= 48 &&
nextNextCode <= 57))
) {
pos += nextCode === plus || nextCode === minus ? 3 : 2;
while (pos < length) {
code = value.charCodeAt(pos);
if (code < 48 || code > 57) {
break;
}
pos += 1;
}
}
return {
number: value.slice(0, pos),
unit: value.slice(pos)
};
};
@@ -0,0 +1,22 @@
module.exports = function walk(nodes, cb, bubble) {
var i, max, node, result;
for (i = 0, max = nodes.length; i < max; i += 1) {
node = nodes[i];
if (!bubble) {
result = cb(node, i, nodes);
}
if (
result !== false &&
node.type === "function" &&
Array.isArray(node.nodes)
) {
walk(node.nodes, cb, bubble);
}
if (bubble) {
cb(node, i, nodes);
}
}
};
@@ -0,0 +1,58 @@
{
"name": "postcss-value-parser",
"version": "4.2.0",
"description": "Transforms css values and at-rule params into the tree",
"main": "lib/index.js",
"files": [
"lib"
],
"devDependencies": {
"eslint": "^5.16.0",
"husky": "^2.3.0",
"lint-staged": "^8.1.7",
"prettier": "^1.17.1",
"tap-spec": "^5.0.0",
"tape": "^4.10.2"
},
"scripts": {
"lint:prettier": "prettier \"**/*.js\" \"**/*.ts\" --list-different",
"lint:js": "eslint . --cache",
"lint": "yarn lint:js && yarn lint:prettier",
"pretest": "yarn lint",
"test": "tape test/*.js | tap-spec"
},
"eslintConfig": {
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended"
},
"lint-staged": {
"*.js": [
"eslint",
"prettier --write",
"git add"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"author": "Bogdan Chadkin <trysound@yandex.ru>",
"license": "MIT",
"homepage": "https://github.com/TrySound/postcss-value-parser",
"repository": {
"type": "git",
"url": "https://github.com/TrySound/postcss-value-parser.git"
},
"keywords": [
"postcss",
"value",
"parser"
],
"bugs": {
"url": "https://github.com/TrySound/postcss-value-parser/issues"
}
}
+62
View File
@@ -0,0 +1,62 @@
{
"name": "postcss-calc",
"version": "8.2.4",
"description": "PostCSS plugin to reduce calc()",
"keywords": [
"css",
"postcss",
"postcss-plugin",
"calculation",
"calc"
],
"main": "src/index.js",
"types": "types/index.d.ts",
"files": [
"src",
"types",
"LICENSE"
],
"author": "Andy Jansson",
"license": "MIT",
"repository": "https://github.com/postcss/postcss-calc.git",
"eslintConfig": {
"extends": [
"eslint:recommended",
"prettier"
],
"env": {
"node": true,
"es2017": true
},
"ignorePatterns": [
"src/parser.js"
],
"rules": {
"curly": "error"
}
},
"devDependencies": {
"@types/node": "^17.0.15",
"eslint": "^8.8.0",
"eslint-config-prettier": "^8.3.0",
"jison-gho": "^0.6.1-216",
"postcss": "^8.2.2",
"prettier": "^2.5.1",
"typescript": "^4.5.5",
"uvu": "^0.5.3"
},
"dependencies": {
"postcss-selector-parser": "^6.0.9",
"postcss-value-parser": "^4.2.0"
},
"peerDependencies": {
"postcss": "^8.2.2"
},
"scripts": {
"build": "jison src/parser.jison -o src/parser.js",
"lint": "eslint src && tsc",
"pretest": "pnpm run build",
"test": "uvu src/__tests__"
},
"readme": "# PostCSS Calc [<img src=\"https://postcss.github.io/postcss/logo.svg\" alt=\"PostCSS\" width=\"90\" height=\"90\" align=\"right\">][PostCSS]\n\n[![NPM Version][npm-img]][npm-url]\n[![Support Chat][git-img]][git-url]\n\n[PostCSS Calc] lets you reduce `calc()` references whenever it's possible.\nWhen multiple units are mixed together in the same expression, the `calc()`\nstatement is left as is, to fallback to the [W3C calc() implementation].\n\n## Installation\n\n```bash\nnpm install postcss-calc\n```\n\n## Usage\n\n```js\n// dependencies\nvar fs = require(\"fs\")\nvar postcss = require(\"postcss\")\nvar calc = require(\"postcss-calc\")\n\n// css to be processed\nvar css = fs.readFileSync(\"input.css\", \"utf8\")\n\n// process css\nvar output = postcss()\n .use(calc())\n .process(css)\n .css\n```\n\nUsing this `input.css`:\n\n```css\nh1 {\n font-size: calc(16px * 2);\n height: calc(100px - 2em);\n width: calc(2*var(--base-width));\n margin-bottom: calc(16px * 1.5);\n}\n```\n\nyou will get:\n\n```css\nh1 {\n font-size: 32px;\n height: calc(100px - 2em);\n width: calc(2*var(--base-width));\n margin-bottom: 24px\n}\n```\nCheckout [tests] for more examples.\n\n### Options\n\n#### `precision` (default: `5`)\n\nAllow you to define the precision for decimal numbers.\n\n```js\nvar out = postcss()\n .use(calc({precision: 10}))\n .process(css)\n .css\n```\n\n#### `preserve` (default: `false`)\n\nAllow you to preserve calc() usage in output so browsers will handle decimal\nprecision themselves.\n\n```js\nvar out = postcss()\n .use(calc({preserve: true}))\n .process(css)\n .css\n```\n\n#### `warnWhenCannotResolve` (default: `false`)\n\nAdds warnings when calc() are not reduced to a single value.\n\n```js\nvar out = postcss()\n .use(calc({warnWhenCannotResolve: true}))\n .process(css)\n .css\n```\n\n#### `mediaQueries` (default: `false`)\n\nAllows calc() usage as part of media query declarations.\n\n```js\nvar out = postcss()\n .use(calc({mediaQueries: true}))\n .process(css)\n .css\n```\n\n#### `selectors` (default: `false`)\n\nAllows calc() usage as part of selectors.\n\n```js\nvar out = postcss()\n .use(calc({selectors: true}))\n .process(css)\n .css\n```\n\nExample:\n\n```css\ndiv[data-size=\"calc(3*3)\"] {\n width: 100px;\n}\n```\n\n---\n\n## Related PostCSS plugins\nTo replace the value of CSS custom properties at build time, try [PostCSS Custom Properties].\n\n## Contributing\n\nWork on a branch, install dev-dependencies, respect coding style & run tests\nbefore submitting a bug fix or a feature.\n\n```bash\ngit clone git@github.com:postcss/postcss-calc.git\ngit checkout -b patch-1\nnpm install\nnpm test\n```\n\n## [Changelog](CHANGELOG.md)\n\n## [License](LICENSE)\n\n[git-img]: https://img.shields.io/badge/support-chat-blue.svg\n[git-url]: https://gitter.im/postcss/postcss\n[npm-img]: https://img.shields.io/npm/v/postcss-calc.svg\n[npm-url]: https://www.npmjs.com/package/postcss-calc\n\n[PostCSS]: https://github.com/postcss\n[PostCSS Calc]: https://github.com/postcss/postcss-calc\n[PostCSS Custom Properties]: https://github.com/postcss/postcss-custom-properties\n[tests]: src/__tests__/index.js\n[W3C calc() implementation]: https://www.w3.org/TR/css3-values/#calc-notation\n"
}
+421
View File
@@ -0,0 +1,421 @@
'use strict';
const { test } = require('uvu');
const assert = require('uvu/assert');
const convertUnit = require('../lib/convertUnit.js');
test('valid conversions', () => {
const conversions = [
// source value, source unit, expected value, target unit
[10, 'px', 10, 'px'],
[10, 'px', 0.26458, 'cm'],
[10, 'px', 2.64583, 'mm'],
[10, 'px', 10.58333, 'q'],
[10, 'px', 0.10417, 'in'],
[10, 'px', 7.5, 'pt'],
[10, 'px', 0.625, 'pc'],
[10, 'cm', 377.95276, 'px'],
[10, 'cm', 10, 'cm'],
[10, 'cm', 100, 'mm'],
[10, 'cm', 400, 'q'],
[10, 'cm', 3.93701, 'in'],
[10, 'cm', 283.46457, 'pt'],
[10, 'cm', 23.62205, 'pc'],
[10, 'mm', 37.79528, 'px'],
[10, 'mm', 1, 'cm'],
[10, 'mm', 10, 'mm'],
[10, 'mm', 40, 'q'],
[10, 'mm', 0.3937, 'in'],
[10, 'mm', 28.34646, 'pt'],
[10, 'mm', 2.3622, 'pc'],
[10, 'q', 9.44882, 'px'],
[10, 'q', 0.25, 'cm'],
[10, 'q', 2.5, 'mm'],
[10, 'q', 0.09843, 'in'],
[10, 'q', 7.08661, 'pt'],
[10, 'q', 0.59055, 'pc'],
[10, 'in', 960, 'px'],
[10, 'in', 25.4, 'cm'],
[10, 'in', 254, 'mm'],
[10, 'in', 1016, 'q'],
[10, 'in', 10, 'in'],
[10, 'in', 720, 'pt'],
[10, 'in', 60, 'pc'],
[10, 'pt', 13.33333, 'px'],
[10, 'pt', 0.35278, 'cm'],
[10, 'pt', 3.52778, 'mm'],
[10, 'pt', 14.11111, 'q'],
[10, 'pt', 0.13889, 'in'],
[10, 'pt', 10, 'pt'],
[10, 'pt', 0.83333, 'pc'],
[10, 'pc', 160, 'px'],
[10, 'pc', 4.23333, 'cm'],
[10, 'pc', 42.33333, 'mm'],
[10, 'pc', 169.33333, 'q'],
[10, 'pc', 1.66667, 'in'],
[10, 'pc', 120, 'pt'],
[10, 'pc', 10, 'pc'],
[10, 'deg', 10, 'deg'],
[10, 'deg', 11.11111, 'grad'],
[10, 'deg', 0.17453, 'rad'],
[10, 'deg', 0.02778, 'turn'],
[10, 'grad', 9, 'deg'],
[10, 'grad', 10, 'grad'],
[10, 'grad', 0.15708, 'rad'],
[10, 'grad', 0.025, 'turn'],
[10, 'rad', 572.9578, 'deg'],
[10, 'rad', 636.61977, 'grad'],
[10, 'rad', 10, 'rad'],
[10, 'rad', 1.59155, 'turn'],
[10, 'turn', 3600, 'deg'],
[10, 'turn', 4000, 'grad'],
[10, 'turn', 62.83185, 'rad'],
[10, 'turn', 10, 'turn'],
[10, 's', 10, 's'],
[10, 's', 10000, 'ms'],
[10, 'ms', 0.01, 's'],
[10, 'ms', 10, 'ms'],
[10, 'Hz', 10, 'Hz'],
[10, 'Hz', 0.01, 'kHz'],
[10, 'kHz', 10000, 'Hz'],
[10, 'kHz', 10, 'kHz'],
[10, 'dpi', 10, 'dpi'],
[10, 'dpi', 25.4, 'dpcm'],
[10, 'dpi', 960, 'dppx'],
[10, 'dpcm', 3.93701, 'dpi'],
[10, 'dpcm', 10, 'dpcm'],
[10, 'dpcm', 377.95276, 'dppx'],
[10, 'dppx', 0.10417, 'dpi'],
[10, 'dppx', 0.26458, 'dpcm'],
[10, 'dppx', 10, 'dppx'],
];
conversions.forEach(function (e) {
const value = e[0];
const unit = e[1];
const expected = e[2];
const targetUnit = e[3];
assert.is(
convertUnit(value, unit, targetUnit),
expected,
unit + ' -> ' + targetUnit
);
});
});
test('invalid conversions', () => {
const invalid_units = {
px: [
'deg',
'grad',
'rad',
'turn',
's',
'ms',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
cm: [
'deg',
'grad',
'rad',
'turn',
's',
'ms',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
mm: [
'deg',
'grad',
'rad',
'turn',
's',
'ms',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
q: [
'deg',
'grad',
'rad',
'turn',
's',
'ms',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
in: [
'deg',
'grad',
'rad',
'turn',
's',
'ms',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
pt: [
'deg',
'grad',
'rad',
'turn',
's',
'ms',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
pc: [
'deg',
'grad',
'rad',
'turn',
's',
'ms',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
deg: [
'px',
'cm',
'mm',
'in',
'pt',
'pc',
's',
'ms',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
grad: [
'px',
'cm',
'mm',
'in',
'pt',
'pc',
's',
'ms',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
rad: [
'px',
'cm',
'mm',
'in',
'pt',
'pc',
's',
'ms',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
turn: [
'px',
'cm',
'mm',
'in',
'pt',
'pc',
's',
'ms',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
s: [
'px',
'cm',
'mm',
'in',
'pt',
'pc',
'deg',
'grad',
'rad',
'turn',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
ms: [
'px',
'cm',
'mm',
'in',
'pt',
'pc',
'deg',
'grad',
'rad',
'turn',
'Hz',
'kHz',
'dpi',
'dpcm',
'dppx',
],
Hz: [
'px',
'cm',
'mm',
'in',
'pt',
'pc',
'deg',
'grad',
'rad',
'turn',
's',
'ms',
'dpi',
'dpcm',
'dppx',
],
kHz: [
'px',
'cm',
'mm',
'in',
'pt',
'pc',
'deg',
'grad',
'rad',
'turn',
's',
'ms',
'dpi',
'dpcm',
'dppx',
],
dpi: [
'px',
'cm',
'mm',
'in',
'pt',
'pc',
'deg',
'grad',
'rad',
'turn',
's',
'ms',
'Hz',
'kHz',
],
dpcm: [
'px',
'cm',
'mm',
'in',
'pt',
'pc',
'deg',
'grad',
'rad',
'turn',
's',
'ms',
'Hz',
'kHz',
],
dppx: [
'px',
'cm',
'mm',
'in',
'pt',
'pc',
'deg',
'grad',
'rad',
'turn',
's',
'ms',
'Hz',
'kHz',
],
};
for (const unit in invalid_units) {
invalid_units[unit].forEach((targetUnit) => {
let failed = false;
try {
convertUnit(10, unit, targetUnit);
} catch (e) {
failed = true;
}
assert.ok(failed, unit + ' -> ' + targetUnit);
});
}
});
test('precision', () => {
const precision = 10;
const conversions = [
// source value, source unit, expected value, target unit
[10, 'px', 0.2645833333, 'cm'],
[10, 'px', 2.6458333333, 'mm'],
[10, 'px', 0.1041666667, 'in'],
[10, 'cm', 377.9527559055, 'px'],
];
conversions.forEach((e) => {
const value = e[0];
const unit = e[1];
const expected = e[2];
const targetUnit = e[3];
assert.is(
convertUnit(value, unit, targetUnit, precision),
expected,
unit + ' -> ' + targetUnit
);
});
});
test('falsey precision', () => {
assert.is(convertUnit(10, 'px', 'cm', false), 0.26458333333333334);
});
test.run();
+903
View File
@@ -0,0 +1,903 @@
'use strict';
const { test } = require('uvu');
const assert = require('uvu/assert');
const postcss = require('postcss');
const reduceCalc = require('../index.js');
const postcssOpts = { from: undefined };
function testValue(fixture, expected, opts = {}) {
fixture = `foo{bar:${fixture}}`;
expected = `foo{bar:${expected}}`;
return async () => {
const result = await postcss(reduceCalc(opts)).process(
fixture,
postcssOpts
);
assert.is(result.css, expected);
};
}
function testCss(fixture, expected, opts = {}) {
return async () => {
const result = await postcss(reduceCalc(opts)).process(
fixture,
postcssOpts
);
assert.is(result.css, expected);
};
}
function testThrows(fixture, expected, warning, opts = {}) {
fixture = `foo{bar:${fixture}}`;
expected = `foo{bar:${expected}}`;
return async () => {
const result = await postcss(reduceCalc(opts)).process(
fixture,
postcssOpts
);
const warnings = result.warnings();
assert.is(result.css, expected);
assert.is(warnings[0].text, warning);
};
}
test('should reduce simple calc (1)', testValue('calc(1px + 1px)', '2px'));
test(
'should reduce simple calc (2)',
testValue('calc(1px + 1px);baz:calc(2px+3px)', '2px;baz:5px')
);
test('should reduce simple calc (3)', testValue('calc(1rem * 1.5)', '1.5rem'));
test('should reduce simple calc (4)', testValue('calc(3em - 1em)', '2em'));
test('should reduce simple calc (5', testValue('calc(2ex / 2)', '1ex'));
test(
'should reduce simple calc (6)',
testValue('calc(50px - (20px - 30px))', '60px')
);
test(
'should reduce simple calc (7)',
testValue('calc(100px - (100px - 100%))', '100%')
);
test(
'should reduce simple calc (8)',
testValue('calc(100px + (100px - 100%))', 'calc(200px - 100%)')
);
test(
'should reduce additions and subtractions (1)',
testValue('calc(100% - 10px + 20px)', 'calc(100% + 10px)')
);
test(
'should reduce additions and subtractions (2)',
testValue('calc(100% + 10px - 20px)', 'calc(100% - 10px)')
);
test(
'should reduce additions and subtractions (3)',
testValue('calc(1px - (2em + 3%))', 'calc(1px - 2em - 3%)')
);
test(
'should reduce additions and subtractions (4)',
testValue('calc((100vw - 50em) / 2)', 'calc(50vw - 25em)')
);
test(
'should reduce additions and subtractions (5)',
testValue('calc(10px - (100vw - 50em) / 2)', 'calc(10px - 50vw + 25em)')
);
test(
'should reduce additions and subtractions (6)',
testValue('calc(1px - (2em + 4vh + 3%))', 'calc(1px - 2em - 4vh - 3%)')
);
test(
'should reduce additions and subtractions (7)',
testValue(
'calc(0px - (24px - (var(--a) - var(--b)) / 2 + var(--c)))',
'calc(-24px + (var(--a) - var(--b))/2 - var(--c))'
)
);
test(
'should reduce additions and subtractions (8)',
testValue('calc(1px + (2em + (3vh + 4px)))', 'calc(5px + 2em + 3vh)')
);
test(
'should reduce additions and subtractions (9)',
testValue('calc(1px - (2em + 4px - 6vh) / 2)', 'calc(-1px - 1em + 3vh)')
);
test(
'should reduce multiplication',
testValue('calc(((var(--a) + 4px) * 2) * 2)', 'calc((var(--a) + 4px)*2*2)')
);
test(
'should reduce multiplication before reducing additions',
testValue(
'calc(((var(--a) + 4px) * 2) * 2 + 4px)',
'calc((var(--a) + 4px)*2*2 + 4px)'
)
);
test(
'should reduce division',
testValue('calc(((var(--a) + 4px) / 2) / 2)', 'calc((var(--a) + 4px)/2/2)')
);
test(
'should reduce division before reducing additions',
testValue(
'calc(((var(--a) + 4px) / 2) / 2 + 4px)',
'calc((var(--a) + 4px)/2/2 + 4px)'
)
);
test(
'should ignore value surrounding calc function (1)',
testValue('a calc(1px + 1px)', 'a 2px')
);
test(
'should ignore value surrounding calc function (2)',
testValue('calc(1px + 1px) a', '2px a')
);
test(
'should ignore value surrounding calc function (3)',
testValue('a calc(1px + 1px) b', 'a 2px b')
);
test(
'should ignore value surrounding calc function (4)',
testValue('a calc(1px + 1px) b calc(1em + 2em) c', 'a 2px b 3em c')
);
test(
'should reduce nested calc',
testValue('calc(100% - calc(50% + 25px))', 'calc(50% - 25px)')
);
test(
'should reduce vendor-prefixed nested calc',
testValue(
'-webkit-calc(100% - -webkit-calc(50% + 25px))',
'-webkit-calc(50% - 25px)'
)
);
test('should reduce uppercase calc (1)', testValue('CALC(1px + 1px)', '2px'));
test(
'should reduce uppercase calc (2)',
testValue('CALC(1px + CALC(2px / 2))', '2px')
);
test(
'should reduce uppercase calc (3)',
testValue('-WEBKIT-CALC(1px + 1px)', '2px')
);
test(
'should reduce uppercase calc (4)',
testValue('-WEBKIT-CALC(1px + -WEBKIT-CALC(2px / 2))', '2px')
);
test(
'should ignore calc with css variables (1)',
testValue('calc(var(--mouseX) * 1px)', 'calc(var(--mouseX)*1px)')
);
test(
'should ignore calc with css variables (2)',
testValue(
'calc(10px - (100px * var(--mouseX)))',
'calc(10px - 100px*var(--mouseX))'
)
);
test(
'should ignore calc with css variables (3)',
testValue(
'calc(10px - (100px + var(--mouseX)))',
'calc(-90px - var(--mouseX))'
)
);
test(
'should ignore calc with css variables (4)',
testValue(
'calc(10px - (100px / var(--mouseX)))',
'calc(10px - 100px/var(--mouseX))'
)
);
test(
'should ignore calc with css variables (5)',
testValue(
'calc(10px - (100px - var(--mouseX)))',
'calc(-90px + var(--mouseX))'
)
);
test(
'should ignore calc with css variables (6)',
testValue('calc(var(--popupHeight) / 2)', 'calc(var(--popupHeight)/2)')
);
test(
'should ignore calc with css variables (7)',
testValue(
'calc(var(--popupHeight) / 2 + var(--popupWidth) / 2)',
'calc(var(--popupHeight)/2 + var(--popupWidth)/2)'
)
);
test(
'should reduce calc with newline characters',
testValue('calc(\n1rem \n* 2 \n* 1.5)', '3rem')
);
test(
'should preserve calc with incompatible units',
testValue('calc(100% + 1px)', 'calc(100% + 1px)')
);
test(
'should parse fractions without leading zero',
testValue('calc(2rem - .14285em)', 'calc(2rem - 0.14285em)')
);
test('should handle precision correctly (1)', testValue('calc(1/100)', '0.01'));
test(
'should handle precision correctly (2)',
testValue('calc(5/1000000)', '0.00001')
);
test(
'should handle precision correctly (3)',
testValue('calc(5/1000000)', '0.000005', { precision: 6 })
);
test(
'should reduce browser-prefixed calc (1)',
testValue('-webkit-calc(1px + 1px)', '2px')
);
test(
'should reduce browser-prefixed calc (2)',
testValue('-moz-calc(1px + 1px)', '2px')
);
test(
'should discard zero values (#2) (1)',
testValue('calc(100vw / 2 - 6px + 0px)', 'calc(50vw - 6px)')
);
test(
'should discard zero values (#2) (2)',
testValue('calc(500px - 0px)', '500px')
);
test(
'should not perform addition on unitless values (#3)',
testValue('calc(1px + 1)', 'calc(1px + 1)')
);
test(
'should reduce consecutive substractions (#24) (1)',
testValue('calc(100% - 120px - 60px)', 'calc(100% - 180px)')
);
test(
'should reduce consecutive substractions (#24) (2)',
testValue('calc(100% - 10px - 20px)', 'calc(100% - 30px)')
);
test(
'should reduce mixed units of time (postcss-calc#33)',
testValue('calc(1s - 50ms)', '0.95s')
);
test(
'should correctly reduce calc with mixed units (cssnano#211)',
testValue('calc(99.99% * 1/1 - 0rem)', '99.99%')
);
test(
'should apply optimization (cssnano#320)',
testValue('calc(50% + (5em + 5%))', 'calc(55% + 5em)')
);
test(
'should reduce substraction from zero',
testValue('calc( 0 - 10px)', '-10px')
);
test(
'should reduce subtracted expression from zero',
testValue('calc( 0 - calc(1px + 1em) )', 'calc(-1px - 1em)')
);
test(
'should reduce substracted expression from zero (1)',
testValue('calc( 0 - (100vw - 10px) / 2 )', 'calc(-50vw + 5px)')
);
test(
'should reduce substracted expression from zero (2)',
testValue('calc( 0px - (100vw - 10px))', 'calc(10px - 100vw)')
);
test(
'should reduce substracted expression from zero (3)',
testValue('calc( 0px - (100vw - 10px) * 2 )', 'calc(20px - 200vw)')
);
test(
'should reduce substracted expression from zero (4)',
testValue('calc( 0px - (100vw + 10px))', 'calc(-10px - 100vw)')
);
test(
'should reduce substracted expression from zero (css-variable)',
testValue(
'calc( 0px - (var(--foo, 4px) / 2))',
'calc(0px - var(--foo, 4px)/2)'
)
);
test(
'should reduce nested expression',
testValue('calc( (1em - calc( 10px + 1em)) / 2)', '-5px')
);
test(
'should skip constant function',
testValue(
'calc(constant(safe-area-inset-left))',
'calc(constant(safe-area-inset-left))'
)
);
test(
'should skip env function',
testValue(
'calc(env(safe-area-inset-left))',
'calc(env(safe-area-inset-left))'
)
);
test(
'should skip env function (#1)',
testValue(
'calc(env(safe-area-inset-left, 50px 20px))',
'calc(env(safe-area-inset-left, 50px 20px))'
)
);
test(
'should skip unknown function',
testValue(
'calc(unknown(safe-area-inset-left))',
'calc(unknown(safe-area-inset-left))'
)
);
test(
'should preserve the original declaration when `preserve` option is set to true',
testCss('foo{bar:calc(1rem * 1.5)}', 'foo{bar:1.5rem;bar:calc(1rem * 1.5)}', {
preserve: true,
})
);
test(
'should not yield warnings when nothing is wrong',
testValue('calc(500px - 0px)', '500px', { warnWhenCannotResolve: true })
);
test(
'should warn when calc expression cannot be reduced to a single value',
testValue('calc(100% + 1px)', 'calc(100% + 1px)', {
warnWhenCannotResolve: true,
})
);
test(
'should reduce mixed units of time (#33)',
testValue('calc(1s - 50ms)', '0.95s')
);
test(
'should not parse variables as calc expressions (#35)',
testCss(
'foo:nth-child(2n + $var-calc){}',
'foo:nth-child(2n + $var-calc){}',
{ selectors: true }
)
);
test(
'should apply algebraic reduction (cssnano#319)',
testValue('calc((100px - 1em) + (-50px + 1em))', '50px')
);
test(
'should discard zero values (reduce-css-calc#2) (1)',
testValue('calc(100vw / 2 - 6px + 0px)', 'calc(50vw - 6px)')
);
test(
'should discard zero values (reduce-css-calc#2) (2)',
testValue('calc(500px - 0px)', '500px')
);
test(
'should not perform addition on unitless values (reduce-css-calc#3)',
testValue('calc(1px + 1)', 'calc(1px + 1)')
);
test(
'should return the same and not thrown an exception for attribute selectors without a value',
testCss('button[disabled]{}', 'button[disabled]{}', { selectors: true })
);
test(
'should ignore reducing custom property',
testCss(
':root { --foo: calc(var(--bar) / 8); }',
':root { --foo: calc(var(--bar)/8); }'
)
);
test(
'should ignore media queries',
testCss(
'@media (min-width:calc(10px+10px)){}',
'@media (min-width:calc(10px+10px)){}'
)
);
test(
'should reduce calc in media queries when `mediaQueries` option is set to true',
testCss('@media (min-width:calc(10px+10px)){}', '@media (min-width:20px){}', {
mediaQueries: true,
})
);
test(
'should ignore selectors (1)',
testCss('div[data-size="calc(3*3)"]{}', 'div[data-size="calc(3*3)"]{}')
);
test(
'should ignore selectors (2)',
testCss('div:nth-child(2n + calc(3*3)){}', 'div:nth-child(2n + calc(3*3)){}')
);
test(
'should reduce calc in selectors when `selectors` option is set to true (1)',
testCss('div[data-size="calc(3*3)"]{}', 'div[data-size="9"]{}', {
selectors: true,
})
);
test(
'should reduce calc in selectors when `selectors` option is set to true (2)',
testCss('div:nth-child(2n + calc(3*3)){}', 'div:nth-child(2n + 9){}', {
selectors: true,
})
);
test(
'should not reduce 100% to 1 (reduce-css-calc#44)',
testCss(
'.@supports (width:calc(100% - constant(safe-area-inset-left))){.a{width:calc(100% - constant(safe-area-inset-left))}}',
'.@supports (width:calc(100% - constant(safe-area-inset-left))){.a{width:calc(100% - constant(safe-area-inset-left))}}'
)
);
test(
'should not break css variables that have "calc" in their names',
testCss(
'a{transform: translateY(calc(-100% - var(--tooltip-calculated-offset)))}',
'a{transform: translateY(calc(-100% - var(--tooltip-calculated-offset)))}'
)
);
test(
'should handle complex calculations (reduce-css-calc#45) (1)',
testValue(
'calc(100% + (2 * 100px) - ((75.37% - 63.5px) - 900px))',
'calc(24.63% + 1163.5px)'
)
);
test(
'should handle complex calculations (reduce-css-calc#45) (2)',
testValue(
'calc(((((100% + (2 * 30px) + 63.5px) / 0.7537) - (100vw - 60px)) / 2) + 30px)',
'calc(66.33939% + 141.92915px - 50vw)'
)
);
test(
'should handle advanced arithmetic (1)',
testValue(
'calc(((75.37% - 63.5px) - 900px) + (2 * 100px))',
'calc(75.37% - 763.5px)'
)
);
test(
'should handle advanced arithmetic (2)',
testValue(
'calc((900px - (10% - 63.5px)) + (2 * 100px))',
'calc(1163.5px - 10%)'
)
);
test(
'should handle nested calc statements (reduce-css-calc#49)',
testValue('calc(calc(2.25rem + 2px) - 1px * 2)', '2.25rem')
);
test(
'should throw an exception when attempting to divide by zero',
testThrows('calc(500px/0)', 'calc(500px/0)', 'Cannot divide by zero')
);
test(
'should throw an exception when attempting to divide by unit (#1)',
testThrows(
'calc(500px/2px)',
'calc(500px/2px)',
'Cannot divide by "px", number expected'
)
);
test(
'nested var (reduce-css-calc#50)',
testValue(
'calc(var(--xxx, var(--yyy)) / 2)',
'calc(var(--xxx, var(--yyy))/2)'
)
);
test(
'should not throw an exception when unknow function exist in calc',
testValue(
'calc(unknown(#fff) - other-unknown(200px))',
'calc(unknown(#fff) - other-unknown(200px))'
)
);
test(
'should not throw an exception when unknow function exist in calc (#1)',
testValue(
'calc(unknown(#fff) * other-unknown(200px))',
'calc(unknown(#fff)*other-unknown(200px))'
)
);
test(
'should not strip calc with single CSS custom variable',
testValue('calc(var(--foo))', 'calc(var(--foo))')
);
test(
'should strip unnecessary calc with single CSS custom variable',
testValue('calc(calc(var(--foo)))', 'calc(var(--foo))')
);
test(
'should not strip calc with single CSS custom variables and value',
testValue('calc(var(--foo) + 10px)', 'calc(var(--foo) + 10px)')
);
test('should reduce calc (uppercase)', testValue('CALC(1PX + 1PX)', '2PX'));
test(
'should reduce calc (uppercase) (#1)',
testValue('CALC(VAR(--foo) + VAR(--bar))', 'CALC(VAR(--foo) + VAR(--bar))')
);
test(
'should reduce calc (uppercase) (#2)',
testValue('CALC( (1EM - CALC( 10PX + 1EM)) / 2)', '-5PX')
);
test(
'should handle nested calc function (#1)',
testValue(
'calc(calc(var(--foo) + var(--bar)) + var(--baz))',
'calc(var(--foo) + var(--bar) + var(--baz))'
)
);
test(
'should handle nested calc function (#2)',
testValue(
'calc(var(--foo) + calc(var(--bar) + var(--baz)))',
'calc(var(--foo) + var(--bar) + var(--baz))'
)
);
test(
'should handle nested calc function (#3)',
testValue(
'calc(calc(var(--foo) - var(--bar)) - var(--baz))',
'calc(var(--foo) - var(--bar) - var(--baz))'
)
);
test(
'should handle nested calc function (#4)',
testValue(
'calc(var(--foo) - calc(var(--bar) - var(--baz)))',
'calc(var(--foo) - var(--bar) + var(--baz))'
)
);
test(
'should handle nested calc function (#5)',
testValue(
'calc(calc(var(--foo) + var(--bar)) - var(--baz))',
'calc(var(--foo) + var(--bar) - var(--baz))'
)
);
test(
'should handle nested calc function (#6)',
testValue(
'calc(var(--foo) + calc(var(--bar) - var(--baz)))',
'calc(var(--foo) + var(--bar) - var(--baz))'
)
);
test(
'should handle nested calc function (#7)',
testValue(
'calc(calc(var(--foo) - var(--bar)) + var(--baz))',
'calc(var(--foo) - var(--bar) + var(--baz))'
)
);
test(
'should handle nested calc function (#8)',
testValue(
'calc(var(--foo) - calc(var(--bar) + var(--baz)))',
'calc(var(--foo) - var(--bar) - var(--baz))'
)
);
test(
'should handle nested calc function (#9)',
testValue(
'calc(calc(var(--foo) + var(--bar)) * var(--baz))',
'calc((var(--foo) + var(--bar))*var(--baz))'
)
);
test(
'should handle nested calc function (#10)',
testValue(
'calc(var(--foo) * calc(var(--bar) + var(--baz)))',
'calc(var(--foo)*(var(--bar) + var(--baz)))'
)
);
test(
'should handle nested calc function (#11)',
testValue(
'calc(calc(var(--foo) + var(--bar)) / var(--baz))',
'calc((var(--foo) + var(--bar))/var(--baz))'
)
);
test(
'should handle nested calc function (#12)',
testValue(
'calc(var(--foo) / calc(var(--bar) + var(--baz)))',
'calc(var(--foo)/(var(--bar) + var(--baz)))'
)
);
test(
'should handle nested calc function (#13)',
testValue(
'calc(100vh - 5rem - calc(10rem + 100px))',
'calc(100vh - 15rem - 100px)'
)
);
test(
'should handle nested calc function (#14)',
testValue('calc(100% - calc(10px + 2vw))', 'calc(100% - 10px - 2vw)')
);
test(
'should handle nested calc function (#15)',
testValue('calc(100% - calc(10px - 2vw))', 'calc(100% - 10px + 2vw)')
);
test(
'should preserve division precedence',
testValue(
'calc(100%/(var(--aspect-ratio)))',
'calc(100%/(var(--aspect-ratio)))'
)
);
test(
'should preserve division precedence (2)',
testValue(
`calc(
(var(--fluid-screen) - ((var(--fluid-min-width) / 16) * 1rem)) /
((var(--fluid-max-width) / 16) - (var(--fluid-min-width) / 16))
)`,
'calc((var(--fluid-screen) - ((var(--fluid-min-width)/16)*1rem))/(var(--fluid-max-width)/16 - var(--fluid-min-width)/16))'
)
);
test(
'should preserve division precedence (3)',
testValue('calc(1/(10/var(--dot-size)))', 'calc(1/(10/var(--dot-size)))')
);
test(
'should correctly preserve parentheses',
testValue(
'calc(1/((var(--a) - var(--b))/16))',
'calc(1/(var(--a) - var(--b))/16)'
)
);
test('precision for calc', testValue('calc(100% / 3 * 3)', '100%'));
test(
'precision for nested calc',
testValue('calc(calc(100% / 3) * 3)', '100%')
);
test('plus sign', testValue('calc(+100px + +100px)', '200px'));
test('plus sign (#1)', testValue('calc(+100px - +100px)', '0px'));
test('plus sign (#2)', testValue('calc(200px * +1)', '200px'));
test('plus sign (#3)', testValue('calc(200px / +1)', '200px'));
test('minus sign', testValue('calc(-100px + -100px)', '-200px'));
test('minus sign (#2)', testValue('calc(-100px - -100px)', '0px'));
test('minus sign (#3)', testValue('calc(200px * -1)', '-200px'));
test('minus sign (#4)', testValue('calc(200px / -1)', '-200px'));
test('whitespace', testValue('calc( 100px + 100px )', '200px'));
test('whitespace (#1)', testValue('calc(\t100px\t+\t100px\t)', '200px'));
test('whitespace (#2)', testValue('calc(\n100px\n+\n100px\n)', '200px'));
test(
'whitespace (#4)',
testValue('calc(\r\n100px\r\n+\r\n100px\r\n)', '200px')
);
test(
'comments',
testValue('calc(/*test*/100px/*test*/ + /*test*/100px/*test*/)', '200px')
);
test(
'comments (#1)',
testValue('calc(/*test*/100px/*test*/*/*test*/2/*test*/)', '200px')
);
test(
'comments nested',
testValue(
'calc(/*test*/100px + calc(/*test*/100px/*test*/ + /*test*/100px/*test*/))',
'300px'
)
);
test('exponent composed', testValue('calc(1.1e+1px + 1.1e+1px)', '22px'));
test('exponent composed (#1)', testValue('calc(10e+1px + 10e+1px)', '200px'));
test(
'exponent composed (#2)',
testValue('calc(1.1e+10px + 1.1e+10px)', '22000000000px')
);
test('exponent composed (#3)', testValue('calc(9e+1 * 1px)', '90px'));
test('exponent composed (#4)', testValue('calc(9e+1% + 10%)', '100%'));
test(
'exponent composed (uppercase)',
testValue('calc(1.1E+1px + 1.1E+1px)', '22px')
);
test('convert units', testValue('calc(1cm + 1px)', '1.02646cm'));
test('convert units (#1)', testValue('calc(1px + 1cm)', '38.79528px'));
test('convert units (#2)', testValue('calc(10Q + 10Q)', '20Q'));
test('convert units (#3)', testValue('calc(100.9q + 10px)', '111.48333q'));
test('convert units (#4)', testValue('calc(10px + 100.9q)', '105.33858px'));
test('convert units (#5)', testValue('calc(10cm + 1px)', '10.02646cm'));
test('convert units (#6)', testValue('calc(10mm + 1px)', '10.26458mm'));
test('convert units (#7)', testValue('calc(10px + 1q)', '10.94488px'));
test('convert units (#8)', testValue('calc(10cm + 1q)', '10.025cm'));
test('convert units (#9)', testValue('calc(10mm + 1q)', '10.25mm'));
test('convert units (#10)', testValue('calc(10in + 1q)', '10.00984in'));
test('convert units (#11)', testValue('calc(10pt + 1q)', '10.70866pt'));
test('convert units (#12)', testValue('calc(10pc + 1q)', '10.05906pc'));
test('convert units (#13)', testValue('calc(1q + 10px)', '11.58333q'));
test('convert units (#14)', testValue('calc(1q + 10cm)', '401q'));
test('convert units (#15)', testValue('calc(1q + 10mm)', '41q'));
test('convert units (#16)', testValue('calc(1q + 10in)', '1017q'));
test('convert units (#17)', testValue('calc(1q + 10pt)', '15.11111q'));
test('convert units (#18)', testValue('calc(1q + 10pc)', '170.33333q'));
test(
'unknown units',
testValue('calc(1unknown + 2unknown)', 'calc(1unknown + 2unknown)')
);
test(
'unknown units with known',
testValue('calc(1unknown + 2px)', 'calc(1unknown + 2px)')
);
test(
'unknown units with known (#1)',
testValue('calc(1px + 2unknown)', 'calc(1px + 2unknown)')
);
test(
'error with parsing',
testThrows(
'calc(10pc + unknown)',
'calc(10pc + unknown)',
'Lexical error on line 1: Unrecognized text.\n\n Erroneous area:\n1: 10pc + unknown\n^.........^'
)
);
test.run();
+51
View File
@@ -0,0 +1,51 @@
'use strict';
const transform = require('./lib/transform.js');
/**
* @typedef {{precision?: number | false,
* preserve?: boolean,
* warnWhenCannotResolve?: boolean,
* mediaQueries?: boolean,
* selectors?: boolean}} PostCssCalcOptions
*/
/**
* @type {import('postcss').PluginCreator<PostCssCalcOptions>}
* @param {PostCssCalcOptions} opts
* @return {import('postcss').Plugin}
*/
function pluginCreator(opts) {
const options = Object.assign(
{
precision: 5,
preserve: false,
warnWhenCannotResolve: false,
mediaQueries: false,
selectors: false,
},
opts
);
return {
postcssPlugin: 'postcss-calc',
OnceExit(css, { result }) {
css.walk((node) => {
const { type } = node;
if (type === 'decl') {
transform(node, 'value', options, result);
}
if (type === 'atrule' && options.mediaQueries) {
transform(node, 'params', options, result);
}
if (type === 'rule' && options.selectors) {
transform(node, 'selector', options, result);
}
});
},
};
}
pluginCreator.postcss = true;
module.exports = pluginCreator;
+160
View File
@@ -0,0 +1,160 @@
'use strict';
/**
* @type {{[key:string]: {[key:string]: number}}}
*/
const conversions = {
// Absolute length units
px: {
px: 1,
cm: 96 / 2.54,
mm: 96 / 25.4,
q: 96 / 101.6,
in: 96,
pt: 96 / 72,
pc: 16,
},
cm: {
px: 2.54 / 96,
cm: 1,
mm: 0.1,
q: 0.025,
in: 2.54,
pt: 2.54 / 72,
pc: 2.54 / 6,
},
mm: {
px: 25.4 / 96,
cm: 10,
mm: 1,
q: 0.25,
in: 25.4,
pt: 25.4 / 72,
pc: 25.4 / 6,
},
q: {
px: 101.6 / 96,
cm: 40,
mm: 4,
q: 1,
in: 101.6,
pt: 101.6 / 72,
pc: 101.6 / 6,
},
in: {
px: 1 / 96,
cm: 1 / 2.54,
mm: 1 / 25.4,
q: 1 / 101.6,
in: 1,
pt: 1 / 72,
pc: 1 / 6,
},
pt: {
px: 0.75,
cm: 72 / 2.54,
mm: 72 / 25.4,
q: 72 / 101.6,
in: 72,
pt: 1,
pc: 12,
},
pc: {
px: 0.0625,
cm: 6 / 2.54,
mm: 6 / 25.4,
q: 6 / 101.6,
in: 6,
pt: 6 / 72,
pc: 1,
},
// Angle units
deg: {
deg: 1,
grad: 0.9,
rad: 180 / Math.PI,
turn: 360,
},
grad: {
deg: 400 / 360,
grad: 1,
rad: 200 / Math.PI,
turn: 400,
},
rad: {
deg: Math.PI / 180,
grad: Math.PI / 200,
rad: 1,
turn: Math.PI * 2,
},
turn: {
deg: 1 / 360,
grad: 0.0025,
rad: 0.5 / Math.PI,
turn: 1,
},
// Duration units
s: {
s: 1,
ms: 0.001,
},
ms: {
s: 1000,
ms: 1,
},
// Frequency units
hz: {
hz: 1,
khz: 1000,
},
khz: {
hz: 0.001,
khz: 1,
},
// Resolution units
dpi: {
dpi: 1,
dpcm: 1 / 2.54,
dppx: 1 / 96,
},
dpcm: {
dpi: 2.54,
dpcm: 1,
dppx: 2.54 / 96,
},
dppx: {
dpi: 96,
dpcm: 96 / 2.54,
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();
if (!conversions[targetUnitNormalized]) {
throw new Error('Cannot convert to ' + targetUnit);
}
if (!conversions[targetUnitNormalized][sourceUnitNormalized]) {
throw new Error('Cannot convert from ' + sourceUnit + ' to ' + targetUnit);
}
const converted =
conversions[targetUnitNormalized][sourceUnitNormalized] * value;
if (precision !== false) {
precision = Math.pow(10, Math.ceil(precision) || 5);
return Math.round(converted * precision) / precision;
}
return converted;
}
module.exports = convertUnit;
+362
View File
@@ -0,0 +1,362 @@
'use strict';
const convertUnit = require('./convertUnit.js');
/**
* @param {import('../parser').CalcNode} node
* @return {node is import('../parser').ValueExpression}
*/
function isValueType(node) {
switch (node.type) {
case 'LengthValue':
case 'AngleValue':
case 'TimeValue':
case 'FrequencyValue':
case 'ResolutionValue':
case 'EmValue':
case 'ExValue':
case 'ChValue':
case 'RemValue':
case 'VhValue':
case 'VwValue':
case 'VminValue':
case 'VmaxValue':
case 'PercentageValue':
case 'Number':
return true;
}
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}`);
}
if (isValueType(node)) {
const itemIndex = collected.findIndex((x) => x.node.type === node.type);
if (itemIndex >= 0) {
if (node.value === 0) {
return;
}
// 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 = '+';
reducedNode.value *= -1;
}
if (preOperator === '+') {
reducedNode.value += current.value;
} else {
reducedNode.value -= current.value;
}
// make sure reducedNode.value >= 0
if (reducedNode.value >= 0) {
collected[itemIndex] = { node: reducedNode, preOperator: '+' };
} else {
reducedNode.value *= -1;
collected[itemIndex] = { node: reducedNode, preOperator: '-' };
}
} else {
// make sure node.value >= 0
if (node.value >= 0) {
collected.push({ node, preOperator });
} else {
node.value *= -1;
collected.push({ node, preOperator: flip(preOperator) });
}
}
} else if (node.type === 'MathExpression') {
if (isAddSubOperator(node.operator)) {
collectAddSubItems(preOperator, node.left, collected, precision);
const collectRightOperator =
preOperator === '-' ? flip(node.operator) : node.operator;
collectAddSubItems(
collectRightOperator,
node.right,
collected,
precision
);
} else {
// * or /
const reducedNode = reduce(node, precision);
// prevent infinite recursive call
if (
reducedNode.type !== 'MathExpression' ||
isAddSubOperator(reducedNode.operator)
) {
collectAddSubItems(preOperator, reducedNode, collected, precision);
} else {
collected.push({ node: reducedNode, preOperator });
}
}
} else if (node.type === 'ParenthesizedExpression') {
collectAddSubItems(preOperator, node.content, collected, precision);
} else {
collected.push({ node, preOperator });
}
}
/**
* @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) && 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))
) {
const firstZeroItem = collected.find(
(item) => isValueType(item.node) && item.node.value === 0
);
if (firstZeroItem) {
withoutZeroItem.unshift(firstZeroItem);
}
}
// make sure the preOperator of the first item is +
if (
withoutZeroItem[0].preOperator === '-' &&
isValueType(withoutZeroItem[0].node)
) {
withoutZeroItem[0].node.value *= -1;
withoutZeroItem[0].preOperator = '+';
}
let root = withoutZeroItem[0].node;
for (let i = 1; i < withoutZeroItem.length; i++) {
root = {
type: 'MathExpression',
operator: withoutZeroItem[i].preOperator,
left: root,
right: withoutZeroItem[i].node,
};
}
return root;
}
/**
* @param {import('../parser').MathExpression} node
*/
function reduceDivisionExpression(node) {
if (!isValueType(node.right)) {
return node;
}
if (node.right.type !== 'Number') {
throw new Error(`Cannot divide by "${node.right.unit}", number expected`);
}
return applyNumberDivision(node.left, node.right.value);
}
/**
* 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)) {
node.value /= divisor;
return node;
}
if (node.type === 'MathExpression' && isAddSubOperator(node.operator)) {
// turn (a + b) / num into a/num + b/num
// is good for further reduction
// checkout the test case
// "should reduce division before reducing additions"
return {
type: 'MathExpression',
operator: node.operator,
left: applyNumberDivision(node.left, divisor),
right: applyNumberDivision(node.right, divisor),
};
}
// it is impossible to reduce it into a single value
// .e.g the node contains css variable
// so we just preserve the division and let browser do it
return {
type: 'MathExpression',
operator: '/',
left: node,
right: {
type: 'Number',
value: divisor,
},
};
}
/**
* @param {import('../parser').MathExpression} node
*/
function reduceMultiplicationExpression(node) {
// (expr) * number
if (node.right.type === 'Number') {
return applyNumberMultiplication(node.left, node.right.value);
}
// number * (expr)
if (node.left.type === 'Number') {
return applyNumberMultiplication(node.right, node.left.value);
}
return node;
}
/**
* apply (expr) * number
* @param {number} multiplier
* @param {import('../parser').CalcNode} node
* @return {import('../parser').CalcNode}
*/
function applyNumberMultiplication(node, multiplier) {
if (isValueType(node)) {
node.value *= multiplier;
return node;
}
if (node.type === 'MathExpression' && isAddSubOperator(node.operator)) {
// turn (a + b) * num into a*num + b*num
// is good for further reduction
// checkout the test case
// "should reduce multiplication before reducing additions"
return {
type: 'MathExpression',
operator: node.operator,
left: applyNumberMultiplication(node.left, multiplier),
right: applyNumberMultiplication(node.right, multiplier),
};
}
// it is impossible to reduce it into a single value
// .e.g the node contains css variable
// so we just preserve the division and let browser do it
return {
type: 'MathExpression',
operator: '*',
left: node,
right: {
type: 'Number',
value: multiplier,
},
};
}
/**
* @param {import('../parser').ValueExpression} left
* @param {import('../parser').ValueExpression} right
* @param {number} precision
*/
function convertNodesUnits(left, right, precision) {
switch (left.type) {
case 'LengthValue':
case 'AngleValue':
case 'TimeValue':
case 'FrequencyValue':
case 'ResolutionValue':
if (right.type === left.type && right.unit && left.unit) {
const converted = convertUnit(
right.value,
right.unit,
left.unit,
precision
);
right = {
type: left.type,
value: converted,
unit: left.unit,
};
}
return { left, right };
default:
return { left, right };
}
}
/**
* @param {import('../parser').ParenthesizedExpression} node
*/
function includesNoCssProperties(node) {
return (
node.content.type !== 'Function' &&
(node.content.type !== 'MathExpression' ||
(node.content.right.type !== 'Function' &&
node.content.left.type !== 'Function'))
);
}
/**
* @param {import('../parser').CalcNode} node
* @param {number} precision
* @return {import('../parser').CalcNode}
*/
function reduce(node, precision) {
if (node.type === 'MathExpression') {
if (isAddSubOperator(node.operator)) {
// reduceAddSubExpression will call reduce recursively
return reduceAddSubExpression(node, precision);
}
node.left = reduce(node.left, precision);
node.right = reduce(node.right, precision);
switch (node.operator) {
case '/':
return reduceDivisionExpression(node);
case '*':
return reduceMultiplicationExpression(node);
}
return node;
}
if (node.type === 'ParenthesizedExpression') {
if (includesNoCssProperties(node)) {
return reduce(node.content, precision);
}
}
return node;
}
module.exports = reduce;
+93
View File
@@ -0,0 +1,93 @@
'use strict';
const order = {
'*': 0,
'/': 0,
'+': 1,
'-': 1,
};
/**
* @param {number} value
* @param {number | false} prec
*/
function round(value, prec) {
if (prec !== false) {
const precision = Math.pow(10, prec);
return Math.round(value * precision) / precision;
}
return value;
}
/**
* @param {number | false} prec
* @param {import('../parser').CalcNode} node
*
* @return {string}
*/
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 {
str += stringify(left, prec);
}
str += order[op] ? ` ${node.operator} ` : node.operator;
if (
right.type === 'MathExpression' &&
order[op] < order[right.operator]
) {
str += `(${stringify(right, prec)})`;
} else {
str += stringify(right, prec);
}
return str;
}
case 'Number':
return round(node.value, prec).toString();
case 'Function':
return node.value.toString();
case 'ParenthesizedExpression':
return `(${stringify(node.content, prec)})`;
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}
*/
module.exports = function (calc, node, originalValue, options, result, item) {
let str = stringify(node, options.precision);
const shouldPrintCalc =
node.type === 'MathExpression' || node.type === 'Function';
if (shouldPrintCalc) {
// if calc expression couldn't be resolved to a single value, re-wrap it as
// a calc()
str = `${calc}(${str})`;
// if the warnWhenCannotResolve option is on, inform the user that the calc
// expression could not be resolved to a single value
if (options.warnWhenCannotResolve) {
result.warn('Could not reduce expression: ' + originalValue, {
plugin: 'postcss-calc',
node: item,
});
}
}
return str;
};
+109
View File
@@ -0,0 +1,109 @@
'use strict';
const selectorParser = require('postcss-selector-parser');
const valueParser = require('postcss-value-parser');
const { parser } = require('../parser.js');
const reducer = require('./reducer.js');
const stringifier = require('./stringifier.js');
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;
}
// stringify calc expression and produce an AST
const contents = valueParser.stringify(node.nodes);
const ast = parser.parse(contents);
// reduce AST to its simplest form, that is, either to a single value
// or a simplified calc expression
const reducedAst = reducer(ast, options.precision);
// stringify AST and write it back
/** @type {valueParser.Node} */ (node).type = 'word';
node.value = stringifier(
node.value,
reducedAst,
value,
options,
result,
item
);
return false;
})
.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) => {
// attribute value
// e.g. the "calc(3*3)" part of "div[data-size="calc(3*3)"]"
if (node.type === 'attribute' && node.value) {
node.setValue(transformValue(node.value, options, result, item));
}
// tag value
// e.g. the "calc(3*3)" part of "div:nth-child(2n + calc(3*3))"
if (node.type === 'tag') {
node.value = transformValue(node.value, options, result, item);
}
return;
});
}).processSync(value);
}
/**
* @param {any} node
* @param {{precision: number, preserve: boolean, warnWhenCannotResolve: boolean}} options
* @param {'value'|'params'|'selector'} property
* @param {import("postcss").Result} result
*/
module.exports = (node, property, options, result) => {
let value = node[property];
try {
value =
property === 'selector'
? transformSelector(node[property], options, result, node)
: transformValue(node[property], options, result, node);
} catch (error) {
if (error instanceof Error) {
result.warn(error.message, { node });
} else {
result.warn('Error', { node });
}
return;
}
// if the preserve option is enabled and the value has changed, write the
// transformed value into a cloned node which is inserted before the current
// node, preserving the original value. Otherwise, overwrite the original
// value.
if (options.preserve && node[property] !== value) {
const clone = node.clone();
clone[property] = value;
node.parent.insertBefore(node, clone);
} else {
node[property] = value;
}
};
+51
View File
@@ -0,0 +1,51 @@
export interface MathExpression {
type: 'MathExpression';
right: CalcNode;
left: CalcNode;
operator: '*' | '+' | '-' | '/';
}
export interface ParenthesizedExpression {
type: 'ParenthesizedExpression';
content: CalcNode;
}
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 | ParenthesizedExpression;
export interface Parser {
parse: (arg: string) => CalcNode;
}
export const parser: Parser;
+111
View File
@@ -0,0 +1,111 @@
/* description: Parses expressions. */
/* lexical grammar */
%lex
%options case-insensitive
%%
\s+ /* skip whitespace */
(\-(webkit|moz)\-)?calc\b return 'CALC';
[a-z][a-z0-9-]*\s*\((?:(?:\"(?:\\.|[^\"\\])*\"|\'(?:\\.|[^\'\\])*\')|\([^)]*\)|[^\(\)]*)*\) return 'FUNCTION';
"*" return 'MUL';
"/" return 'DIV';
"+" return 'ADD';
"-" return 'SUB';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)em\b return 'EMS';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)ex\b return 'EXS';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)ch\b return 'CHS';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)rem\b return 'REMS';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)vw\b return 'VWS';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)vh\b return 'VHS';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)vmin\b return 'VMINS';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)vmax\b return 'VMAXS';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)cm\b return 'LENGTH';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)mm\b return 'LENGTH';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)Q\b return 'LENGTH';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)in\b return 'LENGTH';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)pt\b return 'LENGTH';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)pc\b return 'LENGTH';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)px\b return 'LENGTH';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)deg\b return 'ANGLE';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)grad\b return 'ANGLE';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)rad\b return 'ANGLE';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)turn\b return 'ANGLE';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)s\b return 'TIME';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)ms\b return 'TIME';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)Hz\b return 'FREQ';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)kHz\b return 'FREQ';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)dpi\b return 'RES';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)dpcm\b return 'RES';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)dppx\b return 'RES';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)\% return 'PERCENTAGE';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)\b return 'NUMBER';
(([0-9]+("."[0-9]+)?|"."[0-9]+)(e(\+|-)[0-9]+)?)-?([a-zA-Z_]|[\240-\377]|(\\[0-9a-fA-F]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-fA-F]))([a-zA-Z0-9_-]|[\240-\377]|(\\[0-9a-fA-F]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-fA-F]))*\b return 'UNKNOWN_DIMENSION';
"(" return 'LPAREN';
")" return 'RPAREN';
<<EOF>> return 'EOF';
/lex
%left ADD SUB
%left MUL DIV
%left UPREC
%start expression
%%
expression
: math_expression EOF { return $1; }
;
math_expression
: CALC LPAREN math_expression RPAREN { $$ = $3; }
| math_expression ADD math_expression { $$ = { type: 'MathExpression', operator: $2, left: $1, right: $3 }; }
| math_expression SUB math_expression { $$ = { type: 'MathExpression', operator: $2, left: $1, right: $3 }; }
| math_expression MUL math_expression { $$ = { type: 'MathExpression', operator: $2, left: $1, right: $3 }; }
| math_expression DIV math_expression { $$ = { type: 'MathExpression', operator: $2, left: $1, right: $3 }; }
| LPAREN math_expression RPAREN { $$ = { type: 'ParenthesizedExpression', content: $2 }; }
| function { $$ = $1; }
| dimension { $$ = $1; }
| number { $$ = $1; }
;
function
: FUNCTION { $$ = { type: 'Function', value: $1 }; }
;
dimension
: LENGTH { $$ = { type: 'LengthValue', value: parseFloat($1), unit: /[a-z]+$/i.exec($1)[0] }; }
| ANGLE { $$ = { type: 'AngleValue', value: parseFloat($1), unit: /[a-z]+$/i.exec($1)[0] }; }
| TIME { $$ = { type: 'TimeValue', value: parseFloat($1), unit: /[a-z]+$/i.exec($1)[0] }; }
| FREQ { $$ = { type: 'FrequencyValue', value: parseFloat($1), unit: /[a-z]+$/i.exec($1)[0] }; }
| RES { $$ = { type: 'ResolutionValue', value: parseFloat($1), unit: /[a-z]+$/i.exec($1)[0] }; }
| UNKNOWN_DIMENSION { $$ = { type: 'UnknownDimension', value: parseFloat($1), unit: /[a-z]+$/i.exec($1)[0] }; }
| EMS { $$ = { type: 'EmValue', value: parseFloat($1), unit: 'em' }; }
| EXS { $$ = { type: 'ExValue', value: parseFloat($1), unit: 'ex' }; }
| CHS { $$ = { type: 'ChValue', value: parseFloat($1), unit: 'ch' }; }
| REMS { $$ = { type: 'RemValue', value: parseFloat($1), unit: 'rem' }; }
| VHS { $$ = { type: 'VhValue', value: parseFloat($1), unit: 'vh' }; }
| VWS { $$ = { type: 'VwValue', value: parseFloat($1), unit: 'vw' }; }
| VMINS { $$ = { type: 'VminValue', value: parseFloat($1), unit: 'vmin' }; }
| VMAXS { $$ = { type: 'VmaxValue', value: parseFloat($1), unit: 'vmax' }; }
| PERCENTAGE { $$ = { type: 'PercentageValue', value: parseFloat($1), unit: '%' }; }
| ADD dimension { var prev = $2; $$ = prev; }
| SUB dimension { var prev = $2; prev.value *= -1; $$ = prev; }
;
number
: NUMBER { $$ = { type: 'Number', value: parseFloat($1) }; }
| ADD NUMBER { $$ = { type: 'Number', value: parseFloat($2) }; }
| SUB NUMBER { $$ = { type: 'Number', value: parseFloat($2) * -1 }; }
;
+3808
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
export = pluginCreator;
/**
* @typedef {{precision?: number | false,
* preserve?: boolean,
* warnWhenCannotResolve?: boolean,
* mediaQueries?: boolean,
* selectors?: boolean}} PostCssCalcOptions
*/
/**
* @type {import('postcss').PluginCreator<PostCssCalcOptions>}
* @param {PostCssCalcOptions} opts
* @return {import('postcss').Plugin}
*/
declare function pluginCreator(opts: PostCssCalcOptions): import('postcss').Plugin;
declare namespace pluginCreator {
export { postcss, PostCssCalcOptions };
}
type PostCssCalcOptions = {
precision?: number | false;
preserve?: boolean;
warnWhenCannotResolve?: boolean;
mediaQueries?: boolean;
selectors?: boolean;
};
declare var postcss: true;
+8
View File
@@ -0,0 +1,8 @@
export = 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;
+14
View File
@@ -0,0 +1,14 @@
export = reduce;
/**
* @param {import('../parser').CalcNode} node
* @param {number} precision
* @return {import('../parser').CalcNode}
*/
declare function reduce(node: import('../parser').CalcNode, precision: number): import('../parser').CalcNode;
declare namespace reduce {
export { Collectible };
}
type Collectible = {
preOperator: '+' | '-';
node: import('../parser').CalcNode;
};
+5
View File
@@ -0,0 +1,5 @@
declare function _exports(calc: string, node: import('../parser').CalcNode, originalValue: string, options: {
precision: number | false;
warnWhenCannotResolve: boolean;
}, result: import("postcss").Result, item: import("postcss").ChildNode): string;
export = _exports;
+6
View File
@@ -0,0 +1,6 @@
declare function _exports(node: any, property: 'value' | 'params' | 'selector', options: {
precision: number;
preserve: boolean;
warnWhenCannotResolve: boolean;
}, result: import("postcss").Result): void;
export = _exports;