emily-css 1.0.28 → 1.1.1
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/CHANGELOG.md +31 -0
- package/bin/emilyui.js +7 -1
- package/package.json +1 -1
- package/src/doctor.js +293 -0
- package/src/index.js +38 -0
- package/src/manifest.js +307 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,38 @@
|
|
|
3
3
|
All notable changes to `emily-css` are documented here.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
|
+
## v1.1.0 — May 2026
|
|
6
7
|
|
|
8
|
+
### Added
|
|
9
|
+
- Added `emily-css doctor`, a manifest-powered project checker that scans configured source files and reports unknown EmilyCSS classes with suggestions.
|
|
10
|
+
- Added variant-aware class validation for responsive, state, ARIA, data-state, motion, dark, and forced-colours variants.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## v1.1.1 — May 2026
|
|
15
|
+
|
|
16
|
+
**updated changes and added**
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- updted changes
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
## v1.1.0 — May 2026
|
|
23
|
+
|
|
24
|
+
**add utility manifest generation): chore: release v1.1.0**
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- add utility manifest generation): chore: release v1.1.0
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
## v1.0.29 — May 2026
|
|
31
|
+
|
|
32
|
+
**added json manifest for future use**
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- added json manifest for future use
|
|
36
|
+
|
|
37
|
+
---
|
|
7
38
|
## v1.0.28 — May 2026
|
|
8
39
|
|
|
9
40
|
**added new utilities**
|
package/bin/emilyui.js
CHANGED
|
@@ -14,6 +14,10 @@ if (command === "init") {
|
|
|
14
14
|
require("../src/watch.js");
|
|
15
15
|
} else if (command === "showcase") {
|
|
16
16
|
require("../src/showcase.js");
|
|
17
|
+
} else if (command === "doctor") {
|
|
18
|
+
const { doctor } = require("../src/doctor.js");
|
|
19
|
+
const result = doctor();
|
|
20
|
+
process.exitCode = result.exitCode;
|
|
17
21
|
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
18
22
|
console.log(packageJson.version);
|
|
19
23
|
} else if (command === "help") {
|
|
@@ -24,6 +28,7 @@ if (command === "init") {
|
|
|
24
28
|
emily-css init Set up a new project (interactive wizard)
|
|
25
29
|
emily-css build Generate production CSS to the configured output path
|
|
26
30
|
emily-css watch Dev mode: watch for changes and rebuild
|
|
31
|
+
emily-css doctor Scan project files for unknown EmilyCSS classes
|
|
27
32
|
emily-css showcase Launch the component showcase in your browser
|
|
28
33
|
emily-css version Show installed version
|
|
29
34
|
emily-css help Show this help text
|
|
@@ -44,9 +49,10 @@ if (command === "init") {
|
|
|
44
49
|
emily-css init Set up a new project
|
|
45
50
|
emily-css build Generate production CSS to the configured output path
|
|
46
51
|
emily-css watch Dev mode: rebuild on changes
|
|
52
|
+
emily-css doctor Scan project files for unknown EmilyCSS classes
|
|
47
53
|
emily-css showcase Browse components in your browser
|
|
48
54
|
emily-css help Full command reference
|
|
49
55
|
|
|
50
56
|
Run emily-css help for more detail.
|
|
51
57
|
`);
|
|
52
|
-
}
|
|
58
|
+
}
|
package/package.json
CHANGED
package/src/doctor.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fg = require("fast-glob");
|
|
4
|
+
const { extractClassNames } = require("./purge.js");
|
|
5
|
+
const { ensureFullFramework, generateManifest } = require("./index.js");
|
|
6
|
+
|
|
7
|
+
const DEFAULT_EXTENSIONS = [
|
|
8
|
+
".html",
|
|
9
|
+
".htm",
|
|
10
|
+
".twig",
|
|
11
|
+
".njk",
|
|
12
|
+
".liquid",
|
|
13
|
+
".hbs",
|
|
14
|
+
".js",
|
|
15
|
+
".jsx",
|
|
16
|
+
".ts",
|
|
17
|
+
".tsx",
|
|
18
|
+
".vue",
|
|
19
|
+
".php",
|
|
20
|
+
".astro",
|
|
21
|
+
".svelte",
|
|
22
|
+
".blade.php",
|
|
23
|
+
".jinja",
|
|
24
|
+
".jinja2",
|
|
25
|
+
".j2",
|
|
26
|
+
".md",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function getConfigPath() {
|
|
30
|
+
return path.join(process.cwd(), "emily.config.json");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getConfig() {
|
|
34
|
+
const configPath = getConfigPath();
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(configPath)) {
|
|
37
|
+
console.error('\n emily-css: No config found. Run "emily-css init" first.\n');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getFullCssPath(config) {
|
|
45
|
+
return path.join(process.cwd(), config.output?.fullCss || "dist/emily.css");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getManifestSettings(config) {
|
|
49
|
+
const manifestConfig = config.manifest;
|
|
50
|
+
|
|
51
|
+
if (manifestConfig === true) {
|
|
52
|
+
return { enabled: true, output: "dist/emily.manifest.json" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (manifestConfig && typeof manifestConfig === "object") {
|
|
56
|
+
return {
|
|
57
|
+
enabled: manifestConfig.enabled === true,
|
|
58
|
+
output: manifestConfig.output || "dist/emily.manifest.json",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { enabled: false, output: "dist/emily.manifest.json" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getManifestOutputPath(config) {
|
|
66
|
+
const manifestSettings = getManifestSettings(config);
|
|
67
|
+
const outputPath = manifestSettings.output || "dist/emily.manifest.json";
|
|
68
|
+
|
|
69
|
+
return path.isAbsolute(outputPath)
|
|
70
|
+
? outputPath
|
|
71
|
+
: path.join(process.cwd(), outputPath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normaliseClassForManifest(className) {
|
|
75
|
+
if (!className || typeof className !== "string") {
|
|
76
|
+
return { original: className, baseClass: "", variants: [], variant: null };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const parts = className.split(":").filter(Boolean);
|
|
80
|
+
|
|
81
|
+
if (parts.length <= 1) {
|
|
82
|
+
return {
|
|
83
|
+
original: className,
|
|
84
|
+
baseClass: className,
|
|
85
|
+
variants: [],
|
|
86
|
+
variant: null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const baseClass = parts[parts.length - 1];
|
|
91
|
+
const variants = parts.slice(0, -1);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
original: className,
|
|
95
|
+
baseClass,
|
|
96
|
+
variants,
|
|
97
|
+
variant: variants.join(":"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function levenshtein(a, b) {
|
|
102
|
+
if (a === b) return 0;
|
|
103
|
+
if (!a.length) return b.length;
|
|
104
|
+
if (!b.length) return a.length;
|
|
105
|
+
|
|
106
|
+
const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
|
|
109
|
+
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
|
|
110
|
+
|
|
111
|
+
for (let i = 1; i <= a.length; i++) {
|
|
112
|
+
for (let j = 1; j <= b.length; j++) {
|
|
113
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
114
|
+
dp[i][j] = Math.min(
|
|
115
|
+
dp[i - 1][j] + 1,
|
|
116
|
+
dp[i][j - 1] + 1,
|
|
117
|
+
dp[i - 1][j - 1] + cost
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return dp[a.length][b.length];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function findClosest(target, candidates) {
|
|
126
|
+
if (!target || candidates.length === 0) return null;
|
|
127
|
+
|
|
128
|
+
let best = null;
|
|
129
|
+
let bestDistance = Infinity;
|
|
130
|
+
|
|
131
|
+
for (const candidate of candidates) {
|
|
132
|
+
const distance = levenshtein(target, candidate);
|
|
133
|
+
if (distance < bestDistance) {
|
|
134
|
+
bestDistance = distance;
|
|
135
|
+
best = candidate;
|
|
136
|
+
if (distance === 0) break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const threshold = Math.max(2, Math.floor(target.length / 3));
|
|
141
|
+
return bestDistance <= threshold ? best : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function suggestClassName(className, utilitySet, variantSet) {
|
|
145
|
+
const utilityList = Array.isArray(utilitySet) ? utilitySet : Array.from(utilitySet);
|
|
146
|
+
const variantList = Array.isArray(variantSet) ? variantSet : Array.from(variantSet);
|
|
147
|
+
const parsed = normaliseClassForManifest(className);
|
|
148
|
+
|
|
149
|
+
if (!parsed.variants.length) {
|
|
150
|
+
return findClosest(parsed.baseClass, utilityList);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const correctedVariants = parsed.variants.map((variant) => {
|
|
154
|
+
if (variantSet.has(variant)) return variant;
|
|
155
|
+
return findClosest(variant, variantList) || variant;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const correctedBase = utilitySet.has(parsed.baseClass)
|
|
159
|
+
? parsed.baseClass
|
|
160
|
+
: findClosest(parsed.baseClass, utilityList);
|
|
161
|
+
|
|
162
|
+
if (!correctedBase) return null;
|
|
163
|
+
|
|
164
|
+
const rebuilt = correctedVariants.length
|
|
165
|
+
? `${correctedVariants.join(":")}:${correctedBase}`
|
|
166
|
+
: correctedBase;
|
|
167
|
+
|
|
168
|
+
return rebuilt === className ? null : rebuilt;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getFilesToScan(config) {
|
|
172
|
+
const extensions = config?.purge?.extensions || DEFAULT_EXTENSIONS;
|
|
173
|
+
const ignore = config?.purge?.ignore || [];
|
|
174
|
+
|
|
175
|
+
if (config?.purge?.sourceGlobs && config.purge.sourceGlobs.length > 0) {
|
|
176
|
+
return fg.sync(config.purge.sourceGlobs, {
|
|
177
|
+
ignore,
|
|
178
|
+
onlyFiles: true,
|
|
179
|
+
unique: true,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const sourceDir = config?.purge?.sourceDir || ".";
|
|
184
|
+
const scanDir = path.isAbsolute(sourceDir)
|
|
185
|
+
? sourceDir
|
|
186
|
+
: path.join(process.cwd(), sourceDir);
|
|
187
|
+
const patterns = extensions.map((ext) => `**/*${ext}`);
|
|
188
|
+
|
|
189
|
+
return fg.sync(patterns, {
|
|
190
|
+
cwd: scanDir,
|
|
191
|
+
ignore,
|
|
192
|
+
onlyFiles: true,
|
|
193
|
+
unique: true,
|
|
194
|
+
absolute: true,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function loadManifest(config, css) {
|
|
199
|
+
const manifestSettings = getManifestSettings(config);
|
|
200
|
+
const manifestOutputPath = getManifestOutputPath(config);
|
|
201
|
+
|
|
202
|
+
if (manifestSettings.enabled && fs.existsSync(manifestOutputPath)) {
|
|
203
|
+
return JSON.parse(fs.readFileSync(manifestOutputPath, "utf8"));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return generateManifest(css, config);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function doctor() {
|
|
210
|
+
const config = getConfig();
|
|
211
|
+
|
|
212
|
+
ensureFullFramework();
|
|
213
|
+
|
|
214
|
+
const fullCssPath = getFullCssPath(config);
|
|
215
|
+
if (!fs.existsSync(fullCssPath)) {
|
|
216
|
+
console.error("\nEmilyCSS doctor could not locate generated CSS.\n");
|
|
217
|
+
return { ok: false, issues: [], exitCode: 1 };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const css = fs.readFileSync(fullCssPath, "utf8");
|
|
221
|
+
const manifest = loadManifest(config, css);
|
|
222
|
+
const utilities = manifest.utilities || [];
|
|
223
|
+
const utilitySet = new Set(utilities.map((utility) => utility.class));
|
|
224
|
+
const variantSet = new Set();
|
|
225
|
+
|
|
226
|
+
utilities.forEach((utility) => {
|
|
227
|
+
(utility.variants || []).forEach((variant) => variantSet.add(variant));
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const files = getFilesToScan(config);
|
|
231
|
+
const issues = [];
|
|
232
|
+
const suggestionCache = new Map();
|
|
233
|
+
|
|
234
|
+
files.forEach((filePath) => {
|
|
235
|
+
try {
|
|
236
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
237
|
+
const classes = extractClassNames(content);
|
|
238
|
+
|
|
239
|
+
classes.forEach((className) => {
|
|
240
|
+
const parsed = normaliseClassForManifest(className);
|
|
241
|
+
const unknownVariants = parsed.variants.filter((variant) => !variantSet.has(variant));
|
|
242
|
+
const knownBase = utilitySet.has(parsed.baseClass);
|
|
243
|
+
|
|
244
|
+
if (unknownVariants.length === 0 && knownBase) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!suggestionCache.has(className)) {
|
|
249
|
+
suggestionCache.set(className, suggestClassName(className, utilitySet, variantSet));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
issues.push({
|
|
253
|
+
file: filePath,
|
|
254
|
+
className,
|
|
255
|
+
reason: unknownVariants.length > 0 ? "unknown-variant" : "unknown-class",
|
|
256
|
+
unknownVariants,
|
|
257
|
+
suggestion: suggestionCache.get(className),
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
// Keep parity with purge behaviour: unreadable files are skipped.
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
if (issues.length === 0) {
|
|
266
|
+
console.log("✓ EmilyCSS doctor found no class issues");
|
|
267
|
+
return { ok: true, issues: [], exitCode: 0 };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log(`EmilyCSS doctor found ${issues.length} issue${issues.length === 1 ? "" : "s"}\n`);
|
|
271
|
+
|
|
272
|
+
issues.forEach((issue) => {
|
|
273
|
+
console.log(path.relative(process.cwd(), issue.file));
|
|
274
|
+
if (issue.reason === "unknown-variant") {
|
|
275
|
+
console.log(` Unknown variant in class: ${issue.className}`);
|
|
276
|
+
} else {
|
|
277
|
+
console.log(` Unknown class: ${issue.className}`);
|
|
278
|
+
}
|
|
279
|
+
if (issue.suggestion) {
|
|
280
|
+
console.log(` Did you mean: ${issue.suggestion}?`);
|
|
281
|
+
}
|
|
282
|
+
console.log("");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
console.log("Run `emily-css build` after fixing classes.");
|
|
286
|
+
return { ok: false, issues, exitCode: 1 };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
doctor,
|
|
291
|
+
normaliseClassForManifest,
|
|
292
|
+
suggestClassName,
|
|
293
|
+
};
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { generateManifest } = require('./manifest');
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
// ============================================================================
|
|
@@ -1479,6 +1480,32 @@ function getProductionCssPath(config) {
|
|
|
1479
1480
|
return path.join(process.cwd(), config.output?.css || 'dist/emily.min.css');
|
|
1480
1481
|
}
|
|
1481
1482
|
|
|
1483
|
+
function getManifestSettings(config) {
|
|
1484
|
+
const manifestConfig = config.manifest;
|
|
1485
|
+
|
|
1486
|
+
if (manifestConfig === true) {
|
|
1487
|
+
return { enabled: true, output: 'dist/emily.manifest.json' };
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (manifestConfig && typeof manifestConfig === 'object') {
|
|
1491
|
+
return {
|
|
1492
|
+
enabled: manifestConfig.enabled === true,
|
|
1493
|
+
output: manifestConfig.output || 'dist/emily.manifest.json',
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
return { enabled: false, output: 'dist/emily.manifest.json' };
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function getManifestOutputPath(config) {
|
|
1501
|
+
const manifestSettings = getManifestSettings(config);
|
|
1502
|
+
const outputPath = manifestSettings.output || 'dist/emily.manifest.json';
|
|
1503
|
+
|
|
1504
|
+
return path.isAbsolute(outputPath)
|
|
1505
|
+
? outputPath
|
|
1506
|
+
: path.join(process.cwd(), outputPath);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1482
1509
|
function ensureDirectoryForFile(filePath) {
|
|
1483
1510
|
const dir = path.dirname(filePath);
|
|
1484
1511
|
|
|
@@ -1700,6 +1727,16 @@ ${bodyFont}`;
|
|
|
1700
1727
|
ensureDirectoryForFile(fullCssPath);
|
|
1701
1728
|
fs.writeFileSync(fullCssPath, css);
|
|
1702
1729
|
|
|
1730
|
+
const manifestSettings = getManifestSettings(config);
|
|
1731
|
+
if (manifestSettings.enabled) {
|
|
1732
|
+
const manifestPath = getManifestOutputPath(config);
|
|
1733
|
+
const manifest = generateManifest(css, config);
|
|
1734
|
+
|
|
1735
|
+
ensureDirectoryForFile(manifestPath);
|
|
1736
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1737
|
+
console.log(`✓ Generated manifest: ${manifestPath}`);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1703
1740
|
console.log(`✓ Generated CSS: ${fullCssPath}`);
|
|
1704
1741
|
console.log(`✓ File size: ${(css.length / 1024).toFixed(2)} KB (unminified)`);
|
|
1705
1742
|
console.log('Full framework build complete');
|
|
@@ -1808,6 +1845,7 @@ module.exports = {
|
|
|
1808
1845
|
addStateVariants,
|
|
1809
1846
|
addAriaDataVariants,
|
|
1810
1847
|
addResponsiveVariants,
|
|
1848
|
+
generateManifest,
|
|
1811
1849
|
generateFontCSS,
|
|
1812
1850
|
codeUtilities,
|
|
1813
1851
|
};
|
package/src/manifest.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const MANIFEST_VERSION = '1.1.0';
|
|
2
|
+
const DEFAULT_RESPONSIVE_VARIANTS = ['sm', 'md', 'lg', 'xl', '2xl'];
|
|
3
|
+
const BASE_VARIANTS = [
|
|
4
|
+
'hover',
|
|
5
|
+
'focus',
|
|
6
|
+
'focus-within',
|
|
7
|
+
'focus-visible',
|
|
8
|
+
'active',
|
|
9
|
+
'disabled',
|
|
10
|
+
'motion-reduce',
|
|
11
|
+
'motion-safe',
|
|
12
|
+
'aria-expanded',
|
|
13
|
+
'aria-selected',
|
|
14
|
+
'aria-current',
|
|
15
|
+
'aria-disabled',
|
|
16
|
+
'data-open',
|
|
17
|
+
'data-closed',
|
|
18
|
+
'dark',
|
|
19
|
+
'forced-colors',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function parseDeclarations(block) {
|
|
23
|
+
const declarations = {};
|
|
24
|
+
const declarationRegex = /([a-zA-Z-]+)\s*:\s*([^;]+)\s*;?/g;
|
|
25
|
+
let firstProperty = null;
|
|
26
|
+
let firstValue = null;
|
|
27
|
+
let match;
|
|
28
|
+
|
|
29
|
+
while ((match = declarationRegex.exec(block)) !== null) {
|
|
30
|
+
const property = match[1].trim();
|
|
31
|
+
const value = match[2].trim();
|
|
32
|
+
declarations[property] = value;
|
|
33
|
+
|
|
34
|
+
if (!firstProperty) {
|
|
35
|
+
firstProperty = property;
|
|
36
|
+
firstValue = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { declarations, firstProperty, firstValue };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getTokenFromDeclarations(declarations) {
|
|
44
|
+
const values = Object.values(declarations);
|
|
45
|
+
|
|
46
|
+
for (const value of values) {
|
|
47
|
+
const tokenMatch = value.match(/var\(\s*(--[a-zA-Z0-9-_]+)\s*(?:,[^)]+)?\)/);
|
|
48
|
+
if (tokenMatch) {
|
|
49
|
+
return tokenMatch[1];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isSimpleBaseClassSelector(selector) {
|
|
57
|
+
if (!selector || !selector.startsWith('.')) return false;
|
|
58
|
+
if (selector.includes(' ')) return false;
|
|
59
|
+
if (selector.includes(',')) return false;
|
|
60
|
+
if (selector.includes('[')) return false;
|
|
61
|
+
if (selector.includes(':')) return false;
|
|
62
|
+
if (selector.includes('::')) return false;
|
|
63
|
+
if (selector.includes('>')) return false;
|
|
64
|
+
if (selector.includes('+')) return false;
|
|
65
|
+
if (selector.includes('~')) return false;
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function inferCategory(className, property) {
|
|
71
|
+
if (
|
|
72
|
+
className === 'prose' ||
|
|
73
|
+
className === 'prose-emily' ||
|
|
74
|
+
className === 'center-screen' ||
|
|
75
|
+
className === 'center-absolute' ||
|
|
76
|
+
className === 'field-container' ||
|
|
77
|
+
className === 'form-hint' ||
|
|
78
|
+
className === 'form-error-message' ||
|
|
79
|
+
className === 'error-summary' ||
|
|
80
|
+
className === 'btn' ||
|
|
81
|
+
className === 'btn-primary' ||
|
|
82
|
+
className === 'btn-secondary' ||
|
|
83
|
+
className === 'btn-ghost' ||
|
|
84
|
+
className === 'btn-danger' ||
|
|
85
|
+
className === 'btn-sm' ||
|
|
86
|
+
className === 'btn-lg' ||
|
|
87
|
+
className === 'code-window' ||
|
|
88
|
+
className === 'code-title-bar' ||
|
|
89
|
+
className === 'code-dot' ||
|
|
90
|
+
className === 'code-dot-red' ||
|
|
91
|
+
className === 'code-dot-yellow' ||
|
|
92
|
+
className === 'code-dot-green' ||
|
|
93
|
+
className === 'code-filename' ||
|
|
94
|
+
className.startsWith('token-')
|
|
95
|
+
) {
|
|
96
|
+
return 'component';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (className.startsWith('text-')) {
|
|
100
|
+
return property === 'color' ? 'colour' : 'typography';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
className.startsWith('font-') ||
|
|
105
|
+
className.startsWith('leading-') ||
|
|
106
|
+
className.startsWith('tracking-') ||
|
|
107
|
+
className.startsWith('list-')
|
|
108
|
+
) {
|
|
109
|
+
return 'typography';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (className.startsWith('bg-')) return 'background';
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
className === 'border' ||
|
|
116
|
+
className.startsWith('border-') ||
|
|
117
|
+
className.startsWith('divide-') ||
|
|
118
|
+
className === 'outline' ||
|
|
119
|
+
className.startsWith('outline-') ||
|
|
120
|
+
className === 'ring' ||
|
|
121
|
+
className.startsWith('ring-')
|
|
122
|
+
) {
|
|
123
|
+
return 'border';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
className.startsWith('p-') ||
|
|
128
|
+
className.startsWith('px-') ||
|
|
129
|
+
className.startsWith('py-') ||
|
|
130
|
+
className.startsWith('pt-') ||
|
|
131
|
+
className.startsWith('pr-') ||
|
|
132
|
+
className.startsWith('pb-') ||
|
|
133
|
+
className.startsWith('pl-') ||
|
|
134
|
+
className.startsWith('m-') ||
|
|
135
|
+
className.startsWith('mx-') ||
|
|
136
|
+
className.startsWith('my-') ||
|
|
137
|
+
className.startsWith('mt-') ||
|
|
138
|
+
className.startsWith('mr-') ||
|
|
139
|
+
className.startsWith('mb-') ||
|
|
140
|
+
className.startsWith('ml-') ||
|
|
141
|
+
className.startsWith('gap-') ||
|
|
142
|
+
className.startsWith('space-') ||
|
|
143
|
+
className.startsWith('inset-') ||
|
|
144
|
+
className.startsWith('top-') ||
|
|
145
|
+
className.startsWith('right-') ||
|
|
146
|
+
className.startsWith('bottom-') ||
|
|
147
|
+
className.startsWith('left-')
|
|
148
|
+
) {
|
|
149
|
+
return 'spacing';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
className.startsWith('w-') ||
|
|
154
|
+
className.startsWith('h-') ||
|
|
155
|
+
className.startsWith('min-w-') ||
|
|
156
|
+
className.startsWith('max-w-') ||
|
|
157
|
+
className.startsWith('min-h-') ||
|
|
158
|
+
className.startsWith('max-h-') ||
|
|
159
|
+
className.startsWith('size-')
|
|
160
|
+
) {
|
|
161
|
+
return 'sizing';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (
|
|
165
|
+
className === 'flex' ||
|
|
166
|
+
className === 'grid' ||
|
|
167
|
+
className === 'block' ||
|
|
168
|
+
className === 'inline' ||
|
|
169
|
+
className === 'inline-block' ||
|
|
170
|
+
className === 'inline-flex' ||
|
|
171
|
+
className === 'hidden' ||
|
|
172
|
+
className === 'container' ||
|
|
173
|
+
className === 'relative' ||
|
|
174
|
+
className === 'absolute' ||
|
|
175
|
+
className === 'fixed' ||
|
|
176
|
+
className === 'sticky' ||
|
|
177
|
+
className === 'static'
|
|
178
|
+
) {
|
|
179
|
+
return 'layout';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
className.startsWith('items-') ||
|
|
184
|
+
className.startsWith('justify-') ||
|
|
185
|
+
className.startsWith('content-') ||
|
|
186
|
+
className.startsWith('self-') ||
|
|
187
|
+
className.startsWith('place-') ||
|
|
188
|
+
className.startsWith('order-') ||
|
|
189
|
+
className.startsWith('col-') ||
|
|
190
|
+
className.startsWith('row-')
|
|
191
|
+
) {
|
|
192
|
+
return 'layout';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (className === 'rounded' || className.startsWith('rounded-')) return 'radius';
|
|
196
|
+
if (className === 'shadow' || className.startsWith('shadow-')) return 'shadow';
|
|
197
|
+
|
|
198
|
+
if (
|
|
199
|
+
className.startsWith('opacity-') ||
|
|
200
|
+
className.startsWith('blur-') ||
|
|
201
|
+
className.startsWith('backdrop-') ||
|
|
202
|
+
className === 'filter' ||
|
|
203
|
+
className === 'grayscale' ||
|
|
204
|
+
className.startsWith('saturate-') ||
|
|
205
|
+
className.startsWith('brightness-') ||
|
|
206
|
+
className.startsWith('contrast-')
|
|
207
|
+
) {
|
|
208
|
+
return 'effects';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
className === 'transition' ||
|
|
213
|
+
className.startsWith('transition-') ||
|
|
214
|
+
className.startsWith('duration-') ||
|
|
215
|
+
className.startsWith('ease-') ||
|
|
216
|
+
className.startsWith('delay-') ||
|
|
217
|
+
className.startsWith('animate-')
|
|
218
|
+
) {
|
|
219
|
+
return 'motion';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (
|
|
223
|
+
className === 'sr-only' ||
|
|
224
|
+
className === 'not-sr-only' ||
|
|
225
|
+
className === 'focus-ring' ||
|
|
226
|
+
className === 'skip-to-content' ||
|
|
227
|
+
className === 'js-hidden' ||
|
|
228
|
+
className === 'no-js'
|
|
229
|
+
) {
|
|
230
|
+
return 'accessibility';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (
|
|
234
|
+
className === 'input' ||
|
|
235
|
+
className === 'select' ||
|
|
236
|
+
className === 'textarea' ||
|
|
237
|
+
className === 'checkbox' ||
|
|
238
|
+
className === 'radio' ||
|
|
239
|
+
className === 'stack' ||
|
|
240
|
+
className === 'cluster' ||
|
|
241
|
+
className === 'width-container'
|
|
242
|
+
) {
|
|
243
|
+
return 'component';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return 'utility';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function normalizeClassName(selector) {
|
|
250
|
+
return selector.slice(1).replace(/\\(.)/g, '$1');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getVariants(config) {
|
|
254
|
+
const breakpoints =
|
|
255
|
+
config &&
|
|
256
|
+
config.breakpoints &&
|
|
257
|
+
typeof config.breakpoints === 'object' &&
|
|
258
|
+
Object.keys(config.breakpoints).length > 0
|
|
259
|
+
? Object.keys(config.breakpoints)
|
|
260
|
+
: DEFAULT_RESPONSIVE_VARIANTS;
|
|
261
|
+
|
|
262
|
+
return [...BASE_VARIANTS, ...breakpoints];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function generateManifest(css, config = {}) {
|
|
266
|
+
const manifest = {
|
|
267
|
+
version: MANIFEST_VERSION,
|
|
268
|
+
generatedAt: new Date().toISOString(),
|
|
269
|
+
utilities: [],
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if (typeof css !== 'string' || css.length === 0) {
|
|
273
|
+
return manifest;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const variants = getVariants(config);
|
|
277
|
+
const cleanedCss = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
278
|
+
const ruleRegex = /([^{}]+)\{([^{}]*)\}/g;
|
|
279
|
+
let ruleMatch;
|
|
280
|
+
|
|
281
|
+
while ((ruleMatch = ruleRegex.exec(cleanedCss)) !== null) {
|
|
282
|
+
const selector = ruleMatch[1].trim();
|
|
283
|
+
const declarationBlock = ruleMatch[2].trim();
|
|
284
|
+
|
|
285
|
+
if (!isSimpleBaseClassSelector(selector)) continue;
|
|
286
|
+
|
|
287
|
+
const { declarations, firstProperty, firstValue } = parseDeclarations(declarationBlock);
|
|
288
|
+
if (!firstProperty) continue;
|
|
289
|
+
|
|
290
|
+
manifest.utilities.push({
|
|
291
|
+
class: normalizeClassName(selector),
|
|
292
|
+
category: inferCategory(normalizeClassName(selector), firstProperty),
|
|
293
|
+
property: firstProperty,
|
|
294
|
+
value: firstValue,
|
|
295
|
+
token: getTokenFromDeclarations(declarations),
|
|
296
|
+
declarations,
|
|
297
|
+
variants,
|
|
298
|
+
source: 'generated-css',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return manifest;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
generateManifest,
|
|
307
|
+
};
|