@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 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
- /** A `-<prop>-<value>` (or boolean `-<flag>`) modifier on a component's base class. */
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
- /** The modifier class without the leading dot, e.g. `-color-secondary` or `-condensed`. */
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, e.g. `color` (the text after the first `-`, up to the next `-`). */
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+\.?(-[\w-]+)\s*\}/u);
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: link?.[1]
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, baseEsc, prefixNoDot, inScope) {
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+\.(-[\w-]+)/u);
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, baseEsc, prefixNoDot, inScope || node.name === "scope");
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
- 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
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(modName, entry);
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(/[.*+?^${}()|[\]\\]/gu, "\\$&"), className.endsWith(name) ? className.slice(1, className.length - name.length) : "", false);
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 } = splitModifier(modName);
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
- // A `-<prop>` boolean modifier, or a `-<prop>-<value>` modifier. `Prop` is the segment up to the
85
- // first interior hyphen; `Value` is the remainder (which may itself contain hyphens).
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cssdoc/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A generic CSS documentation extractor: parse doc-comments + the CSS AST into a serializable model (TSDoc, for CSS).",
5
5
  "keywords": [
6
6
  "css",