@cssdoc/providers 0.4.2 → 0.5.1

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
@@ -38,6 +38,14 @@ interface Diagnostic {
38
38
  span?: SourceSpan$1;
39
39
  /** Severity (defaults to `warning`). */
40
40
  severity: Severity;
41
+ /**
42
+ * Optional structured payload for consumers. `maskedName` is the class token as it appears in the
43
+ * linted (possibly projected) source, so the language server can restore an embedded `${…}`
44
+ * interpolation for display instead of the masked filler.
45
+ */
46
+ data?: {
47
+ maskedName?: string;
48
+ };
41
49
  }
42
50
  /** What kind of thing a completion inserts. */
43
51
  type CompletionKind = "component" | "modifier" | "part" | "property" | "function" | "state";
@@ -177,7 +185,7 @@ declare function applyDirectives(diagnostics: Diagnostic[], source: string): Dia
177
185
  //#endregion
178
186
  //#region src/aspects.d.ts
179
187
  declare const record: {
180
- model(index: CssDocIndex, naming?: ResolvedNaming, structureIgnore?: readonly string[]): Diagnostic[];
188
+ model(index: CssDocIndex, naming?: ResolvedNaming, structureIgnore?: readonly string[], siblingIndex?: CssDocIndex): Diagnostic[];
181
189
  completions(index: CssDocIndex): Completion[];
182
190
  hover(base: string, index: CssDocIndex, detail?: HoverDetail, sections?: HoverSections, sectionOrder?: HoverSectionOrder): Hover | undefined;
183
191
  definition(base: string, index: CssDocIndex): Location$1 | undefined;
@@ -217,7 +225,7 @@ declare const func: {
217
225
  /** The aspect names covered, in a stable order (the extension point for future aspects). */
218
226
  declare const ASPECTS: readonly ["record", "modifier", "part", "css-part", "custom-property", "structure", "function", "state", "condition"];
219
227
  /** Author-side hygiene diagnostics over the whole model (missing summaries, undocumented members, drift, invalid defaults). */
220
- declare function lintModel(index: CssDocIndex, severities?: RuleSeverities, naming?: ResolvedNaming, structureIgnore?: readonly string[]): Diagnostic[];
228
+ declare function lintModel(index: CssDocIndex, severities?: RuleSeverities, naming?: ResolvedNaming, structureIgnore?: readonly string[], siblingIndex?: CssDocIndex): Diagnostic[];
221
229
  /** Consumer-side diagnostics for custom-property assignments (`--x: value` must match its `@property` syntax). */
222
230
  declare function checkPropertyAssignments(assignments: readonly PropertyAssignment[], index: CssDocIndex, severities?: RuleSeverities): Diagnostic[];
223
231
  /**
package/dist/index.mjs CHANGED
@@ -177,6 +177,38 @@ const warn = (d) => ({
177
177
  ...d,
178
178
  severity: "warning"
179
179
  });
180
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
181
+ /** A `*`-glob name (e.g. `-icon-*`) as a regex source over one class token (`*` → any `[\w-]` run). */
182
+ const globSource = (name) => name.split("*").map(escapeRe).join("[\\w-]*");
183
+ /** `class` attribute selectors, capturing the operator (`~`/`^`/`$`/`*`/`|`/none) and the value. */
184
+ const CLASS_ATTR_RE = /\[\s*class\s*([~^$*|]?)=\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]*))\s*\]/gu;
185
+ /**
186
+ * Whether `selectorText` (the record's concatenated selectors) defines the modifier/part `selector`
187
+ * (`.name`, where `name` may be a `*` glob for a family like `.-icon-*`; a name with no `*` is exact).
188
+ * Beyond a literal class token, `class` attribute selectors count with their real CSS operator
189
+ * semantics — `[class~=v]`/`[class=v]` (exact word), `[class*=v]` (substring), and `[class$=v]` (suffix)
190
+ * can define a chained modifier; `[class^=v]` cannot, since `^=` anchors to the start of the whole
191
+ * attribute (the base class), never a chained modifier.
192
+ */
193
+ const selectorDefines = (selectorText, selector) => {
194
+ if (!selector.startsWith(".")) return selectorText.includes(selector);
195
+ const name = stripDot(selector);
196
+ const wild = name.includes("*");
197
+ const prefix = name.split("*")[0];
198
+ if (new RegExp(`\\.${globSource(name)}(?![\\w-])`, "u").test(selectorText)) return true;
199
+ const glob = new RegExp(`^${globSource(name)}$`, "u");
200
+ for (const m of selectorText.matchAll(CLASS_ATTR_RE)) {
201
+ const op = m[1];
202
+ const v = m[2] ?? m[3] ?? m[4] ?? "";
203
+ if (!v || op === "^") continue;
204
+ if (op === "*") {
205
+ if (wild ? prefix.includes(v) || v.includes(prefix) : name.includes(v)) return true;
206
+ } else if (op === "$") {
207
+ if (wild ? v.endsWith(prefix) || prefix.endsWith(v) : name.endsWith(v)) return true;
208
+ } else if (glob.test(v)) return true;
209
+ }
210
+ return false;
211
+ };
180
212
  /**
181
213
  * Match a class name against a `structureIgnore` pattern — a literal name or a simple glob where `*`
182
214
  * stands for any run of characters (e.g. `util-*`, `*--legacy`, `*`). Matched literally otherwise.
@@ -199,8 +231,10 @@ const renderStructureTree = (nodes, depth = 0) => nodes.flatMap((n) => {
199
231
  ] : [`${pad}${n.selector}`];
200
232
  });
201
233
  const record = {
202
- model(index, naming, structureIgnore = []) {
234
+ model(index, naming, structureIgnore = [], siblingIndex = index) {
203
235
  const out = [];
236
+ const siblingClasses = new Set(siblingIndex.records.map((r) => stripDot(r.entry.className)));
237
+ const siblingNames = new Set(siblingIndex.records.flatMap((r) => r.entry.kind === "component" ? [r.entry.name] : []));
204
238
  for (const info of index.records) {
205
239
  if (!info.entry.summary?.trim()) out.push(warn({
206
240
  aspect: "record",
@@ -221,25 +255,30 @@ const record = {
221
255
  if (info.entry.structure?.length) {
222
256
  const known = /* @__PURE__ */ new Set([
223
257
  stripDot(info.entry.className),
258
+ ...siblingClasses,
224
259
  ...info.entry.parts.flatMap((p) => [stripDot(p.name), ...(p.modifiers ?? []).map((m) => m.name)]),
225
260
  ...info.entry.shadowParts.map((p) => stripDot(p.name)),
226
261
  ...info.entry.states.map((s) => s.name),
227
262
  ...info.entry.modifiers.map((m) => m.name),
228
263
  ...info.entry.slots.map((s) => s.name)
229
264
  ]);
265
+ const own = stripDot(info.entry.className);
266
+ const prefix = info.entry.name && own.endsWith(info.entry.name) ? own.slice(0, own.length - info.entry.name.length) : "";
267
+ const isSibling = (cls) => prefix !== "" && cls.startsWith(prefix) && siblingNames.has(cls.slice(prefix.length));
230
268
  const seen = /* @__PURE__ */ new Set();
231
269
  const walk = (nodes) => {
232
270
  for (const node of nodes) {
233
271
  for (const m of node.selector.matchAll(/\.([\w-]+)/gu)) {
234
272
  const cls = m[1];
235
- if (seen.has(cls) || known.has(cls) || structureIgnore.some((g) => globMatch(g, cls))) continue;
273
+ if (seen.has(cls) || known.has(cls) || isSibling(cls) || structureIgnore.some((g) => globMatch(g, cls))) continue;
236
274
  seen.add(cls);
237
275
  out.push(warn({
238
276
  aspect: "record",
239
277
  rule: "structure-unknown-selector",
240
278
  message: `@structure references ".${cls}", which isn't the component class or a documented member (add it, or list it under structureIgnore).`,
241
279
  record: info.entry.name,
242
- span: info.span
280
+ span: info.span,
281
+ data: { maskedName: cls }
243
282
  }));
244
283
  }
245
284
  walk(node.children);
@@ -390,13 +429,18 @@ const modifier = {
390
429
  span
391
430
  }));
392
431
  }
393
- for (const authored of info.authoredModifiers) if (!info.selectorText.includes(index.matcher.selectorFor(authored))) out.push(warn({
394
- aspect: "modifier",
395
- rule: "name-not-in-css",
396
- message: `Documented modifier "${index.matcher.selectorFor(authored)}" of "${name}" is not defined by any selector.`,
397
- record: name,
398
- span: info.span
399
- }));
432
+ const deprecated = new Set(info.entry.modifiers.filter((m) => m.deprecated).map((m) => m.name));
433
+ for (const authored of info.authoredModifiers) {
434
+ if (deprecated.has(authored)) continue;
435
+ const sel = index.matcher.selectorFor(authored);
436
+ if (!selectorDefines(info.selectorText, sel)) out.push(warn({
437
+ aspect: "modifier",
438
+ rule: "name-not-in-css",
439
+ message: `Documented modifier "${sel}" of "${name}" is not defined by any selector.`,
440
+ record: name,
441
+ span: info.span
442
+ }));
443
+ }
400
444
  }
401
445
  return out;
402
446
  },
@@ -473,7 +517,7 @@ const part = { model(index, naming) {
473
517
  span: info.memberSpans.get(memberKey("part", p.name)) ?? info.span
474
518
  }));
475
519
  }
476
- for (const authored of info.authoredParts) if (!info.selectorText.includes(`.${authored}`)) out.push(warn({
520
+ for (const authored of info.authoredParts) if (!selectorDefines(info.selectorText, `.${authored}`)) out.push(warn({
477
521
  aspect: "part",
478
522
  rule: "name-not-in-css",
479
523
  message: `Documented part ".${authored}" of "${name}" is not defined by any selector.`,
@@ -762,9 +806,9 @@ const ASPECTS = [
762
806
  "condition"
763
807
  ];
764
808
  /** Author-side hygiene diagnostics over the whole model (missing summaries, undocumented members, drift, invalid defaults). */
765
- function lintModel(index, severities = DEFAULT_RULE_SEVERITIES, naming, structureIgnore) {
809
+ function lintModel(index, severities = DEFAULT_RULE_SEVERITIES, naming, structureIgnore, siblingIndex) {
766
810
  return applySeverities([
767
- ...record.model(index, naming, structureIgnore),
811
+ ...record.model(index, naming, structureIgnore, siblingIndex),
768
812
  ...modifier.model(index),
769
813
  ...part.model(index, naming),
770
814
  ...cssPart.model(index),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cssdoc/providers",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
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.4.2",
33
- "@cssdoc/index": "0.4.2"
32
+ "@cssdoc/core": "0.5.1",
33
+ "@cssdoc/index": "0.5.1"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/css-tree": "^2.3.11",