Skip to content

Commit 6368291

Browse files
committed
feat: allow custom at-rule name
1 parent b743e0a commit 6368291

5 files changed

Lines changed: 211 additions & 57 deletions

File tree

.changeset/loud-ads-repair.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'postcss-color-scheme': minor
3+
---
4+
5+
allow customize at-rule name and behavior upon invalid parameter

lib/postcss.js

Lines changed: 66 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -49,69 +49,82 @@ function getHtmlSelector(selector) {
4949
}
5050

5151
/**
52-
* @param {import('postcss').Helpers} helpers
53-
* @param {import('postcss').AtRule} atRule
52+
* @typedef Options
53+
* @property {string} name - name of the custom at-rule, default to 'color-scheme'
54+
* @property {'throw' | 'skip'} onInvalidParameter - behavior when an invalid parameter is found, default to 'throw'
5455
*/
55-
function transform(helpers, atRule) {
56-
const theme = atRule.params.trim();
57-
if (!theme) {
58-
throw atRule.error(noParameter());
59-
}
60-
61-
if (!['light', 'dark'].includes(theme)) {
62-
throw atRule.error(invalidParameter(theme));
63-
}
64-
const complement = theme === 'dark' ? 'light' : 'dark';
65-
66-
const firstNonAtRuleParent = findFirstNonAtRuleParent(atRule);
67-
68-
const isAtRoot = !firstNonAtRuleParent;
69-
const isNestedInHtml = firstNonAtRuleParent && !!getHtmlSelector(firstNonAtRuleParent.selector);
70-
71-
let implicitSelector = `:not([data-color-scheme="${complement}"])`;
72-
let explicitSelector = `[data-color-scheme="${theme}"]`;
73-
if (isNestedInHtml) {
74-
implicitSelector = `&${implicitSelector}`;
75-
explicitSelector = `&${explicitSelector}`;
76-
} else if (!isAtRoot) {
77-
implicitSelector = `html${implicitSelector} &`;
78-
explicitSelector = `html${explicitSelector} &`;
79-
} else {
80-
implicitSelector = `html${implicitSelector}`;
81-
explicitSelector = `html${explicitSelector}`;
82-
transformFirstChildrenNonAtRule(helpers, atRule);
83-
}
84-
85-
const implicitRule = new helpers.Rule({
86-
selector: implicitSelector,
87-
nodes: atRule.nodes,
88-
});
89-
const implicitAtRule = new helpers.AtRule({
90-
source: atRule.source,
91-
name: 'media',
92-
params: `(prefers-color-scheme: ${theme})`,
93-
nodes: [implicitRule],
94-
});
95-
96-
const explicitRule = new helpers.Rule({
97-
selector: explicitSelector,
98-
source: atRule.source,
99-
nodes: atRule.nodes,
100-
});
101-
102-
atRule.replaceWith(implicitAtRule, explicitRule);
103-
}
10456

10557
/**
10658
* @namespace
59+
* @param {Partial<Options>} [options]
10760
* @returns {import('postcss').Plugin}
10861
* @property
10962
*/
110-
function pluginCreator() {
63+
function pluginCreator(options = {}) {
64+
const name = options?.name ?? 'color-scheme';
65+
let onInvalidParameter = options?.onInvalidParameter;
66+
if (!onInvalidParameter) {
67+
onInvalidParameter = name === 'color-scheme' ? 'throw' : 'skip';
68+
}
11169
return {
11270
postcssPlugin: 'postcss-color-scheme',
11371
AtRule: {
114-
'color-scheme': (atRule, helpers) => transform(helpers, atRule),
72+
[name]: function (atRule, helpers) {
73+
const theme = atRule.params.trim();
74+
if (!theme) {
75+
if (onInvalidParameter === 'throw') {
76+
throw atRule.error(noParameter());
77+
}
78+
return;
79+
}
80+
81+
if (!['light', 'dark'].includes(theme)) {
82+
if (onInvalidParameter === 'throw') {
83+
throw atRule.error(invalidParameter(theme));
84+
}
85+
return;
86+
}
87+
const complement = theme === 'dark' ? 'light' : 'dark';
88+
89+
const firstNonAtRuleParent = findFirstNonAtRuleParent(atRule);
90+
91+
const isAtRoot = !firstNonAtRuleParent;
92+
const isNestedInHtml =
93+
firstNonAtRuleParent && !!getHtmlSelector(firstNonAtRuleParent.selector);
94+
95+
let implicitSelector = `:not([data-color-scheme="${complement}"])`;
96+
let explicitSelector = `[data-color-scheme="${theme}"]`;
97+
if (isNestedInHtml) {
98+
implicitSelector = `&${implicitSelector}`;
99+
explicitSelector = `&${explicitSelector}`;
100+
} else if (!isAtRoot) {
101+
implicitSelector = `html${implicitSelector} &`;
102+
explicitSelector = `html${explicitSelector} &`;
103+
} else {
104+
implicitSelector = `html${implicitSelector}`;
105+
explicitSelector = `html${explicitSelector}`;
106+
transformFirstChildrenNonAtRule(helpers, atRule);
107+
}
108+
109+
const implicitRule = new helpers.Rule({
110+
selector: implicitSelector,
111+
nodes: atRule.nodes,
112+
});
113+
const implicitAtRule = new helpers.AtRule({
114+
source: atRule.source,
115+
name: 'media',
116+
params: `(prefers-color-scheme: ${theme})`,
117+
nodes: [implicitRule],
118+
});
119+
120+
const explicitRule = new helpers.Rule({
121+
selector: explicitSelector,
122+
source: atRule.source,
123+
nodes: atRule.nodes,
124+
});
125+
126+
atRule.replaceWith(implicitAtRule, explicitRule);
127+
},
115128
},
116129
};
117130
}

tests/context.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ import { expect, test as vTest } from 'vitest';
66

77
import plugin from '../lib/postcss.js';
88

9-
async function compile(input: string, plugins: postcss.Plugin[] = []): Promise<string> {
9+
type CompileConfig = {
10+
options?: Parameters<typeof plugin>[0];
11+
plugins?: postcss.AcceptedPlugin[];
12+
};
13+
14+
async function compile(input: string, config: CompileConfig = {}): Promise<string> {
1015
const { currentTestName } = expect.getState();
11-
const processor = postcss([plugin(), ...plugins]);
16+
const processor = postcss([plugin(config.options), ...(config.plugins ?? [])]);
1217
const result = await processor.process(input, {
1318
from: `${path.resolve(import.meta.url)}?test=${currentTestName}`,
1419
});

tests/units/custom-name.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { invalidParameter, noParameter } from '../../lib/errors';
2+
import { test } from '../context';
3+
4+
test('with valid parameter', async ({ expect, utils: { compile, css } }) => {
5+
const compiled = await compile(
6+
css`
7+
html {
8+
--color: black;
9+
10+
@media dark {
11+
--color: white;
12+
}
13+
}
14+
`,
15+
{
16+
options: {
17+
name: 'media',
18+
},
19+
},
20+
);
21+
expect(compiled).toMatchInlineSnapshot(`
22+
"html {
23+
--color: black;
24+
25+
@media (prefers-color-scheme: dark) {
26+
27+
&:not([data-color-scheme="light"]) {
28+
--color: white
29+
}
30+
}
31+
32+
&[data-color-scheme="dark"] {
33+
--color: white
34+
}
35+
}"
36+
`);
37+
});
38+
39+
test('with invalid parameter - automatically skip', async ({ expect, utils: { compile, css } }) => {
40+
const compiled = await compile(
41+
css`
42+
html {
43+
--color: black;
44+
45+
@media (width >= 48rem) {
46+
--color: white;
47+
}
48+
}
49+
`,
50+
{
51+
options: {
52+
name: 'media',
53+
},
54+
},
55+
);
56+
expect(compiled).toMatchInlineSnapshot(`
57+
"html {
58+
--color: black;
59+
60+
@media (width >= 48rem) {
61+
--color: white;
62+
}
63+
}"
64+
`);
65+
});
66+
67+
test('with no parameter - force throw', async ({ expect, utils: { compile, css } }) => {
68+
await expect(
69+
compile(
70+
css`
71+
html {
72+
--color: black;
73+
74+
@media {
75+
--color: white;
76+
}
77+
}
78+
`,
79+
{
80+
options: {
81+
name: 'media',
82+
onInvalidParameter: 'throw',
83+
},
84+
},
85+
),
86+
).rejects.toThrowError(noParameter());
87+
});
88+
89+
test('with invalid parameter', async ({ expect, utils: { compile, css } }) => {
90+
await expect(
91+
compile(
92+
css`
93+
html {
94+
--color: black;
95+
96+
@media invalid {
97+
--color: white;
98+
}
99+
}
100+
`,
101+
{
102+
options: {
103+
name: 'media',
104+
onInvalidParameter: 'throw',
105+
},
106+
},
107+
),
108+
).rejects.toThrowError(invalidParameter('invalid'));
109+
});

tests/units/errors.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,40 @@
11
import { invalidParameter, noParameter } from '../../lib/errors';
22
import { test } from '../context';
33

4-
test('no parameter', async ({ expect, utils: { compile, css } }) => {
4+
test('no parameter - throw', async ({ expect, utils: { compile, css } }) => {
55
const input = css` @color-scheme; `;
66

77
await expect(compile(input)).rejects.toThrowError(noParameter());
88
});
99

10-
test('invalid parameter', async ({ expect, utils: { compile, css } }) => {
10+
test('no parameter - skip', async ({ expect, utils: { compile, css } }) => {
11+
const input = css` @color-scheme; `;
12+
13+
const compiled = await compile(input, {
14+
options: {
15+
onInvalidParameter: 'skip',
16+
},
17+
});
18+
expect(compiled).toBe(input);
19+
});
20+
21+
test('invalid parameter - throw', async ({ expect, utils: { compile, css } }) => {
1122
const input = css` @color-scheme invalid; `;
1223

1324
await expect(compile(input)).rejects.toThrowError(invalidParameter('invalid'));
1425
});
1526

27+
test('invalid parameter - skip', async ({ expect, utils: { compile, css } }) => {
28+
const input = css` @color-scheme invalid; `;
29+
30+
const compiled = await compile(input, {
31+
options: {
32+
onInvalidParameter: 'skip',
33+
},
34+
});
35+
expect(compiled).toBe(input);
36+
});
37+
1638
test('empty css', async ({ expect, utils: { compile, css } }) => {
1739
const input = css``;
1840

0 commit comments

Comments
 (0)