aliaset
Version:
twind monorepo
323 lines (259 loc) • 9.65 kB
text/typescript
import type { BaseTheme, Context } from '../types'
import type { ParsedRule } from '../parse'
import { asArray, mql } from '../utils'
import { toClassName } from './to-class-name'
// Based on https://github.com/kripod/otion
// License MIT
// export const enum Shifts {
// darkMode = 30,
// layer = 27,
// screens = 26,
// responsive = 22,
// atRules = 18,
// variants = 0,
// }
export const Layer = {
/**
* 1. `default` (public)
*/
d /* efaults */: 0b000 << 27 /* Shifts.layer */,
/**
* 2. `base` (public) — for things like reset rules or default styles applied to plain HTML elements.
*/
b /* ase */: 0b001 << 27 /* Shifts.layer */,
/**
* 3. `components` (public, used by `style()`) — is for class-based styles that you want to be able to override with utilities.
*/
c /* omponents */: 0b010 << 27 /* Shifts.layer */,
// reserved for style():
// - props: 0b011
// - when: 0b100
/**
* 6. `aliases` (public, used by `apply()`) — `~(...)`
*/
a /* liases */: 0b101 << 27 /* Shifts.layer */,
/**
* 6. `utilities` (public) — for small, single-purpose classes
*/
u /* tilities */: 0b110 << 27 /* Shifts.layer */,
/**
* 7. `overrides` (public, used by `css()`)
*/
o /* verrides */: 0b111 << 27 /* Shifts.layer */,
} as const
/*
To have a predictable styling the styles must be ordered.
This order is represented by a precedence number. The lower values
are inserted before higher values. Meaning higher precedence styles
overwrite lower precedence styles.
Each rule has some traits that are put into a bit set which form
the precedence:
| bits | trait |
| ---- | ---------------------------------------------------- |
| 1 | dark mode |
| 2 | layer: preflight, global, components, utilities, css |
| 1 | screens: is this a responsive variation of a rule |
| 5 | responsive based on min-width |
| 4 | at-rules |
| 18 | pseudo and group variants |
| 4 | number of declarations (descending) |
| 4 | greatest precedence of properties |
**Dark Mode: 1 bit**
Flag for dark mode rules.
**Layer: 3 bits**
- defaults = 0: The preflight styles and any base styles registered by plugins.
- base = 1: The global styles registered by plugins.
- components = 2
- variants = 3
- compounds = 4
- aliases = 5
- utilities = 6: Utility classes and any utility classes registered by plugins.
- css = 7: Styles generated by css
**Screens: 1 bit**
Flag for screen variants. They may not always have a `min-width` to be detected by _Responsive_ below.
**Responsive: 4 bits**
Based on extracted `min-width` value:
- 576px -> 3
- 1536px -> 10
- 36rem -> 3
- 96rem -> 9
**At-Rules: 4 bits**
Based on the count of special chars (`-:,`) within the at-rule.
**Pseudo and group variants: 18 bits**
Ensures predictable order of pseudo classes.
- https://bitsofco.de/when-do-the-hover-focus-and-active-pseudo-classes-apply/#orderofstyleshoverthenfocusthenactive
- https://developer.mozilla.org/docs/Web/CSS/:active#Active_links
- https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js#L718
**Number of declarations (descending): 4 bits**
Allows single declaration styles to overwrite styles from multi declaration styles.
**Greatest precedence of properties: 4 bits**
Ensure shorthand properties are inserted before longhand properties; eg longhand override shorthand
*/
export function moveToLayer(precedence: number, layer: number): number {
// Set layer (first reset, than set)
return (precedence & ~Layer.o) | layer
}
/*
To set a bit: n |= mask;
To clear a bit: n &= ~mask;
To test if a bit is set: (n & mask)
Bit shifts for the primary bits:
| bits | trait | shift |
| ---- | ------------------------------------------------------- | ----- |
| 1 | dark mode | 30 |
| 3 | layer: preflight, global, components, utilities, css | 27 |
| 1 | screens: is this a responsive variation of a rule | 26 |
| 4 | responsive based on min-width, max-width or width | 22 |
| 4 | at-rules | 18 |
| 18 | pseudo and group variants | 0 |
Layer: 0 - 7: 3 bits
- defaults: 0 << 27
- base: 1 << 27
- components: 2 << 27
- variants: 3 << 27
- joints: 4 << 27
- aliases: 5 << 27
- utilities: 6 << 27
- overrides: 7 << 27
These are calculated by serialize and added afterwards:
| bits | trait |
| ---- | ----------------------------------- |
| 4 | number of selectors (descending) |
| 4 | number of declarations (descending) |
| 4 | greatest precedence of properties |
These are added by shifting the primary bits using multiplication as js only
supports bit shift up to 32 bits.
*/
// Colon and dash count of string (ascending)
export function seperatorPrecedence(string: string): number {
return string.match(/[-=:;]/g)?.length || 0
}
export function atRulePrecedence(css: string): number {
// 0=none, 1=sm, 2=md, 3=lg, 4=xl, 5=2xl, 6=??, 7=??
// 0 - 15: 4 bits (max 150rem or 2250px)
// 576px -> 3
// 1536px -> 10
// 36rem -> 3
// 96rem -> 9
return (
(Math.min(
/(?:^|width[^\d]+)(\d+(?:.\d+)?)(p)?/.test(css) ? +RegExp.$1 / (RegExp.$2 ? 15 : 1) / 10 : 0,
15,
) <<
22) /* Shifts.responsive */ |
(Math.min(seperatorPrecedence(css), 15) << 18) /* Shifts.atRules */
)
}
// Pesudo variant presedence
// Chars 3 - 8: Uniquely identifies a pseudo selector
// represented as a bit set for each relevant value
// 18 bits: one for each variant plus one for unknown variants
//
// ':group-*' variants are normalized to their native pseudo class (':group-hover' -> ':hover')
// as they already have a higher selector presedence due to the add '.group' ('.group:hover .group-hover:...')
// Sources:
// - https://bitsofco.de/when-do-the-hover-focus-and-active-pseudo-classes-apply/#orderofstyleshoverthenfocusthenactive
// - https://developer.mozilla.org/docs/Web/CSS/:active#Active_links
// - https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js#L931
const PRECEDENCES_BY_PSEUDO_CLASS = [
/* fi */ 'rst-c' /* hild: 0 */,
/* la */ 'st-ch' /* ild: 1 */,
// even and odd use: nth-child
/* nt */ 'h-chi' /* ld: 2 */,
/* an */ 'y-lin' /* k: 3 */,
/* li */ 'nk' /* : 4 */,
/* vi */ 'sited' /* : 5 */,
/* ch */ 'ecked' /* : 6 */,
/* em */ 'pty' /* : 7 */,
/* re */ 'ad-on' /* ly: 8 */,
/* fo */ 'cus-w' /* ithin : 9 */,
/* ho */ 'ver' /* : 10 */,
/* fo */ 'cus' /* : 11 */,
/* fo */ 'cus-v' /* isible : 12 */,
/* ac */ 'tive' /* : 13 */,
/* di */ 'sable' /* d : 14 */,
/* op */ 'tiona' /* l: 15 */,
/* re */ 'quire' /* d: 16 */,
]
function pseudoPrecedence(selector: string): number {
// use first found pseudo-class
return (
1 <<
~(
(/:([a-z-]+)/.test(selector) &&
~PRECEDENCES_BY_PSEUDO_CLASS.indexOf(RegExp.$1.slice(2, 7))) ||
~17
)
)
}
// https://github.com/kripod/otion/blob/main/packages/otion/src/propertyMatchers.ts
// "+1": [
// /* ^border-.*(w|c|sty) */
// "border-.*(width,color,style)",
// /* ^[tlbr].{2,4}m?$ */
// "top",
// "left",
// "bottom",
// "right",
// /* ^c.{7}$ */
// "continue",
// ],
// "-1": [
// /* ^[fl].{5}l */
// "flex-flow",
// "line-clamp",
// /* ^g.{8}$ */
// "grid-area",
// /* ^pl */
// "place-content",
// "place-items",
// "place-self",
// ],
// group: 1 => +1
// group: 2 => -1
// 0 - 15 => 4 bits
// Ignore vendor prefixed and custom properties
export function declarationPropertyPrecedence(property: string): number {
return property[0] == '-'
? 0
: seperatorPrecedence(property) +
(/^(?:(border-(?!w|c|sty)|[tlbr].{2,4}m?$|c.{7}$)|([fl].{5}l|g.{8}$|pl))/.test(property)
? +!!RegExp.$1 /* +1 */ || -!!RegExp.$2 /* -1 */
: 0) +
1
}
export interface ConvertedRule {
/** The name to use for `&` expansion in selectors. Maybe empty for at-rules like `@import`, `@font-face`, `@media`, ... */
n?: string | undefined
/** The calculated precedence taking all variants into account. */
p: number
/** The rulesets (selectors and at-rules). expanded variants `@media ...`, `@supports ...`, `&:focus`, `.dark &` */
r?: string[]
/** Is this rule `!important` eg something like `!underline` or `!bg-red-500` or `!red-500` */
i?: boolean | undefined
}
export function convert<Theme extends BaseTheme = BaseTheme>(
{ n: name, i: important, v: variants = [] }: Partial<ParsedRule>,
context: Context<Theme>,
precedence: number,
conditions?: string[],
): ConvertedRule {
if (name) {
name = toClassName({ n: name, i: important, v: variants })
}
conditions = [...asArray(conditions)]
for (const variant of variants) {
const screen = context.theme('screens', variant)
for (const condition of asArray((screen && mql(screen)) || context.v(variant))) {
conditions.push(condition)
precedence |= screen
? (1 << 26) /* Shifts.screens */ | atRulePrecedence(condition)
: variant == 'dark'
? 1 << 30 /* Shifts.darkMode */
: condition[0] == '@'
? atRulePrecedence(condition)
: pseudoPrecedence(condition)
}
}
return { n: name, p: precedence, r: conditions, i: important }
}