Skip to content

Commit 16c1bb1

Browse files
committed
feat: Add "css-pseudo-4" CSS Module with new pseudo-classes and pseudo-elements
1 parent 9cda2ca commit 16c1bb1

File tree

2 files changed

+268
-0
lines changed

2 files changed

+268
-0
lines changed

src/syntax-definitions.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,35 @@ export const cssModules = {
470470
Selector: ['slotted']
471471
}
472472
}
473+
},
474+
'css-pseudo-4': {
475+
pseudoClasses: {
476+
definitions: {
477+
NoArgument: [
478+
'focus-visible',
479+
'focus-within',
480+
'target-within',
481+
'blank',
482+
'user-invalid',
483+
'user-valid'
484+
],
485+
Selector: ['has', 'is', 'where', 'not']
486+
}
487+
},
488+
pseudoElements: {
489+
definitions: {
490+
NoArgument: [
491+
'marker',
492+
'selection',
493+
'target-text',
494+
'spelling-error',
495+
'grammar-error',
496+
'backdrop'
497+
],
498+
String: ['highlight', 'cue'],
499+
Selector: ['part']
500+
}
501+
}
473502
}
474503
} satisfies Record<string, SyntaxDefinition>;
475504

test/modules.test.ts

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,178 @@ describe('CSS Modules', () => {
259259
});
260260
});
261261

262+
describe('css-pseudo-4', () => {
263+
it('should parse pseudo-4 pseudo-classes', () => {
264+
const parse = createParser({
265+
modules: ['css-pseudo-4']
266+
});
267+
268+
// Simple pseudo-classes
269+
expect(parse(':focus-visible')).toEqual(
270+
ast.selector({
271+
rules: [
272+
ast.rule({
273+
items: [ast.pseudoClass({name: 'focus-visible'})]
274+
})
275+
]
276+
})
277+
);
278+
279+
expect(parse(':blank')).toEqual(
280+
ast.selector({
281+
rules: [
282+
ast.rule({
283+
items: [ast.pseudoClass({name: 'blank'})]
284+
})
285+
]
286+
})
287+
);
288+
289+
// Functional pseudo-classes
290+
expect(parse(':has(> img)')).toEqual(
291+
ast.selector({
292+
rules: [
293+
ast.rule({
294+
items: [
295+
ast.pseudoClass({
296+
name: 'has',
297+
argument: ast.selector({
298+
rules: [
299+
ast.rule({
300+
items: [ast.tagName({name: 'img'})],
301+
combinator: '>'
302+
})
303+
]
304+
})
305+
})
306+
]
307+
})
308+
]
309+
})
310+
);
311+
312+
expect(parse(':is(h1, h2, h3)')).toEqual(
313+
ast.selector({
314+
rules: [
315+
ast.rule({
316+
items: [
317+
ast.pseudoClass({
318+
name: 'is',
319+
argument: ast.selector({
320+
rules: [
321+
ast.rule({
322+
items: [ast.tagName({name: 'h1'})]
323+
}),
324+
ast.rule({
325+
items: [ast.tagName({name: 'h2'})]
326+
}),
327+
ast.rule({
328+
items: [ast.tagName({name: 'h3'})]
329+
})
330+
]
331+
})
332+
})
333+
]
334+
})
335+
]
336+
})
337+
);
338+
});
339+
340+
it('should parse pseudo-4 pseudo-elements', () => {
341+
const parse = createParser({
342+
modules: ['css-pseudo-4']
343+
});
344+
345+
// Simple pseudo-elements
346+
expect(parse('::marker')).toEqual(
347+
ast.selector({
348+
rules: [
349+
ast.rule({
350+
items: [ast.pseudoElement({name: 'marker'})]
351+
})
352+
]
353+
})
354+
);
355+
356+
expect(parse('::selection')).toEqual(
357+
ast.selector({
358+
rules: [
359+
ast.rule({
360+
items: [ast.pseudoElement({name: 'selection'})]
361+
})
362+
]
363+
})
364+
);
365+
366+
expect(parse('::target-text')).toEqual(
367+
ast.selector({
368+
rules: [
369+
ast.rule({
370+
items: [ast.pseudoElement({name: 'target-text'})]
371+
})
372+
]
373+
})
374+
);
375+
376+
// String argument pseudo-elements
377+
expect(parse('::highlight(example)')).toEqual(
378+
ast.selector({
379+
rules: [
380+
ast.rule({
381+
items: [
382+
ast.pseudoElement({
383+
name: 'highlight',
384+
argument: ast.string({value: 'example'})
385+
})
386+
]
387+
})
388+
]
389+
})
390+
);
391+
392+
// Selector argument pseudo-elements
393+
expect(parse('::part(button)')).toEqual(
394+
ast.selector({
395+
rules: [
396+
ast.rule({
397+
items: [
398+
ast.pseudoElement({
399+
name: 'part',
400+
argument: ast.selector({
401+
rules: [
402+
ast.rule({
403+
items: [ast.tagName({name: 'button'})]
404+
})
405+
]
406+
})
407+
})
408+
]
409+
})
410+
]
411+
})
412+
);
413+
});
414+
415+
it('should reject pseudo-4 selectors when module is not enabled', () => {
416+
const parse = createParser({
417+
syntax: {
418+
pseudoClasses: {
419+
unknown: 'reject'
420+
},
421+
pseudoElements: {
422+
unknown: 'reject'
423+
}
424+
}
425+
});
426+
427+
expect(() => parse(':focus-visible')).toThrow('Unknown pseudo-class: "focus-visible".');
428+
expect(() => parse(':has(> img)')).toThrow('Unknown pseudo-class: "has".');
429+
expect(() => parse('::marker')).toThrow('Unknown pseudo-element "marker".');
430+
expect(() => parse('::highlight(example)')).toThrow('Unknown pseudo-element "highlight".');
431+
});
432+
});
433+
262434
describe('Multiple modules', () => {
263435
it('should support multiple modules at once', () => {
264436
const parse = createParser({
@@ -309,5 +481,72 @@ describe('CSS Modules', () => {
309481
})
310482
);
311483
});
484+
485+
it('should support combining css-position and css-pseudo modules', () => {
486+
const parse = createParser({
487+
modules: ['css-position-3', 'css-pseudo-4']
488+
});
489+
490+
// Position pseudo-class
491+
expect(parse(':sticky')).toEqual(
492+
ast.selector({
493+
rules: [
494+
ast.rule({
495+
items: [ast.pseudoClass({name: 'sticky'})]
496+
})
497+
]
498+
})
499+
);
500+
501+
// Pseudo-4 pseudo-class
502+
expect(parse(':focus-visible')).toEqual(
503+
ast.selector({
504+
rules: [
505+
ast.rule({
506+
items: [ast.pseudoClass({name: 'focus-visible'})]
507+
})
508+
]
509+
})
510+
);
511+
512+
// Pseudo-4 pseudo-element
513+
expect(parse('::marker')).toEqual(
514+
ast.selector({
515+
rules: [
516+
ast.rule({
517+
items: [ast.pseudoElement({name: 'marker'})]
518+
})
519+
]
520+
})
521+
);
522+
523+
// Complex selector using both modules
524+
expect(parse('div:sticky:has(> img::marker)')).toEqual(
525+
ast.selector({
526+
rules: [
527+
ast.rule({
528+
items: [
529+
ast.tagName({name: 'div'}),
530+
ast.pseudoClass({name: 'sticky'}),
531+
ast.pseudoClass({
532+
name: 'has',
533+
argument: ast.selector({
534+
rules: [
535+
ast.rule({
536+
items: [
537+
ast.tagName({name: 'img'}),
538+
ast.pseudoElement({name: 'marker'})
539+
],
540+
combinator: '>'
541+
})
542+
]
543+
})
544+
})
545+
]
546+
})
547+
]
548+
})
549+
);
550+
});
312551
});
313552
});

0 commit comments

Comments
 (0)