Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Philipp Spiess
8e00c01a1d Add reference CSS coverage for parser 2025-10-29 12:15:24 +01:00
4 changed files with 441 additions and 0 deletions

View file

@ -0,0 +1,256 @@
/* Tailwind CSS syntax reference: demonstrates all supported directives and functions */
@config "./reference/reference.config.js";
@plugin "./reference/reference.plugin.js" {
enable-grid: true;
card-color: 'white';
accent-scale: 'sky', 'amber';
shadow-depth: 24;
}
@import 'tailwindcss' prefix(tw);
@import 'tailwindcss/utilities' important;
@import 'tailwindcss/theme' theme(static);
@import './reference/reference-import.css' layer(utilities) supports(display: grid);
@reference 'tailwindcss/theme';
@reference './reference/reference-variant.css';
@source "./app/**/*.{js,ts,jsx,tsx}";
@source not "./app/**/__tests__/*";
@source inline("tw:prose tw:underline");
@source not inline("tw:legacy");
@theme reference prefix(tw);
@theme default {
--color-brand: #1d4ed8;
--font-sans: 'Inter', system-ui, sans-serif;
--spacing: 0.25rem;
@keyframes reference-pulse {
from {
opacity: 1;
}
to {
opacity: 0.35;
}
}
}
@theme {
--color-accent: oklch(70% 0.12 220);
--radius-card: 1rem;
--value-sm: 14px;
--modifier-7: 28px;
}
@theme inline {
--shadow-card: 0 25px 50px -12px color-mix(in oklab, var(--color-brand) 25%, transparent);
}
@theme inline prefix(tw) {
--size-icon: 1.25rem;
}
@theme reference inline {
--card-outline: 1px solid color-mix(in oklab, var(--color-brand) 60%, transparent);
}
@theme static {
--ring-brand: var(--color-brand);
@keyframes reference-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
@media theme(reference) prefix(tw) important {
@theme {
--color-card-foreground: var(--color-accent);
}
}
@media source("./components/**/*.{tsx,jsx}") theme(inline) {
@theme {
--spacing-component: calc(var(--spacing) * 6);
}
}
@tailwind base;
@tailwind components;
@tailwind utilities source("./app/**/*.{html,js,ts,jsx,tsx}");
@layer base {
*, *::before, *::after {
box-sizing: border-box;
}
html {
font-family: theme('fontFamily.sans', var(--font-sans));
}
body {
background-color: --alpha(var(--color-brand) / 7.5%);
color: var(--color-card-foreground, var(--color-accent));
}
}
@layer theme {
@theme {
--layer-spacing-8: calc(var(--spacing) * 8);
}
}
@layer components {
.card {
@apply tw:bg-white tw:text-slate-900 tw:shadow-lg tw:rounded;
padding: --spacing(6);
border-radius: --theme(--radius-card, 0.75rem);
outline: --theme(--card-outline inline, 0 solid transparent);
box-shadow: var(--shadow-card, --theme(--shadow-card, 0 10px 30px rgba(15, 23, 42, 0.15)));
}
.card {
@variant reference-dark {
outline-color: --theme(--ring-brand inline, var(--color-brand));
}
@variant motion-safe {
transition: transform 200ms ease;
}
@variant hover {
transform: translateY(-2px);
}
}
.chip {
display: inline-flex;
align-items: center;
gap: --spacing(2);
font-size: theme('fontSize.sm', 0.875rem);
}
.chip {
@variant hocus {
@apply tw:ring-2 tw:ring-offset-2;
}
}
}
@layer utilities {
@variant hover {
.hover\\:reference-underline {
text-decoration-line: underline;
}
}
.content-auto {
content-visibility: auto;
}
}
@utility card {
--tw-card-shadow: var(--shadow-card);
box-shadow: var(--tw-card-shadow);
}
@utility stack-* {
--stack-gap: calc(var(--spacing) * --value(number));
margin-top: var(--stack-gap);
margin-bottom: var(--stack-gap);
}
@utility aspect-* {
aspect-ratio: --value(ratio);
}
@utility example-* {
--value: --value(--value, [length]);
--modifier: --modifier(--modifier, [length]);
--modifier-with-calc: calc(--modifier(--modifier, [length]) * 2);
--modifier-literals: --modifier('literal', 'literal-2');
}
@utility mask-r-* {
--mask-right: linear-gradient(
to left,
transparent calc(var(--spacing) * --modifier(integer)),
black calc(var(--spacing) * --value(integer))
);
mask-image: var(--mask-right);
}
@custom-variant hocus (&:hover, &:focus);
@custom-variant theme-wrap {
@variant reference-dark {
@slot;
}
}
@variant reference-dark {
&:where([data-theme='reference-dark'] *) {
@slot;
}
}
@variant motion-safe {
@media (prefers-reduced-motion: no-preference) {
@slot;
}
}
@variant hover {
&:hover {
@slot;
}
}
@variant hover {
@variant reference-dark {
.reference-hover-dark {
color: var(--color-brand);
}
}
}
@media (prefers-color-scheme: dark) {
@variant reference-dark {
@page {
margin: 1in;
}
}
}
.badge {
border-radius: var(--radius-card);
padding-inline: --spacing(3);
padding-block: --spacing(1.5);
}
.badge {
@variant reference-dark {
border-color: var(--color-brand);
}
}
.icon {
width: var(--size-icon);
height: var(--size-icon);
}
.icon {
@variant theme-wrap {
color: var(--color-card-foreground, var(--color-brand));
}
}

View file

@ -0,0 +1,12 @@
/* Imported stylesheet used by reference.css via @import */
@theme inline {
--imported-gap: 1.5rem;
}
@layer utilities {
.from-import {
display: grid;
gap: var(--imported-gap, 1rem);
}
}

View file

@ -0,0 +1,19 @@
/* Referenced stylesheet providing additional variants and theme tokens */
@theme reference {
--reference-accent: #f97316;
}
@variant data-open {
&[data-open='true'] {
@slot;
}
}
@variant data-open {
@variant hover {
.data-open-hover {
color: var(--reference-accent, currentColor);
}
}
}

View file

@ -217,6 +217,160 @@ describe('compiling CSS', () => {
`) `)
}) })
test('reference.css compiles end-to-end', async () => {
let referencePath = path.resolve(__dirname, '..', 'reference.css')
let referenceCss = fs.readFileSync(referencePath, 'utf-8')
let referenceDir = path.dirname(referencePath)
let builtinStyles = new Map<string, string>([
['tailwindcss', path.resolve(referenceDir, 'index.css')],
['tailwindcss/index.css', path.resolve(referenceDir, 'index.css')],
['tailwindcss/preflight', path.resolve(referenceDir, 'preflight.css')],
['tailwindcss/preflight.css', path.resolve(referenceDir, 'preflight.css')],
['tailwindcss/theme', path.resolve(referenceDir, 'theme.css')],
['tailwindcss/theme.css', path.resolve(referenceDir, 'theme.css')],
['tailwindcss/utilities', path.resolve(referenceDir, 'utilities.css')],
['tailwindcss/utilities.css', path.resolve(referenceDir, 'utilities.css')],
])
let loadStylesheet = async (id: string, base: string) => {
let normalizedBase = base === '' ? referenceDir : base
let normalizedId = id.replace(/\\/g, '/')
if (builtinStyles.has(normalizedId)) {
let filePath = builtinStyles.get(normalizedId)!
return {
path: filePath,
base: path.dirname(filePath),
content: fs.readFileSync(filePath, 'utf-8'),
}
}
let resolved = path.resolve(normalizedBase, id)
return {
path: resolved,
base: path.dirname(resolved),
content: fs.readFileSync(resolved, 'utf-8'),
}
}
let referencePlugin = plugin.withOptions(
(options: Record<string, unknown> = {}) => {
return ({ addUtilities, addVariant, theme }: PluginAPI) => {
let enableGrid =
typeof options['enable-grid'] === 'boolean' ? options['enable-grid'] : true
let cardColor =
typeof options['card-color'] === 'string' ? options['card-color'] : 'white'
let shadowDepthValue = options['shadow-depth']
let shadowDepth =
typeof shadowDepthValue === 'number'
? shadowDepthValue
: Number(shadowDepthValue) || 16
let accentScale = Array.isArray(options['accent-scale']) ? options['accent-scale'] : []
let accentUtilities: Record<string, Record<string, string>> = {}
for (let accent of accentScale) {
if (typeof accent === 'string') {
accentUtilities[`.plugin-accent-${accent}`] = {
color: `var(--color-${accent}, currentColor)`,
}
}
}
addUtilities({
'.plugin-card': {
display: enableGrid ? 'grid' : 'block',
gap: theme('spacing.6', '1.5rem'),
'background-color': cardColor,
'box-shadow': `0 ${shadowDepth}px 32px -12px rgba(15, 23, 42, 0.18)`,
},
...accentUtilities,
})
addVariant('supports-grid', {
'@supports (display: grid)': '@slot',
})
}
},
() => ({
theme: {
extend: {
colors: {
brand: '#2563eb',
'card-foreground': '#1f2937',
},
},
},
}),
)
let referenceConfig = {
darkMode: ['class', '[data-theme="reference-dark"]'],
safelist: ['tw:underline', 'plugin-card'],
theme: {
extend: {
colors: {
brand: '#1d4ed8',
'card-foreground': '#111827',
},
spacing: {
6: '1.5rem',
},
},
},
}
let moduleRegistry = new Map<string, any>([
[path.resolve(referenceDir, 'reference/reference.plugin.js'), referencePlugin],
[path.resolve(referenceDir, 'reference/reference.config.js'), referenceConfig],
])
let loadModule = async (id: string, base: string) => {
let normalizedBase = base === '' ? referenceDir : base
let resolved = path.resolve(normalizedBase, id)
if (!moduleRegistry.has(resolved)) {
throw new Error(`Unknown module requested by reference.css test: ${id}`)
}
return {
path: resolved,
base: path.dirname(resolved),
module: moduleRegistry.get(resolved),
}
}
let candidates = [
'tw:underline',
'stack-4',
'aspect-16/9',
'example-sm/7',
'example-[12px]/[16px]',
'mask-r-4/6',
'theme-wrap:tw:bg-brand',
'supports-grid:tw:flex',
'motion-safe:tw:animate-pulse',
'reference-dark:tw:text-brand',
'data-open:tw:px-4',
'hocus:tw:ring-2',
'plugin-card',
]
let output = await compileCss(referenceCss, candidates, {
from: referencePath,
loadStylesheet,
loadModule,
})
expect(output).toContain('.stack-4')
expect(output).toContain('.example-sm\\/7')
expect(output).toContain('.mask-r-4\\/6')
expect(output).toContain('.supports-grid\\:tw\\:flex')
expect(output).toContain('.theme-wrap\\:tw\\:bg-brand')
expect(output).toContain('.plugin-card')
expect(output).toContain('.data-open\\:tw\\:px-4')
expect(output).toContain('.from-import')
expect(output).toMatch(/@media \\(prefers-reduced-motion: no-preference\\)/)
})
test('adds vendor prefixes', async () => { test('adds vendor prefixes', async () => {
expect( expect(
await compileCss( await compileCss(