@takazudo/zudo-doc 5.1.0 → 5.2.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/CHANGELOG.md +38 -0
- package/bin/gen-z-index.mjs +767 -73
- package/dist/auto-logo/icon.d.ts +12 -0
- package/dist/auto-logo/icon.js +23 -0
- package/dist/auto-logo/index.d.ts +1 -0
- package/dist/auto-logo/index.js +2 -0
- package/dist/auto-logo/render-shape.d.ts +9 -0
- package/dist/auto-logo/render-shape.js +14 -0
- package/dist/auto-logo/shapes-square.d.ts +7 -0
- package/dist/auto-logo/shapes-square.js +14 -0
- package/dist/auto-logo/shapes.d.ts +35 -9
- package/dist/auto-logo/shapes.js +49 -36
- package/dist/auto-logo/standalone.js +2 -8
- package/dist/design-token-panel-bootstrap.d.ts +22 -11
- package/dist/design-token-panel-bootstrap.js +190 -58
- package/dist/safelist.css +1 -1
- package/package.json +9 -9
package/bin/gen-z-index.mjs
CHANGED
|
@@ -6,59 +6,294 @@
|
|
|
6
6
|
// src/config/z-index-tokens.ts.
|
|
7
7
|
//
|
|
8
8
|
// Reads from the project root (process.cwd()). Conventional paths:
|
|
9
|
-
// - Tokens: src/config/z-index-tokens.ts
|
|
10
|
-
// - CSS: src/styles/global.css
|
|
9
|
+
// - Tokens: src/config/z-index-tokens.ts (override with --tokens <path>)
|
|
10
|
+
// - CSS: src/styles/global.css (override with --css <path>)
|
|
11
11
|
//
|
|
12
12
|
// Usage (after pnpm install, via scripts in package.json):
|
|
13
|
-
// gen-z-index
|
|
14
|
-
// gen-z-index --check
|
|
13
|
+
// gen-z-index # rewrite the @theme block (conventional paths)
|
|
14
|
+
// gen-z-index --check # verify committed block is up to date (exit 1 on drift)
|
|
15
|
+
// gen-z-index --tokens <path> --css <path> # use non-conventional source/destination paths
|
|
16
|
+
// gen-z-index --no-theme-wrapper # emit bare --z-index-<name> declarations, no @theme wrapper
|
|
17
|
+
// gen-z-index --md-table <path> # also generate/verify a Z_INDEX_TABLE region in a
|
|
18
|
+
// # markdown/MDX file (opt-in, no conventional default path)
|
|
15
19
|
//
|
|
16
|
-
// MUST be run with the project root as cwd
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
+
// MUST be run with the project root as cwd — it resolves --tokens/--css/
|
|
21
|
+
// --md-table (or their conventional defaults) against process.cwd(), NOT
|
|
22
|
+
// against this file's location. A consuming project's own package.json
|
|
23
|
+
// scripts (e.g. gen:z-index / check:z-index) are responsible for invoking it
|
|
24
|
+
// from the project root. This generator is opt-in per project — a project
|
|
25
|
+
// only needs it when overriding one of the default tiers shipped
|
|
26
|
+
// unconditionally by @takazudo/zudo-doc/theme.css — and it is not wired into
|
|
27
|
+
// this repo's own b4push or CI; that integration was retired in
|
|
28
|
+
// zudolab/zudo-doc#2661.
|
|
20
29
|
//
|
|
21
30
|
// The block is a Tailwind v4 `@theme { --z-index-<name>: <value>; }` for every
|
|
22
31
|
// tier, so Tailwind generates `z-<name>` utilities and raw CSS can reference
|
|
23
|
-
// `z-index: var(--z-index-<name>)`.
|
|
32
|
+
// `z-index: var(--z-index-<name>)`. `--no-theme-wrapper` drops the `@theme`
|
|
33
|
+
// wrapper and emits bare `--z-index-<name>: <value>;` declarations instead,
|
|
34
|
+
// for projects that want to compose the block into their own `@theme` block.
|
|
24
35
|
//
|
|
25
|
-
//
|
|
36
|
+
// `--md-table <path>` additionally generates/verifies a second, independent
|
|
37
|
+
// `GENERATED:Z_INDEX_TABLE` region — a `| Token | Kind | Role |` table, one
|
|
38
|
+
// row per tier — inside a markdown/MDX file at <path>. It uses the MDX-safe
|
|
39
|
+
// brace-comment marker form `{/* GENERATED:Z_INDEX_TABLE_BEGIN/END */}`
|
|
40
|
+
// rather than an HTML `<!-- -->` comment, because an HTML comment is a parse
|
|
41
|
+
// error in MDX. `--check` verifies BOTH regions when --md-table is given;
|
|
42
|
+
// drift in either exits 1. Like the CSS block, the md-table region must be
|
|
43
|
+
// seeded once by hand (just the marker pair) before the generator can find
|
|
44
|
+
// it to replace. On this surface ONLY, marker lines inside a CommonMark
|
|
45
|
+
// fenced code block are ignored, so a doc page may reproduce the marker
|
|
46
|
+
// verbatim while explaining the generator (see `scanMarkerLines`).
|
|
26
47
|
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
48
|
+
// Pure Node (fs only — NO npm deps, no minimist). Idempotent: running twice
|
|
49
|
+
// produces no diff.
|
|
50
|
+
//
|
|
51
|
+
// MAINTENANCE: edit src/config/z-index-tokens.ts (the source of truth), then
|
|
52
|
+
// run `pnpm gen:z-index` (or the equivalent --tokens/--css/--md-table
|
|
53
|
+
// invocation) and commit the regenerated CSS (and md table, if used). Never
|
|
54
|
+
// hand-edit either block between its BEGIN/END markers.
|
|
30
55
|
|
|
31
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
56
|
+
import { readFileSync, writeFileSync, realpathSync } from "node:fs";
|
|
32
57
|
import { resolve } from "node:path";
|
|
33
|
-
|
|
34
|
-
// Project root is always the current working directory — this is a project bin,
|
|
35
|
-
// not a monorepo script.
|
|
36
|
-
const ROOT = process.cwd();
|
|
37
|
-
|
|
38
|
-
const TOKENS_PATH = resolve(ROOT, "src/config/z-index-tokens.ts");
|
|
39
|
-
const CSS_PATH = resolve(ROOT, "src/styles/global.css");
|
|
58
|
+
import { fileURLToPath } from "node:url";
|
|
40
59
|
|
|
41
60
|
const BEGIN_MARKER = "GENERATED:Z_INDEX_BEGIN";
|
|
42
61
|
const END_MARKER = "GENERATED:Z_INDEX_END";
|
|
43
62
|
|
|
63
|
+
// MDX-safe markers for the optional --md-table region. An HTML `<!-- -->`
|
|
64
|
+
// comment is a parse error in MDX, which is the entire reason this region
|
|
65
|
+
// uses the brace-comment form instead — both markers are the literal,
|
|
66
|
+
// complete text that must appear (on their own line) in the seeded file.
|
|
67
|
+
const MD_TABLE_BEGIN_MARKER = "{/* GENERATED:Z_INDEX_TABLE_BEGIN */}";
|
|
68
|
+
const MD_TABLE_END_MARKER = "{/* GENERATED:Z_INDEX_TABLE_END */}";
|
|
69
|
+
|
|
70
|
+
// Conventional paths, relative to the project root (process.cwd()). Used as
|
|
71
|
+
// the default --tokens/--css values AND as the literal text shown in the
|
|
72
|
+
// generated header / log messages when no override is given — this is what
|
|
73
|
+
// keeps the default invocation's output byte-identical to pre-flag output.
|
|
74
|
+
// --md-table has NO conventional default: it's an opt-in region and there's
|
|
75
|
+
// no natural project-wide path to assume, so it stays undefined unless given.
|
|
76
|
+
const DEFAULT_TOKENS_PATH = "src/config/z-index-tokens.ts";
|
|
77
|
+
const DEFAULT_CSS_PATH = "src/styles/global.css";
|
|
78
|
+
|
|
79
|
+
const TIER_NAME_RE = /^[a-z0-9-]+$/;
|
|
80
|
+
|
|
81
|
+
const FLAG_USAGE =
|
|
82
|
+
"Supported flags: --check, --tokens <path>, --css <path>, --md-table <path>, --no-theme-wrapper.";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Hand-rolled argv parser (no minimist — this bin stays dependency-free).
|
|
86
|
+
* Supports `--flag value` and `--flag=value` for value flags. Unknown flags,
|
|
87
|
+
* repeated flags, a missing value for a value flag, and a value attached to a
|
|
88
|
+
* boolean flag (`--no-theme-wrapper=x`) are all hard errors — this bin does
|
|
89
|
+
* not silently ignore or guess at malformed invocations.
|
|
90
|
+
*
|
|
91
|
+
* Returns `{ check, tokens, css, mdTable, noThemeWrapper }`; `tokens`/`css`/
|
|
92
|
+
* `mdTable` are `undefined` when not passed (callers apply the conventional
|
|
93
|
+
* defaults — `mdTable` has none, so it stays undefined and the md-table
|
|
94
|
+
* region is skipped entirely).
|
|
95
|
+
*
|
|
96
|
+
* Exported for unit testing.
|
|
97
|
+
*/
|
|
98
|
+
export function parseArgs(argv) {
|
|
99
|
+
const result = {
|
|
100
|
+
check: false,
|
|
101
|
+
tokens: undefined,
|
|
102
|
+
css: undefined,
|
|
103
|
+
mdTable: undefined,
|
|
104
|
+
noThemeWrapper: false,
|
|
105
|
+
};
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < argv.length; i++) {
|
|
109
|
+
const raw = argv[i];
|
|
110
|
+
const eqIdx = raw.startsWith("--") ? raw.indexOf("=") : -1;
|
|
111
|
+
const flag = eqIdx === -1 ? raw : raw.slice(0, eqIdx);
|
|
112
|
+
const inlineValue = eqIdx === -1 ? undefined : raw.slice(eqIdx + 1);
|
|
113
|
+
|
|
114
|
+
const isBoolean = flag === "--check" || flag === "--no-theme-wrapper";
|
|
115
|
+
const isValueFlag = flag === "--tokens" || flag === "--css" || flag === "--md-table";
|
|
116
|
+
|
|
117
|
+
if (!isBoolean && !isValueFlag) {
|
|
118
|
+
throw new Error(`Unknown flag "${raw}". ${FLAG_USAGE}`);
|
|
119
|
+
}
|
|
120
|
+
if (seen.has(flag)) {
|
|
121
|
+
throw new Error(`Flag "${flag}" was passed more than once.`);
|
|
122
|
+
}
|
|
123
|
+
seen.add(flag);
|
|
124
|
+
|
|
125
|
+
if (isBoolean) {
|
|
126
|
+
if (inlineValue !== undefined) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Flag "${flag}" does not take a value (got "${raw}"); it is a boolean switch.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (flag === "--check") result.check = true;
|
|
132
|
+
if (flag === "--no-theme-wrapper") result.noThemeWrapper = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Value flag (--tokens / --css / --md-table): accept an inline `=value`,
|
|
137
|
+
// otherwise consume the next argv token. A missing/empty value or a next
|
|
138
|
+
// token that looks like another flag is a hard error, not a silent
|
|
139
|
+
// default.
|
|
140
|
+
let value = inlineValue;
|
|
141
|
+
if (value === undefined) {
|
|
142
|
+
const next = argv[i + 1];
|
|
143
|
+
if (next === undefined || next.startsWith("--")) {
|
|
144
|
+
throw new Error(`Flag "${flag}" requires a value (e.g. "${flag} <path>").`);
|
|
145
|
+
}
|
|
146
|
+
value = next;
|
|
147
|
+
i++;
|
|
148
|
+
}
|
|
149
|
+
if (value === "") {
|
|
150
|
+
throw new Error(`Flag "${flag}" requires a non-empty value.`);
|
|
151
|
+
}
|
|
152
|
+
if (flag === "--tokens") result.tokens = value;
|
|
153
|
+
if (flag === "--css") result.css = value;
|
|
154
|
+
if (flag === "--md-table") result.mdTable = value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Structural validations that apply regardless of `kind`/`purpose` usage,
|
|
162
|
+
* plus the opt-in per-kind value-uniqueness check. Called by `parseTiers`
|
|
163
|
+
* after regex-extraction, and exported separately so tests can exercise it
|
|
164
|
+
* directly against hand-built tier arrays.
|
|
165
|
+
*
|
|
166
|
+
* - Rejects an empty tiers array (preserves the pre-refactor behavior).
|
|
167
|
+
* - Rejects a tier name that doesn't match `^[a-z0-9-]+$`.
|
|
168
|
+
* - Rejects duplicate tier names.
|
|
169
|
+
* - If at least one tier carries `kind`, rejects two "global" tiers sharing a
|
|
170
|
+
* value. "local" tiers MAY share a value with each other; kind-less tiers
|
|
171
|
+
* are exempt from this check entirely.
|
|
172
|
+
*
|
|
173
|
+
* Exported for unit testing.
|
|
174
|
+
*/
|
|
175
|
+
export function validateTiers(tiers, tokensPath = DEFAULT_TOKENS_PATH) {
|
|
176
|
+
if (tiers.length === 0) {
|
|
177
|
+
throw new Error(`Z_INDEX_TIERS in ${tokensPath} parsed to an empty list`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const seenNames = new Set();
|
|
181
|
+
for (const tier of tiers) {
|
|
182
|
+
if (!TIER_NAME_RE.test(tier.name)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Invalid tier name "${tier.name}" in ${tokensPath}: tier names must match ` +
|
|
185
|
+
`${TIER_NAME_RE} (lowercase letters, digits, hyphens only).`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (seenNames.has(tier.name)) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Duplicate tier name "${tier.name}" in ${tokensPath}. Tier names must be unique.`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
seenNames.add(tier.name);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const anyKinded = tiers.some((tier) => tier.kind !== undefined);
|
|
197
|
+
if (anyKinded) {
|
|
198
|
+
const seenGlobalValues = new Map();
|
|
199
|
+
for (const tier of tiers) {
|
|
200
|
+
if (tier.kind !== "global") continue;
|
|
201
|
+
const existing = seenGlobalValues.get(tier.value);
|
|
202
|
+
if (existing !== undefined) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Duplicate z-index value ${tier.value} shared by "global" tiers "${existing}" ` +
|
|
205
|
+
`and "${tier.name}" in ${tokensPath}. Global tiers must have unique values ` +
|
|
206
|
+
`(local tiers may share a value).`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
seenGlobalValues.set(tier.value, tier.name);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return tiers;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Scans the raw Z_INDEX_TIERS array body for `purpose:` fields and rejects
|
|
218
|
+
* any whose quoted value contains a brace or a backslash (which also covers
|
|
219
|
+
* escaped quotes) BEFORE the per-object splitter runs. The per-object
|
|
220
|
+
* splitter below uses a non-greedy `{...}` match to isolate each tier object,
|
|
221
|
+
* which only works when no field value contains a brace — a purpose string
|
|
222
|
+
* with a stray `}` would silently truncate the object split and corrupt
|
|
223
|
+
* parsing instead of failing loudly. Only a flat, plain double-quoted string
|
|
224
|
+
* is supported; a newline between `purpose:` and the opening quote is fine
|
|
225
|
+
* (the field-key regexes below all use `\s*`, which matches newlines).
|
|
226
|
+
*/
|
|
227
|
+
function assertSupportedPurposeGrammar(body, tokensPath) {
|
|
228
|
+
const purposeKeyRe = /purpose:\s*/g;
|
|
229
|
+
let m;
|
|
230
|
+
while ((m = purposeKeyRe.exec(body)) !== null) {
|
|
231
|
+
const afterKey = m.index + m[0].length;
|
|
232
|
+
if (body[afterKey] !== '"') {
|
|
233
|
+
// Not immediately followed by a quote — not a value this scan can
|
|
234
|
+
// confirm is a real field; let the per-object parser's generic
|
|
235
|
+
// "malformed tier object" error handle it if it really is one.
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
let i = afterKey + 1;
|
|
239
|
+
let closed = false;
|
|
240
|
+
for (; i < body.length; i++) {
|
|
241
|
+
const ch = body[i];
|
|
242
|
+
if (ch === "{" || ch === "}" || ch === "\\") {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Unsupported purpose string grammar in ${tokensPath}: purpose values may not ` +
|
|
245
|
+
`contain braces, backslashes, or escaped quotes — only flat, plain double-quoted ` +
|
|
246
|
+
`strings are supported (the object parser cannot safely handle anything else). ` +
|
|
247
|
+
`Offending text near: ${JSON.stringify(body.slice(afterKey, Math.min(afterKey + 40, body.length)))}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (ch === '"') {
|
|
251
|
+
closed = true;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!closed) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Unterminated purpose string in ${tokensPath} (no closing double quote found).`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
purposeKeyRe.lastIndex = i + 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
44
264
|
/**
|
|
45
265
|
* Parse the Z_INDEX_TIERS array out of z-index-tokens.ts WITHOUT importing it
|
|
46
266
|
* (this bin is a dependency-free .mjs and cannot resolve TypeScript). Reads
|
|
47
|
-
* each `{ name: "...", value: <n>, ... }`
|
|
48
|
-
*
|
|
267
|
+
* each `{ name: "...", value: <n>, kind?: "global"|"local", purpose?: "..." }`
|
|
268
|
+
* object literal. Throws on a malformed source, an unsupported purpose-string
|
|
269
|
+
* grammar, or an unknown `kind` value so drift between the parser and the
|
|
270
|
+
* file surfaces loudly. Delegates the structural invariants (non-empty,
|
|
271
|
+
* name shape, duplicate names, per-kind value uniqueness) to `validateTiers`.
|
|
272
|
+
*
|
|
273
|
+
* `tokensPath` is used purely for error-message context — pass the same path
|
|
274
|
+
* string (conventional default or an explicit --tokens value) that was used
|
|
275
|
+
* to read `src`, so the thrown error tells the reader exactly which file to
|
|
276
|
+
* fix.
|
|
277
|
+
*
|
|
278
|
+
* Exported for unit testing.
|
|
49
279
|
*/
|
|
50
|
-
function parseTiers(src) {
|
|
280
|
+
export function parseTiers(src, tokensPath = DEFAULT_TOKENS_PATH) {
|
|
51
281
|
const arrayMatch = src.match(
|
|
52
282
|
/export const Z_INDEX_TIERS[^=]*=\s*\[([\s\S]*?)\];/,
|
|
53
283
|
);
|
|
54
284
|
if (!arrayMatch) {
|
|
55
285
|
throw new Error(
|
|
56
|
-
`Could not locate "export const Z_INDEX_TIERS = [ ... ]" in ${
|
|
286
|
+
`Could not locate "export const Z_INDEX_TIERS = [ ... ]" in ${tokensPath}`,
|
|
57
287
|
);
|
|
58
288
|
}
|
|
59
289
|
const body = arrayMatch[1];
|
|
290
|
+
|
|
291
|
+
assertSupportedPurposeGrammar(body, tokensPath);
|
|
292
|
+
|
|
60
293
|
const tiers = [];
|
|
61
|
-
// Each tier is a `{ ... }` object literal; iterate top-level braces.
|
|
294
|
+
// Each tier is a `{ ... }` object literal; iterate top-level braces. Safe
|
|
295
|
+
// because assertSupportedPurposeGrammar above already ruled out braces
|
|
296
|
+
// inside any purpose string.
|
|
62
297
|
const objectRe = /\{([\s\S]*?)\}/g;
|
|
63
298
|
let m;
|
|
64
299
|
while ((m = objectRe.exec(body)) !== null) {
|
|
@@ -67,98 +302,557 @@ function parseTiers(src) {
|
|
|
67
302
|
const valueMatch = obj.match(/value:\s*(-?\d+)/);
|
|
68
303
|
if (!nameMatch || !valueMatch) {
|
|
69
304
|
throw new Error(
|
|
70
|
-
`Malformed tier object in Z_INDEX_TIERS (missing name/value): ${obj.trim()}`,
|
|
305
|
+
`Malformed tier object in Z_INDEX_TIERS (missing name/value) in ${tokensPath}: ${obj.trim()}`,
|
|
71
306
|
);
|
|
72
307
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
308
|
+
|
|
309
|
+
const tier = { name: nameMatch[1], value: Number(valueMatch[1]) };
|
|
310
|
+
|
|
311
|
+
const kindMatch = obj.match(/kind:\s*"([^"]*)"/);
|
|
312
|
+
if (kindMatch) {
|
|
313
|
+
const kindValue = kindMatch[1];
|
|
314
|
+
if (kindValue !== "global" && kindValue !== "local") {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`Invalid kind "${kindValue}" for tier "${tier.name}" in ${tokensPath} ` +
|
|
317
|
+
`(expected "global" or "local").`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
tier.kind = kindValue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const purposeMatch = obj.match(/purpose:\s*"([^"]*)"/);
|
|
324
|
+
if (purposeMatch) {
|
|
325
|
+
tier.purpose = purposeMatch[1];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
tiers.push(tier);
|
|
77
329
|
}
|
|
78
|
-
|
|
330
|
+
|
|
331
|
+
return validateTiers(tiers, tokensPath);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Wraps a path in double quotes so it copy-pastes safely into a shell even
|
|
336
|
+
* when it contains a space (an unquoted `--tokens a b.ts` would otherwise
|
|
337
|
+
* split into two argv tokens on rerun).
|
|
338
|
+
*/
|
|
339
|
+
function shellQuote(value) {
|
|
340
|
+
return `"${value}"`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Builds the rerun-guidance command shown in the generated header comment and
|
|
345
|
+
* in main()'s drift/error messages. Returns the exact legacy "pnpm
|
|
346
|
+
* gen:z-index" text when every option is at its conventional default (the
|
|
347
|
+
* byte-identity case); otherwise builds an explicit, directly-runnable
|
|
348
|
+
* `pnpm exec gen-z-index ...` invocation carrying only the non-default flags
|
|
349
|
+
* (quoted), so the guidance always reruns with the SAME effective options
|
|
350
|
+
* that produced the current output. `pnpm exec` — rather than a bare
|
|
351
|
+
* `gen-z-index` — is used because a project customizing --tokens/--css has
|
|
352
|
+
* no guarantee it also defined a package.json script alias for that exact
|
|
353
|
+
* invocation; `pnpm exec` resolves the package-local bin regardless.
|
|
354
|
+
*
|
|
355
|
+
* `mdTablePath` has no conventional default (undefined = --md-table wasn't
|
|
356
|
+
* given), so its mere presence always disqualifies the byte-identity case.
|
|
357
|
+
*/
|
|
358
|
+
function buildRerunCommand({ tokensPath, cssPath, themeWrapper, mdTablePath }) {
|
|
359
|
+
const isDefault =
|
|
360
|
+
tokensPath === DEFAULT_TOKENS_PATH &&
|
|
361
|
+
cssPath === DEFAULT_CSS_PATH &&
|
|
362
|
+
themeWrapper === true &&
|
|
363
|
+
mdTablePath === undefined;
|
|
364
|
+
if (isDefault) return "pnpm gen:z-index";
|
|
365
|
+
|
|
366
|
+
const parts = ["pnpm exec gen-z-index"];
|
|
367
|
+
if (tokensPath !== DEFAULT_TOKENS_PATH) parts.push(`--tokens ${shellQuote(tokensPath)}`);
|
|
368
|
+
if (cssPath !== DEFAULT_CSS_PATH) parts.push(`--css ${shellQuote(cssPath)}`);
|
|
369
|
+
if (!themeWrapper) parts.push("--no-theme-wrapper");
|
|
370
|
+
if (mdTablePath !== undefined) parts.push(`--md-table ${shellQuote(mdTablePath)}`);
|
|
371
|
+
return parts.join(" ");
|
|
79
372
|
}
|
|
80
373
|
|
|
81
374
|
/**
|
|
82
375
|
* Build the full generated block (markers included). Two leading spaces of
|
|
83
376
|
* indentation match the surrounding `@theme` style in global.css.
|
|
377
|
+
*
|
|
378
|
+
* `options.tokensPath`/`options.cssPath` feed the "Source of truth:" and
|
|
379
|
+
* rerun-guidance lines in the header comment — pass the SAME path strings
|
|
380
|
+
* (conventional defaults or explicit --tokens/--css values) used to read/
|
|
381
|
+
* write the actual files, so a reader of the committed CSS can tell exactly
|
|
382
|
+
* where the block came from and how to regenerate it. With every option at
|
|
383
|
+
* its default, the header is byte-identical to the pre-flag generator.
|
|
384
|
+
*
|
|
385
|
+
* `options.themeWrapper` (default `true`) wraps the declarations in an
|
|
386
|
+
* `@theme { ... }` block; `false` (the `--no-theme-wrapper` CLI flag) emits
|
|
387
|
+
* bare `--z-index-<name>: <value>;` declarations at 2-space indent instead,
|
|
388
|
+
* for projects composing the block into their own `@theme` block.
|
|
389
|
+
*
|
|
390
|
+
* Deliberately does NOT accept `mdTablePath`: the CSS block's own content
|
|
391
|
+
* must depend only on the CSS-region flags (--tokens/--css/--no-theme-
|
|
392
|
+
* wrapper), never on whether --md-table happens to be passed in a given
|
|
393
|
+
* invocation — otherwise adding/dropping --md-table would flip the embedded
|
|
394
|
+
* rerun-guidance text and manufacture CSS-region drift unrelated to any
|
|
395
|
+
* actual tier change. `main()`'s own drift/error messages use
|
|
396
|
+
* `buildRerunCommand` directly (with `mdTablePath`) for that guidance instead.
|
|
397
|
+
*
|
|
398
|
+
* Exported for unit testing.
|
|
84
399
|
*/
|
|
85
|
-
function buildBlock(tiers) {
|
|
400
|
+
export function buildBlock(tiers, options = {}) {
|
|
401
|
+
const {
|
|
402
|
+
tokensPath = DEFAULT_TOKENS_PATH,
|
|
403
|
+
cssPath = DEFAULT_CSS_PATH,
|
|
404
|
+
themeWrapper = true,
|
|
405
|
+
} = options;
|
|
406
|
+
|
|
86
407
|
const lines = [];
|
|
87
408
|
lines.push(` /* ${BEGIN_MARKER}`);
|
|
88
409
|
lines.push(
|
|
89
|
-
` * GENERATED:Z_INDEX — do not hand-edit; run
|
|
90
|
-
);
|
|
91
|
-
lines.push(
|
|
92
|
-
` * Source of truth: src/config/z-index-tokens.ts. Tailwind v4 reads the`,
|
|
410
|
+
` * GENERATED:Z_INDEX — do not hand-edit; run ${buildRerunCommand({ tokensPath, cssPath, themeWrapper })}.`,
|
|
93
411
|
);
|
|
412
|
+
lines.push(` * Source of truth: ${tokensPath}. Tailwind v4 reads the`);
|
|
94
413
|
lines.push(
|
|
95
414
|
` * --z-index-<name> theme key and generates a z-<name> utility. */`,
|
|
96
415
|
);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
416
|
+
if (themeWrapper) {
|
|
417
|
+
lines.push(` @theme {`);
|
|
418
|
+
for (const tier of tiers) {
|
|
419
|
+
lines.push(` --z-index-${tier.name}: ${tier.value};`);
|
|
420
|
+
}
|
|
421
|
+
lines.push(` }`);
|
|
422
|
+
} else {
|
|
423
|
+
for (const tier of tiers) {
|
|
424
|
+
lines.push(` --z-index-${tier.name}: ${tier.value};`);
|
|
425
|
+
}
|
|
100
426
|
}
|
|
101
|
-
lines.push(` }`);
|
|
102
427
|
lines.push(` /* ${END_MARKER} */`);
|
|
103
428
|
return lines.join("\n");
|
|
104
429
|
}
|
|
105
430
|
|
|
431
|
+
// CommonMark fenced-code-block rules (https://spec.commonmark.org/0.31.2/
|
|
432
|
+
// #fenced-code-blocks), transcribed only as far as `scanMarkerLines` needs
|
|
433
|
+
// them. Each of these is a rule a `trim()` + `startsWith` approximation gets
|
|
434
|
+
// wrong, which is why they are spelled out here rather than eyeballed:
|
|
435
|
+
//
|
|
436
|
+
// - An opening fence carries AT MOST three spaces of indentation. Four
|
|
437
|
+
// spaces makes the line indented code, not a fence.
|
|
438
|
+
// - A fence may open on a list-item line (`- ```mdx`), because the list
|
|
439
|
+
// marker is a container prefix rather than content. Missing this one is
|
|
440
|
+
// not a harmless false negative: the item's closing fence — indented to
|
|
441
|
+
// the item's content column, so carrying NO list marker — would then be
|
|
442
|
+
// read as a fresh opener and swallow every marker below it, silently
|
|
443
|
+
// splicing the generated block into the quoted example. Loud failure is
|
|
444
|
+
// acceptable here; silent corruption is not.
|
|
445
|
+
// - A backtick opening fence's info string may not itself contain a
|
|
446
|
+
// backtick (a tilde fence's info string may contain anything).
|
|
447
|
+
// - A closing fence repeats the SAME character, at least as many times as
|
|
448
|
+
// the opener, followed by whitespace only — so an info-string line such
|
|
449
|
+
// as ```js does not close an already-open fence. It carries no list
|
|
450
|
+
// marker of its own, which is why only OPEN_FENCE_RE accepts one.
|
|
451
|
+
// - A closing fence's indentation allowance is measured from the OPENING
|
|
452
|
+
// fence, not from column zero: inside a container the closer sits at the
|
|
453
|
+
// container's content column and may carry up to three further spaces.
|
|
454
|
+
// So the bound is `opener indent + 3`, where the opener's indent is its
|
|
455
|
+
// leading spaces PLUS any list-marker prefix width. A flat three-space
|
|
456
|
+
// cap is wrong for any marker four or more characters wide (`10) `, or
|
|
457
|
+
// `1.` followed by two spaces): the item's real closer would be rejected,
|
|
458
|
+
// the fence would never close, and every marker below it would stay
|
|
459
|
+
// ineligible. Since the opener's indent is never negative, a closer at
|
|
460
|
+
// 0-3 spaces still closes a fence opened at ANY indent.
|
|
461
|
+
// - Indentation is measured in COLUMNS, with a tab advancing to the next
|
|
462
|
+
// four-column tab stop. The opener accepts a tab as list-marker padding,
|
|
463
|
+
// and `"1.\t".length` (3) undercounts that item's real content column
|
|
464
|
+
// (4) — which would then reject a legal closer sitting at column + 3.
|
|
465
|
+
const OPEN_FENCE_RE = /^( {0,3}(?:(?:[-*+]|\d{1,9}[.)])[ \t]+)?)(`{3,}|~{3,})(.*)$/;
|
|
466
|
+
const CLOSE_FENCE_RE = /^( *)(`{3,}|~{3,})[ \t]*$/;
|
|
467
|
+
|
|
106
468
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
469
|
+
* Column width of an opening fence's prefix (leading spaces plus any
|
|
470
|
+
* list-marker prefix), expanding tabs to CommonMark's four-column tab stops —
|
|
471
|
+
* see the tab-stop rule in the block comment above. `CLOSE_FENCE_RE` matches
|
|
472
|
+
* spaces only, so the closer side needs no equivalent (and accepting a
|
|
473
|
+
* tab-indented closer would widen behaviour the pre-existing `^ {0,3}` cap
|
|
474
|
+
* never had).
|
|
109
475
|
*/
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
476
|
+
function fenceIndentColumns(prefix) {
|
|
477
|
+
let column = 0;
|
|
478
|
+
for (const ch of prefix) {
|
|
479
|
+
column = ch === "\t" ? column + 4 - (column % 4) : column + 1;
|
|
480
|
+
}
|
|
481
|
+
return column;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Walks `source` line by line and returns the character offsets (into
|
|
486
|
+
* `source`, in source order) of every LINE-ANCHORED occurrence of `marker` —
|
|
487
|
+
* a line where removing the marker text leaves behind only whitespace and
|
|
488
|
+
* comment/brace delimiter characters. This is the single scanner both
|
|
489
|
+
* `replaceBlock`'s validation (duplicate/missing/inverted) and its splice
|
|
490
|
+
* positions read from, so counting and locating can never diverge: a marker
|
|
491
|
+
* mentioned in prose (e.g. "see GENERATED:Z_INDEX_BEGIN for details") is
|
|
492
|
+
* invisible to this scanner, whether it appears before, between, or after the
|
|
493
|
+
* real structural markers.
|
|
494
|
+
*
|
|
495
|
+
* Predicate: for a line containing `marker`, `line.replace(marker, "")` must
|
|
496
|
+
* match `/^[\s{}/*]*$/`. This covers both real marker forms — the CSS
|
|
497
|
+
* mid-comment-line pair (e.g. ` /* GENERATED:Z_INDEX_BEGIN`) and the MDX
|
|
498
|
+
* whole-line brace-comment pair (e.g. `{/* GENERATED:Z_INDEX_TABLE_BEGIN`,
|
|
499
|
+
* closed by a trailing brace-comment on the same line) — while rejecting a
|
|
500
|
+
* line where the marker is only part of a prose sentence.
|
|
501
|
+
*
|
|
502
|
+
* `options.excludeFencedCode` (default `false`) additionally tracks
|
|
503
|
+
* CommonMark fenced-code state (see the two regexes above) and makes every
|
|
504
|
+
* line from an opening fence through its closing fence — inclusive, and
|
|
505
|
+
* through end-of-source for an unterminated fence — ineligible. It is
|
|
506
|
+
* OFF by default and passed only from the `--md-table` surface: fences are a
|
|
507
|
+
* markdown construct, and the CSS surface must keep byte-identical behaviour.
|
|
508
|
+
*
|
|
509
|
+
* ACCEPTED LIMITATION: a marker line inside a **four-space-indented** code
|
|
510
|
+
* block is still counted. Indented code is not a fence, and detecting it
|
|
511
|
+
* needs far more markdown awareness (list-item continuation indentation,
|
|
512
|
+
* paragraph interruption rules) than a dependency-free bin should carry —
|
|
513
|
+
* zudolab/zudo-doc#3290 names fenced code, not indented code. The failure
|
|
514
|
+
* mode stays loud (a "duplicate markers" error), never silent corruption.
|
|
515
|
+
*
|
|
516
|
+
* Exported for unit testing.
|
|
517
|
+
*/
|
|
518
|
+
export function scanMarkerLines(source, marker, options = {}) {
|
|
519
|
+
const { excludeFencedCode = false } = options;
|
|
520
|
+
const RESIDUE_RE = /^[\s{}/*]*$/;
|
|
521
|
+
const offsets = [];
|
|
522
|
+
// `{ char, length, indent }` while inside an open fenced region, else null.
|
|
523
|
+
// `indent` is the opening fence's content COLUMN (leading spaces plus any
|
|
524
|
+
// list-marker prefix, tabs expanded), which bounds how far its closer may be
|
|
525
|
+
// indented.
|
|
526
|
+
let fence = null;
|
|
527
|
+
let lineStart = 0;
|
|
528
|
+
while (lineStart <= source.length) {
|
|
529
|
+
const newlineIdx = source.indexOf("\n", lineStart);
|
|
530
|
+
const lineEnd = newlineIdx === -1 ? source.length : newlineIdx;
|
|
531
|
+
const line = source.slice(lineStart, lineEnd);
|
|
532
|
+
|
|
533
|
+
let eligible = true;
|
|
534
|
+
if (excludeFencedCode) {
|
|
535
|
+
// Classify against the line minus a CRLF carriage return, so the
|
|
536
|
+
// "whitespace only after a closing fence" rule still holds on a CRLF
|
|
537
|
+
// source. Offsets are unaffected — `line` itself is untouched.
|
|
538
|
+
const text = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
539
|
+
if (fence !== null) {
|
|
540
|
+
const close = CLOSE_FENCE_RE.exec(text);
|
|
541
|
+
if (
|
|
542
|
+
close &&
|
|
543
|
+
close[2][0] === fence.char &&
|
|
544
|
+
close[2].length >= fence.length &&
|
|
545
|
+
close[1].length <= fence.indent + 3
|
|
546
|
+
) {
|
|
547
|
+
fence = null;
|
|
548
|
+
}
|
|
549
|
+
eligible = false;
|
|
550
|
+
} else {
|
|
551
|
+
const open = OPEN_FENCE_RE.exec(text);
|
|
552
|
+
const backtickInInfoString = open !== null && open[2][0] === "`" && open[3].includes("`");
|
|
553
|
+
if (open !== null && !backtickInInfoString) {
|
|
554
|
+
fence = {
|
|
555
|
+
char: open[2][0],
|
|
556
|
+
length: open[2].length,
|
|
557
|
+
indent: fenceIndentColumns(open[1]),
|
|
558
|
+
};
|
|
559
|
+
eligible = false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (eligible) {
|
|
565
|
+
const markerIdxInLine = line.indexOf(marker);
|
|
566
|
+
if (markerIdxInLine !== -1 && RESIDUE_RE.test(line.replace(marker, ""))) {
|
|
567
|
+
offsets.push(lineStart + markerIdxInLine);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (newlineIdx === -1) break;
|
|
571
|
+
lineStart = newlineIdx + 1;
|
|
572
|
+
}
|
|
573
|
+
return offsets;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Replace the existing BEGIN…END block in `source` with `block`. Requires
|
|
578
|
+
* EXACTLY one line-anchored BEGIN and one line-anchored END marker (per
|
|
579
|
+
* `scanMarkerLines`), with BEGIN preceding END — throws a clear, distinct
|
|
580
|
+
* error for each failure mode: missing (the block must be seeded once by
|
|
581
|
+
* hand), duplicated, or inverted markers.
|
|
582
|
+
*
|
|
583
|
+
* `beginMarker`/`endMarker` default to the CSS `@theme` block's markers so
|
|
584
|
+
* existing call sites (the CSS region) are unaffected; the `--md-table`
|
|
585
|
+
* region passes `MD_TABLE_BEGIN_MARKER`/`MD_TABLE_END_MARKER` instead — same
|
|
586
|
+
* function, parameterized, mirroring how gen-component-tokens.mjs reuses one
|
|
587
|
+
* `replaceBlock` across its content/chrome surfaces. `filePath` is used
|
|
588
|
+
* purely for error-message context (pass the conventional default or an
|
|
589
|
+
* explicit --css/--md-table value).
|
|
590
|
+
*
|
|
591
|
+
* `options.excludeFencedCode` is forwarded verbatim to `scanMarkerLines` and
|
|
592
|
+
* is gated on the SURFACE, not on the marker strings: only the `--md-table`
|
|
593
|
+
* call site passes it. Defaulting it off keeps every CSS call site — and its
|
|
594
|
+
* output — byte-unchanged.
|
|
595
|
+
*
|
|
596
|
+
* Exported for unit testing.
|
|
597
|
+
*/
|
|
598
|
+
export function replaceBlock(
|
|
599
|
+
source,
|
|
600
|
+
block,
|
|
601
|
+
beginMarker = BEGIN_MARKER,
|
|
602
|
+
endMarker = END_MARKER,
|
|
603
|
+
filePath = DEFAULT_CSS_PATH,
|
|
604
|
+
options = {},
|
|
605
|
+
) {
|
|
606
|
+
const { excludeFencedCode = false } = options;
|
|
607
|
+
const scanOptions = { excludeFencedCode };
|
|
608
|
+
const beginOffsets = scanMarkerLines(source, beginMarker, scanOptions);
|
|
609
|
+
const endOffsets = scanMarkerLines(source, endMarker, scanOptions);
|
|
610
|
+
|
|
611
|
+
if (beginOffsets.length === 0 || endOffsets.length === 0) {
|
|
114
612
|
throw new Error(
|
|
115
|
-
`Could not find ${
|
|
613
|
+
`Could not find ${beginMarker} … ${endMarker} markers in ${filePath}.\n` +
|
|
116
614
|
`Seed the marker block once by hand, then re-run the generator.`,
|
|
117
615
|
);
|
|
118
616
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
617
|
+
if (beginOffsets.length > 1 || endOffsets.length > 1) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
`Found duplicate markers in ${filePath} (${beginOffsets.length} BEGIN "${beginMarker}", ` +
|
|
620
|
+
`${endOffsets.length} END "${endMarker}"; expected exactly one of each). Remove the extra ` +
|
|
621
|
+
`marker(s) by hand, then re-run the generator.`,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const beginIdx = beginOffsets[0];
|
|
626
|
+
const endIdx = endOffsets[0];
|
|
627
|
+
if (beginIdx > endIdx) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`Markers in ${filePath} are inverted — the END marker (${endMarker}) appears before ` +
|
|
630
|
+
`the BEGIN marker (${beginMarker}). Fix the marker order by hand, then re-run the ` +
|
|
631
|
+
`generator.`,
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Expand to the full line that opens the block and to the end of the line
|
|
636
|
+
// that closes it, so the whole region (CSS comment or MDX brace-comment) is
|
|
637
|
+
// replaced.
|
|
638
|
+
const lineStart = source.lastIndexOf("\n", beginIdx) + 1;
|
|
639
|
+
const afterEnd = source.indexOf("\n", endIdx);
|
|
640
|
+
const lineEnd = afterEnd === -1 ? source.length : afterEnd;
|
|
641
|
+
return source.slice(0, lineStart) + block + source.slice(lineEnd);
|
|
125
642
|
}
|
|
126
643
|
|
|
127
|
-
|
|
128
|
-
|
|
644
|
+
/**
|
|
645
|
+
* Collapses whitespace runs — including an embedded literal newline from a
|
|
646
|
+
* multi-line `purpose:` source string (the source grammar permits a raw
|
|
647
|
+
* newline between the opening/closing quotes; only braces/backslashes/
|
|
648
|
+
* escaped quotes are rejected, see `assertSupportedPurposeGrammar`) — into a
|
|
649
|
+
* single space, and trims the result. A markdown/GFM table cell cannot
|
|
650
|
+
* contain a raw newline without corrupting the row.
|
|
651
|
+
*/
|
|
652
|
+
function collapseWhitespace(str) {
|
|
653
|
+
return str.replace(/\s+/g, " ").trim();
|
|
654
|
+
}
|
|
129
655
|
|
|
130
|
-
|
|
131
|
-
|
|
656
|
+
/** Escapes a literal `|` so it can't be misread as a table-cell boundary. */
|
|
657
|
+
function escapeTableCell(str) {
|
|
658
|
+
return str.replace(/\|/g, "\\|");
|
|
659
|
+
}
|
|
132
660
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
661
|
+
/**
|
|
662
|
+
* Escapes the characters MDX treats as syntax in text position — `<` (JSX
|
|
663
|
+
* tag open) and `{`/`}` (expression delimiters) — as character references,
|
|
664
|
+
* so a purpose like "search <dialog>" is emitted as literal text instead of
|
|
665
|
+
* failing MDX compilation as an unclosed JSX element. `&` is escaped FIRST
|
|
666
|
+
* so a purpose that already spells out an entity (e.g. "<") stays
|
|
667
|
+
* literal `<` on the rendered page instead of collapsing to `<`, and so
|
|
668
|
+
* the replacements below can never double-escape their own output.
|
|
669
|
+
*
|
|
670
|
+
* Braces cannot reach here via the CLI (`assertSupportedPurposeGrammar`
|
|
671
|
+
* rejects them at parse time), but `buildMdTable` is an exported helper that
|
|
672
|
+
* accepts hand-built tier arrays, so it defends against them itself.
|
|
673
|
+
*/
|
|
674
|
+
function escapeMdxText(str) {
|
|
675
|
+
return str
|
|
676
|
+
.replace(/&/g, "&")
|
|
677
|
+
.replace(/</g, "<")
|
|
678
|
+
.replace(/\{/g, "{")
|
|
679
|
+
.replace(/\}/g, "}");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Build the full `--md-table` region (markers included): a GFM
|
|
684
|
+
* `| Token | Kind | Role |` table, one row per tier, in source order.
|
|
685
|
+
* - Token: the tier name.
|
|
686
|
+
* - Kind: the tier's optional `kind` field, or `-` when absent.
|
|
687
|
+
* - Role: the tier's optional `purpose` field with internal whitespace/
|
|
688
|
+
* newlines collapsed to single spaces, or `-` when absent/empty.
|
|
689
|
+
* `|` in any cell is escaped so a stray pipe in a purpose string can't
|
|
690
|
+
* corrupt the table structure. The Role cell is additionally MDX-escaped
|
|
691
|
+
* (`&` first, then `<`/`{`/`}` — see `escapeMdxText`) so a purpose like
|
|
692
|
+
* "search <dialog>" can't make the emitted .mdx fail to compile as an
|
|
693
|
+
* unclosed JSX element.
|
|
694
|
+
*
|
|
695
|
+
* `options.tokensPath` feeds the "do not hand-edit" note beneath the BEGIN
|
|
696
|
+
* marker (same default/meaning as `buildBlock`'s `tokensPath`).
|
|
697
|
+
* `options.beginMarker`/`options.endMarker` default to the MDX-safe
|
|
698
|
+
* `MD_TABLE_BEGIN_MARKER`/`MD_TABLE_END_MARKER` pair — overridable for tests,
|
|
699
|
+
* same convention as `replaceBlock`.
|
|
700
|
+
*
|
|
701
|
+
* Exported for unit testing.
|
|
702
|
+
*/
|
|
703
|
+
export function buildMdTable(tiers, options = {}) {
|
|
704
|
+
const {
|
|
705
|
+
tokensPath = DEFAULT_TOKENS_PATH,
|
|
706
|
+
beginMarker = MD_TABLE_BEGIN_MARKER,
|
|
707
|
+
endMarker = MD_TABLE_END_MARKER,
|
|
708
|
+
} = options;
|
|
709
|
+
|
|
710
|
+
const lines = [];
|
|
711
|
+
lines.push(beginMarker);
|
|
712
|
+
lines.push("");
|
|
713
|
+
lines.push(
|
|
714
|
+
`_Generated by \`gen-z-index --md-table\`; do not hand-edit — edit \`${tokensPath}\` instead._`,
|
|
715
|
+
);
|
|
716
|
+
lines.push("");
|
|
717
|
+
lines.push("| Token | Kind | Role |");
|
|
718
|
+
lines.push("| --- | --- | --- |");
|
|
719
|
+
for (const tier of tiers) {
|
|
720
|
+
const token = escapeTableCell(tier.name);
|
|
721
|
+
const kind = escapeTableCell(tier.kind ?? "-");
|
|
722
|
+
// Collapse first, THEN decide the fallback — a whitespace-only purpose
|
|
723
|
+
// (e.g. " ") is truthy but collapses to "", which must still fall back
|
|
724
|
+
// to "-" rather than emit a blank Role cell.
|
|
725
|
+
const collapsedPurpose = tier.purpose ? collapseWhitespace(tier.purpose) : "";
|
|
726
|
+
// Role is the only free-text cell (Token is ^[a-z0-9-]+$, Kind is
|
|
727
|
+
// global|local|-), so it alone needs MDX escaping on top of the pipe
|
|
728
|
+
// escaping.
|
|
729
|
+
const role = collapsedPurpose
|
|
730
|
+
? escapeTableCell(escapeMdxText(collapsedPurpose))
|
|
731
|
+
: "-";
|
|
732
|
+
lines.push(`| ${token} | ${kind} | ${role} |`);
|
|
733
|
+
}
|
|
734
|
+
lines.push("");
|
|
735
|
+
lines.push(endMarker);
|
|
736
|
+
return lines.join("\n");
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* CLI entrypoint. `argv` defaults to the real process argv (minus the node/
|
|
741
|
+
* script prefix) so `isDirectInvocation()` below can call `main()` with no
|
|
742
|
+
* arguments, while tests can pass a synthetic argv without touching
|
|
743
|
+
* `process.argv`. Resolves --tokens/--css/--md-table against `process.cwd()`
|
|
744
|
+
* (the project root) for actual file I/O, but threads the AS-GIVEN path
|
|
745
|
+
* strings (conventional defaults or explicit flag values) through to every
|
|
746
|
+
* message and into the generated header — never the resolved absolute path —
|
|
747
|
+
* so the committed CSS/md file never embeds a machine-specific path.
|
|
748
|
+
*
|
|
749
|
+
* The CSS `@theme` region is always generated/verified. The `--md-table`
|
|
750
|
+
* region is entirely opt-in: when `--md-table <path>` isn't passed, no md
|
|
751
|
+
* file is read, built, or written, and `--check` only covers the CSS region
|
|
752
|
+
* (unchanged from pre-`--md-table` behavior). When it IS passed, `--check`
|
|
753
|
+
* covers BOTH regions — drift in either exits 1.
|
|
754
|
+
*
|
|
755
|
+
* Exported for unit testing.
|
|
756
|
+
*/
|
|
757
|
+
export function main(argv = process.argv.slice(2)) {
|
|
758
|
+
const args = parseArgs(argv);
|
|
759
|
+
const tokensPath = args.tokens ?? DEFAULT_TOKENS_PATH;
|
|
760
|
+
const cssPath = args.css ?? DEFAULT_CSS_PATH;
|
|
761
|
+
const themeWrapper = !args.noThemeWrapper;
|
|
762
|
+
const mdTablePath = args.mdTable;
|
|
763
|
+
|
|
764
|
+
const root = process.cwd();
|
|
765
|
+
const tokensAbsPath = resolve(root, tokensPath);
|
|
766
|
+
const cssAbsPath = resolve(root, cssPath);
|
|
136
767
|
|
|
137
|
-
|
|
138
|
-
|
|
768
|
+
const tokensSrc = readFileSync(tokensAbsPath, "utf8");
|
|
769
|
+
const css = readFileSync(cssAbsPath, "utf8");
|
|
770
|
+
|
|
771
|
+
const tiers = parseTiers(tokensSrc, tokensPath);
|
|
772
|
+
const block = buildBlock(tiers, { tokensPath, cssPath, themeWrapper });
|
|
773
|
+
const nextCss = replaceBlock(css, block, BEGIN_MARKER, END_MARKER, cssPath);
|
|
774
|
+
|
|
775
|
+
let mdAbsPath;
|
|
776
|
+
let mdSrc;
|
|
777
|
+
let nextMd;
|
|
778
|
+
if (mdTablePath !== undefined) {
|
|
779
|
+
mdAbsPath = resolve(root, mdTablePath);
|
|
780
|
+
mdSrc = readFileSync(mdAbsPath, "utf8");
|
|
781
|
+
const mdBlock = buildMdTable(tiers, { tokensPath });
|
|
782
|
+
nextMd = replaceBlock(
|
|
783
|
+
mdSrc,
|
|
784
|
+
mdBlock,
|
|
785
|
+
MD_TABLE_BEGIN_MARKER,
|
|
786
|
+
MD_TABLE_END_MARKER,
|
|
787
|
+
mdTablePath,
|
|
788
|
+
{ excludeFencedCode: true },
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (args.check) {
|
|
793
|
+
let drift = false;
|
|
794
|
+
if (nextCss !== css) {
|
|
795
|
+
console.error(`z-index codegen drift detected: ${cssPath} is out of date.`);
|
|
796
|
+
drift = true;
|
|
797
|
+
}
|
|
798
|
+
if (mdTablePath !== undefined && nextMd !== mdSrc) {
|
|
799
|
+
console.error(`z-index codegen drift detected: ${mdTablePath} is out of date.`);
|
|
800
|
+
drift = true;
|
|
801
|
+
}
|
|
802
|
+
if (drift) {
|
|
139
803
|
console.error(
|
|
140
|
-
|
|
804
|
+
`Run \`${buildRerunCommand({ tokensPath, cssPath, themeWrapper, mdTablePath })}\` and commit the result.`,
|
|
141
805
|
);
|
|
142
|
-
console.error("Run `pnpm gen:z-index` and commit the result.");
|
|
143
806
|
return 1;
|
|
144
807
|
}
|
|
145
808
|
console.log(
|
|
146
|
-
|
|
809
|
+
mdTablePath !== undefined
|
|
810
|
+
? `OK — z-index @theme block and md table are up to date (${tiers.length} tiers).`
|
|
811
|
+
: `OK — z-index @theme block is up to date (${tiers.length} tiers).`,
|
|
147
812
|
);
|
|
148
813
|
return 0;
|
|
149
814
|
}
|
|
150
815
|
|
|
151
|
-
if (
|
|
816
|
+
if (nextCss === css) {
|
|
152
817
|
console.log(
|
|
153
818
|
`z-index @theme block already up to date (${tiers.length} tiers); no change.`,
|
|
154
819
|
);
|
|
155
|
-
|
|
820
|
+
} else {
|
|
821
|
+
writeFileSync(cssAbsPath, nextCss);
|
|
822
|
+
console.log(`Wrote z-index @theme block to ${cssPath} (${tiers.length} tiers).`);
|
|
156
823
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
824
|
+
|
|
825
|
+
if (mdTablePath !== undefined) {
|
|
826
|
+
if (nextMd === mdSrc) {
|
|
827
|
+
console.log(`z-index table already up to date at ${mdTablePath}; no change.`);
|
|
828
|
+
} else {
|
|
829
|
+
writeFileSync(mdAbsPath, nextMd);
|
|
830
|
+
console.log(`Wrote z-index table to ${mdTablePath} (${tiers.length} tiers).`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
161
834
|
return 0;
|
|
162
835
|
}
|
|
163
836
|
|
|
164
|
-
|
|
837
|
+
// Run the CLI only when executed directly, NOT when imported by tests (an
|
|
838
|
+
// import must not write global.css or exit the process as a side effect — it
|
|
839
|
+
// would break `pnpm test`). Compare REAL paths on both sides: when invoked
|
|
840
|
+
// through the pnpm bin shim, argv[1] is the `node_modules/.bin/…` symlink,
|
|
841
|
+
// NOT the real file, so a raw path-equality check is always false and
|
|
842
|
+
// main() would silently no-op. realpathSync resolves the shim/symlink to the
|
|
843
|
+
// real file on both sides so direct invocation is detected however the bin
|
|
844
|
+
// is launched.
|
|
845
|
+
function isDirectInvocation() {
|
|
846
|
+
if (!process.argv[1]) return false;
|
|
847
|
+
try {
|
|
848
|
+
return (
|
|
849
|
+
realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])
|
|
850
|
+
);
|
|
851
|
+
} catch {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (isDirectInvocation()) {
|
|
857
|
+
process.exit(main());
|
|
858
|
+
}
|