Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ All notable changes to this project will be documented in this file. See [commit

### Bug fixes

- `reduceCalc` and the plugin no longer print an error for a
value nested more than 1024 levels deep which is not a math function, for
example when `calc(` only appears inside `url()`, a string or a comment. The
- `reduceCalc` and the plugin no longer report the nesting-limit error for a
value nested more than 1024 levels deep that has no supported math function
outside `url()`, a string or a comment, such as `url(calc(1+2))((((…`. The
output is unchanged.

### Changes

- `onParseError` and `onWarn` are now called in source order, one candidate at
a time, instead of all parse errors first. A value with several errors may
report a different first error, for example `clamp(.5/*c*/+!;)` now reports
the `+` error at position 13 instead of the `!`. The output is unchanged.

## 11.2.3 (2026-10-10)

### Bug fixes
Expand Down
12 changes: 8 additions & 4 deletions scripts/benchmark/benchmark-serialization.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
// through both the worktree serializer and the serializer from HEAD.
import { execFileSync } from 'node:child_process';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { serialize as serializeWorktree } from '../../src/lib/serialize.js';
import {
num,
Expand Down Expand Up @@ -143,7 +142,12 @@ function benchmarkPair(worktreeSerializer, headSerializer, node, materialize) {

/** @return {Promise<typeof import('../../src/lib/serialize.js')>} */
async function loadHeadSerializer() {
const root = mkdtempSync(join(tmpdir(), 'postcss-calc-serialize-'));
// Extract under the repository so the HEAD sources resolve node_modules.
const cache = fileURLToPath(
new URL('../../node_modules/.cache/', import.meta.url)
);
mkdirSync(cache, { recursive: true });
const root = mkdtempSync(join(cache, 'postcss-calc-serialize-'));
try {
const archive = execFileSync('git', ['archive', 'HEAD', '--', 'src/lib']);
execFileSync('tar', ['-x', '-f', '-', '-C', root], { input: archive });
Expand Down
82 changes: 59 additions & 23 deletions scripts/benchmark/parser-benchmark-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,15 @@ if (typeof sourceRoot !== 'string')
throw new Error('missing parser source root');

const parser = await import(pathToFileURL(`${sourceRoot}/lib/parser.js`).href);
let blockIndex;
try {
blockIndex = await import(
pathToFileURL(`${sourceRoot}/lib/block-index.js`).href
);
} catch (error) {
if (
!(error instanceof Error) ||
!('code' in error) ||
error.code !== 'ERR_MODULE_NOT_FOUND'
) {
throw error;
}
blockIndex = undefined;
}
// The candidate builds a closer array with `matchBlocks`; the baseline builds a
// `BlockIndex` with `indexBlocks`. Either is passed to `parse` as the fourth
// argument, so one timed closure serves both.
const closerBuilder = await importOptional(`${sourceRoot}/lib/blocks.js`).then(
(blocks) => blocks && ((tokens) => blocks.matchBlocks(tokens).close)
);
const buildCloser =
closerBuilder ??
(await importOptional(`${sourceRoot}/lib/block-index.js`))?.indexBlocks;
if (typeof parser.parse !== 'function')
throw new Error('incompatible baseline parser API');

Expand All @@ -37,7 +31,51 @@ const MIN_WARMUPS = payload.warmupMinimum ?? 5;
const MAX_WARMUPS = payload.warmupMaximum ?? 10;
const MEASURED_BATCHES = payload.measuredBatchCount ?? 6;

function digest(value) {
/** @param {string} url @return {Promise<any>} undefined when the module is absent */
async function importOptional(url) {
try {
return await import(pathToFileURL(url).href);
} catch (error) {
if (
error instanceof Error &&
'code' in error &&
error.code === 'ERR_MODULE_NOT_FOUND'
) {
return undefined;
}
throw error;
}
}

/**
* Opaque components were nested arrays before the flat representation. Both
* shapes describe the same text, so compare them by their flattened form:
* arrays are spliced in and adjacent strings are merged.
*/
function normalizeTree(value) {
if (value === null || typeof value !== 'object') return value;
if (Array.isArray(value)) return value.map(normalizeTree);
if (value.type === 'OpaqueCall') {
const parts = [];
const pushPart = (part) => {
if (Array.isArray(part)) {
for (const child of part) pushPart(child);
} else if (typeof part === 'string' && typeof parts.at(-1) === 'string') {
parts[parts.length - 1] += part;
} else {
parts.push(normalizeTree(part));
}
};
for (const part of value.components) pushPart(part);
return { ...value, components: parts };
}
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [key, normalizeTree(child)])
);
}

function digest(input) {
const value = normalizeTree(input);
const hash = createHash('sha256');
const stack = [value];
while (stack.length) {
Expand Down Expand Up @@ -75,14 +113,12 @@ function consume(value) {

function makeRun(workload) {
const tokens = tokenize({ css: workload.source });
const index =
blockIndex && workload.mode === 'hot-shared-index'
? blockIndex.indexBlocks(tokens)
: undefined;
const shared = buildCloser && workload.mode === 'hot-shared-index';
const closer = shared ? buildCloser(tokens) : undefined;
return () => {
if (!blockIndex) return parser.parse(tokens, 0, tokens.length);
const currentIndex = index ?? blockIndex.indexBlocks(tokens);
return parser.parse(tokens, 0, tokens.length, currentIndex);
if (!buildCloser) return parser.parse(tokens, 0, tokens.length);
const current = closer ?? buildCloser(tokens);
return parser.parse(tokens, 0, tokens.length, current);
};
}

Expand Down
10 changes: 4 additions & 6 deletions scripts/lib/corpus-policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { createHash } from 'node:crypto';
import { calc as csstoolsCalc } from '@csstools/css-calc';
import { tokenize } from '@csstools/css-tokenizer';
import { indexBlocks } from '../../src/lib/block-index.js';
import { parse } from '../../src/lib/parser.js';
import { simplify } from '../../src/lib/simplify.js';
import { serialize } from '../../src/lib/serialize.js';
Expand Down Expand Up @@ -52,10 +51,9 @@ export const NEUTRAL_CORPUS_CATEGORIES = new Set([
export function ourOutput(input) {
try {
const tokens = tokenize({ css: input });
return serialize(
simplify(parse(tokens, 0, tokens.length, indexBlocks(tokens))),
{ precision: COMPARE_PRECISION }
);
return serialize(simplify(parse(tokens, 0, tokens.length)), {
precision: COMPARE_PRECISION,
});
} catch {
return null;
}
Expand Down Expand Up @@ -137,7 +135,7 @@ export function stableHash(value) {

export function rootShape(input) {
const tokens = tokenize({ css: input });
const ast = parse(tokens, 0, tokens.length, indexBlocks(tokens));
const ast = parse(tokens, 0, tokens.length);
const root =
ast.type === 'Call' &&
ast.name.toLowerCase().endsWith('calc') &&
Expand Down
8 changes: 3 additions & 5 deletions scripts/randomizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { fileURLToPath } from 'node:url';
import fc from 'fast-check';
import { calc as csstoolsCalc } from '@csstools/css-calc';
import { tokenize } from '@csstools/css-tokenizer';
import { indexBlocks } from '../src/lib/block-index.js';
import { parse } from '../src/lib/parser.js';
import { simplify } from '../src/lib/simplify.js';
import { serialize } from '../src/lib/serialize.js';
Expand All @@ -32,10 +31,9 @@ const COMPARE_PRECISION = 9;
function ourOut(input) {
try {
const tokens = tokenize({ css: input });
return serialize(
simplify(parse(tokens, 0, tokens.length, indexBlocks(tokens))),
{ precision: COMPARE_PRECISION }
);
return serialize(simplify(parse(tokens, 0, tokens.length)), {
precision: COMPARE_PRECISION,
});
} catch {
return null;
}
Expand Down
8 changes: 3 additions & 5 deletions scripts/show-divergences.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Bucket github-pure corpus divergences against @csstools/css-calc.
import { tokenize } from '@csstools/css-tokenizer';
import { indexBlocks } from '../src/lib/block-index.js';
import { parse } from '../src/lib/parser.js';
import { simplify } from '../src/lib/simplify.js';
import { serialize } from '../src/lib/serialize.js';
Expand All @@ -10,10 +9,9 @@ const lines = loadCorpus();
const ours = (s) => {
try {
const tokens = tokenize({ css: s });
return serialize(
simplify(parse(tokens, 0, tokens.length, indexBlocks(tokens))),
{ precision: 10 }
);
return serialize(simplify(parse(tokens, 0, tokens.length)), {
precision: 10,
});
} catch {
return null;
}
Expand Down
3 changes: 1 addition & 2 deletions scripts/split-corpus.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { tokenize } from '@csstools/css-tokenizer';
import { indexBlocks } from '../src/lib/block-index.js';
import { parse } from '../src/lib/parser.js';
import { simplify } from '../src/lib/simplify.js';
import { serialize } from '../src/lib/serialize.js';
Expand All @@ -20,7 +19,7 @@ const PREPROC_RE = /#\{|\$[A-Za-z_]|@[A-Za-z_]|~["']/;
function ourParserAccepts(s) {
try {
const tokens = tokenize({ css: s });
serialize(simplify(parse(tokens, 0, tokens.length, indexBlocks(tokens))), {
serialize(simplify(parse(tokens, 0, tokens.length)), {
precision: 10,
});
return true;
Expand Down
108 changes: 0 additions & 108 deletions src/lib/block-index.js

This file was deleted.

Loading
Loading