style-capture 0.0.1 → 0.0.3
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/README.md +77 -0
- package/dist/cli.js +1768 -0
- package/dist/cli.js.map +1 -0
- package/package.json +43 -12
- package/src/capture.ts +0 -507
- package/src/index.ts +0 -152
- package/src/run.ts +0 -106
- package/tsconfig.json +0 -20
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1768 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { cancel, group, intro, log, outro, select, spinner, text } from "@clack/prompts";
|
|
4
|
+
import { chromium } from "playwright";
|
|
5
|
+
//#region ../../packages/core/dist/index.js
|
|
6
|
+
const CAPTURE_NODE_ATTRIBUTE = "data-lc";
|
|
7
|
+
const MAX_TAILWIND_SUGGESTIONS = 6;
|
|
8
|
+
const MAX_OPEN_QUESTION_ITEMS = 8;
|
|
9
|
+
const CLAUDE_CAPTURE_INSTRUCTION = "Recreate or refactor this UI faithfully. html_capture + css_capture are ground truth. Preserve structure unless simplifying is clearly better. Tailwind hints are hints. Use the smallest codebase-ready change and state ambiguities instead of inventing details.";
|
|
10
|
+
const compactInlineText = (value) => value.replaceAll(/\s+/g, " ").trim();
|
|
11
|
+
const escapeXmlAttribute = (value) => value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
12
|
+
const compactSelector = (selector) => selector.replaceAll(/\s*([>+~])\s*/g, "$1").replaceAll(/,\s+/g, ",").replaceAll(/\s+/g, " ").trim();
|
|
13
|
+
const buildCompactRefs = (order) => {
|
|
14
|
+
const refs = {};
|
|
15
|
+
for (const [index, elementId] of order.entries()) refs[elementId] = index.toString(36);
|
|
16
|
+
return refs;
|
|
17
|
+
};
|
|
18
|
+
const buildCompactSelector = (ref) => `[${CAPTURE_NODE_ATTRIBUTE}="${ref}"]`;
|
|
19
|
+
const buildFallbackSelectors = (capture) => {
|
|
20
|
+
const selectors = {};
|
|
21
|
+
for (const elementId of capture.order) {
|
|
22
|
+
const snapshot = capture.elements[elementId];
|
|
23
|
+
if (!snapshot) continue;
|
|
24
|
+
selectors[elementId] = compactSelector(snapshot.selector);
|
|
25
|
+
}
|
|
26
|
+
return selectors;
|
|
27
|
+
};
|
|
28
|
+
const stripCommentNodes = (root) => {
|
|
29
|
+
const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
30
|
+
const comments = [];
|
|
31
|
+
while (walker.nextNode()) if (walker.currentNode instanceof Comment) comments.push(walker.currentNode);
|
|
32
|
+
for (const comment of comments) comment.remove();
|
|
33
|
+
};
|
|
34
|
+
const minifyHtmlString = (html) => html.replaceAll(/<!--[\s\S]*?-->/g, "").replaceAll(/>\s+</g, "><").trim();
|
|
35
|
+
const elementMatchesSnapshot = (element, snapshot) => {
|
|
36
|
+
if (element.tagName.toLowerCase() !== snapshot.tagName) return false;
|
|
37
|
+
for (const className of snapshot.classList) if (!element.classList.contains(className)) return false;
|
|
38
|
+
for (const [name, value] of Object.entries(snapshot.attributes)) {
|
|
39
|
+
if (name === "class") continue;
|
|
40
|
+
if (element.getAttribute(name) !== value) return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
};
|
|
44
|
+
const findMatchingElementIndex = (candidates, snapshot, startIndex) => {
|
|
45
|
+
for (let index = startIndex; index < candidates.length; index += 1) if (elementMatchesSnapshot(candidates[index], snapshot)) return index;
|
|
46
|
+
return -1;
|
|
47
|
+
};
|
|
48
|
+
const annotateCaptureHtml = (capture) => {
|
|
49
|
+
const refs = buildCompactRefs(capture.order);
|
|
50
|
+
if (!capture.rootOuterHtml.trim()) return {
|
|
51
|
+
html: "",
|
|
52
|
+
refs,
|
|
53
|
+
selectors: buildFallbackSelectors(capture)
|
|
54
|
+
};
|
|
55
|
+
if (typeof DOMParser === "undefined") return {
|
|
56
|
+
html: minifyHtmlString(capture.rootOuterHtml),
|
|
57
|
+
refs,
|
|
58
|
+
selectors: buildFallbackSelectors(capture)
|
|
59
|
+
};
|
|
60
|
+
const root = new DOMParser().parseFromString(capture.rootOuterHtml, "text/html").body.firstElementChild;
|
|
61
|
+
if (!root) return {
|
|
62
|
+
html: minifyHtmlString(capture.rootOuterHtml),
|
|
63
|
+
refs,
|
|
64
|
+
selectors: buildFallbackSelectors(capture)
|
|
65
|
+
};
|
|
66
|
+
stripCommentNodes(root);
|
|
67
|
+
const candidates = [root, ...root.querySelectorAll("*")];
|
|
68
|
+
const selectors = buildFallbackSelectors(capture);
|
|
69
|
+
let searchStartIndex = 0;
|
|
70
|
+
for (const elementId of capture.order) {
|
|
71
|
+
const snapshot = capture.elements[elementId];
|
|
72
|
+
const ref = refs[elementId];
|
|
73
|
+
if (!(snapshot && ref)) continue;
|
|
74
|
+
const matchIndex = findMatchingElementIndex(candidates, snapshot, searchStartIndex);
|
|
75
|
+
if (matchIndex === -1) continue;
|
|
76
|
+
candidates[matchIndex].setAttribute(CAPTURE_NODE_ATTRIBUTE, ref);
|
|
77
|
+
selectors[elementId] = buildCompactSelector(ref);
|
|
78
|
+
searchStartIndex = matchIndex + 1;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
html: minifyHtmlString(root.outerHTML),
|
|
82
|
+
refs,
|
|
83
|
+
selectors
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const formatCssPropertyName = (property) => {
|
|
87
|
+
if (property.includes("-")) return property;
|
|
88
|
+
return property.replaceAll(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
89
|
+
};
|
|
90
|
+
const formatDeclarationBlock = (styles) => Object.entries(styles).filter(([, value]) => value.trim().length > 0).toSorted(([left], [right]) => left.localeCompare(right)).map(([property, value]) => `${formatCssPropertyName(property)}:${compactInlineText(value)}`).join(";");
|
|
91
|
+
const formatPseudoBlock = (selector, pseudo) => {
|
|
92
|
+
if (!pseudo) return "";
|
|
93
|
+
const declarationBlock = formatDeclarationBlock(pseudo.styles);
|
|
94
|
+
if (!declarationBlock) return "";
|
|
95
|
+
return `${selector}::${pseudo.kind}{${declarationBlock}}`;
|
|
96
|
+
};
|
|
97
|
+
const formatElementCssBlock = (snapshot, selector) => {
|
|
98
|
+
const parts = [];
|
|
99
|
+
const declarationBlock = formatDeclarationBlock(snapshot.styles);
|
|
100
|
+
const beforeBlock = formatPseudoBlock(selector, snapshot.pseudo.before);
|
|
101
|
+
const afterBlock = formatPseudoBlock(selector, snapshot.pseudo.after);
|
|
102
|
+
if (declarationBlock) parts.push(`${selector}{${declarationBlock}}`);
|
|
103
|
+
if (beforeBlock) parts.push(beforeBlock);
|
|
104
|
+
if (afterBlock) parts.push(afterBlock);
|
|
105
|
+
return parts.join("");
|
|
106
|
+
};
|
|
107
|
+
const formatCaptureCss = (capture, selectors) => capture.order.map((elementId) => {
|
|
108
|
+
const snapshot = capture.elements[elementId];
|
|
109
|
+
if (!snapshot) return "";
|
|
110
|
+
return formatElementCssBlock(snapshot, selectors[elementId] ?? snapshot.selector);
|
|
111
|
+
}).filter(Boolean).join("");
|
|
112
|
+
const buildTailwindHintEntries = (capture, mapping, refs) => {
|
|
113
|
+
if (!mapping) return [];
|
|
114
|
+
const entries = [];
|
|
115
|
+
const rootMapping = mapping.elements[capture.rootElementId];
|
|
116
|
+
if (rootMapping?.suggestedClassName || rootMapping?.className) entries.push({
|
|
117
|
+
key: "root",
|
|
118
|
+
value: rootMapping.suggestedClassName || rootMapping.className
|
|
119
|
+
});
|
|
120
|
+
const topSuggestions = capture.order.filter((elementId) => elementId !== capture.rootElementId).map((elementId) => mapping.elements[elementId]).filter((element) => Boolean(element?.suggestedClassName || element?.className)).slice(0, MAX_TAILWIND_SUGGESTIONS);
|
|
121
|
+
for (const suggestion of topSuggestions) entries.push({
|
|
122
|
+
key: refs[suggestion.elementId] ?? suggestion.elementId,
|
|
123
|
+
value: suggestion.suggestedClassName || suggestion.className
|
|
124
|
+
});
|
|
125
|
+
return entries;
|
|
126
|
+
};
|
|
127
|
+
const buildOpenQuestionEntries = (mapping, refs) => {
|
|
128
|
+
if (!mapping || mapping.reviewQueue.length === 0) return [];
|
|
129
|
+
return mapping.reviewQueue.slice(0, MAX_OPEN_QUESTION_ITEMS).map((item) => ({
|
|
130
|
+
key: refs[item.elementId] ?? item.elementId,
|
|
131
|
+
value: item.reasons.join("; ")
|
|
132
|
+
}));
|
|
133
|
+
};
|
|
134
|
+
const buildClaudeCapturePrompt = (capture, mapping) => {
|
|
135
|
+
const annotation = annotateCaptureHtml(capture);
|
|
136
|
+
const rootElement = capture.elements[capture.rootElementId];
|
|
137
|
+
return {
|
|
138
|
+
cssCapture: formatCaptureCss(capture, annotation.selectors),
|
|
139
|
+
htmlCapture: annotation.html,
|
|
140
|
+
instruction: CLAUDE_CAPTURE_INSTRUCTION,
|
|
141
|
+
metadata: {
|
|
142
|
+
elementCount: capture.summary.elementCount,
|
|
143
|
+
mode: capture.settings.captureMode,
|
|
144
|
+
pseudoCount: capture.summary.pseudoElementCount,
|
|
145
|
+
rootRef: annotation.refs[capture.rootElementId] ?? capture.rootElementId,
|
|
146
|
+
rootSelector: rootElement?.selector ?? "Unavailable",
|
|
147
|
+
url: capture.metadata.url
|
|
148
|
+
},
|
|
149
|
+
openQuestions: buildOpenQuestionEntries(mapping, annotation.refs),
|
|
150
|
+
tailwindHints: buildTailwindHintEntries(capture, mapping, annotation.refs)
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
const formatCaptureForClaudeMarkdown = (capture, mapping) => {
|
|
154
|
+
const prompt = buildClaudeCapturePrompt(capture, mapping);
|
|
155
|
+
const sections = [
|
|
156
|
+
`<style_capture url="${escapeXmlAttribute(prompt.metadata.url)}" mode="${prompt.metadata.mode}" root_ref="${prompt.metadata.rootRef}" root_selector="${escapeXmlAttribute(prompt.metadata.rootSelector)}" elements="${prompt.metadata.elementCount}" pseudos="${prompt.metadata.pseudoCount}">`,
|
|
157
|
+
prompt.instruction,
|
|
158
|
+
`<html_capture>${prompt.htmlCapture}</html_capture>`,
|
|
159
|
+
`<css_capture>${prompt.cssCapture}</css_capture>`
|
|
160
|
+
];
|
|
161
|
+
if (prompt.tailwindHints.length > 0) sections.push("<tailwind_hints>", ...prompt.tailwindHints.map((entry) => `${entry.key}=${compactInlineText(entry.value)}`), "</tailwind_hints>");
|
|
162
|
+
if (prompt.openQuestions.length > 0) sections.push("<open_questions>", ...prompt.openQuestions.map((entry) => `${entry.key}:${compactInlineText(entry.value)}`), "</open_questions>");
|
|
163
|
+
sections.push("</style_capture>");
|
|
164
|
+
return sections.join("\n").trim();
|
|
165
|
+
};
|
|
166
|
+
const createDefaultSettings = () => ({
|
|
167
|
+
captureMode: "curated",
|
|
168
|
+
includeHiddenElements: false,
|
|
169
|
+
includePseudoElements: true
|
|
170
|
+
});
|
|
171
|
+
const HIGH_CONFIDENCE = .92;
|
|
172
|
+
const MEDIUM_CONFIDENCE = .72;
|
|
173
|
+
const LOW_CONFIDENCE = .45;
|
|
174
|
+
const PASSIVE_CONFIDENCE = .84;
|
|
175
|
+
const LENGTH_PATTERN = /^(-?\d*\.?\d+)(px|rem|em|%|vh|vw)?$/;
|
|
176
|
+
const RGB_PATTERN = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([0-9.]+))?\)$/;
|
|
177
|
+
const WHITESPACE_SPLIT_PATTERN = /\s+/;
|
|
178
|
+
const SPACING_SCALE = new Map([
|
|
179
|
+
[0, "0"],
|
|
180
|
+
[1, "px"],
|
|
181
|
+
[2, "0.5"],
|
|
182
|
+
[4, "1"],
|
|
183
|
+
[6, "1.5"],
|
|
184
|
+
[8, "2"],
|
|
185
|
+
[10, "2.5"],
|
|
186
|
+
[12, "3"],
|
|
187
|
+
[14, "3.5"],
|
|
188
|
+
[16, "4"],
|
|
189
|
+
[20, "5"],
|
|
190
|
+
[24, "6"],
|
|
191
|
+
[28, "7"],
|
|
192
|
+
[32, "8"],
|
|
193
|
+
[36, "9"],
|
|
194
|
+
[40, "10"],
|
|
195
|
+
[44, "11"],
|
|
196
|
+
[48, "12"],
|
|
197
|
+
[56, "14"],
|
|
198
|
+
[64, "16"],
|
|
199
|
+
[80, "20"],
|
|
200
|
+
[96, "24"],
|
|
201
|
+
[112, "28"],
|
|
202
|
+
[128, "32"],
|
|
203
|
+
[144, "36"],
|
|
204
|
+
[160, "40"],
|
|
205
|
+
[176, "44"],
|
|
206
|
+
[192, "48"],
|
|
207
|
+
[224, "56"],
|
|
208
|
+
[256, "64"],
|
|
209
|
+
[288, "72"],
|
|
210
|
+
[320, "80"],
|
|
211
|
+
[384, "96"]
|
|
212
|
+
]);
|
|
213
|
+
const FONT_SIZE_SCALE = new Map([
|
|
214
|
+
[12, "xs"],
|
|
215
|
+
[14, "sm"],
|
|
216
|
+
[16, "base"],
|
|
217
|
+
[18, "lg"],
|
|
218
|
+
[20, "xl"],
|
|
219
|
+
[24, "2xl"],
|
|
220
|
+
[30, "3xl"],
|
|
221
|
+
[36, "4xl"],
|
|
222
|
+
[48, "5xl"],
|
|
223
|
+
[60, "6xl"],
|
|
224
|
+
[72, "7xl"],
|
|
225
|
+
[96, "8xl"],
|
|
226
|
+
[128, "9xl"]
|
|
227
|
+
]);
|
|
228
|
+
const LINE_HEIGHT_SCALE = new Map([
|
|
229
|
+
[12, "3"],
|
|
230
|
+
[16, "4"],
|
|
231
|
+
[20, "5"],
|
|
232
|
+
[24, "6"],
|
|
233
|
+
[28, "7"],
|
|
234
|
+
[32, "8"],
|
|
235
|
+
[36, "9"],
|
|
236
|
+
[40, "10"]
|
|
237
|
+
]);
|
|
238
|
+
const RADIUS_SCALE = new Map([
|
|
239
|
+
[0, "none"],
|
|
240
|
+
[2, "sm"],
|
|
241
|
+
[4, ""],
|
|
242
|
+
[6, "md"],
|
|
243
|
+
[8, "lg"],
|
|
244
|
+
[12, "xl"],
|
|
245
|
+
[16, "2xl"],
|
|
246
|
+
[24, "3xl"]
|
|
247
|
+
]);
|
|
248
|
+
const BORDER_WIDTH_SCALE = new Map([
|
|
249
|
+
[1, ""],
|
|
250
|
+
[2, "2"],
|
|
251
|
+
[4, "4"],
|
|
252
|
+
[8, "8"]
|
|
253
|
+
]);
|
|
254
|
+
const Z_INDEX_SCALE = new Set([
|
|
255
|
+
0,
|
|
256
|
+
10,
|
|
257
|
+
20,
|
|
258
|
+
30,
|
|
259
|
+
40,
|
|
260
|
+
50
|
|
261
|
+
]);
|
|
262
|
+
const OPACITY_SCALE = new Set([
|
|
263
|
+
0,
|
|
264
|
+
5,
|
|
265
|
+
10,
|
|
266
|
+
15,
|
|
267
|
+
20,
|
|
268
|
+
25,
|
|
269
|
+
30,
|
|
270
|
+
35,
|
|
271
|
+
40,
|
|
272
|
+
45,
|
|
273
|
+
50,
|
|
274
|
+
55,
|
|
275
|
+
60,
|
|
276
|
+
65,
|
|
277
|
+
70,
|
|
278
|
+
75,
|
|
279
|
+
80,
|
|
280
|
+
85,
|
|
281
|
+
90,
|
|
282
|
+
95,
|
|
283
|
+
100
|
|
284
|
+
]);
|
|
285
|
+
const CLEAN_ARBITRARY_SOURCE_PROPERTIES = new Set([
|
|
286
|
+
"background-color",
|
|
287
|
+
"border-color",
|
|
288
|
+
"color",
|
|
289
|
+
"text-decoration-color"
|
|
290
|
+
]);
|
|
291
|
+
const REVIEW_ONLY_SOURCE_PROPERTIES = new Set([
|
|
292
|
+
"background-image",
|
|
293
|
+
"bottom",
|
|
294
|
+
"font-family",
|
|
295
|
+
"grid-template-columns",
|
|
296
|
+
"grid-template-rows",
|
|
297
|
+
"height",
|
|
298
|
+
"left",
|
|
299
|
+
"pseudo-elements",
|
|
300
|
+
"right",
|
|
301
|
+
"top",
|
|
302
|
+
"transform",
|
|
303
|
+
"transform-origin",
|
|
304
|
+
"width"
|
|
305
|
+
]);
|
|
306
|
+
const DISPLAY_MAP = {
|
|
307
|
+
contents: "contents",
|
|
308
|
+
flex: "flex",
|
|
309
|
+
"flow-root": "flow-root",
|
|
310
|
+
grid: "grid",
|
|
311
|
+
inline: "inline",
|
|
312
|
+
"inline-block": "inline-block",
|
|
313
|
+
"inline-flex": "inline-flex",
|
|
314
|
+
"inline-grid": "inline-grid",
|
|
315
|
+
none: "hidden"
|
|
316
|
+
};
|
|
317
|
+
const POSITION_MAP = {
|
|
318
|
+
absolute: "absolute",
|
|
319
|
+
fixed: "fixed",
|
|
320
|
+
relative: "relative",
|
|
321
|
+
sticky: "sticky"
|
|
322
|
+
};
|
|
323
|
+
const FLEX_DIRECTION_MAP = {
|
|
324
|
+
column: "flex-col",
|
|
325
|
+
"column-reverse": "flex-col-reverse",
|
|
326
|
+
"row-reverse": "flex-row-reverse"
|
|
327
|
+
};
|
|
328
|
+
const FLEX_WRAP_MAP = {
|
|
329
|
+
wrap: "flex-wrap",
|
|
330
|
+
"wrap-reverse": "flex-wrap-reverse"
|
|
331
|
+
};
|
|
332
|
+
const ALIGN_ITEMS_MAP = {
|
|
333
|
+
baseline: "items-baseline",
|
|
334
|
+
center: "items-center",
|
|
335
|
+
end: "items-end",
|
|
336
|
+
"flex-end": "items-end",
|
|
337
|
+
"flex-start": "items-start",
|
|
338
|
+
start: "items-start"
|
|
339
|
+
};
|
|
340
|
+
const JUSTIFY_CONTENT_MAP = {
|
|
341
|
+
center: "justify-center",
|
|
342
|
+
end: "justify-end",
|
|
343
|
+
"flex-end": "justify-end",
|
|
344
|
+
"flex-start": "justify-start",
|
|
345
|
+
left: "justify-start",
|
|
346
|
+
right: "justify-end",
|
|
347
|
+
"space-around": "justify-around",
|
|
348
|
+
"space-between": "justify-between",
|
|
349
|
+
"space-evenly": "justify-evenly",
|
|
350
|
+
start: "justify-start"
|
|
351
|
+
};
|
|
352
|
+
const GRID_AUTO_FLOW_MAP = {
|
|
353
|
+
column: "grid-flow-col",
|
|
354
|
+
"column dense": "grid-flow-col-dense",
|
|
355
|
+
dense: "grid-flow-dense",
|
|
356
|
+
"row dense": "grid-flow-row-dense"
|
|
357
|
+
};
|
|
358
|
+
const TEXT_ALIGN_MAP = {
|
|
359
|
+
center: "text-center",
|
|
360
|
+
end: "text-end",
|
|
361
|
+
justify: "text-justify",
|
|
362
|
+
right: "text-right"
|
|
363
|
+
};
|
|
364
|
+
const TEXT_TRANSFORM_MAP = {
|
|
365
|
+
capitalize: "capitalize",
|
|
366
|
+
lowercase: "lowercase",
|
|
367
|
+
uppercase: "uppercase"
|
|
368
|
+
};
|
|
369
|
+
const WHITE_SPACE_MAP = {
|
|
370
|
+
"break-spaces": "whitespace-break-spaces",
|
|
371
|
+
nowrap: "whitespace-nowrap",
|
|
372
|
+
pre: "whitespace-pre",
|
|
373
|
+
"pre-line": "whitespace-pre-line",
|
|
374
|
+
"pre-wrap": "whitespace-pre-wrap"
|
|
375
|
+
};
|
|
376
|
+
const LIST_STYLE_MAP = {
|
|
377
|
+
decimal: "list-decimal",
|
|
378
|
+
none: "list-none"
|
|
379
|
+
};
|
|
380
|
+
const OBJECT_FIT_MAP = {
|
|
381
|
+
contain: "object-contain",
|
|
382
|
+
cover: "object-cover",
|
|
383
|
+
none: "object-none",
|
|
384
|
+
"scale-down": "object-scale-down"
|
|
385
|
+
};
|
|
386
|
+
const OVERFLOW_MAP = {
|
|
387
|
+
auto: "auto",
|
|
388
|
+
clip: "clip",
|
|
389
|
+
hidden: "hidden",
|
|
390
|
+
scroll: "scroll",
|
|
391
|
+
visible: "visible"
|
|
392
|
+
};
|
|
393
|
+
const BORDER_STYLE_MAP = {
|
|
394
|
+
dashed: "border-dashed",
|
|
395
|
+
dotted: "border-dotted",
|
|
396
|
+
double: "border-double"
|
|
397
|
+
};
|
|
398
|
+
const FONT_WEIGHT_MAP = {
|
|
399
|
+
"100": "thin",
|
|
400
|
+
"200": "extralight",
|
|
401
|
+
"300": "light",
|
|
402
|
+
"500": "medium",
|
|
403
|
+
"600": "semibold",
|
|
404
|
+
"700": "bold",
|
|
405
|
+
"800": "extrabold",
|
|
406
|
+
"900": "black"
|
|
407
|
+
};
|
|
408
|
+
const POSITION_KEYWORD_MAP = {
|
|
409
|
+
"0% 0%": "top-left",
|
|
410
|
+
"0% 100%": "bottom-left",
|
|
411
|
+
"0% 50%": "left",
|
|
412
|
+
"100% 0%": "top-right",
|
|
413
|
+
"100% 100%": "bottom-right",
|
|
414
|
+
"100% 50%": "right",
|
|
415
|
+
"50% 0%": "top",
|
|
416
|
+
"50% 100%": "bottom"
|
|
417
|
+
};
|
|
418
|
+
const DIMENSION_KEYWORD_MAP = {
|
|
419
|
+
"100%": "full",
|
|
420
|
+
"100vh": "screen",
|
|
421
|
+
"100vw": "screen",
|
|
422
|
+
"fit-content": "fit",
|
|
423
|
+
"max-content": "max",
|
|
424
|
+
"min-content": "min"
|
|
425
|
+
};
|
|
426
|
+
const FONT_FAMILY_MAP = [
|
|
427
|
+
{
|
|
428
|
+
keyword: "monospace",
|
|
429
|
+
utility: "font-mono"
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
keyword: "sans-serif",
|
|
433
|
+
utility: "font-sans"
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
keyword: "system-ui",
|
|
437
|
+
utility: "font-sans"
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
keyword: "serif",
|
|
441
|
+
utility: "font-serif"
|
|
442
|
+
}
|
|
443
|
+
];
|
|
444
|
+
const roundToTwo = (value) => Math.round(value * 100) / 100;
|
|
445
|
+
const dedupe = (values) => [...new Set((values ?? []).filter(Boolean))];
|
|
446
|
+
const allEqual = (values) => values.every((value) => value === values[0]);
|
|
447
|
+
const normalizeWhitespace = (value) => value.replaceAll(/\s+/g, " ").trim();
|
|
448
|
+
const parseLength = (value) => {
|
|
449
|
+
const match = value.trim().match(LENGTH_PATTERN);
|
|
450
|
+
if (!match) return null;
|
|
451
|
+
return {
|
|
452
|
+
unit: match[2] ?? "px",
|
|
453
|
+
value: Number(match[1])
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
const isZeroLength = (value) => {
|
|
457
|
+
const length = parseLength(value);
|
|
458
|
+
return Boolean(length && Math.abs(length.value) <= .01);
|
|
459
|
+
};
|
|
460
|
+
const isTransparentColor = (value) => {
|
|
461
|
+
const normalized = value.trim().replaceAll(/\s+/g, "").toLowerCase();
|
|
462
|
+
return normalized === "rgba(0,0,0,0)" || normalized === "transparent";
|
|
463
|
+
};
|
|
464
|
+
const normalizeColor = (value) => {
|
|
465
|
+
const trimmed = value.trim().toLowerCase();
|
|
466
|
+
if (trimmed === "transparent" || isTransparentColor(trimmed)) return "transparent";
|
|
467
|
+
const rgbMatch = trimmed.match(RGB_PATTERN);
|
|
468
|
+
if (!rgbMatch) return trimmed;
|
|
469
|
+
const { 1: redRaw, 2: greenRaw, 3: blueRaw, 4: alpha } = rgbMatch;
|
|
470
|
+
const red = Number(redRaw).toString(16).padStart(2, "0");
|
|
471
|
+
const green = Number(greenRaw).toString(16).padStart(2, "0");
|
|
472
|
+
const blue = Number(blueRaw).toString(16).padStart(2, "0");
|
|
473
|
+
if (!alpha || alpha === "1") return `#${red}${green}${blue}`;
|
|
474
|
+
return trimmed.replaceAll(/\s+/g, "");
|
|
475
|
+
};
|
|
476
|
+
const sanitizeArbitraryValue = (value) => value.trim().replaceAll(/\s*,\s*/g, ",").replaceAll(/\s*\/\s*/g, "/").replaceAll(/\s+/g, "_");
|
|
477
|
+
const createArbitraryUtility = (prefix, value) => `${prefix}-[${sanitizeArbitraryValue(value)}]`;
|
|
478
|
+
const createArbitraryPropertyClass = (property, value) => `[${property}:${sanitizeArbitraryValue(value)}]`;
|
|
479
|
+
const lookupMappedUtility = (map, value) => map[value] ?? null;
|
|
480
|
+
const labelFromConfidence = (confidence) => {
|
|
481
|
+
if (confidence >= .85) return "high";
|
|
482
|
+
if (confidence >= .62) return "medium";
|
|
483
|
+
return "low";
|
|
484
|
+
};
|
|
485
|
+
const addMatch = (accumulator, match) => {
|
|
486
|
+
const utility = match.utility.trim();
|
|
487
|
+
if (!utility) return;
|
|
488
|
+
const nextMatch = {
|
|
489
|
+
...match,
|
|
490
|
+
label: labelFromConfidence(match.confidence),
|
|
491
|
+
notes: dedupe(match.notes),
|
|
492
|
+
sourceProperties: dedupe(match.sourceProperties),
|
|
493
|
+
sourceValues: dedupe(match.sourceValues),
|
|
494
|
+
utility
|
|
495
|
+
};
|
|
496
|
+
const existing = accumulator.matches.find((entry) => entry.utility === utility);
|
|
497
|
+
if (existing) {
|
|
498
|
+
existing.confidence = Math.max(existing.confidence, nextMatch.confidence);
|
|
499
|
+
existing.label = labelFromConfidence(existing.confidence);
|
|
500
|
+
existing.notes = dedupe([...existing.notes, ...nextMatch.notes]);
|
|
501
|
+
existing.sourceProperties = dedupe([...existing.sourceProperties, ...nextMatch.sourceProperties]);
|
|
502
|
+
existing.sourceValues = dedupe([...existing.sourceValues, ...nextMatch.sourceValues]);
|
|
503
|
+
} else accumulator.matches.push(nextMatch);
|
|
504
|
+
if (accumulator.classSet.has(utility)) return;
|
|
505
|
+
accumulator.classSet.add(utility);
|
|
506
|
+
accumulator.classes.push(utility);
|
|
507
|
+
};
|
|
508
|
+
const addUnsupported = (accumulator, property, value, reason) => {
|
|
509
|
+
const key = `${property}:${value}:${reason}`;
|
|
510
|
+
if (accumulator.unsupported.some((entry) => `${entry.property}:${entry.value}:${entry.reason}` === key)) return;
|
|
511
|
+
accumulator.unsupported.push({
|
|
512
|
+
property,
|
|
513
|
+
reason,
|
|
514
|
+
value
|
|
515
|
+
});
|
|
516
|
+
};
|
|
517
|
+
const addCandidateMatch = (accumulator, property, value, candidate) => {
|
|
518
|
+
if (!(value && candidate)) return;
|
|
519
|
+
addMatch(accumulator, {
|
|
520
|
+
confidence: candidate.confidence,
|
|
521
|
+
notes: candidate.notes ?? [],
|
|
522
|
+
sourceProperties: property.includes("|") ? property.split("|") : [property],
|
|
523
|
+
sourceValues: [value],
|
|
524
|
+
strategy: candidate.strategy,
|
|
525
|
+
utility: candidate.utility
|
|
526
|
+
});
|
|
527
|
+
};
|
|
528
|
+
const addMappedUtility = (accumulator, property, value, map, shouldAdd = true) => {
|
|
529
|
+
if (!(shouldAdd && value)) return;
|
|
530
|
+
const utility = lookupMappedUtility(map, value);
|
|
531
|
+
if (!utility) {
|
|
532
|
+
addUnsupported(accumulator, property, value, `${property} needs manual review.`);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
addMatch(accumulator, {
|
|
536
|
+
confidence: HIGH_CONFIDENCE,
|
|
537
|
+
sourceProperties: [property],
|
|
538
|
+
sourceValues: [value],
|
|
539
|
+
strategy: "semantic",
|
|
540
|
+
utility
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
const addArbitraryPropertyMatch = (accumulator, property, value, note) => {
|
|
544
|
+
if (!value || value === "none") return;
|
|
545
|
+
addMatch(accumulator, {
|
|
546
|
+
confidence: LOW_CONFIDENCE,
|
|
547
|
+
notes: [note],
|
|
548
|
+
sourceProperties: [property],
|
|
549
|
+
sourceValues: [value],
|
|
550
|
+
strategy: "arbitrary",
|
|
551
|
+
utility: createArbitraryPropertyClass(property, value)
|
|
552
|
+
});
|
|
553
|
+
};
|
|
554
|
+
const shouldEmitInheritedValue = (property, value, parentValue) => {
|
|
555
|
+
if (!value) return false;
|
|
556
|
+
if (!parentValue) return true;
|
|
557
|
+
if (property === "color") return normalizeColor(value) !== normalizeColor(parentValue);
|
|
558
|
+
return value !== parentValue;
|
|
559
|
+
};
|
|
560
|
+
const shouldMapDimension = (property, value) => {
|
|
561
|
+
if (!value) return false;
|
|
562
|
+
if (property.startsWith("min-")) return value !== "0px" && value !== "auto";
|
|
563
|
+
if (property.startsWith("max-")) return value !== "none";
|
|
564
|
+
return value !== "auto";
|
|
565
|
+
};
|
|
566
|
+
const buildSpacingCandidate = (prefix, value, allowNegative) => {
|
|
567
|
+
if (isZeroLength(value)) return null;
|
|
568
|
+
if (value === "auto" && prefix.startsWith("m")) return {
|
|
569
|
+
confidence: HIGH_CONFIDENCE,
|
|
570
|
+
strategy: "semantic",
|
|
571
|
+
utility: `${prefix}-auto`
|
|
572
|
+
};
|
|
573
|
+
const length = parseLength(value);
|
|
574
|
+
if (!length) return {
|
|
575
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
576
|
+
notes: ["Spacing required an arbitrary value."],
|
|
577
|
+
strategy: "arbitrary",
|
|
578
|
+
utility: createArbitraryUtility(prefix, value)
|
|
579
|
+
};
|
|
580
|
+
if (length.value < 0 && !allowNegative) return null;
|
|
581
|
+
const token = length.unit === "px" ? SPACING_SCALE.get(Math.abs(length.value)) : null;
|
|
582
|
+
if (!token) return {
|
|
583
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
584
|
+
notes: ["Spacing required an arbitrary value."],
|
|
585
|
+
strategy: "arbitrary",
|
|
586
|
+
utility: createArbitraryUtility(prefix, value)
|
|
587
|
+
};
|
|
588
|
+
const baseUtility = token === "px" ? `${prefix}-px` : `${prefix}-${token}`;
|
|
589
|
+
return {
|
|
590
|
+
confidence: HIGH_CONFIDENCE,
|
|
591
|
+
strategy: "scale",
|
|
592
|
+
utility: length.value < 0 ? `-${baseUtility}` : baseUtility
|
|
593
|
+
};
|
|
594
|
+
};
|
|
595
|
+
const buildDimensionCandidate = (prefix, value, options) => {
|
|
596
|
+
if (value === "auto") return {
|
|
597
|
+
confidence: HIGH_CONFIDENCE,
|
|
598
|
+
strategy: "semantic",
|
|
599
|
+
utility: `${prefix}-auto`
|
|
600
|
+
};
|
|
601
|
+
const keywordSuffix = DIMENSION_KEYWORD_MAP[value];
|
|
602
|
+
if (keywordSuffix) return {
|
|
603
|
+
confidence: HIGH_CONFIDENCE,
|
|
604
|
+
strategy: "semantic",
|
|
605
|
+
utility: `${prefix}-${keywordSuffix}`
|
|
606
|
+
};
|
|
607
|
+
const length = parseLength(value);
|
|
608
|
+
const token = length && length.unit === "px" ? SPACING_SCALE.get(Math.abs(length.value)) : null;
|
|
609
|
+
if (token) return {
|
|
610
|
+
confidence: options.confidence,
|
|
611
|
+
notes: options.note ? [options.note] : [],
|
|
612
|
+
strategy: "scale",
|
|
613
|
+
utility: token === "px" ? `${prefix}-px` : `${prefix}-${token}`
|
|
614
|
+
};
|
|
615
|
+
return {
|
|
616
|
+
confidence: options.confidence,
|
|
617
|
+
notes: options.note ? [options.note] : ["Length required an arbitrary value."],
|
|
618
|
+
strategy: "arbitrary",
|
|
619
|
+
utility: createArbitraryUtility(prefix, value)
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
const buildScaleCandidate = (prefix, value, scale, note) => {
|
|
623
|
+
const length = parseLength(value);
|
|
624
|
+
const token = length && length.unit === "px" ? scale.get(length.value) : null;
|
|
625
|
+
if (token) return {
|
|
626
|
+
confidence: HIGH_CONFIDENCE,
|
|
627
|
+
strategy: "scale",
|
|
628
|
+
utility: `${prefix}-${token}`
|
|
629
|
+
};
|
|
630
|
+
return {
|
|
631
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
632
|
+
notes: [note],
|
|
633
|
+
strategy: "arbitrary",
|
|
634
|
+
utility: createArbitraryUtility(prefix, value)
|
|
635
|
+
};
|
|
636
|
+
};
|
|
637
|
+
const buildColorCandidate = (prefix, value) => {
|
|
638
|
+
const normalized = normalizeColor(value);
|
|
639
|
+
let semanticUtility = null;
|
|
640
|
+
if (normalized === "transparent") semanticUtility = `${prefix}-transparent`;
|
|
641
|
+
else if (normalized === "#000000") semanticUtility = `${prefix}-black`;
|
|
642
|
+
else if (normalized === "#ffffff") semanticUtility = `${prefix}-white`;
|
|
643
|
+
if (semanticUtility) return {
|
|
644
|
+
confidence: HIGH_CONFIDENCE,
|
|
645
|
+
strategy: "semantic",
|
|
646
|
+
utility: semanticUtility
|
|
647
|
+
};
|
|
648
|
+
return {
|
|
649
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
650
|
+
strategy: "arbitrary",
|
|
651
|
+
utility: createArbitraryUtility(prefix, normalized)
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
const buildFontFamilyCandidate = (value) => {
|
|
655
|
+
const normalized = value.toLowerCase();
|
|
656
|
+
for (const { keyword, utility } of FONT_FAMILY_MAP) if (normalized.includes(keyword)) return {
|
|
657
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
658
|
+
notes: ["Mapped to the nearest generic Tailwind family."],
|
|
659
|
+
strategy: "heuristic",
|
|
660
|
+
utility
|
|
661
|
+
};
|
|
662
|
+
return {
|
|
663
|
+
confidence: LOW_CONFIDENCE,
|
|
664
|
+
notes: ["Font family required an arbitrary property utility."],
|
|
665
|
+
strategy: "arbitrary",
|
|
666
|
+
utility: createArbitraryPropertyClass("font-family", value)
|
|
667
|
+
};
|
|
668
|
+
};
|
|
669
|
+
const buildBorderWidthCandidate = (prefix, value) => {
|
|
670
|
+
const length = parseLength(value);
|
|
671
|
+
const token = length && length.unit === "px" ? BORDER_WIDTH_SCALE.get(length.value) : null;
|
|
672
|
+
if (token !== void 0) return {
|
|
673
|
+
confidence: HIGH_CONFIDENCE,
|
|
674
|
+
strategy: "scale",
|
|
675
|
+
utility: token ? `${prefix}-${token}` : prefix
|
|
676
|
+
};
|
|
677
|
+
return {
|
|
678
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
679
|
+
notes: ["Border width required an arbitrary value."],
|
|
680
|
+
strategy: "arbitrary",
|
|
681
|
+
utility: createArbitraryUtility(prefix, value)
|
|
682
|
+
};
|
|
683
|
+
};
|
|
684
|
+
const buildBorderColorCandidate = (value) => buildColorCandidate("border", value);
|
|
685
|
+
const buildRadiusCandidate = (prefix, value) => {
|
|
686
|
+
const length = parseLength(value);
|
|
687
|
+
if (length && length.value >= 9999) return {
|
|
688
|
+
confidence: HIGH_CONFIDENCE,
|
|
689
|
+
strategy: "semantic",
|
|
690
|
+
utility: `${prefix}-full`
|
|
691
|
+
};
|
|
692
|
+
const token = length && length.unit === "px" ? RADIUS_SCALE.get(length.value) : null;
|
|
693
|
+
if (token !== void 0) return {
|
|
694
|
+
confidence: HIGH_CONFIDENCE,
|
|
695
|
+
strategy: "scale",
|
|
696
|
+
utility: token ? `${prefix}-${token}` : prefix
|
|
697
|
+
};
|
|
698
|
+
return {
|
|
699
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
700
|
+
notes: ["Border radius required an arbitrary value."],
|
|
701
|
+
strategy: "arbitrary",
|
|
702
|
+
utility: createArbitraryUtility(prefix, value)
|
|
703
|
+
};
|
|
704
|
+
};
|
|
705
|
+
const buildZIndexCandidate = (value) => {
|
|
706
|
+
const numericValue = Number(value);
|
|
707
|
+
if (Number.isFinite(numericValue) && Z_INDEX_SCALE.has(numericValue)) return {
|
|
708
|
+
confidence: HIGH_CONFIDENCE,
|
|
709
|
+
strategy: "scale",
|
|
710
|
+
utility: `z-${numericValue}`
|
|
711
|
+
};
|
|
712
|
+
return {
|
|
713
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
714
|
+
notes: ["Resolved z-index required an arbitrary value."],
|
|
715
|
+
strategy: "arbitrary",
|
|
716
|
+
utility: `z-[${sanitizeArbitraryValue(value)}]`
|
|
717
|
+
};
|
|
718
|
+
};
|
|
719
|
+
const addGapMatches = (accumulator, gap, rowGap, columnGap) => {
|
|
720
|
+
if (gap && gap !== "normal" && !isZeroLength(gap)) {
|
|
721
|
+
addCandidateMatch(accumulator, "gap", gap, buildSpacingCandidate("gap", gap, false));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
addCandidateMatch(accumulator, "row-gap", rowGap, rowGap ? buildSpacingCandidate("gap-y", rowGap, false) : null);
|
|
725
|
+
addCandidateMatch(accumulator, "column-gap", columnGap, columnGap ? buildSpacingCandidate("gap-x", columnGap, false) : null);
|
|
726
|
+
};
|
|
727
|
+
const addFlexNumberMatch = (accumulator, property, value, prefix) => {
|
|
728
|
+
if (!value) return;
|
|
729
|
+
let utility = createArbitraryUtility(prefix, value);
|
|
730
|
+
if (value === "1") utility = prefix;
|
|
731
|
+
else if (value === "0") utility = `${prefix}-0`;
|
|
732
|
+
const confidence = value === "0" || value === "1" ? HIGH_CONFIDENCE : LOW_CONFIDENCE;
|
|
733
|
+
const strategy = value === "0" || value === "1" ? "semantic" : "arbitrary";
|
|
734
|
+
addMatch(accumulator, {
|
|
735
|
+
confidence,
|
|
736
|
+
notes: strategy === "semantic" ? [] : [`${property} required an arbitrary value.`],
|
|
737
|
+
sourceProperties: [property],
|
|
738
|
+
sourceValues: [value],
|
|
739
|
+
strategy,
|
|
740
|
+
utility
|
|
741
|
+
});
|
|
742
|
+
};
|
|
743
|
+
const addAxisSpacingMatch = (accumulator, prefix, first, second, allowNegative) => {
|
|
744
|
+
if (!(first[1] && second[1]) || first[1] !== second[1]) return;
|
|
745
|
+
addCandidateMatch(accumulator, `${first[0]}|${second[0]}`, first[1], buildSpacingCandidate(prefix, first[1], allowNegative));
|
|
746
|
+
};
|
|
747
|
+
const addEdgeSpacingMatch = (accumulator, prefix, entry, allowNegative) => {
|
|
748
|
+
if (!entry[1] || isZeroLength(entry[1])) return;
|
|
749
|
+
addCandidateMatch(accumulator, entry[0], entry[1], buildSpacingCandidate(prefix, entry[1], allowNegative));
|
|
750
|
+
};
|
|
751
|
+
const addBoxSpacingMatches = (accumulator, prefix, entries) => {
|
|
752
|
+
const values = entries.map((entry) => entry[1]);
|
|
753
|
+
if (values.some((value) => !value)) return;
|
|
754
|
+
const normalizedValues = values;
|
|
755
|
+
if (normalizedValues.every((value) => isZeroLength(value))) return;
|
|
756
|
+
if (allEqual(normalizedValues)) {
|
|
757
|
+
addCandidateMatch(accumulator, entries[0][0], normalizedValues[0], buildSpacingCandidate(prefix, normalizedValues[0], prefix === "m"));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
addAxisSpacingMatch(accumulator, `${prefix}y`, entries[0], entries[2], prefix === "m");
|
|
761
|
+
addAxisSpacingMatch(accumulator, `${prefix}x`, entries[1], entries[3], prefix === "m");
|
|
762
|
+
addEdgeSpacingMatch(accumulator, `${prefix}t`, entries[0], prefix === "m");
|
|
763
|
+
addEdgeSpacingMatch(accumulator, `${prefix}r`, entries[1], prefix === "m");
|
|
764
|
+
addEdgeSpacingMatch(accumulator, `${prefix}b`, entries[2], prefix === "m");
|
|
765
|
+
addEdgeSpacingMatch(accumulator, `${prefix}l`, entries[3], prefix === "m");
|
|
766
|
+
};
|
|
767
|
+
const addColorMatch = (accumulator, prefix, property, value, shouldAdd) => {
|
|
768
|
+
if (!(shouldAdd && value)) return;
|
|
769
|
+
addCandidateMatch(accumulator, property, value, buildColorCandidate(prefix, value));
|
|
770
|
+
};
|
|
771
|
+
const addFontFamilyMatch = (accumulator, value, shouldAdd) => {
|
|
772
|
+
if (!(shouldAdd && value)) return;
|
|
773
|
+
addCandidateMatch(accumulator, "font-family", value, buildFontFamilyCandidate(value));
|
|
774
|
+
};
|
|
775
|
+
const addScaleMatch = (accumulator, property, value, shouldAdd, scale, prefix, note) => {
|
|
776
|
+
if (!(shouldAdd && value)) return;
|
|
777
|
+
addCandidateMatch(accumulator, property, value, buildScaleCandidate(prefix, value, scale, note));
|
|
778
|
+
};
|
|
779
|
+
const addFontWeightMatch = (accumulator, value, shouldAdd) => {
|
|
780
|
+
if (!(shouldAdd && value) || value === "400") return;
|
|
781
|
+
const utility = lookupMappedUtility(FONT_WEIGHT_MAP, value);
|
|
782
|
+
addMatch(accumulator, {
|
|
783
|
+
confidence: utility ? HIGH_CONFIDENCE : LOW_CONFIDENCE,
|
|
784
|
+
notes: utility ? [] : ["Font weight required an arbitrary value."],
|
|
785
|
+
sourceProperties: ["font-weight"],
|
|
786
|
+
sourceValues: [value],
|
|
787
|
+
strategy: utility ? "semantic" : "arbitrary",
|
|
788
|
+
utility: utility ? `font-${utility}` : createArbitraryUtility("font", value)
|
|
789
|
+
});
|
|
790
|
+
};
|
|
791
|
+
const addFontStyleMatch = (accumulator, value, shouldAdd) => {
|
|
792
|
+
if (!(shouldAdd && value) || value === "normal") return;
|
|
793
|
+
addMatch(accumulator, {
|
|
794
|
+
confidence: HIGH_CONFIDENCE,
|
|
795
|
+
sourceProperties: ["font-style"],
|
|
796
|
+
sourceValues: [value],
|
|
797
|
+
strategy: "semantic",
|
|
798
|
+
utility: value === "italic" ? "italic" : "not-italic"
|
|
799
|
+
});
|
|
800
|
+
};
|
|
801
|
+
const addTrackingMatch = (accumulator, value, shouldAdd) => {
|
|
802
|
+
if (!(shouldAdd && value) || value === "normal" || isZeroLength(value)) return;
|
|
803
|
+
addMatch(accumulator, {
|
|
804
|
+
confidence: LOW_CONFIDENCE,
|
|
805
|
+
notes: ["Letter spacing required an arbitrary value."],
|
|
806
|
+
sourceProperties: ["letter-spacing"],
|
|
807
|
+
sourceValues: [value],
|
|
808
|
+
strategy: "arbitrary",
|
|
809
|
+
utility: createArbitraryUtility("tracking", value)
|
|
810
|
+
});
|
|
811
|
+
};
|
|
812
|
+
const addDecorationLineMatches = (accumulator, value) => {
|
|
813
|
+
if (!value || value === "none") return;
|
|
814
|
+
for (const part of value.split(WHITESPACE_SPLIT_PATTERN)) {
|
|
815
|
+
const utility = {
|
|
816
|
+
"line-through": "line-through",
|
|
817
|
+
overline: "overline",
|
|
818
|
+
underline: "underline"
|
|
819
|
+
}[part] ?? null;
|
|
820
|
+
if (!utility) continue;
|
|
821
|
+
addMatch(accumulator, {
|
|
822
|
+
confidence: HIGH_CONFIDENCE,
|
|
823
|
+
sourceProperties: ["text-decoration-line"],
|
|
824
|
+
sourceValues: [value],
|
|
825
|
+
strategy: "semantic",
|
|
826
|
+
utility
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
const addUniformBorderMatch = (element, accumulator) => {
|
|
831
|
+
const widths = [
|
|
832
|
+
element.styles["border-top-width"],
|
|
833
|
+
element.styles["border-right-width"],
|
|
834
|
+
element.styles["border-bottom-width"],
|
|
835
|
+
element.styles["border-left-width"]
|
|
836
|
+
];
|
|
837
|
+
const styles = [
|
|
838
|
+
element.styles["border-top-style"],
|
|
839
|
+
element.styles["border-right-style"],
|
|
840
|
+
element.styles["border-bottom-style"],
|
|
841
|
+
element.styles["border-left-style"]
|
|
842
|
+
];
|
|
843
|
+
const colors = [
|
|
844
|
+
element.styles["border-top-color"],
|
|
845
|
+
element.styles["border-right-color"],
|
|
846
|
+
element.styles["border-bottom-color"],
|
|
847
|
+
element.styles["border-left-color"]
|
|
848
|
+
];
|
|
849
|
+
if (widths.some((value) => !value || isZeroLength(value)) || styles.some((value) => !value || value === "none")) return;
|
|
850
|
+
if (!(allEqual(widths) && allEqual(styles) && allEqual(colors))) {
|
|
851
|
+
addUnsupported(accumulator, "border", "mixed sides", "Per-side border variations need manual review.");
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const width = widths[0];
|
|
855
|
+
const borderStyle = styles[0];
|
|
856
|
+
const color = colors[0];
|
|
857
|
+
addCandidateMatch(accumulator, "border-width", width, buildBorderWidthCandidate("border", width));
|
|
858
|
+
if (borderStyle !== "solid") {
|
|
859
|
+
const utility = lookupMappedUtility(BORDER_STYLE_MAP, borderStyle);
|
|
860
|
+
if (utility) addMatch(accumulator, {
|
|
861
|
+
confidence: HIGH_CONFIDENCE,
|
|
862
|
+
sourceProperties: ["border-style"],
|
|
863
|
+
sourceValues: [borderStyle],
|
|
864
|
+
strategy: "semantic",
|
|
865
|
+
utility
|
|
866
|
+
});
|
|
867
|
+
else addUnsupported(accumulator, "border-style", borderStyle, "Border style needs manual review.");
|
|
868
|
+
}
|
|
869
|
+
addCandidateMatch(accumulator, "border-color", color, buildBorderColorCandidate(color));
|
|
870
|
+
};
|
|
871
|
+
const addRadiusMatches = (element, accumulator) => {
|
|
872
|
+
const values = [
|
|
873
|
+
element.styles["border-top-left-radius"],
|
|
874
|
+
element.styles["border-top-right-radius"],
|
|
875
|
+
element.styles["border-bottom-right-radius"],
|
|
876
|
+
element.styles["border-bottom-left-radius"]
|
|
877
|
+
];
|
|
878
|
+
if (values.some((value) => !value) || values.every((value) => isZeroLength(value))) return;
|
|
879
|
+
const radiusValues = values;
|
|
880
|
+
if (allEqual(radiusValues)) {
|
|
881
|
+
addCandidateMatch(accumulator, "border-radius", radiusValues[0], buildRadiusCandidate("rounded", radiusValues[0]));
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
addUnsupported(accumulator, "border-radius", radiusValues.join(", "), "Mixed corner radii need manual review.");
|
|
885
|
+
};
|
|
886
|
+
const addShadowMatch = (accumulator, value) => {
|
|
887
|
+
if (!value || value === "none") return;
|
|
888
|
+
addMatch(accumulator, {
|
|
889
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
890
|
+
sourceProperties: ["box-shadow"],
|
|
891
|
+
sourceValues: [value],
|
|
892
|
+
strategy: "arbitrary",
|
|
893
|
+
utility: createArbitraryUtility("shadow", value)
|
|
894
|
+
});
|
|
895
|
+
};
|
|
896
|
+
const addOpacityMatch = (accumulator, value) => {
|
|
897
|
+
if (!value || value === "1") return;
|
|
898
|
+
const numericValue = Number(value);
|
|
899
|
+
if (!Number.isFinite(numericValue)) return;
|
|
900
|
+
const percent = Math.round(numericValue * 100);
|
|
901
|
+
const semantic = OPACITY_SCALE.has(percent);
|
|
902
|
+
addMatch(accumulator, {
|
|
903
|
+
confidence: semantic ? MEDIUM_CONFIDENCE : LOW_CONFIDENCE,
|
|
904
|
+
notes: semantic ? [] : ["Opacity required an arbitrary value."],
|
|
905
|
+
sourceProperties: ["opacity"],
|
|
906
|
+
sourceValues: [value],
|
|
907
|
+
strategy: semantic ? "scale" : "arbitrary",
|
|
908
|
+
utility: semantic ? `opacity-${percent}` : createArbitraryUtility("opacity", value)
|
|
909
|
+
});
|
|
910
|
+
};
|
|
911
|
+
const addPositionMatch = (accumulator, prefix, property, value) => {
|
|
912
|
+
if (!value || value === "50% 50%") return;
|
|
913
|
+
const keyword = POSITION_KEYWORD_MAP[normalizeWhitespace(value)];
|
|
914
|
+
const named = keyword ? `${prefix}-${keyword}` : null;
|
|
915
|
+
addMatch(accumulator, {
|
|
916
|
+
confidence: named ? HIGH_CONFIDENCE : MEDIUM_CONFIDENCE,
|
|
917
|
+
notes: named ? [] : [`${property} required an arbitrary value.`],
|
|
918
|
+
sourceProperties: [property],
|
|
919
|
+
sourceValues: [value],
|
|
920
|
+
strategy: named ? "semantic" : "arbitrary",
|
|
921
|
+
utility: named ?? createArbitraryUtility(prefix, value)
|
|
922
|
+
});
|
|
923
|
+
};
|
|
924
|
+
const addOverflowAxisMatch = (accumulator, property, value) => {
|
|
925
|
+
if (value === "visible") return;
|
|
926
|
+
const suffix = lookupMappedUtility(OVERFLOW_MAP, value);
|
|
927
|
+
if (!suffix) {
|
|
928
|
+
addUnsupported(accumulator, property, value, "Overflow needs manual review.");
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
addMatch(accumulator, {
|
|
932
|
+
confidence: HIGH_CONFIDENCE,
|
|
933
|
+
sourceProperties: [property],
|
|
934
|
+
sourceValues: [value],
|
|
935
|
+
strategy: "semantic",
|
|
936
|
+
utility: `${property}-${suffix}`
|
|
937
|
+
});
|
|
938
|
+
};
|
|
939
|
+
const addObjectPositionMatch = (accumulator, value) => {
|
|
940
|
+
addPositionMatch(accumulator, "object", "object-position", value);
|
|
941
|
+
};
|
|
942
|
+
const mapDisplay = (context, accumulator) => {
|
|
943
|
+
const { display } = context.element.styles;
|
|
944
|
+
if (!display || display === "block") return;
|
|
945
|
+
const utility = lookupMappedUtility(DISPLAY_MAP, display);
|
|
946
|
+
if (utility) {
|
|
947
|
+
addMatch(accumulator, {
|
|
948
|
+
confidence: HIGH_CONFIDENCE,
|
|
949
|
+
sourceProperties: ["display"],
|
|
950
|
+
sourceValues: [display],
|
|
951
|
+
strategy: "semantic",
|
|
952
|
+
utility
|
|
953
|
+
});
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
addUnsupported(accumulator, "display", display, "Display needs manual review.");
|
|
957
|
+
};
|
|
958
|
+
const mapPositioning = (context, accumulator) => {
|
|
959
|
+
const { styles } = context.element;
|
|
960
|
+
const { position } = styles;
|
|
961
|
+
if (position && position !== "static") {
|
|
962
|
+
const utility = lookupMappedUtility(POSITION_MAP, position);
|
|
963
|
+
if (utility) addMatch(accumulator, {
|
|
964
|
+
confidence: HIGH_CONFIDENCE,
|
|
965
|
+
sourceProperties: ["position"],
|
|
966
|
+
sourceValues: [position],
|
|
967
|
+
strategy: "semantic",
|
|
968
|
+
utility
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
if (!position || position === "static") return;
|
|
972
|
+
for (const property of [
|
|
973
|
+
"top",
|
|
974
|
+
"right",
|
|
975
|
+
"bottom",
|
|
976
|
+
"left"
|
|
977
|
+
]) {
|
|
978
|
+
const value = styles[property];
|
|
979
|
+
if (!value || value === "auto") continue;
|
|
980
|
+
addCandidateMatch(accumulator, property, value, buildDimensionCandidate(property, value, {
|
|
981
|
+
confidence: LOW_CONFIDENCE,
|
|
982
|
+
note: "Computed insets are layout-derived and need review."
|
|
983
|
+
}));
|
|
984
|
+
}
|
|
985
|
+
const zIndex = styles["z-index"];
|
|
986
|
+
if (!zIndex || zIndex === "auto") return;
|
|
987
|
+
addCandidateMatch(accumulator, "z-index", zIndex, buildZIndexCandidate(zIndex));
|
|
988
|
+
};
|
|
989
|
+
const mapFlexLayout = (context, accumulator) => {
|
|
990
|
+
const { styles } = context.element;
|
|
991
|
+
const { display } = styles;
|
|
992
|
+
if (display !== "flex" && display !== "inline-flex") return;
|
|
993
|
+
addMappedUtility(accumulator, "flex-direction", styles["flex-direction"], FLEX_DIRECTION_MAP);
|
|
994
|
+
addMappedUtility(accumulator, "flex-wrap", styles["flex-wrap"], FLEX_WRAP_MAP);
|
|
995
|
+
addMappedUtility(accumulator, "justify-content", styles["justify-content"], JUSTIFY_CONTENT_MAP);
|
|
996
|
+
addMappedUtility(accumulator, "align-items", styles["align-items"], ALIGN_ITEMS_MAP);
|
|
997
|
+
addGapMatches(accumulator, styles.gap, styles["row-gap"], styles["column-gap"]);
|
|
998
|
+
addFlexNumberMatch(accumulator, "flex-grow", styles["flex-grow"], "grow");
|
|
999
|
+
addFlexNumberMatch(accumulator, "flex-shrink", styles["flex-shrink"], "shrink");
|
|
1000
|
+
const basis = styles["flex-basis"];
|
|
1001
|
+
if (!basis || basis === "auto") return;
|
|
1002
|
+
addCandidateMatch(accumulator, "flex-basis", basis, buildDimensionCandidate("basis", basis, { confidence: MEDIUM_CONFIDENCE }));
|
|
1003
|
+
};
|
|
1004
|
+
const mapGridLayout = (context, accumulator) => {
|
|
1005
|
+
const { styles } = context.element;
|
|
1006
|
+
const { display } = styles;
|
|
1007
|
+
if (display !== "grid" && display !== "inline-grid") return;
|
|
1008
|
+
addGapMatches(accumulator, styles.gap, styles["row-gap"], styles["column-gap"]);
|
|
1009
|
+
addMappedUtility(accumulator, "grid-auto-flow", styles["grid-auto-flow"], GRID_AUTO_FLOW_MAP);
|
|
1010
|
+
for (const [property, prefix] of [
|
|
1011
|
+
["grid-column-start", "col-start"],
|
|
1012
|
+
["grid-column-end", "col-end"],
|
|
1013
|
+
["grid-row-start", "row-start"],
|
|
1014
|
+
["grid-row-end", "row-end"]
|
|
1015
|
+
]) {
|
|
1016
|
+
const value = styles[property];
|
|
1017
|
+
if (!value || value === "auto") continue;
|
|
1018
|
+
addMatch(accumulator, {
|
|
1019
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
1020
|
+
sourceProperties: [property],
|
|
1021
|
+
sourceValues: [value],
|
|
1022
|
+
strategy: "arbitrary",
|
|
1023
|
+
utility: createArbitraryUtility(prefix, value)
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
addArbitraryPropertyMatch(accumulator, "grid-template-columns", styles["grid-template-columns"], "Computed grid tracks lose authored repeat syntax.");
|
|
1027
|
+
addArbitraryPropertyMatch(accumulator, "grid-template-rows", styles["grid-template-rows"], "Computed grid tracks lose authored repeat syntax.");
|
|
1028
|
+
};
|
|
1029
|
+
const mapSpacing = (context, accumulator) => {
|
|
1030
|
+
addBoxSpacingMatches(accumulator, "p", [
|
|
1031
|
+
["padding-top", context.element.styles["padding-top"]],
|
|
1032
|
+
["padding-right", context.element.styles["padding-right"]],
|
|
1033
|
+
["padding-bottom", context.element.styles["padding-bottom"]],
|
|
1034
|
+
["padding-left", context.element.styles["padding-left"]]
|
|
1035
|
+
]);
|
|
1036
|
+
addBoxSpacingMatches(accumulator, "m", [
|
|
1037
|
+
["margin-top", context.element.styles["margin-top"]],
|
|
1038
|
+
["margin-right", context.element.styles["margin-right"]],
|
|
1039
|
+
["margin-bottom", context.element.styles["margin-bottom"]],
|
|
1040
|
+
["margin-left", context.element.styles["margin-left"]]
|
|
1041
|
+
]);
|
|
1042
|
+
};
|
|
1043
|
+
const mapSizing = (context, accumulator) => {
|
|
1044
|
+
const { styles } = context.element;
|
|
1045
|
+
for (const [property, prefix, confidence] of [
|
|
1046
|
+
[
|
|
1047
|
+
"min-width",
|
|
1048
|
+
"min-w",
|
|
1049
|
+
MEDIUM_CONFIDENCE
|
|
1050
|
+
],
|
|
1051
|
+
[
|
|
1052
|
+
"min-height",
|
|
1053
|
+
"min-h",
|
|
1054
|
+
MEDIUM_CONFIDENCE
|
|
1055
|
+
],
|
|
1056
|
+
[
|
|
1057
|
+
"max-width",
|
|
1058
|
+
"max-w",
|
|
1059
|
+
MEDIUM_CONFIDENCE
|
|
1060
|
+
],
|
|
1061
|
+
[
|
|
1062
|
+
"max-height",
|
|
1063
|
+
"max-h",
|
|
1064
|
+
MEDIUM_CONFIDENCE
|
|
1065
|
+
],
|
|
1066
|
+
[
|
|
1067
|
+
"width",
|
|
1068
|
+
"w",
|
|
1069
|
+
LOW_CONFIDENCE
|
|
1070
|
+
],
|
|
1071
|
+
[
|
|
1072
|
+
"height",
|
|
1073
|
+
"h",
|
|
1074
|
+
LOW_CONFIDENCE
|
|
1075
|
+
]
|
|
1076
|
+
]) {
|
|
1077
|
+
const value = styles[property];
|
|
1078
|
+
if (!shouldMapDimension(property, value)) continue;
|
|
1079
|
+
addCandidateMatch(accumulator, property, value, buildDimensionCandidate(prefix, value, {
|
|
1080
|
+
confidence,
|
|
1081
|
+
note: property === "width" || property === "height" ? "Computed size values are often layout-dependent." : void 0
|
|
1082
|
+
}));
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
const mapTypographyBasics = (context, accumulator) => {
|
|
1086
|
+
const { element, parent } = context;
|
|
1087
|
+
addColorMatch(accumulator, "text", "color", element.styles.color, shouldEmitInheritedValue("color", element.styles.color, parent?.styles.color));
|
|
1088
|
+
addFontFamilyMatch(accumulator, element.styles["font-family"], shouldEmitInheritedValue("font-family", element.styles["font-family"], parent?.styles["font-family"]));
|
|
1089
|
+
addScaleMatch(accumulator, "font-size", element.styles["font-size"], shouldEmitInheritedValue("font-size", element.styles["font-size"], parent?.styles["font-size"]), FONT_SIZE_SCALE, "text", "Font size required an arbitrary value.");
|
|
1090
|
+
addFontWeightMatch(accumulator, element.styles["font-weight"], shouldEmitInheritedValue("font-weight", element.styles["font-weight"], parent?.styles["font-weight"]));
|
|
1091
|
+
addFontStyleMatch(accumulator, element.styles["font-style"], shouldEmitInheritedValue("font-style", element.styles["font-style"], parent?.styles["font-style"]));
|
|
1092
|
+
addScaleMatch(accumulator, "line-height", element.styles["line-height"], shouldEmitInheritedValue("line-height", element.styles["line-height"], parent?.styles["line-height"]) && element.styles["line-height"] !== "normal", LINE_HEIGHT_SCALE, "leading", "Line height required an arbitrary value.");
|
|
1093
|
+
addTrackingMatch(accumulator, element.styles["letter-spacing"], shouldEmitInheritedValue("letter-spacing", element.styles["letter-spacing"], parent?.styles["letter-spacing"]));
|
|
1094
|
+
};
|
|
1095
|
+
const mapTypographyPresentation = (context, accumulator) => {
|
|
1096
|
+
const { element, parent } = context;
|
|
1097
|
+
addMappedUtility(accumulator, "text-align", element.styles["text-align"], TEXT_ALIGN_MAP, shouldEmitInheritedValue("text-align", element.styles["text-align"], parent?.styles["text-align"]) && !["left", "start"].includes(element.styles["text-align"] ?? ""));
|
|
1098
|
+
addMappedUtility(accumulator, "text-transform", element.styles["text-transform"], TEXT_TRANSFORM_MAP, shouldEmitInheritedValue("text-transform", element.styles["text-transform"], parent?.styles["text-transform"]) && element.styles["text-transform"] !== "none");
|
|
1099
|
+
addMappedUtility(accumulator, "white-space", element.styles["white-space"], WHITE_SPACE_MAP, shouldEmitInheritedValue("white-space", element.styles["white-space"], parent?.styles["white-space"]) && element.styles["white-space"] !== "normal");
|
|
1100
|
+
addMappedUtility(accumulator, "list-style-type", element.styles["list-style-type"], LIST_STYLE_MAP, shouldEmitInheritedValue("list-style-type", element.styles["list-style-type"], parent?.styles["list-style-type"]) && element.styles["list-style-type"] !== "disc");
|
|
1101
|
+
addDecorationLineMatches(accumulator, element.styles["text-decoration-line"]);
|
|
1102
|
+
addColorMatch(accumulator, "decoration", "text-decoration-color", element.styles["text-decoration-color"], element.styles["text-decoration-line"] !== "none" && Boolean(element.styles["text-decoration-color"]));
|
|
1103
|
+
};
|
|
1104
|
+
const mapBackground = (context, accumulator) => {
|
|
1105
|
+
addColorMatch(accumulator, "bg", "background-color", context.element.styles["background-color"], !isTransparentColor(context.element.styles["background-color"] ?? ""));
|
|
1106
|
+
addArbitraryPropertyMatch(accumulator, "background-image", context.element.styles["background-image"], "Background images are emitted as arbitrary properties.");
|
|
1107
|
+
};
|
|
1108
|
+
const mapBorder = (context, accumulator) => {
|
|
1109
|
+
addUniformBorderMatch(context.element, accumulator);
|
|
1110
|
+
addRadiusMatches(context.element, accumulator);
|
|
1111
|
+
};
|
|
1112
|
+
const mapEffects = (context, accumulator) => {
|
|
1113
|
+
const { styles } = context.element;
|
|
1114
|
+
addShadowMatch(accumulator, styles["box-shadow"]);
|
|
1115
|
+
addOpacityMatch(accumulator, styles.opacity);
|
|
1116
|
+
addArbitraryPropertyMatch(accumulator, "transform", styles.transform, "Computed transforms are emitted as raw properties.");
|
|
1117
|
+
addPositionMatch(accumulator, "origin", "transform-origin", styles["transform-origin"]);
|
|
1118
|
+
if (styles.visibility === "hidden") addMatch(accumulator, {
|
|
1119
|
+
confidence: HIGH_CONFIDENCE,
|
|
1120
|
+
sourceProperties: ["visibility"],
|
|
1121
|
+
sourceValues: ["hidden"],
|
|
1122
|
+
strategy: "semantic",
|
|
1123
|
+
utility: "invisible"
|
|
1124
|
+
});
|
|
1125
|
+
};
|
|
1126
|
+
const mapOverflow = (context, accumulator) => {
|
|
1127
|
+
const overflowX = context.element.styles["overflow-x"];
|
|
1128
|
+
const overflowY = context.element.styles["overflow-y"];
|
|
1129
|
+
if (!(overflowX && overflowY)) return;
|
|
1130
|
+
if (overflowX === overflowY && overflowX !== "visible") {
|
|
1131
|
+
const suffix = lookupMappedUtility(OVERFLOW_MAP, overflowX);
|
|
1132
|
+
if (suffix) addMatch(accumulator, {
|
|
1133
|
+
confidence: HIGH_CONFIDENCE,
|
|
1134
|
+
sourceProperties: ["overflow-x", "overflow-y"],
|
|
1135
|
+
sourceValues: [overflowX, overflowY],
|
|
1136
|
+
strategy: "semantic",
|
|
1137
|
+
utility: `overflow-${suffix}`
|
|
1138
|
+
});
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
addOverflowAxisMatch(accumulator, "overflow-x", overflowX);
|
|
1142
|
+
addOverflowAxisMatch(accumulator, "overflow-y", overflowY);
|
|
1143
|
+
};
|
|
1144
|
+
const mapObjectLayout = (context, accumulator) => {
|
|
1145
|
+
const objectFit = context.element.styles["object-fit"];
|
|
1146
|
+
if (objectFit && objectFit !== "fill") {
|
|
1147
|
+
const utility = lookupMappedUtility(OBJECT_FIT_MAP, objectFit);
|
|
1148
|
+
if (utility) addMatch(accumulator, {
|
|
1149
|
+
confidence: HIGH_CONFIDENCE,
|
|
1150
|
+
sourceProperties: ["object-fit"],
|
|
1151
|
+
sourceValues: [objectFit],
|
|
1152
|
+
strategy: "semantic",
|
|
1153
|
+
utility
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
addObjectPositionMatch(accumulator, context.element.styles["object-position"]);
|
|
1157
|
+
};
|
|
1158
|
+
const mapPseudoElements = (context, accumulator) => {
|
|
1159
|
+
const pseudoKinds = Object.keys(context.element.pseudo);
|
|
1160
|
+
if (pseudoKinds.length === 0) return;
|
|
1161
|
+
addUnsupported(accumulator, "pseudo-elements", pseudoKinds.join(", "), "Pseudo-elements were captured, but Tailwind output still needs manual content utilities.");
|
|
1162
|
+
};
|
|
1163
|
+
const isReviewOnlyNote = (note) => {
|
|
1164
|
+
const normalizedNote = note.toLowerCase();
|
|
1165
|
+
return normalizedNote.includes("manual review") || normalizedNote.includes("layout-dependent") || normalizedNote.includes("layout-derived") || normalizedNote.includes("arbitrary property utility") || normalizedNote.includes("required an arbitrary value") || normalizedNote.includes("computed transforms") || normalizedNote.includes("lose authored repeat syntax");
|
|
1166
|
+
};
|
|
1167
|
+
const shouldMoveMatchToReview = (match) => {
|
|
1168
|
+
if (match.label === "low" || match.utility.startsWith("[")) return true;
|
|
1169
|
+
if (match.sourceProperties.some((property) => REVIEW_ONLY_SOURCE_PROPERTIES.has(property))) return true;
|
|
1170
|
+
if (match.strategy === "arbitrary" && !match.sourceProperties.every((property) => CLEAN_ARBITRARY_SOURCE_PROPERTIES.has(property))) return true;
|
|
1171
|
+
return match.notes.some((note) => isReviewOnlyNote(note));
|
|
1172
|
+
};
|
|
1173
|
+
const splitMatchesForSuggestion = (matches) => {
|
|
1174
|
+
const reviewMatches = [];
|
|
1175
|
+
const suggestedMatches = [];
|
|
1176
|
+
for (const match of matches) if (shouldMoveMatchToReview(match)) reviewMatches.push(match);
|
|
1177
|
+
else suggestedMatches.push(match);
|
|
1178
|
+
return {
|
|
1179
|
+
reviewMatches,
|
|
1180
|
+
suggestedMatches
|
|
1181
|
+
};
|
|
1182
|
+
};
|
|
1183
|
+
const calculateElementConfidence = (accumulator) => {
|
|
1184
|
+
if (accumulator.matches.length === 0) return accumulator.unsupported.length > 0 ? LOW_CONFIDENCE : PASSIVE_CONFIDENCE;
|
|
1185
|
+
const average = accumulator.matches.reduce((sum, match) => sum + match.confidence, 0) / accumulator.matches.length;
|
|
1186
|
+
if (accumulator.unsupported.length === 0) return roundToTwo(average);
|
|
1187
|
+
return roundToTwo(Math.max(LOW_CONFIDENCE, average - .12));
|
|
1188
|
+
};
|
|
1189
|
+
const buildReviewFallbackNote = (match) => {
|
|
1190
|
+
if (match.label === "low") return "Low-confidence utility needs manual review.";
|
|
1191
|
+
if (match.sourceProperties.some((property) => REVIEW_ONLY_SOURCE_PROPERTIES.has(property))) return "Computed layout or custom CSS was kept out of the clean suggestion.";
|
|
1192
|
+
if (match.strategy === "arbitrary") return "Arbitrary utility was kept out of the clean suggestion.";
|
|
1193
|
+
return "Utility was kept out of the clean suggestion for review.";
|
|
1194
|
+
};
|
|
1195
|
+
const buildReviewItem = (mapping) => {
|
|
1196
|
+
const reasons = [...mapping.unsupported.map((entry) => `${entry.property}: ${entry.reason}`), ...mapping.matches.filter((match) => shouldMoveMatchToReview(match)).flatMap((match) => {
|
|
1197
|
+
return (match.notes.length ? match.notes : [buildReviewFallbackNote(match)]).map((note) => `${match.utility}: ${note}`);
|
|
1198
|
+
})];
|
|
1199
|
+
if (reasons.length === 0) return null;
|
|
1200
|
+
return {
|
|
1201
|
+
confidence: mapping.confidence,
|
|
1202
|
+
confidenceLabel: mapping.confidenceLabel,
|
|
1203
|
+
elementId: mapping.elementId,
|
|
1204
|
+
reasons: dedupe(reasons).slice(0, 6),
|
|
1205
|
+
selector: mapping.selector,
|
|
1206
|
+
unsupportedCount: mapping.unsupported.length
|
|
1207
|
+
};
|
|
1208
|
+
};
|
|
1209
|
+
const createAccumulator = () => ({
|
|
1210
|
+
classSet: /* @__PURE__ */ new Set(),
|
|
1211
|
+
classes: [],
|
|
1212
|
+
matches: [],
|
|
1213
|
+
unsupported: []
|
|
1214
|
+
});
|
|
1215
|
+
const MAPPING_STEPS = [
|
|
1216
|
+
mapDisplay,
|
|
1217
|
+
mapPositioning,
|
|
1218
|
+
mapFlexLayout,
|
|
1219
|
+
mapGridLayout,
|
|
1220
|
+
mapSpacing,
|
|
1221
|
+
mapSizing,
|
|
1222
|
+
mapTypographyBasics,
|
|
1223
|
+
mapTypographyPresentation,
|
|
1224
|
+
mapBackground,
|
|
1225
|
+
mapBorder,
|
|
1226
|
+
mapEffects,
|
|
1227
|
+
mapOverflow,
|
|
1228
|
+
mapObjectLayout,
|
|
1229
|
+
mapPseudoElements
|
|
1230
|
+
];
|
|
1231
|
+
const mapElementSnapshot = (context) => {
|
|
1232
|
+
const accumulator = createAccumulator();
|
|
1233
|
+
for (const step of MAPPING_STEPS) step(context, accumulator);
|
|
1234
|
+
const confidence = calculateElementConfidence(accumulator);
|
|
1235
|
+
const { reviewMatches, suggestedMatches } = splitMatchesForSuggestion(accumulator.matches);
|
|
1236
|
+
return {
|
|
1237
|
+
className: accumulator.classes.join(" "),
|
|
1238
|
+
confidence,
|
|
1239
|
+
confidenceLabel: labelFromConfidence(confidence),
|
|
1240
|
+
elementId: context.element.id,
|
|
1241
|
+
matchCount: accumulator.matches.length,
|
|
1242
|
+
matches: accumulator.matches,
|
|
1243
|
+
reviewClassName: reviewMatches.map((match) => match.utility).join(" "),
|
|
1244
|
+
reviewMatchCount: reviewMatches.length,
|
|
1245
|
+
selector: context.element.selector,
|
|
1246
|
+
suggestedClassName: suggestedMatches.map((match) => match.utility).join(" "),
|
|
1247
|
+
suggestedMatchCount: suggestedMatches.length,
|
|
1248
|
+
tagName: context.element.tagName,
|
|
1249
|
+
unsupported: accumulator.unsupported
|
|
1250
|
+
};
|
|
1251
|
+
};
|
|
1252
|
+
const mapCaptureToTailwind = (capture) => {
|
|
1253
|
+
const elements = {};
|
|
1254
|
+
const reviewQueue = [];
|
|
1255
|
+
let cleanUtilityCount = 0;
|
|
1256
|
+
let confidenceSum = 0;
|
|
1257
|
+
let lowConfidenceElementCount = 0;
|
|
1258
|
+
let mappedElementCount = 0;
|
|
1259
|
+
let reviewUtilityCount = 0;
|
|
1260
|
+
let unsupportedPropertyCount = 0;
|
|
1261
|
+
let utilityCount = 0;
|
|
1262
|
+
for (const elementId of capture.order) {
|
|
1263
|
+
const element = capture.elements[elementId];
|
|
1264
|
+
if (!element) continue;
|
|
1265
|
+
const mapping = mapElementSnapshot({
|
|
1266
|
+
element,
|
|
1267
|
+
parent: element.parentId === null ? null : capture.elements[element.parentId] ?? null
|
|
1268
|
+
});
|
|
1269
|
+
elements[elementId] = mapping;
|
|
1270
|
+
confidenceSum += mapping.confidence;
|
|
1271
|
+
if (mapping.confidenceLabel === "low") lowConfidenceElementCount += 1;
|
|
1272
|
+
if (mapping.matchCount > 0) mappedElementCount += 1;
|
|
1273
|
+
unsupportedPropertyCount += mapping.unsupported.length;
|
|
1274
|
+
cleanUtilityCount += mapping.suggestedMatchCount;
|
|
1275
|
+
reviewUtilityCount += mapping.reviewMatchCount;
|
|
1276
|
+
utilityCount += mapping.matches.length;
|
|
1277
|
+
const reviewItem = buildReviewItem(mapping);
|
|
1278
|
+
if (reviewItem) reviewQueue.push(reviewItem);
|
|
1279
|
+
}
|
|
1280
|
+
reviewQueue.sort((left, right) => {
|
|
1281
|
+
if (left.confidence !== right.confidence) return left.confidence - right.confidence;
|
|
1282
|
+
return right.unsupportedCount - left.unsupportedCount;
|
|
1283
|
+
});
|
|
1284
|
+
const elementCount = capture.order.length;
|
|
1285
|
+
return {
|
|
1286
|
+
elements,
|
|
1287
|
+
order: capture.order,
|
|
1288
|
+
reviewQueue,
|
|
1289
|
+
summary: {
|
|
1290
|
+
averageConfidence: elementCount ? roundToTwo(confidenceSum / elementCount) : 0,
|
|
1291
|
+
cleanUtilityCount,
|
|
1292
|
+
elementCount,
|
|
1293
|
+
lowConfidenceElementCount,
|
|
1294
|
+
mappedElementCount,
|
|
1295
|
+
reviewCount: reviewQueue.length,
|
|
1296
|
+
reviewUtilityCount,
|
|
1297
|
+
unsupportedPropertyCount,
|
|
1298
|
+
utilityCount
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
};
|
|
1302
|
+
//#endregion
|
|
1303
|
+
//#region src/capture.ts
|
|
1304
|
+
/**
|
|
1305
|
+
* Runs the capture pipeline inside a Playwright page context.
|
|
1306
|
+
* This mirrors the extension's `buildCapture` from `run-picker.ts`,
|
|
1307
|
+
* adapted to run via `page.evaluate()`.
|
|
1308
|
+
*/
|
|
1309
|
+
const captureElement = async (page, selector, settings) => {
|
|
1310
|
+
return await page.evaluate(({ selector: sel, settings: opts }) => {
|
|
1311
|
+
const OMITTED_ELEMENT_NAMES = new Set([
|
|
1312
|
+
"base",
|
|
1313
|
+
"iframe",
|
|
1314
|
+
"link",
|
|
1315
|
+
"meta",
|
|
1316
|
+
"noscript",
|
|
1317
|
+
"object",
|
|
1318
|
+
"script",
|
|
1319
|
+
"style",
|
|
1320
|
+
"template"
|
|
1321
|
+
]);
|
|
1322
|
+
const OMITTED_ATTRIBUTE_NAMES = new Set([
|
|
1323
|
+
"checked",
|
|
1324
|
+
"selected",
|
|
1325
|
+
"value"
|
|
1326
|
+
]);
|
|
1327
|
+
const OMITTED_URL_ATTRIBUTE_NAMES = new Set([
|
|
1328
|
+
"action",
|
|
1329
|
+
"formaction",
|
|
1330
|
+
"href",
|
|
1331
|
+
"poster",
|
|
1332
|
+
"src",
|
|
1333
|
+
"srcdoc",
|
|
1334
|
+
"srcset",
|
|
1335
|
+
"xlink:href"
|
|
1336
|
+
]);
|
|
1337
|
+
const BASELINE_ATTRIBUTE_NAMES = [
|
|
1338
|
+
"checked",
|
|
1339
|
+
"cols",
|
|
1340
|
+
"disabled",
|
|
1341
|
+
"multiple",
|
|
1342
|
+
"open",
|
|
1343
|
+
"rows",
|
|
1344
|
+
"selected",
|
|
1345
|
+
"size",
|
|
1346
|
+
"type",
|
|
1347
|
+
"wrap"
|
|
1348
|
+
];
|
|
1349
|
+
const INHERITED_PROPERTIES = new Set([
|
|
1350
|
+
"color",
|
|
1351
|
+
"font-family",
|
|
1352
|
+
"font-size",
|
|
1353
|
+
"font-style",
|
|
1354
|
+
"font-weight",
|
|
1355
|
+
"letter-spacing",
|
|
1356
|
+
"line-height",
|
|
1357
|
+
"list-style-type",
|
|
1358
|
+
"text-align",
|
|
1359
|
+
"text-decoration-color",
|
|
1360
|
+
"text-decoration-line",
|
|
1361
|
+
"text-transform",
|
|
1362
|
+
"visibility",
|
|
1363
|
+
"white-space"
|
|
1364
|
+
]);
|
|
1365
|
+
const CURATED_PROPERTIES = [
|
|
1366
|
+
"align-items",
|
|
1367
|
+
"background-color",
|
|
1368
|
+
"background-image",
|
|
1369
|
+
"border-bottom-color",
|
|
1370
|
+
"border-bottom-left-radius",
|
|
1371
|
+
"border-bottom-right-radius",
|
|
1372
|
+
"border-bottom-style",
|
|
1373
|
+
"border-bottom-width",
|
|
1374
|
+
"border-left-color",
|
|
1375
|
+
"border-left-style",
|
|
1376
|
+
"border-left-width",
|
|
1377
|
+
"border-right-color",
|
|
1378
|
+
"border-right-style",
|
|
1379
|
+
"border-right-width",
|
|
1380
|
+
"border-top-color",
|
|
1381
|
+
"border-top-left-radius",
|
|
1382
|
+
"border-top-right-radius",
|
|
1383
|
+
"border-top-style",
|
|
1384
|
+
"border-top-width",
|
|
1385
|
+
"bottom",
|
|
1386
|
+
"box-shadow",
|
|
1387
|
+
"color",
|
|
1388
|
+
"column-gap",
|
|
1389
|
+
"display",
|
|
1390
|
+
"flex-basis",
|
|
1391
|
+
"flex-direction",
|
|
1392
|
+
"flex-grow",
|
|
1393
|
+
"flex-shrink",
|
|
1394
|
+
"flex-wrap",
|
|
1395
|
+
"font-family",
|
|
1396
|
+
"font-size",
|
|
1397
|
+
"font-style",
|
|
1398
|
+
"font-weight",
|
|
1399
|
+
"gap",
|
|
1400
|
+
"grid-auto-flow",
|
|
1401
|
+
"grid-column-end",
|
|
1402
|
+
"grid-column-start",
|
|
1403
|
+
"grid-row-end",
|
|
1404
|
+
"grid-row-start",
|
|
1405
|
+
"grid-template-columns",
|
|
1406
|
+
"grid-template-rows",
|
|
1407
|
+
"height",
|
|
1408
|
+
"justify-content",
|
|
1409
|
+
"left",
|
|
1410
|
+
"letter-spacing",
|
|
1411
|
+
"line-height",
|
|
1412
|
+
"list-style-type",
|
|
1413
|
+
"margin-bottom",
|
|
1414
|
+
"margin-left",
|
|
1415
|
+
"margin-right",
|
|
1416
|
+
"margin-top",
|
|
1417
|
+
"max-height",
|
|
1418
|
+
"max-width",
|
|
1419
|
+
"min-height",
|
|
1420
|
+
"min-width",
|
|
1421
|
+
"object-fit",
|
|
1422
|
+
"object-position",
|
|
1423
|
+
"opacity",
|
|
1424
|
+
"overflow-x",
|
|
1425
|
+
"overflow-y",
|
|
1426
|
+
"padding-bottom",
|
|
1427
|
+
"padding-left",
|
|
1428
|
+
"padding-right",
|
|
1429
|
+
"padding-top",
|
|
1430
|
+
"position",
|
|
1431
|
+
"right",
|
|
1432
|
+
"row-gap",
|
|
1433
|
+
"text-align",
|
|
1434
|
+
"text-decoration-color",
|
|
1435
|
+
"text-decoration-line",
|
|
1436
|
+
"text-transform",
|
|
1437
|
+
"top",
|
|
1438
|
+
"transform",
|
|
1439
|
+
"transform-origin",
|
|
1440
|
+
"visibility",
|
|
1441
|
+
"white-space",
|
|
1442
|
+
"width",
|
|
1443
|
+
"z-index"
|
|
1444
|
+
];
|
|
1445
|
+
const rootEl = document.querySelector(sel);
|
|
1446
|
+
if (!rootEl) throw new Error(`No element found for selector: ${sel}`);
|
|
1447
|
+
const elements = {};
|
|
1448
|
+
const order = [];
|
|
1449
|
+
const idByElement = /* @__PURE__ */ new WeakMap();
|
|
1450
|
+
let pseudoElementCount = 0;
|
|
1451
|
+
let nextId = 0;
|
|
1452
|
+
const defaultStyleFrame = document.createElement("iframe");
|
|
1453
|
+
defaultStyleFrame.setAttribute("aria-hidden", "true");
|
|
1454
|
+
defaultStyleFrame.tabIndex = -1;
|
|
1455
|
+
defaultStyleFrame.style.cssText = "position:fixed;top:-9999px;left:-9999px;width:0;height:0;border:0;opacity:0;pointer-events:none";
|
|
1456
|
+
document.documentElement.append(defaultStyleFrame);
|
|
1457
|
+
const defaultDoc = defaultStyleFrame.contentDocument;
|
|
1458
|
+
if (defaultDoc) {
|
|
1459
|
+
defaultDoc.open();
|
|
1460
|
+
defaultDoc.write("<!doctype html><html><body></body></html>");
|
|
1461
|
+
defaultDoc.close();
|
|
1462
|
+
}
|
|
1463
|
+
const defaultStyleCache = /* @__PURE__ */ new Map();
|
|
1464
|
+
const getDefaultStyles = (element) => {
|
|
1465
|
+
const parts = [element.tagName.toLowerCase()];
|
|
1466
|
+
for (const attr of BASELINE_ATTRIBUTE_NAMES) if (element.hasAttribute(attr)) parts.push(`${attr}=${element.getAttribute(attr) ?? ""}`);
|
|
1467
|
+
const key = parts.join("|");
|
|
1468
|
+
const cached = defaultStyleCache.get(key);
|
|
1469
|
+
if (cached) return cached;
|
|
1470
|
+
const frameDoc = defaultStyleFrame.contentDocument;
|
|
1471
|
+
const frameWin = defaultStyleFrame.contentWindow;
|
|
1472
|
+
if (!(frameDoc && frameWin)) return {};
|
|
1473
|
+
const baseline = frameDoc.createElement(element.tagName.toLowerCase());
|
|
1474
|
+
for (const attr of BASELINE_ATTRIBUTE_NAMES) if (element.hasAttribute(attr)) baseline.setAttribute(attr, element.getAttribute(attr) ?? "");
|
|
1475
|
+
frameDoc.body.append(baseline);
|
|
1476
|
+
const computed = frameWin.getComputedStyle(baseline);
|
|
1477
|
+
const defaults = {};
|
|
1478
|
+
for (const prop of CURATED_PROPERTIES) {
|
|
1479
|
+
const val = computed.getPropertyValue(prop).trim();
|
|
1480
|
+
if (val) defaults[prop] = val;
|
|
1481
|
+
}
|
|
1482
|
+
baseline.remove();
|
|
1483
|
+
defaultStyleCache.set(key, defaults);
|
|
1484
|
+
return defaults;
|
|
1485
|
+
};
|
|
1486
|
+
const snapshotStyles = (element, styles, includeAll, parentStyles) => {
|
|
1487
|
+
const output = {};
|
|
1488
|
+
const properties = includeAll ? [...styles] : [...CURATED_PROPERTIES];
|
|
1489
|
+
const defaultStyles = includeAll ? null : getDefaultStyles(element);
|
|
1490
|
+
for (const property of properties) {
|
|
1491
|
+
const value = styles.getPropertyValue(property);
|
|
1492
|
+
if (!value) continue;
|
|
1493
|
+
const trimmed = value.trim();
|
|
1494
|
+
if (!trimmed) continue;
|
|
1495
|
+
if (defaultStyles?.[property] === trimmed) continue;
|
|
1496
|
+
if (parentStyles && INHERITED_PROPERTIES.has(property) && parentStyles[property] === trimmed) continue;
|
|
1497
|
+
output[property] = trimmed;
|
|
1498
|
+
}
|
|
1499
|
+
return output;
|
|
1500
|
+
};
|
|
1501
|
+
const snapshotPseudo = (element, kind, includeAll) => {
|
|
1502
|
+
const styles = window.getComputedStyle(element, `::${kind}`);
|
|
1503
|
+
const content = styles.getPropertyValue("content").trim();
|
|
1504
|
+
const display = styles.getPropertyValue("display").trim();
|
|
1505
|
+
const w = styles.getPropertyValue("width").trim();
|
|
1506
|
+
const h = styles.getPropertyValue("height").trim();
|
|
1507
|
+
const bg = styles.getPropertyValue("background-color").trim();
|
|
1508
|
+
const bw = styles.getPropertyValue("border-top-width").trim();
|
|
1509
|
+
if (content === "none" && display === "inline" && w === "auto" && h === "auto" && bg === "rgba(0, 0, 0, 0)" && bw === "0px") return null;
|
|
1510
|
+
return {
|
|
1511
|
+
kind,
|
|
1512
|
+
styles: snapshotStyles(element, styles, includeAll, null)
|
|
1513
|
+
};
|
|
1514
|
+
};
|
|
1515
|
+
const buildSelector = (element) => {
|
|
1516
|
+
if (element.id) return `#${CSS.escape(element.id)}`;
|
|
1517
|
+
const segments = [];
|
|
1518
|
+
let current = element;
|
|
1519
|
+
while (current && current.nodeType === Node.ELEMENT_NODE && segments.length < 4) {
|
|
1520
|
+
const tag = current.tagName.toLowerCase();
|
|
1521
|
+
const classes = [...current.classList].slice(0, 2).map((c) => `.${CSS.escape(c)}`).join("");
|
|
1522
|
+
const idx = current.parentElement ? [...current.parentElement.children].indexOf(current) + 1 : 1;
|
|
1523
|
+
segments.unshift(`${tag}${classes}:nth-child(${idx})`);
|
|
1524
|
+
current = current.parentElement;
|
|
1525
|
+
}
|
|
1526
|
+
return segments.join(" > ");
|
|
1527
|
+
};
|
|
1528
|
+
const shouldOmitAttr = (name) => {
|
|
1529
|
+
const n = name.toLowerCase();
|
|
1530
|
+
return n.startsWith("on") || n === "nonce" || OMITTED_ATTRIBUTE_NAMES.has(n) || OMITTED_URL_ATTRIBUTE_NAMES.has(n);
|
|
1531
|
+
};
|
|
1532
|
+
const getSafeAttributes = (el) => {
|
|
1533
|
+
const safe = {};
|
|
1534
|
+
for (const attr of el.attributes) if (!shouldOmitAttr(attr.name)) safe[attr.name] = attr.value;
|
|
1535
|
+
return safe;
|
|
1536
|
+
};
|
|
1537
|
+
const getBoundingBox = (rect) => ({
|
|
1538
|
+
bottom: rect.bottom,
|
|
1539
|
+
height: rect.height,
|
|
1540
|
+
left: rect.left,
|
|
1541
|
+
right: rect.right,
|
|
1542
|
+
top: rect.top,
|
|
1543
|
+
width: rect.width,
|
|
1544
|
+
x: rect.x,
|
|
1545
|
+
y: rect.y
|
|
1546
|
+
});
|
|
1547
|
+
const isHidden = (el) => {
|
|
1548
|
+
const s = window.getComputedStyle(el);
|
|
1549
|
+
return s.display === "none" || s.visibility === "hidden";
|
|
1550
|
+
};
|
|
1551
|
+
const pruneExcludedDescendants = (sourceRoot, cloneRoot, includeHiddenElements) => {
|
|
1552
|
+
const sourceElements = [...sourceRoot.querySelectorAll("*")];
|
|
1553
|
+
const cloneElements = [...cloneRoot.querySelectorAll("*")];
|
|
1554
|
+
for (let index = cloneElements.length - 1; index >= 0; index -= 1) {
|
|
1555
|
+
const sourceElement = sourceElements[index];
|
|
1556
|
+
const cloneElement = cloneElements[index];
|
|
1557
|
+
if (!(sourceElement && cloneElement)) continue;
|
|
1558
|
+
if (OMITTED_ELEMENT_NAMES.has(sourceElement.tagName.toLowerCase()) || !includeHiddenElements && isHidden(sourceElement)) cloneElement.replaceWith(cloneElement.ownerDocument.createComment(cloneElement.tagName.toLowerCase()));
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
const sanitizeOuterHtml = (el, includeHiddenElements) => {
|
|
1562
|
+
const clone = el.cloneNode(true);
|
|
1563
|
+
if (!(clone instanceof Element)) return "";
|
|
1564
|
+
if (OMITTED_ELEMENT_NAMES.has(el.tagName.toLowerCase()) || !includeHiddenElements && isHidden(el)) return `<!--${el.tagName.toLowerCase()}-->`;
|
|
1565
|
+
pruneExcludedDescendants(el, clone, includeHiddenElements);
|
|
1566
|
+
const sanitize = (target) => {
|
|
1567
|
+
for (const attr of [...target.attributes]) if (shouldOmitAttr(attr.name)) target.removeAttribute(attr.name);
|
|
1568
|
+
if (target instanceof HTMLTextAreaElement) target.textContent = "";
|
|
1569
|
+
};
|
|
1570
|
+
sanitize(clone);
|
|
1571
|
+
for (const child of clone.querySelectorAll("*")) sanitize(child);
|
|
1572
|
+
return clone.outerHTML;
|
|
1573
|
+
};
|
|
1574
|
+
const captureEl = (element, parentId) => {
|
|
1575
|
+
const id = `node-${nextId}`;
|
|
1576
|
+
nextId += 1;
|
|
1577
|
+
idByElement.set(element, id);
|
|
1578
|
+
order.push(id);
|
|
1579
|
+
const snapshot = {
|
|
1580
|
+
attributes: getSafeAttributes(element),
|
|
1581
|
+
boundingBox: getBoundingBox(element.getBoundingClientRect()),
|
|
1582
|
+
children: [],
|
|
1583
|
+
classList: [...element.classList],
|
|
1584
|
+
id,
|
|
1585
|
+
parentId,
|
|
1586
|
+
pseudo: {},
|
|
1587
|
+
selector: buildSelector(element),
|
|
1588
|
+
styles: snapshotStyles(element, window.getComputedStyle(element), opts.captureMode === "full", parentId ? elements[parentId]?.styles ?? null : null),
|
|
1589
|
+
tagName: element.tagName.toLowerCase()
|
|
1590
|
+
};
|
|
1591
|
+
if (opts.includePseudoElements) {
|
|
1592
|
+
const before = snapshotPseudo(element, "before", opts.captureMode === "full");
|
|
1593
|
+
const after = snapshotPseudo(element, "after", opts.captureMode === "full");
|
|
1594
|
+
if (before) {
|
|
1595
|
+
snapshot.pseudo.before = before;
|
|
1596
|
+
pseudoElementCount += 1;
|
|
1597
|
+
}
|
|
1598
|
+
if (after) {
|
|
1599
|
+
snapshot.pseudo.after = after;
|
|
1600
|
+
pseudoElementCount += 1;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
elements[id] = snapshot;
|
|
1604
|
+
if (parentId && elements[parentId]) elements[parentId].children.push(id);
|
|
1605
|
+
return id;
|
|
1606
|
+
};
|
|
1607
|
+
const rootElementId = captureEl(rootEl, null);
|
|
1608
|
+
const walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_ELEMENT, { acceptNode(node) {
|
|
1609
|
+
if (node instanceof Element && node !== rootEl && !opts.includeHiddenElements && isHidden(node)) return NodeFilter.FILTER_REJECT;
|
|
1610
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
1611
|
+
} });
|
|
1612
|
+
while (walker.nextNode()) {
|
|
1613
|
+
const current = walker.currentNode;
|
|
1614
|
+
if (!(current instanceof Element)) continue;
|
|
1615
|
+
captureEl(current, current.parentElement ? idByElement.get(current.parentElement) ?? null : null);
|
|
1616
|
+
}
|
|
1617
|
+
defaultStyleFrame.remove();
|
|
1618
|
+
return {
|
|
1619
|
+
elements,
|
|
1620
|
+
metadata: {
|
|
1621
|
+
title: document.title,
|
|
1622
|
+
url: window.location.href
|
|
1623
|
+
},
|
|
1624
|
+
order,
|
|
1625
|
+
rootElementId,
|
|
1626
|
+
rootOuterHtml: sanitizeOuterHtml(rootEl, opts.includeHiddenElements),
|
|
1627
|
+
settings: opts,
|
|
1628
|
+
summary: {
|
|
1629
|
+
elementCount: order.length,
|
|
1630
|
+
pseudoElementCount
|
|
1631
|
+
},
|
|
1632
|
+
version: 1
|
|
1633
|
+
};
|
|
1634
|
+
}, {
|
|
1635
|
+
selector,
|
|
1636
|
+
settings
|
|
1637
|
+
});
|
|
1638
|
+
};
|
|
1639
|
+
//#endregion
|
|
1640
|
+
//#region src/interactive.ts
|
|
1641
|
+
const interactive = async () => {
|
|
1642
|
+
intro("style-capture");
|
|
1643
|
+
const inputs = await group({
|
|
1644
|
+
mode: () => select({
|
|
1645
|
+
message: "Capture mode",
|
|
1646
|
+
options: [{
|
|
1647
|
+
hint: "Common visual properties only",
|
|
1648
|
+
label: "Curated",
|
|
1649
|
+
value: "curated"
|
|
1650
|
+
}, {
|
|
1651
|
+
hint: "All computed styles",
|
|
1652
|
+
label: "Full",
|
|
1653
|
+
value: "full"
|
|
1654
|
+
}]
|
|
1655
|
+
}),
|
|
1656
|
+
output: () => select({
|
|
1657
|
+
message: "Output",
|
|
1658
|
+
options: [{
|
|
1659
|
+
label: "Clipboard",
|
|
1660
|
+
value: "clipboard"
|
|
1661
|
+
}, {
|
|
1662
|
+
label: "Stdout",
|
|
1663
|
+
value: "stdout"
|
|
1664
|
+
}]
|
|
1665
|
+
}),
|
|
1666
|
+
selector: () => text({
|
|
1667
|
+
message: "CSS selector for target element",
|
|
1668
|
+
placeholder: "main, .hero, #app",
|
|
1669
|
+
validate: (val) => {
|
|
1670
|
+
if (!val?.trim()) return "Selector is required";
|
|
1671
|
+
}
|
|
1672
|
+
}),
|
|
1673
|
+
url: () => text({
|
|
1674
|
+
message: "URL to capture",
|
|
1675
|
+
placeholder: "https://example.com",
|
|
1676
|
+
validate: (val) => {
|
|
1677
|
+
if (!val) return "URL is required";
|
|
1678
|
+
if (!URL.canParse(val)) return "Please enter a valid URL";
|
|
1679
|
+
}
|
|
1680
|
+
})
|
|
1681
|
+
}, { onCancel: () => {
|
|
1682
|
+
cancel("Cancelled.");
|
|
1683
|
+
process.exit(0);
|
|
1684
|
+
} });
|
|
1685
|
+
const settings = {
|
|
1686
|
+
...createDefaultSettings(),
|
|
1687
|
+
captureMode: inputs.mode
|
|
1688
|
+
};
|
|
1689
|
+
const spin = spinner();
|
|
1690
|
+
spin.start("Launching browser");
|
|
1691
|
+
const browser = await chromium.launch({ headless: true });
|
|
1692
|
+
try {
|
|
1693
|
+
const page = await browser.newPage();
|
|
1694
|
+
spin.message(`Loading ${inputs.url}`);
|
|
1695
|
+
await page.goto(inputs.url, { waitUntil: "networkidle" });
|
|
1696
|
+
spin.message("Verifying selector");
|
|
1697
|
+
const found = await page.locator(inputs.selector).count();
|
|
1698
|
+
if (found === 0) {
|
|
1699
|
+
spin.stop("No element found");
|
|
1700
|
+
log.error(`Selector "${inputs.selector}" matched 0 elements on ${inputs.url}`);
|
|
1701
|
+
await browser.close();
|
|
1702
|
+
process.exit(1);
|
|
1703
|
+
}
|
|
1704
|
+
if (found > 1) log.warn(`Selector matched ${found} elements — capturing the first one`);
|
|
1705
|
+
spin.message(`Capturing ${found} element(s)`);
|
|
1706
|
+
const capture = await captureElement(page, inputs.selector, settings);
|
|
1707
|
+
spin.message("Mapping to Tailwind");
|
|
1708
|
+
const mapping = mapCaptureToTailwind(capture);
|
|
1709
|
+
spin.message("Formatting");
|
|
1710
|
+
const markdown = formatCaptureForClaudeMarkdown(capture, mapping);
|
|
1711
|
+
spin.stop("Capture complete");
|
|
1712
|
+
log.info(`${capture.summary.elementCount} elements, ${capture.summary.pseudoElementCount} pseudo-elements`);
|
|
1713
|
+
log.info(`Tailwind: ${mapping.summary.utilityCount} utilities mapped (${mapping.summary.averageConfidence.toFixed(0)}% avg confidence)`);
|
|
1714
|
+
if (inputs.output === "clipboard") {
|
|
1715
|
+
const { default: clipboardy } = await import("clipboardy");
|
|
1716
|
+
await clipboardy.write(markdown);
|
|
1717
|
+
log.success("Copied to clipboard");
|
|
1718
|
+
} else console.log(`\n${markdown}`);
|
|
1719
|
+
} finally {
|
|
1720
|
+
await browser.close();
|
|
1721
|
+
}
|
|
1722
|
+
outro("Done");
|
|
1723
|
+
};
|
|
1724
|
+
//#endregion
|
|
1725
|
+
//#region src/run.ts
|
|
1726
|
+
/**
|
|
1727
|
+
* Non-interactive capture — designed for agent/skill usage.
|
|
1728
|
+
* Returns the formatted style_capture prompt as a string.
|
|
1729
|
+
*/
|
|
1730
|
+
const run = async (options) => {
|
|
1731
|
+
const settings = {
|
|
1732
|
+
...createDefaultSettings(),
|
|
1733
|
+
captureMode: options.mode ?? "curated"
|
|
1734
|
+
};
|
|
1735
|
+
const browser = await chromium.launch({ headless: true });
|
|
1736
|
+
try {
|
|
1737
|
+
const page = await browser.newPage();
|
|
1738
|
+
await page.goto(options.url, { waitUntil: "networkidle" });
|
|
1739
|
+
const count = await page.locator(options.selector).count();
|
|
1740
|
+
if (count === 0) throw new Error(`Selector "${options.selector}" matched 0 elements on ${options.url}`);
|
|
1741
|
+
if (count > 1) process.stderr.write(`Warning: selector matched ${count} elements, capturing the first\n`);
|
|
1742
|
+
const capture = await captureElement(page, options.selector, settings);
|
|
1743
|
+
const mapping = mapCaptureToTailwind(capture);
|
|
1744
|
+
const output = formatCaptureForClaudeMarkdown(capture, mapping);
|
|
1745
|
+
process.stderr.write(`Captured ${capture.summary.elementCount} elements, ${mapping.summary.utilityCount} Tailwind utilities mapped\n`);
|
|
1746
|
+
return output;
|
|
1747
|
+
} finally {
|
|
1748
|
+
await browser.close();
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
//#endregion
|
|
1752
|
+
//#region src/cli.ts
|
|
1753
|
+
const program = new Command();
|
|
1754
|
+
program.name("style-capture").description("Capture computed CSS and HTML from web pages, map to Tailwind utilities").version("0.0.1").argument("[url]", "URL to capture").argument("[selector]", "CSS selector for target element").option("-m, --mode <mode>", "capture mode: curated or full", "curated").action(async (url, selector, options) => {
|
|
1755
|
+
if (url && selector) {
|
|
1756
|
+
const result = await run({
|
|
1757
|
+
mode: options?.mode ?? "curated",
|
|
1758
|
+
selector,
|
|
1759
|
+
url
|
|
1760
|
+
});
|
|
1761
|
+
process.stdout.write(result);
|
|
1762
|
+
} else await interactive();
|
|
1763
|
+
});
|
|
1764
|
+
program.parse();
|
|
1765
|
+
//#endregion
|
|
1766
|
+
export {};
|
|
1767
|
+
|
|
1768
|
+
//# sourceMappingURL=cli.js.map
|