Skip to content

Commit

Permalink
Support main dynamic variants
Browse files Browse the repository at this point in the history
  • Loading branch information
ArnaudBarre committed Dec 23, 2023
1 parent b2cc7f0 commit bf44db3
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 65 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Support main dynamic variants (`min-*`, `max-*`, `(group/peer-)data-*`, `(group/peer-)aria-*`)

## 0.7.2

- Fix plugin usage in build watch mode
Expand Down
16 changes: 7 additions & 9 deletions README.md
Expand Up @@ -151,15 +151,11 @@ export const config: DownwindConfig = {

### Dynamic variants

`supports-*` is supported.
`supports-*`, `min-*`, `max-*`, `(group/peer-)data-*`, `(group/peer-)aria-*` are supported.

`max-<screen>` is supported when the screens config is a basic `min-width` only. No sorting is done.

Other dynamic variants are not implemented for now. It means `min-*`, `data-*`, `aria-*`, `group-*`, `peer-*` are **not** supported.

Punctual need usage can be accomplished using arbitrary variants: `[@media(min-width:900px)]:block`

Variants modifier (ex. `group/sidebar`) are not supported either. The few cases were there are needed can also be covered with arbitrary variants:
`group-*`, `peer-*` and variants modifier (ex. `group/sidebar`) are not supported. The few cases were there are needed can be covered with arbitrary variants:
`group-hover/sidebar:opacity-75 group-hover/navitem:bg-black/75` -> `[.sidebar:hover_&]:opacity-75 group-hover:bg-black/75`

### Variants
Expand Down Expand Up @@ -222,7 +218,7 @@ To avoid parsing errors in WebStorm, double quotes are required. And because [th
### Almost exhaustive list of non-supported features

- Container queries, but this will probably be added later
- Some `addVariant` capabilities like generating at-rules. Also something useful to support in the future
- Adding variants via plugins. Also something useful to support in the future
- [prefix](https://tailwindcss.com/docs/configuration#prefix), [separator](https://tailwindcss.com/docs/configuration#separator) and [important](https://tailwindcss.com/docs/configuration#important) configuration options
- These deprecated utils: `transform`, `transform-cpu`, `decoration-slice` `decoration-clone`, `filter`, `backdrop-filter`, `blur-0`
- These deprecated colors: `lightBlue`, `warmGray`, `trueGray`, `coolGray`, `blueGray`
Expand All @@ -244,7 +240,7 @@ To avoid parsing errors in WebStorm, double quotes are required. And because [th

## How it works

When loading the configuration, three maps are generated: one for all possible variants, one for all static rules and one for all prefix with possible arbitrary values.
When loading the configuration, four maps are generated: one for static variants, one for prefixes of dynamic variants, one for static rules and one for prefixes of arbitrary values.

Then an object with few methods is returned:

Expand All @@ -265,7 +261,9 @@ Then an object with few methods is returned:
- `scan` is used to scan some source code. A regex is first use to match candidates and then these candidates are parsed roughly like this:
- Search for variants (repeat until not match):
- If the token start `[`, looks for next `]:` and add the content as arbitrary variant. If no `]:`, test if it's an arbitrary value (`[color:red]`).
- else search `:` and use the prefix to search in the variant map
- else search `:`
- if the left part contains `-[`, search for the prefix in the dynamic variant map
- otherwise lookup the value in the static variant map
- Test if the remaining token is part of the static rules
- Search for `-[`
- if matchs:
Expand Down
61 changes: 40 additions & 21 deletions src/index.ts
Expand Up @@ -30,7 +30,11 @@ import {
} from "./utils/print.ts";
import { escapeSelector, selectorRE } from "./utils/regex.ts";
import { themeGet } from "./utils/themeGet.ts";
import { type AtRuleVariant, getVariants, type Variant } from "./variants.ts";
import {
type AtRuleVariant,
getVariants,
type StaticVariant,
} from "./variants.ts";

export const VERSION = __VERSION__;

Expand All @@ -40,7 +44,7 @@ const themeRE = /theme\("([^)]+)"\)/g;

type Match = {
token: string;
variants: Variant[];
variants: StaticVariant[];
important: boolean;
} & (
| { type: "Rule"; ruleEntry: RuleEntry }
Expand Down Expand Up @@ -76,7 +80,7 @@ export const initDownwindWithConfig = ({
}) => {
const config = resolveConfig(userConfig);
const defaults = getDefaults(config);
const variantsMap = getVariants(config);
const { staticVariantsMap, dynamicVariantsMap } = getVariants(config);
const rules = getRules(config);
const { rulesEntries, arbitraryEntries } = getEntries(rules);

Expand All @@ -88,7 +92,7 @@ export const initDownwindWithConfig = ({
// This init is for the `container` edge case
Object.keys(config.theme.screens).map(
(screen): MatchesGroup["atRules"][number] => {
const variant = variantsMap.get(screen) as AtRuleVariant;
const variant = staticVariantsMap.get(screen) as AtRuleVariant;
return {
screenKey: screen,
order: variant.order,
Expand Down Expand Up @@ -183,10 +187,10 @@ export const initDownwindWithConfig = ({

let important = false;
let tokenWithoutVariants = token;
const variants: Variant[] = [];
const variants: StaticVariant[] = [];
let isArbitraryProperty = false;

const extractVariant = (): Variant | "NO_VARIANT" | undefined => {
const extractVariant = (): StaticVariant | "NO_VARIANT" | undefined => {
important = tokenWithoutVariants.startsWith("!");
if (important) tokenWithoutVariants = tokenWithoutVariants.slice(1);
if (tokenWithoutVariants.startsWith("[")) {
Expand Down Expand Up @@ -219,23 +223,38 @@ export const initDownwindWithConfig = ({
return undefined;
} else if (important) {
return "NO_VARIANT"; // Using ! prefix is not valid for variant
} else if (tokenWithoutVariants.startsWith("supports-[")) {
const index = tokenWithoutVariants.indexOf("]:");
if (index === -1) return;
const content = tokenWithoutVariants.slice(10, index);
tokenWithoutVariants = tokenWithoutVariants.slice(index + 2);
const check = content.includes(":") ? content : `${content}: var(--tw)`;
return {
type: "atRule",
order: Infinity,
condition: `@supports (${check})`,
};
}
const index = tokenWithoutVariants.indexOf(":");
if (index === -1) return "NO_VARIANT";
const prefix = tokenWithoutVariants.slice(0, index);
tokenWithoutVariants = tokenWithoutVariants.slice(index + 1);
return variantsMap.get(prefix);
const dynamicIndex = tokenWithoutVariants.indexOf("-[");
// -[ can be for arbitrary values
if (dynamicIndex === -1 || dynamicIndex > index) {
const prefix = tokenWithoutVariants.slice(0, index);
tokenWithoutVariants = tokenWithoutVariants.slice(index + 1);
return staticVariantsMap.get(prefix);
}
const endIndex = tokenWithoutVariants.indexOf("]:");
if (endIndex === -1) return;
const dynamicPrefix = tokenWithoutVariants.slice(0, dynamicIndex);
const dynamicVariant = dynamicVariantsMap.get(dynamicPrefix);
if (!dynamicVariant) return;
const content = tokenWithoutVariants
.slice(dynamicIndex + 2, endIndex)
.replaceAll("_", " ");
tokenWithoutVariants = tokenWithoutVariants.slice(endIndex + 2);
switch (dynamicVariant.type) {
case "dynamicAtRule":
return {
type: "atRule",
order: dynamicVariant.order,
condition: dynamicVariant.get(content),
};
case "dynamicSelectorRewrite":
return {
type: "selectorRewrite",
selectorRewrite: dynamicVariant.get(content),
};
}
};

let variant = extractVariant();
Expand Down Expand Up @@ -471,7 +490,7 @@ export const initDownwindWithConfig = ({
const hasScreen = content.includes("screen(");
if (hasScreen) {
content = content.replaceAll(screenRE, (substring, value: string) => {
const variant = variantsMap.get(value);
const variant = staticVariantsMap.get(value);
if (variant === undefined) {
throw new DownwindError(
`No variant matching "${value}"`,
Expand Down
12 changes: 12 additions & 0 deletions src/theme/getBaseTheme.ts
Expand Up @@ -85,6 +85,17 @@ export const getBaseTheme = (): DownwindTheme => ({
pulse: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
bounce: "bounce 1s infinite",
},
aria: {
busy: 'busy="true"',
checked: 'checked="true"',
disabled: 'disabled="true"',
expanded: 'expanded="true"',
hidden: 'hidden="true"',
pressed: 'pressed="true"',
readonly: 'readonly="true"',
required: 'required="true"',
selected: 'selected="true"',
},
aspectRatio: {
auto: "auto",
square: "1 / 1",
Expand Down Expand Up @@ -751,6 +762,7 @@ export const getBaseTheme = (): DownwindTheme => ({
2: "2",
},
supports: {},
data: {},
textColor: (theme) => theme("colors"),
textDecorationColor: (theme) => theme("colors"),
textDecorationThickness: {
Expand Down
2 changes: 2 additions & 0 deletions src/types.d.ts
Expand Up @@ -161,6 +161,7 @@ type ThemeKey =
| "columns"
| "spacing"
| "animation"
| "aria"
| "aspectRatio"
| "backdropBlur"
| "backdropBrightness"
Expand Down Expand Up @@ -255,6 +256,7 @@ type ThemeKey =
| "stroke"
| "strokeWidth"
| "supports"
| "data"
| "textColor"
| "textDecorationColor"
| "textDecorationThickness"
Expand Down
4 changes: 2 additions & 2 deletions src/utils/print.ts
@@ -1,6 +1,6 @@
import type { ResolvedConfig } from "../resolveConfig.ts";
import type { Container, CSSEntries, RuleMeta } from "../types.d.ts";
import type { Variant } from "../variants.ts";
import type { StaticVariant } from "../variants.ts";

export const printBlock = (
selector: string,
Expand Down Expand Up @@ -57,7 +57,7 @@ export const arbitraryPropertyMatchToLine = (match: {

export const applyVariants = (
selector: string,
variants: Variant[],
variants: StaticVariant[],
meta: RuleMeta | undefined,
) => {
let hasAtRule = false;
Expand Down
8 changes: 5 additions & 3 deletions src/utils/regex.ts
Expand Up @@ -8,12 +8,14 @@ export const escapeSelector = (selector: string) => {
};

const regularVariant = /[a-z0-9][a-z0-9-]+/;
const dynamicVariant = /[a-z]+-\[[a-z:-]+]/;
// ="'_ for attributes: aria-[labelledby='a_b']
// : for supports query
const dynamicVariant = /[a-z-]+-\[[a-z0-9="'_:-]+]/;
// & to position the selector
// []=" for attributes: [&[type="email"]]
// []="' for attributes: [&[type="email"]]
// :>+*~.()_ for css selectors: [&:nth-child(3)] [&_p] [&>*] [.sidebar+&]
// @ for media: [@media(min-width:900px)]
const arbitraryVariant = /\[[a-z0-9&[\]=":>+*~.()_@-]+]/;
const arbitraryVariant = /\[[a-z0-9&[\]="':>+*~.()_@-]+]/;
const variant = new RegExp(
`(?:${regularVariant.source}|${dynamicVariant.source}|${arbitraryVariant.source}):`,
);
Expand Down

0 comments on commit bf44db3

Please sign in to comment.