Skip to content

Commit 8fcf47d

Browse files
authored
postcss-random-function (#1509)
1 parent c732370 commit 8fcf47d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1266
-54
lines changed

.github/ISSUE_TEMPLATE/css-issue.yml

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ body:
118118
- PostCSS Place
119119
- PostCSS Progressive Custom Properties
120120
- PostCSS Pseudo Class Any Link
121+
- PostCSS Random Function
121122
- PostCSS Rebase URL
122123
- PostCSS Rewrite URL
123124
- PostCSS Rebeccapurple

.github/ISSUE_TEMPLATE/plugin-issue.yml

+1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ body:
115115
- PostCSS Place
116116
- PostCSS Progressive Custom Properties
117117
- PostCSS Pseudo Class Any Link
118+
- PostCSS Random Function
118119
- PostCSS Rebase URL
119120
- PostCSS Rewrite URL
120121
- PostCSS Rebeccapurple

.github/labeler.yml

+6
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,12 @@
389389
- plugins/postcss-pseudo-class-any-link/**
390390
- experimental/postcss-pseudo-class-any-link/**
391391

392+
"plugins/postcss-random-function":
393+
- changed-files:
394+
- any-glob-to-any-file:
395+
- plugins/postcss-random-function/**
396+
- experimental/postcss-random-function/**
397+
392398
"plugins/postcss-rebase-url":
393399
- changed-files:
394400
- any-glob-to-any-file:

package-lock.json

+37-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/css-calc/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changes to CSS Calc
22

3+
### Unreleased (minor)
4+
5+
- Add support for `random()`
6+
37
### 2.0.4
48

59
_November 1, 2024_

packages/css-calc/dist/index.cjs

+1-1
Large diffs are not rendered by default.

packages/css-calc/dist/index.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export declare type conversionOptions = {
3939
* @see https://drafts.csswg.org/css-values-4/#calc-simplification
4040
*/
4141
rawPercentages?: boolean;
42+
/**
43+
* Seed the pseudo random number generator used in `random()`
44+
*/
45+
randomSeed?: number;
4246
};
4347

4448
export declare type GlobalsWithStrings = Map<string, TokenDimension | TokenNumber | TokenPercentage | string>;

packages/css-calc/dist/index.mjs

+1-1
Large diffs are not rendered by default.

packages/css-calc/docs/css-calc.api.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@
361361
},
362362
{
363363
"kind": "Content",
364-
"text": ";\n precision?: number;\n toCanonicalUnits?: boolean;\n censorIntoStandardRepresentableValues?: boolean;\n rawPercentages?: boolean;\n}"
364+
"text": ";\n precision?: number;\n toCanonicalUnits?: boolean;\n censorIntoStandardRepresentableValues?: boolean;\n rawPercentages?: boolean;\n randomSeed?: number;\n}"
365365
},
366366
{
367367
"kind": "Content",

packages/css-calc/docs/css-calc.conversionoptions.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type conversionOptions = {
1313
toCanonicalUnits?: boolean;
1414
censorIntoStandardRepresentableValues?: boolean;
1515
rawPercentages?: boolean;
16+
randomSeed?: number;
1617
};
1718
```
1819
**References:** [GlobalsWithStrings](./css-calc.globalswithstrings.md)

packages/css-calc/src/functions/calc.ts

+111-18
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { TokenType, NumberType, isTokenOpenParen, isTokenDelim, isTokenComma, is
55
import { addition } from '../operation/addition';
66
import { division } from '../operation/division';
77
import { isCalculation, solve } from '../calculation';
8-
import { isCommentNode, FunctionNode, TokenNode, isFunctionNode, isSimpleBlockNode, isTokenNode, isWhitespaceNode } from '@csstools/css-parser-algorithms';
8+
import { FunctionNode, TokenNode, isFunctionNode, isSimpleBlockNode, isTokenNode, isWhiteSpaceOrCommentNode } from '@csstools/css-parser-algorithms';
99
import { multiplication } from '../operation/multiplication';
1010
import { resolveGlobalsAndConstants } from './globals-and-constants';
1111
import { solveACos } from './acos';
@@ -32,6 +32,7 @@ import { unary } from '../operation/unary';
3232
import { solveLog } from './log';
3333
import { isNone } from '../util/is-none';
3434
import type { conversionOptions } from '../options';
35+
import { solveRandom } from './random';
3536

3637
type mathFunction = (node: FunctionNode, globals: Globals, options: conversionOptions) => Calculation | -1
3738

@@ -51,6 +52,7 @@ export const mathFunctions: Map<string, mathFunction> = new Map([
5152
['min', min],
5253
['mod', mod],
5354
['pow', pow],
55+
['random', random],
5456
['rem', rem],
5557
['round', round],
5658
['sign', sign],
@@ -61,7 +63,7 @@ export const mathFunctions: Map<string, mathFunction> = new Map([
6163

6264
function calc(calcNode: FunctionNode | SimpleBlockNode, globals: Globals, options: conversionOptions): Calculation | -1 {
6365
const nodes: Array<ComponentValue | Calculation> = resolveGlobalsAndConstants(
64-
[...(calcNode.value.filter(x => !isCommentNode(x) && !isWhitespaceNode(x)))],
66+
[...(calcNode.value.filter(x => !isWhiteSpaceOrCommentNode(x)))],
6567
globals,
6668
);
6769

@@ -215,8 +217,17 @@ function calc(calcNode: FunctionNode | SimpleBlockNode, globals: Globals, option
215217
}
216218

217219
function singleNodeSolver(fnNode: FunctionNode, globals: Globals, options: conversionOptions, solveFn: (node: FunctionNode, a: TokenNode, options: conversionOptions) => Calculation | -1): Calculation | -1 {
220+
const a = singleArgument(fnNode.value, globals, options);
221+
if (a === -1) {
222+
return -1;
223+
}
224+
225+
return solveFn(fnNode, a, options);
226+
}
227+
228+
function singleArgument(values: Array<ComponentValue>, globals: Globals, options: conversionOptions): TokenNode | -1 {
218229
const nodes: Array<ComponentValue> = resolveGlobalsAndConstants(
219-
[...(fnNode.value.filter(x => !isCommentNode(x) && !isWhitespaceNode(x)))],
230+
[...(values.filter(x => !isWhiteSpaceOrCommentNode(x)))],
220231
globals,
221232
);
222233

@@ -225,12 +236,23 @@ function singleNodeSolver(fnNode: FunctionNode, globals: Globals, options: conve
225236
return -1;
226237
}
227238

228-
return solveFn(fnNode, a, options);
239+
return a;
229240
}
230241

231242
function twoCommaSeparatedNodesSolver(fnNode: FunctionNode, globals: Globals, options: conversionOptions, solveFn: (node: FunctionNode, a: TokenNode, b: TokenNode, options: conversionOptions) => Calculation | -1): Calculation | -1 {
243+
const solvedNodes = twoCommaSeparatedArguments(fnNode.value, globals, options);
244+
if (solvedNodes === -1) {
245+
return -1;
246+
}
247+
248+
const [a, b] = solvedNodes;
249+
250+
return solveFn(fnNode, a, b, options);
251+
}
252+
253+
function twoCommaSeparatedArguments(values: Array<ComponentValue>, globals: Globals, options: conversionOptions): [TokenNode, TokenNode] | -1 {
232254
const nodes: Array<ComponentValue> = resolveGlobalsAndConstants(
233-
[...(fnNode.value.filter(x => !isCommentNode(x) && !isWhitespaceNode(x)))],
255+
[...(values.filter(x => !isWhiteSpaceOrCommentNode(x)))],
234256
globals,
235257
);
236258

@@ -270,20 +292,29 @@ function twoCommaSeparatedNodesSolver(fnNode: FunctionNode, globals: Globals, op
270292
return -1;
271293
}
272294

273-
return solveFn(fnNode, a, b, options);
295+
return [a, b];
296+
}
297+
298+
function variadicNodesSolver(fnNode: FunctionNode, values: Array<ComponentValue>, globals: Globals, options: conversionOptions, solveFn: (node: FunctionNode, x: Array<ComponentValue>, options: conversionOptions) => Calculation | -1): Calculation | -1 {
299+
const solvedNodes = variadicArguments(fnNode.value, globals, options);
300+
if (solvedNodes === -1) {
301+
return -1;
302+
}
303+
304+
return solveFn(fnNode, solvedNodes, options);
274305
}
275306

276-
function variadicNodesSolver(fnNode: FunctionNode, globals: Globals, options: conversionOptions, solveFn: (node: FunctionNode, x: Array<ComponentValue>, options: conversionOptions) => Calculation | -1): Calculation | -1 {
307+
function variadicArguments(values: Array<ComponentValue>, globals: Globals, options: conversionOptions): Array<TokenNode> | -1 {
277308
const nodes: Array<ComponentValue> = resolveGlobalsAndConstants(
278-
[...(fnNode.value.filter(x => !isCommentNode(x) && !isWhitespaceNode(x)))],
309+
[...(values.filter(x => !isWhiteSpaceOrCommentNode(x)))],
279310
globals,
280311
);
281312

282-
const solvedNodes: Array<ComponentValue> = [];
313+
const solvedNodes: Array<TokenNode> = [];
283314

284315
{
285-
const chunks = [];
286-
let chunk = [];
316+
const chunks: Array<Array<ComponentValue>> = [];
317+
let chunk: Array<ComponentValue> = [];
287318
for (let i = 0; i < nodes.length; i++) {
288319
const node = nodes[i];
289320
if (isTokenNode(node) && isTokenComma(node.value)) {
@@ -311,12 +342,12 @@ function variadicNodesSolver(fnNode: FunctionNode, globals: Globals, options: co
311342
}
312343
}
313344

314-
return solveFn(fnNode, solvedNodes, options);
345+
return solvedNodes;
315346
}
316347

317348
function clamp(clampNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
318349
const nodes: Array<ComponentValue> = resolveGlobalsAndConstants(
319-
[...(clampNode.value.filter(x => !isCommentNode(x) && !isWhitespaceNode(x)))],
350+
[...(clampNode.value.filter(x => !isWhiteSpaceOrCommentNode(x)))],
320351
globals,
321352
);
322353

@@ -394,11 +425,11 @@ function clamp(clampNode: FunctionNode, globals: Globals, options: conversionOpt
394425
}
395426

396427
function max(maxNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
397-
return variadicNodesSolver(maxNode, globals, options, solveMax);
428+
return variadicNodesSolver(maxNode, maxNode.value, globals, options, solveMax);
398429
}
399430

400431
function min(minNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
401-
return variadicNodesSolver(minNode, globals, options, solveMin);
432+
return variadicNodesSolver(minNode, minNode.value, globals, options, solveMin);
402433
}
403434

404435
const roundingStrategies = new Set([
@@ -410,7 +441,7 @@ const roundingStrategies = new Set([
410441

411442
function round(roundNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
412443
const nodes: Array<ComponentValue> = resolveGlobalsAndConstants(
413-
[...(roundNode.value.filter(x => !isCommentNode(x) && !isWhitespaceNode(x)))],
444+
[...(roundNode.value.filter(x => !isWhiteSpaceOrCommentNode(x)))],
414445
globals,
415446
);
416447

@@ -537,11 +568,73 @@ function pow(powNode: FunctionNode, globals: Globals, options: conversionOptions
537568
}
538569

539570
function hypot(hypotNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
540-
return variadicNodesSolver(hypotNode, globals, options, solveHypot);
571+
return variadicNodesSolver(hypotNode, hypotNode.value, globals, options, solveHypot);
541572
}
542573

543574
function log(logNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
544-
return variadicNodesSolver(logNode, globals, options, solveLog);
575+
return variadicNodesSolver(logNode, logNode.value, globals, options, solveLog);
576+
}
577+
578+
function random(randomNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
579+
const nodes: Array<ComponentValue> = randomNode.value.filter(x => !isWhiteSpaceOrCommentNode(x));
580+
581+
let randomCachingOptions = '';
582+
const stepValues: Array<ComponentValue> = []
583+
const values: Array<ComponentValue> = []
584+
585+
{
586+
for (let i = 0; i < nodes.length; i++) {
587+
const node = nodes[i];
588+
if (!randomCachingOptions && values.length === 0 && isTokenNode(node) && isTokenIdent(node.value)) {
589+
const token = node.value;
590+
const tokenStr = token[4].value.toLowerCase();
591+
if (tokenStr === 'per-element' || tokenStr.startsWith('--')) {
592+
randomCachingOptions = tokenStr;
593+
594+
const nextNode = nodes[i + 1];
595+
if (!isTokenNode(nextNode) || !isTokenComma(nextNode.value)) {
596+
return -1;
597+
}
598+
599+
i++;
600+
continue;
601+
}
602+
}
603+
604+
if (isTokenNode(node) && isTokenComma(node.value)) {
605+
const nextNode = nodes[i + 1];
606+
607+
if (values.length > 0 && isTokenNode(nextNode) && isTokenIdent(nextNode.value)) {
608+
const token = nextNode.value;
609+
const tokenStr = token[4].value.toLowerCase();
610+
if (tokenStr === 'by' || tokenStr.startsWith('--')) {
611+
stepValues.push(...nodes.slice(i + 2));
612+
613+
break;
614+
}
615+
}
616+
}
617+
618+
values.push(node);
619+
}
620+
}
621+
622+
const solvedValues = twoCommaSeparatedArguments(values, globals, options);
623+
if (solvedValues === -1) {
624+
return -1;
625+
}
626+
627+
const [a, b] = solvedValues;
628+
629+
let solvedStepValue: TokenNode | -1 | null = null;
630+
if (stepValues.length) {
631+
solvedStepValue = singleArgument(stepValues, globals, options);
632+
if (solvedStepValue === -1) {
633+
return -1;
634+
}
635+
}
636+
637+
return solveRandom(randomNode, randomCachingOptions, a, b, solvedStepValue, options);
545638
}
546639

547640
function calcWrapper(v: Array<ComponentValue>): FunctionNode {

packages/css-calc/src/functions/hypot.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Calculation } from '../calculation';
2-
import type { ComponentValue, FunctionNode, TokenNode } from '@csstools/css-parser-algorithms';
2+
import type { ComponentValue, FunctionNode } from '@csstools/css-parser-algorithms';
33
import { convertUnit } from '../unit-conversions';
44
import { isTokenNode } from '@csstools/css-parser-algorithms';
55
import { resultToCalculation } from './result-to-calculation';
@@ -8,15 +8,11 @@ import { isTokenNumeric, isTokenPercentage } from '@csstools/css-tokenizer';
88
import type { conversionOptions } from '../options';
99

1010
export function solveHypot(hypotNode: FunctionNode, solvedNodes: Array<ComponentValue>, options: conversionOptions): Calculation | -1 {
11-
const firstSolvedNode = solvedNodes[0];
12-
if (!firstSolvedNode || !isTokenNode(firstSolvedNode)) {
13-
return -1;
11+
if (!solvedNodes.every(isTokenNode)) {
12+
return -1
1413
}
1514

16-
const componentTypes = new Set(solvedNodes.map((x) => x.type));
17-
if (componentTypes.size !== 1) {
18-
return -1;
19-
}
15+
const firstSolvedNode = solvedNodes[0];
2016

2117
const firstSolvedToken = firstSolvedNode.value;
2218
if (!isTokenNumeric(firstSolvedToken)) {
@@ -27,7 +23,7 @@ export function solveHypot(hypotNode: FunctionNode, solvedNodes: Array<Component
2723
return -1;
2824
}
2925

30-
const tokens = solvedNodes.map((x) => convertUnit(firstSolvedToken, (x as TokenNode).value));
26+
const tokens = solvedNodes.map((x) => convertUnit(firstSolvedToken, x.value));
3127
if (!arrayOfSameNumeric(tokens)) {
3228
return -1;
3329
}

0 commit comments

Comments
 (0)