@maizzle/framework 6.0.0-rc.12 → 6.0.0-rc.14

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.
Files changed (99) hide show
  1. package/dist/build.mjs +4 -1
  2. package/dist/build.mjs.map +1 -1
  3. package/dist/components/Button.vue +2 -2
  4. package/dist/components/CodeBlock.vue +2 -1
  5. package/dist/components/Column.vue +28 -22
  6. package/dist/components/Container.vue +47 -9
  7. package/dist/components/Font.vue +96 -0
  8. package/dist/components/Layout.vue +9 -4
  9. package/dist/components/Overlap.vue +75 -18
  10. package/dist/components/Row.vue +40 -19
  11. package/dist/components/Section.vue +35 -8
  12. package/dist/components/utils.d.mts +14 -1
  13. package/dist/components/utils.d.mts.map +1 -1
  14. package/dist/components/utils.mjs +32 -1
  15. package/dist/components/utils.mjs.map +1 -1
  16. package/dist/components/utils.ts +39 -0
  17. package/dist/composables/renderContext.d.mts +8 -1
  18. package/dist/composables/renderContext.d.mts.map +1 -1
  19. package/dist/composables/renderContext.mjs.map +1 -1
  20. package/dist/composables/useFont.d.mts +50 -0
  21. package/dist/composables/useFont.d.mts.map +1 -0
  22. package/dist/composables/useFont.mjs +93 -0
  23. package/dist/composables/useFont.mjs.map +1 -0
  24. package/dist/index.d.mts +2 -1
  25. package/dist/index.mjs +2 -1
  26. package/dist/plugins/postcss/quoteFontFamilies.d.mts +13 -0
  27. package/dist/plugins/postcss/quoteFontFamilies.d.mts.map +1 -0
  28. package/dist/plugins/postcss/quoteFontFamilies.mjs +84 -0
  29. package/dist/plugins/postcss/quoteFontFamilies.mjs.map +1 -0
  30. package/dist/render/createRenderer.mjs +8 -2
  31. package/dist/render/createRenderer.mjs.map +1 -1
  32. package/dist/render/injectFonts.d.mts +15 -0
  33. package/dist/render/injectFonts.d.mts.map +1 -0
  34. package/dist/render/injectFonts.mjs +46 -0
  35. package/dist/render/injectFonts.mjs.map +1 -0
  36. package/dist/serve.d.mts.map +1 -1
  37. package/dist/serve.mjs +28 -12
  38. package/dist/serve.mjs.map +1 -1
  39. package/dist/server/compatibility.d.mts +54 -2
  40. package/dist/server/compatibility.d.mts.map +1 -1
  41. package/dist/server/compatibility.mjs +890 -76
  42. package/dist/server/compatibility.mjs.map +1 -1
  43. package/dist/server/linter.d.mts +15 -2
  44. package/dist/server/linter.d.mts.map +1 -1
  45. package/dist/server/linter.mjs +194 -43
  46. package/dist/server/linter.mjs.map +1 -1
  47. package/dist/server/sfc-utils.d.mts +18 -0
  48. package/dist/server/sfc-utils.d.mts.map +1 -0
  49. package/dist/server/sfc-utils.mjs +184 -0
  50. package/dist/server/sfc-utils.mjs.map +1 -0
  51. package/dist/server/ui/App.vue +27 -50
  52. package/dist/server/ui/components/SidebarClose.vue +12 -0
  53. package/dist/server/ui/components/ui/command/Command.vue +1 -0
  54. package/dist/server/ui/components/ui/input/Input.vue +1 -1
  55. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +1 -1
  56. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +1 -1
  57. package/dist/server/ui/lib/emulated-dark-mode.ts +131 -0
  58. package/dist/server/ui/pages/Preview.vue +215 -156
  59. package/dist/transformers/addAttributes.mjs +10 -6
  60. package/dist/transformers/addAttributes.mjs.map +1 -1
  61. package/dist/transformers/columnWidth.d.mts +31 -0
  62. package/dist/transformers/columnWidth.d.mts.map +1 -0
  63. package/dist/transformers/columnWidth.mjs +166 -0
  64. package/dist/transformers/columnWidth.mjs.map +1 -0
  65. package/dist/transformers/index.d.mts.map +1 -1
  66. package/dist/transformers/index.mjs +4 -0
  67. package/dist/transformers/index.mjs.map +1 -1
  68. package/dist/transformers/inlineCSS.mjs +2 -2
  69. package/dist/transformers/inlineCSS.mjs.map +1 -1
  70. package/dist/transformers/msoWidthFromClass.d.mts +19 -0
  71. package/dist/transformers/msoWidthFromClass.d.mts.map +1 -0
  72. package/dist/transformers/msoWidthFromClass.mjs +61 -0
  73. package/dist/transformers/msoWidthFromClass.mjs.map +1 -0
  74. package/dist/transformers/purgeCSS.mjs +1 -1
  75. package/dist/transformers/purgeCSS.mjs.map +1 -1
  76. package/dist/transformers/tailwindcss.d.mts.map +1 -1
  77. package/dist/transformers/tailwindcss.mjs +6 -16
  78. package/dist/transformers/tailwindcss.mjs.map +1 -1
  79. package/dist/types/config.d.mts +42 -2
  80. package/dist/types/config.d.mts.map +1 -1
  81. package/dist/types/index.d.mts +2 -2
  82. package/dist/utils/decodeStyleEntities.d.mts +15 -0
  83. package/dist/utils/decodeStyleEntities.d.mts.map +1 -0
  84. package/dist/utils/decodeStyleEntities.mjs +18 -0
  85. package/dist/utils/decodeStyleEntities.mjs.map +1 -0
  86. package/package.json +2 -3
  87. package/dist/_virtual/_rolldown/runtime.mjs +0 -32
  88. package/dist/node_modules/picomatch/index.mjs +0 -13
  89. package/dist/node_modules/picomatch/index.mjs.map +0 -1
  90. package/dist/node_modules/picomatch/lib/constants.mjs +0 -174
  91. package/dist/node_modules/picomatch/lib/constants.mjs.map +0 -1
  92. package/dist/node_modules/picomatch/lib/parse.mjs +0 -1067
  93. package/dist/node_modules/picomatch/lib/parse.mjs.map +0 -1
  94. package/dist/node_modules/picomatch/lib/picomatch.mjs +0 -304
  95. package/dist/node_modules/picomatch/lib/picomatch.mjs.map +0 -1
  96. package/dist/node_modules/picomatch/lib/scan.mjs +0 -296
  97. package/dist/node_modules/picomatch/lib/scan.mjs.map +0 -1
  98. package/dist/node_modules/picomatch/lib/utils.mjs +0 -53
  99. package/dist/node_modules/picomatch/lib/utils.mjs.map +0 -1
@@ -1,91 +1,905 @@
1
- import { caniemail, rawData } from "caniemail";
1
+ import { tailwindcss } from "../transformers/tailwindcss.mjs";
2
+ import { buildComponentMap, findComponentTags, parseSfcBlocks } from "./sfc-utils.mjs";
3
+ import { scanLint } from "./linter.mjs";
4
+ import { readFileSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+ import { Parser } from "htmlparser2";
7
+ import { DomHandler } from "domhandler";
8
+ import safeParser from "postcss-safe-parser";
9
+ import valueParser from "postcss-value-parser";
2
10
 
3
11
  //#region src/server/compatibility.ts
4
- async function serveCompatibility(req, res) {
12
+ const API_URL = "https://www.caniemail.com/api/data.json";
13
+ const DEFAULT_CLIENTS = new Set([
14
+ "gmail",
15
+ "apple-mail",
16
+ "outlook",
17
+ "yahoo"
18
+ ]);
19
+ let indexes = null;
20
+ let initPromise = null;
21
+ function mpush(m, k, v) {
22
+ const arr = m.get(k);
23
+ if (arr) arr.push(v);
24
+ else m.set(k, [v]);
25
+ }
26
+ function emptyIndexes(nicenames, familyNicenames) {
27
+ return {
28
+ nicenames,
29
+ familyNicenames,
30
+ cssProp: /* @__PURE__ */ new Map(),
31
+ cssPropValue: /* @__PURE__ */ new Map(),
32
+ cssAtRule: /* @__PURE__ */ new Map(),
33
+ cssMediaFeature: /* @__PURE__ */ new Map(),
34
+ cssPseudoClass: /* @__PURE__ */ new Map(),
35
+ cssPseudoElement: /* @__PURE__ */ new Map(),
36
+ cssFunction: /* @__PURE__ */ new Map(),
37
+ cssUnit: /* @__PURE__ */ new Map(),
38
+ htmlTag: /* @__PURE__ */ new Map(),
39
+ htmlAttr: /* @__PURE__ */ new Map(),
40
+ htmlInputType: /* @__PURE__ */ new Map(),
41
+ htmlButtonType: /* @__PURE__ */ new Map(),
42
+ imageExt: /* @__PURE__ */ new Map(),
43
+ bySlug: /* @__PURE__ */ new Map()
44
+ };
45
+ }
46
+ function hasAnyNonY(stats) {
47
+ if (!stats) return false;
48
+ for (const family in stats) for (const plat in stats[family]) for (const ver in stats[family][plat]) {
49
+ const v = stripNotes(String(stats[family][plat][ver]).trim());
50
+ if (v && v !== "y") return true;
51
+ }
52
+ return false;
53
+ }
54
+ /** Strip `#N` note markers — `"y #1"` → `"y"`. Notes document edge cases but
55
+ * don't change support semantics, so treat `y #1` as fully supported. */
56
+ function stripNotes(v) {
57
+ return v.split(/\s+/).filter((t) => t && !t.startsWith("#")).join(" ");
58
+ }
59
+ function computeSupport(stats, familyNicenames, allowedClients) {
60
+ let nY = 0, nN = 0, nU = 0, nPartial = 0, total = 0;
61
+ const affectedFamilies = /* @__PURE__ */ new Set();
62
+ for (const family in stats) {
63
+ if (allowedClients !== "all" && !allowedClients.has(family)) continue;
64
+ let familyHasNonY = false;
65
+ for (const plat in stats[family]) {
66
+ const versions = Object.keys(stats[family][plat]).sort();
67
+ const latest = versions[versions.length - 1];
68
+ if (!latest) continue;
69
+ total++;
70
+ const v = stripNotes(String(stats[family][plat][latest]).trim());
71
+ if (v === "y") nY++;
72
+ else if (v === "n") {
73
+ nN++;
74
+ familyHasNonY = true;
75
+ } else if (v === "u") {
76
+ nU++;
77
+ familyHasNonY = true;
78
+ } else {
79
+ nPartial++;
80
+ familyHasNonY = true;
81
+ }
82
+ }
83
+ if (familyHasNonY) affectedFamilies.add(family);
84
+ }
85
+ if (!total) return null;
86
+ if (nY === total) return null;
87
+ const affected = [...affectedFamilies].map((f) => familyNicenames[f] ?? f).sort();
88
+ if (nN === total) return {
89
+ level: "unsupported",
90
+ affected
91
+ };
92
+ if (nU === total) return {
93
+ level: "unknown",
94
+ affected
95
+ };
96
+ return {
97
+ level: "mitigated",
98
+ affected
99
+ };
100
+ }
101
+ /**
102
+ * Slugs we never report. Fundamental HTML (every email uses these) plus
103
+ * CSS noise that's not actionable (comments, !important usage).
104
+ */
105
+ const IGNORED_SLUGS = new Set([
106
+ "html-doctype",
107
+ "html-comments",
108
+ "html-html",
109
+ "html-head",
110
+ "html-body",
111
+ "html-title",
112
+ "html-meta",
113
+ "html-meta-color-scheme",
114
+ "html-style",
115
+ "html-link",
116
+ "html-div",
117
+ "html-span",
118
+ "html-br",
119
+ "html-p",
120
+ "html-a",
121
+ "html-img",
122
+ "html-table",
123
+ "html-tr",
124
+ "html-td",
125
+ "html-th",
126
+ "html-thead",
127
+ "html-tbody",
128
+ "html-tfoot",
129
+ "html-h1-h6",
130
+ "html-lists",
131
+ "html-strong",
132
+ "html-em",
133
+ "html-b",
134
+ "html-i",
135
+ "html-u",
136
+ "html-semantics",
137
+ "html-role",
138
+ "html-hidden",
139
+ "html-width",
140
+ "html-height",
141
+ "css-comments",
142
+ "css-important",
143
+ "css-margin",
144
+ "css-padding",
145
+ "css-border",
146
+ "css-font-size",
147
+ "css-font-weight",
148
+ "css-font",
149
+ "css-font-family",
150
+ "css-line-height",
151
+ "css-letter-spacing",
152
+ "css-text-align",
153
+ "css-text-decoration",
154
+ "css-text-transform",
155
+ "css-color",
156
+ "css-background",
157
+ "css-background-color",
158
+ "css-width",
159
+ "css-height",
160
+ "css-display"
161
+ ]);
162
+ function classify(f, idx) {
163
+ const slug = f.slug;
164
+ if (slug === "html-style") {
165
+ idx.htmlStyleInBody = {
166
+ ...f,
167
+ title: `${f.title} in <body>`
168
+ };
169
+ return;
170
+ }
171
+ if (IGNORED_SLUGS.has(slug)) return;
172
+ if (f.category === "css") return classifyCss(f, slug, idx);
173
+ if (f.category === "html") return classifyHtml(f, slug, idx);
174
+ if (f.category === "image") {
175
+ const ext = slug.slice(6);
176
+ if (ext === "base64") return;
177
+ mpush(idx.imageExt, ext, f);
178
+ }
179
+ }
180
+ function classifyCss(f, slug, idx) {
181
+ switch (slug) {
182
+ case "css-important":
183
+ idx.cssImportant = f;
184
+ return;
185
+ case "css-variables":
186
+ idx.cssVariables = f;
187
+ return;
188
+ case "css-nesting":
189
+ idx.cssNesting = f;
190
+ return;
191
+ case "css-comments":
192
+ idx.cssComments = f;
193
+ return;
194
+ case "css-modern-color":
195
+ idx.cssModernColor = f;
196
+ return;
197
+ case "css-display-flex":
198
+ mpushPropValue(idx, "display", "flex", f);
199
+ return;
200
+ case "css-display-grid":
201
+ mpushPropValue(idx, "display", "grid", f);
202
+ return;
203
+ case "css-display-none":
204
+ mpushPropValue(idx, "display", "none", f);
205
+ return;
206
+ case "css-rgb":
207
+ mpush(idx.cssFunction, "rgb", f);
208
+ return;
209
+ case "css-rgba":
210
+ mpush(idx.cssFunction, "rgba", f);
211
+ return;
212
+ case "css-linear-gradient":
213
+ mpush(idx.cssFunction, "linear-gradient", f);
214
+ return;
215
+ case "css-radial-gradient":
216
+ mpush(idx.cssFunction, "radial-gradient", f);
217
+ return;
218
+ case "css-conic-gradient":
219
+ mpush(idx.cssFunction, "conic-gradient", f);
220
+ return;
221
+ }
222
+ if (slug.startsWith("css-at-media-") && slug !== "css-at-media") {
223
+ mpush(idx.cssMediaFeature, slug.slice(13), f);
224
+ return;
225
+ }
226
+ if (slug.startsWith("css-at-")) {
227
+ mpush(idx.cssAtRule, slug.slice(7), f);
228
+ return;
229
+ }
230
+ if (slug.startsWith("css-pseudo-class-")) {
231
+ mpush(idx.cssPseudoClass, slug.slice(17), f);
232
+ return;
233
+ }
234
+ if (slug.startsWith("css-pseudo-element-")) {
235
+ mpush(idx.cssPseudoElement, slug.slice(19), f);
236
+ return;
237
+ }
238
+ if (slug.startsWith("css-unit-")) {
239
+ const u = slug.slice(9);
240
+ if (u === "calc") {
241
+ mpush(idx.cssFunction, "calc", f);
242
+ return;
243
+ }
244
+ if (u === "initial") return;
245
+ const unit = u === "percent" ? "%" : u;
246
+ mpush(idx.cssUnit, unit, f);
247
+ return;
248
+ }
249
+ if (slug.startsWith("css-function-")) {
250
+ mpush(idx.cssFunction, slug.slice(13), f);
251
+ return;
252
+ }
253
+ if (slug.startsWith("css-selector-")) return;
254
+ mpush(idx.cssProp, slug.slice(4), f);
255
+ }
256
+ function mpushPropValue(idx, prop, value, f) {
257
+ const arr = idx.cssPropValue.get(prop);
258
+ if (arr) arr.push({
259
+ value,
260
+ feature: f
261
+ });
262
+ else idx.cssPropValue.set(prop, [{
263
+ value,
264
+ feature: f
265
+ }]);
266
+ }
267
+ const HTML_ATTR_SLUGS = new Set([
268
+ "align",
269
+ "background",
270
+ "cellpadding",
271
+ "cellspacing",
272
+ "height",
273
+ "width",
274
+ "valign",
275
+ "target",
276
+ "srcset",
277
+ "lang",
278
+ "dir",
279
+ "role",
280
+ "required",
281
+ "hidden"
282
+ ]);
283
+ function classifyHtml(f, slug, idx) {
284
+ switch (slug) {
285
+ case "html-doctype":
286
+ idx.htmlDoctype = f;
287
+ return;
288
+ case "html-comments":
289
+ idx.htmlComments = f;
290
+ return;
291
+ case "html-anchor-links":
292
+ idx.htmlAnchorLinks = f;
293
+ return;
294
+ case "html-mailto-links":
295
+ idx.htmlMailtoLinks = f;
296
+ return;
297
+ case "html-meta-color-scheme":
298
+ idx.htmlMetaColorScheme = f;
299
+ return;
300
+ case "html-semantics":
301
+ idx.htmlSemantics = f;
302
+ return;
303
+ case "html-loading-attribute":
304
+ mpush(idx.htmlAttr, "loading", f);
305
+ return;
306
+ case "html-image-maps":
307
+ mpush(idx.htmlTag, "map", f);
308
+ mpush(idx.htmlTag, "area", f);
309
+ mpush(idx.htmlAttr, "usemap", f);
310
+ return;
311
+ case "html-lists":
312
+ for (const t of [
313
+ "ul",
314
+ "ol",
315
+ "li",
316
+ "dl",
317
+ "dt",
318
+ "dd"
319
+ ]) mpush(idx.htmlTag, t, f);
320
+ return;
321
+ case "html-h1-h6":
322
+ for (const t of [
323
+ "h1",
324
+ "h2",
325
+ "h3",
326
+ "h4",
327
+ "h5",
328
+ "h6"
329
+ ]) mpush(idx.htmlTag, t, f);
330
+ return;
331
+ }
332
+ if (slug.startsWith("html-input-")) {
333
+ mpush(idx.htmlInputType, slug.slice(11), f);
334
+ return;
335
+ }
336
+ if (slug.startsWith("html-button-")) {
337
+ mpush(idx.htmlButtonType, slug.slice(12), f);
338
+ return;
339
+ }
340
+ if (slug.startsWith("html-aria-")) {
341
+ mpush(idx.htmlAttr, slug.slice(5), f);
342
+ return;
343
+ }
344
+ const name = slug.slice(5);
345
+ if (HTML_ATTR_SLUGS.has(name)) {
346
+ mpush(idx.htmlAttr, name, f);
347
+ return;
348
+ }
349
+ mpush(idx.htmlTag, name, f);
350
+ }
351
+ async function initCompatibility() {
352
+ if (indexes) return indexes;
353
+ if (initPromise) return initPromise;
354
+ initPromise = (async () => {
355
+ try {
356
+ const res = await fetch(API_URL);
357
+ if (!res.ok) return null;
358
+ const data = await res.json();
359
+ const idx = emptyIndexes(data.nicenames?.support ?? {}, data.nicenames?.family ?? {});
360
+ for (const item of data.data ?? []) {
361
+ if (item.slug && item.url) idx.bySlug.set(item.slug, {
362
+ title: item.title,
363
+ url: item.url
364
+ });
365
+ if (!hasAnyNonY(item.stats)) continue;
366
+ classify({
367
+ slug: item.slug,
368
+ title: item.title,
369
+ url: item.url,
370
+ category: item.category,
371
+ stats: item.stats
372
+ }, idx);
373
+ }
374
+ indexes = idx;
375
+ return idx;
376
+ } catch {
377
+ return null;
378
+ }
379
+ })();
380
+ return initPromise;
381
+ }
382
+ function collectStreams(filePath, componentMap, visited, out) {
383
+ if (visited.has(filePath)) return;
384
+ visited.add(filePath);
385
+ let source;
5
386
  try {
6
- const html = await new Promise((resolve, reject) => {
7
- let body = "";
8
- req.on("data", (chunk) => {
9
- body += chunk;
387
+ source = readFileSync(filePath, "utf-8");
388
+ } catch {
389
+ return;
390
+ }
391
+ const { template, styles } = parseSfcBlocks(source);
392
+ const classes = /* @__PURE__ */ new Set();
393
+ if (template) extractClasses(template.content, classes);
394
+ out.push({
395
+ path: filePath,
396
+ source,
397
+ template,
398
+ styles,
399
+ classes
400
+ });
401
+ if (template) for (const tag of findComponentTags(template.content)) {
402
+ const cp = componentMap.get(tag.toLowerCase());
403
+ if (cp) collectStreams(cp, componentMap, visited, out);
404
+ }
405
+ }
406
+ function extractClasses(html, out) {
407
+ const parser = new Parser({ onopentag(_tag, attrs) {
408
+ const c = attrs.class;
409
+ if (!c) return;
410
+ for (const t of c.split(/\s+/)) if (t) out.add(t);
411
+ } }, { decodeEntities: true });
412
+ parser.write(html);
413
+ parser.end();
414
+ }
415
+ function parseWithIndices(html) {
416
+ const handler = new DomHandler(void 0, { withStartIndices: true });
417
+ const parser = new Parser(handler);
418
+ parser.write(html);
419
+ parser.end();
420
+ return handler.dom;
421
+ }
422
+ function findStyleNodes(nodes, out = []) {
423
+ for (const n of nodes) {
424
+ const el = n;
425
+ if (el.name === "style") out.push(el);
426
+ if (el.children?.length) findStyleNodes(el.children, out);
427
+ }
428
+ return out;
429
+ }
430
+ /**
431
+ * Parse each file's template, collect every `<style>` node with its source
432
+ * line (via htmlparser2 start indices), then pass the combined DOM through
433
+ * the framework's real Tailwind pipeline. The pipeline resolves imports
434
+ * (@maizzle/tailwindcss), compiles utilities from class attrs, lowers modern
435
+ * CSS via lightningcss, and resolves static calc() — so what we walk matches
436
+ * what ships.
437
+ */
438
+ async function compileViaPipeline(streams, config, rootFile) {
439
+ const all = [];
440
+ const tracked = [];
441
+ for (const s of streams) {
442
+ if (!s.template) continue;
443
+ const templateStart = s.source.indexOf(s.template.content);
444
+ const nodes = parseWithIndices(s.template.content);
445
+ for (const styleNode of findStyleNodes(nodes)) {
446
+ const startIdx = styleNode.startIndex ?? 0;
447
+ const line = offsetToLine(s.source, templateStart + startIdx);
448
+ tracked.push({
449
+ node: styleNode,
450
+ file: s.path,
451
+ line
10
452
  });
11
- req.on("end", () => resolve(body));
12
- req.on("error", reject);
13
- });
14
- if (!html) {
15
- res.setHeader("Content-Type", "application/json");
16
- res.end(JSON.stringify([]));
17
- return;
18
453
  }
19
- const result = caniemail({
20
- clients: [
21
- "apple-mail.*",
22
- "gmail.*",
23
- "outlook.*",
24
- "yahoo.*"
25
- ],
26
- html
454
+ for (const n of nodes) all.push(n);
455
+ }
456
+ if (!tracked.length) return [];
457
+ try {
458
+ await tailwindcss(all, config, rootFile);
459
+ } catch {
460
+ return [];
461
+ }
462
+ return tracked.map((t) => {
463
+ const txt = t.node.children?.find((c) => c.type === "text");
464
+ return txt?.data ? {
465
+ file: t.file,
466
+ css: txt.data,
467
+ line: t.line
468
+ } : null;
469
+ }).filter((x) => x !== null);
470
+ }
471
+ /**
472
+ * Walk CSS AST with detectors. Calls onHit per feature hit.
473
+ * `selector` is the containing rule's selector (undefined if no rule ancestor).
474
+ */
475
+ function walkCss(css, idx, onHit) {
476
+ let root;
477
+ try {
478
+ root = safeParser(css);
479
+ } catch {
480
+ return;
481
+ }
482
+ const containingSelector = (n) => {
483
+ let p = n?.parent;
484
+ while (p && p.type !== "root") {
485
+ if (p.type === "rule") return p.selector;
486
+ p = p.parent;
487
+ }
488
+ };
489
+ if (idx.cssComments) root.walkComments((c) => {
490
+ onHit(idx.cssComments, {
491
+ line: c.source?.start?.line,
492
+ selector: containingSelector(c)
27
493
  });
28
- const urlMap = /* @__PURE__ */ new Map();
29
- const categoryMap = /* @__PURE__ */ new Map();
30
- for (const item of rawData.data) {
31
- urlMap.set(item.title, item.url);
32
- categoryMap.set(item.title, item.category);
494
+ });
495
+ root.walkAtRules((atRule) => {
496
+ const line = atRule.source?.start?.line;
497
+ let sel = containingSelector(atRule);
498
+ if (atRule.name === "media" && !sel) {
499
+ const innerSelectors = [];
500
+ atRule.walkRules((r) => {
501
+ innerSelectors.push(r.selector);
502
+ });
503
+ if (innerSelectors.length) sel = innerSelectors.join(", ");
504
+ }
505
+ if (atRule.name === "media") {
506
+ const specific = [];
507
+ if (idx.cssMediaFeature.size) {
508
+ for (const [feat, fs2] of idx.cssMediaFeature) if (atRule.params.includes(`(${feat}`) || atRule.params.includes(feat)) specific.push(...fs2);
509
+ }
510
+ if (specific.length) for (const f of specific) onHit(f, {
511
+ line,
512
+ selector: sel
513
+ });
514
+ else {
515
+ const fs = idx.cssAtRule.get("media");
516
+ if (fs) for (const f of fs) onHit(f, {
517
+ line,
518
+ selector: sel
519
+ });
520
+ }
521
+ } else {
522
+ const fs = idx.cssAtRule.get(atRule.name);
523
+ if (fs) for (const f of fs) onHit(f, {
524
+ line,
525
+ selector: sel
526
+ });
527
+ }
528
+ });
529
+ root.walkRules((rule) => {
530
+ const line = rule.source?.start?.line;
531
+ const sel = rule.selector;
532
+ if (idx.cssPseudoClass.size) {
533
+ for (const [name, fs] of idx.cssPseudoClass) if (new RegExp(`(^|[^:]):${escapeRe(name)}(\\b|\\()`).test(sel)) for (const f of fs) onHit(f, {
534
+ line,
535
+ selector: sel
536
+ });
33
537
  }
34
- const issues = [];
35
- for (const [client, clientIssues] of result.issues.errors) for (const issue of clientIssues) issues.push({
36
- type: "error",
37
- client,
38
- title: issue.title,
39
- notes: issue.notes,
40
- line: issue.position?.start.line
538
+ if (idx.cssPseudoElement.size) {
539
+ for (const [name, fs] of idx.cssPseudoElement) if (new RegExp(`::${escapeRe(name)}\\b`).test(sel)) for (const f of fs) onHit(f, {
540
+ line,
541
+ selector: sel
542
+ });
543
+ }
544
+ });
545
+ root.walkDecls((decl) => {
546
+ const line = decl.source?.start?.line;
547
+ const sel = containingSelector(decl);
548
+ const prop = decl.prop;
549
+ if (idx.cssImportant && decl.important) onHit(idx.cssImportant, {
550
+ line,
551
+ selector: sel
41
552
  });
42
- for (const [client, clientIssues] of result.issues.warnings) for (const issue of clientIssues) issues.push({
43
- type: "warning",
44
- client,
45
- title: issue.title,
46
- notes: issue.notes,
47
- line: issue.position?.start.line
553
+ if (idx.cssVariables && prop.startsWith("--")) onHit(idx.cssVariables, {
554
+ line,
555
+ selector: sel
48
556
  });
49
- const grouped = /* @__PURE__ */ new Map();
50
- for (const issue of issues) {
51
- const key = `${issue.type}:${issue.title}`;
52
- const existing = grouped.get(key);
53
- const clientName = issue.client.split(".")[0].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
54
- if (existing) {
55
- const existingClient = existing.clients.find((c) => c.name === clientName);
56
- if (existingClient) {
57
- for (const note of issue.notes) if (!existingClient.notes.includes(note)) existingClient.notes.push(note);
58
- } else existing.clients.push({
59
- name: clientName,
60
- notes: [...issue.notes]
61
- });
62
- } else grouped.set(key, {
63
- type: issue.type,
64
- title: issue.title,
65
- category: categoryMap.get(issue.title) || "others",
66
- clients: [{
67
- name: clientName,
68
- notes: [...issue.notes]
69
- }],
70
- url: urlMap.get(issue.title),
71
- line: issue.line
557
+ const fs = idx.cssProp.get(prop);
558
+ if (fs) for (const f of fs) onHit(f, {
559
+ line,
560
+ selector: sel
561
+ });
562
+ const pvs = idx.cssPropValue.get(prop);
563
+ if (pvs) {
564
+ const v = decl.value.trim().toLowerCase();
565
+ for (const pv of pvs) if (v === pv.value) onHit(pv.feature, {
566
+ line,
567
+ selector: sel
72
568
  });
73
569
  }
74
- const categoryOrder = [
75
- "css",
76
- "html",
77
- "image",
78
- "others"
79
- ];
80
- const sortedIssues = [...grouped.values()].sort((a, b) => {
81
- const catA = categoryOrder.indexOf(a.category);
82
- const catB = categoryOrder.indexOf(b.category);
83
- if (catA !== catB) return catA - catB;
84
- if (a.type !== b.type) return a.type === "error" ? -1 : 1;
85
- return a.title.localeCompare(b.title);
570
+ if (idx.cssFunction.size || idx.cssUnit.size || idx.cssVariables || idx.cssModernColor) try {
571
+ valueParser(decl.value).walk((n) => {
572
+ if (n.type === "function") {
573
+ const fname = n.value.toLowerCase();
574
+ const fs2 = idx.cssFunction.get(fname);
575
+ if (fs2) for (const f of fs2) onHit(f, {
576
+ line,
577
+ selector: sel
578
+ });
579
+ if (idx.cssVariables && fname === "var") onHit(idx.cssVariables, {
580
+ line,
581
+ selector: sel
582
+ });
583
+ if (idx.cssModernColor && MODERN_COLOR_FNS.has(fname)) onHit(idx.cssModernColor, {
584
+ line,
585
+ selector: sel
586
+ });
587
+ } else if (n.type === "word") {
588
+ const m = /^-?\d*\.?\d+([a-z%]+)$/i.exec(n.value);
589
+ if (m) {
590
+ const unit = m[1].toLowerCase();
591
+ const fs2 = idx.cssUnit.get(unit);
592
+ if (fs2) for (const f of fs2) onHit(f, {
593
+ line,
594
+ selector: sel
595
+ });
596
+ }
597
+ }
598
+ });
599
+ } catch {}
600
+ });
601
+ }
602
+ const MODERN_COLOR_FNS = new Set([
603
+ "oklch",
604
+ "oklab",
605
+ "lch",
606
+ "lab",
607
+ "color",
608
+ "color-mix",
609
+ "hwb"
610
+ ]);
611
+ function escapeRe(s) {
612
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
613
+ }
614
+ function offsetToLine(source, offset) {
615
+ let line = 1;
616
+ for (let i = 0; i < offset && i < source.length; i++) if (source.charCodeAt(i) === 10) line++;
617
+ return line;
618
+ }
619
+ function walkTemplate(html, idx, fileLineOffset, source, templateStartOffset, onHit) {
620
+ const semanticTags = new Set([
621
+ "article",
622
+ "aside",
623
+ "details",
624
+ "figcaption",
625
+ "figure",
626
+ "footer",
627
+ "header",
628
+ "main",
629
+ "mark",
630
+ "nav",
631
+ "section",
632
+ "time",
633
+ "summary"
634
+ ]);
635
+ const bodyScopeStack = [];
636
+ const parser = new Parser({
637
+ onopentag(tag, attrs) {
638
+ const startIdx = parser.startIndex;
639
+ const line = offsetToLine(source, templateStartOffset + startIdx);
640
+ const tagFs = idx.htmlTag.get(tag);
641
+ if (tagFs) for (const f of tagFs) onHit(f, line);
642
+ if (idx.htmlSemantics && semanticTags.has(tag)) onHit(idx.htmlSemantics, line);
643
+ if (tag === "style" && bodyScopeStack.length > 0 && idx.htmlStyleInBody) onHit(idx.htmlStyleInBody, line);
644
+ if (tag === "body") bodyScopeStack.push(tag);
645
+ else if (tag === "teleport" && /body/i.test(attrs.to ?? "")) bodyScopeStack.push(tag);
646
+ for (const attr in attrs) {
647
+ const attrFs = idx.htmlAttr.get(attr);
648
+ if (attrFs) for (const f of attrFs) onHit(f, line);
649
+ }
650
+ if (tag === "input" && attrs.type) {
651
+ const fs = idx.htmlInputType.get(attrs.type.toLowerCase());
652
+ if (fs) for (const f of fs) onHit(f, line);
653
+ }
654
+ if (tag === "button" && attrs.type) {
655
+ const fs = idx.htmlButtonType.get(attrs.type.toLowerCase());
656
+ if (fs) for (const f of fs) onHit(f, line);
657
+ }
658
+ if (tag === "a" && attrs.href) {
659
+ const h = attrs.href.trim();
660
+ if (idx.htmlMailtoLinks && /^mailto:/i.test(h)) onHit(idx.htmlMailtoLinks, line);
661
+ else if (idx.htmlAnchorLinks && h.startsWith("#")) onHit(idx.htmlAnchorLinks, line);
662
+ }
663
+ if (tag === "meta" && idx.htmlMetaColorScheme && attrs.name?.toLowerCase() === "color-scheme") onHit(idx.htmlMetaColorScheme, line);
664
+ if (idx.imageExt.size && (attrs.src || attrs.srcset)) {
665
+ const urls = [];
666
+ if (attrs.src) urls.push(attrs.src);
667
+ if (attrs.srcset) for (const part of attrs.srcset.split(",")) urls.push(part.trim().split(/\s+/)[0]);
668
+ for (const url of urls) {
669
+ const m = /\.([a-z0-9]+)(?:\?|#|$)/i.exec(url);
670
+ if (!m) continue;
671
+ const fs = idx.imageExt.get(m[1].toLowerCase());
672
+ if (fs) for (const f of fs) onHit(f, line);
673
+ }
674
+ }
675
+ if (attrs.style) scanInlineStyle(attrs.style, idx, line, onHit);
676
+ },
677
+ onclosetag(tag) {
678
+ if (bodyScopeStack[bodyScopeStack.length - 1] === tag) bodyScopeStack.pop();
679
+ },
680
+ onprocessinginstruction(name) {
681
+ if (idx.htmlDoctype && name.toLowerCase() === "!doctype") {
682
+ const startIdx = parser.startIndex;
683
+ onHit(idx.htmlDoctype, offsetToLine(source, templateStartOffset + startIdx));
684
+ }
685
+ },
686
+ oncomment() {
687
+ if (idx.htmlComments) {
688
+ const startIdx = parser.startIndex;
689
+ onHit(idx.htmlComments, offsetToLine(source, templateStartOffset + startIdx));
690
+ }
691
+ }
692
+ }, {
693
+ decodeEntities: false,
694
+ lowerCaseTags: true,
695
+ lowerCaseAttributeNames: true
696
+ });
697
+ parser.write(html);
698
+ parser.end();
699
+ }
700
+ function scanInlineStyle(style, idx, line, onHit) {
701
+ const wrapped = `*{${style}}`;
702
+ try {
703
+ safeParser(wrapped).walkDecls((decl) => {
704
+ if (idx.cssImportant && decl.important) onHit(idx.cssImportant, line);
705
+ const fs = idx.cssProp.get(decl.prop);
706
+ if (fs) for (const f of fs) onHit(f, line);
707
+ if (idx.cssVariables && decl.prop.startsWith("--")) onHit(idx.cssVariables, line);
708
+ const pvs = idx.cssPropValue.get(decl.prop);
709
+ if (pvs) {
710
+ const v = decl.value.trim().toLowerCase();
711
+ for (const pv of pvs) if (v === pv.value) onHit(pv.feature, line);
712
+ }
713
+ if (idx.cssFunction.size || idx.cssUnit.size || idx.cssVariables || idx.cssModernColor) try {
714
+ valueParser(decl.value).walk((n) => {
715
+ if (n.type === "function") {
716
+ const fname = n.value.toLowerCase();
717
+ const fs2 = idx.cssFunction.get(fname);
718
+ if (fs2) for (const f of fs2) onHit(f, line);
719
+ if (idx.cssVariables && fname === "var") onHit(idx.cssVariables, line);
720
+ if (idx.cssModernColor && MODERN_COLOR_FNS.has(fname)) onHit(idx.cssModernColor, line);
721
+ } else if (n.type === "word") {
722
+ const m = /^-?\d*\.?\d+([a-z%]+)$/i.exec(n.value);
723
+ if (m) {
724
+ const fs2 = idx.cssUnit.get(m[1].toLowerCase());
725
+ if (fs2) for (const f of fs2) onHit(f, line);
726
+ }
727
+ }
728
+ });
729
+ } catch {}
730
+ });
731
+ } catch {}
732
+ }
733
+ function labelFor(idx, level) {
734
+ const n = idx.nicenames;
735
+ if (level === "unsupported") return n.unsupported ?? "Not supported";
736
+ if (level === "mitigated") return n.mitigated ?? "Partially supported";
737
+ return n.unknown ?? "Support unknown";
738
+ }
739
+ async function scan(rootFile, config, componentDirs, allowedClients) {
740
+ const idx = await initCompatibility();
741
+ if (!idx) return [];
742
+ const componentMap = await buildComponentMap(config.root ?? process.cwd(), componentDirs);
743
+ const streams = [];
744
+ collectStreams(rootFile, componentMap, /* @__PURE__ */ new Set(), streams);
745
+ const issues = [];
746
+ const seen = /* @__PURE__ */ new Set();
747
+ const resolvedCache = /* @__PURE__ */ new Map();
748
+ const resolveSupport = (f) => {
749
+ let cached = resolvedCache.get(f.slug);
750
+ if (cached === void 0) {
751
+ cached = computeSupport(f.stats, idx.familyNicenames, allowedClients);
752
+ resolvedCache.set(f.slug, cached);
753
+ }
754
+ return cached;
755
+ };
756
+ const add = (f, file, line) => {
757
+ const key = `${f.slug}|${file}|${line ?? 0}`;
758
+ if (seen.has(key)) return;
759
+ const support = resolveSupport(f);
760
+ if (!support) return;
761
+ seen.add(key);
762
+ issues.push({
763
+ kind: "compat",
764
+ slug: f.slug,
765
+ title: f.title,
766
+ url: f.url,
767
+ category: f.category,
768
+ supportLevel: support.level,
769
+ supportLabel: labelFor(idx, support.level),
770
+ affectedClients: support.affected,
771
+ line,
772
+ file
86
773
  });
774
+ };
775
+ const compiledBlocks = await compileViaPipeline(streams, config, rootFile);
776
+ for (const block of compiledBlocks) walkCss(block.css, idx, (feature, node) => {
777
+ const locations = classLocations(node.selector, streams);
778
+ if (!locations.length) {
779
+ add(feature, block.file, block.line);
780
+ return;
781
+ }
782
+ if (feature.slug.startsWith("css-at-media")) add(feature, locations[0].file, locations[0].line);
783
+ else for (const { file, line } of locations) add(feature, file, line);
784
+ });
785
+ for (const s of streams) {
786
+ if (!s.template) continue;
787
+ walkTemplate(s.template.content, idx, s.template.offset, s.source, s.source.indexOf(s.template.content), (feature, line) => add(feature, s.path, line));
788
+ }
789
+ return issues;
790
+ }
791
+ /**
792
+ * Return every (file, line) where any class from the selector appears in a
793
+ * template. Scans every stream so a shared utility class used in multiple
794
+ * components surfaces once per occurrence.
795
+ */
796
+ function classLocations(selector, streams) {
797
+ if (!selector) return [];
798
+ const classNames = extractSelectorClasses(selector);
799
+ if (!classNames.length) return [];
800
+ const out = [];
801
+ const seen = /* @__PURE__ */ new Set();
802
+ for (const cn of classNames) for (const s of streams) {
803
+ if (!s.classes.has(cn) || !s.template) continue;
804
+ const tpl = s.template.content;
805
+ const tplStart = s.source.indexOf(tpl);
806
+ let pos = 0;
807
+ while (true) {
808
+ const i = tpl.indexOf(cn, pos);
809
+ if (i < 0) break;
810
+ pos = i + cn.length;
811
+ const before = i > 0 ? tpl[i - 1] : " ";
812
+ const after = i + cn.length < tpl.length ? tpl[i + cn.length] : " ";
813
+ if (!isClassBoundary(before) || !isClassBoundary(after)) continue;
814
+ const line = offsetToLine(s.source, tplStart + i);
815
+ const key = `${s.path}|${line}`;
816
+ if (seen.has(key)) continue;
817
+ seen.add(key);
818
+ out.push({
819
+ file: s.path,
820
+ line
821
+ });
822
+ }
823
+ }
824
+ return out;
825
+ }
826
+ function isClassBoundary(c) {
827
+ return c === " " || c === " " || c === "\n" || c === "\r" || c === "\"" || c === "'";
828
+ }
829
+ function extractSelectorClasses(selector) {
830
+ const out = [];
831
+ const re = /\.((?:\\.|[\w-])+)/g;
832
+ let m;
833
+ while ((m = re.exec(selector)) !== null) out.push(m[1].replace(/\\(.)/g, "$1"));
834
+ return out;
835
+ }
836
+ const CATEGORY_ORDER = [
837
+ "css",
838
+ "html",
839
+ "image",
840
+ "others"
841
+ ];
842
+ const LEVEL_ORDER = {
843
+ error: 0,
844
+ unsupported: 1,
845
+ warning: 2,
846
+ mitigated: 3,
847
+ unknown: 4
848
+ };
849
+ function orderKey(i) {
850
+ if (i.kind === "lint") return LEVEL_ORDER[i.severity] ?? 99;
851
+ return LEVEL_ORDER[i.supportLevel] ?? 99;
852
+ }
853
+ function resolveChecksConfig(config) {
854
+ const raw = config.server?.checks;
855
+ if (raw === false) return null;
856
+ return {
857
+ clients: raw?.clients === "all" ? "all" : Array.isArray(raw?.clients) && raw.clients.length ? new Set(raw.clients) : DEFAULT_CLIENTS,
858
+ level: raw?.level ?? null
859
+ };
860
+ }
861
+ function passesLevelFilter(issue, level) {
862
+ if (!level) return true;
863
+ if (level === "lint") return issue.kind === "lint";
864
+ if (issue.kind === "lint") return level === "error" ? issue.severity === "error" : issue.severity === "warning";
865
+ return level === "error" ? issue.supportLevel === "unsupported" : issue.supportLevel === "mitigated" || issue.supportLevel === "unknown";
866
+ }
867
+ async function serveCompatibility(url, res, config, componentDirs) {
868
+ const filePath = url.replace("/__maizzle/compatibility/", "").replace(/\?.*$/, "");
869
+ const checksCfg = resolveChecksConfig(config);
870
+ try {
87
871
  res.setHeader("Content-Type", "application/json");
88
- res.end(JSON.stringify(sortedIssues));
872
+ if (!checksCfg) {
873
+ res.end(JSON.stringify([]));
874
+ return;
875
+ }
876
+ const absolutePath = resolve(filePath);
877
+ const [compatIssues, lintIssues] = await Promise.all([scan(absolutePath, config, componentDirs, checksCfg.clients), scanLint(absolutePath, config, componentDirs)]);
878
+ const idx = await initCompatibility();
879
+ const lintAsIssues = lintIssues.map((li) => {
880
+ const info = li.slug ? idx?.bySlug.get(li.slug) : void 0;
881
+ return {
882
+ kind: "lint",
883
+ slug: li.slug,
884
+ title: li.title,
885
+ url: info?.url,
886
+ category: li.category,
887
+ severity: li.type,
888
+ message: li.message,
889
+ line: li.line,
890
+ file: li.file
891
+ };
892
+ });
893
+ let issues = [...compatIssues, ...lintAsIssues];
894
+ if (checksCfg.level) issues = issues.filter((i) => passesLevelFilter(i, checksCfg.level));
895
+ issues.sort((a, b) => {
896
+ const c = CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category);
897
+ if (c) return c;
898
+ const l = orderKey(a) - orderKey(b);
899
+ if (l) return l;
900
+ return (a.slug ?? a.title).localeCompare(b.slug ?? b.title);
901
+ });
902
+ res.end(JSON.stringify(issues));
89
903
  } catch (error) {
90
904
  res.statusCode = 500;
91
905
  res.end(JSON.stringify({ error: error.message }));
@@ -93,5 +907,5 @@ async function serveCompatibility(req, res) {
93
907
  }
94
908
 
95
909
  //#endregion
96
- export { serveCompatibility };
910
+ export { initCompatibility, serveCompatibility };
97
911
  //# sourceMappingURL=compatibility.mjs.map