@cssdoc/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/index.d.mts +457 -0
- package/dist/index.mjs +1004 -0
- package/grammar/CssDoc.grammarkdown +390 -0
- package/package.json +52 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
import postcss from "postcss";
|
|
2
|
+
//#region src/configuration.ts
|
|
3
|
+
const TAG_NAME_RE = /^@?[a-zA-Z][a-zA-Z0-9-]*$/u;
|
|
4
|
+
/** One tag in the vocabulary: its name, kind, and how it may be used. */
|
|
5
|
+
var CssDocTagDefinition = class {
|
|
6
|
+
/** The normalized tag name, always with a leading `@` (e.g. `@modifier`). */
|
|
7
|
+
tagName;
|
|
8
|
+
/** The tag name without its leading `@` (e.g. `modifier`). */
|
|
9
|
+
tagNameWithoutAt;
|
|
10
|
+
syntaxKind;
|
|
11
|
+
allowMultiple;
|
|
12
|
+
recordKind;
|
|
13
|
+
/** The canonical tag name (without `@`) this aliases, if any; otherwise its own name. */
|
|
14
|
+
aliasFor;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
const raw = options.tagName.trim();
|
|
17
|
+
if (!TAG_NAME_RE.test(raw)) throw new Error(`Invalid CSS-doc tag name: ${JSON.stringify(options.tagName)}`);
|
|
18
|
+
this.tagNameWithoutAt = raw.replace(/^@/u, "");
|
|
19
|
+
this.tagName = `@${this.tagNameWithoutAt}`;
|
|
20
|
+
this.syntaxKind = options.syntaxKind;
|
|
21
|
+
this.allowMultiple = options.allowMultiple ?? false;
|
|
22
|
+
this.recordKind = options.recordKind;
|
|
23
|
+
this.aliasFor = options.aliasFor;
|
|
24
|
+
}
|
|
25
|
+
/** The tag this definition resolves to when handled — its alias target, or itself. */
|
|
26
|
+
get canonicalName() {
|
|
27
|
+
return this.aliasFor ?? this.tagNameWithoutAt;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function def(options) {
|
|
31
|
+
return new CssDocTagDefinition(options);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A parse configuration: the set of tag definitions plus which are supported. Construct one to get the
|
|
35
|
+
* full standard vocabulary, then add custom tags or disable standard ones.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { CssDocConfiguration, CssDocTagDefinition, parseCssDocs } from "@cssdoc/core";
|
|
40
|
+
*
|
|
41
|
+
* const config = new CssDocConfiguration();
|
|
42
|
+
* config.addTagDefinition(new CssDocTagDefinition({ tagName: "@token", syntaxKind: "block" }), true);
|
|
43
|
+
* const model = parseCssDocs(css, { configuration: config });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
var CssDocConfiguration = class CssDocConfiguration {
|
|
47
|
+
_tagDefinitions = [];
|
|
48
|
+
_byName = /* @__PURE__ */ new Map();
|
|
49
|
+
_supported = /* @__PURE__ */ new Set();
|
|
50
|
+
constructor() {
|
|
51
|
+
this.addTagDefinitions(CssDocConfiguration.standardTags(), true);
|
|
52
|
+
}
|
|
53
|
+
/** Every registered tag definition, in registration order. */
|
|
54
|
+
get tagDefinitions() {
|
|
55
|
+
return this._tagDefinitions;
|
|
56
|
+
}
|
|
57
|
+
/** Only the tag definitions that are currently supported. */
|
|
58
|
+
get supportedTagDefinitions() {
|
|
59
|
+
return this._tagDefinitions.filter((d) => this._supported.has(d));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Register a tag definition. Re-registering a tag name replaces the earlier definition.
|
|
63
|
+
*
|
|
64
|
+
* @param definition - The tag to add.
|
|
65
|
+
* @param supported - Whether it is supported (defaults to `true`).
|
|
66
|
+
*/
|
|
67
|
+
addTagDefinition(definition, supported = true) {
|
|
68
|
+
const existing = this._byName.get(definition.tagNameWithoutAt);
|
|
69
|
+
if (existing) {
|
|
70
|
+
this._tagDefinitions.splice(this._tagDefinitions.indexOf(existing), 1);
|
|
71
|
+
this._supported.delete(existing);
|
|
72
|
+
}
|
|
73
|
+
this._tagDefinitions.push(definition);
|
|
74
|
+
this._byName.set(definition.tagNameWithoutAt, definition);
|
|
75
|
+
if (supported) this._supported.add(definition);
|
|
76
|
+
}
|
|
77
|
+
/** Register several tag definitions. */
|
|
78
|
+
addTagDefinitions(definitions, supported = true) {
|
|
79
|
+
for (const definition of definitions) this.addTagDefinition(definition, supported);
|
|
80
|
+
}
|
|
81
|
+
/** Look up a tag definition by name (with or without a leading `@`). */
|
|
82
|
+
tryGetTagDefinition(tagName) {
|
|
83
|
+
return this._byName.get(tagName.replace(/^@/u, ""));
|
|
84
|
+
}
|
|
85
|
+
/** Whether a tag definition is supported. */
|
|
86
|
+
isTagSupported(definition) {
|
|
87
|
+
return this._supported.has(definition);
|
|
88
|
+
}
|
|
89
|
+
/** Enable or disable support for a tag. */
|
|
90
|
+
setSupportForTag(definition, supported) {
|
|
91
|
+
if (supported) this._supported.add(definition);
|
|
92
|
+
else this._supported.delete(definition);
|
|
93
|
+
}
|
|
94
|
+
/** Enable or disable support for several tags. */
|
|
95
|
+
setSupportForTags(definitions, supported) {
|
|
96
|
+
for (const definition of definitions) this.setSupportForTag(definition, supported);
|
|
97
|
+
}
|
|
98
|
+
/** Disable support for every standard tag (custom tags added later remain supported). */
|
|
99
|
+
setNoStandardTags() {
|
|
100
|
+
this.setSupportForTags(CssDocConfiguration.standardTagNames.map((n) => this._byName.get(n)), false);
|
|
101
|
+
}
|
|
102
|
+
/** The names (without `@`) of every standard tag. */
|
|
103
|
+
static standardTagNames = [
|
|
104
|
+
"component",
|
|
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). */
|
|
149
|
+
static standardTags() {
|
|
150
|
+
return [
|
|
151
|
+
def({
|
|
152
|
+
tagName: "component",
|
|
153
|
+
syntaxKind: "record",
|
|
154
|
+
recordKind: "component"
|
|
155
|
+
}),
|
|
156
|
+
def({
|
|
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
|
+
];
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/grammar.ts
|
|
358
|
+
/**
|
|
359
|
+
* The CSS doc-comment grammar — an expansive, TSDoc-shaped tag vocabulary parsed out of `/** … *\/`
|
|
360
|
+
* block comments. The tags describe a CSS surface (a component's base class, modifiers, parts, custom
|
|
361
|
+
* properties, functions, animations, layers, conditions, states, slots) and adopt the Custom Elements
|
|
362
|
+
* Manifest names (`@cssproperty`, `@csspart`, `@cssstate`) where they exist, so the vocabulary is
|
|
363
|
+
* standards-aligned.
|
|
364
|
+
*
|
|
365
|
+
* The grammar is specified formally in `grammar/CssDoc.grammarkdown` (RFC-style, modeling TSDoc's
|
|
366
|
+
* `DeclarationReference.grammarkdown`); the functions here are hand-written to conform to those
|
|
367
|
+
* productions, and `tests/grammar.test.ts` keeps the spec valid. Which tags are active — and which
|
|
368
|
+
* custom tags to capture — is governed by a {@link CssDocConfiguration}.
|
|
369
|
+
*
|
|
370
|
+
* A block looks like:
|
|
371
|
+
* ```css
|
|
372
|
+
* /**
|
|
373
|
+
* * @component button
|
|
374
|
+
* * @summary The primary action control.
|
|
375
|
+
* * @modifier -color-secondary — A lower-emphasis action.
|
|
376
|
+
* * @part .icon — A leading glyph.
|
|
377
|
+
* * @cssproperty --value <number> — The 0–100 fill.
|
|
378
|
+
* * @demo self:button
|
|
379
|
+
* *\/
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* @module
|
|
383
|
+
*/
|
|
384
|
+
/**
|
|
385
|
+
* The record-opening tags and the {@link CssRecordKind} each selects, as the default boundary map.
|
|
386
|
+
* A doc comment carrying one of these opens a new record; `@name` is an alias for `@component`. A
|
|
387
|
+
* {@link CssDocConfiguration} may add more record tags.
|
|
388
|
+
*/
|
|
389
|
+
const RECORD_TAGS = {
|
|
390
|
+
component: "component",
|
|
391
|
+
name: "component",
|
|
392
|
+
utility: "utility",
|
|
393
|
+
rule: "rule",
|
|
394
|
+
declaration: "declaration"
|
|
395
|
+
};
|
|
396
|
+
/** Split a tag's argument into `head` (a selector/name/token) and `description` on ` — ` or ` - `. */
|
|
397
|
+
function splitDesc(rest) {
|
|
398
|
+
const m = rest.match(/^(\S+)\s+(?:—|-{1,2})\s+(.*)$/u);
|
|
399
|
+
if (m) return {
|
|
400
|
+
head: m[1],
|
|
401
|
+
description: m[2].trim()
|
|
402
|
+
};
|
|
403
|
+
return { head: rest.trim() || rest };
|
|
404
|
+
}
|
|
405
|
+
/** Split any argument into `query` (everything before the first ` — `/` - `) and `description`. */
|
|
406
|
+
function splitQuery(rest) {
|
|
407
|
+
const m = rest.match(/^([\s\S]*?)\s+(?:—|-{1,2})\s+([\s\S]*)$/u);
|
|
408
|
+
if (m) return {
|
|
409
|
+
query: m[1].trim(),
|
|
410
|
+
description: m[2].trim()
|
|
411
|
+
};
|
|
412
|
+
return { query: rest.trim() };
|
|
413
|
+
}
|
|
414
|
+
/** Strip the comment framing (`/**`, `*\/`, and leading ` * `) from a raw block-comment body. */
|
|
415
|
+
function stripCommentFraming(raw) {
|
|
416
|
+
return raw.replace(/^\/\*\*?/, "").replace(/\*\/$/, "").split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n").trim();
|
|
417
|
+
}
|
|
418
|
+
/** Parse the inner body of a `@modifier` line's argument into a {@link DocModifier}. */
|
|
419
|
+
function parseModifierBody(description) {
|
|
420
|
+
const dep = description?.match(/^@deprecated\b\s*([\s\S]*)$/u);
|
|
421
|
+
if (dep) {
|
|
422
|
+
const rawNote = dep[1].trim();
|
|
423
|
+
const link = rawNote.match(/\{@link\s+\.?(-[\w-]+)\s*\}/u);
|
|
424
|
+
const note = rawNote.replace(/\{@link\s+[^}]*\}/u, "").trim();
|
|
425
|
+
if (!note && !link) return { deprecatedFlag: true };
|
|
426
|
+
return {
|
|
427
|
+
deprecated: note || void 0,
|
|
428
|
+
deprecatedCanonical: link?.[1]
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return { description: description ?? "" };
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Parse a doc-comment's INNER text (already stripped of `/* *\/` framing, or a raw block — both are
|
|
435
|
+
* handled) into a {@link ParsedDoc}. The `configuration` decides which tags are active and which custom
|
|
436
|
+
* tags to capture; unknown or unsupported tags are ignored, so the grammar degrades gracefully.
|
|
437
|
+
*
|
|
438
|
+
* @param raw - The comment text (with or without `/** … *\/` framing).
|
|
439
|
+
* @param configuration - The active tag configuration (defaults to the full standard vocabulary).
|
|
440
|
+
* @returns The structured tags.
|
|
441
|
+
*/
|
|
442
|
+
function parseDocComment(raw, configuration = new CssDocConfiguration()) {
|
|
443
|
+
const body = stripCommentFraming(raw);
|
|
444
|
+
const doc = {
|
|
445
|
+
modifiers: /* @__PURE__ */ new Map(),
|
|
446
|
+
parts: /* @__PURE__ */ new Map(),
|
|
447
|
+
cssProperties: [],
|
|
448
|
+
cssStates: /* @__PURE__ */ new Map(),
|
|
449
|
+
slots: /* @__PURE__ */ new Map(),
|
|
450
|
+
functions: /* @__PURE__ */ new Map(),
|
|
451
|
+
animations: /* @__PURE__ */ new Map(),
|
|
452
|
+
layers: /* @__PURE__ */ new Map(),
|
|
453
|
+
conditions: [],
|
|
454
|
+
examples: [],
|
|
455
|
+
see: [],
|
|
456
|
+
customBlocks: /* @__PURE__ */ new Map()
|
|
457
|
+
};
|
|
458
|
+
const blocks = [];
|
|
459
|
+
for (const line of body.split("\n")) if (/^\s*@[a-zA-Z]/u.test(line)) blocks.push(line.trim());
|
|
460
|
+
else if (blocks.length) blocks[blocks.length - 1] += `\n${line}`;
|
|
461
|
+
for (const block of blocks) {
|
|
462
|
+
const m = block.match(/^@([a-zA-Z][\w-]*)\s*([\s\S]*)$/u);
|
|
463
|
+
if (!m) continue;
|
|
464
|
+
const tagName = m[1];
|
|
465
|
+
const rest = m[2].trim();
|
|
466
|
+
const definition = configuration.tryGetTagDefinition(tagName);
|
|
467
|
+
if (!definition || !configuration.isTagSupported(definition)) continue;
|
|
468
|
+
if (definition.syntaxKind === "record") {
|
|
469
|
+
doc.component = rest.split(/\s/u)[0];
|
|
470
|
+
doc.kind = definition.recordKind;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (definition.syntaxKind === "modifier") {
|
|
474
|
+
doc.releaseStage = definition.canonicalName;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (definition.syntaxKind === "inline") continue;
|
|
478
|
+
applyBlockTag(doc, definition.canonicalName, definition.tagNameWithoutAt, rest);
|
|
479
|
+
}
|
|
480
|
+
return doc;
|
|
481
|
+
}
|
|
482
|
+
/** Apply one supported block tag (resolved to its canonical name) to the accumulating {@link ParsedDoc}. */
|
|
483
|
+
function applyBlockTag(doc, canonical, tagName, rest) {
|
|
484
|
+
switch (canonical) {
|
|
485
|
+
case "class":
|
|
486
|
+
doc.className = rest.split(/\s/u)[0];
|
|
487
|
+
break;
|
|
488
|
+
case "summary":
|
|
489
|
+
doc.summary = rest.replace(/\s+/gu, " ").trim();
|
|
490
|
+
break;
|
|
491
|
+
case "remarks":
|
|
492
|
+
doc.remarks = rest.trim();
|
|
493
|
+
break;
|
|
494
|
+
case "privateRemarks":
|
|
495
|
+
doc.privateRemarks = rest.trim();
|
|
496
|
+
break;
|
|
497
|
+
case "since":
|
|
498
|
+
doc.since = rest.trim();
|
|
499
|
+
break;
|
|
500
|
+
case "group":
|
|
501
|
+
doc.group = rest.trim();
|
|
502
|
+
break;
|
|
503
|
+
case "a11y":
|
|
504
|
+
doc.accessibility = rest.trim();
|
|
505
|
+
break;
|
|
506
|
+
case "structure":
|
|
507
|
+
doc.structure = parseStructure(rest);
|
|
508
|
+
break;
|
|
509
|
+
case "modifier": {
|
|
510
|
+
const { head, description } = splitDesc(rest);
|
|
511
|
+
doc.modifiers.set(head.replace(/^\./u, ""), parseModifierBody(description));
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
case "part": {
|
|
515
|
+
const { head, description } = splitDesc(rest);
|
|
516
|
+
doc.parts.set(head.replace(/^\./u, ""), description ?? "");
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case "cssproperty": {
|
|
520
|
+
const propMatch = rest.match(/^(--[\w-]+)\s*(<[^>]+>)?\s*(?:(?:—|-{1,2})\s*(.*))?$/u);
|
|
521
|
+
if (propMatch) doc.cssProperties.push({
|
|
522
|
+
name: propMatch[1],
|
|
523
|
+
syntax: propMatch[2],
|
|
524
|
+
description: propMatch[3]?.trim() || void 0
|
|
525
|
+
});
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
case "defaultValue": {
|
|
529
|
+
const last = doc.cssProperties.at(-1);
|
|
530
|
+
if (last) last.defaultValue = rest.trim();
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
case "cssstate": {
|
|
534
|
+
const { head, description } = splitDesc(rest);
|
|
535
|
+
doc.cssStates.set(head, description ?? "");
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
case "slot": {
|
|
539
|
+
const { head, description } = splitDesc(rest);
|
|
540
|
+
doc.slots.set(head.replace(/^\./u, ""), description ?? "");
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
case "function": {
|
|
544
|
+
const nameMatch = rest.match(/^(--[\w-]+)/u);
|
|
545
|
+
const { description } = splitDesc(rest);
|
|
546
|
+
if (nameMatch) doc.functions.set(nameMatch[1], description ?? "");
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
case "keyframes": {
|
|
550
|
+
const { head, description } = splitDesc(rest);
|
|
551
|
+
doc.animations.set(head, description ?? "");
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case "layer": {
|
|
555
|
+
const { head, description } = splitDesc(rest);
|
|
556
|
+
doc.layers.set(head, description ?? "");
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
case "container":
|
|
560
|
+
case "supports":
|
|
561
|
+
case "media": {
|
|
562
|
+
const { query, description } = splitQuery(rest);
|
|
563
|
+
doc.conditions.push({
|
|
564
|
+
type: canonical,
|
|
565
|
+
query,
|
|
566
|
+
description
|
|
567
|
+
});
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case "example":
|
|
571
|
+
doc.examples.push(rest);
|
|
572
|
+
break;
|
|
573
|
+
case "deprecated":
|
|
574
|
+
doc.deprecated = rest;
|
|
575
|
+
break;
|
|
576
|
+
case "demo":
|
|
577
|
+
doc.demo = rest.split(/\s/u)[0];
|
|
578
|
+
break;
|
|
579
|
+
case "see":
|
|
580
|
+
doc.see.push(rest);
|
|
581
|
+
break;
|
|
582
|
+
default: {
|
|
583
|
+
const list = doc.customBlocks.get(tagName) ?? [];
|
|
584
|
+
list.push(rest);
|
|
585
|
+
doc.customBlocks.set(tagName, list);
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Whether a comment's text opens a record — i.e. carries a record tag. Uses the `configuration`'s
|
|
592
|
+
* record tags when given, else the default {@link RECORD_TAGS}.
|
|
593
|
+
*
|
|
594
|
+
* @param commentText - The comment body.
|
|
595
|
+
* @param configuration - The active configuration (optional).
|
|
596
|
+
* @returns The record name, or `undefined`.
|
|
597
|
+
*/
|
|
598
|
+
function recordNameOf(commentText, configuration) {
|
|
599
|
+
const tagNames = configuration ? configuration.supportedTagDefinitions.filter((d) => d.syntaxKind === "record").map((d) => d.tagNameWithoutAt) : Object.keys(RECORD_TAGS);
|
|
600
|
+
if (tagNames.length === 0) return void 0;
|
|
601
|
+
return commentText.match(new RegExp(`@(?:${tagNames.join("|")})\\s+(\\S+)`, "u"))?.[1];
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Parse a `@structure` body — an indentation-nested list of element selectors — into a
|
|
605
|
+
* {@link StructureNode} tree. Indentation depth (any consistent width) sets nesting; blank lines are
|
|
606
|
+
* ignored.
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* ```
|
|
610
|
+
* .tabs
|
|
611
|
+
* .list
|
|
612
|
+
* .tab
|
|
613
|
+
* .panel
|
|
614
|
+
* ```
|
|
615
|
+
*/
|
|
616
|
+
function parseStructure(raw) {
|
|
617
|
+
const roots = [];
|
|
618
|
+
const stack = [];
|
|
619
|
+
for (const line of raw.split("\n")) {
|
|
620
|
+
if (!line.trim()) continue;
|
|
621
|
+
const indent = line.length - line.trimStart().length;
|
|
622
|
+
const node = {
|
|
623
|
+
selector: line.trim(),
|
|
624
|
+
children: []
|
|
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
|
+
});
|
|
633
|
+
}
|
|
634
|
+
return roots;
|
|
635
|
+
}
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region src/parse.ts
|
|
638
|
+
/**
|
|
639
|
+
* The PostCSS-based parser: turn a CSS string into a {@link CssDocEntry}[] documentation model.
|
|
640
|
+
*
|
|
641
|
+
* It is AST-first — the machine facts (base class, modifiers, parts, consumed/declared custom
|
|
642
|
+
* properties, custom functions, animations, cascade layers, conditional-support blocks, states, and
|
|
643
|
+
* deprecated-alias links) are derived from the actual selectors and at-rules, so they can't drift from
|
|
644
|
+
* the shipping CSS. Authored `/** … *\/` doc comments (parsed by {@link parseDocComment}) supply only
|
|
645
|
+
* prose (summaries, descriptions) and demo/see links, and delimit one component from the next.
|
|
646
|
+
*
|
|
647
|
+
* @module
|
|
648
|
+
*/
|
|
649
|
+
/** Matches a `var(--name` reference; group 1 is the custom-property name. */
|
|
650
|
+
const VAR_RE = /var\(\s*(--[\w-]+)/gu;
|
|
651
|
+
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
|
+
/** Record a conditional-support block, de-duplicating by type + query. */
|
|
663
|
+
function addCondition(acc, condition) {
|
|
664
|
+
if (!acc.conditions.some((c) => c.type === condition.type && c.query === condition.query)) acc.conditions.push(condition);
|
|
665
|
+
}
|
|
666
|
+
/** Derive a CSS custom function from an `@function` at-rule. */
|
|
667
|
+
function collectFunction(node, acc) {
|
|
668
|
+
const params = node.params.trim();
|
|
669
|
+
const nameMatch = params.match(/^(--[\w-]+)/u);
|
|
670
|
+
if (!nameMatch) return;
|
|
671
|
+
const paren = params.match(/\(([^)]*)\)/u);
|
|
672
|
+
const parameters = paren ? [...paren[1].matchAll(/(--[\w-]+)/gu)].map((m) => m[1]) : [];
|
|
673
|
+
let result = params.match(/\breturns\b\s+(.+)$/iu)?.[1]?.trim();
|
|
674
|
+
if (!result) {
|
|
675
|
+
const resultDecl = node.nodes?.find((n) => n.type === "decl" && n.prop === "result");
|
|
676
|
+
if (resultDecl && "value" in resultDecl) result = resultDecl.value.trim();
|
|
677
|
+
}
|
|
678
|
+
acc.functions.set(nameMatch[1], {
|
|
679
|
+
name: nameMatch[1],
|
|
680
|
+
parameters,
|
|
681
|
+
result
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/** Extract every fact from one record's nodes into `acc`. */
|
|
685
|
+
function collect(nodes, acc, baseEsc, prefixNoDot, inScope) {
|
|
686
|
+
const modRe = new RegExp(`(?:${baseEsc}|:scope)((?:\\.-[\\w-]+)+)`, "gu");
|
|
687
|
+
let pendingCanonical;
|
|
688
|
+
for (const node of nodes) {
|
|
689
|
+
if (node.type === "comment") {
|
|
690
|
+
const dep = node.text.match(/@deprecated.*?use\s+\.(-[\w-]+)/u);
|
|
691
|
+
if (dep) pendingCanonical = dep[1];
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (node.type === "decl") {
|
|
695
|
+
for (const m of node.value.matchAll(VAR_RE)) acc.consumed.add(m[1]);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (node.type === "atrule") {
|
|
699
|
+
if (node.name === "property") {
|
|
700
|
+
const name = node.params.trim();
|
|
701
|
+
const decl = (prop) => {
|
|
702
|
+
const d = node.nodes?.find((n) => n.type === "decl" && n.prop === prop);
|
|
703
|
+
return d && "value" in d ? d.value : void 0;
|
|
704
|
+
};
|
|
705
|
+
const inherits = decl("inherits");
|
|
706
|
+
const initial = decl("initial-value");
|
|
707
|
+
const syntax = decl("syntax");
|
|
708
|
+
acc.declared.set(name, {
|
|
709
|
+
name,
|
|
710
|
+
syntax: syntax === void 0 ? void 0 : unquote(syntax),
|
|
711
|
+
inherits: inherits === void 0 ? void 0 : /^true$/iu.test(inherits.trim()),
|
|
712
|
+
defaultValue: initial === void 0 ? void 0 : initial.trim()
|
|
713
|
+
});
|
|
714
|
+
} else if (node.name === "function") collectFunction(node, acc);
|
|
715
|
+
else if (node.name === "keyframes") {
|
|
716
|
+
const animName = node.params.trim();
|
|
717
|
+
if (animName) acc.animations.set(animName, { name: animName });
|
|
718
|
+
} else if (node.name === "layer") for (const raw of node.params.split(",")) {
|
|
719
|
+
const layerName = raw.trim();
|
|
720
|
+
if (layerName) acc.layers.set(layerName, { name: layerName });
|
|
721
|
+
}
|
|
722
|
+
else if (node.name === "container") {
|
|
723
|
+
const params = node.params.trim();
|
|
724
|
+
let containerName;
|
|
725
|
+
let query = params;
|
|
726
|
+
if (params && !params.startsWith("(")) {
|
|
727
|
+
const sp = params.indexOf(" ");
|
|
728
|
+
if (sp > 0) {
|
|
729
|
+
containerName = params.slice(0, sp);
|
|
730
|
+
query = params.slice(sp + 1).trim();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
addCondition(acc, {
|
|
734
|
+
type: "container",
|
|
735
|
+
query,
|
|
736
|
+
containerName
|
|
737
|
+
});
|
|
738
|
+
} else if (node.name === "supports") addCondition(acc, {
|
|
739
|
+
type: "supports",
|
|
740
|
+
query: node.params.trim()
|
|
741
|
+
});
|
|
742
|
+
else if (node.name === "media") addCondition(acc, {
|
|
743
|
+
type: "media",
|
|
744
|
+
query: node.params.trim()
|
|
745
|
+
});
|
|
746
|
+
if (node.nodes) collect(node.nodes, acc, baseEsc, prefixNoDot, inScope || node.name === "scope");
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (node.type === "rule") {
|
|
750
|
+
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], { name: s[1] });
|
|
752
|
+
const bare = selector.replace(/::?[\w-]+(\([^)]*\))?/gu, "");
|
|
753
|
+
for (const m of bare.matchAll(modRe)) for (const mod of m[1].matchAll(/\.(-[\w-]+)/gu)) {
|
|
754
|
+
const modName = mod[1];
|
|
755
|
+
const existing = acc.modifiers.get(modName);
|
|
756
|
+
const { prop, value } = splitModifier(modName);
|
|
757
|
+
const entry = existing ?? {
|
|
758
|
+
name: modName,
|
|
759
|
+
prop,
|
|
760
|
+
value
|
|
761
|
+
};
|
|
762
|
+
if (pendingCanonical) entry.deprecated = { canonical: pendingCanonical };
|
|
763
|
+
acc.modifiers.set(modName, entry);
|
|
764
|
+
}
|
|
765
|
+
if (inScope) for (const m of bare.matchAll(/\.([a-z][\w-]*)/gu)) {
|
|
766
|
+
const part = m[1];
|
|
767
|
+
if (prefixNoDot && part.startsWith(prefixNoDot)) continue;
|
|
768
|
+
if (!acc.parts.has(part)) acc.parts.set(part, { name: part });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
for (const child of node.nodes ?? []) if (child.type === "decl") for (const m of child.value.matchAll(VAR_RE)) acc.consumed.add(m[1]);
|
|
772
|
+
pendingCanonical = void 0;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
777
|
+
/** Build one entry from its record name, doc comment, and nodes. */
|
|
778
|
+
function buildEntry(name, doc, nodes) {
|
|
779
|
+
let className = doc.className ?? "";
|
|
780
|
+
if (!className) {
|
|
781
|
+
const bare = nodes.filter((n) => n.type === "rule").map((n) => n.selector.trim()).filter((sel) => /^\.[a-z][\w-]*$/u.test(sel));
|
|
782
|
+
const nameEsc = name.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
783
|
+
const endsWithName = new RegExp(`(?:^|-)${nameEsc}$`, "u");
|
|
784
|
+
className = bare.find((sel) => endsWithName.test(sel.slice(1))) ?? bare[0] ?? "";
|
|
785
|
+
}
|
|
786
|
+
if (!className) className = `.${name}`;
|
|
787
|
+
const acc = {
|
|
788
|
+
className,
|
|
789
|
+
modifiers: /* @__PURE__ */ new Map(),
|
|
790
|
+
parts: /* @__PURE__ */ new Map(),
|
|
791
|
+
states: /* @__PURE__ */ new Map(),
|
|
792
|
+
consumed: /* @__PURE__ */ new Set(),
|
|
793
|
+
declared: /* @__PURE__ */ new Map(),
|
|
794
|
+
functions: /* @__PURE__ */ new Map(),
|
|
795
|
+
animations: /* @__PURE__ */ new Map(),
|
|
796
|
+
layers: /* @__PURE__ */ new Map(),
|
|
797
|
+
conditions: []
|
|
798
|
+
};
|
|
799
|
+
collect(nodes, acc, className.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"), className.endsWith(name) ? className.slice(1, className.length - name.length) : "", false);
|
|
800
|
+
for (const [modName, mdoc] of doc.modifiers) {
|
|
801
|
+
const existing = acc.modifiers.get(modName);
|
|
802
|
+
const dep = mdoc.deprecated || mdoc.deprecatedCanonical || mdoc.deprecatedFlag ? {
|
|
803
|
+
...mdoc.deprecated ? { note: mdoc.deprecated } : {},
|
|
804
|
+
...mdoc.deprecatedCanonical ? { canonical: mdoc.deprecatedCanonical } : {}
|
|
805
|
+
} : void 0;
|
|
806
|
+
if (existing) {
|
|
807
|
+
if (mdoc.description) existing.description = mdoc.description;
|
|
808
|
+
if (dep) existing.deprecated = {
|
|
809
|
+
...existing.deprecated,
|
|
810
|
+
...dep
|
|
811
|
+
};
|
|
812
|
+
} else {
|
|
813
|
+
const { prop, value } = splitModifier(modName);
|
|
814
|
+
acc.modifiers.set(modName, {
|
|
815
|
+
name: modName,
|
|
816
|
+
prop,
|
|
817
|
+
value,
|
|
818
|
+
description: mdoc.description,
|
|
819
|
+
deprecated: dep
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
for (const [part, description] of doc.parts) {
|
|
824
|
+
const existing = acc.parts.get(part);
|
|
825
|
+
if (existing) existing.description = description || existing.description;
|
|
826
|
+
else acc.parts.set(part, {
|
|
827
|
+
name: part,
|
|
828
|
+
description
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
for (const [state, description] of doc.cssStates) {
|
|
832
|
+
const existing = acc.states.get(state);
|
|
833
|
+
if (existing) existing.description = description || existing.description;
|
|
834
|
+
else acc.states.set(state, {
|
|
835
|
+
name: state,
|
|
836
|
+
description: description || void 0
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
for (const prop of doc.cssProperties) {
|
|
840
|
+
const existing = acc.declared.get(prop.name);
|
|
841
|
+
acc.declared.set(prop.name, {
|
|
842
|
+
name: prop.name,
|
|
843
|
+
syntax: prop.syntax ?? existing?.syntax,
|
|
844
|
+
inherits: existing?.inherits,
|
|
845
|
+
defaultValue: prop.defaultValue ?? existing?.defaultValue,
|
|
846
|
+
description: prop.description ?? existing?.description
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
for (const [fnName, description] of doc.functions) {
|
|
850
|
+
const existing = acc.functions.get(fnName);
|
|
851
|
+
if (existing) existing.description = description || existing.description;
|
|
852
|
+
else acc.functions.set(fnName, {
|
|
853
|
+
name: fnName,
|
|
854
|
+
parameters: [],
|
|
855
|
+
description: description || void 0
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
for (const [animName, description] of doc.animations) {
|
|
859
|
+
const existing = acc.animations.get(animName);
|
|
860
|
+
if (existing) existing.description = description || existing.description;
|
|
861
|
+
else acc.animations.set(animName, {
|
|
862
|
+
name: animName,
|
|
863
|
+
description: description || void 0
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
for (const [layerName, description] of doc.layers) {
|
|
867
|
+
const existing = acc.layers.get(layerName);
|
|
868
|
+
if (existing) existing.description = description || existing.description;
|
|
869
|
+
else acc.layers.set(layerName, {
|
|
870
|
+
name: layerName,
|
|
871
|
+
description: description || void 0
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
for (const cond of doc.conditions) {
|
|
875
|
+
const existing = acc.conditions.find((c) => c.type === cond.type && c.query === cond.query);
|
|
876
|
+
if (existing) existing.description = cond.description || existing.description;
|
|
877
|
+
else acc.conditions.push({
|
|
878
|
+
type: cond.type,
|
|
879
|
+
query: cond.query,
|
|
880
|
+
description: cond.description
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
const modifiers = [...acc.modifiers.values()].sort((a, b) => a.prop.localeCompare(b.prop) || (a.value ?? "").localeCompare(b.value ?? ""));
|
|
884
|
+
return {
|
|
885
|
+
name,
|
|
886
|
+
kind: doc.kind ?? "component",
|
|
887
|
+
className,
|
|
888
|
+
summary: doc.summary,
|
|
889
|
+
remarks: doc.remarks,
|
|
890
|
+
privateRemarks: doc.privateRemarks,
|
|
891
|
+
releaseStage: doc.releaseStage,
|
|
892
|
+
since: doc.since,
|
|
893
|
+
group: doc.group,
|
|
894
|
+
accessibility: doc.accessibility,
|
|
895
|
+
modifiers,
|
|
896
|
+
parts: [...acc.parts.values()].sort(byName),
|
|
897
|
+
states: [...acc.states.values()].sort(byName),
|
|
898
|
+
slots: [...doc.slots].map(([slotName, description]) => ({
|
|
899
|
+
name: slotName,
|
|
900
|
+
description: description || void 0
|
|
901
|
+
})).sort(byName),
|
|
902
|
+
cssPropertiesConsumed: [...acc.consumed].sort(),
|
|
903
|
+
cssPropertiesDeclared: [...acc.declared.values()].sort(byName),
|
|
904
|
+
functions: [...acc.functions.values()].sort(byName),
|
|
905
|
+
animations: [...acc.animations.values()].sort(byName),
|
|
906
|
+
layers: [...acc.layers.values()].sort(byName),
|
|
907
|
+
conditions: acc.conditions,
|
|
908
|
+
examples: doc.examples,
|
|
909
|
+
structure: doc.structure,
|
|
910
|
+
demo: doc.demo,
|
|
911
|
+
deprecated: doc.deprecated,
|
|
912
|
+
see: doc.see,
|
|
913
|
+
...doc.customBlocks.size > 0 ? { customBlocks: Object.fromEntries(doc.customBlocks) } : {}
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Parse a CSS string into a documentation model. Records are delimited by doc comments carrying a
|
|
918
|
+
* record tag (`@component`/`@name` by default; override via {@link ParseOptions.isRecordBoundary});
|
|
919
|
+
* everything from one boundary comment to the next belongs to that record.
|
|
920
|
+
*
|
|
921
|
+
* @param css - The CSS source (a generated stylesheet, with authored doc comments).
|
|
922
|
+
* @param options - {@link ParseOptions}.
|
|
923
|
+
* @returns One {@link CssDocEntry} per record, in document order.
|
|
924
|
+
*
|
|
925
|
+
* @example
|
|
926
|
+
* ```ts
|
|
927
|
+
* import { parseCssDocs } from "@cssdoc/core";
|
|
928
|
+
*
|
|
929
|
+
* const [badge] = parseCssDocs(badgeCssWithDocComments);
|
|
930
|
+
* badge.modifiers.map((m) => m.name); // ["-color-danger", "-color-success", …]
|
|
931
|
+
* ```
|
|
932
|
+
*/
|
|
933
|
+
function parseCssDocs(css, options = {}) {
|
|
934
|
+
const configuration = options.configuration ?? new CssDocConfiguration();
|
|
935
|
+
const boundary = options.isRecordBoundary ?? ((text) => recordNameOf(text, configuration));
|
|
936
|
+
const root = postcss.parse(css);
|
|
937
|
+
const records = [];
|
|
938
|
+
let current = null;
|
|
939
|
+
for (const node of root.nodes) {
|
|
940
|
+
if (node.type === "comment") {
|
|
941
|
+
const name = boundary(node.text);
|
|
942
|
+
if (name) {
|
|
943
|
+
current = {
|
|
944
|
+
name,
|
|
945
|
+
doc: parseDocComment(node.text, configuration),
|
|
946
|
+
nodes: []
|
|
947
|
+
};
|
|
948
|
+
records.push(current);
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (current) current.nodes.push(node);
|
|
953
|
+
}
|
|
954
|
+
return records.map((r) => buildEntry(r.name, r.doc, r.nodes));
|
|
955
|
+
}
|
|
956
|
+
//#endregion
|
|
957
|
+
//#region src/mermaid.ts
|
|
958
|
+
/** Escape a selector label for a Mermaid node body (quoted to allow dots, brackets, and spaces). */
|
|
959
|
+
const label = (selector) => `"${selector.replace(/"/gu, """)}"`;
|
|
960
|
+
/**
|
|
961
|
+
* Convert a structure tree to a Mermaid `flowchart` definition. Each node gets a stable id (`n0`, `n1`,
|
|
962
|
+
* …) in depth-first order; edges connect a parent to each child.
|
|
963
|
+
*
|
|
964
|
+
* @param roots - Top-level {@link StructureNode}s (an authored `@structure`).
|
|
965
|
+
* @param options - `direction` sets the flowchart orientation (default `TD`, top-down).
|
|
966
|
+
* @returns Mermaid source, or an empty string when there are no nodes.
|
|
967
|
+
*
|
|
968
|
+
* @example
|
|
969
|
+
* ```ts
|
|
970
|
+
* toMermaid([{ selector: ".tabs", children: [{ selector: ".panel", children: [] }] }]);
|
|
971
|
+
* // flowchart TD
|
|
972
|
+
* // n0[".tabs"]
|
|
973
|
+
* // n1[".panel"]
|
|
974
|
+
* // n0 --> n1
|
|
975
|
+
* ```
|
|
976
|
+
*/
|
|
977
|
+
function toMermaid(roots, options = {}) {
|
|
978
|
+
if (!roots.length) return "";
|
|
979
|
+
const lines = [`flowchart ${options.direction ?? "TD"}`];
|
|
980
|
+
const edges = [];
|
|
981
|
+
let counter = 0;
|
|
982
|
+
const walk = (node) => {
|
|
983
|
+
const id = `n${counter++}`;
|
|
984
|
+
lines.push(` ${id}[${label(node.selector)}]`);
|
|
985
|
+
for (const child of node.children) edges.push(` ${id} --> ${walk(child)}`);
|
|
986
|
+
return id;
|
|
987
|
+
};
|
|
988
|
+
for (const root of roots) walk(root);
|
|
989
|
+
return [...lines, ...edges].join("\n");
|
|
990
|
+
}
|
|
991
|
+
//#endregion
|
|
992
|
+
//#region src/index.ts
|
|
993
|
+
/**
|
|
994
|
+
* Serialize a documentation model to pretty JSON (the raw, emitter-agnostic artifact — like TypeDoc's
|
|
995
|
+
* `--json`).
|
|
996
|
+
*
|
|
997
|
+
* @param model - The entries from {@link parseCssDocs}.
|
|
998
|
+
* @returns A JSON string.
|
|
999
|
+
*/
|
|
1000
|
+
function toJson(model) {
|
|
1001
|
+
return `${JSON.stringify(model, null, 2)}\n`;
|
|
1002
|
+
}
|
|
1003
|
+
//#endregion
|
|
1004
|
+
export { CssDocConfiguration, CssDocTagDefinition, RECORD_TAGS, parseCssDocs, parseDocComment, parseStructure, recordNameOf, stripCommentFraming, toJson, toMermaid };
|