mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Internal refactor, introduce AtRule (#14802)
This PR introduces an internal refactor where we introduce the `AtRule`
CSS Node in our AST.
The motivation for this is that in a lot of places we need to
differentiate between a `Rule` and an `AtRule`. We often do this with
code that looks like this:
```ts
rule.selector[0] === '@' && rule.selector.startsWith('@media')
```
Another issue we have is that we often need to check for `'@media '`
including the space, because we don't want to match `@mediafoobar` if
somebody has this in their CSS. Alternatively, if you CSS is minified
then it could be that you have a rule that looks like
`@media(width>=100px)`, in this case we _also_ have to check for
`@media(`.
Here is a snippet of code that we have in our codebase:
```ts
// Find at-rules rules
if (node.kind === 'rule') {
if (
node.selector[0] === '@' &&
(node.selector.startsWith('@media ') ||
node.selector.startsWith('@media(') ||
node.selector.startsWith('@custom-media ') ||
node.selector.startsWith('@custom-media(') ||
node.selector.startsWith('@container ') ||
node.selector.startsWith('@container(') ||
node.selector.startsWith('@supports ') ||
node.selector.startsWith('@supports(')) &&
node.selector.includes(THEME_FUNCTION_INVOCATION)
) {
node.selector = substituteFunctionsInValue(node.selector, resolveThemeValue)
}
}
```
Which will now be replaced with a much simpler version:
```ts
// Find at-rules rules
if (node.kind === 'at-rule') {
if (
(node.name === '@media' ||
node.name === '@custom-media' ||
node.name === '@container' ||
node.name === '@supports') &&
node.params.includes(THEME_FUNCTION_INVOCATION)
) {
node.params = substituteFunctionsInValue(node.params, resolveThemeValue)
}
}
```
Checking for all the cases from the first snippet is not the end of the
world, but it is error prone. It's easy to miss a case.
A direct comparison is also faster than comparing via the
`startsWith(…)` function.
---
Note: this is only a refactor without changing other code _unless_ it
was required to make the tests pass. The tests themselves are all
passing and none of them changed (because the behavior should be the
same).
The one exception is the tests where we check the parsed AST, which now
includes `at-rule` nodes instead of `rule` nodes when we have an
at-rule.
This commit is contained in:
parent
4e5e0a3e1b
commit
c439cdf43c
@ -57,7 +57,7 @@ function isAtRuleVariant(designSystem: DesignSystem, variant: Variant) {
|
||||
return true
|
||||
}
|
||||
let stack = getAppliedNodeStack(designSystem, variant)
|
||||
return stack.every((node) => node.kind === 'rule' && node.selector[0] === '@')
|
||||
return stack.every((node) => node.kind === 'at-rule')
|
||||
}
|
||||
|
||||
function isCombinatorVariant(designSystem: DesignSystem, variant: Variant) {
|
||||
@ -65,8 +65,6 @@ function isCombinatorVariant(designSystem: DesignSystem, variant: Variant) {
|
||||
return stack.some(
|
||||
(node) =>
|
||||
node.kind === 'rule' &&
|
||||
// Ignore at-rules as they are hoisted
|
||||
node.selector[0] !== '@' &&
|
||||
// Combinators include any of the following characters
|
||||
(node.selector.includes(' ') ||
|
||||
node.selector.includes('>') ||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports":true
|
||||
}
|
||||
"allowSyntheticDefaultImports": true,
|
||||
},
|
||||
}
|
||||
|
||||
@ -5,28 +5,21 @@ import { escape } from './utils/escape'
|
||||
|
||||
export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
if (node.kind !== 'rule') return
|
||||
if (node.kind !== 'at-rule') return
|
||||
|
||||
// Do not allow `@apply` rules inside `@keyframes` rules.
|
||||
if (node.selector[0] === '@' && node.selector.startsWith('@keyframes')) {
|
||||
if (node.name === '@keyframes') {
|
||||
walk(node.nodes, (child) => {
|
||||
if (
|
||||
child.kind === 'rule' &&
|
||||
child.selector[0] === '@' &&
|
||||
child.selector.startsWith('@apply ')
|
||||
) {
|
||||
if (child.kind === 'at-rule' && child.name === '@apply') {
|
||||
throw new Error(`You cannot use \`@apply\` inside \`@keyframes\`.`)
|
||||
}
|
||||
})
|
||||
return WalkAction.Skip
|
||||
}
|
||||
|
||||
if (!(node.selector[0] === '@' && node.selector.startsWith('@apply '))) return
|
||||
if (node.name !== '@apply') return
|
||||
|
||||
let candidates = node.selector
|
||||
.slice(7 /* Ignore `@apply ` when parsing the selector */)
|
||||
.trim()
|
||||
.split(/\s+/g)
|
||||
let candidates = node.params.split(/\s+/g)
|
||||
|
||||
// Replace the `@apply` rule with the actual utility classes
|
||||
{
|
||||
@ -43,7 +36,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
|
||||
// don't want the wrapping selector.
|
||||
let newNodes: AstNode[] = []
|
||||
for (let candidateNode of candidateAst) {
|
||||
if (candidateNode.kind === 'rule' && candidateNode.selector[0] !== '@') {
|
||||
if (candidateNode.kind === 'rule') {
|
||||
for (let child of candidateNode.nodes) {
|
||||
newNodes.push(child)
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { context, decl, rule, toCss, walk } from './ast'
|
||||
import { context, decl, styleRule, toCss, walk } from './ast'
|
||||
import * as CSS from './css-parser'
|
||||
|
||||
it('should pretty print an AST', () => {
|
||||
@ -16,13 +16,13 @@ it('should pretty print an AST', () => {
|
||||
|
||||
it('allows the placement of context nodes', () => {
|
||||
const ast = [
|
||||
rule('.foo', [decl('color', 'red')]),
|
||||
styleRule('.foo', [decl('color', 'red')]),
|
||||
context({ context: 'a' }, [
|
||||
rule('.bar', [
|
||||
styleRule('.bar', [
|
||||
decl('color', 'blue'),
|
||||
context({ context: 'b' }, [
|
||||
//
|
||||
rule('.baz', [decl('color', 'green')]),
|
||||
styleRule('.baz', [decl('color', 'green')]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
@ -1,9 +1,20 @@
|
||||
export type Rule = {
|
||||
import { parseAtRule } from './css-parser'
|
||||
|
||||
const AT_SIGN = 0x40
|
||||
|
||||
export type StyleRule = {
|
||||
kind: 'rule'
|
||||
selector: string
|
||||
nodes: AstNode[]
|
||||
}
|
||||
|
||||
export type AtRule = {
|
||||
kind: 'at-rule'
|
||||
name: string
|
||||
params: string
|
||||
nodes: AstNode[]
|
||||
}
|
||||
|
||||
export type Declaration = {
|
||||
kind: 'declaration'
|
||||
property: string
|
||||
@ -27,9 +38,10 @@ export type AtRoot = {
|
||||
nodes: AstNode[]
|
||||
}
|
||||
|
||||
export type AstNode = Rule | Declaration | Comment | Context | AtRoot
|
||||
export type Rule = StyleRule | AtRule
|
||||
export type AstNode = StyleRule | AtRule | Declaration | Comment | Context | AtRoot
|
||||
|
||||
export function rule(selector: string, nodes: AstNode[]): Rule {
|
||||
export function styleRule(selector: string, nodes: AstNode[] = []): StyleRule {
|
||||
return {
|
||||
kind: 'rule',
|
||||
selector,
|
||||
@ -37,6 +49,23 @@ export function rule(selector: string, nodes: AstNode[]): Rule {
|
||||
}
|
||||
}
|
||||
|
||||
export function atRule(name: string, params: string = '', nodes: AstNode[] = []): AtRule {
|
||||
return {
|
||||
kind: 'at-rule',
|
||||
name,
|
||||
params,
|
||||
nodes,
|
||||
}
|
||||
}
|
||||
|
||||
export function rule(selector: string, nodes: AstNode[] = []): StyleRule | AtRule {
|
||||
if (selector.charCodeAt(0) === AT_SIGN) {
|
||||
return parseAtRule(selector, nodes)
|
||||
}
|
||||
|
||||
return styleRule(selector, nodes)
|
||||
}
|
||||
|
||||
export function decl(property: string, value: string | undefined): Declaration {
|
||||
return {
|
||||
kind: 'declaration',
|
||||
@ -126,7 +155,7 @@ export function walk(
|
||||
// Skip visiting the children of this node
|
||||
if (status === WalkAction.Skip) continue
|
||||
|
||||
if (node.kind === 'rule') {
|
||||
if (node.kind === 'rule' || node.kind === 'at-rule') {
|
||||
walk(node.nodes, visit, path, context)
|
||||
}
|
||||
}
|
||||
@ -152,7 +181,7 @@ export function walkDepth(
|
||||
let path = [...parentPath, node]
|
||||
let parent = parentPath.at(-1) ?? null
|
||||
|
||||
if (node.kind === 'rule') {
|
||||
if (node.kind === 'rule' || node.kind === 'at-rule') {
|
||||
walkDepth(node.nodes, visit, path, context)
|
||||
} else if (node.kind === 'context') {
|
||||
walkDepth(node.nodes, visit, parentPath, { ...context, ...node.context })
|
||||
@ -185,7 +214,16 @@ export function toCss(ast: AstNode[]) {
|
||||
|
||||
// Rule
|
||||
if (node.kind === 'rule') {
|
||||
if (node.selector === '@tailwind utilities') {
|
||||
css += `${indent}${node.selector} {\n`
|
||||
for (let child of node.nodes) {
|
||||
css += stringify(child, depth + 1)
|
||||
}
|
||||
css += `${indent}}\n`
|
||||
}
|
||||
|
||||
// AtRule
|
||||
else if (node.kind === 'at-rule') {
|
||||
if (node.name === '@tailwind' && node.params === 'utilities') {
|
||||
for (let child of node.nodes) {
|
||||
css += stringify(child, depth)
|
||||
}
|
||||
@ -199,20 +237,21 @@ export function toCss(ast: AstNode[]) {
|
||||
// ```css
|
||||
// @layer base, components, utilities;
|
||||
// ```
|
||||
if (node.selector[0] === '@' && node.nodes.length === 0) {
|
||||
return `${indent}${node.selector};\n`
|
||||
else if (node.nodes.length === 0) {
|
||||
return `${indent}${node.name} ${node.params};\n`
|
||||
}
|
||||
|
||||
if (node.selector[0] === '@' && node.selector.startsWith('@property ') && depth === 0) {
|
||||
//
|
||||
else if (node.name === '@property' && depth === 0) {
|
||||
// Don't output duplicate `@property` rules
|
||||
if (seenAtProperties.has(node.selector)) {
|
||||
if (seenAtProperties.has(node.params)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Collect fallbacks for `@property` rules for Firefox support
|
||||
// We turn these into rules on `:root` or `*` and some pseudo-elements
|
||||
// based on the value of `inherits``
|
||||
let property = node.selector.replace(/@property\s*/g, '')
|
||||
let property = node.params
|
||||
let initialValue = null
|
||||
let inherits = false
|
||||
|
||||
@ -231,10 +270,10 @@ export function toCss(ast: AstNode[]) {
|
||||
propertyFallbacksUniversal.push(decl(property, initialValue ?? 'initial'))
|
||||
}
|
||||
|
||||
seenAtProperties.add(node.selector)
|
||||
seenAtProperties.add(node.params)
|
||||
}
|
||||
|
||||
css += `${indent}${node.selector} {\n`
|
||||
css += `${indent}${node.name}${node.params ? ` ${node.params} ` : ' '}{\n`
|
||||
for (let child of node.nodes) {
|
||||
css += stringify(child, depth + 1)
|
||||
}
|
||||
@ -292,7 +331,7 @@ export function toCss(ast: AstNode[]) {
|
||||
|
||||
if (fallbackAst.length) {
|
||||
fallback = stringify(
|
||||
rule('@supports (-moz-orient: inline)', [rule('@layer base', fallbackAst)]),
|
||||
atRule('@supports', '(-moz-orient: inline)', [atRule('@layer', 'base', fallbackAst)]),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { context, rule, walk, WalkAction, type AstNode } from './ast'
|
||||
import { atRule, context, walk, WalkAction, type AstNode } from './ast'
|
||||
import * as CSS from './css-parser'
|
||||
import * as ValueParser from './value-parser'
|
||||
|
||||
@ -13,15 +13,9 @@ export async function substituteAtImports(
|
||||
let promises: Promise<void>[] = []
|
||||
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
if (
|
||||
node.kind === 'rule' &&
|
||||
node.selector[0] === '@' &&
|
||||
node.selector.toLowerCase().startsWith('@import ')
|
||||
) {
|
||||
if (node.kind === 'at-rule' && node.name === '@import') {
|
||||
try {
|
||||
let { uri, layer, media, supports } = parseImportParams(
|
||||
ValueParser.parse(node.selector.slice(8)),
|
||||
)
|
||||
let { uri, layer, media, supports } = parseImportParams(ValueParser.parse(node.params))
|
||||
|
||||
// Skip importing data or remote URIs
|
||||
if (uri.startsWith('data:')) return
|
||||
@ -132,15 +126,15 @@ function buildImportNodes(
|
||||
let root = importedAst
|
||||
|
||||
if (layer !== null) {
|
||||
root = [rule('@layer ' + layer, root)]
|
||||
root = [atRule('@layer', layer, root)]
|
||||
}
|
||||
|
||||
if (media !== null) {
|
||||
root = [rule('@media ' + media, root)]
|
||||
root = [atRule('@media', media, root)]
|
||||
}
|
||||
|
||||
if (supports !== null) {
|
||||
root = [rule(`@supports ${supports[0] === '(' ? supports : `(${supports})`}`, root)]
|
||||
root = [atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root)]
|
||||
}
|
||||
|
||||
return root
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { rule, toCss, walk, WalkAction, type AstNode } from '../ast'
|
||||
import { styleRule, toCss, walk, WalkAction, type AstNode } from '../ast'
|
||||
import type { DesignSystem } from '../design-system'
|
||||
import { segment } from '../utils/segment'
|
||||
import { applyConfigToTheme } from './apply-config-to-theme'
|
||||
@ -34,15 +34,15 @@ export async function applyCompatibilityHooks({
|
||||
let configPaths: { id: string; base: string }[] = []
|
||||
|
||||
walk(ast, (node, { parent, replaceWith, context }) => {
|
||||
if (node.kind !== 'rule' || node.selector[0] !== '@') return
|
||||
if (node.kind !== 'at-rule') return
|
||||
|
||||
// Collect paths from `@plugin` at-rules
|
||||
if (node.selector === '@plugin' || node.selector.startsWith('@plugin ')) {
|
||||
if (node.name === '@plugin') {
|
||||
if (parent !== null) {
|
||||
throw new Error('`@plugin` cannot be nested.')
|
||||
}
|
||||
|
||||
let pluginPath = node.selector.slice(9, -1)
|
||||
let pluginPath = node.params.slice(1, -1)
|
||||
if (pluginPath.length === 0) {
|
||||
throw new Error('`@plugin` must have a path.')
|
||||
}
|
||||
@ -100,7 +100,7 @@ export async function applyCompatibilityHooks({
|
||||
}
|
||||
|
||||
// Collect paths from `@config` at-rules
|
||||
if (node.selector === '@config' || node.selector.startsWith('@config ')) {
|
||||
if (node.name === '@config') {
|
||||
if (node.nodes.length > 0) {
|
||||
throw new Error('`@config` cannot have a body.')
|
||||
}
|
||||
@ -109,7 +109,7 @@ export async function applyCompatibilityHooks({
|
||||
throw new Error('`@config` cannot be nested.')
|
||||
}
|
||||
|
||||
configPaths.push({ id: node.selector.slice(9, -1), base: context.base })
|
||||
configPaths.push({ id: node.params.slice(1, -1), base: context.base })
|
||||
replaceWith([])
|
||||
return
|
||||
}
|
||||
@ -268,15 +268,15 @@ function upgradeToFullPluginSupport({
|
||||
let wrappingSelector = resolvedConfig.important
|
||||
|
||||
walk(ast, (node, { replaceWith, parent }) => {
|
||||
if (node.kind !== 'rule') return
|
||||
if (node.selector !== '@tailwind utilities') return
|
||||
if (node.kind !== 'at-rule') return
|
||||
if (node.name !== '@tailwind' || node.params !== 'utilities') return
|
||||
|
||||
// The AST node was already manually wrapped so there's nothing to do
|
||||
if (parent?.kind === 'rule' && parent.selector === wrappingSelector) {
|
||||
return WalkAction.Stop
|
||||
}
|
||||
|
||||
replaceWith(rule(wrappingSelector, [node]))
|
||||
replaceWith(styleRule(wrappingSelector, [node]))
|
||||
|
||||
return WalkAction.Stop
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { expect, test } from 'vitest'
|
||||
import { decl, rule, toCss } from '../ast'
|
||||
import { atRule, decl, styleRule, toCss } from '../ast'
|
||||
import { buildDesignSystem } from '../design-system'
|
||||
import { Theme } from '../theme'
|
||||
import { applyKeyframesToTheme } from './apply-keyframes-to-theme'
|
||||
@ -58,15 +58,15 @@ test('will append to the default keyframes with new keyframes', () => {
|
||||
let design = buildDesignSystem(theme)
|
||||
|
||||
theme.addKeyframes(
|
||||
rule('@keyframes slide-in', [
|
||||
rule('from', [decl('opacity', 'translateX(0%)')]),
|
||||
rule('to', [decl('opacity', 'translateX(100%)')]),
|
||||
atRule('@keyframes', 'slide-in', [
|
||||
styleRule('from', [decl('opacity', 'translateX(0%)')]),
|
||||
styleRule('to', [decl('opacity', 'translateX(100%)')]),
|
||||
]),
|
||||
)
|
||||
theme.addKeyframes(
|
||||
rule('@keyframes slide-out', [
|
||||
rule('from', [decl('opacity', 'translateX(100%)')]),
|
||||
rule('to', [decl('opacity', 'translateX(0%)')]),
|
||||
atRule('@keyframes', 'slide-out', [
|
||||
styleRule('from', [decl('opacity', 'translateX(100%)')]),
|
||||
styleRule('to', [decl('opacity', 'translateX(0%)')]),
|
||||
]),
|
||||
)
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { rule, type Rule } from '../ast'
|
||||
import { atRule, type AtRule } from '../ast'
|
||||
import type { DesignSystem } from '../design-system'
|
||||
import type { ResolvedConfig } from './config/types'
|
||||
import { objectToAst } from './plugin-api'
|
||||
@ -13,11 +13,11 @@ export function applyKeyframesToTheme(
|
||||
}
|
||||
}
|
||||
|
||||
export function keyframesToRules(resolvedConfig: Pick<ResolvedConfig, 'theme'>): Rule[] {
|
||||
let rules: Rule[] = []
|
||||
export function keyframesToRules(resolvedConfig: Pick<ResolvedConfig, 'theme'>): AtRule[] {
|
||||
let rules: AtRule[] = []
|
||||
if ('keyframes' in resolvedConfig.theme) {
|
||||
for (let [name, keyframe] of Object.entries(resolvedConfig.theme.keyframes)) {
|
||||
rules.push(rule(`@keyframes ${name}`, objectToAst(keyframe as any)))
|
||||
rules.push(atRule('@keyframes', name, objectToAst(keyframe as any)))
|
||||
}
|
||||
}
|
||||
return rules
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { substituteAtApply } from '../apply'
|
||||
import { decl, rule, type AstNode } from '../ast'
|
||||
import { atRule, decl, rule, type AstNode } from '../ast'
|
||||
import type { Candidate, CandidateModifier, NamedUtilityValue } from '../candidate'
|
||||
import { substituteFunctions } from '../css-functions'
|
||||
import * as CSS from '../css-parser'
|
||||
@ -86,7 +86,7 @@ export function buildPluginApi(
|
||||
addBase(css) {
|
||||
let baseNodes = objectToAst(css)
|
||||
substituteFunctions(baseNodes, api.theme)
|
||||
ast.push(rule('@layer base', baseNodes))
|
||||
ast.push(atRule('@layer', 'base', baseNodes))
|
||||
},
|
||||
|
||||
addVariant(name, variant) {
|
||||
@ -434,7 +434,7 @@ export function objectToAst(rules: CssInJs | CssInJs[]): AstNode[] {
|
||||
for (let [name, value] of entries) {
|
||||
if (typeof value !== 'object') {
|
||||
if (!name.startsWith('--') && value === '@slot') {
|
||||
ast.push(rule(name, [rule('@slot', [])]))
|
||||
ast.push(rule(name, [atRule('@slot')]))
|
||||
} else {
|
||||
// Convert camelCase to kebab-case:
|
||||
// https://github.com/postcss/postcss-js/blob/b3db658b932b42f6ac14ca0b1d50f50c4569805b/parser.js#L30-L35
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { rule } from '../ast'
|
||||
import { atRule } from '../ast'
|
||||
import type { DesignSystem } from '../design-system'
|
||||
import type { ResolvedConfig } from './config/types'
|
||||
|
||||
@ -45,7 +45,7 @@ export function registerScreensConfig(userConfig: ResolvedConfig, designSystem:
|
||||
designSystem.variants.static(
|
||||
name,
|
||||
(ruleNode) => {
|
||||
ruleNode.nodes = [rule(`@media ${query}`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [atRule('@media', query, ruleNode.nodes)]
|
||||
},
|
||||
{ order },
|
||||
)
|
||||
|
||||
@ -1,4 +1,13 @@
|
||||
import { decl, rule, walk, WalkAction, type AstNode, type Rule } from './ast'
|
||||
import {
|
||||
atRule,
|
||||
decl,
|
||||
rule,
|
||||
walk,
|
||||
WalkAction,
|
||||
type AstNode,
|
||||
type Rule,
|
||||
type StyleRule,
|
||||
} from './ast'
|
||||
import { type Candidate, type Variant } from './candidate'
|
||||
import { substituteFunctions } from './css-functions'
|
||||
import { type DesignSystem } from './design-system'
|
||||
@ -144,7 +153,7 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem
|
||||
applyImportant(nodes)
|
||||
}
|
||||
|
||||
let node: Rule = {
|
||||
let node: StyleRule = {
|
||||
kind: 'rule',
|
||||
selector,
|
||||
nodes,
|
||||
@ -205,7 +214,7 @@ export function applyVariant(
|
||||
// To solve this, we provide an isolated placeholder node to the variant.
|
||||
// The variant can now apply its logic to the isolated node without
|
||||
// affecting the original node.
|
||||
let isolatedNode = rule('@slot', [])
|
||||
let isolatedNode = atRule('@slot')
|
||||
|
||||
let result = applyVariant(isolatedNode, variant.variant, variants, depth + 1)
|
||||
if (result === null) return null
|
||||
@ -218,16 +227,16 @@ export function applyVariant(
|
||||
// This means `child` may be a declaration and we don't want to apply the
|
||||
// variant to it. This also means the entire variant as a whole is not
|
||||
// applicable to the rule and should generate nothing.
|
||||
if (child.kind !== 'rule') return null
|
||||
if (child.kind !== 'rule' && child.kind !== 'at-rule') return null
|
||||
|
||||
let result = applyFn(child as Rule, variant)
|
||||
let result = applyFn(child, variant)
|
||||
if (result === null) return null
|
||||
}
|
||||
|
||||
// Replace the placeholder node with the actual node
|
||||
{
|
||||
walk(isolatedNode.nodes, (child) => {
|
||||
if (child.kind === 'rule' && child.nodes.length <= 0) {
|
||||
if ((child.kind === 'rule' || child.kind === 'at-rule') && child.nodes.length <= 0) {
|
||||
child.nodes = node.nodes
|
||||
return WalkAction.Skip
|
||||
}
|
||||
@ -301,7 +310,7 @@ function applyImportant(ast: AstNode[]): void {
|
||||
|
||||
if (node.kind === 'declaration') {
|
||||
node.important = true
|
||||
} else if (node.kind === 'rule') {
|
||||
} else if (node.kind === 'rule' || node.kind === 'at-rule') {
|
||||
applyImportant(node.nodes)
|
||||
}
|
||||
}
|
||||
@ -327,7 +336,7 @@ function getPropertySort(nodes: AstNode[]) {
|
||||
|
||||
let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.property)
|
||||
if (idx !== -1) propertySort.add(idx)
|
||||
} else if (node.kind === 'rule') {
|
||||
} else if (node.kind === 'rule' || node.kind === 'at-rule') {
|
||||
for (let child of node.nodes) {
|
||||
q.push(child)
|
||||
}
|
||||
|
||||
@ -15,20 +15,15 @@ export function substituteFunctions(ast: AstNode[], resolveThemeValue: ResolveTh
|
||||
}
|
||||
|
||||
// Find at-rules rules
|
||||
if (node.kind === 'rule') {
|
||||
if (node.kind === 'at-rule') {
|
||||
if (
|
||||
node.selector[0] === '@' &&
|
||||
(node.selector.startsWith('@media ') ||
|
||||
node.selector.startsWith('@media(') ||
|
||||
node.selector.startsWith('@custom-media ') ||
|
||||
node.selector.startsWith('@custom-media(') ||
|
||||
node.selector.startsWith('@container ') ||
|
||||
node.selector.startsWith('@container(') ||
|
||||
node.selector.startsWith('@supports ') ||
|
||||
node.selector.startsWith('@supports(')) &&
|
||||
node.selector.includes(THEME_FUNCTION_INVOCATION)
|
||||
(node.name === '@media' ||
|
||||
node.name === '@custom-media' ||
|
||||
node.name === '@container' ||
|
||||
node.name === '@supports') &&
|
||||
node.params.includes(THEME_FUNCTION_INVOCATION)
|
||||
) {
|
||||
node.selector = substituteFunctionsInValue(node.selector, resolveThemeValue)
|
||||
node.params = substituteFunctionsInValue(node.params, resolveThemeValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -630,7 +630,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
||||
parse(css`
|
||||
@charset "UTF-8";
|
||||
`),
|
||||
).toEqual([{ kind: 'rule', selector: '@charset "UTF-8"', nodes: [] }])
|
||||
).toEqual([{ kind: 'at-rule', name: '@charset', params: '"UTF-8"', nodes: [] }])
|
||||
})
|
||||
|
||||
it('should parse an at-rule without a block or semicolon', () => {
|
||||
@ -638,7 +638,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
||||
parse(`
|
||||
@tailwind utilities
|
||||
`),
|
||||
).toEqual([{ kind: 'rule', selector: '@tailwind utilities', nodes: [] }])
|
||||
).toEqual([{ kind: 'at-rule', name: '@tailwind', params: 'utilities', nodes: [] }])
|
||||
})
|
||||
|
||||
it("should parse an at-rule without a block or semicolon when it's the last rule in a block", () => {
|
||||
@ -650,9 +650,10 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@layer utilities',
|
||||
nodes: [{ kind: 'rule', selector: '@tailwind utilities', nodes: [] }],
|
||||
kind: 'at-rule',
|
||||
name: '@layer',
|
||||
params: 'utilities',
|
||||
nodes: [{ kind: 'at-rule', name: '@tailwind', params: 'utilities', nodes: [] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
@ -670,14 +671,17 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@layer utilities',
|
||||
nodes: [{ kind: 'rule', selector: '@charset "UTF-8"', nodes: [] }],
|
||||
kind: 'at-rule',
|
||||
name: '@layer',
|
||||
params: 'utilities',
|
||||
nodes: [{ kind: 'at-rule', name: '@charset', params: '"UTF-8"', nodes: [] }],
|
||||
},
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [{ kind: 'rule', selector: '@apply font-bold hover:text-red-500', nodes: [] }],
|
||||
nodes: [
|
||||
{ kind: 'at-rule', name: '@apply', params: 'font-bold hover:text-red-500', nodes: [] },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
@ -689,8 +693,8 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
||||
@tailwind base;
|
||||
`),
|
||||
).toEqual([
|
||||
{ kind: 'rule', selector: '@tailwind', nodes: [] },
|
||||
{ kind: 'rule', selector: '@tailwind base', nodes: [] },
|
||||
{ kind: 'at-rule', name: '@tailwind', params: '', nodes: [] },
|
||||
{ kind: 'at-rule', name: '@tailwind', params: 'base', nodes: [] },
|
||||
])
|
||||
})
|
||||
|
||||
@ -711,8 +715,9 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@media (width >= 600px)',
|
||||
kind: 'at-rule',
|
||||
name: '@media',
|
||||
params: '(width >= 600px)',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'rule',
|
||||
@ -720,15 +725,17 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'red', important: false },
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@media (width >= 800px)',
|
||||
kind: 'at-rule',
|
||||
name: '@media',
|
||||
params: '(width >= 800px)',
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'blue', important: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@media (width >= 1000px)',
|
||||
kind: 'at-rule',
|
||||
name: '@media',
|
||||
params: '(width >= 1000px)',
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'green', important: false },
|
||||
],
|
||||
@ -756,10 +763,11 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
||||
kind: 'rule',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'rule',
|
||||
kind: 'at-rule',
|
||||
name: '@apply',
|
||||
params:
|
||||
'hover:text-red-100 sm:hover:text-red-200 md:hover:text-red-300 lg:hover:text-red-400 xl:hover:text-red-500',
|
||||
nodes: [],
|
||||
selector:
|
||||
'@apply hover:text-red-100 sm:hover:text-red-200 md:hover:text-red-300 lg:hover:text-red-400 xl:hover:text-red-500',
|
||||
},
|
||||
],
|
||||
selector: '.foo',
|
||||
@ -923,8 +931,9 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@custom \\{',
|
||||
kind: 'at-rule',
|
||||
name: '@custom',
|
||||
params: '\\{',
|
||||
nodes: [{ kind: 'declaration', property: 'foo', value: 'bar', important: false }],
|
||||
},
|
||||
])
|
||||
@ -940,8 +949,9 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'red', important: false },
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@media(width>=600px)',
|
||||
kind: 'at-rule',
|
||||
name: '@media',
|
||||
params: '(width>=600px)',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'rule',
|
||||
|
||||
@ -1,4 +1,13 @@
|
||||
import { comment, rule, type AstNode, type Comment, type Declaration, type Rule } from './ast'
|
||||
import {
|
||||
atRule,
|
||||
comment,
|
||||
rule,
|
||||
type AstNode,
|
||||
type AtRule,
|
||||
type Comment,
|
||||
type Declaration,
|
||||
type Rule,
|
||||
} from './ast'
|
||||
|
||||
const BACKSLASH = 0x5c
|
||||
const SLASH = 0x2f
|
||||
@ -294,7 +303,7 @@ export function parse(input: string) {
|
||||
// ^
|
||||
// ```
|
||||
else if (currentChar === SEMICOLON && buffer.charCodeAt(0) === AT_SIGN) {
|
||||
node = rule(buffer, [])
|
||||
node = parseAtRule(buffer)
|
||||
|
||||
// At-rule is nested inside of a rule, attach it to the parent.
|
||||
if (parent) {
|
||||
@ -338,7 +347,7 @@ export function parse(input: string) {
|
||||
closingBracketStack += '}'
|
||||
|
||||
// At this point `buffer` should resemble a selector or an at-rule.
|
||||
node = rule(buffer.trim(), [])
|
||||
node = rule(buffer.trim())
|
||||
|
||||
// Attach the rule to the parent in case it's nested.
|
||||
if (parent) {
|
||||
@ -381,7 +390,7 @@ export function parse(input: string) {
|
||||
// }
|
||||
// ```
|
||||
if (buffer.charCodeAt(0) === AT_SIGN) {
|
||||
node = rule(buffer.trim(), [])
|
||||
node = parseAtRule(buffer)
|
||||
|
||||
// At-rule is nested inside of a rule, attach it to the parent.
|
||||
if (parent) {
|
||||
@ -464,14 +473,19 @@ export function parse(input: string) {
|
||||
// If we have a leftover `buffer` that happens to start with an `@` then it
|
||||
// means that we have an at-rule that is not terminated with a semicolon at
|
||||
// the end of the input.
|
||||
if (buffer[0] === '@') {
|
||||
ast.push(rule(buffer.trim(), []))
|
||||
if (buffer.charCodeAt(0) === AT_SIGN) {
|
||||
ast.push(parseAtRule(buffer))
|
||||
}
|
||||
|
||||
// When we are done parsing then everything should be balanced. If we still
|
||||
// have a leftover `parent`, then it means that we have an unterminated block.
|
||||
if (closingBracketStack.length > 0 && parent) {
|
||||
throw new Error(`Missing closing } at ${parent.selector}`)
|
||||
if (parent.kind === 'rule') {
|
||||
throw new Error(`Missing closing } at ${parent.selector}`)
|
||||
}
|
||||
if (parent.kind === 'at-rule') {
|
||||
throw new Error(`Missing closing } at ${parent.name} ${parent.params}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (licenseComments.length > 0) {
|
||||
@ -481,6 +495,36 @@ export function parse(input: string) {
|
||||
return ast
|
||||
}
|
||||
|
||||
export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule {
|
||||
// Assumption: The smallest at-rule in CSS right now is `@page`, this means
|
||||
// that we can always skip the first 5 characters and start at the
|
||||
// sixth (at index 5).
|
||||
//
|
||||
// There is a chance someone is using a shorter at-rule, in that case we have
|
||||
// to adjust this number back to 2, e.g.: `@x`.
|
||||
//
|
||||
// This issue can only occur if somebody does the following things:
|
||||
//
|
||||
// 1. Uses a shorter at-rule than `@page`
|
||||
// 2. Disables Lightning CSS from `@tailwindcss/postcss` (because Lightning
|
||||
// CSS doesn't handle custom at-rules properly right now)
|
||||
// 3. Sandwiches the `@tailwindcss/postcss` plugin between two other plugins
|
||||
// that can handle the shorter at-rule
|
||||
//
|
||||
// Let's use the more common case as the default and we can adjust this
|
||||
// behavior if necessary.
|
||||
for (let i = 5 /* '@page'.length */; i < buffer.length; i++) {
|
||||
let currentChar = buffer.charCodeAt(i)
|
||||
if (currentChar === SPACE || currentChar === OPEN_PAREN) {
|
||||
let name = buffer.slice(0, i).trim()
|
||||
let params = buffer.slice(i).trim()
|
||||
return atRule(name, params, nodes)
|
||||
}
|
||||
}
|
||||
|
||||
return atRule(buffer.trim(), '', nodes)
|
||||
}
|
||||
|
||||
function parseDeclaration(buffer: string, colonIdx: number = buffer.indexOf(':')): Declaration {
|
||||
let importantIdx = buffer.indexOf('!important', colonIdx + 1)
|
||||
return {
|
||||
|
||||
@ -2,15 +2,18 @@ import { version } from '../package.json'
|
||||
import { substituteAtApply } from './apply'
|
||||
import {
|
||||
atRoot,
|
||||
atRule,
|
||||
comment,
|
||||
context,
|
||||
decl,
|
||||
rule,
|
||||
styleRule,
|
||||
toCss,
|
||||
walk,
|
||||
WalkAction,
|
||||
type AstNode,
|
||||
type Rule,
|
||||
type AtRule,
|
||||
type StyleRule,
|
||||
} from './ast'
|
||||
import { substituteAtImports } from './at-import'
|
||||
import { applyCompatibilityHooks } from './compat/apply-compat-hooks'
|
||||
@ -46,11 +49,11 @@ function throwOnLoadStylesheet(): never {
|
||||
throw new Error('No `loadStylesheet` function provided to `compile`')
|
||||
}
|
||||
|
||||
function parseThemeOptions(selector: string) {
|
||||
function parseThemeOptions(params: string) {
|
||||
let options = ThemeOptions.NONE
|
||||
let prefix = null
|
||||
|
||||
for (let option of segment(selector.slice(6) /* '@theme'.length */, ' ')) {
|
||||
for (let option of segment(params, ' ')) {
|
||||
if (option === 'reference') {
|
||||
options |= ThemeOptions.REFERENCE
|
||||
} else if (option === 'inline') {
|
||||
@ -81,21 +84,20 @@ async function parseCss(
|
||||
let theme = new Theme()
|
||||
let customVariants: ((designSystem: DesignSystem) => void)[] = []
|
||||
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
|
||||
let firstThemeRule: Rule | null = null
|
||||
let firstThemeRule = null as StyleRule | null
|
||||
let globs: { base: string; pattern: string }[] = []
|
||||
|
||||
// Handle at-rules
|
||||
walk(ast, (node, { parent, replaceWith, context }) => {
|
||||
if (node.kind !== 'rule') return
|
||||
if (node.selector[0] !== '@') return
|
||||
if (node.kind !== 'at-rule') return
|
||||
|
||||
// Collect custom `@utility` at-rules
|
||||
if (node.selector.startsWith('@utility ')) {
|
||||
if (node.name === '@utility') {
|
||||
if (parent !== null) {
|
||||
throw new Error('`@utility` cannot be nested.')
|
||||
}
|
||||
|
||||
let name = node.selector.slice(9).trim()
|
||||
let name = node.params
|
||||
|
||||
if (!IS_VALID_UTILITY_NAME.test(name)) {
|
||||
throw new Error(
|
||||
@ -120,7 +122,7 @@ async function parseCss(
|
||||
}
|
||||
|
||||
// Collect paths from `@source` at-rules
|
||||
if (node.selector.startsWith('@source ')) {
|
||||
if (node.name === '@source') {
|
||||
if (node.nodes.length > 0) {
|
||||
throw new Error('`@source` cannot have a body.')
|
||||
}
|
||||
@ -129,7 +131,7 @@ async function parseCss(
|
||||
throw new Error('`@source` cannot be nested.')
|
||||
}
|
||||
|
||||
let path = node.selector.slice(8)
|
||||
let path = node.params
|
||||
if (
|
||||
(path[0] === '"' && path[path.length - 1] !== '"') ||
|
||||
(path[0] === "'" && path[path.length - 1] !== "'") ||
|
||||
@ -143,7 +145,7 @@ async function parseCss(
|
||||
}
|
||||
|
||||
// Register custom variants from `@variant` at-rules
|
||||
if (node.selector.startsWith('@variant ')) {
|
||||
if (node.name === '@variant') {
|
||||
if (parent !== null) {
|
||||
throw new Error('`@variant` cannot be nested.')
|
||||
}
|
||||
@ -151,7 +153,7 @@ async function parseCss(
|
||||
// Remove `@variant` at-rule so it's not included in the compiled CSS
|
||||
replaceWith([])
|
||||
|
||||
let [name, selector] = segment(node.selector.slice(9), ' ')
|
||||
let [name, selector] = segment(node.params, ' ')
|
||||
|
||||
if (node.nodes.length > 0 && selector) {
|
||||
throw new Error(`\`@variant ${name}\` cannot have both a selector and a body.`)
|
||||
@ -165,14 +167,14 @@ async function parseCss(
|
||||
|
||||
let selectors = segment(selector.slice(1, -1), ',')
|
||||
|
||||
let atRuleSelectors: string[] = []
|
||||
let atRuleParams: string[] = []
|
||||
let styleRuleSelectors: string[] = []
|
||||
|
||||
for (let selector of selectors) {
|
||||
selector = selector.trim()
|
||||
|
||||
if (selector[0] === '@') {
|
||||
atRuleSelectors.push(selector)
|
||||
atRuleParams.push(selector)
|
||||
} else {
|
||||
styleRuleSelectors.push(selector)
|
||||
}
|
||||
@ -185,17 +187,17 @@ async function parseCss(
|
||||
let nodes: AstNode[] = []
|
||||
|
||||
if (styleRuleSelectors.length > 0) {
|
||||
nodes.push(rule(styleRuleSelectors.join(', '), r.nodes))
|
||||
nodes.push(styleRule(styleRuleSelectors.join(', '), r.nodes))
|
||||
}
|
||||
|
||||
for (let selector of atRuleSelectors) {
|
||||
for (let selector of atRuleParams) {
|
||||
nodes.push(rule(selector, r.nodes))
|
||||
}
|
||||
|
||||
r.nodes = nodes
|
||||
},
|
||||
{
|
||||
compounds: compoundsForSelectors([...styleRuleSelectors, ...atRuleSelectors]),
|
||||
compounds: compoundsForSelectors([...styleRuleSelectors, ...atRuleParams]),
|
||||
},
|
||||
)
|
||||
})
|
||||
@ -227,8 +229,8 @@ async function parseCss(
|
||||
}
|
||||
}
|
||||
|
||||
if (node.selector.startsWith('@media ')) {
|
||||
let params = segment(node.selector.slice(7), ' ')
|
||||
if (node.name === '@media') {
|
||||
let params = segment(node.params, ' ')
|
||||
let unknownParams: string[] = []
|
||||
|
||||
for (let param of params) {
|
||||
@ -241,13 +243,13 @@ async function parseCss(
|
||||
let themeParams = param.slice(6, -1)
|
||||
|
||||
walk(node.nodes, (child) => {
|
||||
if (child.kind !== 'rule') {
|
||||
if (child.kind !== 'at-rule') {
|
||||
throw new Error(
|
||||
'Files imported with `@import "…" theme(…)` must only contain `@theme` blocks.',
|
||||
)
|
||||
}
|
||||
if (child.selector === '@theme' || child.selector.startsWith('@theme ')) {
|
||||
child.selector += ' ' + themeParams
|
||||
if (child.name === '@theme') {
|
||||
child.params += ' ' + themeParams
|
||||
return WalkAction.Skip
|
||||
}
|
||||
})
|
||||
@ -261,9 +263,9 @@ async function parseCss(
|
||||
let prefix = param.slice(7, -1)
|
||||
|
||||
walk(node.nodes, (child) => {
|
||||
if (child.kind !== 'rule') return
|
||||
if (child.selector === '@theme' || child.selector.startsWith('@theme ')) {
|
||||
child.selector += ` prefix(${prefix})`
|
||||
if (child.kind !== 'at-rule') return
|
||||
if (child.name === '@theme') {
|
||||
child.params += ` prefix(${prefix})`
|
||||
return WalkAction.Skip
|
||||
}
|
||||
})
|
||||
@ -281,7 +283,7 @@ async function parseCss(
|
||||
}
|
||||
|
||||
if (unknownParams.length > 0) {
|
||||
node.selector = `@media ${unknownParams.join(' ')}`
|
||||
node.params = unknownParams.join(' ')
|
||||
} else if (params.length > 0) {
|
||||
replaceWith(node.nodes)
|
||||
}
|
||||
@ -290,8 +292,8 @@ async function parseCss(
|
||||
}
|
||||
|
||||
// Handle `@theme`
|
||||
if (node.selector === '@theme' || node.selector.startsWith('@theme ')) {
|
||||
let [themeOptions, themePrefix] = parseThemeOptions(node.selector)
|
||||
if (node.name === '@theme') {
|
||||
let [themeOptions, themePrefix] = parseThemeOptions(node.params)
|
||||
|
||||
if (themePrefix) {
|
||||
if (!IS_VALID_PREFIX.test(themePrefix)) {
|
||||
@ -307,7 +309,7 @@ async function parseCss(
|
||||
walk(node.nodes, (child, { replaceWith }) => {
|
||||
// Collect `@keyframes` rules to re-insert with theme variables later,
|
||||
// since the `@theme` rule itself will be removed.
|
||||
if (child.kind === 'rule' && child.selector.startsWith('@keyframes ')) {
|
||||
if (child.kind === 'at-rule' && child.name === '@keyframes') {
|
||||
theme.addKeyframes(child)
|
||||
replaceWith([])
|
||||
return WalkAction.Skip
|
||||
@ -319,7 +321,7 @@ async function parseCss(
|
||||
return
|
||||
}
|
||||
|
||||
let snippet = toCss([rule(node.selector, [child])])
|
||||
let snippet = toCss([atRule(node.name, node.params, [child])])
|
||||
.split('\n')
|
||||
.map((line, idx, all) => `${idx === 0 || idx >= all.length - 2 ? ' ' : '>'} ${line}`)
|
||||
.join('\n')
|
||||
@ -332,7 +334,8 @@ async function parseCss(
|
||||
// Keep a reference to the first `@theme` rule to update with the full
|
||||
// theme later, and delete any other `@theme` rules.
|
||||
if (!firstThemeRule && !(themeOptions & ThemeOptions.REFERENCE)) {
|
||||
firstThemeRule = node
|
||||
firstThemeRule = styleRule(':root', node.nodes)
|
||||
replaceWith([firstThemeRule])
|
||||
} else {
|
||||
replaceWith([])
|
||||
}
|
||||
@ -363,9 +366,6 @@ async function parseCss(
|
||||
// Output final set of theme variables at the position of the first `@theme`
|
||||
// rule.
|
||||
if (firstThemeRule) {
|
||||
firstThemeRule = firstThemeRule as Rule
|
||||
firstThemeRule.selector = ':root'
|
||||
|
||||
let nodes = []
|
||||
|
||||
for (let [key, value] of theme.entries()) {
|
||||
@ -381,16 +381,14 @@ async function parseCss(
|
||||
|
||||
for (let keyframesRule of keyframesRules) {
|
||||
// Remove any keyframes that aren't used by an animation variable.
|
||||
let keyframesName = keyframesRule.selector.slice(11) // `@keyframes `.length
|
||||
let keyframesName = keyframesRule.params
|
||||
if (!animationParts.includes(keyframesName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Wrap `@keyframes` in `AtRoot` so they are hoisted out of `:root` when
|
||||
// printing.
|
||||
nodes.push(
|
||||
Object.assign(keyframesRule, atRoot([rule(keyframesRule.selector, keyframesRule.nodes)])),
|
||||
)
|
||||
nodes.push(atRoot([keyframesRule]))
|
||||
}
|
||||
}
|
||||
firstThemeRule.nodes = nodes
|
||||
@ -404,9 +402,9 @@ async function parseCss(
|
||||
// Remove `@utility`, we couldn't replace it before yet because we had to
|
||||
// handle the nested `@apply` at-rules first.
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
if (node.kind !== 'rule') return
|
||||
if (node.kind !== 'at-rule') return
|
||||
|
||||
if (node.selector[0] === '@' && node.selector.startsWith('@utility ')) {
|
||||
if (node.name === '@utility') {
|
||||
replaceWith([])
|
||||
}
|
||||
|
||||
@ -431,12 +429,12 @@ export async function compile(
|
||||
}> {
|
||||
let { designSystem, ast, globs } = await parseCss(css, opts)
|
||||
|
||||
let tailwindUtilitiesNode: Rule | null = null
|
||||
let tailwindUtilitiesNode: AtRule | null = null
|
||||
|
||||
// Find `@tailwind utilities` so that we can later replace it with the actual
|
||||
// generated utility class CSS.
|
||||
walk(ast, (node) => {
|
||||
if (node.kind === 'rule' && node.selector === '@tailwind utilities') {
|
||||
if (node.kind === 'at-rule' && node.name === '@tailwind' && node.params === 'utilities') {
|
||||
tailwindUtilitiesNode = node
|
||||
|
||||
// Stop walking after finding `@tailwind utilities` to avoid walking all
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { rule, walkDepth } from './ast'
|
||||
import { styleRule, walkDepth } from './ast'
|
||||
import { applyVariant } from './compile'
|
||||
import type { DesignSystem } from './design-system'
|
||||
|
||||
@ -69,7 +69,7 @@ export function getVariants(design: DesignSystem) {
|
||||
if (!variant) return []
|
||||
|
||||
// Apply the variant to a placeholder rule
|
||||
let node = rule('.__placeholder__', [])
|
||||
let node = styleRule('.__placeholder__', [])
|
||||
|
||||
// If the rule produces no nodes it means the variant does not apply
|
||||
if (applyVariant(node, variant, design.variants) === null) {
|
||||
@ -82,16 +82,13 @@ export function getVariants(design: DesignSystem) {
|
||||
// Produce v3-style selector strings in the face of nested rules
|
||||
// this is more visible for things like group-*, not-*, etc…
|
||||
walkDepth(node.nodes, (node, { path }) => {
|
||||
if (node.kind !== 'rule') return
|
||||
if (node.kind !== 'rule' && node.kind !== 'at-rule') return
|
||||
if (node.nodes.length > 0) return
|
||||
|
||||
// Sort at-rules before style rules
|
||||
path.sort((a, b) => {
|
||||
// This won't actually happen, but it's here to make TypeScript happy
|
||||
if (a.kind !== 'rule' || b.kind !== 'rule') return 0
|
||||
|
||||
let aIsAtRule = a.selector[0] === '@'
|
||||
let bIsAtRule = b.selector[0] === '@'
|
||||
let aIsAtRule = a.kind === 'at-rule'
|
||||
let bIsAtRule = b.kind === 'at-rule'
|
||||
|
||||
if (aIsAtRule && !bIsAtRule) return -1
|
||||
if (!aIsAtRule && bIsAtRule) return 1
|
||||
@ -101,8 +98,15 @@ export function getVariants(design: DesignSystem) {
|
||||
|
||||
// A list of the selectors / at rules encountered to get to this point
|
||||
let group = path.flatMap((node) => {
|
||||
if (node.kind !== 'rule') return []
|
||||
return node.selector === '&' ? [] : [node.selector]
|
||||
if (node.kind === 'rule') {
|
||||
return node.selector === '&' ? [] : [node.selector]
|
||||
}
|
||||
|
||||
if (node.kind === 'at-rule') {
|
||||
return [`${node.name} ${node.params}`]
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
// Build a v3-style nested selector
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Rule } from './ast'
|
||||
import type { AtRule } from './ast'
|
||||
import { escape } from './utils/escape'
|
||||
|
||||
export const enum ThemeOptions {
|
||||
@ -13,7 +13,7 @@ export class Theme {
|
||||
|
||||
constructor(
|
||||
private values = new Map<string, { value: string; options: ThemeOptions }>(),
|
||||
private keyframes = new Set<Rule>([]),
|
||||
private keyframes = new Set<AtRule>([]),
|
||||
) {}
|
||||
|
||||
add(key: string, value: string, options = ThemeOptions.NONE): void {
|
||||
@ -204,7 +204,7 @@ export class Theme {
|
||||
return values
|
||||
}
|
||||
|
||||
addKeyframes(value: Rule): void {
|
||||
addKeyframes(value: AtRule): void {
|
||||
this.keyframes.add(value)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { atRoot, decl, rule, type AstNode } from './ast'
|
||||
import { atRoot, atRule, decl, styleRule, type AstNode } from './ast'
|
||||
import type { Candidate, CandidateModifier, NamedUtilityValue } from './candidate'
|
||||
import type { Theme, ThemeKey } from './theme'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
@ -85,7 +85,7 @@ export class Utilities {
|
||||
}
|
||||
|
||||
function property(ident: string, initialValue?: string, syntax?: string) {
|
||||
return rule(`@property ${ident}`, [
|
||||
return atRule('@property', ident, [
|
||||
decl('syntax', syntax ? `"${syntax}"` : `"*"`),
|
||||
decl('inherits', 'false'),
|
||||
|
||||
@ -2024,7 +2024,7 @@ export function createUtilities(theme: Theme) {
|
||||
handle: (value) => [
|
||||
atRoot([property('--tw-space-x-reverse', '0', '<number>')]),
|
||||
|
||||
rule(':where(& > :not(:last-child))', [
|
||||
styleRule(':where(& > :not(:last-child))', [
|
||||
decl('--tw-sort', 'row-gap'),
|
||||
decl('margin-inline-start', `calc(${value} * var(--tw-space-x-reverse))`),
|
||||
decl('margin-inline-end', `calc(${value} * calc(1 - var(--tw-space-x-reverse)))`),
|
||||
@ -2038,7 +2038,7 @@ export function createUtilities(theme: Theme) {
|
||||
handle: (value) => [
|
||||
atRoot([property('--tw-space-y-reverse', '0', '<number>')]),
|
||||
|
||||
rule(':where(& > :not(:last-child))', [
|
||||
styleRule(':where(& > :not(:last-child))', [
|
||||
decl('--tw-sort', 'column-gap'),
|
||||
decl('margin-block-start', `calc(${value} * var(--tw-space-y-reverse))`),
|
||||
decl('margin-block-end', `calc(${value} * calc(1 - var(--tw-space-y-reverse)))`),
|
||||
@ -2049,7 +2049,7 @@ export function createUtilities(theme: Theme) {
|
||||
staticUtility('space-x-reverse', [
|
||||
() => atRoot([property('--tw-space-x-reverse', '0', '<number>')]),
|
||||
() =>
|
||||
rule(':where(& > :not(:last-child))', [
|
||||
styleRule(':where(& > :not(:last-child))', [
|
||||
decl('--tw-sort', 'row-gap'),
|
||||
decl('--tw-space-x-reverse', '1'),
|
||||
]),
|
||||
@ -2058,7 +2058,7 @@ export function createUtilities(theme: Theme) {
|
||||
staticUtility('space-y-reverse', [
|
||||
() => atRoot([property('--tw-space-y-reverse', '0', '<number>')]),
|
||||
() =>
|
||||
rule(':where(& > :not(:last-child))', [
|
||||
styleRule(':where(& > :not(:last-child))', [
|
||||
decl('--tw-sort', 'column-gap'),
|
||||
decl('--tw-space-y-reverse', '1'),
|
||||
]),
|
||||
@ -2078,7 +2078,7 @@ export function createUtilities(theme: Theme) {
|
||||
colorUtility('divide', {
|
||||
themeKeys: ['--divide-color', '--color'],
|
||||
handle: (value) => [
|
||||
rule(':where(& > :not(:last-child))', [
|
||||
styleRule(':where(& > :not(:last-child))', [
|
||||
decl('--tw-sort', 'divide-color'),
|
||||
decl('border-color', value),
|
||||
]),
|
||||
@ -2386,7 +2386,7 @@ export function createUtilities(theme: Theme) {
|
||||
handle: (value) => [
|
||||
atRoot([property('--tw-divide-x-reverse', '0', '<number>')]),
|
||||
|
||||
rule(':where(& > :not(:last-child))', [
|
||||
styleRule(':where(& > :not(:last-child))', [
|
||||
decl('--tw-sort', 'divide-x-width'),
|
||||
borderProperties(),
|
||||
decl('border-inline-style', 'var(--tw-border-style)'),
|
||||
@ -2406,7 +2406,7 @@ export function createUtilities(theme: Theme) {
|
||||
handle: (value) => [
|
||||
atRoot([property('--tw-divide-y-reverse', '0', '<number>')]),
|
||||
|
||||
rule(':where(& > :not(:last-child))', [
|
||||
styleRule(':where(& > :not(:last-child))', [
|
||||
decl('--tw-sort', 'divide-y-width'),
|
||||
borderProperties(),
|
||||
decl('border-bottom-style', 'var(--tw-border-style)'),
|
||||
@ -2435,18 +2435,18 @@ export function createUtilities(theme: Theme) {
|
||||
|
||||
staticUtility('divide-x-reverse', [
|
||||
() => atRoot([property('--tw-divide-x-reverse', '0', '<number>')]),
|
||||
() => rule(':where(& > :not(:last-child))', [decl('--tw-divide-x-reverse', '1')]),
|
||||
() => styleRule(':where(& > :not(:last-child))', [decl('--tw-divide-x-reverse', '1')]),
|
||||
])
|
||||
|
||||
staticUtility('divide-y-reverse', [
|
||||
() => atRoot([property('--tw-divide-y-reverse', '0', '<number>')]),
|
||||
() => rule(':where(& > :not(:last-child))', [decl('--tw-divide-y-reverse', '1')]),
|
||||
() => styleRule(':where(& > :not(:last-child))', [decl('--tw-divide-y-reverse', '1')]),
|
||||
])
|
||||
|
||||
for (let value of ['solid', 'dashed', 'dotted', 'double', 'none']) {
|
||||
staticUtility(`divide-${value}`, [
|
||||
() =>
|
||||
rule(':where(& > :not(:last-child))', [
|
||||
styleRule(':where(& > :not(:last-child))', [
|
||||
decl('--tw-sort', 'divide-style'),
|
||||
decl('--tw-border-style', value),
|
||||
decl('border-style', value),
|
||||
@ -3150,7 +3150,7 @@ export function createUtilities(theme: Theme) {
|
||||
colorUtility('placeholder', {
|
||||
themeKeys: ['--background-color', '--color'],
|
||||
handle: (value) => [
|
||||
rule('&::placeholder', [decl('--tw-sort', 'placeholder-color'), decl('color', value)]),
|
||||
styleRule('&::placeholder', [decl('--tw-sort', 'placeholder-color'), decl('color', value)]),
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@ -1,4 +1,16 @@
|
||||
import { WalkAction, atRoot, decl, rule, walk, type AstNode, type Rule } from './ast'
|
||||
import {
|
||||
WalkAction,
|
||||
atRoot,
|
||||
atRule,
|
||||
decl,
|
||||
rule,
|
||||
styleRule,
|
||||
walk,
|
||||
type AstNode,
|
||||
type AtRule,
|
||||
type Rule,
|
||||
type StyleRule,
|
||||
} from './ast'
|
||||
import { type Variant } from './candidate'
|
||||
import type { Theme } from './theme'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
@ -69,9 +81,11 @@ export class Variants {
|
||||
let selectors: string[] = []
|
||||
|
||||
walk(ast, (node) => {
|
||||
if (node.kind !== 'rule') return
|
||||
if (node.selector === '@slot') return
|
||||
selectors.push(node.selector)
|
||||
if (node.kind === 'rule') {
|
||||
selectors.push(node.selector)
|
||||
} else if (node.kind === 'at-rule' && node.name !== '@slot') {
|
||||
selectors.push(`${node.name} ${node.params}`)
|
||||
}
|
||||
})
|
||||
|
||||
this.static(
|
||||
@ -333,7 +347,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
return parts.slice(1).join(' ')
|
||||
}
|
||||
|
||||
if (ruleName === 'container') {
|
||||
if (ruleName === '@container') {
|
||||
// @container {query}
|
||||
if (parts[0][0] === '(') {
|
||||
return `not ${condition}`
|
||||
@ -356,28 +370,25 @@ export function createVariants(theme: Theme): Variants {
|
||||
|
||||
let conditionalRules = ['@media', '@supports', '@container']
|
||||
|
||||
function negateSelector(selector: string) {
|
||||
if (selector[0] === '@') {
|
||||
for (let ruleName of conditionalRules) {
|
||||
if (!selector.startsWith(ruleName)) continue
|
||||
function negateAtRule(rule: AtRule) {
|
||||
for (let ruleName of conditionalRules) {
|
||||
if (ruleName !== rule.name) continue
|
||||
|
||||
let name = ruleName.slice(1)
|
||||
let params = selector.slice(ruleName.length).trim()
|
||||
let conditions = segment(rule.params, ',')
|
||||
|
||||
let conditions = segment(params, ',')
|
||||
// We don't support things like `@media screen, print` because
|
||||
// the negation would be `@media not screen and print` and we don't
|
||||
// want to deal with that complexity.
|
||||
if (conditions.length > 1) return null
|
||||
|
||||
// We don't support things like `@media screen, print` because
|
||||
// the negation would be `@media not screen and print` and we don't
|
||||
// want to deal with that complexity.
|
||||
if (conditions.length > 1) return null
|
||||
|
||||
conditions = negateConditions(name, conditions)
|
||||
return `@${name} ${conditions.join(', ')}`
|
||||
}
|
||||
|
||||
return null
|
||||
conditions = negateConditions(rule.name, conditions)
|
||||
return atRule(rule.name, conditions.join(', '))
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function negateSelector(selector: string) {
|
||||
if (selector.includes('::')) return null
|
||||
|
||||
let selectors = segment(selector, ',').map((sel) => {
|
||||
@ -404,18 +415,17 @@ export function createVariants(theme: Theme): Variants {
|
||||
let didApply = false
|
||||
|
||||
walk([ruleNode], (node, { path }) => {
|
||||
if (node.kind !== 'rule') return WalkAction.Continue
|
||||
if (node.kind !== 'rule' && node.kind !== 'at-rule') return WalkAction.Continue
|
||||
if (node.nodes.length > 0) return WalkAction.Continue
|
||||
|
||||
// Throw out any candidates with variants using nested style rules
|
||||
let atRules: Rule[] = []
|
||||
let styleRules: Rule[] = []
|
||||
let atRules: AtRule[] = []
|
||||
let styleRules: StyleRule[] = []
|
||||
|
||||
for (let parent of path) {
|
||||
if (parent.kind !== 'rule') continue
|
||||
if (parent.selector[0] === '@') {
|
||||
if (parent.kind === 'at-rule') {
|
||||
atRules.push(parent)
|
||||
} else {
|
||||
} else if (parent.kind === 'rule') {
|
||||
styleRules.push(parent)
|
||||
}
|
||||
}
|
||||
@ -425,28 +435,27 @@ export function createVariants(theme: Theme): Variants {
|
||||
|
||||
let rules: Rule[] = []
|
||||
|
||||
for (let styleRule of styleRules) {
|
||||
let selector = negateSelector(styleRule.selector)
|
||||
for (let node of styleRules) {
|
||||
let selector = negateSelector(node.selector)
|
||||
if (!selector) {
|
||||
didApply = false
|
||||
return WalkAction.Stop
|
||||
}
|
||||
|
||||
rules.push(rule(selector, []))
|
||||
rules.push(styleRule(selector, []))
|
||||
}
|
||||
|
||||
for (let atRule of atRules) {
|
||||
let selector = negateSelector(atRule.selector)
|
||||
if (!selector) {
|
||||
for (let node of atRules) {
|
||||
let negatedAtRule = negateAtRule(node)
|
||||
if (!negatedAtRule) {
|
||||
didApply = false
|
||||
return WalkAction.Stop
|
||||
}
|
||||
|
||||
rules.push(rule(selector, []))
|
||||
rules.push(negatedAtRule)
|
||||
}
|
||||
|
||||
ruleNode.selector = '&'
|
||||
ruleNode.nodes = rules
|
||||
Object.assign(ruleNode, styleRule('&', rules))
|
||||
|
||||
// Track that the variant was actually applied
|
||||
didApply = true
|
||||
@ -455,9 +464,8 @@ export function createVariants(theme: Theme): Variants {
|
||||
})
|
||||
|
||||
// TODO: Tweak group, peer, has to ignore intermediate `&` selectors (maybe?)
|
||||
if (ruleNode.selector === '&' && ruleNode.nodes.length === 1) {
|
||||
ruleNode.selector = (ruleNode.nodes[0] as Rule).selector
|
||||
ruleNode.nodes = (ruleNode.nodes[0] as Rule).nodes
|
||||
if (ruleNode.kind === 'rule' && ruleNode.selector === '&' && ruleNode.nodes.length === 1) {
|
||||
Object.assign(ruleNode, ruleNode.nodes[0])
|
||||
}
|
||||
|
||||
// If the node wasn't modified, this variant is not compatible with
|
||||
@ -485,13 +493,9 @@ export function createVariants(theme: Theme): Variants {
|
||||
walk([ruleNode], (node, { path }) => {
|
||||
if (node.kind !== 'rule') return WalkAction.Continue
|
||||
|
||||
// Skip past at-rules, and continue traversing the children of the at-rule
|
||||
if (node.selector[0] === '@') return WalkAction.Continue
|
||||
|
||||
// Throw out any candidates with variants using nested style rules
|
||||
for (let parent of path.slice(0, -1)) {
|
||||
if (parent.kind !== 'rule') continue
|
||||
if (parent.selector[0] === '@') continue
|
||||
|
||||
didApply = false
|
||||
return WalkAction.Stop
|
||||
@ -541,13 +545,9 @@ export function createVariants(theme: Theme): Variants {
|
||||
walk([ruleNode], (node, { path }) => {
|
||||
if (node.kind !== 'rule') return WalkAction.Continue
|
||||
|
||||
// Skip past at-rules, and continue traversing the children of the at-rule
|
||||
if (node.selector[0] === '@') return WalkAction.Continue
|
||||
|
||||
// Throw out any candidates with variants using nested style rules
|
||||
for (let parent of path.slice(0, -1)) {
|
||||
if (parent.kind !== 'rule') continue
|
||||
if (parent.selector[0] === '@') continue
|
||||
|
||||
didApply = false
|
||||
return WalkAction.Stop
|
||||
@ -597,7 +597,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
{
|
||||
function contentProperties() {
|
||||
return atRoot([
|
||||
rule('@property --tw-content', [
|
||||
atRule('@property', '--tw-content', [
|
||||
decl('syntax', '"*"'),
|
||||
decl('initial-value', '""'),
|
||||
decl('inherits', 'false'),
|
||||
@ -608,7 +608,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
'before',
|
||||
(v) => {
|
||||
v.nodes = [
|
||||
rule('&::before', [
|
||||
styleRule('&::before', [
|
||||
contentProperties(),
|
||||
decl('content', 'var(--tw-content)'),
|
||||
...v.nodes,
|
||||
@ -622,7 +622,11 @@ export function createVariants(theme: Theme): Variants {
|
||||
'after',
|
||||
(v) => {
|
||||
v.nodes = [
|
||||
rule('&::after', [contentProperties(), decl('content', 'var(--tw-content)'), ...v.nodes]),
|
||||
styleRule('&::after', [
|
||||
contentProperties(),
|
||||
decl('content', 'var(--tw-content)'),
|
||||
...v.nodes,
|
||||
]),
|
||||
]
|
||||
},
|
||||
{ compounds: Compounds.Never },
|
||||
@ -664,7 +668,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
// Interactive
|
||||
staticVariant('focus-within', ['&:focus-within'])
|
||||
variants.static('hover', (r) => {
|
||||
r.nodes = [rule('&:hover', [rule('@media (hover: hover)', r.nodes)])]
|
||||
r.nodes = [styleRule('&:hover', [atRule('@media', '(hover: hover)', r.nodes)])]
|
||||
})
|
||||
staticVariant('focus', ['&:focus'])
|
||||
staticVariant('focus-visible', ['&:focus-visible'])
|
||||
@ -682,13 +686,9 @@ export function createVariants(theme: Theme): Variants {
|
||||
walk([ruleNode], (node, { path }) => {
|
||||
if (node.kind !== 'rule') return WalkAction.Continue
|
||||
|
||||
// Skip past at-rules, and continue traversing the children of the at-rule
|
||||
if (node.selector[0] === '@') return WalkAction.Continue
|
||||
|
||||
// Throw out any candidates with variants using nested style rules
|
||||
for (let parent of path.slice(0, -1)) {
|
||||
if (parent.kind !== 'rule') continue
|
||||
if (parent.selector[0] === '@') continue
|
||||
|
||||
didApply = false
|
||||
return WalkAction.Stop
|
||||
@ -717,9 +717,11 @@ export function createVariants(theme: Theme): Variants {
|
||||
if (!variant.value || variant.modifier) return null
|
||||
|
||||
if (variant.value.kind === 'arbitrary') {
|
||||
ruleNode.nodes = [rule(`&[aria-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [
|
||||
styleRule(`&[aria-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes),
|
||||
]
|
||||
} else {
|
||||
ruleNode.nodes = [rule(`&[aria-${variant.value.value}="true"]`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [styleRule(`&[aria-${variant.value.value}="true"]`, ruleNode.nodes)]
|
||||
}
|
||||
})
|
||||
|
||||
@ -738,7 +740,9 @@ export function createVariants(theme: Theme): Variants {
|
||||
variants.functional('data', (ruleNode, variant) => {
|
||||
if (!variant.value || variant.modifier) return null
|
||||
|
||||
ruleNode.nodes = [rule(`&[data-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [
|
||||
styleRule(`&[data-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes),
|
||||
]
|
||||
})
|
||||
|
||||
variants.functional('nth', (ruleNode, variant) => {
|
||||
@ -747,7 +751,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
// Only numeric bare values are allowed
|
||||
if (variant.value.kind === 'named' && !isPositiveInteger(variant.value.value)) return null
|
||||
|
||||
ruleNode.nodes = [rule(`&:nth-child(${variant.value.value})`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [styleRule(`&:nth-child(${variant.value.value})`, ruleNode.nodes)]
|
||||
})
|
||||
|
||||
variants.functional('nth-last', (ruleNode, variant) => {
|
||||
@ -756,7 +760,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
// Only numeric bare values are allowed
|
||||
if (variant.value.kind === 'named' && !isPositiveInteger(variant.value.value)) return null
|
||||
|
||||
ruleNode.nodes = [rule(`&:nth-last-child(${variant.value.value})`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [styleRule(`&:nth-last-child(${variant.value.value})`, ruleNode.nodes)]
|
||||
})
|
||||
|
||||
variants.functional('nth-of-type', (ruleNode, variant) => {
|
||||
@ -765,7 +769,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
// Only numeric bare values are allowed
|
||||
if (variant.value.kind === 'named' && !isPositiveInteger(variant.value.value)) return null
|
||||
|
||||
ruleNode.nodes = [rule(`&:nth-of-type(${variant.value.value})`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [styleRule(`&:nth-of-type(${variant.value.value})`, ruleNode.nodes)]
|
||||
})
|
||||
|
||||
variants.functional('nth-last-of-type', (ruleNode, variant) => {
|
||||
@ -774,7 +778,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
// Only numeric bare values are allowed
|
||||
if (variant.value.kind === 'named' && !isPositiveInteger(variant.value.value)) return null
|
||||
|
||||
ruleNode.nodes = [rule(`&:nth-last-of-type(${variant.value.value})`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [styleRule(`&:nth-last-of-type(${variant.value.value})`, ruleNode.nodes)]
|
||||
})
|
||||
|
||||
variants.functional(
|
||||
@ -792,7 +796,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
// `(condition1) or (condition2)` is supported.
|
||||
let query = value.replace(/\b(and|or|not)\b/g, ' $1 ')
|
||||
|
||||
ruleNode.nodes = [rule(`@supports ${query}`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [atRule('@supports', query, ruleNode.nodes)]
|
||||
return
|
||||
}
|
||||
|
||||
@ -818,7 +822,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
value = `(${value})`
|
||||
}
|
||||
|
||||
ruleNode.nodes = [rule(`@supports ${value}`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [atRule('@supports', value, ruleNode.nodes)]
|
||||
},
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
@ -937,7 +941,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
let value = resolvedBreakpoints.get(variant)
|
||||
if (value === null) return null
|
||||
|
||||
ruleNode.nodes = [rule(`@media (width < ${value})`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [atRule('@media', `(width < ${value})`, ruleNode.nodes)]
|
||||
},
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
@ -959,7 +963,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
variants.static(
|
||||
key,
|
||||
(ruleNode) => {
|
||||
ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [atRule('@media', `(width >= ${value})`, ruleNode.nodes)]
|
||||
},
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
@ -972,7 +976,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
let value = resolvedBreakpoints.get(variant)
|
||||
if (value === null) return null
|
||||
|
||||
ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)]
|
||||
ruleNode.nodes = [atRule('@media', `(width >= ${value})`, ruleNode.nodes)]
|
||||
},
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
@ -1024,10 +1028,11 @@ export function createVariants(theme: Theme): Variants {
|
||||
if (value === null) return null
|
||||
|
||||
ruleNode.nodes = [
|
||||
rule(
|
||||
atRule(
|
||||
'@container',
|
||||
variant.modifier
|
||||
? `@container ${variant.modifier.value} (width < ${value})`
|
||||
: `@container (width < ${value})`,
|
||||
? `${variant.modifier.value} (width < ${value})`
|
||||
: `(width < ${value})`,
|
||||
ruleNode.nodes,
|
||||
),
|
||||
]
|
||||
@ -1052,10 +1057,11 @@ export function createVariants(theme: Theme): Variants {
|
||||
if (value === null) return null
|
||||
|
||||
ruleNode.nodes = [
|
||||
rule(
|
||||
atRule(
|
||||
'@container',
|
||||
variant.modifier
|
||||
? `@container ${variant.modifier.value} (width >= ${value})`
|
||||
: `@container (width >= ${value})`,
|
||||
? `${variant.modifier.value} (width >= ${value})`
|
||||
: `(width >= ${value})`,
|
||||
ruleNode.nodes,
|
||||
),
|
||||
]
|
||||
@ -1069,10 +1075,11 @@ export function createVariants(theme: Theme): Variants {
|
||||
if (value === null) return null
|
||||
|
||||
ruleNode.nodes = [
|
||||
rule(
|
||||
atRule(
|
||||
'@container',
|
||||
variant.modifier
|
||||
? `@container ${variant.modifier.value} (width >= ${value})`
|
||||
: `@container (width >= ${value})`,
|
||||
? `${variant.modifier.value} (width >= ${value})`
|
||||
: `(width >= ${value})`,
|
||||
ruleNode.nodes,
|
||||
),
|
||||
]
|
||||
@ -1140,17 +1147,13 @@ function quoteAttributeValue(input: string) {
|
||||
export function substituteAtSlot(ast: AstNode[], nodes: AstNode[]) {
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
// Replace `@slot` with rule nodes
|
||||
if (node.kind === 'rule' && node.selector === '@slot') {
|
||||
if (node.kind === 'at-rule' && node.name === '@slot') {
|
||||
replaceWith(nodes)
|
||||
}
|
||||
|
||||
// Wrap `@keyframes` and `@property` in `AtRoot` nodes
|
||||
else if (
|
||||
node.kind === 'rule' &&
|
||||
node.selector[0] === '@' &&
|
||||
(node.selector.startsWith('@keyframes ') || node.selector.startsWith('@property '))
|
||||
) {
|
||||
Object.assign(node, atRoot([rule(node.selector, node.nodes)]))
|
||||
else if (node.kind === 'at-rule' && (node.name === '@keyframes' || node.name === '@property')) {
|
||||
Object.assign(node, atRoot([atRule(node.name, node.params, node.nodes)]))
|
||||
return WalkAction.Skip
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user