@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 +82 -8
- package/dist/index.mjs +328 -50
- package/package.json +3 -3
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
|
-
/**
|
|
105
|
-
|
|
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)
|
|
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`,
|
|
@@ -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 "
|
|
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 "
|
|
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(
|
|
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 "
|
|
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.
|
|
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: `"
|
|
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 "
|
|
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 "
|
|
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:
|
|
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 ===
|
|
228
|
+
const m = entry?.modifiers.find((x) => x.name === index.matcher.normalizeMember(token));
|
|
186
229
|
if (!m) return void 0;
|
|
187
|
-
const lines = [
|
|
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
|
|
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",
|
|
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)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
/**
|
|
357
|
-
|
|
358
|
-
|
|
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.
|
|
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.
|
|
33
|
-
"@cssdoc/index": "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",
|