@cssdoc/providers 0.2.0 → 0.3.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 +59 -6
- package/dist/index.mjs +256 -31
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -10,7 +10,7 @@ type RuleSeverity = "off" | "warn" | "error";
|
|
|
10
10
|
* author-side `RuleName` (which omits the consumer-side `unknown-modifier`/`deprecated-modifier` and
|
|
11
11
|
* the opt-in `unknown-custom-property`).
|
|
12
12
|
*/
|
|
13
|
-
type RuleId = "missing-summary" | "undocumented-modifier" | "deprecated-requires-canonical" | "name-not-in-css" | "unknown-modifier" | "deprecated-modifier" | "undocumented-part" | "invalid-default-value" | "invalid-property-value" | "invalid-fallback-value" | "unknown-custom-property";
|
|
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
14
|
/** A resolved severity for every rule. */
|
|
15
15
|
type RuleSeverities = Record<RuleId, RuleSeverity>;
|
|
16
16
|
/**
|
|
@@ -65,6 +65,27 @@ interface UsageOptions {
|
|
|
65
65
|
*/
|
|
66
66
|
propertyPrefix?: string;
|
|
67
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;
|
|
68
89
|
//#endregion
|
|
69
90
|
//#region src/syntax.d.ts
|
|
70
91
|
/** The result of matching a value against a syntax. */
|
|
@@ -84,9 +105,31 @@ interface SyntaxMatch {
|
|
|
84
105
|
*/
|
|
85
106
|
declare function matchesSyntax(syntax: string, value: string): SyntaxMatch;
|
|
86
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
|
|
87
130
|
//#region src/aspects.d.ts
|
|
88
131
|
declare const record: {
|
|
89
|
-
model(index: CssDocIndex): Diagnostic[];
|
|
132
|
+
model(index: CssDocIndex, naming?: ResolvedNaming, structureIgnore?: readonly string[]): Diagnostic[];
|
|
90
133
|
completions(index: CssDocIndex): Completion[];
|
|
91
134
|
hover(base: string, index: CssDocIndex): Hover | undefined;
|
|
92
135
|
definition(base: string, index: CssDocIndex): Location$1 | undefined;
|
|
@@ -99,8 +142,15 @@ declare const modifier: {
|
|
|
99
142
|
definition(base: string, token: string, index: CssDocIndex): Location$1 | undefined;
|
|
100
143
|
};
|
|
101
144
|
declare const part: {
|
|
145
|
+
model(index: CssDocIndex, naming?: ResolvedNaming): Diagnostic[];
|
|
146
|
+
};
|
|
147
|
+
declare const cssPart: {
|
|
102
148
|
model(index: CssDocIndex): Diagnostic[];
|
|
103
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[];
|
|
104
154
|
declare const customProperty: {
|
|
105
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. */
|
|
106
156
|
assignment(a: PropertyAssignment, index: CssDocIndex): Diagnostic[];
|
|
@@ -117,12 +167,15 @@ declare const func: {
|
|
|
117
167
|
//#endregion
|
|
118
168
|
//#region src/index.d.ts
|
|
119
169
|
/** The aspect names covered, in a stable order (the extension point for future aspects). */
|
|
120
|
-
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"];
|
|
121
171
|
/** Author-side hygiene diagnostics over the whole model (missing summaries, undocumented members, drift, invalid defaults). */
|
|
122
|
-
declare function lintModel(index: CssDocIndex, severities?: RuleSeverities): Diagnostic[];
|
|
172
|
+
declare function lintModel(index: CssDocIndex, severities?: RuleSeverities, naming?: ResolvedNaming, structureIgnore?: readonly string[]): Diagnostic[];
|
|
123
173
|
/** Consumer-side diagnostics for custom-property assignments (`--x: value` must match its `@property` syntax). */
|
|
124
174
|
declare function checkPropertyAssignments(assignments: readonly PropertyAssignment[], index: CssDocIndex, severities?: RuleSeverities): Diagnostic[];
|
|
125
|
-
/**
|
|
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
|
+
*/
|
|
126
179
|
declare function checkClassUsage(usages: readonly ClassUsage[], index: CssDocIndex, severities?: RuleSeverities): Diagnostic[];
|
|
127
180
|
/** Consumer-side diagnostics for `var(--…)` references (unknown custom properties; opt-in via prefix). */
|
|
128
181
|
declare function checkPropertyUsage(usages: readonly PropertyUsage[], index: CssDocIndex, options?: UsageOptions, severities?: RuleSeverities): Diagnostic[];
|
|
@@ -145,4 +198,4 @@ declare function definitionForCustomProperty(name: string, index: CssDocIndex):
|
|
|
145
198
|
/** Definition of a custom function (its `@function` rule). */
|
|
146
199
|
declare function definitionForFunction(name: string, index: CssDocIndex): Location$1 | undefined;
|
|
147
200
|
//#endregion
|
|
148
|
-
export { ASPECTS, Completion, CompletionKind, DEFAULT_RULE_SEVERITIES, Diagnostic, Hover, type Location, RuleId, RuleSeverities, RuleSeverity, Severity, type SourceSpan, SyntaxMatch, UsageOptions, checkClassUsage, checkPropertyAssignments, checkPropertyUsage, completeClasses, completeCustomProperties, completeFunctions, customProperty, definitionForClass, definitionForCustomProperty, definitionForFunction, func, hoverForClass, hoverForCustomProperty, hoverForFunction, lintModel, matchesSyntax, modifier, part, record, resolveRuleSeverities };
|
|
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)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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`,
|
|
@@ -199,17 +240,26 @@ const modifier = {
|
|
|
199
240
|
return entry ? index.location(entry.name, memberKey("modifier", index.matcher.normalizeMember(token))) : void 0;
|
|
200
241
|
}
|
|
201
242
|
};
|
|
202
|
-
const part = { model(index) {
|
|
243
|
+
const part = { model(index, naming) {
|
|
203
244
|
const out = [];
|
|
204
245
|
for (const info of index.records) {
|
|
205
246
|
const name = info.entry.name;
|
|
206
|
-
for (const p of info.entry.parts)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
}
|
|
213
263
|
for (const authored of info.authoredParts) if (!info.selectorText.includes(`.${authored}`)) out.push(warn({
|
|
214
264
|
aspect: "part",
|
|
215
265
|
rule: "name-not-in-css",
|
|
@@ -220,6 +270,50 @@ const part = { model(index) {
|
|
|
220
270
|
}
|
|
221
271
|
return out;
|
|
222
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
|
+
}
|
|
223
317
|
function findProperty(index, name) {
|
|
224
318
|
for (const rec of index.records) {
|
|
225
319
|
const i = rec.entry.cssPropertiesDeclared.findIndex((p) => p.name === name);
|
|
@@ -343,11 +437,18 @@ const DEFAULT_RULE_SEVERITIES = {
|
|
|
343
437
|
"name-not-in-css": "warn",
|
|
344
438
|
"unknown-modifier": "warn",
|
|
345
439
|
"deprecated-modifier": "warn",
|
|
440
|
+
"unknown-state": "warn",
|
|
441
|
+
"unknown-part": "warn",
|
|
346
442
|
"undocumented-part": "warn",
|
|
443
|
+
"undocumented-css-part": "warn",
|
|
444
|
+
"component-name-case": "warn",
|
|
445
|
+
"part-name-case": "warn",
|
|
446
|
+
"structure-unknown-selector": "warn",
|
|
347
447
|
"invalid-default-value": "warn",
|
|
348
448
|
"invalid-property-value": "warn",
|
|
349
449
|
"invalid-fallback-value": "warn",
|
|
350
|
-
"unknown-custom-property": "off"
|
|
450
|
+
"unknown-custom-property": "off",
|
|
451
|
+
"cssdoc-directive": "warn"
|
|
351
452
|
};
|
|
352
453
|
/**
|
|
353
454
|
* Merge per-rule overrides over {@link DEFAULT_RULE_SEVERITIES}. A `boolean` override is accepted for
|
|
@@ -363,6 +464,120 @@ function resolveRuleSeverities(overrides) {
|
|
|
363
464
|
}
|
|
364
465
|
return resolved;
|
|
365
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
|
+
}
|
|
366
581
|
//#endregion
|
|
367
582
|
//#region src/index.ts
|
|
368
583
|
/**
|
|
@@ -387,6 +602,7 @@ const ASPECTS = [
|
|
|
387
602
|
"record",
|
|
388
603
|
"modifier",
|
|
389
604
|
"part",
|
|
605
|
+
"css-part",
|
|
390
606
|
"custom-property",
|
|
391
607
|
"structure",
|
|
392
608
|
"function",
|
|
@@ -394,11 +610,12 @@ const ASPECTS = [
|
|
|
394
610
|
"condition"
|
|
395
611
|
];
|
|
396
612
|
/** Author-side hygiene diagnostics over the whole model (missing summaries, undocumented members, drift, invalid defaults). */
|
|
397
|
-
function lintModel(index, severities = DEFAULT_RULE_SEVERITIES) {
|
|
613
|
+
function lintModel(index, severities = DEFAULT_RULE_SEVERITIES, naming, structureIgnore) {
|
|
398
614
|
return applySeverities([
|
|
399
|
-
...record.model(index),
|
|
615
|
+
...record.model(index, naming, structureIgnore),
|
|
400
616
|
...modifier.model(index),
|
|
401
|
-
...part.model(index),
|
|
617
|
+
...part.model(index, naming),
|
|
618
|
+
...cssPart.model(index),
|
|
402
619
|
...customProperty.model(index)
|
|
403
620
|
], severities);
|
|
404
621
|
}
|
|
@@ -406,9 +623,17 @@ function lintModel(index, severities = DEFAULT_RULE_SEVERITIES) {
|
|
|
406
623
|
function checkPropertyAssignments(assignments, index, severities = DEFAULT_RULE_SEVERITIES) {
|
|
407
624
|
return applySeverities(assignments.flatMap((a) => customProperty.assignment(a, index)), severities);
|
|
408
625
|
}
|
|
409
|
-
/**
|
|
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
|
+
*/
|
|
410
630
|
function checkClassUsage(usages, index, severities = DEFAULT_RULE_SEVERITIES) {
|
|
411
|
-
return applySeverities(usages.flatMap((usage) =>
|
|
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);
|
|
412
637
|
}
|
|
413
638
|
/** Consumer-side diagnostics for `var(--…)` references (unknown custom properties; opt-in via prefix). */
|
|
414
639
|
function checkPropertyUsage(usages, index, options = {}, severities = DEFAULT_RULE_SEVERITIES) {
|
|
@@ -451,4 +676,4 @@ function definitionForFunction(name, index) {
|
|
|
451
676
|
return func.definition(name, index);
|
|
452
677
|
}
|
|
453
678
|
//#endregion
|
|
454
|
-
export { ASPECTS, DEFAULT_RULE_SEVERITIES, checkClassUsage, checkPropertyAssignments, checkPropertyUsage, completeClasses, completeCustomProperties, completeFunctions, customProperty, definitionForClass, definitionForCustomProperty, definitionForFunction, func, hoverForClass, hoverForCustomProperty, hoverForFunction, lintModel, matchesSyntax, modifier, part, record, resolveRuleSeverities };
|
|
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.
|
|
3
|
+
"version": "0.3.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.
|
|
33
|
-
"@cssdoc/index": "0.
|
|
32
|
+
"@cssdoc/core": "0.3.1",
|
|
33
|
+
"@cssdoc/index": "0.3.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/css-tree": "^2.3.11",
|