@cssdoc/core 0.2.0 → 0.3.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/dist/index.d.mts +139 -29
- package/dist/index.mjs +292 -303
- package/package.json +4 -5
- package/grammar/CssDoc.grammarkdown +0 -406
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import postcss from "postcss";
|
|
2
|
+
import { CSSDOC_TAGS, CSSDOC_TAG_NAMES } from "@cssdoc/spec";
|
|
2
3
|
//#region src/modifier.ts
|
|
4
|
+
/**
|
|
5
|
+
* The pseudo-classes cssdoc treats as component states by default — form and UI states a component
|
|
6
|
+
* meaningfully declares. Ubiquitous interaction pseudos (`:hover`, `:focus`, `:active`) are omitted so
|
|
7
|
+
* incidental rules don't become documented states; add them via `statePseudoClasses` if you want them.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_STATE_PSEUDO_CLASSES = [
|
|
10
|
+
"checked",
|
|
11
|
+
"disabled",
|
|
12
|
+
"enabled",
|
|
13
|
+
"indeterminate",
|
|
14
|
+
"default",
|
|
15
|
+
"open",
|
|
16
|
+
"placeholder-shown",
|
|
17
|
+
"read-only",
|
|
18
|
+
"read-write",
|
|
19
|
+
"required",
|
|
20
|
+
"optional",
|
|
21
|
+
"valid",
|
|
22
|
+
"invalid",
|
|
23
|
+
"in-range",
|
|
24
|
+
"out-of-range"
|
|
25
|
+
];
|
|
3
26
|
/** The built-in convention presets. Other schemes use the custom object (see the docs). */
|
|
4
27
|
const MODIFIER_PRESETS = {
|
|
5
|
-
/** BEM / SUIT — `.button--primary`. The default. */
|
|
28
|
+
/** BEM / SUIT — `.button--primary`, with `.button__element` sub-elements. The default. */
|
|
6
29
|
bem: {
|
|
7
30
|
structure: "suffix",
|
|
8
31
|
separator: "--",
|
|
32
|
+
elementSeparator: "__",
|
|
9
33
|
propValue: false
|
|
10
34
|
},
|
|
11
35
|
/** rscss — `.button.-color-secondary`, split into `prop`/`value`. */
|
|
@@ -39,6 +63,9 @@ function resolveModifierConvention(input) {
|
|
|
39
63
|
return {
|
|
40
64
|
structure: base.structure,
|
|
41
65
|
separator: base.separator,
|
|
66
|
+
...base.elementSeparator !== void 0 ? { elementSeparator: base.elementSeparator } : {},
|
|
67
|
+
...base.statePrefixes !== void 0 ? { statePrefixes: base.statePrefixes } : {},
|
|
68
|
+
statePseudoClasses: base.statePseudoClasses ?? [...DEFAULT_STATE_PSEUDO_CLASSES],
|
|
42
69
|
propValue: base.propValue ?? false,
|
|
43
70
|
propValueSeparator: base.propValueSeparator ?? "-"
|
|
44
71
|
};
|
|
@@ -54,10 +81,45 @@ const unquote$1 = (v) => v.trim().replace(/^(["'])([\s\S]*)\1$/u, "$2");
|
|
|
54
81
|
*/
|
|
55
82
|
var ModifierMatcher = class {
|
|
56
83
|
convention;
|
|
57
|
-
|
|
84
|
+
/** The separator(s), longest-first so overlapping prefixes (e.g. `--` before `-`) match greedily. */
|
|
85
|
+
separators;
|
|
86
|
+
/** The separators as a non-capturing regex alternation, e.g. `(?:is-|has-)` (or `(?:)` when empty). */
|
|
87
|
+
sepAlt;
|
|
88
|
+
/** BEM-style element separators (longest-first), or empty when the convention has none. */
|
|
89
|
+
elementSeparators;
|
|
90
|
+
/** The element separators as a non-capturing alternation, or `""` when there are none. */
|
|
91
|
+
elementSepAlt;
|
|
92
|
+
/** State-class prefixes (longest-first), or empty when the convention has none. */
|
|
93
|
+
statePrefixes;
|
|
94
|
+
/** Native pseudo-classes (no `:`) recognized as states. */
|
|
95
|
+
statePseudoClasses;
|
|
58
96
|
constructor(convention) {
|
|
59
97
|
this.convention = convention;
|
|
60
|
-
|
|
98
|
+
const longestFirst = (value) => (Array.isArray(value) ? value : [value]).slice().sort((a, b) => b.length - a.length);
|
|
99
|
+
this.separators = longestFirst(convention.separator);
|
|
100
|
+
this.sepAlt = `(?:${this.separators.map(escapeRe).join("|")})`;
|
|
101
|
+
this.elementSeparators = convention.elementSeparator ? longestFirst(convention.elementSeparator).filter((s) => s !== "") : [];
|
|
102
|
+
this.elementSepAlt = this.elementSeparators.length ? `(?:${this.elementSeparators.map(escapeRe).join("|")})` : "";
|
|
103
|
+
this.statePrefixes = (convention.statePrefixes ?? []).filter((p) => p !== "").slice().sort((a, b) => b.length - a.length);
|
|
104
|
+
this.statePseudoClasses = new Set(convention.statePseudoClasses ?? []);
|
|
105
|
+
}
|
|
106
|
+
/** Does a class name (no leading dot) start with one of the convention's state prefixes? */
|
|
107
|
+
isStateClass(name) {
|
|
108
|
+
return this.statePrefixes.some((p) => name.startsWith(p));
|
|
109
|
+
}
|
|
110
|
+
/** Strip a leading chained-class separator from `name` (the longest that matches), else return it. */
|
|
111
|
+
stripPrefix(name) {
|
|
112
|
+
for (const s of this.separators) if (name.startsWith(s)) return name.slice(s.length);
|
|
113
|
+
return name;
|
|
114
|
+
}
|
|
115
|
+
/** Return the suffix body — the part after the first (longest) non-empty separator occurrence. */
|
|
116
|
+
stripToBody(name) {
|
|
117
|
+
for (const s of this.separators) {
|
|
118
|
+
if (s === "") continue;
|
|
119
|
+
const at = name.indexOf(s);
|
|
120
|
+
if (at !== -1) return name.slice(at + s.length);
|
|
121
|
+
}
|
|
122
|
+
return name;
|
|
61
123
|
}
|
|
62
124
|
/**
|
|
63
125
|
* Every modifier attached to `baseNoDot` within one selector. `selector` should have its pseudos
|
|
@@ -76,7 +138,7 @@ var ModifierMatcher = class {
|
|
|
76
138
|
});
|
|
77
139
|
};
|
|
78
140
|
if (this.convention.structure === "suffix") {
|
|
79
|
-
const re = new RegExp(`\\.(${baseEsc}${this.
|
|
141
|
+
const re = new RegExp(`\\.(${baseEsc}${this.sepAlt}[\\w-]+)`, "gu");
|
|
80
142
|
for (const m of selector.matchAll(re)) push(m[1]);
|
|
81
143
|
return hits;
|
|
82
144
|
}
|
|
@@ -88,12 +150,93 @@ var ModifierMatcher = class {
|
|
|
88
150
|
}
|
|
89
151
|
return hits;
|
|
90
152
|
}
|
|
91
|
-
const cls = `\\.${this.
|
|
153
|
+
const cls = `\\.${this.sepAlt}[\\w-]+`;
|
|
92
154
|
const chain = new RegExp(`(?:\\.${baseEsc}|:scope)((?:${cls})+)`, "gu");
|
|
93
|
-
const inner = new RegExp(`\\.(${this.
|
|
94
|
-
for (const m of selector.matchAll(chain)) for (const c of m[1].matchAll(inner)) push(c[1]);
|
|
155
|
+
const inner = new RegExp(`\\.(${this.sepAlt}[\\w-]+)`, "gu");
|
|
156
|
+
for (const m of selector.matchAll(chain)) for (const c of m[1].matchAll(inner)) if (!this.isStateClass(c[1])) push(c[1]);
|
|
95
157
|
return hits;
|
|
96
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Every BEM-style element attached to `baseNoDot` within one selector (`.base<elementSep><name>`),
|
|
161
|
+
* as parts, each with any element-scoped modifiers (`.base__element--mod` → element `base__element`
|
|
162
|
+
* with modifier `mod`). Only meaningful for `suffix` conventions with an `elementSeparator`.
|
|
163
|
+
*/
|
|
164
|
+
elementsIn(selector, baseNoDot) {
|
|
165
|
+
if (this.convention.structure !== "suffix" || this.elementSepAlt === "") return [];
|
|
166
|
+
const baseEsc = escapeRe(baseNoDot);
|
|
167
|
+
const re = new RegExp(`\\.(${baseEsc}${this.elementSepAlt}[\\w-]+)`, "gu");
|
|
168
|
+
const byName = /* @__PURE__ */ new Map();
|
|
169
|
+
for (const m of selector.matchAll(re)) {
|
|
170
|
+
const { element, modifier } = this.splitElementModifier(m[1], baseNoDot);
|
|
171
|
+
const mods = byName.get(element) ?? [];
|
|
172
|
+
if (modifier && !mods.some((x) => x.name === modifier.name)) mods.push(modifier);
|
|
173
|
+
byName.set(element, mods);
|
|
174
|
+
}
|
|
175
|
+
return [...byName].map(([name, modifiers]) => ({
|
|
176
|
+
name,
|
|
177
|
+
modifiers
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Split `.base__element--mod`-style tokens into the element class (`base__element`) and, if a
|
|
182
|
+
* modifier separator follows the element name, the element-scoped modifier.
|
|
183
|
+
*/
|
|
184
|
+
splitElementModifier(token, baseNoDot) {
|
|
185
|
+
const elsep = this.elementSeparators.find((s) => token.startsWith(baseNoDot + s));
|
|
186
|
+
if (!elsep) return { element: token };
|
|
187
|
+
const afterElement = baseNoDot.length + elsep.length;
|
|
188
|
+
let at = -1;
|
|
189
|
+
for (const sep of this.separators) {
|
|
190
|
+
if (sep === "") continue;
|
|
191
|
+
const i = token.indexOf(sep, afterElement);
|
|
192
|
+
if (i !== -1 && (at === -1 || i < at)) at = i;
|
|
193
|
+
}
|
|
194
|
+
if (at === -1) return { element: token };
|
|
195
|
+
return {
|
|
196
|
+
element: token.slice(0, at),
|
|
197
|
+
modifier: {
|
|
198
|
+
name: token,
|
|
199
|
+
...this.analyze(token)
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Every state class chained to `baseNoDot` within one selector — a class whose name starts with one
|
|
205
|
+
* of the convention's {@link ModifierConvention.statePrefixes} (e.g. `.tabs.is-open` → `is-open`).
|
|
206
|
+
* Empty when the convention sets no state prefixes.
|
|
207
|
+
*/
|
|
208
|
+
statesIn(selector, baseNoDot) {
|
|
209
|
+
if (this.statePrefixes.length === 0) return [];
|
|
210
|
+
const baseEsc = escapeRe(baseNoDot);
|
|
211
|
+
const prefixAlt = `(?:${this.statePrefixes.map(escapeRe).join("|")})`;
|
|
212
|
+
const chain = new RegExp(`(?:\\.${baseEsc}|:scope)((?:\\.[\\w-]+)+)`, "gu");
|
|
213
|
+
const inner = new RegExp(`\\.(${prefixAlt}[\\w-]+)`, "gu");
|
|
214
|
+
const seen = /* @__PURE__ */ new Set();
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const m of selector.matchAll(chain)) for (const c of m[1].matchAll(inner)) if (!seen.has(c[1])) {
|
|
217
|
+
seen.add(c[1]);
|
|
218
|
+
out.push({ name: c[1] });
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Every native pseudo-class on the selector recognized as a state — a `:name` whose `name` is in the
|
|
224
|
+
* convention's {@link ModifierConvention.statePseudoClasses} (e.g. `.tab:disabled` → `disabled`).
|
|
225
|
+
* Pseudo-elements (`::part`) and pseudos not in the set are ignored.
|
|
226
|
+
*/
|
|
227
|
+
pseudoStatesIn(selector) {
|
|
228
|
+
if (this.statePseudoClasses.size === 0) return [];
|
|
229
|
+
const seen = /* @__PURE__ */ new Set();
|
|
230
|
+
const out = [];
|
|
231
|
+
for (const m of selector.matchAll(/(?<!:):([\w-]+)/gu)) {
|
|
232
|
+
const name = m[1];
|
|
233
|
+
if (this.statePseudoClasses.has(name) && !seen.has(name)) {
|
|
234
|
+
seen.add(name);
|
|
235
|
+
out.push({ name });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
97
240
|
/** Derive `prop`/`value` for a modifier `name` (as returned by {@link modifiersIn} or authored). */
|
|
98
241
|
analyze(name) {
|
|
99
242
|
if (this.convention.structure === "attribute") {
|
|
@@ -101,15 +244,11 @@ var ModifierMatcher = class {
|
|
|
101
244
|
const eq = canonical.indexOf("=");
|
|
102
245
|
const attr = eq === -1 ? canonical : canonical.slice(0, eq);
|
|
103
246
|
return {
|
|
104
|
-
prop:
|
|
247
|
+
prop: this.stripPrefix(attr),
|
|
105
248
|
value: eq === -1 ? void 0 : unquote$1(canonical.slice(eq + 1))
|
|
106
249
|
};
|
|
107
250
|
}
|
|
108
|
-
|
|
109
|
-
if (this.convention.structure === "suffix") {
|
|
110
|
-
const at = name.indexOf(this.convention.separator);
|
|
111
|
-
body = at === -1 ? name : name.slice(at + this.convention.separator.length);
|
|
112
|
-
} else body = this.convention.separator && name.startsWith(this.convention.separator) ? name.slice(this.convention.separator.length) : name;
|
|
251
|
+
const body = this.convention.structure === "suffix" ? this.stripToBody(name) : this.stripPrefix(name);
|
|
113
252
|
if (!this.convention.propValue) return { prop: body };
|
|
114
253
|
const sep = this.convention.propValueSeparator ?? "-";
|
|
115
254
|
const at = body.indexOf(sep);
|
|
@@ -138,12 +277,25 @@ var ModifierMatcher = class {
|
|
|
138
277
|
looksLikeUsage(token, baseNoDot) {
|
|
139
278
|
if (this.convention.structure === "attribute") return this.attributeMatches(this.normalizeMember(token));
|
|
140
279
|
const name = token.replace(/^\./u, "");
|
|
280
|
+
if (this.isStateClass(name)) return false;
|
|
141
281
|
if (this.convention.structure === "suffix") {
|
|
142
|
-
if (baseNoDot) return name.startsWith(`${baseNoDot}${
|
|
143
|
-
return this.
|
|
282
|
+
if (baseNoDot) return this.separators.some((s) => name.startsWith(`${baseNoDot}${s}`));
|
|
283
|
+
return this.separators.some((s) => s !== "" && name.includes(s));
|
|
144
284
|
}
|
|
145
285
|
if (baseNoDot && name === baseNoDot) return false;
|
|
146
|
-
return name.startsWith(
|
|
286
|
+
return this.separators.some((s) => name.startsWith(s));
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Classify a host-document class token relative to `baseNoDot`: a `modifier` usage, a `state` class
|
|
290
|
+
* (a `statePrefixes` prefix), a BEM `element` class (`base<elementSep>…`), or `undefined` if it's
|
|
291
|
+
* none of those. Consumer-side linting routes each kind to the right "unknown-…" check.
|
|
292
|
+
*/
|
|
293
|
+
usageKind(token, baseNoDot) {
|
|
294
|
+
if (this.convention.structure === "attribute") return this.looksLikeUsage(token, baseNoDot) ? "modifier" : void 0;
|
|
295
|
+
const name = token.replace(/^\./u, "");
|
|
296
|
+
if (this.isStateClass(name)) return "state";
|
|
297
|
+
if (baseNoDot && this.convention.structure === "suffix" && this.elementSeparators.some((s) => name.startsWith(`${baseNoDot}${s}`))) return "element";
|
|
298
|
+
return this.looksLikeUsage(token, baseNoDot) ? "modifier" : void 0;
|
|
147
299
|
}
|
|
148
300
|
/** Canonicalize an attribute expression (bracket-inner): normalize quotes to double, trim. */
|
|
149
301
|
normalizeAttribute(inner) {
|
|
@@ -152,15 +304,29 @@ var ModifierMatcher = class {
|
|
|
152
304
|
if (eq === -1) return trimmed;
|
|
153
305
|
return `${trimmed.slice(0, eq + 1)}"${unquote$1(trimmed.slice(eq + 1))}"`;
|
|
154
306
|
}
|
|
155
|
-
/** Does an attribute-expression's name carry the convention's required
|
|
307
|
+
/** Does an attribute-expression's name carry one of the convention's required prefixes? */
|
|
156
308
|
attributeMatches(name) {
|
|
157
309
|
const eq = name.indexOf("=");
|
|
158
310
|
const attr = (eq === -1 ? name : name.slice(0, eq)).replace(/[~|^$*]$/u, "");
|
|
159
|
-
return attr.length > 0 && attr.startsWith(
|
|
311
|
+
return attr.length > 0 && this.separators.some((s) => attr.startsWith(s));
|
|
160
312
|
}
|
|
161
313
|
};
|
|
162
314
|
//#endregion
|
|
163
315
|
//#region src/configuration.ts
|
|
316
|
+
/**
|
|
317
|
+
* The tag-definition registry — cssdoc's analog to `@microsoft/tsdoc`'s `TSDocConfiguration` /
|
|
318
|
+
* `TSDocTagDefinition`. It names the vocabulary a parse understands: the built-in **standard** tags
|
|
319
|
+
* (seeded automatically) plus any **custom** tags registered on top (typically loaded from a
|
|
320
|
+
* `cssdoc.json` by `@cssdoc/config`). The parser consults the configuration to decide which record
|
|
321
|
+
* tags open a boundary, which standard tags are active, and which unknown tags to capture as custom
|
|
322
|
+
* blocks rather than ignore.
|
|
323
|
+
*
|
|
324
|
+
* The vocabulary is expansive and modeled on TSDoc's kind taxonomy — `record` / `block` / `modifier` /
|
|
325
|
+
* `inline` — and covers the modern CSSOM surface. See `@cssdoc/spec`'s `grammar/CssDoc.grammarkdown`
|
|
326
|
+
* for the formal shape of each tag.
|
|
327
|
+
*
|
|
328
|
+
* @module
|
|
329
|
+
*/
|
|
164
330
|
const TAG_NAME_RE = /^@?[a-zA-Z][a-zA-Z0-9-]*$/u;
|
|
165
331
|
/** One tag in the vocabulary: its name, kind, and how it may be used. */
|
|
166
332
|
var CssDocTagDefinition = class {
|
|
@@ -269,258 +435,17 @@ var CssDocConfiguration = class CssDocConfiguration {
|
|
|
269
435
|
setNoStandardTags() {
|
|
270
436
|
this.setSupportForTags(CssDocConfiguration.standardTagNames.map((n) => this._byName.get(n)), false);
|
|
271
437
|
}
|
|
272
|
-
/** The names (without `@`) of every standard tag
|
|
273
|
-
static standardTagNames =
|
|
274
|
-
|
|
275
|
-
"name",
|
|
276
|
-
"utility",
|
|
277
|
-
"rule",
|
|
278
|
-
"declaration",
|
|
279
|
-
"class",
|
|
280
|
-
"summary",
|
|
281
|
-
"remarks",
|
|
282
|
-
"privateRemarks",
|
|
283
|
-
"deprecated",
|
|
284
|
-
"example",
|
|
285
|
-
"see",
|
|
286
|
-
"since",
|
|
287
|
-
"group",
|
|
288
|
-
"category",
|
|
289
|
-
"defaultValue",
|
|
290
|
-
"modifier",
|
|
291
|
-
"part",
|
|
292
|
-
"csspart",
|
|
293
|
-
"cssproperty",
|
|
294
|
-
"property",
|
|
295
|
-
"cssstate",
|
|
296
|
-
"slot",
|
|
297
|
-
"function",
|
|
298
|
-
"keyframes",
|
|
299
|
-
"animation",
|
|
300
|
-
"layer",
|
|
301
|
-
"container",
|
|
302
|
-
"supports",
|
|
303
|
-
"media",
|
|
304
|
-
"responsive",
|
|
305
|
-
"a11y",
|
|
306
|
-
"accessibility",
|
|
307
|
-
"structure",
|
|
308
|
-
"demo",
|
|
309
|
-
"alpha",
|
|
310
|
-
"beta",
|
|
311
|
-
"experimental",
|
|
312
|
-
"internal",
|
|
313
|
-
"public",
|
|
314
|
-
"link",
|
|
315
|
-
"inheritDoc",
|
|
316
|
-
"label"
|
|
317
|
-
];
|
|
318
|
-
/** A fresh set of the standard tag definitions (new instances on each call). */
|
|
438
|
+
/** The names (without `@`) of every standard tag — the canonical vocabulary from `@cssdoc/spec`. */
|
|
439
|
+
static standardTagNames = CSSDOC_TAG_NAMES;
|
|
440
|
+
/** A fresh set of the standard tag definitions (new instances on each call), built from the spec. */
|
|
319
441
|
static standardTags() {
|
|
320
|
-
return
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
tagName: "name",
|
|
328
|
-
syntaxKind: "record",
|
|
329
|
-
recordKind: "component"
|
|
330
|
-
}),
|
|
331
|
-
def({
|
|
332
|
-
tagName: "utility",
|
|
333
|
-
syntaxKind: "record",
|
|
334
|
-
recordKind: "utility"
|
|
335
|
-
}),
|
|
336
|
-
def({
|
|
337
|
-
tagName: "rule",
|
|
338
|
-
syntaxKind: "record",
|
|
339
|
-
recordKind: "rule"
|
|
340
|
-
}),
|
|
341
|
-
def({
|
|
342
|
-
tagName: "declaration",
|
|
343
|
-
syntaxKind: "record",
|
|
344
|
-
recordKind: "declaration"
|
|
345
|
-
}),
|
|
346
|
-
def({
|
|
347
|
-
tagName: "class",
|
|
348
|
-
syntaxKind: "block"
|
|
349
|
-
}),
|
|
350
|
-
def({
|
|
351
|
-
tagName: "summary",
|
|
352
|
-
syntaxKind: "block"
|
|
353
|
-
}),
|
|
354
|
-
def({
|
|
355
|
-
tagName: "remarks",
|
|
356
|
-
syntaxKind: "block"
|
|
357
|
-
}),
|
|
358
|
-
def({
|
|
359
|
-
tagName: "privateRemarks",
|
|
360
|
-
syntaxKind: "block"
|
|
361
|
-
}),
|
|
362
|
-
def({
|
|
363
|
-
tagName: "deprecated",
|
|
364
|
-
syntaxKind: "block"
|
|
365
|
-
}),
|
|
366
|
-
def({
|
|
367
|
-
tagName: "example",
|
|
368
|
-
syntaxKind: "block",
|
|
369
|
-
allowMultiple: true
|
|
370
|
-
}),
|
|
371
|
-
def({
|
|
372
|
-
tagName: "see",
|
|
373
|
-
syntaxKind: "block",
|
|
374
|
-
allowMultiple: true
|
|
375
|
-
}),
|
|
376
|
-
def({
|
|
377
|
-
tagName: "since",
|
|
378
|
-
syntaxKind: "block"
|
|
379
|
-
}),
|
|
380
|
-
def({
|
|
381
|
-
tagName: "group",
|
|
382
|
-
syntaxKind: "block"
|
|
383
|
-
}),
|
|
384
|
-
def({
|
|
385
|
-
tagName: "category",
|
|
386
|
-
syntaxKind: "block",
|
|
387
|
-
aliasFor: "group"
|
|
388
|
-
}),
|
|
389
|
-
def({
|
|
390
|
-
tagName: "defaultValue",
|
|
391
|
-
syntaxKind: "block"
|
|
392
|
-
}),
|
|
393
|
-
def({
|
|
394
|
-
tagName: "modifier",
|
|
395
|
-
syntaxKind: "block",
|
|
396
|
-
allowMultiple: true
|
|
397
|
-
}),
|
|
398
|
-
def({
|
|
399
|
-
tagName: "part",
|
|
400
|
-
syntaxKind: "block",
|
|
401
|
-
allowMultiple: true
|
|
402
|
-
}),
|
|
403
|
-
def({
|
|
404
|
-
tagName: "csspart",
|
|
405
|
-
syntaxKind: "block",
|
|
406
|
-
allowMultiple: true,
|
|
407
|
-
aliasFor: "part"
|
|
408
|
-
}),
|
|
409
|
-
def({
|
|
410
|
-
tagName: "cssproperty",
|
|
411
|
-
syntaxKind: "block",
|
|
412
|
-
allowMultiple: true
|
|
413
|
-
}),
|
|
414
|
-
def({
|
|
415
|
-
tagName: "property",
|
|
416
|
-
syntaxKind: "block",
|
|
417
|
-
allowMultiple: true,
|
|
418
|
-
aliasFor: "cssproperty"
|
|
419
|
-
}),
|
|
420
|
-
def({
|
|
421
|
-
tagName: "cssstate",
|
|
422
|
-
syntaxKind: "block",
|
|
423
|
-
allowMultiple: true
|
|
424
|
-
}),
|
|
425
|
-
def({
|
|
426
|
-
tagName: "slot",
|
|
427
|
-
syntaxKind: "block",
|
|
428
|
-
allowMultiple: true
|
|
429
|
-
}),
|
|
430
|
-
def({
|
|
431
|
-
tagName: "function",
|
|
432
|
-
syntaxKind: "block",
|
|
433
|
-
allowMultiple: true
|
|
434
|
-
}),
|
|
435
|
-
def({
|
|
436
|
-
tagName: "keyframes",
|
|
437
|
-
syntaxKind: "block",
|
|
438
|
-
allowMultiple: true
|
|
439
|
-
}),
|
|
440
|
-
def({
|
|
441
|
-
tagName: "animation",
|
|
442
|
-
syntaxKind: "block",
|
|
443
|
-
allowMultiple: true,
|
|
444
|
-
aliasFor: "keyframes"
|
|
445
|
-
}),
|
|
446
|
-
def({
|
|
447
|
-
tagName: "layer",
|
|
448
|
-
syntaxKind: "block",
|
|
449
|
-
allowMultiple: true
|
|
450
|
-
}),
|
|
451
|
-
def({
|
|
452
|
-
tagName: "container",
|
|
453
|
-
syntaxKind: "block",
|
|
454
|
-
allowMultiple: true
|
|
455
|
-
}),
|
|
456
|
-
def({
|
|
457
|
-
tagName: "supports",
|
|
458
|
-
syntaxKind: "block",
|
|
459
|
-
allowMultiple: true
|
|
460
|
-
}),
|
|
461
|
-
def({
|
|
462
|
-
tagName: "media",
|
|
463
|
-
syntaxKind: "block",
|
|
464
|
-
allowMultiple: true
|
|
465
|
-
}),
|
|
466
|
-
def({
|
|
467
|
-
tagName: "responsive",
|
|
468
|
-
syntaxKind: "block",
|
|
469
|
-
allowMultiple: true,
|
|
470
|
-
aliasFor: "media"
|
|
471
|
-
}),
|
|
472
|
-
def({
|
|
473
|
-
tagName: "a11y",
|
|
474
|
-
syntaxKind: "block",
|
|
475
|
-
allowMultiple: true
|
|
476
|
-
}),
|
|
477
|
-
def({
|
|
478
|
-
tagName: "accessibility",
|
|
479
|
-
syntaxKind: "block",
|
|
480
|
-
allowMultiple: true,
|
|
481
|
-
aliasFor: "a11y"
|
|
482
|
-
}),
|
|
483
|
-
def({
|
|
484
|
-
tagName: "structure",
|
|
485
|
-
syntaxKind: "block"
|
|
486
|
-
}),
|
|
487
|
-
def({
|
|
488
|
-
tagName: "demo",
|
|
489
|
-
syntaxKind: "block"
|
|
490
|
-
}),
|
|
491
|
-
def({
|
|
492
|
-
tagName: "alpha",
|
|
493
|
-
syntaxKind: "modifier"
|
|
494
|
-
}),
|
|
495
|
-
def({
|
|
496
|
-
tagName: "beta",
|
|
497
|
-
syntaxKind: "modifier"
|
|
498
|
-
}),
|
|
499
|
-
def({
|
|
500
|
-
tagName: "experimental",
|
|
501
|
-
syntaxKind: "modifier"
|
|
502
|
-
}),
|
|
503
|
-
def({
|
|
504
|
-
tagName: "internal",
|
|
505
|
-
syntaxKind: "modifier"
|
|
506
|
-
}),
|
|
507
|
-
def({
|
|
508
|
-
tagName: "public",
|
|
509
|
-
syntaxKind: "modifier"
|
|
510
|
-
}),
|
|
511
|
-
def({
|
|
512
|
-
tagName: "link",
|
|
513
|
-
syntaxKind: "inline"
|
|
514
|
-
}),
|
|
515
|
-
def({
|
|
516
|
-
tagName: "inheritDoc",
|
|
517
|
-
syntaxKind: "inline"
|
|
518
|
-
}),
|
|
519
|
-
def({
|
|
520
|
-
tagName: "label",
|
|
521
|
-
syntaxKind: "inline"
|
|
522
|
-
})
|
|
523
|
-
];
|
|
442
|
+
return CSSDOC_TAGS.map((tag) => def({
|
|
443
|
+
tagName: tag.name,
|
|
444
|
+
syntaxKind: tag.kind,
|
|
445
|
+
...tag.recordKind ? { recordKind: tag.recordKind } : {},
|
|
446
|
+
...tag.aliasFor ? { aliasFor: tag.aliasFor } : {},
|
|
447
|
+
...tag.allowMultiple ? { allowMultiple: true } : {}
|
|
448
|
+
}));
|
|
524
449
|
}
|
|
525
450
|
};
|
|
526
451
|
//#endregion
|
|
@@ -532,9 +457,9 @@ var CssDocConfiguration = class CssDocConfiguration {
|
|
|
532
457
|
* Manifest names (`@cssproperty`, `@csspart`, `@cssstate`) where they exist, so the vocabulary is
|
|
533
458
|
* standards-aligned.
|
|
534
459
|
*
|
|
535
|
-
* The grammar is specified formally in `grammar/CssDoc.grammarkdown` (RFC-style,
|
|
536
|
-
* `DeclarationReference.grammarkdown`); the functions here are hand-written to conform
|
|
537
|
-
* productions, and
|
|
460
|
+
* The grammar is specified formally in `@cssdoc/spec`'s `grammar/CssDoc.grammarkdown` (RFC-style,
|
|
461
|
+
* modeling TSDoc's `DeclarationReference.grammarkdown`); the functions here are hand-written to conform
|
|
462
|
+
* to those productions, and a test in `@cssdoc/spec` keeps the spec valid. Which tags are active — and which
|
|
538
463
|
* custom tags to capture — is governed by a {@link CssDocConfiguration}.
|
|
539
464
|
*
|
|
540
465
|
* A block looks like:
|
|
@@ -615,6 +540,7 @@ function parseDocComment(raw, configuration = new CssDocConfiguration()) {
|
|
|
615
540
|
const doc = {
|
|
616
541
|
modifiers: /* @__PURE__ */ new Map(),
|
|
617
542
|
parts: /* @__PURE__ */ new Map(),
|
|
543
|
+
cssParts: /* @__PURE__ */ new Map(),
|
|
618
544
|
cssProperties: [],
|
|
619
545
|
cssStates: /* @__PURE__ */ new Map(),
|
|
620
546
|
slots: /* @__PURE__ */ new Map(),
|
|
@@ -674,9 +600,12 @@ function applyBlockTag(doc, canonical, tagName, rest) {
|
|
|
674
600
|
case "a11y":
|
|
675
601
|
doc.accessibility = rest.trim();
|
|
676
602
|
break;
|
|
677
|
-
case "structure":
|
|
678
|
-
|
|
603
|
+
case "structure": {
|
|
604
|
+
const { description, css } = splitStructureBody(rest);
|
|
605
|
+
doc.structure = parseStructure(css);
|
|
606
|
+
if (description) doc.structureDescription = description;
|
|
679
607
|
break;
|
|
608
|
+
}
|
|
680
609
|
case "modifier": {
|
|
681
610
|
const { head, description } = splitDesc(rest);
|
|
682
611
|
doc.modifiers.set(head.replace(/^\./u, ""), parseModifierBody(description));
|
|
@@ -687,6 +616,11 @@ function applyBlockTag(doc, canonical, tagName, rest) {
|
|
|
687
616
|
doc.parts.set(head.replace(/^\./u, ""), description ?? "");
|
|
688
617
|
break;
|
|
689
618
|
}
|
|
619
|
+
case "csspart": {
|
|
620
|
+
const { head, description } = splitDesc(rest);
|
|
621
|
+
doc.cssParts.set(head.replace(/^\./u, ""), description ?? "");
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
690
624
|
case "cssproperty": {
|
|
691
625
|
const propMatch = rest.match(/^(--[\w-]+)\s*(<[^>]+>)?\s*(?:(?:—|-{1,2})\s*(.*))?$/u);
|
|
692
626
|
if (propMatch) doc.cssProperties.push({
|
|
@@ -772,37 +706,51 @@ function recordNameOf(commentText, configuration) {
|
|
|
772
706
|
return commentText.match(new RegExp(`@(?:${tagNames.join("|")})\\s+(\\S+)`, "u"))?.[1];
|
|
773
707
|
}
|
|
774
708
|
/**
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
709
|
+
* Peel an optional leading prose description off a `@structure` body. The nested CSS begins at the
|
|
710
|
+
* first line that opens a rule (contains `{`); any prose before it is the description. If the body
|
|
711
|
+
* begins with a selector — including a multi-line selector like `.a,\n.b {` — there is no description
|
|
712
|
+
* and the whole body is CSS.
|
|
713
|
+
*
|
|
714
|
+
* @param raw - The `@structure` body (description and/or nested CSS).
|
|
715
|
+
* @returns The split `description` (when present) and the `css` to parse.
|
|
716
|
+
*/
|
|
717
|
+
function splitStructureBody(raw) {
|
|
718
|
+
const lines = raw.split("\n");
|
|
719
|
+
const braceLine = lines.findIndex((l) => l.includes("{"));
|
|
720
|
+
if (braceLine <= 0) return { css: raw };
|
|
721
|
+
const lead = lines.slice(0, braceLine);
|
|
722
|
+
if (/^\s*[.#:[*&>+~]/u.test(lead[0])) return { css: raw };
|
|
723
|
+
const description = lead.join("\n").trim();
|
|
724
|
+
return description ? {
|
|
725
|
+
description,
|
|
726
|
+
css: lines.slice(braceLine).join("\n")
|
|
727
|
+
} : { css: raw };
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Parse a `@structure` body — nested CSS (brace-delimited rules) — into a {@link StructureNode} tree.
|
|
731
|
+
* Each rule's selector becomes a node; nested rules become its children. Because a node is a real
|
|
732
|
+
* compound selector, `:has()` (contains), `:is()` / selector-lists (one-of), and `:not()` (not) express
|
|
733
|
+
* relationships natively. Leaf nodes are written as empty rules (`.tab {}`). A malformed body parses to
|
|
734
|
+
* an empty tree rather than throwing.
|
|
778
735
|
*
|
|
779
736
|
* @example
|
|
780
737
|
* ```
|
|
781
|
-
* .tabs
|
|
782
|
-
* .list
|
|
783
|
-
*
|
|
784
|
-
*
|
|
738
|
+
* .tabs {
|
|
739
|
+
* .list { .tab {} }
|
|
740
|
+
* .panel {}
|
|
741
|
+
* }
|
|
785
742
|
* ```
|
|
786
743
|
*/
|
|
787
744
|
function parseStructure(raw) {
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
};
|
|
797
|
-
while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
798
|
-
if (stack.length) stack[stack.length - 1].node.children.push(node);
|
|
799
|
-
else roots.push(node);
|
|
800
|
-
stack.push({
|
|
801
|
-
indent,
|
|
802
|
-
node
|
|
803
|
-
});
|
|
745
|
+
const build = (nodes) => nodes.filter((n) => n.type === "rule").map((rule) => ({
|
|
746
|
+
selector: rule.selector.trim(),
|
|
747
|
+
children: build(rule.nodes ?? [])
|
|
748
|
+
}));
|
|
749
|
+
try {
|
|
750
|
+
return build(postcss.parse(raw).nodes);
|
|
751
|
+
} catch {
|
|
752
|
+
return [];
|
|
804
753
|
}
|
|
805
|
-
return roots;
|
|
806
754
|
}
|
|
807
755
|
//#endregion
|
|
808
756
|
//#region src/parse.ts
|
|
@@ -908,7 +856,15 @@ function collect(nodes, acc, matcher, baseNoDot, prefixNoDot, inScope) {
|
|
|
908
856
|
}
|
|
909
857
|
if (node.type === "rule") {
|
|
910
858
|
for (const selector of node.selector.split(",")) {
|
|
911
|
-
for (const s of selector.matchAll(/:state\(\s*([\w-]+)\s*\)/gu)) if (!acc.states.has(s[1])) acc.states.set(s[1], {
|
|
859
|
+
for (const s of selector.matchAll(/:state\(\s*([\w-]+)\s*\)/gu)) if (!acc.states.has(s[1])) acc.states.set(s[1], {
|
|
860
|
+
name: s[1],
|
|
861
|
+
kind: "custom"
|
|
862
|
+
});
|
|
863
|
+
for (const ps of matcher.pseudoStatesIn(selector)) if (!acc.states.has(ps.name)) acc.states.set(ps.name, {
|
|
864
|
+
name: ps.name,
|
|
865
|
+
kind: "pseudo-class"
|
|
866
|
+
});
|
|
867
|
+
for (const sp of selector.matchAll(/::part\(\s*([\w-]+)\s*\)/gu)) if (!acc.shadowParts.has(sp[1])) acc.shadowParts.set(sp[1], { name: sp[1] });
|
|
912
868
|
const bare = selector.replace(/::?[\w-]+(\([^)]*\))?/gu, "");
|
|
913
869
|
const mods = matcher.modifiersIn(bare, baseNoDot);
|
|
914
870
|
const modNames = new Set(mods.map((mod) => mod.name));
|
|
@@ -921,6 +877,22 @@ function collect(nodes, acc, matcher, baseNoDot, prefixNoDot, inScope) {
|
|
|
921
877
|
if (pendingCanonical) entry.deprecated = { canonical: pendingCanonical };
|
|
922
878
|
acc.modifiers.set(mod.name, entry);
|
|
923
879
|
}
|
|
880
|
+
for (const el of matcher.elementsIn(bare, baseNoDot)) {
|
|
881
|
+
const part = acc.parts.get(el.name) ?? { name: el.name };
|
|
882
|
+
for (const m of el.modifiers) {
|
|
883
|
+
part.modifiers ??= [];
|
|
884
|
+
if (!part.modifiers.some((x) => x.name === m.name)) part.modifiers.push({
|
|
885
|
+
name: m.name,
|
|
886
|
+
prop: m.prop,
|
|
887
|
+
value: m.value
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
acc.parts.set(el.name, part);
|
|
891
|
+
}
|
|
892
|
+
for (const st of matcher.statesIn(bare, baseNoDot)) if (!acc.states.has(st.name)) acc.states.set(st.name, {
|
|
893
|
+
name: st.name,
|
|
894
|
+
kind: "class"
|
|
895
|
+
});
|
|
924
896
|
if (inScope) for (const m of bare.matchAll(/\.([a-z][\w-]*)/gu)) {
|
|
925
897
|
const part = m[1];
|
|
926
898
|
if (modNames.has(part)) continue;
|
|
@@ -948,6 +920,7 @@ function buildEntry(name, doc, nodes, matcher) {
|
|
|
948
920
|
className,
|
|
949
921
|
modifiers: /* @__PURE__ */ new Map(),
|
|
950
922
|
parts: /* @__PURE__ */ new Map(),
|
|
923
|
+
shadowParts: /* @__PURE__ */ new Map(),
|
|
951
924
|
states: /* @__PURE__ */ new Map(),
|
|
952
925
|
consumed: /* @__PURE__ */ new Set(),
|
|
953
926
|
declared: /* @__PURE__ */ new Map(),
|
|
@@ -988,11 +961,22 @@ function buildEntry(name, doc, nodes, matcher) {
|
|
|
988
961
|
description
|
|
989
962
|
});
|
|
990
963
|
}
|
|
991
|
-
for (const [
|
|
964
|
+
for (const [part, description] of doc.cssParts) {
|
|
965
|
+
const existing = acc.shadowParts.get(part);
|
|
966
|
+
if (existing) existing.description = description || existing.description;
|
|
967
|
+
else acc.shadowParts.set(part, {
|
|
968
|
+
name: part,
|
|
969
|
+
description: description || void 0
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
for (const [rawState, description] of doc.cssStates) {
|
|
973
|
+
const isPseudo = rawState.startsWith(":");
|
|
974
|
+
const state = isPseudo ? rawState.slice(1) : rawState;
|
|
992
975
|
const existing = acc.states.get(state);
|
|
993
976
|
if (existing) existing.description = description || existing.description;
|
|
994
977
|
else acc.states.set(state, {
|
|
995
978
|
name: state,
|
|
979
|
+
kind: isPseudo ? "pseudo-class" : "custom",
|
|
996
980
|
description: description || void 0
|
|
997
981
|
});
|
|
998
982
|
}
|
|
@@ -1053,7 +1037,11 @@ function buildEntry(name, doc, nodes, matcher) {
|
|
|
1053
1037
|
group: doc.group,
|
|
1054
1038
|
accessibility: doc.accessibility,
|
|
1055
1039
|
modifiers,
|
|
1056
|
-
parts: [...acc.parts.values()].sort(byName)
|
|
1040
|
+
parts: [...acc.parts.values()].sort(byName).map((p) => p.modifiers ? {
|
|
1041
|
+
...p,
|
|
1042
|
+
modifiers: [...p.modifiers].sort(byName)
|
|
1043
|
+
} : p),
|
|
1044
|
+
shadowParts: [...acc.shadowParts.values()].sort(byName),
|
|
1057
1045
|
states: [...acc.states.values()].sort(byName),
|
|
1058
1046
|
slots: [...doc.slots].map(([slotName, description]) => ({
|
|
1059
1047
|
name: slotName,
|
|
@@ -1067,6 +1055,7 @@ function buildEntry(name, doc, nodes, matcher) {
|
|
|
1067
1055
|
conditions: acc.conditions,
|
|
1068
1056
|
examples: doc.examples,
|
|
1069
1057
|
structure: doc.structure,
|
|
1058
|
+
structureDescription: doc.structureDescription,
|
|
1070
1059
|
demo: doc.demo,
|
|
1071
1060
|
deprecated: doc.deprecated,
|
|
1072
1061
|
see: doc.see,
|
|
@@ -1094,7 +1083,7 @@ function parseCssDocs(css, options = {}) {
|
|
|
1094
1083
|
const configuration = options.configuration ?? new CssDocConfiguration();
|
|
1095
1084
|
const matcher = new ModifierMatcher(resolveModifierConvention(options.modifierConvention ?? configuration.modifierConvention));
|
|
1096
1085
|
const boundary = options.isRecordBoundary ?? ((text) => recordNameOf(text, configuration));
|
|
1097
|
-
const root = postcss.parse(css);
|
|
1086
|
+
const root = (options.parse ?? postcss.parse)(css);
|
|
1098
1087
|
const records = [];
|
|
1099
1088
|
let current = null;
|
|
1100
1089
|
for (const node of root.nodes) {
|
|
@@ -1162,4 +1151,4 @@ function toJson(model) {
|
|
|
1162
1151
|
return `${JSON.stringify(model, null, 2)}\n`;
|
|
1163
1152
|
}
|
|
1164
1153
|
//#endregion
|
|
1165
|
-
export { CssDocConfiguration, CssDocTagDefinition, DEFAULT_MODIFIER_CONVENTION, MODIFIER_PRESETS, ModifierMatcher, RECORD_TAGS, parseCssDocs, parseDocComment, parseStructure, recordNameOf, resolveModifierConvention, stripCommentFraming, toJson, toMermaid };
|
|
1154
|
+
export { CssDocConfiguration, CssDocTagDefinition, DEFAULT_MODIFIER_CONVENTION, DEFAULT_STATE_PSEUDO_CLASSES, MODIFIER_PRESETS, ModifierMatcher, RECORD_TAGS, parseCssDocs, parseDocComment, parseStructure, recordNameOf, resolveModifierConvention, stripCommentFraming, toJson, toMermaid };
|