@cssdoc/providers 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -3,6 +3,27 @@ import { ClassUsage, CssDocIndex, Location, Location as Location$1, PropertyAssi
3
3
  //#region src/types.d.ts
4
4
  /** Diagnostic severity. */
5
5
  type Severity = "error" | "warning";
6
+ /** A configurable per-rule severity — `off` suppresses the rule entirely. */
7
+ type RuleSeverity = "off" | "warn" | "error";
8
+ /**
9
+ * Every rule id an aspect can emit, author-side and consumer-side. A superset of `@cssdoc/lint-core`'s
10
+ * author-side `RuleName` (which omits the consumer-side `unknown-modifier`/`deprecated-modifier` and
11
+ * the opt-in `unknown-custom-property`).
12
+ */
13
+ type RuleId = "missing-summary" | "undocumented-modifier" | "deprecated-requires-canonical" | "name-not-in-css" | "unknown-modifier" | "deprecated-modifier" | "unknown-state" | "unknown-part" | "undocumented-part" | "undocumented-css-part" | "component-name-case" | "part-name-case" | "structure-unknown-selector" | "invalid-default-value" | "invalid-property-value" | "invalid-fallback-value" | "unknown-custom-property" | "cssdoc-directive";
14
+ /** A resolved severity for every rule. */
15
+ type RuleSeverities = Record<RuleId, RuleSeverity>;
16
+ /**
17
+ * The default rule severities. `unknown-modifier` defaults to `warn` — safe under the BEM default's
18
+ * strong `--` signal; lower it to `off` for weak-signal conventions (bare/OOCSS) where every chained
19
+ * class is a candidate. `unknown-custom-property` stays `off` (it needs an opt-in `propertyPrefix`).
20
+ */
21
+ declare const DEFAULT_RULE_SEVERITIES: RuleSeverities;
22
+ /**
23
+ * Merge per-rule overrides over {@link DEFAULT_RULE_SEVERITIES}. A `boolean` override is accepted for
24
+ * back-compat: `false` → `off`, `true` → the rule's default.
25
+ */
26
+ declare function resolveRuleSeverities(overrides?: Partial<Record<RuleId, RuleSeverity | boolean>>): RuleSeverities;
6
27
  /** One diagnostic from an aspect. */
7
28
  interface Diagnostic {
8
29
  /** The aspect that produced it (e.g. `modifier`). */
@@ -44,6 +65,27 @@ interface UsageOptions {
44
65
  */
45
66
  propertyPrefix?: string;
46
67
  }
68
+ /**
69
+ * A required name case: a built-in preset, or a custom regular-expression source string tested
70
+ * against the class name. `(string & {})` keeps preset autocomplete while accepting any pattern.
71
+ */
72
+ type NameCase = "pascalCase" | "camelCase" | "lowercase" | (string & {});
73
+ /** Which class names a `naming` convention enforces. Absent members aren't checked. */
74
+ interface NamingRules {
75
+ /** The component base-class name (e.g. SUIT `Card` → `pascalCase`). */
76
+ component?: NameCase;
77
+ /** Sub-element part class names. */
78
+ part?: NameCase;
79
+ }
80
+ /** The built-in name-case patterns, tested against a class name without its leading dot. */
81
+ declare const NAME_CASE_PRESETS: Record<string, RegExp>;
82
+ /** A {@link NamingRules} compiled to regexes (custom patterns compiled from their source). */
83
+ interface ResolvedNaming {
84
+ component?: RegExp;
85
+ part?: RegExp;
86
+ }
87
+ /** Compile {@link NamingRules} to regexes once, for the record/part name-case checks. */
88
+ declare function resolveNaming(naming?: NamingRules): ResolvedNaming;
47
89
  //#endregion
48
90
  //#region src/syntax.d.ts
49
91
  /** The result of matching a value against a syntax. */
@@ -63,9 +105,31 @@ interface SyntaxMatch {
63
105
  */
64
106
  declare function matchesSyntax(syntax: string, value: string): SyntaxMatch;
65
107
  //#endregion
108
+ //#region src/directives.d.ts
109
+ type DirectiveKind = "disable" | "enable" | "disable-line" | "disable-next-line" | "expect-error";
110
+ interface Directive {
111
+ kind: DirectiveKind;
112
+ /** Rule ids the directive scopes to, or `null` for every rule. */
113
+ rules: string[] | null;
114
+ /** 1-based line the comment starts / ends on. */
115
+ startLine: number;
116
+ endLine: number;
117
+ }
118
+ /** Parse every `cssdoc-*` directive comment out of a CSS source. */
119
+ declare function parseDirectives(source: string): Directive[];
120
+ /**
121
+ * Drop diagnostics suppressed by directive comments in `source`, and add a `cssdoc-directive`
122
+ * diagnostic for any `cssdoc-expect-error` that matched nothing.
123
+ *
124
+ * @param diagnostics - The diagnostics to filter (author-side, with CSS spans).
125
+ * @param source - The CSS the diagnostics came from (the same text passed to the linter).
126
+ * @returns The surviving diagnostics.
127
+ */
128
+ declare function applyDirectives(diagnostics: Diagnostic[], source: string): Diagnostic[];
129
+ //#endregion
66
130
  //#region src/aspects.d.ts
67
131
  declare const record: {
68
- model(index: CssDocIndex): Diagnostic[];
132
+ model(index: CssDocIndex, naming?: ResolvedNaming, structureIgnore?: readonly string[]): Diagnostic[];
69
133
  completions(index: CssDocIndex): Completion[];
70
134
  hover(base: string, index: CssDocIndex): Hover | undefined;
71
135
  definition(base: string, index: CssDocIndex): Location$1 | undefined;
@@ -78,8 +142,15 @@ declare const modifier: {
78
142
  definition(base: string, token: string, index: CssDocIndex): Location$1 | undefined;
79
143
  };
80
144
  declare const part: {
145
+ model(index: CssDocIndex, naming?: ResolvedNaming): Diagnostic[];
146
+ };
147
+ declare const cssPart: {
81
148
  model(index: CssDocIndex): Diagnostic[];
82
149
  };
150
+ /** A host-document class that looks like a state (via `statePrefixes`) but isn't a documented state. */
151
+ declare function stateUsage(usage: ClassUsage, index: CssDocIndex): Diagnostic[];
152
+ /** A host-document class that looks like a BEM element (`base<sep>…`) but isn't a documented part. */
153
+ declare function partUsage(usage: ClassUsage, index: CssDocIndex): Diagnostic[];
83
154
  declare const customProperty: {
84
155
  /** Author-side: a registered property's default (`initial-value`/`@defaultValue`) must match its syntax. */model(index: CssDocIndex): Diagnostic[]; /** Consumer-side: an assignment `--name: value` must match the property's declared syntax. */
85
156
  assignment(a: PropertyAssignment, index: CssDocIndex): Diagnostic[];
@@ -96,15 +167,18 @@ declare const func: {
96
167
  //#endregion
97
168
  //#region src/index.d.ts
98
169
  /** The aspect names covered, in a stable order (the extension point for future aspects). */
99
- declare const ASPECTS: readonly ["record", "modifier", "part", "custom-property", "structure", "function", "state", "condition"];
170
+ declare const ASPECTS: readonly ["record", "modifier", "part", "css-part", "custom-property", "structure", "function", "state", "condition"];
100
171
  /** Author-side hygiene diagnostics over the whole model (missing summaries, undocumented members, drift, invalid defaults). */
101
- declare function lintModel(index: CssDocIndex): Diagnostic[];
172
+ declare function lintModel(index: CssDocIndex, severities?: RuleSeverities, naming?: ResolvedNaming, structureIgnore?: readonly string[]): Diagnostic[];
102
173
  /** Consumer-side diagnostics for custom-property assignments (`--x: value` must match its `@property` syntax). */
103
- declare function checkPropertyAssignments(assignments: readonly PropertyAssignment[], index: CssDocIndex): Diagnostic[];
104
- /** Consumer-side diagnostics for class-attribute usage (unknown or deprecated modifiers). */
105
- declare function checkClassUsage(usages: readonly ClassUsage[], index: CssDocIndex): Diagnostic[];
174
+ declare function checkPropertyAssignments(assignments: readonly PropertyAssignment[], index: CssDocIndex, severities?: RuleSeverities): Diagnostic[];
175
+ /**
176
+ * Consumer-side diagnostics for class-attribute usage: unknown/deprecated modifiers, plus unknown
177
+ * state classes (`statePrefixes`) and unknown BEM element classes, routed by the token's kind.
178
+ */
179
+ declare function checkClassUsage(usages: readonly ClassUsage[], index: CssDocIndex, severities?: RuleSeverities): Diagnostic[];
106
180
  /** Consumer-side diagnostics for `var(--…)` references (unknown custom properties; opt-in via prefix). */
107
- declare function checkPropertyUsage(usages: readonly PropertyUsage[], index: CssDocIndex, options?: UsageOptions): Diagnostic[];
181
+ declare function checkPropertyUsage(usages: readonly PropertyUsage[], index: CssDocIndex, options?: UsageOptions, severities?: RuleSeverities): Diagnostic[];
108
182
  /** Completions for a class attribute: modifiers of `base` when given, else the component classes. */
109
183
  declare function completeClasses(base: string | undefined, index: CssDocIndex): Completion[];
110
184
  /** Completions for `var(--…)`: the declared custom properties. */
@@ -124,4 +198,4 @@ declare function definitionForCustomProperty(name: string, index: CssDocIndex):
124
198
  /** Definition of a custom function (its `@function` rule). */
125
199
  declare function definitionForFunction(name: string, index: CssDocIndex): Location$1 | undefined;
126
200
  //#endregion
127
- export { ASPECTS, Completion, CompletionKind, Diagnostic, Hover, type Location, Severity, type SourceSpan, SyntaxMatch, UsageOptions, checkClassUsage, checkPropertyAssignments, checkPropertyUsage, completeClasses, completeCustomProperties, completeFunctions, customProperty, definitionForClass, definitionForCustomProperty, definitionForFunction, func, hoverForClass, hoverForCustomProperty, hoverForFunction, lintModel, matchesSyntax, modifier, part, record };
201
+ export { ASPECTS, Completion, CompletionKind, DEFAULT_RULE_SEVERITIES, Diagnostic, Hover, type Location, NAME_CASE_PRESETS, NameCase, NamingRules, ResolvedNaming, RuleId, RuleSeverities, RuleSeverity, Severity, type SourceSpan, SyntaxMatch, UsageOptions, applyDirectives, checkClassUsage, checkPropertyAssignments, checkPropertyUsage, completeClasses, completeCustomProperties, completeFunctions, cssPart, customProperty, definitionForClass, definitionForCustomProperty, definitionForFunction, func, hoverForClass, hoverForCustomProperty, hoverForFunction, lintModel, matchesSyntax, modifier, parseDirectives, part, partUsage, record, resolveNaming, resolveRuleSeverities, stateUsage };
package/dist/index.mjs CHANGED
@@ -56,29 +56,69 @@ function matchesSyntax(syntax, value) {
56
56
  }
57
57
  //#endregion
58
58
  //#region src/aspects.ts
59
- /**
60
- * The aspect modules. Each documented aspect contributes some of: author-side model diagnostics,
61
- * consumer-side usage diagnostics, completions, hover, and definitions. All six aspects the model
62
- * carries are represented; the component hover folds in the structure, state, and condition facts, and
63
- * the generators (phase 3) surface them further. `index.ts` aggregates these into the public API.
64
- *
65
- * @module
66
- */
67
59
  const stripDot = (name) => name.replace(/^\./u, "");
68
60
  const warn = (d) => ({
69
61
  ...d,
70
62
  severity: "warning"
71
63
  });
64
+ /**
65
+ * Match a class name against a `structureIgnore` pattern — a literal name or a simple glob where `*`
66
+ * stands for any run of characters (e.g. `util-*`, `*--legacy`, `*`). Matched literally otherwise.
67
+ */
68
+ const globMatch = (pattern, value) => {
69
+ if (!pattern.includes("*")) return pattern === value;
70
+ return new RegExp(`^${pattern.replace(/[.+?^${}()|[\]\\]/gu, "\\$&").replace(/\*/gu, ".*")}$`, "u").test(value);
71
+ };
72
72
  const record = {
73
- model(index) {
73
+ model(index, naming, structureIgnore = []) {
74
74
  const out = [];
75
- for (const info of index.records) if (!info.entry.summary?.trim()) out.push(warn({
76
- aspect: "record",
77
- rule: "missing-summary",
78
- message: `Record "${info.entry.name}" has no @summary.`,
79
- record: info.entry.name,
80
- span: info.span
81
- }));
75
+ for (const info of index.records) {
76
+ if (!info.entry.summary?.trim()) out.push(warn({
77
+ aspect: "record",
78
+ rule: "missing-summary",
79
+ message: `Record "${info.entry.name}" has no @summary.`,
80
+ record: info.entry.name,
81
+ span: info.span
82
+ }));
83
+ if (naming?.component && info.entry.kind === "component" && info.entry.className) {
84
+ if (!naming.component.test(stripDot(info.entry.className))) out.push(warn({
85
+ aspect: "record",
86
+ rule: "component-name-case",
87
+ message: `Component class "${info.entry.className}" doesn't match the configured name case.`,
88
+ record: info.entry.name,
89
+ span: info.span
90
+ }));
91
+ }
92
+ if (info.entry.structure?.length) {
93
+ const known = /* @__PURE__ */ new Set([
94
+ stripDot(info.entry.className),
95
+ ...info.entry.parts.flatMap((p) => [stripDot(p.name), ...(p.modifiers ?? []).map((m) => m.name)]),
96
+ ...info.entry.shadowParts.map((p) => stripDot(p.name)),
97
+ ...info.entry.states.map((s) => s.name),
98
+ ...info.entry.modifiers.map((m) => m.name),
99
+ ...info.entry.slots.map((s) => s.name)
100
+ ]);
101
+ const seen = /* @__PURE__ */ new Set();
102
+ const walk = (nodes) => {
103
+ for (const node of nodes) {
104
+ for (const m of node.selector.matchAll(/\.([\w-]+)/gu)) {
105
+ const cls = m[1];
106
+ if (seen.has(cls) || known.has(cls) || structureIgnore.some((g) => globMatch(g, cls))) continue;
107
+ seen.add(cls);
108
+ out.push(warn({
109
+ aspect: "record",
110
+ rule: "structure-unknown-selector",
111
+ message: `@structure references ".${cls}", which isn't the component class or a documented member (add it, or list it under structureIgnore).`,
112
+ record: info.entry.name,
113
+ span: info.span
114
+ }));
115
+ }
116
+ walk(node.children);
117
+ }
118
+ };
119
+ walk(info.entry.structure);
120
+ }
121
+ }
82
122
  return out;
83
123
  },
84
124
  completions(index) {
@@ -99,6 +139,7 @@ const record = {
99
139
  const facets = [
100
140
  entry.modifiers.length && `${entry.modifiers.length} modifiers`,
101
141
  entry.parts.length && `${entry.parts.length} parts`,
142
+ entry.shadowParts.length && `${entry.shadowParts.length} shadow parts`,
102
143
  entry.states.length && `${entry.states.length} states`,
103
144
  entry.cssPropertiesDeclared.length && `${entry.cssPropertiesDeclared.length} custom properties`,
104
145
  entry.functions.length && `${entry.functions.length} functions`,
@@ -119,26 +160,27 @@ const modifier = {
119
160
  for (const info of index.records) {
120
161
  const name = info.entry.name;
121
162
  for (const m of info.entry.modifiers) {
163
+ const sel = index.matcher.selectorFor(m.name);
122
164
  const span = info.memberSpans.get(memberKey("modifier", m.name)) ?? info.span;
123
165
  if (!m.description?.trim() && !m.deprecated) out.push(warn({
124
166
  aspect: "modifier",
125
167
  rule: "undocumented-modifier",
126
- message: `Modifier ".${m.name}" of "${name}" has no @modifier description.`,
168
+ message: `Modifier "${sel}" of "${name}" has no @modifier description.`,
127
169
  record: name,
128
170
  span
129
171
  }));
130
172
  if (m.deprecated && !m.deprecated.canonical && !m.deprecated.note?.trim()) out.push(warn({
131
173
  aspect: "modifier",
132
174
  rule: "deprecated-requires-canonical",
133
- message: `Deprecated modifier ".${m.name}" of "${name}" needs a canonical replacement ({@link -x}) or a note.`,
175
+ message: `Deprecated modifier "${sel}" of "${name}" needs a canonical replacement ({@link}) or a note.`,
134
176
  record: name,
135
177
  span
136
178
  }));
137
179
  }
138
- for (const authored of info.authoredModifiers) if (!info.selectorText.includes(`.${authored}`)) out.push(warn({
180
+ for (const authored of info.authoredModifiers) if (!info.selectorText.includes(index.matcher.selectorFor(authored))) out.push(warn({
139
181
  aspect: "modifier",
140
182
  rule: "name-not-in-css",
141
- message: `Documented modifier ".${authored}" of "${name}" is not defined by any selector.`,
183
+ message: `Documented modifier "${index.matcher.selectorFor(authored)}" of "${name}" is not defined by any selector.`,
142
184
  record: name,
143
185
  span: info.span
144
186
  }));
@@ -146,23 +188,24 @@ const modifier = {
146
188
  return out;
147
189
  },
148
190
  classUsage(usage, index) {
149
- if (!usage.base || !usage.token.startsWith("-")) return [];
191
+ if (!usage.base || !index.matcher.looksLikeUsage(usage.token, usage.base)) return [];
150
192
  const entry = index.componentForClass(usage.base);
151
193
  if (!entry) return [];
194
+ const sel = index.matcher.selectorFor(index.matcher.normalizeMember(usage.token));
152
195
  if (!index.isModifier(usage.base, usage.token)) return [warn({
153
196
  aspect: "modifier",
154
197
  rule: "unknown-modifier",
155
- message: `".${usage.token}" is not a documented modifier of "${entry.name}".`,
198
+ message: `"${sel}" is not a documented modifier of "${entry.name}".`,
156
199
  record: entry.name,
157
200
  span: usage.loc
158
201
  })];
159
202
  const dep = index.deprecationOf(usage.base, usage.token);
160
203
  if (dep) {
161
- const advice = dep.canonical ? `use ".${dep.canonical}"` : dep.note ?? "no replacement given";
204
+ const advice = dep.canonical ? `use "${index.matcher.selectorFor(dep.canonical)}"` : dep.note ?? "no replacement given";
162
205
  return [warn({
163
206
  aspect: "modifier",
164
207
  rule: "deprecated-modifier",
165
- message: `Modifier ".${usage.token}" of "${entry.name}" is deprecated — ${advice}.`,
208
+ message: `Modifier "${sel}" of "${entry.name}" is deprecated — ${advice}.`,
166
209
  record: entry.name,
167
210
  span: usage.loc
168
211
  })];
@@ -173,7 +216,7 @@ const modifier = {
173
216
  const entry = index.componentForClass(base);
174
217
  if (!entry) return [];
175
218
  return entry.modifiers.map((m) => ({
176
- label: `-${m.name.replace(/^-/u, "")}`,
219
+ label: m.name,
177
220
  kind: "modifier",
178
221
  detail: m.prop,
179
222
  documentation: m.description,
@@ -182,32 +225,41 @@ const modifier = {
182
225
  },
183
226
  hover(base, token, index) {
184
227
  const entry = index.componentForClass(base);
185
- const m = entry?.modifiers.find((x) => x.name === stripDot(token));
228
+ const m = entry?.modifiers.find((x) => x.name === index.matcher.normalizeMember(token));
186
229
  if (!m) return void 0;
187
- const lines = [`\`.${m.name}\` — modifier of \`${entry.className}\``];
230
+ const lines = [`\`${index.matcher.selectorFor(m.name)}\` — modifier of \`${entry.className}\``];
188
231
  if (m.description) lines.push("", m.description);
189
232
  if (m.deprecated) {
190
- const advice = m.deprecated.canonical ? `use \`.${m.deprecated.canonical}\`` : m.deprecated.note ?? "";
233
+ const advice = m.deprecated.canonical ? `use \`${index.matcher.selectorFor(m.deprecated.canonical)}\`` : m.deprecated.note ?? "";
191
234
  lines.push("", `**Deprecated** — ${advice}`);
192
235
  }
193
236
  return { contents: lines.join("\n") };
194
237
  },
195
238
  definition(base, token, index) {
196
239
  const entry = index.componentForClass(base);
197
- return entry ? index.location(entry.name, memberKey("modifier", stripDot(token))) : void 0;
240
+ return entry ? index.location(entry.name, memberKey("modifier", index.matcher.normalizeMember(token))) : void 0;
198
241
  }
199
242
  };
200
- const part = { model(index) {
243
+ const part = { model(index, naming) {
201
244
  const out = [];
202
245
  for (const info of index.records) {
203
246
  const name = info.entry.name;
204
- for (const p of info.entry.parts) if (!p.description?.trim()) out.push(warn({
205
- aspect: "part",
206
- rule: "undocumented-part",
207
- message: `Part ".${p.name}" of "${name}" has no @part description.`,
208
- record: name,
209
- span: info.memberSpans.get(memberKey("part", p.name)) ?? info.span
210
- }));
247
+ for (const p of info.entry.parts) {
248
+ if (!p.description?.trim()) out.push(warn({
249
+ aspect: "part",
250
+ rule: "undocumented-part",
251
+ message: `Part ".${p.name}" of "${name}" has no @part description.`,
252
+ record: name,
253
+ span: info.memberSpans.get(memberKey("part", p.name)) ?? info.span
254
+ }));
255
+ if (naming?.part && !naming.part.test(p.name)) out.push(warn({
256
+ aspect: "part",
257
+ rule: "part-name-case",
258
+ message: `Part ".${p.name}" of "${name}" doesn't match the configured name case.`,
259
+ record: name,
260
+ span: info.memberSpans.get(memberKey("part", p.name)) ?? info.span
261
+ }));
262
+ }
211
263
  for (const authored of info.authoredParts) if (!info.selectorText.includes(`.${authored}`)) out.push(warn({
212
264
  aspect: "part",
213
265
  rule: "name-not-in-css",
@@ -218,6 +270,50 @@ const part = { model(index) {
218
270
  }
219
271
  return out;
220
272
  } };
273
+ const cssPart = { model(index) {
274
+ const out = [];
275
+ for (const info of index.records) {
276
+ const name = info.entry.name;
277
+ for (const p of info.entry.shadowParts) if (!p.description?.trim()) out.push(warn({
278
+ aspect: "css-part",
279
+ rule: "undocumented-css-part",
280
+ message: `Shadow part "::part(${p.name})" of "${name}" has no @csspart description.`,
281
+ record: name,
282
+ span: info.memberSpans.get(memberKey("shadow-part", p.name)) ?? info.span
283
+ }));
284
+ }
285
+ return out;
286
+ } };
287
+ /** A host-document class that looks like a state (via `statePrefixes`) but isn't a documented state. */
288
+ function stateUsage(usage, index) {
289
+ if (!usage.base) return [];
290
+ const entry = index.componentForClass(usage.base);
291
+ if (!entry) return [];
292
+ const name = usage.token.replace(/^\./u, "");
293
+ if (entry.states.some((s) => s.name === name)) return [];
294
+ return [warn({
295
+ aspect: "state",
296
+ rule: "unknown-state",
297
+ message: `".${name}" is not a documented state of "${entry.name}".`,
298
+ record: entry.name,
299
+ span: usage.loc
300
+ })];
301
+ }
302
+ /** A host-document class that looks like a BEM element (`base<sep>…`) but isn't a documented part. */
303
+ function partUsage(usage, index) {
304
+ if (!usage.base) return [];
305
+ const entry = index.componentForClass(usage.base);
306
+ if (!entry) return [];
307
+ const name = usage.token.replace(/^\./u, "");
308
+ if (entry.parts.some((p) => p.name === name || p.modifiers?.some((m) => m.name === name))) return [];
309
+ return [warn({
310
+ aspect: "part",
311
+ rule: "unknown-part",
312
+ message: `".${name}" is not a documented part of "${entry.name}".`,
313
+ record: entry.name,
314
+ span: usage.loc
315
+ })];
316
+ }
221
317
  function findProperty(index, name) {
222
318
  for (const rec of index.records) {
223
319
  const i = rec.entry.cssPropertiesDeclared.findIndex((p) => p.name === name);
@@ -328,12 +424,185 @@ const func = {
328
424
  }
329
425
  };
330
426
  //#endregion
427
+ //#region src/types.ts
428
+ /**
429
+ * The default rule severities. `unknown-modifier` defaults to `warn` — safe under the BEM default's
430
+ * strong `--` signal; lower it to `off` for weak-signal conventions (bare/OOCSS) where every chained
431
+ * class is a candidate. `unknown-custom-property` stays `off` (it needs an opt-in `propertyPrefix`).
432
+ */
433
+ const DEFAULT_RULE_SEVERITIES = {
434
+ "missing-summary": "warn",
435
+ "undocumented-modifier": "warn",
436
+ "deprecated-requires-canonical": "warn",
437
+ "name-not-in-css": "warn",
438
+ "unknown-modifier": "warn",
439
+ "deprecated-modifier": "warn",
440
+ "unknown-state": "warn",
441
+ "unknown-part": "warn",
442
+ "undocumented-part": "warn",
443
+ "undocumented-css-part": "warn",
444
+ "component-name-case": "warn",
445
+ "part-name-case": "warn",
446
+ "structure-unknown-selector": "warn",
447
+ "invalid-default-value": "warn",
448
+ "invalid-property-value": "warn",
449
+ "invalid-fallback-value": "warn",
450
+ "unknown-custom-property": "off",
451
+ "cssdoc-directive": "warn"
452
+ };
453
+ /**
454
+ * Merge per-rule overrides over {@link DEFAULT_RULE_SEVERITIES}. A `boolean` override is accepted for
455
+ * back-compat: `false` → `off`, `true` → the rule's default.
456
+ */
457
+ function resolveRuleSeverities(overrides) {
458
+ const resolved = { ...DEFAULT_RULE_SEVERITIES };
459
+ if (!overrides) return resolved;
460
+ for (const [rule, value] of Object.entries(overrides)) {
461
+ if (value === void 0) continue;
462
+ if (typeof value === "boolean") resolved[rule] = value ? DEFAULT_RULE_SEVERITIES[rule] : "off";
463
+ else resolved[rule] = value;
464
+ }
465
+ return resolved;
466
+ }
467
+ /** The built-in name-case patterns, tested against a class name without its leading dot. */
468
+ const NAME_CASE_PRESETS = {
469
+ pascalCase: /^[A-Z][A-Za-z0-9]*$/u,
470
+ camelCase: /^[a-z][A-Za-z0-9]*$/u,
471
+ lowercase: /^[a-z][a-z0-9-]*$/u
472
+ };
473
+ /** Compile one {@link NameCase} to a regex: a preset, else the string as a custom pattern. */
474
+ function compileNameCase(spec) {
475
+ if (!spec) return void 0;
476
+ if (spec in NAME_CASE_PRESETS) return NAME_CASE_PRESETS[spec];
477
+ try {
478
+ return new RegExp(spec, "u");
479
+ } catch {
480
+ return;
481
+ }
482
+ }
483
+ /** Compile {@link NamingRules} to regexes once, for the record/part name-case checks. */
484
+ function resolveNaming(naming) {
485
+ return {
486
+ component: compileNameCase(naming?.component),
487
+ part: compileNameCase(naming?.part)
488
+ };
489
+ }
490
+ //#endregion
491
+ //#region src/directives.ts
492
+ const DIRECTIVE_RE = /\/\*\s*cssdoc-(disable-next-line|disable-line|disable|enable|expect-error)\b([^*]*?)\*\//gu;
493
+ const lineAt = (source, index) => {
494
+ let line = 1;
495
+ for (let i = 0; i < index && i < source.length; i++) if (source[i] === "\n") line += 1;
496
+ return line;
497
+ };
498
+ /** Parse every `cssdoc-*` directive comment out of a CSS source. */
499
+ function parseDirectives(source) {
500
+ const out = [];
501
+ for (const m of source.matchAll(DIRECTIVE_RE)) {
502
+ const at = m.index ?? 0;
503
+ const rest = m[2].trim();
504
+ out.push({
505
+ kind: m[1],
506
+ rules: rest ? rest.split(/[\s,]+/u).filter(Boolean) : null,
507
+ startLine: lineAt(source, at),
508
+ endLine: lineAt(source, at + m[0].length - 1)
509
+ });
510
+ }
511
+ return out;
512
+ }
513
+ const scopesRule = (d, rule) => d.rules === null || d.rules.includes(rule);
514
+ /**
515
+ * Drop diagnostics suppressed by directive comments in `source`, and add a `cssdoc-directive`
516
+ * diagnostic for any `cssdoc-expect-error` that matched nothing.
517
+ *
518
+ * @param diagnostics - The diagnostics to filter (author-side, with CSS spans).
519
+ * @param source - The CSS the diagnostics came from (the same text passed to the linter).
520
+ * @returns The surviving diagnostics.
521
+ */
522
+ function applyDirectives(diagnostics, source) {
523
+ const directives = parseDirectives(source);
524
+ if (directives.length === 0) return diagnostics;
525
+ const block = directives.filter((d) => d.kind === "disable" || d.kind === "enable").sort((a, b) => a.startLine - b.startLine);
526
+ const perLine = directives.filter((d) => d.kind !== "disable" && d.kind !== "enable");
527
+ const blockDisabled = (line, rule) => {
528
+ let all = false;
529
+ const disabled = /* @__PURE__ */ new Set();
530
+ const exceptions = /* @__PURE__ */ new Set();
531
+ for (const d of block) {
532
+ if (d.startLine > line) break;
533
+ if (d.kind === "disable" && d.rules === null) {
534
+ all = true;
535
+ exceptions.clear();
536
+ } else if (d.kind === "disable") for (const r of d.rules ?? []) if (all) exceptions.delete(r);
537
+ else disabled.add(r);
538
+ else if (d.rules === null) {
539
+ all = false;
540
+ disabled.clear();
541
+ exceptions.clear();
542
+ } else for (const r of d.rules) if (all) exceptions.add(r);
543
+ else disabled.delete(r);
544
+ }
545
+ return all ? !exceptions.has(rule) : disabled.has(rule);
546
+ };
547
+ const lineDirectiveFor = (line, rule) => perLine.find((d) => scopesRule(d, rule) && (d.kind === "disable-line" ? line >= d.startLine && line <= d.endLine : line === d.endLine + 1));
548
+ const satisfied = /* @__PURE__ */ new Set();
549
+ const kept = [];
550
+ for (const diag of diagnostics) {
551
+ const line = diag.span?.start.line;
552
+ if (line === void 0) {
553
+ kept.push(diag);
554
+ continue;
555
+ }
556
+ const hit = lineDirectiveFor(line, diag.rule);
557
+ if (hit) {
558
+ satisfied.add(hit);
559
+ continue;
560
+ }
561
+ if (!blockDisabled(line, diag.rule)) kept.push(diag);
562
+ }
563
+ for (const d of perLine) if (d.kind === "expect-error" && !satisfied.has(d)) kept.push({
564
+ aspect: "directive",
565
+ rule: "cssdoc-directive",
566
+ message: "Unused cssdoc-expect-error: no cssdoc problem was reported on the following line.",
567
+ span: {
568
+ start: {
569
+ line: d.startLine,
570
+ column: 1
571
+ },
572
+ end: {
573
+ line: d.endLine,
574
+ column: 2
575
+ }
576
+ },
577
+ severity: "warning"
578
+ });
579
+ return kept;
580
+ }
581
+ //#endregion
331
582
  //#region src/index.ts
583
+ /**
584
+ * Apply resolved rule severities to a batch of diagnostics: drop `off` rules, and stamp the configured
585
+ * severity onto the rest. This is the single place severity/enablement is decided — every host inherits
586
+ * it. Aspects emit a placeholder `warning`; this overrides it.
587
+ */
588
+ function applySeverities(diagnostics, severities) {
589
+ const out = [];
590
+ for (const d of diagnostics) {
591
+ const severity = severities[d.rule] ?? "warn";
592
+ if (severity === "off") continue;
593
+ out.push({
594
+ ...d,
595
+ severity: severity === "error" ? "error" : "warning"
596
+ });
597
+ }
598
+ return out;
599
+ }
332
600
  /** The aspect names covered, in a stable order (the extension point for future aspects). */
333
601
  const ASPECTS = [
334
602
  "record",
335
603
  "modifier",
336
604
  "part",
605
+ "css-part",
337
606
  "custom-property",
338
607
  "structure",
339
608
  "function",
@@ -341,25 +610,34 @@ const ASPECTS = [
341
610
  "condition"
342
611
  ];
343
612
  /** Author-side hygiene diagnostics over the whole model (missing summaries, undocumented members, drift, invalid defaults). */
344
- function lintModel(index) {
345
- return [
346
- ...record.model(index),
613
+ function lintModel(index, severities = DEFAULT_RULE_SEVERITIES, naming, structureIgnore) {
614
+ return applySeverities([
615
+ ...record.model(index, naming, structureIgnore),
347
616
  ...modifier.model(index),
348
- ...part.model(index),
617
+ ...part.model(index, naming),
618
+ ...cssPart.model(index),
349
619
  ...customProperty.model(index)
350
- ];
620
+ ], severities);
351
621
  }
352
622
  /** Consumer-side diagnostics for custom-property assignments (`--x: value` must match its `@property` syntax). */
353
- function checkPropertyAssignments(assignments, index) {
354
- return assignments.flatMap((a) => customProperty.assignment(a, index));
623
+ function checkPropertyAssignments(assignments, index, severities = DEFAULT_RULE_SEVERITIES) {
624
+ return applySeverities(assignments.flatMap((a) => customProperty.assignment(a, index)), severities);
355
625
  }
356
- /** Consumer-side diagnostics for class-attribute usage (unknown or deprecated modifiers). */
357
- function checkClassUsage(usages, index) {
358
- return usages.flatMap((usage) => modifier.classUsage(usage, index));
626
+ /**
627
+ * Consumer-side diagnostics for class-attribute usage: unknown/deprecated modifiers, plus unknown
628
+ * state classes (`statePrefixes`) and unknown BEM element classes, routed by the token's kind.
629
+ */
630
+ function checkClassUsage(usages, index, severities = DEFAULT_RULE_SEVERITIES) {
631
+ return applySeverities(usages.flatMap((usage) => {
632
+ const kind = usage.base ? index.matcher.usageKind(usage.token, usage.base) : void 0;
633
+ if (kind === "state") return stateUsage(usage, index);
634
+ if (kind === "element") return partUsage(usage, index);
635
+ return modifier.classUsage(usage, index);
636
+ }), severities);
359
637
  }
360
638
  /** Consumer-side diagnostics for `var(--…)` references (unknown custom properties; opt-in via prefix). */
361
- function checkPropertyUsage(usages, index, options = {}) {
362
- return usages.flatMap((usage) => customProperty.propertyUsage(usage, index, options));
639
+ function checkPropertyUsage(usages, index, options = {}, severities = DEFAULT_RULE_SEVERITIES) {
640
+ return applySeverities(usages.flatMap((usage) => customProperty.propertyUsage(usage, index, options)), severities);
363
641
  }
364
642
  /** Completions for a class attribute: modifiers of `base` when given, else the component classes. */
365
643
  function completeClasses(base, index) {
@@ -398,4 +676,4 @@ function definitionForFunction(name, index) {
398
676
  return func.definition(name, index);
399
677
  }
400
678
  //#endregion
401
- export { ASPECTS, checkClassUsage, checkPropertyAssignments, checkPropertyUsage, completeClasses, completeCustomProperties, completeFunctions, customProperty, definitionForClass, definitionForCustomProperty, definitionForFunction, func, hoverForClass, hoverForCustomProperty, hoverForFunction, lintModel, matchesSyntax, modifier, part, record };
679
+ export { ASPECTS, DEFAULT_RULE_SEVERITIES, NAME_CASE_PRESETS, applyDirectives, checkClassUsage, checkPropertyAssignments, checkPropertyUsage, completeClasses, completeCustomProperties, completeFunctions, cssPart, customProperty, definitionForClass, definitionForCustomProperty, definitionForFunction, func, hoverForClass, hoverForCustomProperty, hoverForFunction, lintModel, matchesSyntax, modifier, parseDirectives, part, partUsage, record, resolveNaming, resolveRuleSeverities, stateUsage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cssdoc/providers",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Host-agnostic aspect providers over the @cssdoc/index — diagnostics, completions, hover, and definitions for modifiers, custom properties, structure, functions, states, and conditions.",
5
5
  "keywords": [
6
6
  "css",
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "css-tree": "^3.2.1",
32
- "@cssdoc/core": "0.1.0",
33
- "@cssdoc/index": "0.1.0"
32
+ "@cssdoc/core": "0.3.0",
33
+ "@cssdoc/index": "0.3.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/css-tree": "^2.3.11",