@birdycom/omniacss-migrate 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +129 -129
- package/bin/omniacss-migrate.mjs +8 -8
- package/package.json +61 -61
- package/scripts/build-color-map.mjs +31 -31
- package/scripts/extract-palette.mjs +13 -13
- package/scripts/tailwind-palette.json +290 -290
- package/src/bootstrap/compat.mjs +72 -72
- package/src/bootstrap/convert.mjs +102 -102
- package/src/bootstrap/migrate.mjs +259 -259
- package/src/bootstrap/rules.mjs +212 -212
- package/src/cli.mjs +139 -139
- package/src/codemod.mjs +82 -82
- package/src/core.mjs +67 -67
- package/src/data/colors.json +290 -290
- package/src/extract.mjs +91 -91
- package/src/index.mjs +12 -12
- package/src/native.mjs +72 -72
- package/src/postcss.mjs +308 -308
- package/src/preflight.mjs +52 -52
- package/src/project.mjs +194 -194
- package/src/resolve.mjs +220 -220
- package/src/scan.mjs +106 -106
- package/src/setup.mjs +205 -205
- package/src/utilities.mjs +713 -713
- package/src/values.mjs +184 -184
- package/src/variants.mjs +189 -189
- package/src/vite.mjs +81 -81
package/src/resolve.mjs
CHANGED
|
@@ -1,220 +1,220 @@
|
|
|
1
|
-
// File: packages/migrate/src/resolve.mjs
|
|
2
|
-
// Resolver: a Tailwind-style class token (with variants, !important, negatives, modifiers) -> CSS rules
|
|
3
|
-
// that only use OmniaCSS tokens. Entry points: createContext(), resolveToken(), compileTokens().
|
|
4
|
-
// Author: Serge Cools | Framework: OmniaCSS
|
|
5
|
-
|
|
6
|
-
import { applyVariant, esc, splitTop } from "./variants.mjs";
|
|
7
|
-
import { HANDLERS, KEYFRAMES, MARKERS, STATIC } from "./utilities.mjs";
|
|
8
|
-
import { arb, normalizeArb } from "./values.mjs";
|
|
9
|
-
|
|
10
|
-
export const SCREENS = {
|
|
11
|
-
omnia: { sm: "36rem", md: "50rem", lg: "64rem", xl: "80rem", "2xl": "96rem" },
|
|
12
|
-
tailwind: { sm: "40rem", md: "48rem", lg: "64rem", xl: "80rem", "2xl": "96rem" },
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Build the resolver context.
|
|
17
|
-
* @param {object} [o]
|
|
18
|
-
* @param {boolean} [o.v4=true] Tailwind 4 naming (shadow-sm, rounded-sm, ring …)
|
|
19
|
-
* @param {"class"|"media"} [o.darkMode="class"]
|
|
20
|
-
* @param {string} [o.darkSelector=".dark"] selector for class based dark mode
|
|
21
|
-
* @param {"omnia"|"tailwind"} [o.breakpoints="omnia"]
|
|
22
|
-
* @param {Record<string,string>} [o.screens] extra/override screens
|
|
23
|
-
* @param {"nearest"|"keep"} [o.arbitraryColors="nearest"]
|
|
24
|
-
* @param {Iterable<string>} [o.themeVars] custom properties defined in the user's @theme (e.g. "--color-primary")
|
|
25
|
-
* @param {Record<string,string>} [o.customColors] extra color names -> css value (from tailwind.config)
|
|
26
|
-
* @param {string} [o.prefix] Tailwind `prefix` option
|
|
27
|
-
*/
|
|
28
|
-
export function createContext(o = {}) {
|
|
29
|
-
const screens = { ...SCREENS[o.breakpoints || "omnia"], ...(o.screens || {}) };
|
|
30
|
-
const order = Object.keys(screens).sort((a, b) => parseFloat(screens[a]) - parseFloat(screens[b]));
|
|
31
|
-
const themeVars = new Set(o.themeVars || []);
|
|
32
|
-
return {
|
|
33
|
-
v4: o.v4 ?? true,
|
|
34
|
-
darkMode: o.darkMode || "class",
|
|
35
|
-
darkSelector: o.darkSelector || ".dark",
|
|
36
|
-
screens,
|
|
37
|
-
screenRank: (n) => order.indexOf(n) * 10,
|
|
38
|
-
arbitraryColors: o.arbitraryColors || "nearest",
|
|
39
|
-
themeVars,
|
|
40
|
-
hasTheme: (n) => themeVars.has(n),
|
|
41
|
-
customColors: new Map(Object.entries(o.customColors || {})),
|
|
42
|
-
prefix: o.prefix || "",
|
|
43
|
-
container: o.container || {},
|
|
44
|
-
tokensFile: o.tokensFile,
|
|
45
|
-
important: !!o.important,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Roots that accept a leading `-` (negative values). */
|
|
50
|
-
const NEG_OK = /^(m[xytrblse]?|scroll-m[xytrblse]?|inset(-[xy])?|top|right|bottom|left|start|end|z|order|translate(-[xyz])?|rotate(-[xy])?|scale(-[xy])?|skew(-[xy])?|hue-rotate|backdrop-hue-rotate|tracking|indent|underline-offset|outline-offset|col(-start|-end)?|row(-start|-end)?|space-[xy]|spin-in|spin-out|bg)$/;
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------------------------- parsing
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Parse a raw class token.
|
|
56
|
-
* @returns {{ raw: string, variants: string[], utility: string, important: boolean, neg: boolean, body: string }}
|
|
57
|
-
*/
|
|
58
|
-
export function parseToken(raw, ctx) {
|
|
59
|
-
const parts = splitTop(raw, ":");
|
|
60
|
-
const variants = parts.slice(0, -1);
|
|
61
|
-
let utility = parts[parts.length - 1];
|
|
62
|
-
let important = false, neg = false;
|
|
63
|
-
if (utility.startsWith("!")) { important = true; utility = utility.slice(1); }
|
|
64
|
-
if (utility.endsWith("!")) { important = true; utility = utility.slice(0, -1); }
|
|
65
|
-
if (ctx?.prefix) {
|
|
66
|
-
const p = ctx.prefix;
|
|
67
|
-
if (utility.startsWith("-" + p)) utility = "-" + utility.slice(p.length + 1);
|
|
68
|
-
else if (utility.startsWith(p)) utility = utility.slice(p.length);
|
|
69
|
-
else if (utility.startsWith("[") === false && !MARKERS.test(utility)) return { raw, variants, utility, important, neg, body: utility, foreign: true };
|
|
70
|
-
}
|
|
71
|
-
if (utility.startsWith("-") && utility.length > 1) { neg = true; utility = utility.slice(1); }
|
|
72
|
-
return { raw, variants, utility, important, neg, body: utility };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Split `a-b-[c-d]` on dashes outside brackets. */
|
|
76
|
-
function splitDash(s) {
|
|
77
|
-
return splitTop(s, "-");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Split `bg-red-500/50` into [body, modifier]; ignores slashes inside brackets. */
|
|
81
|
-
function splitModifier(body) {
|
|
82
|
-
const parts = splitTop(body, "/");
|
|
83
|
-
if (parts.length < 2) return [body, undefined];
|
|
84
|
-
return [parts.slice(0, -1).join("/"), parts[parts.length - 1]];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const norm = (r) => (!r ? null : Array.isArray(r) ? { decls: r } : r);
|
|
88
|
-
|
|
89
|
-
/** Resolve the utility part (no variants). Returns { decls, tail?, keyframes?, extraRules?, marker? } or null. */
|
|
90
|
-
export function resolveUtility(p, ctx) {
|
|
91
|
-
const { body, neg } = p;
|
|
92
|
-
if (!neg) {
|
|
93
|
-
const m = body.match(/^\[(--[A-Za-z0-9_-]+|-?[a-z][a-z-]*):([^:\s].*)\]$/);
|
|
94
|
-
if (m && !/^[\s;{}]/.test(m[2])) return { decls: [[m[1], normalizeArb(m[2])]] };
|
|
95
|
-
if (MARKERS.test(body)) return { decls: [], marker: true };
|
|
96
|
-
if (Object.hasOwn(STATIC, body)) return { decls: STATIC[body], static: true, ...(body.startsWith("animate-in") || body.startsWith("animate-out") ? { keyframes: [body.slice(8)] } : {}) };
|
|
97
|
-
if (body === "container") return containerRule(ctx);
|
|
98
|
-
}
|
|
99
|
-
const [bare, mod] = splitModifier(body);
|
|
100
|
-
const words = splitDash(bare);
|
|
101
|
-
for (let i = words.length; i >= 1; i--) {
|
|
102
|
-
const root = words.slice(0, i).join("-");
|
|
103
|
-
const fn = Object.hasOwn(HANDLERS, root) ? HANDLERS[root] : null;
|
|
104
|
-
if (!fn) continue;
|
|
105
|
-
if (neg && !NEG_OK.test(root)) continue;
|
|
106
|
-
const value = words.slice(i).join("-");
|
|
107
|
-
if (!value && i === words.length && false) continue;
|
|
108
|
-
// `w-1/2`, `translate-x-1/2`: try the fraction first, then value + opacity/line-height modifier
|
|
109
|
-
let r = mod !== undefined ? norm(fn({ root, value: `${value}/${mod}`, mod: undefined, neg, ctx })) : null;
|
|
110
|
-
if (!r) r = norm(fn({ root, value, mod, neg, ctx }));
|
|
111
|
-
if (r) return r;
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function containerRule(ctx) {
|
|
117
|
-
const decls = [["width", "100%"]];
|
|
118
|
-
if (ctx.container.center) decls.push(["margin-inline", "auto"]);
|
|
119
|
-
if (ctx.container.padding) decls.push(["padding-inline", ctx.container.padding]);
|
|
120
|
-
const extraRules = Object.entries(ctx.screens).map(([k, w]) => ({ media: [`@media (min-width: ${w})`], decls: [["max-width", w]], rank: ctx.screenRank(k) }));
|
|
121
|
-
return { decls, extraRules };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// property rank so shorthands sort before longhands (`p-4` before `px-2` before `pl-1`)
|
|
125
|
-
const SHORTHANDS = new Set(["margin", "padding", "inset", "gap", "border-width", "border-color", "border-radius", "border", "overflow", "scroll-margin", "scroll-padding", "transition-property", "flex", "background", "font", "outline", "translate", "rotate", "scale", "size"]);
|
|
126
|
-
function propRank(prop) {
|
|
127
|
-
if (SHORTHANDS.has(prop)) return 0;
|
|
128
|
-
if (/-(top|right|bottom|left|start|end)\b|^(top|right|bottom|left)$|-(x|y)$/.test(prop)) return 2;
|
|
129
|
-
return 1;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Resolve one token.
|
|
134
|
-
* @param {string} raw class token, e.g. "md:hover:bg-red-500/50"
|
|
135
|
-
* @param {object} ctx from createContext()
|
|
136
|
-
* @param {{ selector?: string }} [opts] selector: use instead of `.escaped-token` (for @apply)
|
|
137
|
-
* @returns {null | { token: string, rules: Array<{selectors:string[], media:string[], decls:string[][], rank:number}>, keyframes: string[], marker?: boolean }}
|
|
138
|
-
*/
|
|
139
|
-
export function resolveToken(raw, ctx, opts = {}) {
|
|
140
|
-
const p = parseToken(raw, ctx);
|
|
141
|
-
if (p.foreign || !p.body) return null;
|
|
142
|
-
const util = resolveUtility(p, ctx);
|
|
143
|
-
if (!util) return null;
|
|
144
|
-
if (util.marker) return { token: raw, rules: [], keyframes: [], marker: true };
|
|
145
|
-
|
|
146
|
-
const st = { sels: [opts.selector || `.${esc(raw)}`], media: [], prelude: [], rank: 0 };
|
|
147
|
-
for (let i = p.variants.length - 1; i >= 0; i--) {
|
|
148
|
-
if (!applyVariant(p.variants[i], st, ctx)) return null;
|
|
149
|
-
}
|
|
150
|
-
// a pseudo-element can only end a selector: `[&_.x]:placeholder:italic` would give `::placeholder .x` (invalid, dropped)
|
|
151
|
-
if (st.sels.some((s) => /::[a-z-]+(?:\([^)]*\))?[\s>+~.[#]/.test(s))) return null;
|
|
152
|
-
const important = p.important || ctx.important;
|
|
153
|
-
const fix = (decls) => decls.map(([k, v]) => [k, important && !/ !important$/.test(v) ? `${v} !important` : v]);
|
|
154
|
-
const hasContent = util.decls.some(([k]) => k === "content");
|
|
155
|
-
const base = fix([...(hasContent ? [] : st.prelude), ...util.decls]);
|
|
156
|
-
const urank = propRank((util.decls[0] || [""])[0]);
|
|
157
|
-
const tail = util.tail || "";
|
|
158
|
-
const selectors = st.sels.map((s) => s + tail);
|
|
159
|
-
const rules = [];
|
|
160
|
-
if (base.length) rules.push({ selectors, media: st.media, decls: base, rank: st.rank, urank });
|
|
161
|
-
for (const x of util.extraRules || []) rules.push({ selectors, media: [...st.media, ...x.media], decls: fix(x.decls), rank: st.rank + 1 + (x.rank || 0), urank });
|
|
162
|
-
return { token: raw, rules, keyframes: util.keyframes || [] };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ---------------------------------------------------------------------------------------------- compile
|
|
166
|
-
|
|
167
|
-
const fmtDecls = (decls, indent) => decls.map(([k, v]) => `${indent}${k}: ${v};`).join("\n");
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Compile a list of resolved tokens into one stylesheet (ordered like a utility framework: base < pseudo < dark < responsive).
|
|
171
|
-
* @param {Array<ReturnType<typeof resolveToken>>} resolved
|
|
172
|
-
*/
|
|
173
|
-
export function serialize(resolved) {
|
|
174
|
-
const rules = [];
|
|
175
|
-
const frames = new Set();
|
|
176
|
-
for (const r of resolved) {
|
|
177
|
-
if (!r) continue;
|
|
178
|
-
r.keyframes.forEach((k) => frames.add(k));
|
|
179
|
-
for (const rule of r.rules) rules.push({ ...rule, token: r.token });
|
|
180
|
-
}
|
|
181
|
-
rules.sort((a, b) => a.rank - b.rank || a.urank - b.urank || (a.token < b.token ? -1 : a.token > b.token ? 1 : 0));
|
|
182
|
-
const renderRule = (r, ind = "") => `${r.selectors.map((x) => ind + x).join(",\n")} {\n${fmtDecls(r.decls, ind + " ")}\n${ind}}`;
|
|
183
|
-
const out = [];
|
|
184
|
-
for (const k of frames) if (KEYFRAMES[k]) out.push(KEYFRAMES[k]);
|
|
185
|
-
let group = null;
|
|
186
|
-
const flush = () => {
|
|
187
|
-
if (!group) return;
|
|
188
|
-
let inner = group.rules.map((r) => renderRule(r, " ".repeat(group.media.length))).join("\n");
|
|
189
|
-
for (let i = group.media.length - 1; i >= 0; i--) {
|
|
190
|
-
const ind = " ".repeat(i);
|
|
191
|
-
inner = `${ind}${group.media[i]} {\n${inner}\n${ind}}`;
|
|
192
|
-
}
|
|
193
|
-
out.push(inner);
|
|
194
|
-
group = null;
|
|
195
|
-
};
|
|
196
|
-
for (const rule of rules) {
|
|
197
|
-
if (!rule.media.length) { flush(); out.push(renderRule(rule)); continue; }
|
|
198
|
-
const key = rule.media.join("|");
|
|
199
|
-
if (!group || group.key !== key) { flush(); group = { key, media: rule.media, rules: [] }; }
|
|
200
|
-
group.rules.push(rule);
|
|
201
|
-
}
|
|
202
|
-
flush();
|
|
203
|
-
return out.join("\n");
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Resolve many tokens and report what could not be resolved. */
|
|
207
|
-
export function compileTokens(tokens, ctx) {
|
|
208
|
-
const resolved = [], unsupported = [];
|
|
209
|
-
for (const t of tokens) {
|
|
210
|
-
const r = resolveToken(t, ctx);
|
|
211
|
-
if (r) resolved.push(r); else unsupported.push(t);
|
|
212
|
-
}
|
|
213
|
-
return { css: serialize(resolved), resolved, unsupported };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Convenience: one token -> css string (or null). */
|
|
217
|
-
export function compileToken(raw, ctx = createContext()) {
|
|
218
|
-
const r = resolveToken(raw, ctx);
|
|
219
|
-
return r ? serialize([r]) : null;
|
|
220
|
-
}
|
|
1
|
+
// File: packages/migrate/src/resolve.mjs
|
|
2
|
+
// Resolver: a Tailwind-style class token (with variants, !important, negatives, modifiers) -> CSS rules
|
|
3
|
+
// that only use OmniaCSS tokens. Entry points: createContext(), resolveToken(), compileTokens().
|
|
4
|
+
// Author: Serge Cools | Framework: OmniaCSS
|
|
5
|
+
|
|
6
|
+
import { applyVariant, esc, splitTop } from "./variants.mjs";
|
|
7
|
+
import { HANDLERS, KEYFRAMES, MARKERS, STATIC } from "./utilities.mjs";
|
|
8
|
+
import { arb, normalizeArb } from "./values.mjs";
|
|
9
|
+
|
|
10
|
+
export const SCREENS = {
|
|
11
|
+
omnia: { sm: "36rem", md: "50rem", lg: "64rem", xl: "80rem", "2xl": "96rem" },
|
|
12
|
+
tailwind: { sm: "40rem", md: "48rem", lg: "64rem", xl: "80rem", "2xl": "96rem" },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build the resolver context.
|
|
17
|
+
* @param {object} [o]
|
|
18
|
+
* @param {boolean} [o.v4=true] Tailwind 4 naming (shadow-sm, rounded-sm, ring …)
|
|
19
|
+
* @param {"class"|"media"} [o.darkMode="class"]
|
|
20
|
+
* @param {string} [o.darkSelector=".dark"] selector for class based dark mode
|
|
21
|
+
* @param {"omnia"|"tailwind"} [o.breakpoints="omnia"]
|
|
22
|
+
* @param {Record<string,string>} [o.screens] extra/override screens
|
|
23
|
+
* @param {"nearest"|"keep"} [o.arbitraryColors="nearest"]
|
|
24
|
+
* @param {Iterable<string>} [o.themeVars] custom properties defined in the user's @theme (e.g. "--color-primary")
|
|
25
|
+
* @param {Record<string,string>} [o.customColors] extra color names -> css value (from tailwind.config)
|
|
26
|
+
* @param {string} [o.prefix] Tailwind `prefix` option
|
|
27
|
+
*/
|
|
28
|
+
export function createContext(o = {}) {
|
|
29
|
+
const screens = { ...SCREENS[o.breakpoints || "omnia"], ...(o.screens || {}) };
|
|
30
|
+
const order = Object.keys(screens).sort((a, b) => parseFloat(screens[a]) - parseFloat(screens[b]));
|
|
31
|
+
const themeVars = new Set(o.themeVars || []);
|
|
32
|
+
return {
|
|
33
|
+
v4: o.v4 ?? true,
|
|
34
|
+
darkMode: o.darkMode || "class",
|
|
35
|
+
darkSelector: o.darkSelector || ".dark",
|
|
36
|
+
screens,
|
|
37
|
+
screenRank: (n) => order.indexOf(n) * 10,
|
|
38
|
+
arbitraryColors: o.arbitraryColors || "nearest",
|
|
39
|
+
themeVars,
|
|
40
|
+
hasTheme: (n) => themeVars.has(n),
|
|
41
|
+
customColors: new Map(Object.entries(o.customColors || {})),
|
|
42
|
+
prefix: o.prefix || "",
|
|
43
|
+
container: o.container || {},
|
|
44
|
+
tokensFile: o.tokensFile,
|
|
45
|
+
important: !!o.important,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Roots that accept a leading `-` (negative values). */
|
|
50
|
+
const NEG_OK = /^(m[xytrblse]?|scroll-m[xytrblse]?|inset(-[xy])?|top|right|bottom|left|start|end|z|order|translate(-[xyz])?|rotate(-[xy])?|scale(-[xy])?|skew(-[xy])?|hue-rotate|backdrop-hue-rotate|tracking|indent|underline-offset|outline-offset|col(-start|-end)?|row(-start|-end)?|space-[xy]|spin-in|spin-out|bg)$/;
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------------------------- parsing
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse a raw class token.
|
|
56
|
+
* @returns {{ raw: string, variants: string[], utility: string, important: boolean, neg: boolean, body: string }}
|
|
57
|
+
*/
|
|
58
|
+
export function parseToken(raw, ctx) {
|
|
59
|
+
const parts = splitTop(raw, ":");
|
|
60
|
+
const variants = parts.slice(0, -1);
|
|
61
|
+
let utility = parts[parts.length - 1];
|
|
62
|
+
let important = false, neg = false;
|
|
63
|
+
if (utility.startsWith("!")) { important = true; utility = utility.slice(1); }
|
|
64
|
+
if (utility.endsWith("!")) { important = true; utility = utility.slice(0, -1); }
|
|
65
|
+
if (ctx?.prefix) {
|
|
66
|
+
const p = ctx.prefix;
|
|
67
|
+
if (utility.startsWith("-" + p)) utility = "-" + utility.slice(p.length + 1);
|
|
68
|
+
else if (utility.startsWith(p)) utility = utility.slice(p.length);
|
|
69
|
+
else if (utility.startsWith("[") === false && !MARKERS.test(utility)) return { raw, variants, utility, important, neg, body: utility, foreign: true };
|
|
70
|
+
}
|
|
71
|
+
if (utility.startsWith("-") && utility.length > 1) { neg = true; utility = utility.slice(1); }
|
|
72
|
+
return { raw, variants, utility, important, neg, body: utility };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Split `a-b-[c-d]` on dashes outside brackets. */
|
|
76
|
+
function splitDash(s) {
|
|
77
|
+
return splitTop(s, "-");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Split `bg-red-500/50` into [body, modifier]; ignores slashes inside brackets. */
|
|
81
|
+
function splitModifier(body) {
|
|
82
|
+
const parts = splitTop(body, "/");
|
|
83
|
+
if (parts.length < 2) return [body, undefined];
|
|
84
|
+
return [parts.slice(0, -1).join("/"), parts[parts.length - 1]];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const norm = (r) => (!r ? null : Array.isArray(r) ? { decls: r } : r);
|
|
88
|
+
|
|
89
|
+
/** Resolve the utility part (no variants). Returns { decls, tail?, keyframes?, extraRules?, marker? } or null. */
|
|
90
|
+
export function resolveUtility(p, ctx) {
|
|
91
|
+
const { body, neg } = p;
|
|
92
|
+
if (!neg) {
|
|
93
|
+
const m = body.match(/^\[(--[A-Za-z0-9_-]+|-?[a-z][a-z-]*):([^:\s].*)\]$/);
|
|
94
|
+
if (m && !/^[\s;{}]/.test(m[2])) return { decls: [[m[1], normalizeArb(m[2])]] };
|
|
95
|
+
if (MARKERS.test(body)) return { decls: [], marker: true };
|
|
96
|
+
if (Object.hasOwn(STATIC, body)) return { decls: STATIC[body], static: true, ...(body.startsWith("animate-in") || body.startsWith("animate-out") ? { keyframes: [body.slice(8)] } : {}) };
|
|
97
|
+
if (body === "container") return containerRule(ctx);
|
|
98
|
+
}
|
|
99
|
+
const [bare, mod] = splitModifier(body);
|
|
100
|
+
const words = splitDash(bare);
|
|
101
|
+
for (let i = words.length; i >= 1; i--) {
|
|
102
|
+
const root = words.slice(0, i).join("-");
|
|
103
|
+
const fn = Object.hasOwn(HANDLERS, root) ? HANDLERS[root] : null;
|
|
104
|
+
if (!fn) continue;
|
|
105
|
+
if (neg && !NEG_OK.test(root)) continue;
|
|
106
|
+
const value = words.slice(i).join("-");
|
|
107
|
+
if (!value && i === words.length && false) continue;
|
|
108
|
+
// `w-1/2`, `translate-x-1/2`: try the fraction first, then value + opacity/line-height modifier
|
|
109
|
+
let r = mod !== undefined ? norm(fn({ root, value: `${value}/${mod}`, mod: undefined, neg, ctx })) : null;
|
|
110
|
+
if (!r) r = norm(fn({ root, value, mod, neg, ctx }));
|
|
111
|
+
if (r) return r;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function containerRule(ctx) {
|
|
117
|
+
const decls = [["width", "100%"]];
|
|
118
|
+
if (ctx.container.center) decls.push(["margin-inline", "auto"]);
|
|
119
|
+
if (ctx.container.padding) decls.push(["padding-inline", ctx.container.padding]);
|
|
120
|
+
const extraRules = Object.entries(ctx.screens).map(([k, w]) => ({ media: [`@media (min-width: ${w})`], decls: [["max-width", w]], rank: ctx.screenRank(k) }));
|
|
121
|
+
return { decls, extraRules };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// property rank so shorthands sort before longhands (`p-4` before `px-2` before `pl-1`)
|
|
125
|
+
const SHORTHANDS = new Set(["margin", "padding", "inset", "gap", "border-width", "border-color", "border-radius", "border", "overflow", "scroll-margin", "scroll-padding", "transition-property", "flex", "background", "font", "outline", "translate", "rotate", "scale", "size"]);
|
|
126
|
+
function propRank(prop) {
|
|
127
|
+
if (SHORTHANDS.has(prop)) return 0;
|
|
128
|
+
if (/-(top|right|bottom|left|start|end)\b|^(top|right|bottom|left)$|-(x|y)$/.test(prop)) return 2;
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resolve one token.
|
|
134
|
+
* @param {string} raw class token, e.g. "md:hover:bg-red-500/50"
|
|
135
|
+
* @param {object} ctx from createContext()
|
|
136
|
+
* @param {{ selector?: string }} [opts] selector: use instead of `.escaped-token` (for @apply)
|
|
137
|
+
* @returns {null | { token: string, rules: Array<{selectors:string[], media:string[], decls:string[][], rank:number}>, keyframes: string[], marker?: boolean }}
|
|
138
|
+
*/
|
|
139
|
+
export function resolveToken(raw, ctx, opts = {}) {
|
|
140
|
+
const p = parseToken(raw, ctx);
|
|
141
|
+
if (p.foreign || !p.body) return null;
|
|
142
|
+
const util = resolveUtility(p, ctx);
|
|
143
|
+
if (!util) return null;
|
|
144
|
+
if (util.marker) return { token: raw, rules: [], keyframes: [], marker: true };
|
|
145
|
+
|
|
146
|
+
const st = { sels: [opts.selector || `.${esc(raw)}`], media: [], prelude: [], rank: 0 };
|
|
147
|
+
for (let i = p.variants.length - 1; i >= 0; i--) {
|
|
148
|
+
if (!applyVariant(p.variants[i], st, ctx)) return null;
|
|
149
|
+
}
|
|
150
|
+
// a pseudo-element can only end a selector: `[&_.x]:placeholder:italic` would give `::placeholder .x` (invalid, dropped)
|
|
151
|
+
if (st.sels.some((s) => /::[a-z-]+(?:\([^)]*\))?[\s>+~.[#]/.test(s))) return null;
|
|
152
|
+
const important = p.important || ctx.important;
|
|
153
|
+
const fix = (decls) => decls.map(([k, v]) => [k, important && !/ !important$/.test(v) ? `${v} !important` : v]);
|
|
154
|
+
const hasContent = util.decls.some(([k]) => k === "content");
|
|
155
|
+
const base = fix([...(hasContent ? [] : st.prelude), ...util.decls]);
|
|
156
|
+
const urank = propRank((util.decls[0] || [""])[0]);
|
|
157
|
+
const tail = util.tail || "";
|
|
158
|
+
const selectors = st.sels.map((s) => s + tail);
|
|
159
|
+
const rules = [];
|
|
160
|
+
if (base.length) rules.push({ selectors, media: st.media, decls: base, rank: st.rank, urank });
|
|
161
|
+
for (const x of util.extraRules || []) rules.push({ selectors, media: [...st.media, ...x.media], decls: fix(x.decls), rank: st.rank + 1 + (x.rank || 0), urank });
|
|
162
|
+
return { token: raw, rules, keyframes: util.keyframes || [] };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------------------------- compile
|
|
166
|
+
|
|
167
|
+
const fmtDecls = (decls, indent) => decls.map(([k, v]) => `${indent}${k}: ${v};`).join("\n");
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Compile a list of resolved tokens into one stylesheet (ordered like a utility framework: base < pseudo < dark < responsive).
|
|
171
|
+
* @param {Array<ReturnType<typeof resolveToken>>} resolved
|
|
172
|
+
*/
|
|
173
|
+
export function serialize(resolved) {
|
|
174
|
+
const rules = [];
|
|
175
|
+
const frames = new Set();
|
|
176
|
+
for (const r of resolved) {
|
|
177
|
+
if (!r) continue;
|
|
178
|
+
r.keyframes.forEach((k) => frames.add(k));
|
|
179
|
+
for (const rule of r.rules) rules.push({ ...rule, token: r.token });
|
|
180
|
+
}
|
|
181
|
+
rules.sort((a, b) => a.rank - b.rank || a.urank - b.urank || (a.token < b.token ? -1 : a.token > b.token ? 1 : 0));
|
|
182
|
+
const renderRule = (r, ind = "") => `${r.selectors.map((x) => ind + x).join(",\n")} {\n${fmtDecls(r.decls, ind + " ")}\n${ind}}`;
|
|
183
|
+
const out = [];
|
|
184
|
+
for (const k of frames) if (KEYFRAMES[k]) out.push(KEYFRAMES[k]);
|
|
185
|
+
let group = null;
|
|
186
|
+
const flush = () => {
|
|
187
|
+
if (!group) return;
|
|
188
|
+
let inner = group.rules.map((r) => renderRule(r, " ".repeat(group.media.length))).join("\n");
|
|
189
|
+
for (let i = group.media.length - 1; i >= 0; i--) {
|
|
190
|
+
const ind = " ".repeat(i);
|
|
191
|
+
inner = `${ind}${group.media[i]} {\n${inner}\n${ind}}`;
|
|
192
|
+
}
|
|
193
|
+
out.push(inner);
|
|
194
|
+
group = null;
|
|
195
|
+
};
|
|
196
|
+
for (const rule of rules) {
|
|
197
|
+
if (!rule.media.length) { flush(); out.push(renderRule(rule)); continue; }
|
|
198
|
+
const key = rule.media.join("|");
|
|
199
|
+
if (!group || group.key !== key) { flush(); group = { key, media: rule.media, rules: [] }; }
|
|
200
|
+
group.rules.push(rule);
|
|
201
|
+
}
|
|
202
|
+
flush();
|
|
203
|
+
return out.join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Resolve many tokens and report what could not be resolved. */
|
|
207
|
+
export function compileTokens(tokens, ctx) {
|
|
208
|
+
const resolved = [], unsupported = [];
|
|
209
|
+
for (const t of tokens) {
|
|
210
|
+
const r = resolveToken(t, ctx);
|
|
211
|
+
if (r) resolved.push(r); else unsupported.push(t);
|
|
212
|
+
}
|
|
213
|
+
return { css: serialize(resolved), resolved, unsupported };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Convenience: one token -> css string (or null). */
|
|
217
|
+
export function compileToken(raw, ctx = createContext()) {
|
|
218
|
+
const r = resolveToken(raw, ctx);
|
|
219
|
+
return r ? serialize([r]) : null;
|
|
220
|
+
}
|