@style-capture/core 0.0.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/LICENSE.md +21 -0
- package/README.md +100 -0
- package/dist/claude-export.d.ts +25 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1308 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +18 -0
- package/dist/tailwind-mapper.d.ts +59 -0
- package/dist/types.d.ts +53 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1308 @@
|
|
|
1
|
+
//#region src/claude-export.ts
|
|
2
|
+
const CAPTURE_NODE_ATTRIBUTE = "data-lc";
|
|
3
|
+
const MAX_TAILWIND_SUGGESTIONS = 6;
|
|
4
|
+
const MAX_OPEN_QUESTION_ITEMS = 8;
|
|
5
|
+
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.";
|
|
6
|
+
const compactInlineText = (value) => value.replaceAll(/\s+/g, " ").trim();
|
|
7
|
+
const escapeXmlAttribute = (value) => value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
8
|
+
const compactSelector = (selector) => selector.replaceAll(/\s*([>+~])\s*/g, "$1").replaceAll(/,\s+/g, ",").replaceAll(/\s+/g, " ").trim();
|
|
9
|
+
const buildCompactRefs = (order) => {
|
|
10
|
+
const refs = {};
|
|
11
|
+
for (const [index, elementId] of order.entries()) refs[elementId] = index.toString(36);
|
|
12
|
+
return refs;
|
|
13
|
+
};
|
|
14
|
+
const buildCompactSelector = (ref) => `[${CAPTURE_NODE_ATTRIBUTE}="${ref}"]`;
|
|
15
|
+
const buildFallbackSelectors = (capture) => {
|
|
16
|
+
const selectors = {};
|
|
17
|
+
for (const elementId of capture.order) {
|
|
18
|
+
const snapshot = capture.elements[elementId];
|
|
19
|
+
if (!snapshot) continue;
|
|
20
|
+
selectors[elementId] = compactSelector(snapshot.selector);
|
|
21
|
+
}
|
|
22
|
+
return selectors;
|
|
23
|
+
};
|
|
24
|
+
const stripCommentNodes = (root) => {
|
|
25
|
+
const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
26
|
+
const comments = [];
|
|
27
|
+
while (walker.nextNode()) if (walker.currentNode instanceof Comment) comments.push(walker.currentNode);
|
|
28
|
+
for (const comment of comments) comment.remove();
|
|
29
|
+
};
|
|
30
|
+
const minifyHtmlString = (html) => html.replaceAll(/<!--[\s\S]*?-->/g, "").replaceAll(/>\s+</g, "><").trim();
|
|
31
|
+
const elementMatchesSnapshot = (element, snapshot) => {
|
|
32
|
+
if (element.tagName.toLowerCase() !== snapshot.tagName) return false;
|
|
33
|
+
for (const className of snapshot.classList) if (!element.classList.contains(className)) return false;
|
|
34
|
+
for (const [name, value] of Object.entries(snapshot.attributes)) {
|
|
35
|
+
if (name === "class") continue;
|
|
36
|
+
if (element.getAttribute(name) !== value) return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
const findMatchingElementIndex = (candidates, snapshot, startIndex) => {
|
|
41
|
+
for (let index = startIndex; index < candidates.length; index += 1) if (elementMatchesSnapshot(candidates[index], snapshot)) return index;
|
|
42
|
+
return -1;
|
|
43
|
+
};
|
|
44
|
+
const annotateCaptureHtml = (capture) => {
|
|
45
|
+
const refs = buildCompactRefs(capture.order);
|
|
46
|
+
if (!capture.rootOuterHtml.trim()) return {
|
|
47
|
+
html: "",
|
|
48
|
+
refs,
|
|
49
|
+
selectors: buildFallbackSelectors(capture)
|
|
50
|
+
};
|
|
51
|
+
if (typeof DOMParser === "undefined") return {
|
|
52
|
+
html: minifyHtmlString(capture.rootOuterHtml),
|
|
53
|
+
refs,
|
|
54
|
+
selectors: buildFallbackSelectors(capture)
|
|
55
|
+
};
|
|
56
|
+
const root = new DOMParser().parseFromString(capture.rootOuterHtml, "text/html").body.firstElementChild;
|
|
57
|
+
if (!root) return {
|
|
58
|
+
html: minifyHtmlString(capture.rootOuterHtml),
|
|
59
|
+
refs,
|
|
60
|
+
selectors: buildFallbackSelectors(capture)
|
|
61
|
+
};
|
|
62
|
+
stripCommentNodes(root);
|
|
63
|
+
const candidates = [root, ...root.querySelectorAll("*")];
|
|
64
|
+
const selectors = buildFallbackSelectors(capture);
|
|
65
|
+
let searchStartIndex = 0;
|
|
66
|
+
for (const elementId of capture.order) {
|
|
67
|
+
const snapshot = capture.elements[elementId];
|
|
68
|
+
const ref = refs[elementId];
|
|
69
|
+
if (!(snapshot && ref)) continue;
|
|
70
|
+
const matchIndex = findMatchingElementIndex(candidates, snapshot, searchStartIndex);
|
|
71
|
+
if (matchIndex === -1) continue;
|
|
72
|
+
candidates[matchIndex].setAttribute(CAPTURE_NODE_ATTRIBUTE, ref);
|
|
73
|
+
selectors[elementId] = buildCompactSelector(ref);
|
|
74
|
+
searchStartIndex = matchIndex + 1;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
html: minifyHtmlString(root.outerHTML),
|
|
78
|
+
refs,
|
|
79
|
+
selectors
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const formatCssPropertyName = (property) => {
|
|
83
|
+
if (property.includes("-")) return property;
|
|
84
|
+
return property.replaceAll(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
85
|
+
};
|
|
86
|
+
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(";");
|
|
87
|
+
const formatPseudoBlock = (selector, pseudo) => {
|
|
88
|
+
if (!pseudo) return "";
|
|
89
|
+
const declarationBlock = formatDeclarationBlock(pseudo.styles);
|
|
90
|
+
if (!declarationBlock) return "";
|
|
91
|
+
return `${selector}::${pseudo.kind}{${declarationBlock}}`;
|
|
92
|
+
};
|
|
93
|
+
const formatElementCssBlock = (snapshot, selector) => {
|
|
94
|
+
const parts = [];
|
|
95
|
+
const declarationBlock = formatDeclarationBlock(snapshot.styles);
|
|
96
|
+
const beforeBlock = formatPseudoBlock(selector, snapshot.pseudo.before);
|
|
97
|
+
const afterBlock = formatPseudoBlock(selector, snapshot.pseudo.after);
|
|
98
|
+
if (declarationBlock) parts.push(`${selector}{${declarationBlock}}`);
|
|
99
|
+
if (beforeBlock) parts.push(beforeBlock);
|
|
100
|
+
if (afterBlock) parts.push(afterBlock);
|
|
101
|
+
return parts.join("");
|
|
102
|
+
};
|
|
103
|
+
const formatCaptureCss = (capture, selectors) => capture.order.map((elementId) => {
|
|
104
|
+
const snapshot = capture.elements[elementId];
|
|
105
|
+
if (!snapshot) return "";
|
|
106
|
+
return formatElementCssBlock(snapshot, selectors[elementId] ?? snapshot.selector);
|
|
107
|
+
}).filter(Boolean).join("");
|
|
108
|
+
const buildTailwindHintEntries = (capture, mapping, refs) => {
|
|
109
|
+
if (!mapping) return [];
|
|
110
|
+
const entries = [];
|
|
111
|
+
const rootMapping = mapping.elements[capture.rootElementId];
|
|
112
|
+
if (rootMapping?.suggestedClassName || rootMapping?.className) entries.push({
|
|
113
|
+
key: "root",
|
|
114
|
+
value: rootMapping.suggestedClassName || rootMapping.className
|
|
115
|
+
});
|
|
116
|
+
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);
|
|
117
|
+
for (const suggestion of topSuggestions) entries.push({
|
|
118
|
+
key: refs[suggestion.elementId] ?? suggestion.elementId,
|
|
119
|
+
value: suggestion.suggestedClassName || suggestion.className
|
|
120
|
+
});
|
|
121
|
+
return entries;
|
|
122
|
+
};
|
|
123
|
+
const buildOpenQuestionEntries = (mapping, refs) => {
|
|
124
|
+
if (!mapping || mapping.reviewQueue.length === 0) return [];
|
|
125
|
+
return mapping.reviewQueue.slice(0, MAX_OPEN_QUESTION_ITEMS).map((item) => ({
|
|
126
|
+
key: refs[item.elementId] ?? item.elementId,
|
|
127
|
+
value: item.reasons.join("; ")
|
|
128
|
+
}));
|
|
129
|
+
};
|
|
130
|
+
const buildClaudeCapturePrompt = (capture, mapping) => {
|
|
131
|
+
const annotation = annotateCaptureHtml(capture);
|
|
132
|
+
const rootElement = capture.elements[capture.rootElementId];
|
|
133
|
+
return {
|
|
134
|
+
cssCapture: formatCaptureCss(capture, annotation.selectors),
|
|
135
|
+
htmlCapture: annotation.html,
|
|
136
|
+
instruction: CLAUDE_CAPTURE_INSTRUCTION,
|
|
137
|
+
metadata: {
|
|
138
|
+
elementCount: capture.summary.elementCount,
|
|
139
|
+
mode: capture.settings.captureMode,
|
|
140
|
+
pseudoCount: capture.summary.pseudoElementCount,
|
|
141
|
+
rootRef: annotation.refs[capture.rootElementId] ?? capture.rootElementId,
|
|
142
|
+
rootSelector: rootElement?.selector ?? "Unavailable",
|
|
143
|
+
url: capture.metadata.url
|
|
144
|
+
},
|
|
145
|
+
openQuestions: buildOpenQuestionEntries(mapping, annotation.refs),
|
|
146
|
+
tailwindHints: buildTailwindHintEntries(capture, mapping, annotation.refs)
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
const formatCaptureForClaudeMarkdown = (capture, mapping) => {
|
|
150
|
+
const prompt = buildClaudeCapturePrompt(capture, mapping);
|
|
151
|
+
const sections = [
|
|
152
|
+
`<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}">`,
|
|
153
|
+
prompt.instruction,
|
|
154
|
+
`<html_capture>${prompt.htmlCapture}</html_capture>`,
|
|
155
|
+
`<css_capture>${prompt.cssCapture}</css_capture>`
|
|
156
|
+
];
|
|
157
|
+
if (prompt.tailwindHints.length > 0) sections.push("<tailwind_hints>", ...prompt.tailwindHints.map((entry) => `${entry.key}=${compactInlineText(entry.value)}`), "</tailwind_hints>");
|
|
158
|
+
if (prompt.openQuestions.length > 0) sections.push("<open_questions>", ...prompt.openQuestions.map((entry) => `${entry.key}:${compactInlineText(entry.value)}`), "</open_questions>");
|
|
159
|
+
sections.push("</style_capture>");
|
|
160
|
+
return sections.join("\n").trim();
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/messages.ts
|
|
164
|
+
const MESSAGE_TYPE_CAPTURE_COMPLETED = "capture/completed";
|
|
165
|
+
const MESSAGE_TYPE_CAPTURE_CANCELLED = "capture/cancelled";
|
|
166
|
+
const MESSAGE_TYPE_CAPTURE_FAILED = "capture/failed";
|
|
167
|
+
const createDefaultSettings = () => ({
|
|
168
|
+
captureMode: "curated",
|
|
169
|
+
includeHiddenElements: false,
|
|
170
|
+
includePseudoElements: true
|
|
171
|
+
});
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/tailwind-mapper.ts
|
|
174
|
+
const HIGH_CONFIDENCE = .92;
|
|
175
|
+
const MEDIUM_CONFIDENCE = .72;
|
|
176
|
+
const LOW_CONFIDENCE = .45;
|
|
177
|
+
const PASSIVE_CONFIDENCE = .84;
|
|
178
|
+
const LENGTH_PATTERN = /^(-?\d*\.?\d+)(px|rem|em|%|vh|vw)?$/;
|
|
179
|
+
const RGB_PATTERN = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([0-9.]+))?\)$/;
|
|
180
|
+
const WHITESPACE_SPLIT_PATTERN = /\s+/;
|
|
181
|
+
const SPACING_SCALE = new Map([
|
|
182
|
+
[0, "0"],
|
|
183
|
+
[1, "px"],
|
|
184
|
+
[2, "0.5"],
|
|
185
|
+
[4, "1"],
|
|
186
|
+
[6, "1.5"],
|
|
187
|
+
[8, "2"],
|
|
188
|
+
[10, "2.5"],
|
|
189
|
+
[12, "3"],
|
|
190
|
+
[14, "3.5"],
|
|
191
|
+
[16, "4"],
|
|
192
|
+
[20, "5"],
|
|
193
|
+
[24, "6"],
|
|
194
|
+
[28, "7"],
|
|
195
|
+
[32, "8"],
|
|
196
|
+
[36, "9"],
|
|
197
|
+
[40, "10"],
|
|
198
|
+
[44, "11"],
|
|
199
|
+
[48, "12"],
|
|
200
|
+
[56, "14"],
|
|
201
|
+
[64, "16"],
|
|
202
|
+
[80, "20"],
|
|
203
|
+
[96, "24"],
|
|
204
|
+
[112, "28"],
|
|
205
|
+
[128, "32"],
|
|
206
|
+
[144, "36"],
|
|
207
|
+
[160, "40"],
|
|
208
|
+
[176, "44"],
|
|
209
|
+
[192, "48"],
|
|
210
|
+
[224, "56"],
|
|
211
|
+
[256, "64"],
|
|
212
|
+
[288, "72"],
|
|
213
|
+
[320, "80"],
|
|
214
|
+
[384, "96"]
|
|
215
|
+
]);
|
|
216
|
+
const FONT_SIZE_SCALE = new Map([
|
|
217
|
+
[12, "xs"],
|
|
218
|
+
[14, "sm"],
|
|
219
|
+
[16, "base"],
|
|
220
|
+
[18, "lg"],
|
|
221
|
+
[20, "xl"],
|
|
222
|
+
[24, "2xl"],
|
|
223
|
+
[30, "3xl"],
|
|
224
|
+
[36, "4xl"],
|
|
225
|
+
[48, "5xl"],
|
|
226
|
+
[60, "6xl"],
|
|
227
|
+
[72, "7xl"],
|
|
228
|
+
[96, "8xl"],
|
|
229
|
+
[128, "9xl"]
|
|
230
|
+
]);
|
|
231
|
+
const LINE_HEIGHT_SCALE = new Map([
|
|
232
|
+
[12, "3"],
|
|
233
|
+
[16, "4"],
|
|
234
|
+
[20, "5"],
|
|
235
|
+
[24, "6"],
|
|
236
|
+
[28, "7"],
|
|
237
|
+
[32, "8"],
|
|
238
|
+
[36, "9"],
|
|
239
|
+
[40, "10"]
|
|
240
|
+
]);
|
|
241
|
+
const RADIUS_SCALE = new Map([
|
|
242
|
+
[0, "none"],
|
|
243
|
+
[2, "sm"],
|
|
244
|
+
[4, ""],
|
|
245
|
+
[6, "md"],
|
|
246
|
+
[8, "lg"],
|
|
247
|
+
[12, "xl"],
|
|
248
|
+
[16, "2xl"],
|
|
249
|
+
[24, "3xl"]
|
|
250
|
+
]);
|
|
251
|
+
const BORDER_WIDTH_SCALE = new Map([
|
|
252
|
+
[1, ""],
|
|
253
|
+
[2, "2"],
|
|
254
|
+
[4, "4"],
|
|
255
|
+
[8, "8"]
|
|
256
|
+
]);
|
|
257
|
+
const Z_INDEX_SCALE = new Set([
|
|
258
|
+
0,
|
|
259
|
+
10,
|
|
260
|
+
20,
|
|
261
|
+
30,
|
|
262
|
+
40,
|
|
263
|
+
50
|
|
264
|
+
]);
|
|
265
|
+
const OPACITY_SCALE = new Set([
|
|
266
|
+
0,
|
|
267
|
+
5,
|
|
268
|
+
10,
|
|
269
|
+
15,
|
|
270
|
+
20,
|
|
271
|
+
25,
|
|
272
|
+
30,
|
|
273
|
+
35,
|
|
274
|
+
40,
|
|
275
|
+
45,
|
|
276
|
+
50,
|
|
277
|
+
55,
|
|
278
|
+
60,
|
|
279
|
+
65,
|
|
280
|
+
70,
|
|
281
|
+
75,
|
|
282
|
+
80,
|
|
283
|
+
85,
|
|
284
|
+
90,
|
|
285
|
+
95,
|
|
286
|
+
100
|
|
287
|
+
]);
|
|
288
|
+
const CLEAN_ARBITRARY_SOURCE_PROPERTIES = new Set([
|
|
289
|
+
"background-color",
|
|
290
|
+
"border-color",
|
|
291
|
+
"color",
|
|
292
|
+
"text-decoration-color"
|
|
293
|
+
]);
|
|
294
|
+
const REVIEW_ONLY_SOURCE_PROPERTIES = new Set([
|
|
295
|
+
"background-image",
|
|
296
|
+
"bottom",
|
|
297
|
+
"font-family",
|
|
298
|
+
"grid-template-columns",
|
|
299
|
+
"grid-template-rows",
|
|
300
|
+
"height",
|
|
301
|
+
"left",
|
|
302
|
+
"pseudo-elements",
|
|
303
|
+
"right",
|
|
304
|
+
"top",
|
|
305
|
+
"transform",
|
|
306
|
+
"transform-origin",
|
|
307
|
+
"width"
|
|
308
|
+
]);
|
|
309
|
+
const DISPLAY_MAP = {
|
|
310
|
+
contents: "contents",
|
|
311
|
+
flex: "flex",
|
|
312
|
+
"flow-root": "flow-root",
|
|
313
|
+
grid: "grid",
|
|
314
|
+
inline: "inline",
|
|
315
|
+
"inline-block": "inline-block",
|
|
316
|
+
"inline-flex": "inline-flex",
|
|
317
|
+
"inline-grid": "inline-grid",
|
|
318
|
+
none: "hidden"
|
|
319
|
+
};
|
|
320
|
+
const POSITION_MAP = {
|
|
321
|
+
absolute: "absolute",
|
|
322
|
+
fixed: "fixed",
|
|
323
|
+
relative: "relative",
|
|
324
|
+
sticky: "sticky"
|
|
325
|
+
};
|
|
326
|
+
const FLEX_DIRECTION_MAP = {
|
|
327
|
+
column: "flex-col",
|
|
328
|
+
"column-reverse": "flex-col-reverse",
|
|
329
|
+
"row-reverse": "flex-row-reverse"
|
|
330
|
+
};
|
|
331
|
+
const FLEX_WRAP_MAP = {
|
|
332
|
+
wrap: "flex-wrap",
|
|
333
|
+
"wrap-reverse": "flex-wrap-reverse"
|
|
334
|
+
};
|
|
335
|
+
const ALIGN_ITEMS_MAP = {
|
|
336
|
+
baseline: "items-baseline",
|
|
337
|
+
center: "items-center",
|
|
338
|
+
end: "items-end",
|
|
339
|
+
"flex-end": "items-end",
|
|
340
|
+
"flex-start": "items-start",
|
|
341
|
+
start: "items-start"
|
|
342
|
+
};
|
|
343
|
+
const JUSTIFY_CONTENT_MAP = {
|
|
344
|
+
center: "justify-center",
|
|
345
|
+
end: "justify-end",
|
|
346
|
+
"flex-end": "justify-end",
|
|
347
|
+
"flex-start": "justify-start",
|
|
348
|
+
left: "justify-start",
|
|
349
|
+
right: "justify-end",
|
|
350
|
+
"space-around": "justify-around",
|
|
351
|
+
"space-between": "justify-between",
|
|
352
|
+
"space-evenly": "justify-evenly",
|
|
353
|
+
start: "justify-start"
|
|
354
|
+
};
|
|
355
|
+
const GRID_AUTO_FLOW_MAP = {
|
|
356
|
+
column: "grid-flow-col",
|
|
357
|
+
"column dense": "grid-flow-col-dense",
|
|
358
|
+
dense: "grid-flow-dense",
|
|
359
|
+
"row dense": "grid-flow-row-dense"
|
|
360
|
+
};
|
|
361
|
+
const TEXT_ALIGN_MAP = {
|
|
362
|
+
center: "text-center",
|
|
363
|
+
end: "text-end",
|
|
364
|
+
justify: "text-justify",
|
|
365
|
+
right: "text-right"
|
|
366
|
+
};
|
|
367
|
+
const TEXT_TRANSFORM_MAP = {
|
|
368
|
+
capitalize: "capitalize",
|
|
369
|
+
lowercase: "lowercase",
|
|
370
|
+
uppercase: "uppercase"
|
|
371
|
+
};
|
|
372
|
+
const WHITE_SPACE_MAP = {
|
|
373
|
+
"break-spaces": "whitespace-break-spaces",
|
|
374
|
+
nowrap: "whitespace-nowrap",
|
|
375
|
+
pre: "whitespace-pre",
|
|
376
|
+
"pre-line": "whitespace-pre-line",
|
|
377
|
+
"pre-wrap": "whitespace-pre-wrap"
|
|
378
|
+
};
|
|
379
|
+
const LIST_STYLE_MAP = {
|
|
380
|
+
decimal: "list-decimal",
|
|
381
|
+
none: "list-none"
|
|
382
|
+
};
|
|
383
|
+
const OBJECT_FIT_MAP = {
|
|
384
|
+
contain: "object-contain",
|
|
385
|
+
cover: "object-cover",
|
|
386
|
+
none: "object-none",
|
|
387
|
+
"scale-down": "object-scale-down"
|
|
388
|
+
};
|
|
389
|
+
const OVERFLOW_MAP = {
|
|
390
|
+
auto: "auto",
|
|
391
|
+
clip: "clip",
|
|
392
|
+
hidden: "hidden",
|
|
393
|
+
scroll: "scroll",
|
|
394
|
+
visible: "visible"
|
|
395
|
+
};
|
|
396
|
+
const BORDER_STYLE_MAP = {
|
|
397
|
+
dashed: "border-dashed",
|
|
398
|
+
dotted: "border-dotted",
|
|
399
|
+
double: "border-double"
|
|
400
|
+
};
|
|
401
|
+
const FONT_WEIGHT_MAP = {
|
|
402
|
+
"100": "thin",
|
|
403
|
+
"200": "extralight",
|
|
404
|
+
"300": "light",
|
|
405
|
+
"500": "medium",
|
|
406
|
+
"600": "semibold",
|
|
407
|
+
"700": "bold",
|
|
408
|
+
"800": "extrabold",
|
|
409
|
+
"900": "black"
|
|
410
|
+
};
|
|
411
|
+
const POSITION_KEYWORD_MAP = {
|
|
412
|
+
"0% 0%": "top-left",
|
|
413
|
+
"0% 100%": "bottom-left",
|
|
414
|
+
"0% 50%": "left",
|
|
415
|
+
"100% 0%": "top-right",
|
|
416
|
+
"100% 100%": "bottom-right",
|
|
417
|
+
"100% 50%": "right",
|
|
418
|
+
"50% 0%": "top",
|
|
419
|
+
"50% 100%": "bottom"
|
|
420
|
+
};
|
|
421
|
+
const DIMENSION_KEYWORD_MAP = {
|
|
422
|
+
"100%": "full",
|
|
423
|
+
"100vh": "screen",
|
|
424
|
+
"100vw": "screen",
|
|
425
|
+
"fit-content": "fit",
|
|
426
|
+
"max-content": "max",
|
|
427
|
+
"min-content": "min"
|
|
428
|
+
};
|
|
429
|
+
const FONT_FAMILY_MAP = [
|
|
430
|
+
{
|
|
431
|
+
keyword: "monospace",
|
|
432
|
+
utility: "font-mono"
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
keyword: "sans-serif",
|
|
436
|
+
utility: "font-sans"
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
keyword: "system-ui",
|
|
440
|
+
utility: "font-sans"
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
keyword: "serif",
|
|
444
|
+
utility: "font-serif"
|
|
445
|
+
}
|
|
446
|
+
];
|
|
447
|
+
const roundToTwo = (value) => Math.round(value * 100) / 100;
|
|
448
|
+
const dedupe = (values) => [...new Set((values ?? []).filter(Boolean))];
|
|
449
|
+
const allEqual = (values) => values.every((value) => value === values[0]);
|
|
450
|
+
const normalizeWhitespace = (value) => value.replaceAll(/\s+/g, " ").trim();
|
|
451
|
+
const parseLength = (value) => {
|
|
452
|
+
const match = value.trim().match(LENGTH_PATTERN);
|
|
453
|
+
if (!match) return null;
|
|
454
|
+
return {
|
|
455
|
+
unit: match[2] ?? "px",
|
|
456
|
+
value: Number(match[1])
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
const isZeroLength = (value) => {
|
|
460
|
+
const length = parseLength(value);
|
|
461
|
+
return Boolean(length && Math.abs(length.value) <= .01);
|
|
462
|
+
};
|
|
463
|
+
const isTransparentColor = (value) => {
|
|
464
|
+
const normalized = value.trim().replaceAll(/\s+/g, "").toLowerCase();
|
|
465
|
+
return normalized === "rgba(0,0,0,0)" || normalized === "transparent";
|
|
466
|
+
};
|
|
467
|
+
const normalizeColor = (value) => {
|
|
468
|
+
const trimmed = value.trim().toLowerCase();
|
|
469
|
+
if (trimmed === "transparent" || isTransparentColor(trimmed)) return "transparent";
|
|
470
|
+
const rgbMatch = trimmed.match(RGB_PATTERN);
|
|
471
|
+
if (!rgbMatch) return trimmed;
|
|
472
|
+
const { 1: redRaw, 2: greenRaw, 3: blueRaw, 4: alpha } = rgbMatch;
|
|
473
|
+
const red = Number(redRaw).toString(16).padStart(2, "0");
|
|
474
|
+
const green = Number(greenRaw).toString(16).padStart(2, "0");
|
|
475
|
+
const blue = Number(blueRaw).toString(16).padStart(2, "0");
|
|
476
|
+
if (!alpha || alpha === "1") return `#${red}${green}${blue}`;
|
|
477
|
+
return trimmed.replaceAll(/\s+/g, "");
|
|
478
|
+
};
|
|
479
|
+
const sanitizeArbitraryValue = (value) => value.trim().replaceAll(/\s*,\s*/g, ",").replaceAll(/\s*\/\s*/g, "/").replaceAll(/\s+/g, "_");
|
|
480
|
+
const createArbitraryUtility = (prefix, value) => `${prefix}-[${sanitizeArbitraryValue(value)}]`;
|
|
481
|
+
const createArbitraryPropertyClass = (property, value) => `[${property}:${sanitizeArbitraryValue(value)}]`;
|
|
482
|
+
const lookupMappedUtility = (map, value) => map[value] ?? null;
|
|
483
|
+
const labelFromConfidence = (confidence) => {
|
|
484
|
+
if (confidence >= .85) return "high";
|
|
485
|
+
if (confidence >= .62) return "medium";
|
|
486
|
+
return "low";
|
|
487
|
+
};
|
|
488
|
+
const addMatch = (accumulator, match) => {
|
|
489
|
+
const utility = match.utility.trim();
|
|
490
|
+
if (!utility) return;
|
|
491
|
+
const nextMatch = {
|
|
492
|
+
...match,
|
|
493
|
+
label: labelFromConfidence(match.confidence),
|
|
494
|
+
notes: dedupe(match.notes),
|
|
495
|
+
sourceProperties: dedupe(match.sourceProperties),
|
|
496
|
+
sourceValues: dedupe(match.sourceValues),
|
|
497
|
+
utility
|
|
498
|
+
};
|
|
499
|
+
const existing = accumulator.matches.find((entry) => entry.utility === utility);
|
|
500
|
+
if (existing) {
|
|
501
|
+
existing.confidence = Math.max(existing.confidence, nextMatch.confidence);
|
|
502
|
+
existing.label = labelFromConfidence(existing.confidence);
|
|
503
|
+
existing.notes = dedupe([...existing.notes, ...nextMatch.notes]);
|
|
504
|
+
existing.sourceProperties = dedupe([...existing.sourceProperties, ...nextMatch.sourceProperties]);
|
|
505
|
+
existing.sourceValues = dedupe([...existing.sourceValues, ...nextMatch.sourceValues]);
|
|
506
|
+
} else accumulator.matches.push(nextMatch);
|
|
507
|
+
if (accumulator.classSet.has(utility)) return;
|
|
508
|
+
accumulator.classSet.add(utility);
|
|
509
|
+
accumulator.classes.push(utility);
|
|
510
|
+
};
|
|
511
|
+
const addUnsupported = (accumulator, property, value, reason) => {
|
|
512
|
+
const key = `${property}:${value}:${reason}`;
|
|
513
|
+
if (accumulator.unsupported.some((entry) => `${entry.property}:${entry.value}:${entry.reason}` === key)) return;
|
|
514
|
+
accumulator.unsupported.push({
|
|
515
|
+
property,
|
|
516
|
+
reason,
|
|
517
|
+
value
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
const addCandidateMatch = (accumulator, property, value, candidate) => {
|
|
521
|
+
if (!(value && candidate)) return;
|
|
522
|
+
addMatch(accumulator, {
|
|
523
|
+
confidence: candidate.confidence,
|
|
524
|
+
notes: candidate.notes ?? [],
|
|
525
|
+
sourceProperties: property.includes("|") ? property.split("|") : [property],
|
|
526
|
+
sourceValues: [value],
|
|
527
|
+
strategy: candidate.strategy,
|
|
528
|
+
utility: candidate.utility
|
|
529
|
+
});
|
|
530
|
+
};
|
|
531
|
+
const addMappedUtility = (accumulator, property, value, map, shouldAdd = true) => {
|
|
532
|
+
if (!(shouldAdd && value)) return;
|
|
533
|
+
const utility = lookupMappedUtility(map, value);
|
|
534
|
+
if (!utility) {
|
|
535
|
+
addUnsupported(accumulator, property, value, `${property} needs manual review.`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
addMatch(accumulator, {
|
|
539
|
+
confidence: HIGH_CONFIDENCE,
|
|
540
|
+
sourceProperties: [property],
|
|
541
|
+
sourceValues: [value],
|
|
542
|
+
strategy: "semantic",
|
|
543
|
+
utility
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
const addArbitraryPropertyMatch = (accumulator, property, value, note) => {
|
|
547
|
+
if (!value || value === "none") return;
|
|
548
|
+
addMatch(accumulator, {
|
|
549
|
+
confidence: LOW_CONFIDENCE,
|
|
550
|
+
notes: [note],
|
|
551
|
+
sourceProperties: [property],
|
|
552
|
+
sourceValues: [value],
|
|
553
|
+
strategy: "arbitrary",
|
|
554
|
+
utility: createArbitraryPropertyClass(property, value)
|
|
555
|
+
});
|
|
556
|
+
};
|
|
557
|
+
const shouldEmitInheritedValue = (property, value, parentValue) => {
|
|
558
|
+
if (!value) return false;
|
|
559
|
+
if (!parentValue) return true;
|
|
560
|
+
if (property === "color") return normalizeColor(value) !== normalizeColor(parentValue);
|
|
561
|
+
return value !== parentValue;
|
|
562
|
+
};
|
|
563
|
+
const shouldMapDimension = (property, value) => {
|
|
564
|
+
if (!value) return false;
|
|
565
|
+
if (property.startsWith("min-")) return value !== "0px" && value !== "auto";
|
|
566
|
+
if (property.startsWith("max-")) return value !== "none";
|
|
567
|
+
return value !== "auto";
|
|
568
|
+
};
|
|
569
|
+
const buildSpacingCandidate = (prefix, value, allowNegative) => {
|
|
570
|
+
if (isZeroLength(value)) return null;
|
|
571
|
+
if (value === "auto" && prefix.startsWith("m")) return {
|
|
572
|
+
confidence: HIGH_CONFIDENCE,
|
|
573
|
+
strategy: "semantic",
|
|
574
|
+
utility: `${prefix}-auto`
|
|
575
|
+
};
|
|
576
|
+
const length = parseLength(value);
|
|
577
|
+
if (!length) return {
|
|
578
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
579
|
+
notes: ["Spacing required an arbitrary value."],
|
|
580
|
+
strategy: "arbitrary",
|
|
581
|
+
utility: createArbitraryUtility(prefix, value)
|
|
582
|
+
};
|
|
583
|
+
if (length.value < 0 && !allowNegative) return null;
|
|
584
|
+
const token = length.unit === "px" ? SPACING_SCALE.get(Math.abs(length.value)) : null;
|
|
585
|
+
if (!token) return {
|
|
586
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
587
|
+
notes: ["Spacing required an arbitrary value."],
|
|
588
|
+
strategy: "arbitrary",
|
|
589
|
+
utility: createArbitraryUtility(prefix, value)
|
|
590
|
+
};
|
|
591
|
+
const baseUtility = token === "px" ? `${prefix}-px` : `${prefix}-${token}`;
|
|
592
|
+
return {
|
|
593
|
+
confidence: HIGH_CONFIDENCE,
|
|
594
|
+
strategy: "scale",
|
|
595
|
+
utility: length.value < 0 ? `-${baseUtility}` : baseUtility
|
|
596
|
+
};
|
|
597
|
+
};
|
|
598
|
+
const buildDimensionCandidate = (prefix, value, options) => {
|
|
599
|
+
if (value === "auto") return {
|
|
600
|
+
confidence: HIGH_CONFIDENCE,
|
|
601
|
+
strategy: "semantic",
|
|
602
|
+
utility: `${prefix}-auto`
|
|
603
|
+
};
|
|
604
|
+
const keywordSuffix = DIMENSION_KEYWORD_MAP[value];
|
|
605
|
+
if (keywordSuffix) return {
|
|
606
|
+
confidence: HIGH_CONFIDENCE,
|
|
607
|
+
strategy: "semantic",
|
|
608
|
+
utility: `${prefix}-${keywordSuffix}`
|
|
609
|
+
};
|
|
610
|
+
const length = parseLength(value);
|
|
611
|
+
const token = length && length.unit === "px" ? SPACING_SCALE.get(Math.abs(length.value)) : null;
|
|
612
|
+
if (token) return {
|
|
613
|
+
confidence: options.confidence,
|
|
614
|
+
notes: options.note ? [options.note] : [],
|
|
615
|
+
strategy: "scale",
|
|
616
|
+
utility: token === "px" ? `${prefix}-px` : `${prefix}-${token}`
|
|
617
|
+
};
|
|
618
|
+
return {
|
|
619
|
+
confidence: options.confidence,
|
|
620
|
+
notes: options.note ? [options.note] : ["Length required an arbitrary value."],
|
|
621
|
+
strategy: "arbitrary",
|
|
622
|
+
utility: createArbitraryUtility(prefix, value)
|
|
623
|
+
};
|
|
624
|
+
};
|
|
625
|
+
const buildScaleCandidate = (prefix, value, scale, note) => {
|
|
626
|
+
const length = parseLength(value);
|
|
627
|
+
const token = length && length.unit === "px" ? scale.get(length.value) : null;
|
|
628
|
+
if (token) return {
|
|
629
|
+
confidence: HIGH_CONFIDENCE,
|
|
630
|
+
strategy: "scale",
|
|
631
|
+
utility: `${prefix}-${token}`
|
|
632
|
+
};
|
|
633
|
+
return {
|
|
634
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
635
|
+
notes: [note],
|
|
636
|
+
strategy: "arbitrary",
|
|
637
|
+
utility: createArbitraryUtility(prefix, value)
|
|
638
|
+
};
|
|
639
|
+
};
|
|
640
|
+
const buildColorCandidate = (prefix, value) => {
|
|
641
|
+
const normalized = normalizeColor(value);
|
|
642
|
+
let semanticUtility = null;
|
|
643
|
+
if (normalized === "transparent") semanticUtility = `${prefix}-transparent`;
|
|
644
|
+
else if (normalized === "#000000") semanticUtility = `${prefix}-black`;
|
|
645
|
+
else if (normalized === "#ffffff") semanticUtility = `${prefix}-white`;
|
|
646
|
+
if (semanticUtility) return {
|
|
647
|
+
confidence: HIGH_CONFIDENCE,
|
|
648
|
+
strategy: "semantic",
|
|
649
|
+
utility: semanticUtility
|
|
650
|
+
};
|
|
651
|
+
return {
|
|
652
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
653
|
+
strategy: "arbitrary",
|
|
654
|
+
utility: createArbitraryUtility(prefix, normalized)
|
|
655
|
+
};
|
|
656
|
+
};
|
|
657
|
+
const buildFontFamilyCandidate = (value) => {
|
|
658
|
+
const normalized = value.toLowerCase();
|
|
659
|
+
for (const { keyword, utility } of FONT_FAMILY_MAP) if (normalized.includes(keyword)) return {
|
|
660
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
661
|
+
notes: ["Mapped to the nearest generic Tailwind family."],
|
|
662
|
+
strategy: "heuristic",
|
|
663
|
+
utility
|
|
664
|
+
};
|
|
665
|
+
return {
|
|
666
|
+
confidence: LOW_CONFIDENCE,
|
|
667
|
+
notes: ["Font family required an arbitrary property utility."],
|
|
668
|
+
strategy: "arbitrary",
|
|
669
|
+
utility: createArbitraryPropertyClass("font-family", value)
|
|
670
|
+
};
|
|
671
|
+
};
|
|
672
|
+
const buildBorderWidthCandidate = (prefix, value) => {
|
|
673
|
+
const length = parseLength(value);
|
|
674
|
+
const token = length && length.unit === "px" ? BORDER_WIDTH_SCALE.get(length.value) : null;
|
|
675
|
+
if (token !== void 0) return {
|
|
676
|
+
confidence: HIGH_CONFIDENCE,
|
|
677
|
+
strategy: "scale",
|
|
678
|
+
utility: token ? `${prefix}-${token}` : prefix
|
|
679
|
+
};
|
|
680
|
+
return {
|
|
681
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
682
|
+
notes: ["Border width required an arbitrary value."],
|
|
683
|
+
strategy: "arbitrary",
|
|
684
|
+
utility: createArbitraryUtility(prefix, value)
|
|
685
|
+
};
|
|
686
|
+
};
|
|
687
|
+
const buildBorderColorCandidate = (value) => buildColorCandidate("border", value);
|
|
688
|
+
const buildRadiusCandidate = (prefix, value) => {
|
|
689
|
+
const length = parseLength(value);
|
|
690
|
+
if (length && length.value >= 9999) return {
|
|
691
|
+
confidence: HIGH_CONFIDENCE,
|
|
692
|
+
strategy: "semantic",
|
|
693
|
+
utility: `${prefix}-full`
|
|
694
|
+
};
|
|
695
|
+
const token = length && length.unit === "px" ? RADIUS_SCALE.get(length.value) : null;
|
|
696
|
+
if (token !== void 0) return {
|
|
697
|
+
confidence: HIGH_CONFIDENCE,
|
|
698
|
+
strategy: "scale",
|
|
699
|
+
utility: token ? `${prefix}-${token}` : prefix
|
|
700
|
+
};
|
|
701
|
+
return {
|
|
702
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
703
|
+
notes: ["Border radius required an arbitrary value."],
|
|
704
|
+
strategy: "arbitrary",
|
|
705
|
+
utility: createArbitraryUtility(prefix, value)
|
|
706
|
+
};
|
|
707
|
+
};
|
|
708
|
+
const buildZIndexCandidate = (value) => {
|
|
709
|
+
const numericValue = Number(value);
|
|
710
|
+
if (Number.isFinite(numericValue) && Z_INDEX_SCALE.has(numericValue)) return {
|
|
711
|
+
confidence: HIGH_CONFIDENCE,
|
|
712
|
+
strategy: "scale",
|
|
713
|
+
utility: `z-${numericValue}`
|
|
714
|
+
};
|
|
715
|
+
return {
|
|
716
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
717
|
+
notes: ["Resolved z-index required an arbitrary value."],
|
|
718
|
+
strategy: "arbitrary",
|
|
719
|
+
utility: `z-[${sanitizeArbitraryValue(value)}]`
|
|
720
|
+
};
|
|
721
|
+
};
|
|
722
|
+
const addGapMatches = (accumulator, gap, rowGap, columnGap) => {
|
|
723
|
+
if (gap && gap !== "normal" && !isZeroLength(gap)) {
|
|
724
|
+
addCandidateMatch(accumulator, "gap", gap, buildSpacingCandidate("gap", gap, false));
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
addCandidateMatch(accumulator, "row-gap", rowGap, rowGap ? buildSpacingCandidate("gap-y", rowGap, false) : null);
|
|
728
|
+
addCandidateMatch(accumulator, "column-gap", columnGap, columnGap ? buildSpacingCandidate("gap-x", columnGap, false) : null);
|
|
729
|
+
};
|
|
730
|
+
const addFlexNumberMatch = (accumulator, property, value, prefix) => {
|
|
731
|
+
if (!value) return;
|
|
732
|
+
let utility = createArbitraryUtility(prefix, value);
|
|
733
|
+
if (value === "1") utility = prefix;
|
|
734
|
+
else if (value === "0") utility = `${prefix}-0`;
|
|
735
|
+
const confidence = value === "0" || value === "1" ? HIGH_CONFIDENCE : LOW_CONFIDENCE;
|
|
736
|
+
const strategy = value === "0" || value === "1" ? "semantic" : "arbitrary";
|
|
737
|
+
addMatch(accumulator, {
|
|
738
|
+
confidence,
|
|
739
|
+
notes: strategy === "semantic" ? [] : [`${property} required an arbitrary value.`],
|
|
740
|
+
sourceProperties: [property],
|
|
741
|
+
sourceValues: [value],
|
|
742
|
+
strategy,
|
|
743
|
+
utility
|
|
744
|
+
});
|
|
745
|
+
};
|
|
746
|
+
const addAxisSpacingMatch = (accumulator, prefix, first, second, allowNegative) => {
|
|
747
|
+
if (!(first[1] && second[1]) || first[1] !== second[1]) return;
|
|
748
|
+
addCandidateMatch(accumulator, `${first[0]}|${second[0]}`, first[1], buildSpacingCandidate(prefix, first[1], allowNegative));
|
|
749
|
+
};
|
|
750
|
+
const addEdgeSpacingMatch = (accumulator, prefix, entry, allowNegative) => {
|
|
751
|
+
if (!entry[1] || isZeroLength(entry[1])) return;
|
|
752
|
+
addCandidateMatch(accumulator, entry[0], entry[1], buildSpacingCandidate(prefix, entry[1], allowNegative));
|
|
753
|
+
};
|
|
754
|
+
const addBoxSpacingMatches = (accumulator, prefix, entries) => {
|
|
755
|
+
const values = entries.map((entry) => entry[1]);
|
|
756
|
+
if (values.some((value) => !value)) return;
|
|
757
|
+
const normalizedValues = values;
|
|
758
|
+
if (normalizedValues.every((value) => isZeroLength(value))) return;
|
|
759
|
+
if (allEqual(normalizedValues)) {
|
|
760
|
+
addCandidateMatch(accumulator, entries[0][0], normalizedValues[0], buildSpacingCandidate(prefix, normalizedValues[0], prefix === "m"));
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
addAxisSpacingMatch(accumulator, `${prefix}y`, entries[0], entries[2], prefix === "m");
|
|
764
|
+
addAxisSpacingMatch(accumulator, `${prefix}x`, entries[1], entries[3], prefix === "m");
|
|
765
|
+
addEdgeSpacingMatch(accumulator, `${prefix}t`, entries[0], prefix === "m");
|
|
766
|
+
addEdgeSpacingMatch(accumulator, `${prefix}r`, entries[1], prefix === "m");
|
|
767
|
+
addEdgeSpacingMatch(accumulator, `${prefix}b`, entries[2], prefix === "m");
|
|
768
|
+
addEdgeSpacingMatch(accumulator, `${prefix}l`, entries[3], prefix === "m");
|
|
769
|
+
};
|
|
770
|
+
const addColorMatch = (accumulator, prefix, property, value, shouldAdd) => {
|
|
771
|
+
if (!(shouldAdd && value)) return;
|
|
772
|
+
addCandidateMatch(accumulator, property, value, buildColorCandidate(prefix, value));
|
|
773
|
+
};
|
|
774
|
+
const addFontFamilyMatch = (accumulator, value, shouldAdd) => {
|
|
775
|
+
if (!(shouldAdd && value)) return;
|
|
776
|
+
addCandidateMatch(accumulator, "font-family", value, buildFontFamilyCandidate(value));
|
|
777
|
+
};
|
|
778
|
+
const addScaleMatch = (accumulator, property, value, shouldAdd, scale, prefix, note) => {
|
|
779
|
+
if (!(shouldAdd && value)) return;
|
|
780
|
+
addCandidateMatch(accumulator, property, value, buildScaleCandidate(prefix, value, scale, note));
|
|
781
|
+
};
|
|
782
|
+
const addFontWeightMatch = (accumulator, value, shouldAdd) => {
|
|
783
|
+
if (!(shouldAdd && value) || value === "400") return;
|
|
784
|
+
const utility = lookupMappedUtility(FONT_WEIGHT_MAP, value);
|
|
785
|
+
addMatch(accumulator, {
|
|
786
|
+
confidence: utility ? HIGH_CONFIDENCE : LOW_CONFIDENCE,
|
|
787
|
+
notes: utility ? [] : ["Font weight required an arbitrary value."],
|
|
788
|
+
sourceProperties: ["font-weight"],
|
|
789
|
+
sourceValues: [value],
|
|
790
|
+
strategy: utility ? "semantic" : "arbitrary",
|
|
791
|
+
utility: utility ? `font-${utility}` : createArbitraryUtility("font", value)
|
|
792
|
+
});
|
|
793
|
+
};
|
|
794
|
+
const addFontStyleMatch = (accumulator, value, shouldAdd) => {
|
|
795
|
+
if (!(shouldAdd && value) || value === "normal") return;
|
|
796
|
+
addMatch(accumulator, {
|
|
797
|
+
confidence: HIGH_CONFIDENCE,
|
|
798
|
+
sourceProperties: ["font-style"],
|
|
799
|
+
sourceValues: [value],
|
|
800
|
+
strategy: "semantic",
|
|
801
|
+
utility: value === "italic" ? "italic" : "not-italic"
|
|
802
|
+
});
|
|
803
|
+
};
|
|
804
|
+
const addTrackingMatch = (accumulator, value, shouldAdd) => {
|
|
805
|
+
if (!(shouldAdd && value) || value === "normal" || isZeroLength(value)) return;
|
|
806
|
+
addMatch(accumulator, {
|
|
807
|
+
confidence: LOW_CONFIDENCE,
|
|
808
|
+
notes: ["Letter spacing required an arbitrary value."],
|
|
809
|
+
sourceProperties: ["letter-spacing"],
|
|
810
|
+
sourceValues: [value],
|
|
811
|
+
strategy: "arbitrary",
|
|
812
|
+
utility: createArbitraryUtility("tracking", value)
|
|
813
|
+
});
|
|
814
|
+
};
|
|
815
|
+
const addDecorationLineMatches = (accumulator, value) => {
|
|
816
|
+
if (!value || value === "none") return;
|
|
817
|
+
for (const part of value.split(WHITESPACE_SPLIT_PATTERN)) {
|
|
818
|
+
const utility = {
|
|
819
|
+
"line-through": "line-through",
|
|
820
|
+
overline: "overline",
|
|
821
|
+
underline: "underline"
|
|
822
|
+
}[part] ?? null;
|
|
823
|
+
if (!utility) continue;
|
|
824
|
+
addMatch(accumulator, {
|
|
825
|
+
confidence: HIGH_CONFIDENCE,
|
|
826
|
+
sourceProperties: ["text-decoration-line"],
|
|
827
|
+
sourceValues: [value],
|
|
828
|
+
strategy: "semantic",
|
|
829
|
+
utility
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
const addUniformBorderMatch = (element, accumulator) => {
|
|
834
|
+
const widths = [
|
|
835
|
+
element.styles["border-top-width"],
|
|
836
|
+
element.styles["border-right-width"],
|
|
837
|
+
element.styles["border-bottom-width"],
|
|
838
|
+
element.styles["border-left-width"]
|
|
839
|
+
];
|
|
840
|
+
const styles = [
|
|
841
|
+
element.styles["border-top-style"],
|
|
842
|
+
element.styles["border-right-style"],
|
|
843
|
+
element.styles["border-bottom-style"],
|
|
844
|
+
element.styles["border-left-style"]
|
|
845
|
+
];
|
|
846
|
+
const colors = [
|
|
847
|
+
element.styles["border-top-color"],
|
|
848
|
+
element.styles["border-right-color"],
|
|
849
|
+
element.styles["border-bottom-color"],
|
|
850
|
+
element.styles["border-left-color"]
|
|
851
|
+
];
|
|
852
|
+
if (widths.some((value) => !value || isZeroLength(value)) || styles.some((value) => !value || value === "none")) return;
|
|
853
|
+
if (!(allEqual(widths) && allEqual(styles) && allEqual(colors))) {
|
|
854
|
+
addUnsupported(accumulator, "border", "mixed sides", "Per-side border variations need manual review.");
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const width = widths[0];
|
|
858
|
+
const borderStyle = styles[0];
|
|
859
|
+
const color = colors[0];
|
|
860
|
+
addCandidateMatch(accumulator, "border-width", width, buildBorderWidthCandidate("border", width));
|
|
861
|
+
if (borderStyle !== "solid") {
|
|
862
|
+
const utility = lookupMappedUtility(BORDER_STYLE_MAP, borderStyle);
|
|
863
|
+
if (utility) addMatch(accumulator, {
|
|
864
|
+
confidence: HIGH_CONFIDENCE,
|
|
865
|
+
sourceProperties: ["border-style"],
|
|
866
|
+
sourceValues: [borderStyle],
|
|
867
|
+
strategy: "semantic",
|
|
868
|
+
utility
|
|
869
|
+
});
|
|
870
|
+
else addUnsupported(accumulator, "border-style", borderStyle, "Border style needs manual review.");
|
|
871
|
+
}
|
|
872
|
+
addCandidateMatch(accumulator, "border-color", color, buildBorderColorCandidate(color));
|
|
873
|
+
};
|
|
874
|
+
const addRadiusMatches = (element, accumulator) => {
|
|
875
|
+
const values = [
|
|
876
|
+
element.styles["border-top-left-radius"],
|
|
877
|
+
element.styles["border-top-right-radius"],
|
|
878
|
+
element.styles["border-bottom-right-radius"],
|
|
879
|
+
element.styles["border-bottom-left-radius"]
|
|
880
|
+
];
|
|
881
|
+
if (values.some((value) => !value) || values.every((value) => isZeroLength(value))) return;
|
|
882
|
+
const radiusValues = values;
|
|
883
|
+
if (allEqual(radiusValues)) {
|
|
884
|
+
addCandidateMatch(accumulator, "border-radius", radiusValues[0], buildRadiusCandidate("rounded", radiusValues[0]));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
addUnsupported(accumulator, "border-radius", radiusValues.join(", "), "Mixed corner radii need manual review.");
|
|
888
|
+
};
|
|
889
|
+
const addShadowMatch = (accumulator, value) => {
|
|
890
|
+
if (!value || value === "none") return;
|
|
891
|
+
addMatch(accumulator, {
|
|
892
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
893
|
+
sourceProperties: ["box-shadow"],
|
|
894
|
+
sourceValues: [value],
|
|
895
|
+
strategy: "arbitrary",
|
|
896
|
+
utility: createArbitraryUtility("shadow", value)
|
|
897
|
+
});
|
|
898
|
+
};
|
|
899
|
+
const addOpacityMatch = (accumulator, value) => {
|
|
900
|
+
if (!value || value === "1") return;
|
|
901
|
+
const numericValue = Number(value);
|
|
902
|
+
if (!Number.isFinite(numericValue)) return;
|
|
903
|
+
const percent = Math.round(numericValue * 100);
|
|
904
|
+
const semantic = OPACITY_SCALE.has(percent);
|
|
905
|
+
addMatch(accumulator, {
|
|
906
|
+
confidence: semantic ? MEDIUM_CONFIDENCE : LOW_CONFIDENCE,
|
|
907
|
+
notes: semantic ? [] : ["Opacity required an arbitrary value."],
|
|
908
|
+
sourceProperties: ["opacity"],
|
|
909
|
+
sourceValues: [value],
|
|
910
|
+
strategy: semantic ? "scale" : "arbitrary",
|
|
911
|
+
utility: semantic ? `opacity-${percent}` : createArbitraryUtility("opacity", value)
|
|
912
|
+
});
|
|
913
|
+
};
|
|
914
|
+
const addPositionMatch = (accumulator, prefix, property, value) => {
|
|
915
|
+
if (!value || value === "50% 50%") return;
|
|
916
|
+
const keyword = POSITION_KEYWORD_MAP[normalizeWhitespace(value)];
|
|
917
|
+
const named = keyword ? `${prefix}-${keyword}` : null;
|
|
918
|
+
addMatch(accumulator, {
|
|
919
|
+
confidence: named ? HIGH_CONFIDENCE : MEDIUM_CONFIDENCE,
|
|
920
|
+
notes: named ? [] : [`${property} required an arbitrary value.`],
|
|
921
|
+
sourceProperties: [property],
|
|
922
|
+
sourceValues: [value],
|
|
923
|
+
strategy: named ? "semantic" : "arbitrary",
|
|
924
|
+
utility: named ?? createArbitraryUtility(prefix, value)
|
|
925
|
+
});
|
|
926
|
+
};
|
|
927
|
+
const addOverflowAxisMatch = (accumulator, property, value) => {
|
|
928
|
+
if (value === "visible") return;
|
|
929
|
+
const suffix = lookupMappedUtility(OVERFLOW_MAP, value);
|
|
930
|
+
if (!suffix) {
|
|
931
|
+
addUnsupported(accumulator, property, value, "Overflow needs manual review.");
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
addMatch(accumulator, {
|
|
935
|
+
confidence: HIGH_CONFIDENCE,
|
|
936
|
+
sourceProperties: [property],
|
|
937
|
+
sourceValues: [value],
|
|
938
|
+
strategy: "semantic",
|
|
939
|
+
utility: `${property}-${suffix}`
|
|
940
|
+
});
|
|
941
|
+
};
|
|
942
|
+
const addObjectPositionMatch = (accumulator, value) => {
|
|
943
|
+
addPositionMatch(accumulator, "object", "object-position", value);
|
|
944
|
+
};
|
|
945
|
+
const mapDisplay = (context, accumulator) => {
|
|
946
|
+
const { display } = context.element.styles;
|
|
947
|
+
if (!display || display === "block") return;
|
|
948
|
+
const utility = lookupMappedUtility(DISPLAY_MAP, display);
|
|
949
|
+
if (utility) {
|
|
950
|
+
addMatch(accumulator, {
|
|
951
|
+
confidence: HIGH_CONFIDENCE,
|
|
952
|
+
sourceProperties: ["display"],
|
|
953
|
+
sourceValues: [display],
|
|
954
|
+
strategy: "semantic",
|
|
955
|
+
utility
|
|
956
|
+
});
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
addUnsupported(accumulator, "display", display, "Display needs manual review.");
|
|
960
|
+
};
|
|
961
|
+
const mapPositioning = (context, accumulator) => {
|
|
962
|
+
const { styles } = context.element;
|
|
963
|
+
const { position } = styles;
|
|
964
|
+
if (position && position !== "static") {
|
|
965
|
+
const utility = lookupMappedUtility(POSITION_MAP, position);
|
|
966
|
+
if (utility) addMatch(accumulator, {
|
|
967
|
+
confidence: HIGH_CONFIDENCE,
|
|
968
|
+
sourceProperties: ["position"],
|
|
969
|
+
sourceValues: [position],
|
|
970
|
+
strategy: "semantic",
|
|
971
|
+
utility
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
if (!position || position === "static") return;
|
|
975
|
+
for (const property of [
|
|
976
|
+
"top",
|
|
977
|
+
"right",
|
|
978
|
+
"bottom",
|
|
979
|
+
"left"
|
|
980
|
+
]) {
|
|
981
|
+
const value = styles[property];
|
|
982
|
+
if (!value || value === "auto") continue;
|
|
983
|
+
addCandidateMatch(accumulator, property, value, buildDimensionCandidate(property, value, {
|
|
984
|
+
confidence: LOW_CONFIDENCE,
|
|
985
|
+
note: "Computed insets are layout-derived and need review."
|
|
986
|
+
}));
|
|
987
|
+
}
|
|
988
|
+
const zIndex = styles["z-index"];
|
|
989
|
+
if (!zIndex || zIndex === "auto") return;
|
|
990
|
+
addCandidateMatch(accumulator, "z-index", zIndex, buildZIndexCandidate(zIndex));
|
|
991
|
+
};
|
|
992
|
+
const mapFlexLayout = (context, accumulator) => {
|
|
993
|
+
const { styles } = context.element;
|
|
994
|
+
const { display } = styles;
|
|
995
|
+
if (display !== "flex" && display !== "inline-flex") return;
|
|
996
|
+
addMappedUtility(accumulator, "flex-direction", styles["flex-direction"], FLEX_DIRECTION_MAP);
|
|
997
|
+
addMappedUtility(accumulator, "flex-wrap", styles["flex-wrap"], FLEX_WRAP_MAP);
|
|
998
|
+
addMappedUtility(accumulator, "justify-content", styles["justify-content"], JUSTIFY_CONTENT_MAP);
|
|
999
|
+
addMappedUtility(accumulator, "align-items", styles["align-items"], ALIGN_ITEMS_MAP);
|
|
1000
|
+
addGapMatches(accumulator, styles.gap, styles["row-gap"], styles["column-gap"]);
|
|
1001
|
+
addFlexNumberMatch(accumulator, "flex-grow", styles["flex-grow"], "grow");
|
|
1002
|
+
addFlexNumberMatch(accumulator, "flex-shrink", styles["flex-shrink"], "shrink");
|
|
1003
|
+
const basis = styles["flex-basis"];
|
|
1004
|
+
if (!basis || basis === "auto") return;
|
|
1005
|
+
addCandidateMatch(accumulator, "flex-basis", basis, buildDimensionCandidate("basis", basis, { confidence: MEDIUM_CONFIDENCE }));
|
|
1006
|
+
};
|
|
1007
|
+
const mapGridLayout = (context, accumulator) => {
|
|
1008
|
+
const { styles } = context.element;
|
|
1009
|
+
const { display } = styles;
|
|
1010
|
+
if (display !== "grid" && display !== "inline-grid") return;
|
|
1011
|
+
addGapMatches(accumulator, styles.gap, styles["row-gap"], styles["column-gap"]);
|
|
1012
|
+
addMappedUtility(accumulator, "grid-auto-flow", styles["grid-auto-flow"], GRID_AUTO_FLOW_MAP);
|
|
1013
|
+
for (const [property, prefix] of [
|
|
1014
|
+
["grid-column-start", "col-start"],
|
|
1015
|
+
["grid-column-end", "col-end"],
|
|
1016
|
+
["grid-row-start", "row-start"],
|
|
1017
|
+
["grid-row-end", "row-end"]
|
|
1018
|
+
]) {
|
|
1019
|
+
const value = styles[property];
|
|
1020
|
+
if (!value || value === "auto") continue;
|
|
1021
|
+
addMatch(accumulator, {
|
|
1022
|
+
confidence: MEDIUM_CONFIDENCE,
|
|
1023
|
+
sourceProperties: [property],
|
|
1024
|
+
sourceValues: [value],
|
|
1025
|
+
strategy: "arbitrary",
|
|
1026
|
+
utility: createArbitraryUtility(prefix, value)
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
addArbitraryPropertyMatch(accumulator, "grid-template-columns", styles["grid-template-columns"], "Computed grid tracks lose authored repeat syntax.");
|
|
1030
|
+
addArbitraryPropertyMatch(accumulator, "grid-template-rows", styles["grid-template-rows"], "Computed grid tracks lose authored repeat syntax.");
|
|
1031
|
+
};
|
|
1032
|
+
const mapSpacing = (context, accumulator) => {
|
|
1033
|
+
addBoxSpacingMatches(accumulator, "p", [
|
|
1034
|
+
["padding-top", context.element.styles["padding-top"]],
|
|
1035
|
+
["padding-right", context.element.styles["padding-right"]],
|
|
1036
|
+
["padding-bottom", context.element.styles["padding-bottom"]],
|
|
1037
|
+
["padding-left", context.element.styles["padding-left"]]
|
|
1038
|
+
]);
|
|
1039
|
+
addBoxSpacingMatches(accumulator, "m", [
|
|
1040
|
+
["margin-top", context.element.styles["margin-top"]],
|
|
1041
|
+
["margin-right", context.element.styles["margin-right"]],
|
|
1042
|
+
["margin-bottom", context.element.styles["margin-bottom"]],
|
|
1043
|
+
["margin-left", context.element.styles["margin-left"]]
|
|
1044
|
+
]);
|
|
1045
|
+
};
|
|
1046
|
+
const mapSizing = (context, accumulator) => {
|
|
1047
|
+
const { styles } = context.element;
|
|
1048
|
+
for (const [property, prefix, confidence] of [
|
|
1049
|
+
[
|
|
1050
|
+
"min-width",
|
|
1051
|
+
"min-w",
|
|
1052
|
+
MEDIUM_CONFIDENCE
|
|
1053
|
+
],
|
|
1054
|
+
[
|
|
1055
|
+
"min-height",
|
|
1056
|
+
"min-h",
|
|
1057
|
+
MEDIUM_CONFIDENCE
|
|
1058
|
+
],
|
|
1059
|
+
[
|
|
1060
|
+
"max-width",
|
|
1061
|
+
"max-w",
|
|
1062
|
+
MEDIUM_CONFIDENCE
|
|
1063
|
+
],
|
|
1064
|
+
[
|
|
1065
|
+
"max-height",
|
|
1066
|
+
"max-h",
|
|
1067
|
+
MEDIUM_CONFIDENCE
|
|
1068
|
+
],
|
|
1069
|
+
[
|
|
1070
|
+
"width",
|
|
1071
|
+
"w",
|
|
1072
|
+
LOW_CONFIDENCE
|
|
1073
|
+
],
|
|
1074
|
+
[
|
|
1075
|
+
"height",
|
|
1076
|
+
"h",
|
|
1077
|
+
LOW_CONFIDENCE
|
|
1078
|
+
]
|
|
1079
|
+
]) {
|
|
1080
|
+
const value = styles[property];
|
|
1081
|
+
if (!shouldMapDimension(property, value)) continue;
|
|
1082
|
+
addCandidateMatch(accumulator, property, value, buildDimensionCandidate(prefix, value, {
|
|
1083
|
+
confidence,
|
|
1084
|
+
note: property === "width" || property === "height" ? "Computed size values are often layout-dependent." : void 0
|
|
1085
|
+
}));
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
const mapTypographyBasics = (context, accumulator) => {
|
|
1089
|
+
const { element, parent } = context;
|
|
1090
|
+
addColorMatch(accumulator, "text", "color", element.styles.color, shouldEmitInheritedValue("color", element.styles.color, parent?.styles.color));
|
|
1091
|
+
addFontFamilyMatch(accumulator, element.styles["font-family"], shouldEmitInheritedValue("font-family", element.styles["font-family"], parent?.styles["font-family"]));
|
|
1092
|
+
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.");
|
|
1093
|
+
addFontWeightMatch(accumulator, element.styles["font-weight"], shouldEmitInheritedValue("font-weight", element.styles["font-weight"], parent?.styles["font-weight"]));
|
|
1094
|
+
addFontStyleMatch(accumulator, element.styles["font-style"], shouldEmitInheritedValue("font-style", element.styles["font-style"], parent?.styles["font-style"]));
|
|
1095
|
+
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.");
|
|
1096
|
+
addTrackingMatch(accumulator, element.styles["letter-spacing"], shouldEmitInheritedValue("letter-spacing", element.styles["letter-spacing"], parent?.styles["letter-spacing"]));
|
|
1097
|
+
};
|
|
1098
|
+
const mapTypographyPresentation = (context, accumulator) => {
|
|
1099
|
+
const { element, parent } = context;
|
|
1100
|
+
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"] ?? ""));
|
|
1101
|
+
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");
|
|
1102
|
+
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");
|
|
1103
|
+
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");
|
|
1104
|
+
addDecorationLineMatches(accumulator, element.styles["text-decoration-line"]);
|
|
1105
|
+
addColorMatch(accumulator, "decoration", "text-decoration-color", element.styles["text-decoration-color"], element.styles["text-decoration-line"] !== "none" && Boolean(element.styles["text-decoration-color"]));
|
|
1106
|
+
};
|
|
1107
|
+
const mapBackground = (context, accumulator) => {
|
|
1108
|
+
addColorMatch(accumulator, "bg", "background-color", context.element.styles["background-color"], !isTransparentColor(context.element.styles["background-color"] ?? ""));
|
|
1109
|
+
addArbitraryPropertyMatch(accumulator, "background-image", context.element.styles["background-image"], "Background images are emitted as arbitrary properties.");
|
|
1110
|
+
};
|
|
1111
|
+
const mapBorder = (context, accumulator) => {
|
|
1112
|
+
addUniformBorderMatch(context.element, accumulator);
|
|
1113
|
+
addRadiusMatches(context.element, accumulator);
|
|
1114
|
+
};
|
|
1115
|
+
const mapEffects = (context, accumulator) => {
|
|
1116
|
+
const { styles } = context.element;
|
|
1117
|
+
addShadowMatch(accumulator, styles["box-shadow"]);
|
|
1118
|
+
addOpacityMatch(accumulator, styles.opacity);
|
|
1119
|
+
addArbitraryPropertyMatch(accumulator, "transform", styles.transform, "Computed transforms are emitted as raw properties.");
|
|
1120
|
+
addPositionMatch(accumulator, "origin", "transform-origin", styles["transform-origin"]);
|
|
1121
|
+
if (styles.visibility === "hidden") addMatch(accumulator, {
|
|
1122
|
+
confidence: HIGH_CONFIDENCE,
|
|
1123
|
+
sourceProperties: ["visibility"],
|
|
1124
|
+
sourceValues: ["hidden"],
|
|
1125
|
+
strategy: "semantic",
|
|
1126
|
+
utility: "invisible"
|
|
1127
|
+
});
|
|
1128
|
+
};
|
|
1129
|
+
const mapOverflow = (context, accumulator) => {
|
|
1130
|
+
const overflowX = context.element.styles["overflow-x"];
|
|
1131
|
+
const overflowY = context.element.styles["overflow-y"];
|
|
1132
|
+
if (!(overflowX && overflowY)) return;
|
|
1133
|
+
if (overflowX === overflowY && overflowX !== "visible") {
|
|
1134
|
+
const suffix = lookupMappedUtility(OVERFLOW_MAP, overflowX);
|
|
1135
|
+
if (suffix) addMatch(accumulator, {
|
|
1136
|
+
confidence: HIGH_CONFIDENCE,
|
|
1137
|
+
sourceProperties: ["overflow-x", "overflow-y"],
|
|
1138
|
+
sourceValues: [overflowX, overflowY],
|
|
1139
|
+
strategy: "semantic",
|
|
1140
|
+
utility: `overflow-${suffix}`
|
|
1141
|
+
});
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
addOverflowAxisMatch(accumulator, "overflow-x", overflowX);
|
|
1145
|
+
addOverflowAxisMatch(accumulator, "overflow-y", overflowY);
|
|
1146
|
+
};
|
|
1147
|
+
const mapObjectLayout = (context, accumulator) => {
|
|
1148
|
+
const objectFit = context.element.styles["object-fit"];
|
|
1149
|
+
if (objectFit && objectFit !== "fill") {
|
|
1150
|
+
const utility = lookupMappedUtility(OBJECT_FIT_MAP, objectFit);
|
|
1151
|
+
if (utility) addMatch(accumulator, {
|
|
1152
|
+
confidence: HIGH_CONFIDENCE,
|
|
1153
|
+
sourceProperties: ["object-fit"],
|
|
1154
|
+
sourceValues: [objectFit],
|
|
1155
|
+
strategy: "semantic",
|
|
1156
|
+
utility
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
addObjectPositionMatch(accumulator, context.element.styles["object-position"]);
|
|
1160
|
+
};
|
|
1161
|
+
const mapPseudoElements = (context, accumulator) => {
|
|
1162
|
+
const pseudoKinds = Object.keys(context.element.pseudo);
|
|
1163
|
+
if (pseudoKinds.length === 0) return;
|
|
1164
|
+
addUnsupported(accumulator, "pseudo-elements", pseudoKinds.join(", "), "Pseudo-elements were captured, but Tailwind output still needs manual content utilities.");
|
|
1165
|
+
};
|
|
1166
|
+
const isReviewOnlyNote = (note) => {
|
|
1167
|
+
const normalizedNote = note.toLowerCase();
|
|
1168
|
+
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");
|
|
1169
|
+
};
|
|
1170
|
+
const shouldMoveMatchToReview = (match) => {
|
|
1171
|
+
if (match.label === "low" || match.utility.startsWith("[")) return true;
|
|
1172
|
+
if (match.sourceProperties.some((property) => REVIEW_ONLY_SOURCE_PROPERTIES.has(property))) return true;
|
|
1173
|
+
if (match.strategy === "arbitrary" && !match.sourceProperties.every((property) => CLEAN_ARBITRARY_SOURCE_PROPERTIES.has(property))) return true;
|
|
1174
|
+
return match.notes.some((note) => isReviewOnlyNote(note));
|
|
1175
|
+
};
|
|
1176
|
+
const splitMatchesForSuggestion = (matches) => {
|
|
1177
|
+
const reviewMatches = [];
|
|
1178
|
+
const suggestedMatches = [];
|
|
1179
|
+
for (const match of matches) if (shouldMoveMatchToReview(match)) reviewMatches.push(match);
|
|
1180
|
+
else suggestedMatches.push(match);
|
|
1181
|
+
return {
|
|
1182
|
+
reviewMatches,
|
|
1183
|
+
suggestedMatches
|
|
1184
|
+
};
|
|
1185
|
+
};
|
|
1186
|
+
const calculateElementConfidence = (accumulator) => {
|
|
1187
|
+
if (accumulator.matches.length === 0) return accumulator.unsupported.length > 0 ? LOW_CONFIDENCE : PASSIVE_CONFIDENCE;
|
|
1188
|
+
const average = accumulator.matches.reduce((sum, match) => sum + match.confidence, 0) / accumulator.matches.length;
|
|
1189
|
+
if (accumulator.unsupported.length === 0) return roundToTwo(average);
|
|
1190
|
+
return roundToTwo(Math.max(LOW_CONFIDENCE, average - .12));
|
|
1191
|
+
};
|
|
1192
|
+
const buildReviewFallbackNote = (match) => {
|
|
1193
|
+
if (match.label === "low") return "Low-confidence utility needs manual review.";
|
|
1194
|
+
if (match.sourceProperties.some((property) => REVIEW_ONLY_SOURCE_PROPERTIES.has(property))) return "Computed layout or custom CSS was kept out of the clean suggestion.";
|
|
1195
|
+
if (match.strategy === "arbitrary") return "Arbitrary utility was kept out of the clean suggestion.";
|
|
1196
|
+
return "Utility was kept out of the clean suggestion for review.";
|
|
1197
|
+
};
|
|
1198
|
+
const buildReviewItem = (mapping) => {
|
|
1199
|
+
const reasons = [...mapping.unsupported.map((entry) => `${entry.property}: ${entry.reason}`), ...mapping.matches.filter((match) => shouldMoveMatchToReview(match)).flatMap((match) => {
|
|
1200
|
+
return (match.notes.length ? match.notes : [buildReviewFallbackNote(match)]).map((note) => `${match.utility}: ${note}`);
|
|
1201
|
+
})];
|
|
1202
|
+
if (reasons.length === 0) return null;
|
|
1203
|
+
return {
|
|
1204
|
+
confidence: mapping.confidence,
|
|
1205
|
+
confidenceLabel: mapping.confidenceLabel,
|
|
1206
|
+
elementId: mapping.elementId,
|
|
1207
|
+
reasons: dedupe(reasons).slice(0, 6),
|
|
1208
|
+
selector: mapping.selector,
|
|
1209
|
+
unsupportedCount: mapping.unsupported.length
|
|
1210
|
+
};
|
|
1211
|
+
};
|
|
1212
|
+
const createAccumulator = () => ({
|
|
1213
|
+
classSet: /* @__PURE__ */ new Set(),
|
|
1214
|
+
classes: [],
|
|
1215
|
+
matches: [],
|
|
1216
|
+
unsupported: []
|
|
1217
|
+
});
|
|
1218
|
+
const MAPPING_STEPS = [
|
|
1219
|
+
mapDisplay,
|
|
1220
|
+
mapPositioning,
|
|
1221
|
+
mapFlexLayout,
|
|
1222
|
+
mapGridLayout,
|
|
1223
|
+
mapSpacing,
|
|
1224
|
+
mapSizing,
|
|
1225
|
+
mapTypographyBasics,
|
|
1226
|
+
mapTypographyPresentation,
|
|
1227
|
+
mapBackground,
|
|
1228
|
+
mapBorder,
|
|
1229
|
+
mapEffects,
|
|
1230
|
+
mapOverflow,
|
|
1231
|
+
mapObjectLayout,
|
|
1232
|
+
mapPseudoElements
|
|
1233
|
+
];
|
|
1234
|
+
const mapElementSnapshot = (context) => {
|
|
1235
|
+
const accumulator = createAccumulator();
|
|
1236
|
+
for (const step of MAPPING_STEPS) step(context, accumulator);
|
|
1237
|
+
const confidence = calculateElementConfidence(accumulator);
|
|
1238
|
+
const { reviewMatches, suggestedMatches } = splitMatchesForSuggestion(accumulator.matches);
|
|
1239
|
+
return {
|
|
1240
|
+
className: accumulator.classes.join(" "),
|
|
1241
|
+
confidence,
|
|
1242
|
+
confidenceLabel: labelFromConfidence(confidence),
|
|
1243
|
+
elementId: context.element.id,
|
|
1244
|
+
matchCount: accumulator.matches.length,
|
|
1245
|
+
matches: accumulator.matches,
|
|
1246
|
+
reviewClassName: reviewMatches.map((match) => match.utility).join(" "),
|
|
1247
|
+
reviewMatchCount: reviewMatches.length,
|
|
1248
|
+
selector: context.element.selector,
|
|
1249
|
+
suggestedClassName: suggestedMatches.map((match) => match.utility).join(" "),
|
|
1250
|
+
suggestedMatchCount: suggestedMatches.length,
|
|
1251
|
+
tagName: context.element.tagName,
|
|
1252
|
+
unsupported: accumulator.unsupported
|
|
1253
|
+
};
|
|
1254
|
+
};
|
|
1255
|
+
const mapCaptureToTailwind = (capture) => {
|
|
1256
|
+
const elements = {};
|
|
1257
|
+
const reviewQueue = [];
|
|
1258
|
+
let cleanUtilityCount = 0;
|
|
1259
|
+
let confidenceSum = 0;
|
|
1260
|
+
let lowConfidenceElementCount = 0;
|
|
1261
|
+
let mappedElementCount = 0;
|
|
1262
|
+
let reviewUtilityCount = 0;
|
|
1263
|
+
let unsupportedPropertyCount = 0;
|
|
1264
|
+
let utilityCount = 0;
|
|
1265
|
+
for (const elementId of capture.order) {
|
|
1266
|
+
const element = capture.elements[elementId];
|
|
1267
|
+
if (!element) continue;
|
|
1268
|
+
const mapping = mapElementSnapshot({
|
|
1269
|
+
element,
|
|
1270
|
+
parent: element.parentId === null ? null : capture.elements[element.parentId] ?? null
|
|
1271
|
+
});
|
|
1272
|
+
elements[elementId] = mapping;
|
|
1273
|
+
confidenceSum += mapping.confidence;
|
|
1274
|
+
if (mapping.confidenceLabel === "low") lowConfidenceElementCount += 1;
|
|
1275
|
+
if (mapping.matchCount > 0) mappedElementCount += 1;
|
|
1276
|
+
unsupportedPropertyCount += mapping.unsupported.length;
|
|
1277
|
+
cleanUtilityCount += mapping.suggestedMatchCount;
|
|
1278
|
+
reviewUtilityCount += mapping.reviewMatchCount;
|
|
1279
|
+
utilityCount += mapping.matches.length;
|
|
1280
|
+
const reviewItem = buildReviewItem(mapping);
|
|
1281
|
+
if (reviewItem) reviewQueue.push(reviewItem);
|
|
1282
|
+
}
|
|
1283
|
+
reviewQueue.sort((left, right) => {
|
|
1284
|
+
if (left.confidence !== right.confidence) return left.confidence - right.confidence;
|
|
1285
|
+
return right.unsupportedCount - left.unsupportedCount;
|
|
1286
|
+
});
|
|
1287
|
+
const elementCount = capture.order.length;
|
|
1288
|
+
return {
|
|
1289
|
+
elements,
|
|
1290
|
+
order: capture.order,
|
|
1291
|
+
reviewQueue,
|
|
1292
|
+
summary: {
|
|
1293
|
+
averageConfidence: elementCount ? roundToTwo(confidenceSum / elementCount) : 0,
|
|
1294
|
+
cleanUtilityCount,
|
|
1295
|
+
elementCount,
|
|
1296
|
+
lowConfidenceElementCount,
|
|
1297
|
+
mappedElementCount,
|
|
1298
|
+
reviewCount: reviewQueue.length,
|
|
1299
|
+
reviewUtilityCount,
|
|
1300
|
+
unsupportedPropertyCount,
|
|
1301
|
+
utilityCount
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
};
|
|
1305
|
+
//#endregion
|
|
1306
|
+
export { MESSAGE_TYPE_CAPTURE_CANCELLED, MESSAGE_TYPE_CAPTURE_COMPLETED, MESSAGE_TYPE_CAPTURE_FAILED, buildClaudeCapturePrompt, createDefaultSettings, formatCaptureForClaudeMarkdown, mapCaptureToTailwind };
|
|
1307
|
+
|
|
1308
|
+
//# sourceMappingURL=index.js.map
|