@cssdoc/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +136 -5
- package/dist/index.mjs +192 -31
- package/grammar/CssDoc.grammarkdown +18 -2
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,116 @@
|
|
|
1
|
+
//#region src/modifier.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The modifier convention — how a component's modifier variations are spelled in CSS, and the one
|
|
4
|
+
* place that spelling is turned into (and back out of) selectors. cssdoc is framework-agnostic, so it
|
|
5
|
+
* doesn't assume any single scheme: BEM (`.button--primary`, the default), rscss (`.button.-color-x`),
|
|
6
|
+
* bare/OOCSS chained classes (`.button.primary`), CUBE data-attribute exceptions
|
|
7
|
+
* (`.card[data-variant="ghost"]`), and anything expressible as the three structural forms below.
|
|
8
|
+
*
|
|
9
|
+
* {@link ModifierMatcher} is the single owner of modifier matching, prop/value analysis, and
|
|
10
|
+
* selector rendering — both `parse.ts` and `index.ts` route through it, so their notions of "what is a
|
|
11
|
+
* modifier" can't drift.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* How a modifier attaches to a component's base class.
|
|
17
|
+
*
|
|
18
|
+
* - `chained` — a separate class on the base element: `.base.primary` (bare/OOCSS), `.base.-mod`
|
|
19
|
+
* (rscss), `.base.is-open` (state). `separator` is the required class prefix (`""` = any chained
|
|
20
|
+
* class, `-`, `is-`).
|
|
21
|
+
* - `suffix` — appended into the base class name itself: `.button--primary` (BEM/SUIT). `separator` is
|
|
22
|
+
* the delimiter between the base and the modifier body (`--`).
|
|
23
|
+
* - `attribute` — an attribute selector on the base element: `.card[data-variant="ghost"]` (CUBE).
|
|
24
|
+
* `separator` is the required attribute-name prefix (`data-`; `""` = any attribute).
|
|
25
|
+
*/
|
|
26
|
+
interface ModifierConvention {
|
|
27
|
+
/** The structural form. An open union — further forms may be added without a breaking change. */
|
|
28
|
+
structure: "chained" | "suffix" | "attribute";
|
|
29
|
+
/** The prefix/delimiter, interpreted per {@link ModifierConvention.structure}. */
|
|
30
|
+
separator: string;
|
|
31
|
+
/**
|
|
32
|
+
* Split the modifier body into `prop`/`value` on {@link ModifierConvention.propValueSeparator}?
|
|
33
|
+
* Defaults to `false`. Ignored for `attribute` (which always derives `prop` from the attribute name
|
|
34
|
+
* and `value` from the attribute value).
|
|
35
|
+
*/
|
|
36
|
+
propValue?: boolean;
|
|
37
|
+
/** The separator for the `prop`/`value` split. Defaults to `-`. */
|
|
38
|
+
propValueSeparator?: string;
|
|
39
|
+
}
|
|
40
|
+
/** The built-in convention presets. Other schemes use the custom object (see the docs). */
|
|
41
|
+
declare const MODIFIER_PRESETS: {
|
|
42
|
+
/** BEM / SUIT — `.button--primary`. The default. */readonly bem: {
|
|
43
|
+
readonly structure: "suffix";
|
|
44
|
+
readonly separator: "--";
|
|
45
|
+
readonly propValue: false;
|
|
46
|
+
}; /** rscss — `.button.-color-secondary`, split into `prop`/`value`. */
|
|
47
|
+
readonly rscss: {
|
|
48
|
+
readonly structure: "chained";
|
|
49
|
+
readonly separator: "-";
|
|
50
|
+
readonly propValue: true;
|
|
51
|
+
readonly propValueSeparator: "-";
|
|
52
|
+
}; /** OOCSS / bare chained classes — `.button.primary` (any class chained to the base). */
|
|
53
|
+
readonly bare: {
|
|
54
|
+
readonly structure: "chained";
|
|
55
|
+
readonly separator: "";
|
|
56
|
+
readonly propValue: false;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
/** A preset name, or a full custom {@link ModifierConvention}. */
|
|
60
|
+
type ModifierConventionInput = keyof typeof MODIFIER_PRESETS | ModifierConvention;
|
|
61
|
+
/** The default convention: BEM. */
|
|
62
|
+
declare const DEFAULT_MODIFIER_CONVENTION: ModifierConvention;
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a preset name or custom object into a fully-populated {@link ModifierConvention} (defaults
|
|
65
|
+
* filled in). No argument resolves to the {@link DEFAULT_MODIFIER_CONVENTION} (BEM).
|
|
66
|
+
*
|
|
67
|
+
* @throws If given an unknown preset name.
|
|
68
|
+
*/
|
|
69
|
+
declare function resolveModifierConvention(input?: ModifierConventionInput): ModifierConvention;
|
|
70
|
+
/** One modifier found on a selector: its canonical name plus the derived `prop`/`value`. */
|
|
71
|
+
interface ModifierHit {
|
|
72
|
+
/** The modifier as written, minus outer punctuation — `button--primary`, `-color-secondary`,
|
|
73
|
+
* `primary`, or `data-variant="ghost"`. Render it back with {@link ModifierMatcher.selectorFor}. */
|
|
74
|
+
name: string;
|
|
75
|
+
/** The property segment (a grouping key). */
|
|
76
|
+
prop: string;
|
|
77
|
+
/** The value segment; absent for boolean modifiers. */
|
|
78
|
+
value?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The single owner of modifier recognition for one convention: finds modifiers on selectors, derives
|
|
82
|
+
* `prop`/`value`, renders a modifier name back to a selector fragment, and answers "does this host-doc
|
|
83
|
+
* token look like a modifier usage?". Constructed once per parse from the resolved convention.
|
|
84
|
+
*/
|
|
85
|
+
declare class ModifierMatcher {
|
|
86
|
+
readonly convention: ModifierConvention;
|
|
87
|
+
private readonly sepEsc;
|
|
88
|
+
constructor(convention: ModifierConvention);
|
|
89
|
+
/**
|
|
90
|
+
* Every modifier attached to `baseNoDot` within one selector. `selector` should have its pseudos
|
|
91
|
+
* already dropped (as `parse.ts`/`index.ts` do); the base is given without its leading dot.
|
|
92
|
+
*/
|
|
93
|
+
modifiersIn(selector: string, baseNoDot: string): ModifierHit[];
|
|
94
|
+
/** Derive `prop`/`value` for a modifier `name` (as returned by {@link modifiersIn} or authored). */
|
|
95
|
+
analyze(name: string): {
|
|
96
|
+
prop: string;
|
|
97
|
+
value?: string;
|
|
98
|
+
};
|
|
99
|
+
/** Render a modifier `name` as the selector fragment it denotes (`.name` or `[name]`). */
|
|
100
|
+
selectorFor(name: string): string;
|
|
101
|
+
/** Normalize a member token/expression to its canonical `name` key (the inverse of authoring noise). */
|
|
102
|
+
normalizeMember(token: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Does a token/expression seen in a host document (an HTML class token, or an attribute expression)
|
|
105
|
+
* look like a modifier usage of `baseNoDot`? Replaces the old `startsWith("-")` gate.
|
|
106
|
+
*/
|
|
107
|
+
looksLikeUsage(token: string, baseNoDot?: string): boolean;
|
|
108
|
+
/** Canonicalize an attribute expression (bracket-inner): normalize quotes to double, trim. */
|
|
109
|
+
private normalizeAttribute;
|
|
110
|
+
/** Does an attribute-expression's name carry the convention's required prefix? */
|
|
111
|
+
private attributeMatches;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
1
114
|
//#region src/configuration.d.ts
|
|
2
115
|
/**
|
|
3
116
|
* The syntactic kind of a tag, mirroring TSDoc's Block/Modifier/Inline split, plus cssdoc's own
|
|
@@ -49,7 +162,12 @@ declare class CssDocConfiguration {
|
|
|
49
162
|
private readonly _tagDefinitions;
|
|
50
163
|
private readonly _byName;
|
|
51
164
|
private readonly _supported;
|
|
165
|
+
private _modifierConvention;
|
|
52
166
|
constructor();
|
|
167
|
+
/** The resolved modifier convention this configuration parses with (defaults to BEM). */
|
|
168
|
+
get modifierConvention(): ModifierConvention;
|
|
169
|
+
/** Set the modifier convention from a preset name or a custom {@link ModifierConvention}. */
|
|
170
|
+
setModifierConvention(input: ModifierConventionInput): void;
|
|
53
171
|
/** Every registered tag definition, in registration order. */
|
|
54
172
|
get tagDefinitions(): readonly CssDocTagDefinition[];
|
|
55
173
|
/** Only the tag definitions that are currently supported. */
|
|
@@ -93,13 +211,20 @@ declare class CssDocConfiguration {
|
|
|
93
211
|
*
|
|
94
212
|
* @module
|
|
95
213
|
*/
|
|
96
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* A modifier variation on a component's base class. How modifiers are spelled is configurable (see
|
|
216
|
+
* {@link ParseOptions.modifierConvention}); the default is BEM (`.button--primary`).
|
|
217
|
+
*/
|
|
97
218
|
interface CssModifier {
|
|
98
|
-
/**
|
|
219
|
+
/**
|
|
220
|
+
* The modifier as written, minus its outer punctuation — its exact spelling depends on the
|
|
221
|
+
* convention: `button--primary` (BEM, the default), `-color-secondary` (rscss), `primary`
|
|
222
|
+
* (bare/OOCSS), or `data-variant="ghost"` (CUBE attribute).
|
|
223
|
+
*/
|
|
99
224
|
name: string;
|
|
100
|
-
/** The property segment
|
|
225
|
+
/** The property segment — a grouping key derived from the modifier (e.g. `color`, `variant`, `primary`). */
|
|
101
226
|
prop: string;
|
|
102
|
-
/** The value segment, e.g. `secondary`; absent for boolean modifiers. */
|
|
227
|
+
/** The value segment, e.g. `secondary` or `ghost`; absent for boolean/flag modifiers. */
|
|
103
228
|
value?: string;
|
|
104
229
|
/** Prose from a `@modifier` doc tag, when authored. */
|
|
105
230
|
description?: string;
|
|
@@ -277,6 +402,12 @@ interface ParseOptions {
|
|
|
277
402
|
* Supply one (e.g. from `@cssdoc/config`) to register custom tags or disable standard ones.
|
|
278
403
|
*/
|
|
279
404
|
configuration?: CssDocConfiguration;
|
|
405
|
+
/**
|
|
406
|
+
* The modifier convention — how modifier classes are spelled (BEM `.button--primary` by default;
|
|
407
|
+
* `rscss`, `bare`, or a custom {@link import("./modifier.ts").ModifierConvention} for SUIT/CUBE/etc.).
|
|
408
|
+
* Overrides the `configuration`'s convention when both are given.
|
|
409
|
+
*/
|
|
410
|
+
modifierConvention?: ModifierConventionInput;
|
|
280
411
|
}
|
|
281
412
|
//#endregion
|
|
282
413
|
//#region src/parse.d.ts
|
|
@@ -454,4 +585,4 @@ declare function toMermaid(roots: StructureNode[], options?: {
|
|
|
454
585
|
*/
|
|
455
586
|
declare function toJson(model: CssDocEntry[]): string;
|
|
456
587
|
//#endregion
|
|
457
|
-
export { type CssAnimation, type CssCondition, CssDocConfiguration, type CssDocEntry, type CssDocSyntaxKind, CssDocTagDefinition, type CssDocTagDefinitionOptions, type CssFunction, type CssLayer, type CssModifier, type CssPart, type CssPropertyDeclared, type CssRecordKind, type CssReleaseStage, type CssSlot, type CssState, type DocCondition, type DocCssProperty, type DocModifier, type ParseOptions, type ParsedDoc, RECORD_TAGS, type StructureNode, parseCssDocs, parseDocComment, parseStructure, recordNameOf, stripCommentFraming, toJson, toMermaid };
|
|
588
|
+
export { type CssAnimation, type CssCondition, CssDocConfiguration, type CssDocEntry, type CssDocSyntaxKind, CssDocTagDefinition, type CssDocTagDefinitionOptions, type CssFunction, type CssLayer, type CssModifier, type CssPart, type CssPropertyDeclared, type CssRecordKind, type CssReleaseStage, type CssSlot, type CssState, DEFAULT_MODIFIER_CONVENTION, type DocCondition, type DocCssProperty, type DocModifier, MODIFIER_PRESETS, type ModifierConvention, type ModifierConventionInput, type ModifierHit, ModifierMatcher, type ParseOptions, type ParsedDoc, RECORD_TAGS, type StructureNode, parseCssDocs, parseDocComment, parseStructure, recordNameOf, resolveModifierConvention, stripCommentFraming, toJson, toMermaid };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,165 @@
|
|
|
1
1
|
import postcss from "postcss";
|
|
2
|
+
//#region src/modifier.ts
|
|
3
|
+
/** The built-in convention presets. Other schemes use the custom object (see the docs). */
|
|
4
|
+
const MODIFIER_PRESETS = {
|
|
5
|
+
/** BEM / SUIT — `.button--primary`. The default. */
|
|
6
|
+
bem: {
|
|
7
|
+
structure: "suffix",
|
|
8
|
+
separator: "--",
|
|
9
|
+
propValue: false
|
|
10
|
+
},
|
|
11
|
+
/** rscss — `.button.-color-secondary`, split into `prop`/`value`. */
|
|
12
|
+
rscss: {
|
|
13
|
+
structure: "chained",
|
|
14
|
+
separator: "-",
|
|
15
|
+
propValue: true,
|
|
16
|
+
propValueSeparator: "-"
|
|
17
|
+
},
|
|
18
|
+
/** OOCSS / bare chained classes — `.button.primary` (any class chained to the base). */
|
|
19
|
+
bare: {
|
|
20
|
+
structure: "chained",
|
|
21
|
+
separator: "",
|
|
22
|
+
propValue: false
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** The default convention: BEM. */
|
|
26
|
+
const DEFAULT_MODIFIER_CONVENTION = MODIFIER_PRESETS.bem;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a preset name or custom object into a fully-populated {@link ModifierConvention} (defaults
|
|
29
|
+
* filled in). No argument resolves to the {@link DEFAULT_MODIFIER_CONVENTION} (BEM).
|
|
30
|
+
*
|
|
31
|
+
* @throws If given an unknown preset name.
|
|
32
|
+
*/
|
|
33
|
+
function resolveModifierConvention(input) {
|
|
34
|
+
let base;
|
|
35
|
+
if (input === void 0) base = DEFAULT_MODIFIER_CONVENTION;
|
|
36
|
+
else if (typeof input === "string") base = MODIFIER_PRESETS[input];
|
|
37
|
+
else base = input;
|
|
38
|
+
if (!base) throw new Error(`Unknown modifier convention preset: ${JSON.stringify(input)}`);
|
|
39
|
+
return {
|
|
40
|
+
structure: base.structure,
|
|
41
|
+
separator: base.separator,
|
|
42
|
+
propValue: base.propValue ?? false,
|
|
43
|
+
propValueSeparator: base.propValueSeparator ?? "-"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Escape a string for literal use inside a `RegExp`. */
|
|
47
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
48
|
+
/** Strip one layer of matching quotes from an attribute value. */
|
|
49
|
+
const unquote$1 = (v) => v.trim().replace(/^(["'])([\s\S]*)\1$/u, "$2");
|
|
50
|
+
/**
|
|
51
|
+
* The single owner of modifier recognition for one convention: finds modifiers on selectors, derives
|
|
52
|
+
* `prop`/`value`, renders a modifier name back to a selector fragment, and answers "does this host-doc
|
|
53
|
+
* token look like a modifier usage?". Constructed once per parse from the resolved convention.
|
|
54
|
+
*/
|
|
55
|
+
var ModifierMatcher = class {
|
|
56
|
+
convention;
|
|
57
|
+
sepEsc;
|
|
58
|
+
constructor(convention) {
|
|
59
|
+
this.convention = convention;
|
|
60
|
+
this.sepEsc = escapeRe(convention.separator);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Every modifier attached to `baseNoDot` within one selector. `selector` should have its pseudos
|
|
64
|
+
* already dropped (as `parse.ts`/`index.ts` do); the base is given without its leading dot.
|
|
65
|
+
*/
|
|
66
|
+
modifiersIn(selector, baseNoDot) {
|
|
67
|
+
const baseEsc = escapeRe(baseNoDot);
|
|
68
|
+
const hits = [];
|
|
69
|
+
const seen = /* @__PURE__ */ new Set();
|
|
70
|
+
const push = (name) => {
|
|
71
|
+
if (seen.has(name)) return;
|
|
72
|
+
seen.add(name);
|
|
73
|
+
hits.push({
|
|
74
|
+
name,
|
|
75
|
+
...this.analyze(name)
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
if (this.convention.structure === "suffix") {
|
|
79
|
+
const re = new RegExp(`\\.(${baseEsc}${this.sepEsc}[\\w-]+)`, "gu");
|
|
80
|
+
for (const m of selector.matchAll(re)) push(m[1]);
|
|
81
|
+
return hits;
|
|
82
|
+
}
|
|
83
|
+
if (this.convention.structure === "attribute") {
|
|
84
|
+
const chain = new RegExp(`(?:\\.${baseEsc}|:scope)((?:\\[[^\\]]*\\])+)`, "gu");
|
|
85
|
+
for (const m of selector.matchAll(chain)) for (const a of m[1].matchAll(/\[([^\]]*)\]/gu)) {
|
|
86
|
+
const name = this.normalizeAttribute(a[1]);
|
|
87
|
+
if (name && this.attributeMatches(name)) push(name);
|
|
88
|
+
}
|
|
89
|
+
return hits;
|
|
90
|
+
}
|
|
91
|
+
const cls = `\\.${this.sepEsc}[\\w-]+`;
|
|
92
|
+
const chain = new RegExp(`(?:\\.${baseEsc}|:scope)((?:${cls})+)`, "gu");
|
|
93
|
+
const inner = new RegExp(`\\.(${this.sepEsc}[\\w-]+)`, "gu");
|
|
94
|
+
for (const m of selector.matchAll(chain)) for (const c of m[1].matchAll(inner)) push(c[1]);
|
|
95
|
+
return hits;
|
|
96
|
+
}
|
|
97
|
+
/** Derive `prop`/`value` for a modifier `name` (as returned by {@link modifiersIn} or authored). */
|
|
98
|
+
analyze(name) {
|
|
99
|
+
if (this.convention.structure === "attribute") {
|
|
100
|
+
const canonical = this.normalizeAttribute(name);
|
|
101
|
+
const eq = canonical.indexOf("=");
|
|
102
|
+
const attr = eq === -1 ? canonical : canonical.slice(0, eq);
|
|
103
|
+
return {
|
|
104
|
+
prop: attr.startsWith(this.convention.separator) ? attr.slice(this.convention.separator.length) : attr,
|
|
105
|
+
value: eq === -1 ? void 0 : unquote$1(canonical.slice(eq + 1))
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
let body;
|
|
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;
|
|
113
|
+
if (!this.convention.propValue) return { prop: body };
|
|
114
|
+
const sep = this.convention.propValueSeparator ?? "-";
|
|
115
|
+
const at = body.indexOf(sep);
|
|
116
|
+
if (at === -1) return { prop: body };
|
|
117
|
+
return {
|
|
118
|
+
prop: body.slice(0, at),
|
|
119
|
+
value: body.slice(at + sep.length)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Render a modifier `name` as the selector fragment it denotes (`.name` or `[name]`). */
|
|
123
|
+
selectorFor(name) {
|
|
124
|
+
return this.convention.structure === "attribute" ? `[${name}]` : `.${name}`;
|
|
125
|
+
}
|
|
126
|
+
/** Normalize a member token/expression to its canonical `name` key (the inverse of authoring noise). */
|
|
127
|
+
normalizeMember(token) {
|
|
128
|
+
if (this.convention.structure === "attribute") {
|
|
129
|
+
const inner = token.replace(/^\[/u, "").replace(/\]$/u, "");
|
|
130
|
+
return this.normalizeAttribute(inner);
|
|
131
|
+
}
|
|
132
|
+
return token.replace(/^\./u, "");
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Does a token/expression seen in a host document (an HTML class token, or an attribute expression)
|
|
136
|
+
* look like a modifier usage of `baseNoDot`? Replaces the old `startsWith("-")` gate.
|
|
137
|
+
*/
|
|
138
|
+
looksLikeUsage(token, baseNoDot) {
|
|
139
|
+
if (this.convention.structure === "attribute") return this.attributeMatches(this.normalizeMember(token));
|
|
140
|
+
const name = token.replace(/^\./u, "");
|
|
141
|
+
if (this.convention.structure === "suffix") {
|
|
142
|
+
if (baseNoDot) return name.startsWith(`${baseNoDot}${this.convention.separator}`);
|
|
143
|
+
return this.convention.separator !== "" && name.includes(this.convention.separator);
|
|
144
|
+
}
|
|
145
|
+
if (baseNoDot && name === baseNoDot) return false;
|
|
146
|
+
return name.startsWith(this.convention.separator);
|
|
147
|
+
}
|
|
148
|
+
/** Canonicalize an attribute expression (bracket-inner): normalize quotes to double, trim. */
|
|
149
|
+
normalizeAttribute(inner) {
|
|
150
|
+
const trimmed = inner.trim();
|
|
151
|
+
const eq = trimmed.indexOf("=");
|
|
152
|
+
if (eq === -1) return trimmed;
|
|
153
|
+
return `${trimmed.slice(0, eq + 1)}"${unquote$1(trimmed.slice(eq + 1))}"`;
|
|
154
|
+
}
|
|
155
|
+
/** Does an attribute-expression's name carry the convention's required prefix? */
|
|
156
|
+
attributeMatches(name) {
|
|
157
|
+
const eq = name.indexOf("=");
|
|
158
|
+
const attr = (eq === -1 ? name : name.slice(0, eq)).replace(/[~|^$*]$/u, "");
|
|
159
|
+
return attr.length > 0 && attr.startsWith(this.convention.separator);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
2
163
|
//#region src/configuration.ts
|
|
3
164
|
const TAG_NAME_RE = /^@?[a-zA-Z][a-zA-Z0-9-]*$/u;
|
|
4
165
|
/** One tag in the vocabulary: its name, kind, and how it may be used. */
|
|
@@ -47,9 +208,18 @@ var CssDocConfiguration = class CssDocConfiguration {
|
|
|
47
208
|
_tagDefinitions = [];
|
|
48
209
|
_byName = /* @__PURE__ */ new Map();
|
|
49
210
|
_supported = /* @__PURE__ */ new Set();
|
|
211
|
+
_modifierConvention = DEFAULT_MODIFIER_CONVENTION;
|
|
50
212
|
constructor() {
|
|
51
213
|
this.addTagDefinitions(CssDocConfiguration.standardTags(), true);
|
|
52
214
|
}
|
|
215
|
+
/** The resolved modifier convention this configuration parses with (defaults to BEM). */
|
|
216
|
+
get modifierConvention() {
|
|
217
|
+
return this._modifierConvention;
|
|
218
|
+
}
|
|
219
|
+
/** Set the modifier convention from a preset name or a custom {@link ModifierConvention}. */
|
|
220
|
+
setModifierConvention(input) {
|
|
221
|
+
this._modifierConvention = resolveModifierConvention(input);
|
|
222
|
+
}
|
|
53
223
|
/** Every registered tag definition, in registration order. */
|
|
54
224
|
get tagDefinitions() {
|
|
55
225
|
return this._tagDefinitions;
|
|
@@ -420,12 +590,13 @@ function parseModifierBody(description) {
|
|
|
420
590
|
const dep = description?.match(/^@deprecated\b\s*([\s\S]*)$/u);
|
|
421
591
|
if (dep) {
|
|
422
592
|
const rawNote = dep[1].trim();
|
|
423
|
-
const link = rawNote.match(/\{@link\s
|
|
593
|
+
const link = rawNote.match(/\{@link\s+(\.?[\w-]+|\[[^\]]*\])\s*\}/u);
|
|
594
|
+
const canonical = link?.[1].replace(/^\./u, "").replace(/^\[/u, "").replace(/\]$/u, "");
|
|
424
595
|
const note = rawNote.replace(/\{@link\s+[^}]*\}/u, "").trim();
|
|
425
596
|
if (!note && !link) return { deprecatedFlag: true };
|
|
426
597
|
return {
|
|
427
598
|
deprecated: note || void 0,
|
|
428
|
-
deprecatedCanonical:
|
|
599
|
+
deprecatedCanonical: canonical
|
|
429
600
|
};
|
|
430
601
|
}
|
|
431
602
|
return { description: description ?? "" };
|
|
@@ -649,16 +820,6 @@ function parseStructure(raw) {
|
|
|
649
820
|
/** Matches a `var(--name` reference; group 1 is the custom-property name. */
|
|
650
821
|
const VAR_RE = /var\(\s*(--[\w-]+)/gu;
|
|
651
822
|
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
823
|
/** Record a conditional-support block, de-duplicating by type + query. */
|
|
663
824
|
function addCondition(acc, condition) {
|
|
664
825
|
if (!acc.conditions.some((c) => c.type === condition.type && c.query === condition.query)) acc.conditions.push(condition);
|
|
@@ -682,13 +843,12 @@ function collectFunction(node, acc) {
|
|
|
682
843
|
});
|
|
683
844
|
}
|
|
684
845
|
/** Extract every fact from one record's nodes into `acc`. */
|
|
685
|
-
function collect(nodes, acc,
|
|
686
|
-
const modRe = new RegExp(`(?:${baseEsc}|:scope)((?:\\.-[\\w-]+)+)`, "gu");
|
|
846
|
+
function collect(nodes, acc, matcher, baseNoDot, prefixNoDot, inScope) {
|
|
687
847
|
let pendingCanonical;
|
|
688
848
|
for (const node of nodes) {
|
|
689
849
|
if (node.type === "comment") {
|
|
690
|
-
const dep = node.text.match(/@deprecated.*?use\s
|
|
691
|
-
if (dep) pendingCanonical = dep[1];
|
|
850
|
+
const dep = node.text.match(/@deprecated.*?use\s+(\.[\w-]+|\[[^\]]*\])/u);
|
|
851
|
+
if (dep) pendingCanonical = matcher.normalizeMember(dep[1]);
|
|
692
852
|
continue;
|
|
693
853
|
}
|
|
694
854
|
if (node.type === "decl") {
|
|
@@ -743,27 +903,27 @@ function collect(nodes, acc, baseEsc, prefixNoDot, inScope) {
|
|
|
743
903
|
type: "media",
|
|
744
904
|
query: node.params.trim()
|
|
745
905
|
});
|
|
746
|
-
if (node.nodes) collect(node.nodes, acc,
|
|
906
|
+
if (node.nodes) collect(node.nodes, acc, matcher, baseNoDot, prefixNoDot, inScope || node.name === "scope");
|
|
747
907
|
continue;
|
|
748
908
|
}
|
|
749
909
|
if (node.type === "rule") {
|
|
750
910
|
for (const selector of node.selector.split(",")) {
|
|
751
911
|
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
912
|
const bare = selector.replace(/::?[\w-]+(\([^)]*\))?/gu, "");
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
value
|
|
913
|
+
const mods = matcher.modifiersIn(bare, baseNoDot);
|
|
914
|
+
const modNames = new Set(mods.map((mod) => mod.name));
|
|
915
|
+
for (const mod of mods) {
|
|
916
|
+
const entry = acc.modifiers.get(mod.name) ?? {
|
|
917
|
+
name: mod.name,
|
|
918
|
+
prop: mod.prop,
|
|
919
|
+
value: mod.value
|
|
761
920
|
};
|
|
762
921
|
if (pendingCanonical) entry.deprecated = { canonical: pendingCanonical };
|
|
763
|
-
acc.modifiers.set(
|
|
922
|
+
acc.modifiers.set(mod.name, entry);
|
|
764
923
|
}
|
|
765
924
|
if (inScope) for (const m of bare.matchAll(/\.([a-z][\w-]*)/gu)) {
|
|
766
925
|
const part = m[1];
|
|
926
|
+
if (modNames.has(part)) continue;
|
|
767
927
|
if (prefixNoDot && part.startsWith(prefixNoDot)) continue;
|
|
768
928
|
if (!acc.parts.has(part)) acc.parts.set(part, { name: part });
|
|
769
929
|
}
|
|
@@ -775,7 +935,7 @@ function collect(nodes, acc, baseEsc, prefixNoDot, inScope) {
|
|
|
775
935
|
}
|
|
776
936
|
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
777
937
|
/** Build one entry from its record name, doc comment, and nodes. */
|
|
778
|
-
function buildEntry(name, doc, nodes) {
|
|
938
|
+
function buildEntry(name, doc, nodes, matcher) {
|
|
779
939
|
let className = doc.className ?? "";
|
|
780
940
|
if (!className) {
|
|
781
941
|
const bare = nodes.filter((n) => n.type === "rule").map((n) => n.selector.trim()).filter((sel) => /^\.[a-z][\w-]*$/u.test(sel));
|
|
@@ -796,7 +956,7 @@ function buildEntry(name, doc, nodes) {
|
|
|
796
956
|
layers: /* @__PURE__ */ new Map(),
|
|
797
957
|
conditions: []
|
|
798
958
|
};
|
|
799
|
-
collect(nodes, acc, className.replace(
|
|
959
|
+
collect(nodes, acc, matcher, className.replace(/^\./u, ""), className.endsWith(name) ? className.slice(1, className.length - name.length) : "", false);
|
|
800
960
|
for (const [modName, mdoc] of doc.modifiers) {
|
|
801
961
|
const existing = acc.modifiers.get(modName);
|
|
802
962
|
const dep = mdoc.deprecated || mdoc.deprecatedCanonical || mdoc.deprecatedFlag ? {
|
|
@@ -810,7 +970,7 @@ function buildEntry(name, doc, nodes) {
|
|
|
810
970
|
...dep
|
|
811
971
|
};
|
|
812
972
|
} else {
|
|
813
|
-
const { prop, value } =
|
|
973
|
+
const { prop, value } = matcher.analyze(modName);
|
|
814
974
|
acc.modifiers.set(modName, {
|
|
815
975
|
name: modName,
|
|
816
976
|
prop,
|
|
@@ -932,6 +1092,7 @@ function buildEntry(name, doc, nodes) {
|
|
|
932
1092
|
*/
|
|
933
1093
|
function parseCssDocs(css, options = {}) {
|
|
934
1094
|
const configuration = options.configuration ?? new CssDocConfiguration();
|
|
1095
|
+
const matcher = new ModifierMatcher(resolveModifierConvention(options.modifierConvention ?? configuration.modifierConvention));
|
|
935
1096
|
const boundary = options.isRecordBoundary ?? ((text) => recordNameOf(text, configuration));
|
|
936
1097
|
const root = postcss.parse(css);
|
|
937
1098
|
const records = [];
|
|
@@ -951,7 +1112,7 @@ function parseCssDocs(css, options = {}) {
|
|
|
951
1112
|
}
|
|
952
1113
|
if (current) current.nodes.push(node);
|
|
953
1114
|
}
|
|
954
|
-
return records.map((r) => buildEntry(r.name, r.doc, r.nodes));
|
|
1115
|
+
return records.map((r) => buildEntry(r.name, r.doc, r.nodes, matcher));
|
|
955
1116
|
}
|
|
956
1117
|
//#endregion
|
|
957
1118
|
//#region src/mermaid.ts
|
|
@@ -1001,4 +1162,4 @@ function toJson(model) {
|
|
|
1001
1162
|
return `${JSON.stringify(model, null, 2)}\n`;
|
|
1002
1163
|
}
|
|
1003
1164
|
//#endregion
|
|
1004
|
-
export { CssDocConfiguration, CssDocTagDefinition, RECORD_TAGS, parseCssDocs, parseDocComment, parseStructure, recordNameOf, stripCommentFraming, toJson, toMermaid };
|
|
1165
|
+
export { CssDocConfiguration, CssDocTagDefinition, DEFAULT_MODIFIER_CONVENTION, MODIFIER_PRESETS, ModifierMatcher, RECORD_TAGS, parseCssDocs, parseDocComment, parseStructure, recordNameOf, resolveModifierConvention, stripCommentFraming, toJson, toMermaid };
|
|
@@ -81,12 +81,28 @@ Identifier ::
|
|
|
81
81
|
RecordName ::
|
|
82
82
|
Identifier
|
|
83
83
|
|
|
84
|
-
//
|
|
85
|
-
//
|
|
84
|
+
// How a modifier is spelled is configurable (see the `ModifierConvention` in `@cssdoc/core`); the
|
|
85
|
+
// DEFAULT is BEM — the modifier is conjoined onto the base class with `--`, e.g. `.button--primary`.
|
|
86
|
+
// Other conventions:
|
|
87
|
+
// rscss — `.button.-color-secondary` (a chained `-`-prefixed class, split into Prop/Value)
|
|
88
|
+
// bare / OOCSS — `.button.primary` (any class chained to the base)
|
|
89
|
+
// CUBE — `.card[data-variant="ghost"]` (a data-attribute exception on the base)
|
|
90
|
+
// The default (BEM) form:
|
|
86
91
|
ModifierName ::
|
|
92
|
+
Identifier `--` Identifier
|
|
93
|
+
|
|
94
|
+
// The rscss form: a `-<prop>` boolean modifier, or a `-<prop>-<value>` modifier. `Prop` is the segment
|
|
95
|
+
// up to the first interior hyphen; `Value` is the remainder (which may itself contain hyphens).
|
|
96
|
+
RscssModifierName ::
|
|
87
97
|
`-` Prop
|
|
88
98
|
`-` Prop `-` Value
|
|
89
99
|
|
|
100
|
+
// The CUBE form: an attribute-selector exception on the base, e.g. `[data-variant="ghost"]`. The
|
|
101
|
+
// value is a quoted string; shown here as an identifier for brevity.
|
|
102
|
+
AttributeModifier ::
|
|
103
|
+
`[` Identifier `]`
|
|
104
|
+
`[` Identifier `=` Identifier `]`
|
|
105
|
+
|
|
90
106
|
Prop ::
|
|
91
107
|
> an identifier segment of letters and digits, containing no hyphen
|
|
92
108
|
|