trykeyline 0.0.0-stage → 0.1.0
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/.claude-plugin/marketplace.json +18 -0
- package/LICENSE +37 -0
- package/README.md +117 -2
- package/bin/keyline.mjs +280 -0
- package/package.json +38 -4
- package/plugins/keyline/.claude-plugin/plugin.json +14 -0
- package/plugins/keyline/skills/keyline/SKILL.md +57 -0
- package/plugins/keyline/skills/keyline/commands/audit.md +25 -0
- package/plugins/keyline/skills/keyline/commands/color.md +7 -0
- package/plugins/keyline/skills/keyline/commands/critique.md +43 -0
- package/plugins/keyline/skills/keyline/commands/depth.md +7 -0
- package/plugins/keyline/skills/keyline/commands/init.md +15 -0
- package/plugins/keyline/skills/keyline/commands/layout.md +7 -0
- package/plugins/keyline/skills/keyline/commands/motion.md +7 -0
- package/plugins/keyline/skills/keyline/commands/patterns.md +7 -0
- package/plugins/keyline/skills/keyline/commands/polish.md +19 -0
- package/plugins/keyline/skills/keyline/commands/refine.md +29 -0
- package/plugins/keyline/skills/keyline/commands/sync.md +17 -0
- package/plugins/keyline/skills/keyline/commands/type.md +7 -0
- package/plugins/keyline/skills/keyline/domains/color/deriving-a-ramp.md +144 -0
- package/plugins/keyline/skills/keyline/domains/color/guide.md +243 -0
- package/plugins/keyline/skills/keyline/domains/color/status-and-exceptions.md +174 -0
- package/plugins/keyline/skills/keyline/domains/color/theming-mechanics.md +121 -0
- package/plugins/keyline/skills/keyline/domains/depth/elevation-recipes.md +191 -0
- package/plugins/keyline/skills/keyline/domains/depth/fallbacks-and-performance.md +132 -0
- package/plugins/keyline/skills/keyline/domains/depth/guide.md +255 -0
- package/plugins/keyline/skills/keyline/domains/depth/translucent-materials.md +159 -0
- package/plugins/keyline/skills/keyline/domains/layout/forks-gating-and-disclosures.md +201 -0
- package/plugins/keyline/skills/keyline/domains/layout/guide.md +284 -0
- package/plugins/keyline/skills/keyline/domains/layout/measure-and-rhythm.md +193 -0
- package/plugins/keyline/skills/keyline/domains/layout/radius-lines-and-layers.md +122 -0
- package/plugins/keyline/skills/keyline/domains/motion/guards.md +162 -0
- package/plugins/keyline/skills/keyline/domains/motion/guide.md +234 -0
- package/plugins/keyline/skills/keyline/domains/motion/ladder-and-curves.md +139 -0
- package/plugins/keyline/skills/keyline/domains/motion/patterns.md +196 -0
- package/plugins/keyline/skills/keyline/domains/motion/performance.md +121 -0
- package/plugins/keyline/skills/keyline/domains/patterns/actions-and-forms.md +204 -0
- package/plugins/keyline/skills/keyline/domains/patterns/agents-and-live-states.md +111 -0
- package/plugins/keyline/skills/keyline/domains/patterns/collections-and-dashboards.md +131 -0
- package/plugins/keyline/skills/keyline/domains/patterns/copy.md +123 -0
- package/plugins/keyline/skills/keyline/domains/patterns/guide.md +216 -0
- package/plugins/keyline/skills/keyline/domains/patterns/iteration.md +78 -0
- package/plugins/keyline/skills/keyline/domains/type/display-tier.md +108 -0
- package/plugins/keyline/skills/keyline/domains/type/guide.md +251 -0
- package/plugins/keyline/skills/keyline/domains/type/ladders.md +169 -0
- package/plugins/keyline/skills/keyline/domains/type/numerals-and-loading.md +107 -0
- package/plugins/keyline/skills/keyline/domains/type/prose-and-emphasis.md +125 -0
- package/plugins/keyline/skills/keyline/domains/type/type-audit.md +90 -0
- package/plugins/keyline/skills/keyline/glass/color/assets/color-tokens.css +319 -0
- package/plugins/keyline/skills/keyline/glass/color/assets/theme.ts +119 -0
- package/plugins/keyline/skills/keyline/glass/color/guide.md +222 -0
- package/plugins/keyline/skills/keyline/glass/color/intent-ramp.md +136 -0
- package/plugins/keyline/skills/keyline/glass/color/theming.md +112 -0
- package/plugins/keyline/skills/keyline/glass/depth/assets/GlassPanel.tsx +171 -0
- package/plugins/keyline/skills/keyline/glass/depth/assets/glass.css +322 -0
- package/plugins/keyline/skills/keyline/glass/depth/blur-and-tint.md +50 -0
- package/plugins/keyline/skills/keyline/glass/depth/depth-cues.md +87 -0
- package/plugins/keyline/skills/keyline/glass/depth/guide.md +216 -0
- package/plugins/keyline/skills/keyline/glass/depth/performance-and-fallbacks.md +37 -0
- package/plugins/keyline/skills/keyline/glass/layout/adaptive-presentations.md +231 -0
- package/plugins/keyline/skills/keyline/glass/layout/assets/SectionNav.tsx +244 -0
- package/plugins/keyline/skills/keyline/glass/layout/assets/layout-tokens.css +199 -0
- package/plugins/keyline/skills/keyline/glass/layout/guide.md +258 -0
- package/plugins/keyline/skills/keyline/glass/layout/measure-and-rhythm.md +134 -0
- package/plugins/keyline/skills/keyline/glass/layout/radius-and-elevation.md +102 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/DropMenu.tsx +106 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/motion-presets.ts +210 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/motion-tokens.css +176 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/useReveal.ts +65 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/useScrollTick.ts +183 -0
- package/plugins/keyline/skills/keyline/glass/motion/easing-and-duration.md +108 -0
- package/plugins/keyline/skills/keyline/glass/motion/guide.md +286 -0
- package/plugins/keyline/skills/keyline/glass/motion/reduced-motion.md +81 -0
- package/plugins/keyline/skills/keyline/glass/motion/scroll-performance.md +58 -0
- package/plugins/keyline/skills/keyline/glass/motion/shared-element.md +161 -0
- package/plugins/keyline/skills/keyline/glass/motion/sound.md +192 -0
- package/plugins/keyline/skills/keyline/glass/motion/springs.md +51 -0
- package/plugins/keyline/skills/keyline/glass/type/assets/prose.tsx +268 -0
- package/plugins/keyline/skills/keyline/glass/type/assets/type-tokens.css +414 -0
- package/plugins/keyline/skills/keyline/glass/type/guide.md +222 -0
- package/plugins/keyline/skills/keyline/glass/type/long-form-prose.md +151 -0
- package/plugins/keyline/skills/keyline/glass/type/scale.md +69 -0
- package/plugins/keyline/skills/keyline/glass/type/weight-and-tracking.md +61 -0
- package/plugins/keyline/skills/keyline/scripts/check/css.mjs +112 -0
- package/plugins/keyline/skills/keyline/scripts/check/design.mjs +67 -0
- package/plugins/keyline/skills/keyline/scripts/check/files.mjs +44 -0
- package/plugins/keyline/skills/keyline/scripts/check/markup.mjs +149 -0
- package/plugins/keyline/skills/keyline/scripts/check/rules.mjs +584 -0
- package/plugins/keyline/skills/keyline/scripts/check.mjs +294 -0
- package/plugins/keyline/skills/keyline/scripts/contrast.mjs +153 -0
- package/plugins/keyline/skills/keyline/scripts/scan.mjs +340 -0
- package/plugins/keyline/skills/keyline/system/design-md.md +136 -0
- package/plugins/keyline/skills/keyline/system/detection.md +53 -0
- package/plugins/keyline/skills/keyline/system/guide.md +101 -0
- package/plugins/keyline/skills/keyline/system/presets.md +58 -0
- package/plugins/keyline/skills/keyline/system/roles.md +150 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the detector takes from DESIGN.md: the preset (which turns some rules on
|
|
3
|
+
* or off), the motion ladder and the z-index ladder. Everything else in the
|
|
4
|
+
* file is for the agent; the detector only needs what changes a verdict.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
const PRESETS = ["editorial", "product", "expressive", "glass"];
|
|
10
|
+
|
|
11
|
+
function section(md, heading) {
|
|
12
|
+
const m = md.match(new RegExp(`^##\\s+${heading}\\b[^\\n]*\\n([\\s\\S]*?)(?=^##\\s|(?![\\s\\S]))`, "mi"));
|
|
13
|
+
return m ? m[1] : "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readDesign(root) {
|
|
17
|
+
const name = existsSync(root) ? readdirSync(root).find((f) => f.toLowerCase() === "design.md") : null;
|
|
18
|
+
if (!name) return { found: false, preset: null, durations: null, roles: new Map(), zLadder: null };
|
|
19
|
+
const md = readFileSync(join(root, name), "utf8");
|
|
20
|
+
const headingLine = (h) => {
|
|
21
|
+
const i = md.search(new RegExp(`^##\\s+${h}\\b`, "mi"));
|
|
22
|
+
return i < 0 ? 1 : md.slice(0, i).split("\n").length;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const profile = section(md, "Profile");
|
|
26
|
+
const presetRow = profile.split("\n").find((l) => /^\|\s*Preset\s*\|/i.test(l)) ?? "";
|
|
27
|
+
const preset = PRESETS.find((p) => new RegExp(`\`${p}\`|\\b${p}\\b`).test(presetRow.split("|").slice(2).join("|"))) ?? null;
|
|
28
|
+
|
|
29
|
+
// Durations: every ms / s value in the Motion section's table rows. Rows on a
|
|
30
|
+
// canonical duration.* role (before any "Named exemptions" line) are also kept by
|
|
31
|
+
// role, so the detector can check they grow with the size of the change.
|
|
32
|
+
const motion = section(md, "Motion");
|
|
33
|
+
const durations = new Set();
|
|
34
|
+
const roles = new Map();
|
|
35
|
+
let inExemptions = false;
|
|
36
|
+
for (const line of motion.split("\n")) {
|
|
37
|
+
if (!line.startsWith("|")) {
|
|
38
|
+
if (/exemption/i.test(line)) inExemptions = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const m of line.matchAll(/(?<![\w.])(\d+(?:\.\d+)?)(ms|s)\b/g)) {
|
|
42
|
+
const ms = m[2] === "s" ? Math.round(parseFloat(m[1]) * 1000) : Math.round(parseFloat(m[1]));
|
|
43
|
+
durations.add(ms);
|
|
44
|
+
const role = line.split("|")[1]?.match(/duration\.(\w+)/)?.[1];
|
|
45
|
+
if (!inExemptions && role && !roles.has(role)) roles.set(role, ms);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// z-index: rows whose role is z.*, or a project role whose name is a z utility
|
|
50
|
+
// or mentions one — their values are the ladder.
|
|
51
|
+
const layout = section(md, "Layout");
|
|
52
|
+
const zLadder = new Set();
|
|
53
|
+
for (const row of layout.split("\n").filter((l) => /^\|\s*`?z\./.test(l) || (l.startsWith("|") && /`-?z-\[?-?\d|`z\./.test(l)))) {
|
|
54
|
+
const cells = row.split("|").slice(1, -1).map((c) => c.trim());
|
|
55
|
+
for (const c of cells.slice(1)) for (const m of c.matchAll(/(?<![\w.#-])(-?\d+)(?![\w.%])/g)) zLadder.add(Number(m[1]));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
found: true,
|
|
60
|
+
file: name,
|
|
61
|
+
lines: { motion: headingLine("Motion"), layout: headingLine("Layout") },
|
|
62
|
+
preset,
|
|
63
|
+
durations: durations.size ? durations : null,
|
|
64
|
+
roles,
|
|
65
|
+
zLadder: zLadder.size ? zLadder : null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which files the detector reads. The same skips as scan.mjs, so the two never
|
|
3
|
+
* disagree about what counts as the project's interface.
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { extname, join, relative, sep } from "node:path";
|
|
7
|
+
|
|
8
|
+
export const SKIP_DIRS = new Set([
|
|
9
|
+
"node_modules", ".git", ".next", ".nuxt", ".output", ".svelte-kit", ".turbo",
|
|
10
|
+
".vercel", ".cache", "dist", "build", "out", "coverage", "vendor",
|
|
11
|
+
"storybook-static", ".claude", ".agents", "public", "static",
|
|
12
|
+
]);
|
|
13
|
+
export const STYLE = new Set([".css", ".scss", ".sass", ".less", ".pcss"]);
|
|
14
|
+
export const MARKUP = new Set([".tsx", ".jsx", ".ts", ".js", ".mjs", ".vue", ".svelte", ".astro", ".html", ".mdx"]);
|
|
15
|
+
const SKIP_FILES = /\.(min|d|test|spec|stories|config)\.[a-z]+$|^(eslint|postcss|tailwind|next|vite|vitest|jest|playwright)\.config\./;
|
|
16
|
+
|
|
17
|
+
/** Every style and markup file under `paths`, relative to `root`, minus ignores. */
|
|
18
|
+
export function collect(root, paths, ignores) {
|
|
19
|
+
const out = [];
|
|
20
|
+
const ignored = (rel) => ignores.some((ig) => ig && (rel === ig || rel.startsWith(ig.endsWith("/") ? ig : ig + "/")));
|
|
21
|
+
const visit = (abs) => {
|
|
22
|
+
const rel = relative(root, abs).split(sep).join("/");
|
|
23
|
+
if (rel && ignored(rel)) return;
|
|
24
|
+
let st;
|
|
25
|
+
try {
|
|
26
|
+
st = statSync(abs);
|
|
27
|
+
} catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (st.isDirectory()) {
|
|
31
|
+
const name = abs.split(sep).pop();
|
|
32
|
+
if (rel && (SKIP_DIRS.has(name) || (name.startsWith(".") && name !== ".storybook"))) return;
|
|
33
|
+
for (const e of readdirSync(abs)) visit(join(abs, e));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const ext = extname(abs);
|
|
37
|
+
if (!(STYLE.has(ext) || MARKUP.has(ext))) return;
|
|
38
|
+
if (SKIP_FILES.test(abs.split(sep).pop())) return;
|
|
39
|
+
if (st.size > 400_000) return; // generated bundles, not source
|
|
40
|
+
out.push({ abs, rel: rel || abs.split(sep).pop(), ext, kind: STYLE.has(ext) ? "style" : "markup" });
|
|
41
|
+
};
|
|
42
|
+
for (const p of paths) visit(p);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the detector needs from markup and script files, found without a parser:
|
|
3
|
+
* class lists (attributes, class helpers, string constants), inline styles,
|
|
4
|
+
* <style> blocks, CSS-in-JS templates, and the text of buttons and links.
|
|
5
|
+
*
|
|
6
|
+
* Regexes rather than an AST so it reads any framework's files the same way
|
|
7
|
+
* and never fails on syntax it doesn't know. The cost is precision: a class
|
|
8
|
+
* built by concatenation is invisible, and a string that only looks like a
|
|
9
|
+
* class list can be read as one. Rules are written to tolerate both.
|
|
10
|
+
*/
|
|
11
|
+
import { blankComments, lineIndex, parseCss, parseInlineStyle } from "./css.mjs";
|
|
12
|
+
|
|
13
|
+
const CLASS_ATTR = /\b(?:class|className|class:list)\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*"([^"]*)"\s*\}|\{\s*'([^']*)'\s*\}|\{\s*`([^`]*)`\s*\})/g;
|
|
14
|
+
const CLASS_CALL = /\b(?:cn|clsx|cx|classNames|classnames|twMerge|twJoin|cva|tv)\s*\(/g;
|
|
15
|
+
const EXPR_STRING = /(?:[=:(,\[?]|\breturn|&&|\|\|)\s*(["'`])((?:(?!\1)[^\\\n]|\\.)*)\1/g;
|
|
16
|
+
const CLASSY = /^[\w\s:\-\[\]\/.#%(),!&>*@=+'"~]*$/;
|
|
17
|
+
const TAILWINDISH = /(?:^|\s)!?(?:[\w-]+:)*-?(?:p[xytrbl]?|m[xytrbl]?|text|bg|border|rounded|flex|grid|gap|w|h|min-w|min-h|max-w|max-h|font|leading|tracking|shadow|z|opacity|transition|duration|ease|animate|backdrop|ring|outline|uppercase|italic|hidden|block|inline|absolute|relative|fixed|sticky|top|left|right|bottom|inset|items|justify|self|space|overflow|cursor|select|pointer|translate|scale|rotate|blur|from|to|via|fill|stroke)(?:-[\w.\[\]#%/():,-]+)?(?=\s|$)/;
|
|
18
|
+
|
|
19
|
+
/** Split a class token into its variants and its utility, respecting [arbitrary:values]. */
|
|
20
|
+
export function splitClass(token) {
|
|
21
|
+
let t = token.replace(/^!/, "").replace(/!$/, "");
|
|
22
|
+
let depth = 0;
|
|
23
|
+
let cut = -1;
|
|
24
|
+
for (let i = 0; i < t.length; i++) {
|
|
25
|
+
if (t[i] === "[" || t[i] === "(") depth++;
|
|
26
|
+
else if (t[i] === "]" || t[i] === ")") depth--;
|
|
27
|
+
else if (t[i] === ":" && depth === 0) cut = i;
|
|
28
|
+
}
|
|
29
|
+
return { variants: cut < 0 ? [] : t.slice(0, cut).split(":"), base: cut < 0 ? t : t.slice(cut + 1), raw: token };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function balanced(text, open, openCh, closeCh) {
|
|
33
|
+
let depth = 0;
|
|
34
|
+
let quote = null;
|
|
35
|
+
for (let i = open; i < text.length; i++) {
|
|
36
|
+
const c = text[i];
|
|
37
|
+
if (quote) {
|
|
38
|
+
if (c === "\\") i++;
|
|
39
|
+
else if (c === quote) quote = null;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (c === '"' || c === "'" || c === "`") quote = c;
|
|
43
|
+
else if (c === openCh) depth++;
|
|
44
|
+
else if (c === closeCh && --depth === 0) return i;
|
|
45
|
+
}
|
|
46
|
+
return -1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Static parts of a template literal body; ${…} becomes spaces of equal length. */
|
|
50
|
+
const staticParts = (s) => s.replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
|
|
51
|
+
|
|
52
|
+
export function readMarkup(source, ext) {
|
|
53
|
+
// Blank JS comments only in script-like files; in HTML `//` is just text.
|
|
54
|
+
const isHtmlLike = ext === ".html" || ext === ".vue" || ext === ".svelte" || ext === ".astro";
|
|
55
|
+
let text = blankComments(source);
|
|
56
|
+
if (!isHtmlLike) text = text.replace(/(^|[^:"'`\\])\/\/[^\n]*/g, (m, pre) => pre + " ".repeat(m.length - pre.length));
|
|
57
|
+
text = text.replace(/<!--[\s\S]*?-->/g, (m) => m.replace(/[^\n]/g, " "));
|
|
58
|
+
const at = lineIndex(text);
|
|
59
|
+
|
|
60
|
+
const classLists = []; // { value, offset, line, tokens: [{...splitClass, line}] }
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
const addList = (value, offset, origin) => {
|
|
63
|
+
if (seen.has(offset)) return;
|
|
64
|
+
seen.add(offset);
|
|
65
|
+
const tokens = [];
|
|
66
|
+
const re = /\S+/g;
|
|
67
|
+
let m;
|
|
68
|
+
while ((m = re.exec(value))) tokens.push({ ...splitClass(m[0]), line: at(offset + m.index) });
|
|
69
|
+
if (tokens.length) classLists.push({ value, offset, line: at(offset), tokens, origin, tag: origin === "attribute" ? tagAround(offset, value.length) : "" });
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The opening tag a class attribute sits in, for rules that need its element or attributes. */
|
|
73
|
+
const tagAround = (offset, length) => {
|
|
74
|
+
let lt = offset;
|
|
75
|
+
while (lt > 0 && !(text[lt] === "<" && /[A-Za-z]/.test(text[lt + 1] ?? ""))) lt--;
|
|
76
|
+
const rest = text.slice(offset + length, offset + length + 600);
|
|
77
|
+
const gt = rest.search(/(?<![=-])>/);
|
|
78
|
+
return text.slice(lt, offset + length + (gt < 0 ? 0 : gt + 1));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
for (const m of text.matchAll(CLASS_ATTR)) {
|
|
82
|
+
const idx = m.slice(1).findIndex((g) => g !== undefined);
|
|
83
|
+
const value = m[idx + 1];
|
|
84
|
+
const offset = m.index + m[0].indexOf(value, m[0].indexOf("="));
|
|
85
|
+
addList(idx === 4 ? staticParts(value) : value, offset, "attribute");
|
|
86
|
+
}
|
|
87
|
+
for (const m of text.matchAll(CLASS_CALL)) {
|
|
88
|
+
const open = m.index + m[0].length - 1;
|
|
89
|
+
const close = balanced(text, open, "(", ")");
|
|
90
|
+
if (close < 0) continue;
|
|
91
|
+
const args = text.slice(open + 1, close);
|
|
92
|
+
for (const s of args.matchAll(/(["'`])((?:(?!\1)[^\\]|\\.)*)\1/g)) {
|
|
93
|
+
addList(s[1] === "`" ? staticParts(s[2]) : s[2], open + 1 + s.index + 1, "helper");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!isHtmlLike || ext !== ".html") {
|
|
97
|
+
for (const m of text.matchAll(EXPR_STRING)) {
|
|
98
|
+
const value = m[2];
|
|
99
|
+
if (value.length < 3 || !CLASSY.test(value) || !TAILWINDISH.test(value)) continue;
|
|
100
|
+
if (/^[a-z]+(-[a-z]+)*$/.test(value) && !/\s/.test(value) && !/^(uppercase|italic|transition-all|h-screen|min-h-screen|outline-none)$/.test(value)) continue;
|
|
101
|
+
addList(m[1] === "`" ? staticParts(value) : value, m.index + m[0].lastIndexOf(m[1] + value) + 1, "string");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Styles: <style> blocks, style="…" attributes, style={{…}} objects, CSS-in-JS templates.
|
|
106
|
+
const css = [];
|
|
107
|
+
for (const m of text.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)) {
|
|
108
|
+
const body = source.slice(m.index + m[0].indexOf(">") + 1, m.index + m[0].lastIndexOf("</"));
|
|
109
|
+
css.push(parseCss(body, at(m.index + m[0].indexOf(">") + 1)));
|
|
110
|
+
}
|
|
111
|
+
const inline = [];
|
|
112
|
+
for (const m of text.matchAll(/\bstyle\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
|
|
113
|
+
const body = m[1] ?? m[2];
|
|
114
|
+
if (body.includes(":")) inline.push(...parseInlineStyle(body, at(m.index)));
|
|
115
|
+
}
|
|
116
|
+
for (const m of text.matchAll(/\bstyle\s*=\s*\{\{/g)) {
|
|
117
|
+
const open = m.index + m[0].length - 1;
|
|
118
|
+
const close = balanced(text, open, "{", "}");
|
|
119
|
+
if (close < 0) continue;
|
|
120
|
+
const body = text.slice(open + 1, close);
|
|
121
|
+
for (const p of body.matchAll(/([A-Za-z]+)\s*:\s*("[^"]*"|'[^']*'|`[^`]*`|-?[\d.]+)/g)) {
|
|
122
|
+
const prop = p[1].replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
|
|
123
|
+
const value = p[2].replace(/^["'`]|["'`]$/g, "");
|
|
124
|
+
inline.push({ prop, value, line: at(open + 1 + p.index), selector: "[style]", atRules: [], block: null, inline: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const m of text.matchAll(/\b(?:styled(?:\.\w+|\([^)]*\))|css|createGlobalStyle|keyframes|injectGlobal)\s*`([^`]*)`/g)) {
|
|
128
|
+
const bodyOffset = m.index + m[0].indexOf("`") + 1;
|
|
129
|
+
css.push(parseCss(`x{${staticParts(m[1])}}`, at(bodyOffset)));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Visible text of buttons and links, for the copy rules.
|
|
133
|
+
const labels = [];
|
|
134
|
+
for (const m of text.matchAll(/<(button|Button|a|Link)\b[^>]*>\s*([^<>{}]+?)\s*<\/\1>/g)) {
|
|
135
|
+
labels.push({ element: m[1], text: m[2].replace(/\s+/g, " ").trim(), line: at(m.index + m[0].indexOf(m[2])) });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Navigation blocks: how many links each writes out, and whether it groups them.
|
|
139
|
+
// Links rendered from an array (`items.map`) are invisible here, so the count is a floor.
|
|
140
|
+
const navs = [];
|
|
141
|
+
for (const m of text.matchAll(/<nav\b[^>]*>([\s\S]*?)<\/nav>/g)) {
|
|
142
|
+
const body = m[1];
|
|
143
|
+
const links = (body.match(/<(?:a|Link|NavLink)\b/g) ?? []).length;
|
|
144
|
+
const grouped = /<details\b|<h[2-6]\b|role\s*=\s*["']group|<section\b|<fieldset\b/.test(body) || (body.match(/<ul\b/g) ?? []).length > 1;
|
|
145
|
+
navs.push({ line: at(m.index), links, grouped });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { text, classLists, css, inline, labels, navs };
|
|
149
|
+
}
|