@cssdoc/core 0.1.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 +267 -26
- package/dist/index.mjs +465 -315
- package/package.json +4 -5
- package/grammar/CssDoc.grammarkdown +0 -390
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,332 @@
|
|
|
1
1
|
import postcss from "postcss";
|
|
2
|
+
import { CSSDOC_TAGS, CSSDOC_TAG_NAMES } from "@cssdoc/spec";
|
|
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
|
+
];
|
|
26
|
+
/** The built-in convention presets. Other schemes use the custom object (see the docs). */
|
|
27
|
+
const MODIFIER_PRESETS = {
|
|
28
|
+
/** BEM / SUIT — `.button--primary`, with `.button__element` sub-elements. The default. */
|
|
29
|
+
bem: {
|
|
30
|
+
structure: "suffix",
|
|
31
|
+
separator: "--",
|
|
32
|
+
elementSeparator: "__",
|
|
33
|
+
propValue: false
|
|
34
|
+
},
|
|
35
|
+
/** rscss — `.button.-color-secondary`, split into `prop`/`value`. */
|
|
36
|
+
rscss: {
|
|
37
|
+
structure: "chained",
|
|
38
|
+
separator: "-",
|
|
39
|
+
propValue: true,
|
|
40
|
+
propValueSeparator: "-"
|
|
41
|
+
},
|
|
42
|
+
/** OOCSS / bare chained classes — `.button.primary` (any class chained to the base). */
|
|
43
|
+
bare: {
|
|
44
|
+
structure: "chained",
|
|
45
|
+
separator: "",
|
|
46
|
+
propValue: false
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/** The default convention: BEM. */
|
|
50
|
+
const DEFAULT_MODIFIER_CONVENTION = MODIFIER_PRESETS.bem;
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a preset name or custom object into a fully-populated {@link ModifierConvention} (defaults
|
|
53
|
+
* filled in). No argument resolves to the {@link DEFAULT_MODIFIER_CONVENTION} (BEM).
|
|
54
|
+
*
|
|
55
|
+
* @throws If given an unknown preset name.
|
|
56
|
+
*/
|
|
57
|
+
function resolveModifierConvention(input) {
|
|
58
|
+
let base;
|
|
59
|
+
if (input === void 0) base = DEFAULT_MODIFIER_CONVENTION;
|
|
60
|
+
else if (typeof input === "string") base = MODIFIER_PRESETS[input];
|
|
61
|
+
else base = input;
|
|
62
|
+
if (!base) throw new Error(`Unknown modifier convention preset: ${JSON.stringify(input)}`);
|
|
63
|
+
return {
|
|
64
|
+
structure: base.structure,
|
|
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],
|
|
69
|
+
propValue: base.propValue ?? false,
|
|
70
|
+
propValueSeparator: base.propValueSeparator ?? "-"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Escape a string for literal use inside a `RegExp`. */
|
|
74
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
75
|
+
/** Strip one layer of matching quotes from an attribute value. */
|
|
76
|
+
const unquote$1 = (v) => v.trim().replace(/^(["'])([\s\S]*)\1$/u, "$2");
|
|
77
|
+
/**
|
|
78
|
+
* The single owner of modifier recognition for one convention: finds modifiers on selectors, derives
|
|
79
|
+
* `prop`/`value`, renders a modifier name back to a selector fragment, and answers "does this host-doc
|
|
80
|
+
* token look like a modifier usage?". Constructed once per parse from the resolved convention.
|
|
81
|
+
*/
|
|
82
|
+
var ModifierMatcher = class {
|
|
83
|
+
convention;
|
|
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;
|
|
96
|
+
constructor(convention) {
|
|
97
|
+
this.convention = convention;
|
|
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;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Every modifier attached to `baseNoDot` within one selector. `selector` should have its pseudos
|
|
126
|
+
* already dropped (as `parse.ts`/`index.ts` do); the base is given without its leading dot.
|
|
127
|
+
*/
|
|
128
|
+
modifiersIn(selector, baseNoDot) {
|
|
129
|
+
const baseEsc = escapeRe(baseNoDot);
|
|
130
|
+
const hits = [];
|
|
131
|
+
const seen = /* @__PURE__ */ new Set();
|
|
132
|
+
const push = (name) => {
|
|
133
|
+
if (seen.has(name)) return;
|
|
134
|
+
seen.add(name);
|
|
135
|
+
hits.push({
|
|
136
|
+
name,
|
|
137
|
+
...this.analyze(name)
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
if (this.convention.structure === "suffix") {
|
|
141
|
+
const re = new RegExp(`\\.(${baseEsc}${this.sepAlt}[\\w-]+)`, "gu");
|
|
142
|
+
for (const m of selector.matchAll(re)) push(m[1]);
|
|
143
|
+
return hits;
|
|
144
|
+
}
|
|
145
|
+
if (this.convention.structure === "attribute") {
|
|
146
|
+
const chain = new RegExp(`(?:\\.${baseEsc}|:scope)((?:\\[[^\\]]*\\])+)`, "gu");
|
|
147
|
+
for (const m of selector.matchAll(chain)) for (const a of m[1].matchAll(/\[([^\]]*)\]/gu)) {
|
|
148
|
+
const name = this.normalizeAttribute(a[1]);
|
|
149
|
+
if (name && this.attributeMatches(name)) push(name);
|
|
150
|
+
}
|
|
151
|
+
return hits;
|
|
152
|
+
}
|
|
153
|
+
const cls = `\\.${this.sepAlt}[\\w-]+`;
|
|
154
|
+
const chain = new RegExp(`(?:\\.${baseEsc}|:scope)((?:${cls})+)`, "gu");
|
|
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]);
|
|
157
|
+
return hits;
|
|
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
|
+
}
|
|
240
|
+
/** Derive `prop`/`value` for a modifier `name` (as returned by {@link modifiersIn} or authored). */
|
|
241
|
+
analyze(name) {
|
|
242
|
+
if (this.convention.structure === "attribute") {
|
|
243
|
+
const canonical = this.normalizeAttribute(name);
|
|
244
|
+
const eq = canonical.indexOf("=");
|
|
245
|
+
const attr = eq === -1 ? canonical : canonical.slice(0, eq);
|
|
246
|
+
return {
|
|
247
|
+
prop: this.stripPrefix(attr),
|
|
248
|
+
value: eq === -1 ? void 0 : unquote$1(canonical.slice(eq + 1))
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const body = this.convention.structure === "suffix" ? this.stripToBody(name) : this.stripPrefix(name);
|
|
252
|
+
if (!this.convention.propValue) return { prop: body };
|
|
253
|
+
const sep = this.convention.propValueSeparator ?? "-";
|
|
254
|
+
const at = body.indexOf(sep);
|
|
255
|
+
if (at === -1) return { prop: body };
|
|
256
|
+
return {
|
|
257
|
+
prop: body.slice(0, at),
|
|
258
|
+
value: body.slice(at + sep.length)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** Render a modifier `name` as the selector fragment it denotes (`.name` or `[name]`). */
|
|
262
|
+
selectorFor(name) {
|
|
263
|
+
return this.convention.structure === "attribute" ? `[${name}]` : `.${name}`;
|
|
264
|
+
}
|
|
265
|
+
/** Normalize a member token/expression to its canonical `name` key (the inverse of authoring noise). */
|
|
266
|
+
normalizeMember(token) {
|
|
267
|
+
if (this.convention.structure === "attribute") {
|
|
268
|
+
const inner = token.replace(/^\[/u, "").replace(/\]$/u, "");
|
|
269
|
+
return this.normalizeAttribute(inner);
|
|
270
|
+
}
|
|
271
|
+
return token.replace(/^\./u, "");
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Does a token/expression seen in a host document (an HTML class token, or an attribute expression)
|
|
275
|
+
* look like a modifier usage of `baseNoDot`? Replaces the old `startsWith("-")` gate.
|
|
276
|
+
*/
|
|
277
|
+
looksLikeUsage(token, baseNoDot) {
|
|
278
|
+
if (this.convention.structure === "attribute") return this.attributeMatches(this.normalizeMember(token));
|
|
279
|
+
const name = token.replace(/^\./u, "");
|
|
280
|
+
if (this.isStateClass(name)) return false;
|
|
281
|
+
if (this.convention.structure === "suffix") {
|
|
282
|
+
if (baseNoDot) return this.separators.some((s) => name.startsWith(`${baseNoDot}${s}`));
|
|
283
|
+
return this.separators.some((s) => s !== "" && name.includes(s));
|
|
284
|
+
}
|
|
285
|
+
if (baseNoDot && name === baseNoDot) return false;
|
|
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;
|
|
299
|
+
}
|
|
300
|
+
/** Canonicalize an attribute expression (bracket-inner): normalize quotes to double, trim. */
|
|
301
|
+
normalizeAttribute(inner) {
|
|
302
|
+
const trimmed = inner.trim();
|
|
303
|
+
const eq = trimmed.indexOf("=");
|
|
304
|
+
if (eq === -1) return trimmed;
|
|
305
|
+
return `${trimmed.slice(0, eq + 1)}"${unquote$1(trimmed.slice(eq + 1))}"`;
|
|
306
|
+
}
|
|
307
|
+
/** Does an attribute-expression's name carry one of the convention's required prefixes? */
|
|
308
|
+
attributeMatches(name) {
|
|
309
|
+
const eq = name.indexOf("=");
|
|
310
|
+
const attr = (eq === -1 ? name : name.slice(0, eq)).replace(/[~|^$*]$/u, "");
|
|
311
|
+
return attr.length > 0 && this.separators.some((s) => attr.startsWith(s));
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
//#endregion
|
|
2
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
|
+
*/
|
|
3
330
|
const TAG_NAME_RE = /^@?[a-zA-Z][a-zA-Z0-9-]*$/u;
|
|
4
331
|
/** One tag in the vocabulary: its name, kind, and how it may be used. */
|
|
5
332
|
var CssDocTagDefinition = class {
|
|
@@ -47,9 +374,18 @@ var CssDocConfiguration = class CssDocConfiguration {
|
|
|
47
374
|
_tagDefinitions = [];
|
|
48
375
|
_byName = /* @__PURE__ */ new Map();
|
|
49
376
|
_supported = /* @__PURE__ */ new Set();
|
|
377
|
+
_modifierConvention = DEFAULT_MODIFIER_CONVENTION;
|
|
50
378
|
constructor() {
|
|
51
379
|
this.addTagDefinitions(CssDocConfiguration.standardTags(), true);
|
|
52
380
|
}
|
|
381
|
+
/** The resolved modifier convention this configuration parses with (defaults to BEM). */
|
|
382
|
+
get modifierConvention() {
|
|
383
|
+
return this._modifierConvention;
|
|
384
|
+
}
|
|
385
|
+
/** Set the modifier convention from a preset name or a custom {@link ModifierConvention}. */
|
|
386
|
+
setModifierConvention(input) {
|
|
387
|
+
this._modifierConvention = resolveModifierConvention(input);
|
|
388
|
+
}
|
|
53
389
|
/** Every registered tag definition, in registration order. */
|
|
54
390
|
get tagDefinitions() {
|
|
55
391
|
return this._tagDefinitions;
|
|
@@ -99,258 +435,17 @@ var CssDocConfiguration = class CssDocConfiguration {
|
|
|
99
435
|
setNoStandardTags() {
|
|
100
436
|
this.setSupportForTags(CssDocConfiguration.standardTagNames.map((n) => this._byName.get(n)), false);
|
|
101
437
|
}
|
|
102
|
-
/** The names (without `@`) of every standard tag
|
|
103
|
-
static standardTagNames =
|
|
104
|
-
|
|
105
|
-
"name",
|
|
106
|
-
"utility",
|
|
107
|
-
"rule",
|
|
108
|
-
"declaration",
|
|
109
|
-
"class",
|
|
110
|
-
"summary",
|
|
111
|
-
"remarks",
|
|
112
|
-
"privateRemarks",
|
|
113
|
-
"deprecated",
|
|
114
|
-
"example",
|
|
115
|
-
"see",
|
|
116
|
-
"since",
|
|
117
|
-
"group",
|
|
118
|
-
"category",
|
|
119
|
-
"defaultValue",
|
|
120
|
-
"modifier",
|
|
121
|
-
"part",
|
|
122
|
-
"csspart",
|
|
123
|
-
"cssproperty",
|
|
124
|
-
"property",
|
|
125
|
-
"cssstate",
|
|
126
|
-
"slot",
|
|
127
|
-
"function",
|
|
128
|
-
"keyframes",
|
|
129
|
-
"animation",
|
|
130
|
-
"layer",
|
|
131
|
-
"container",
|
|
132
|
-
"supports",
|
|
133
|
-
"media",
|
|
134
|
-
"responsive",
|
|
135
|
-
"a11y",
|
|
136
|
-
"accessibility",
|
|
137
|
-
"structure",
|
|
138
|
-
"demo",
|
|
139
|
-
"alpha",
|
|
140
|
-
"beta",
|
|
141
|
-
"experimental",
|
|
142
|
-
"internal",
|
|
143
|
-
"public",
|
|
144
|
-
"link",
|
|
145
|
-
"inheritDoc",
|
|
146
|
-
"label"
|
|
147
|
-
];
|
|
148
|
-
/** 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. */
|
|
149
441
|
static standardTags() {
|
|
150
|
-
return
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
tagName: "name",
|
|
158
|
-
syntaxKind: "record",
|
|
159
|
-
recordKind: "component"
|
|
160
|
-
}),
|
|
161
|
-
def({
|
|
162
|
-
tagName: "utility",
|
|
163
|
-
syntaxKind: "record",
|
|
164
|
-
recordKind: "utility"
|
|
165
|
-
}),
|
|
166
|
-
def({
|
|
167
|
-
tagName: "rule",
|
|
168
|
-
syntaxKind: "record",
|
|
169
|
-
recordKind: "rule"
|
|
170
|
-
}),
|
|
171
|
-
def({
|
|
172
|
-
tagName: "declaration",
|
|
173
|
-
syntaxKind: "record",
|
|
174
|
-
recordKind: "declaration"
|
|
175
|
-
}),
|
|
176
|
-
def({
|
|
177
|
-
tagName: "class",
|
|
178
|
-
syntaxKind: "block"
|
|
179
|
-
}),
|
|
180
|
-
def({
|
|
181
|
-
tagName: "summary",
|
|
182
|
-
syntaxKind: "block"
|
|
183
|
-
}),
|
|
184
|
-
def({
|
|
185
|
-
tagName: "remarks",
|
|
186
|
-
syntaxKind: "block"
|
|
187
|
-
}),
|
|
188
|
-
def({
|
|
189
|
-
tagName: "privateRemarks",
|
|
190
|
-
syntaxKind: "block"
|
|
191
|
-
}),
|
|
192
|
-
def({
|
|
193
|
-
tagName: "deprecated",
|
|
194
|
-
syntaxKind: "block"
|
|
195
|
-
}),
|
|
196
|
-
def({
|
|
197
|
-
tagName: "example",
|
|
198
|
-
syntaxKind: "block",
|
|
199
|
-
allowMultiple: true
|
|
200
|
-
}),
|
|
201
|
-
def({
|
|
202
|
-
tagName: "see",
|
|
203
|
-
syntaxKind: "block",
|
|
204
|
-
allowMultiple: true
|
|
205
|
-
}),
|
|
206
|
-
def({
|
|
207
|
-
tagName: "since",
|
|
208
|
-
syntaxKind: "block"
|
|
209
|
-
}),
|
|
210
|
-
def({
|
|
211
|
-
tagName: "group",
|
|
212
|
-
syntaxKind: "block"
|
|
213
|
-
}),
|
|
214
|
-
def({
|
|
215
|
-
tagName: "category",
|
|
216
|
-
syntaxKind: "block",
|
|
217
|
-
aliasFor: "group"
|
|
218
|
-
}),
|
|
219
|
-
def({
|
|
220
|
-
tagName: "defaultValue",
|
|
221
|
-
syntaxKind: "block"
|
|
222
|
-
}),
|
|
223
|
-
def({
|
|
224
|
-
tagName: "modifier",
|
|
225
|
-
syntaxKind: "block",
|
|
226
|
-
allowMultiple: true
|
|
227
|
-
}),
|
|
228
|
-
def({
|
|
229
|
-
tagName: "part",
|
|
230
|
-
syntaxKind: "block",
|
|
231
|
-
allowMultiple: true
|
|
232
|
-
}),
|
|
233
|
-
def({
|
|
234
|
-
tagName: "csspart",
|
|
235
|
-
syntaxKind: "block",
|
|
236
|
-
allowMultiple: true,
|
|
237
|
-
aliasFor: "part"
|
|
238
|
-
}),
|
|
239
|
-
def({
|
|
240
|
-
tagName: "cssproperty",
|
|
241
|
-
syntaxKind: "block",
|
|
242
|
-
allowMultiple: true
|
|
243
|
-
}),
|
|
244
|
-
def({
|
|
245
|
-
tagName: "property",
|
|
246
|
-
syntaxKind: "block",
|
|
247
|
-
allowMultiple: true,
|
|
248
|
-
aliasFor: "cssproperty"
|
|
249
|
-
}),
|
|
250
|
-
def({
|
|
251
|
-
tagName: "cssstate",
|
|
252
|
-
syntaxKind: "block",
|
|
253
|
-
allowMultiple: true
|
|
254
|
-
}),
|
|
255
|
-
def({
|
|
256
|
-
tagName: "slot",
|
|
257
|
-
syntaxKind: "block",
|
|
258
|
-
allowMultiple: true
|
|
259
|
-
}),
|
|
260
|
-
def({
|
|
261
|
-
tagName: "function",
|
|
262
|
-
syntaxKind: "block",
|
|
263
|
-
allowMultiple: true
|
|
264
|
-
}),
|
|
265
|
-
def({
|
|
266
|
-
tagName: "keyframes",
|
|
267
|
-
syntaxKind: "block",
|
|
268
|
-
allowMultiple: true
|
|
269
|
-
}),
|
|
270
|
-
def({
|
|
271
|
-
tagName: "animation",
|
|
272
|
-
syntaxKind: "block",
|
|
273
|
-
allowMultiple: true,
|
|
274
|
-
aliasFor: "keyframes"
|
|
275
|
-
}),
|
|
276
|
-
def({
|
|
277
|
-
tagName: "layer",
|
|
278
|
-
syntaxKind: "block",
|
|
279
|
-
allowMultiple: true
|
|
280
|
-
}),
|
|
281
|
-
def({
|
|
282
|
-
tagName: "container",
|
|
283
|
-
syntaxKind: "block",
|
|
284
|
-
allowMultiple: true
|
|
285
|
-
}),
|
|
286
|
-
def({
|
|
287
|
-
tagName: "supports",
|
|
288
|
-
syntaxKind: "block",
|
|
289
|
-
allowMultiple: true
|
|
290
|
-
}),
|
|
291
|
-
def({
|
|
292
|
-
tagName: "media",
|
|
293
|
-
syntaxKind: "block",
|
|
294
|
-
allowMultiple: true
|
|
295
|
-
}),
|
|
296
|
-
def({
|
|
297
|
-
tagName: "responsive",
|
|
298
|
-
syntaxKind: "block",
|
|
299
|
-
allowMultiple: true,
|
|
300
|
-
aliasFor: "media"
|
|
301
|
-
}),
|
|
302
|
-
def({
|
|
303
|
-
tagName: "a11y",
|
|
304
|
-
syntaxKind: "block",
|
|
305
|
-
allowMultiple: true
|
|
306
|
-
}),
|
|
307
|
-
def({
|
|
308
|
-
tagName: "accessibility",
|
|
309
|
-
syntaxKind: "block",
|
|
310
|
-
allowMultiple: true,
|
|
311
|
-
aliasFor: "a11y"
|
|
312
|
-
}),
|
|
313
|
-
def({
|
|
314
|
-
tagName: "structure",
|
|
315
|
-
syntaxKind: "block"
|
|
316
|
-
}),
|
|
317
|
-
def({
|
|
318
|
-
tagName: "demo",
|
|
319
|
-
syntaxKind: "block"
|
|
320
|
-
}),
|
|
321
|
-
def({
|
|
322
|
-
tagName: "alpha",
|
|
323
|
-
syntaxKind: "modifier"
|
|
324
|
-
}),
|
|
325
|
-
def({
|
|
326
|
-
tagName: "beta",
|
|
327
|
-
syntaxKind: "modifier"
|
|
328
|
-
}),
|
|
329
|
-
def({
|
|
330
|
-
tagName: "experimental",
|
|
331
|
-
syntaxKind: "modifier"
|
|
332
|
-
}),
|
|
333
|
-
def({
|
|
334
|
-
tagName: "internal",
|
|
335
|
-
syntaxKind: "modifier"
|
|
336
|
-
}),
|
|
337
|
-
def({
|
|
338
|
-
tagName: "public",
|
|
339
|
-
syntaxKind: "modifier"
|
|
340
|
-
}),
|
|
341
|
-
def({
|
|
342
|
-
tagName: "link",
|
|
343
|
-
syntaxKind: "inline"
|
|
344
|
-
}),
|
|
345
|
-
def({
|
|
346
|
-
tagName: "inheritDoc",
|
|
347
|
-
syntaxKind: "inline"
|
|
348
|
-
}),
|
|
349
|
-
def({
|
|
350
|
-
tagName: "label",
|
|
351
|
-
syntaxKind: "inline"
|
|
352
|
-
})
|
|
353
|
-
];
|
|
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
|
+
}));
|
|
354
449
|
}
|
|
355
450
|
};
|
|
356
451
|
//#endregion
|
|
@@ -362,9 +457,9 @@ var CssDocConfiguration = class CssDocConfiguration {
|
|
|
362
457
|
* Manifest names (`@cssproperty`, `@csspart`, `@cssstate`) where they exist, so the vocabulary is
|
|
363
458
|
* standards-aligned.
|
|
364
459
|
*
|
|
365
|
-
* The grammar is specified formally in `grammar/CssDoc.grammarkdown` (RFC-style,
|
|
366
|
-
* `DeclarationReference.grammarkdown`); the functions here are hand-written to conform
|
|
367
|
-
* 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
|
|
368
463
|
* custom tags to capture — is governed by a {@link CssDocConfiguration}.
|
|
369
464
|
*
|
|
370
465
|
* A block looks like:
|
|
@@ -420,12 +515,13 @@ function parseModifierBody(description) {
|
|
|
420
515
|
const dep = description?.match(/^@deprecated\b\s*([\s\S]*)$/u);
|
|
421
516
|
if (dep) {
|
|
422
517
|
const rawNote = dep[1].trim();
|
|
423
|
-
const link = rawNote.match(/\{@link\s
|
|
518
|
+
const link = rawNote.match(/\{@link\s+(\.?[\w-]+|\[[^\]]*\])\s*\}/u);
|
|
519
|
+
const canonical = link?.[1].replace(/^\./u, "").replace(/^\[/u, "").replace(/\]$/u, "");
|
|
424
520
|
const note = rawNote.replace(/\{@link\s+[^}]*\}/u, "").trim();
|
|
425
521
|
if (!note && !link) return { deprecatedFlag: true };
|
|
426
522
|
return {
|
|
427
523
|
deprecated: note || void 0,
|
|
428
|
-
deprecatedCanonical:
|
|
524
|
+
deprecatedCanonical: canonical
|
|
429
525
|
};
|
|
430
526
|
}
|
|
431
527
|
return { description: description ?? "" };
|
|
@@ -444,6 +540,7 @@ function parseDocComment(raw, configuration = new CssDocConfiguration()) {
|
|
|
444
540
|
const doc = {
|
|
445
541
|
modifiers: /* @__PURE__ */ new Map(),
|
|
446
542
|
parts: /* @__PURE__ */ new Map(),
|
|
543
|
+
cssParts: /* @__PURE__ */ new Map(),
|
|
447
544
|
cssProperties: [],
|
|
448
545
|
cssStates: /* @__PURE__ */ new Map(),
|
|
449
546
|
slots: /* @__PURE__ */ new Map(),
|
|
@@ -503,9 +600,12 @@ function applyBlockTag(doc, canonical, tagName, rest) {
|
|
|
503
600
|
case "a11y":
|
|
504
601
|
doc.accessibility = rest.trim();
|
|
505
602
|
break;
|
|
506
|
-
case "structure":
|
|
507
|
-
|
|
603
|
+
case "structure": {
|
|
604
|
+
const { description, css } = splitStructureBody(rest);
|
|
605
|
+
doc.structure = parseStructure(css);
|
|
606
|
+
if (description) doc.structureDescription = description;
|
|
508
607
|
break;
|
|
608
|
+
}
|
|
509
609
|
case "modifier": {
|
|
510
610
|
const { head, description } = splitDesc(rest);
|
|
511
611
|
doc.modifiers.set(head.replace(/^\./u, ""), parseModifierBody(description));
|
|
@@ -516,6 +616,11 @@ function applyBlockTag(doc, canonical, tagName, rest) {
|
|
|
516
616
|
doc.parts.set(head.replace(/^\./u, ""), description ?? "");
|
|
517
617
|
break;
|
|
518
618
|
}
|
|
619
|
+
case "csspart": {
|
|
620
|
+
const { head, description } = splitDesc(rest);
|
|
621
|
+
doc.cssParts.set(head.replace(/^\./u, ""), description ?? "");
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
519
624
|
case "cssproperty": {
|
|
520
625
|
const propMatch = rest.match(/^(--[\w-]+)\s*(<[^>]+>)?\s*(?:(?:—|-{1,2})\s*(.*))?$/u);
|
|
521
626
|
if (propMatch) doc.cssProperties.push({
|
|
@@ -601,37 +706,51 @@ function recordNameOf(commentText, configuration) {
|
|
|
601
706
|
return commentText.match(new RegExp(`@(?:${tagNames.join("|")})\\s+(\\S+)`, "u"))?.[1];
|
|
602
707
|
}
|
|
603
708
|
/**
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
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.
|
|
607
735
|
*
|
|
608
736
|
* @example
|
|
609
737
|
* ```
|
|
610
|
-
* .tabs
|
|
611
|
-
* .list
|
|
612
|
-
*
|
|
613
|
-
*
|
|
738
|
+
* .tabs {
|
|
739
|
+
* .list { .tab {} }
|
|
740
|
+
* .panel {}
|
|
741
|
+
* }
|
|
614
742
|
* ```
|
|
615
743
|
*/
|
|
616
744
|
function parseStructure(raw) {
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
};
|
|
626
|
-
while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
627
|
-
if (stack.length) stack[stack.length - 1].node.children.push(node);
|
|
628
|
-
else roots.push(node);
|
|
629
|
-
stack.push({
|
|
630
|
-
indent,
|
|
631
|
-
node
|
|
632
|
-
});
|
|
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 [];
|
|
633
753
|
}
|
|
634
|
-
return roots;
|
|
635
754
|
}
|
|
636
755
|
//#endregion
|
|
637
756
|
//#region src/parse.ts
|
|
@@ -649,16 +768,6 @@ function parseStructure(raw) {
|
|
|
649
768
|
/** Matches a `var(--name` reference; group 1 is the custom-property name. */
|
|
650
769
|
const VAR_RE = /var\(\s*(--[\w-]+)/gu;
|
|
651
770
|
const unquote = (value) => value.trim().replace(/^["']|["']$/gu, "");
|
|
652
|
-
/** Split a `-<prop>-<value>` (or boolean `-<flag>`) modifier into its prop/value segments. */
|
|
653
|
-
function splitModifier(name) {
|
|
654
|
-
const body = name.replace(/^-/u, "");
|
|
655
|
-
const dash = body.indexOf("-");
|
|
656
|
-
if (dash === -1) return { prop: body };
|
|
657
|
-
return {
|
|
658
|
-
prop: body.slice(0, dash),
|
|
659
|
-
value: body.slice(dash + 1)
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
771
|
/** Record a conditional-support block, de-duplicating by type + query. */
|
|
663
772
|
function addCondition(acc, condition) {
|
|
664
773
|
if (!acc.conditions.some((c) => c.type === condition.type && c.query === condition.query)) acc.conditions.push(condition);
|
|
@@ -682,13 +791,12 @@ function collectFunction(node, acc) {
|
|
|
682
791
|
});
|
|
683
792
|
}
|
|
684
793
|
/** Extract every fact from one record's nodes into `acc`. */
|
|
685
|
-
function collect(nodes, acc,
|
|
686
|
-
const modRe = new RegExp(`(?:${baseEsc}|:scope)((?:\\.-[\\w-]+)+)`, "gu");
|
|
794
|
+
function collect(nodes, acc, matcher, baseNoDot, prefixNoDot, inScope) {
|
|
687
795
|
let pendingCanonical;
|
|
688
796
|
for (const node of nodes) {
|
|
689
797
|
if (node.type === "comment") {
|
|
690
|
-
const dep = node.text.match(/@deprecated.*?use\s
|
|
691
|
-
if (dep) pendingCanonical = dep[1];
|
|
798
|
+
const dep = node.text.match(/@deprecated.*?use\s+(\.[\w-]+|\[[^\]]*\])/u);
|
|
799
|
+
if (dep) pendingCanonical = matcher.normalizeMember(dep[1]);
|
|
692
800
|
continue;
|
|
693
801
|
}
|
|
694
802
|
if (node.type === "decl") {
|
|
@@ -743,27 +851,51 @@ function collect(nodes, acc, baseEsc, prefixNoDot, inScope) {
|
|
|
743
851
|
type: "media",
|
|
744
852
|
query: node.params.trim()
|
|
745
853
|
});
|
|
746
|
-
if (node.nodes) collect(node.nodes, acc,
|
|
854
|
+
if (node.nodes) collect(node.nodes, acc, matcher, baseNoDot, prefixNoDot, inScope || node.name === "scope");
|
|
747
855
|
continue;
|
|
748
856
|
}
|
|
749
857
|
if (node.type === "rule") {
|
|
750
858
|
for (const selector of node.selector.split(",")) {
|
|
751
|
-
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] });
|
|
752
868
|
const bare = selector.replace(/::?[\w-]+(\([^)]*\))?/gu, "");
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
value
|
|
869
|
+
const mods = matcher.modifiersIn(bare, baseNoDot);
|
|
870
|
+
const modNames = new Set(mods.map((mod) => mod.name));
|
|
871
|
+
for (const mod of mods) {
|
|
872
|
+
const entry = acc.modifiers.get(mod.name) ?? {
|
|
873
|
+
name: mod.name,
|
|
874
|
+
prop: mod.prop,
|
|
875
|
+
value: mod.value
|
|
761
876
|
};
|
|
762
877
|
if (pendingCanonical) entry.deprecated = { canonical: pendingCanonical };
|
|
763
|
-
acc.modifiers.set(
|
|
878
|
+
acc.modifiers.set(mod.name, entry);
|
|
764
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
|
+
});
|
|
765
896
|
if (inScope) for (const m of bare.matchAll(/\.([a-z][\w-]*)/gu)) {
|
|
766
897
|
const part = m[1];
|
|
898
|
+
if (modNames.has(part)) continue;
|
|
767
899
|
if (prefixNoDot && part.startsWith(prefixNoDot)) continue;
|
|
768
900
|
if (!acc.parts.has(part)) acc.parts.set(part, { name: part });
|
|
769
901
|
}
|
|
@@ -775,7 +907,7 @@ function collect(nodes, acc, baseEsc, prefixNoDot, inScope) {
|
|
|
775
907
|
}
|
|
776
908
|
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
777
909
|
/** Build one entry from its record name, doc comment, and nodes. */
|
|
778
|
-
function buildEntry(name, doc, nodes) {
|
|
910
|
+
function buildEntry(name, doc, nodes, matcher) {
|
|
779
911
|
let className = doc.className ?? "";
|
|
780
912
|
if (!className) {
|
|
781
913
|
const bare = nodes.filter((n) => n.type === "rule").map((n) => n.selector.trim()).filter((sel) => /^\.[a-z][\w-]*$/u.test(sel));
|
|
@@ -788,6 +920,7 @@ function buildEntry(name, doc, nodes) {
|
|
|
788
920
|
className,
|
|
789
921
|
modifiers: /* @__PURE__ */ new Map(),
|
|
790
922
|
parts: /* @__PURE__ */ new Map(),
|
|
923
|
+
shadowParts: /* @__PURE__ */ new Map(),
|
|
791
924
|
states: /* @__PURE__ */ new Map(),
|
|
792
925
|
consumed: /* @__PURE__ */ new Set(),
|
|
793
926
|
declared: /* @__PURE__ */ new Map(),
|
|
@@ -796,7 +929,7 @@ function buildEntry(name, doc, nodes) {
|
|
|
796
929
|
layers: /* @__PURE__ */ new Map(),
|
|
797
930
|
conditions: []
|
|
798
931
|
};
|
|
799
|
-
collect(nodes, acc, className.replace(
|
|
932
|
+
collect(nodes, acc, matcher, className.replace(/^\./u, ""), className.endsWith(name) ? className.slice(1, className.length - name.length) : "", false);
|
|
800
933
|
for (const [modName, mdoc] of doc.modifiers) {
|
|
801
934
|
const existing = acc.modifiers.get(modName);
|
|
802
935
|
const dep = mdoc.deprecated || mdoc.deprecatedCanonical || mdoc.deprecatedFlag ? {
|
|
@@ -810,7 +943,7 @@ function buildEntry(name, doc, nodes) {
|
|
|
810
943
|
...dep
|
|
811
944
|
};
|
|
812
945
|
} else {
|
|
813
|
-
const { prop, value } =
|
|
946
|
+
const { prop, value } = matcher.analyze(modName);
|
|
814
947
|
acc.modifiers.set(modName, {
|
|
815
948
|
name: modName,
|
|
816
949
|
prop,
|
|
@@ -828,11 +961,22 @@ function buildEntry(name, doc, nodes) {
|
|
|
828
961
|
description
|
|
829
962
|
});
|
|
830
963
|
}
|
|
831
|
-
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;
|
|
832
975
|
const existing = acc.states.get(state);
|
|
833
976
|
if (existing) existing.description = description || existing.description;
|
|
834
977
|
else acc.states.set(state, {
|
|
835
978
|
name: state,
|
|
979
|
+
kind: isPseudo ? "pseudo-class" : "custom",
|
|
836
980
|
description: description || void 0
|
|
837
981
|
});
|
|
838
982
|
}
|
|
@@ -893,7 +1037,11 @@ function buildEntry(name, doc, nodes) {
|
|
|
893
1037
|
group: doc.group,
|
|
894
1038
|
accessibility: doc.accessibility,
|
|
895
1039
|
modifiers,
|
|
896
|
-
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),
|
|
897
1045
|
states: [...acc.states.values()].sort(byName),
|
|
898
1046
|
slots: [...doc.slots].map(([slotName, description]) => ({
|
|
899
1047
|
name: slotName,
|
|
@@ -907,6 +1055,7 @@ function buildEntry(name, doc, nodes) {
|
|
|
907
1055
|
conditions: acc.conditions,
|
|
908
1056
|
examples: doc.examples,
|
|
909
1057
|
structure: doc.structure,
|
|
1058
|
+
structureDescription: doc.structureDescription,
|
|
910
1059
|
demo: doc.demo,
|
|
911
1060
|
deprecated: doc.deprecated,
|
|
912
1061
|
see: doc.see,
|
|
@@ -932,8 +1081,9 @@ function buildEntry(name, doc, nodes) {
|
|
|
932
1081
|
*/
|
|
933
1082
|
function parseCssDocs(css, options = {}) {
|
|
934
1083
|
const configuration = options.configuration ?? new CssDocConfiguration();
|
|
1084
|
+
const matcher = new ModifierMatcher(resolveModifierConvention(options.modifierConvention ?? configuration.modifierConvention));
|
|
935
1085
|
const boundary = options.isRecordBoundary ?? ((text) => recordNameOf(text, configuration));
|
|
936
|
-
const root = postcss.parse(css);
|
|
1086
|
+
const root = (options.parse ?? postcss.parse)(css);
|
|
937
1087
|
const records = [];
|
|
938
1088
|
let current = null;
|
|
939
1089
|
for (const node of root.nodes) {
|
|
@@ -951,7 +1101,7 @@ function parseCssDocs(css, options = {}) {
|
|
|
951
1101
|
}
|
|
952
1102
|
if (current) current.nodes.push(node);
|
|
953
1103
|
}
|
|
954
|
-
return records.map((r) => buildEntry(r.name, r.doc, r.nodes));
|
|
1104
|
+
return records.map((r) => buildEntry(r.name, r.doc, r.nodes, matcher));
|
|
955
1105
|
}
|
|
956
1106
|
//#endregion
|
|
957
1107
|
//#region src/mermaid.ts
|
|
@@ -1001,4 +1151,4 @@ function toJson(model) {
|
|
|
1001
1151
|
return `${JSON.stringify(model, null, 2)}\n`;
|
|
1002
1152
|
}
|
|
1003
1153
|
//#endregion
|
|
1004
|
-
export { CssDocConfiguration, CssDocTagDefinition, RECORD_TAGS, parseCssDocs, parseDocComment, parseStructure, recordNameOf, 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 };
|