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
1 change: 1 addition & 0 deletions packages/media-query-list-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Unreleased (patch)

- Fixed: avoid exponential parse times for deeply nested media conditions.
- Updated [`@csstools/css-tokenizer`](https://github.com/csstools/postcss-plugins/tree/main/packages/css-tokenizer) to [`4.0.1`](https://github.com/csstools/postcss-plugins/tree/main/packages/css-tokenizer/CHANGELOG.md#401) (patch)

### 5.0.0
Expand Down
2 changes: 1 addition & 1 deletion packages/media-query-list-parser/dist/index.mjs

Large diffs are not rendered by default.

126 changes: 43 additions & 83 deletions packages/media-query-list-parser/src/parser/parse-media-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,75 +112,38 @@ export function parseMediaQuery(componentValues: Array<ComponentValue>): MediaQu
}
}

function parseMediaConditionListWithOr(componentValues: Array<ComponentValue>): MediaConditionListWithOr | false {
function parseMediaConditionList(componentValues: Array<ComponentValue>, allowOr: boolean): MediaConditionListWithAnd | MediaConditionListWithOr | false {
let leading: MediaInParens | false = false;
const list: Array<MediaOr> = [];
let operator: 'and' | 'or' | false = false;
const list: Array<MediaAnd | MediaOr> = [];
let firstIndex = -1;
let lastIndex = -1;

for (let i = 0; i < componentValues.length; i++) {
if (leading) {
const part = parseMediaOr(componentValues.slice(i));
if (part !== false) {
i += part.advance;
list.push(part.node);
if (leading && operator === false) {
const mediaAnd = parseMediaAnd(componentValues.slice(i));
if (mediaAnd !== false) {
operator = 'and';
i += mediaAnd.advance;
list.push(mediaAnd.node);
lastIndex = i;
continue;
}
}

const componentValue = componentValues[i];
if (componentValue.type === ComponentValueType.Whitespace) {
continue;
}

if (componentValue.type === ComponentValueType.Comment) {
continue;
}

if (leading) {
return false;
}

if (leading === false && isSimpleBlockNode(componentValue)) {
componentValue.normalize();
leading = parseMediaInParensFromSimpleBlock(componentValue);
if (leading === false) {
return false;
if (allowOr) {
const mediaOr = parseMediaOr(componentValues.slice(i));
if (mediaOr !== false) {
operator = 'or';
i += mediaOr.advance;
list.push(mediaOr.node);
lastIndex = i;
continue;
}
}

firstIndex = i;
continue;
}

return false;
}

if (leading && list.length) {
return new MediaConditionListWithOr(
leading,
list,
componentValues.slice(0, firstIndex).flatMap((x) => {
return x.tokens();
}),
componentValues.slice(lastIndex + 1).flatMap((x) => {
return x.tokens();
}),
);
}

return false;
}

function parseMediaConditionListWithAnd(componentValues: Array<ComponentValue>): MediaConditionListWithAnd | false {
let leading: MediaInParens | false = false;
const list: Array<MediaAnd> = [];
let firstIndex = -1;
let lastIndex = -1;

for (let i = 0; i < componentValues.length; i++) {
if (leading) {
const part = parseMediaAnd(componentValues.slice(i));
if (leading && operator) {
const part = operator === 'and' ? parseMediaAnd(componentValues.slice(i)) : parseMediaOr(componentValues.slice(i));
if (part !== false) {
i += part.advance;
list.push(part.node);
Expand All @@ -202,7 +165,7 @@ function parseMediaConditionListWithAnd(componentValues: Array<ComponentValue>):
return false;
}

if (leading === false && isSimpleBlockNode(componentValue)) {
if (isSimpleBlockNode(componentValue)) {
componentValue.normalize();
leading = parseMediaInParensFromSimpleBlock(componentValue);
if (leading === false) {
Expand All @@ -217,16 +180,18 @@ function parseMediaConditionListWithAnd(componentValues: Array<ComponentValue>):
}

if (leading && list.length) {
return new MediaConditionListWithAnd(
leading,
list,
componentValues.slice(0, firstIndex).flatMap((x) => {
return x.tokens();
}),
componentValues.slice(lastIndex + 1).flatMap((x) => {
return x.tokens();
}),
);
const before = componentValues.slice(0, firstIndex).flatMap((x) => {
return x.tokens();
});
const after = componentValues.slice(lastIndex + 1).flatMap((x) => {
return x.tokens();
});

if (operator === 'and') {
return new MediaConditionListWithAnd(leading, list as Array<MediaAnd>, before, after);
}

return new MediaConditionListWithOr(leading, list as Array<MediaOr>, before, after);
}

return false;
Expand All @@ -238,21 +203,16 @@ function parseMediaCondition(componentValues: Array<ComponentValue>): MediaCondi
return new MediaCondition(mediaNot);
}

const mediaListAnd = parseMediaConditionListWithAnd(componentValues);
if (mediaListAnd !== false) {
return new MediaCondition(mediaListAnd);
}

const mediaListOr = parseMediaConditionListWithOr(componentValues);
if (mediaListOr !== false) {
return new MediaCondition(mediaListOr);
}

const mediaInParens = parseMediaInParens(componentValues);
if (mediaInParens !== false) {
return new MediaCondition(mediaInParens);
}

const mediaConditionList = parseMediaConditionList(componentValues, true);
if (mediaConditionList !== false) {
return new MediaCondition(mediaConditionList);
}

return false;
}

Expand All @@ -262,16 +222,16 @@ function parseMediaConditionWithoutOr(componentValues: Array<ComponentValue>): M
return new MediaCondition(mediaNot);
}

const mediaListAnd = parseMediaConditionListWithAnd(componentValues);
if (mediaListAnd !== false) {
return new MediaCondition(mediaListAnd);
}

const mediaInParens = parseMediaInParens(componentValues);
if (mediaInParens !== false) {
return new MediaCondition(mediaInParens);
}

const mediaConditionList = parseMediaConditionList(componentValues, false);
if (mediaConditionList !== false) {
return new MediaCondition(mediaConditionList);
}

return false;
}

Expand Down
24 changes: 24 additions & 0 deletions packages/media-query-list-parser/test/api/deeply-nested.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import assert from 'node:assert';
import { performance } from 'node:perf_hooks';
import { parse } from '@csstools/media-query-list-parser';

{
const depth = 500;
const source = '('.repeat(depth) + 'width' + ')'.repeat(depth);

const start = performance.now();
const resultAST = parse(source);
const duration = performance.now() - start;

assert.equal(resultAST.length, 1);

assert.equal(
resultAST[0].toString(),
source,
);

assert.ok(
duration < 1000,
`Parsing a deeply nested media query took ${duration}ms, expected well under 1000ms.`,
);
}
1 change: 1 addition & 0 deletions packages/media-query-list-parser/test/test.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './api/deeply-nested.mjs';
import './api/options.mjs';

import './cases/custom-media/0001.mjs';
Expand Down
Loading