@cssdoc/providers 0.1.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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/index.d.mts +127 -0
- package/dist/index.mjs +401 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Danny Wahl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @cssdoc/providers
|
|
2
|
+
|
|
3
|
+
The host-agnostic language-service core. Given a [`@cssdoc/index`](../index), it produces diagnostics,
|
|
4
|
+
completions, hover, and definitions — built from modular per-aspect modules (modifier, custom property,
|
|
5
|
+
structure, function, state, condition, plus record and part). Every cssdoc integration — the Stylelint
|
|
6
|
+
and ESLint plugins, and the language server — is a thin translation of these into its host's API.
|
|
7
|
+
|
|
8
|
+
## API
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createIndex } from "@cssdoc/index";
|
|
12
|
+
import {
|
|
13
|
+
lintModel, // author-side hygiene
|
|
14
|
+
checkClassUsage, // consumer-side: unknown/deprecated modifiers
|
|
15
|
+
checkPropertyUsage, // consumer-side: unknown var(--…) (opt-in)
|
|
16
|
+
completeClasses, // components, or a component's modifiers
|
|
17
|
+
completeCustomProperties,
|
|
18
|
+
hoverForClass,
|
|
19
|
+
definitionForClass,
|
|
20
|
+
} from "@cssdoc/providers";
|
|
21
|
+
|
|
22
|
+
const index = createIndex(css, { file: "components.css" });
|
|
23
|
+
checkClassUsage([{ base: "btn", tokens: ["btn", "-x"], token: "-x" }], index);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Adding an aspect is one module in `aspects.ts` plus a line in the aggregate — no adapter changes.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { ClassUsage, CssDocIndex, Location, Location as Location$1, PropertyAssignment, PropertyUsage, SourceSpan, SourceSpan as SourceSpan$1 } from "@cssdoc/index";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/** Diagnostic severity. */
|
|
5
|
+
type Severity = "error" | "warning";
|
|
6
|
+
/** One diagnostic from an aspect. */
|
|
7
|
+
interface Diagnostic {
|
|
8
|
+
/** The aspect that produced it (e.g. `modifier`). */
|
|
9
|
+
aspect: string;
|
|
10
|
+
/** The rule name (e.g. `unknown-modifier`). */
|
|
11
|
+
rule: string;
|
|
12
|
+
/** A human-readable message. */
|
|
13
|
+
message: string;
|
|
14
|
+
/** The record it concerns, when applicable. */
|
|
15
|
+
record?: string;
|
|
16
|
+
/** The source span, when known (author-side comes from the index; usage-side from the usage). */
|
|
17
|
+
span?: SourceSpan$1;
|
|
18
|
+
/** Severity (defaults to `warning`). */
|
|
19
|
+
severity: Severity;
|
|
20
|
+
}
|
|
21
|
+
/** What kind of thing a completion inserts. */
|
|
22
|
+
type CompletionKind = "component" | "modifier" | "part" | "property" | "function" | "state";
|
|
23
|
+
/** One completion item. */
|
|
24
|
+
interface Completion {
|
|
25
|
+
/** The text to insert (e.g. `-color-secondary`, `--value`). */
|
|
26
|
+
label: string;
|
|
27
|
+
kind: CompletionKind;
|
|
28
|
+
/** A short type/hint shown beside the label. */
|
|
29
|
+
detail?: string;
|
|
30
|
+
/** Markdown documentation. */
|
|
31
|
+
documentation?: string;
|
|
32
|
+
/** Whether the item is deprecated. */
|
|
33
|
+
deprecated?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Hover content (markdown). */
|
|
36
|
+
interface Hover {
|
|
37
|
+
contents: string;
|
|
38
|
+
}
|
|
39
|
+
/** Options that tune usage checks. */
|
|
40
|
+
interface UsageOptions {
|
|
41
|
+
/**
|
|
42
|
+
* When set, a `var(--name)` whose name starts with this prefix but isn't a declared custom property
|
|
43
|
+
* is flagged as unknown. Off by default, since consumed properties are often external tokens.
|
|
44
|
+
*/
|
|
45
|
+
propertyPrefix?: string;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/syntax.d.ts
|
|
49
|
+
/** The result of matching a value against a syntax. */
|
|
50
|
+
interface SyntaxMatch {
|
|
51
|
+
/** Whether the value conforms. True when skipped — we don't flag what we can't check. */
|
|
52
|
+
ok: boolean;
|
|
53
|
+
/** True when the value couldn't be checked statically (universal `*`, `var()`/`env()`, CSS-wide keyword). */
|
|
54
|
+
skipped: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether `value` conforms to a `@property` `syntax` string (e.g. `<length>`, `<color> | none`,
|
|
58
|
+
* `<length>+`). Universal syntax (`*`), substitution values, and CSS-wide keywords are skipped.
|
|
59
|
+
*
|
|
60
|
+
* @param syntax - The `@property` `syntax` descriptor.
|
|
61
|
+
* @param value - The CSS value to check.
|
|
62
|
+
* @returns Whether it matches, and whether the check was skipped.
|
|
63
|
+
*/
|
|
64
|
+
declare function matchesSyntax(syntax: string, value: string): SyntaxMatch;
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/aspects.d.ts
|
|
67
|
+
declare const record: {
|
|
68
|
+
model(index: CssDocIndex): Diagnostic[];
|
|
69
|
+
completions(index: CssDocIndex): Completion[];
|
|
70
|
+
hover(base: string, index: CssDocIndex): Hover | undefined;
|
|
71
|
+
definition(base: string, index: CssDocIndex): Location$1 | undefined;
|
|
72
|
+
};
|
|
73
|
+
declare const modifier: {
|
|
74
|
+
model(index: CssDocIndex): Diagnostic[];
|
|
75
|
+
classUsage(usage: ClassUsage, index: CssDocIndex): Diagnostic[];
|
|
76
|
+
completions(base: string, index: CssDocIndex): Completion[];
|
|
77
|
+
hover(base: string, token: string, index: CssDocIndex): Hover | undefined;
|
|
78
|
+
definition(base: string, token: string, index: CssDocIndex): Location$1 | undefined;
|
|
79
|
+
};
|
|
80
|
+
declare const part: {
|
|
81
|
+
model(index: CssDocIndex): Diagnostic[];
|
|
82
|
+
};
|
|
83
|
+
declare const customProperty: {
|
|
84
|
+
/** 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
|
+
assignment(a: PropertyAssignment, index: CssDocIndex): Diagnostic[];
|
|
86
|
+
propertyUsage(usage: PropertyUsage, index: CssDocIndex, options: UsageOptions): Diagnostic[];
|
|
87
|
+
completions(index: CssDocIndex): Completion[];
|
|
88
|
+
hover(name: string, index: CssDocIndex): Hover | undefined;
|
|
89
|
+
definition(name: string, index: CssDocIndex): Location$1 | undefined;
|
|
90
|
+
};
|
|
91
|
+
declare const func: {
|
|
92
|
+
completions(index: CssDocIndex): Completion[];
|
|
93
|
+
hover(name: string, index: CssDocIndex): Hover | undefined;
|
|
94
|
+
definition(name: string, index: CssDocIndex): Location$1 | undefined;
|
|
95
|
+
};
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/index.d.ts
|
|
98
|
+
/** 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"];
|
|
100
|
+
/** Author-side hygiene diagnostics over the whole model (missing summaries, undocumented members, drift, invalid defaults). */
|
|
101
|
+
declare function lintModel(index: CssDocIndex): Diagnostic[];
|
|
102
|
+
/** 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[];
|
|
106
|
+
/** 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[];
|
|
108
|
+
/** Completions for a class attribute: modifiers of `base` when given, else the component classes. */
|
|
109
|
+
declare function completeClasses(base: string | undefined, index: CssDocIndex): Completion[];
|
|
110
|
+
/** Completions for `var(--…)`: the declared custom properties. */
|
|
111
|
+
declare function completeCustomProperties(index: CssDocIndex): Completion[];
|
|
112
|
+
/** Completions for a value position: the custom functions. */
|
|
113
|
+
declare function completeFunctions(index: CssDocIndex): Completion[];
|
|
114
|
+
/** Hover for a class token: the modifier's docs, else the component's. */
|
|
115
|
+
declare function hoverForClass(base: string, token: string, index: CssDocIndex): Hover | undefined;
|
|
116
|
+
/** Hover for a `var(--…)` custom property. */
|
|
117
|
+
declare function hoverForCustomProperty(name: string, index: CssDocIndex): Hover | undefined;
|
|
118
|
+
/** Hover for a custom function. */
|
|
119
|
+
declare function hoverForFunction(name: string, index: CssDocIndex): Hover | undefined;
|
|
120
|
+
/** Definition of a class token: the modifier's rule, else the component's. */
|
|
121
|
+
declare function definitionForClass(base: string, token: string, index: CssDocIndex): Location$1 | undefined;
|
|
122
|
+
/** Definition of a custom property (its `@property` rule). */
|
|
123
|
+
declare function definitionForCustomProperty(name: string, index: CssDocIndex): Location$1 | undefined;
|
|
124
|
+
/** Definition of a custom function (its `@function` rule). */
|
|
125
|
+
declare function definitionForFunction(name: string, index: CssDocIndex): Location$1 | undefined;
|
|
126
|
+
//#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 };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { memberKey } from "@cssdoc/index";
|
|
2
|
+
import { fork } from "css-tree";
|
|
3
|
+
//#region src/syntax.ts
|
|
4
|
+
/**
|
|
5
|
+
* Match a CSS value against a registered custom property's `@property` `syntax` descriptor, using
|
|
6
|
+
* css-tree's lexer. This is what lets the value rules reject `--gap: red` when `--gap` is declared
|
|
7
|
+
* `<length>`. Values that are runtime-substituted (`var()`, `env()`) or CSS-wide keywords can't be
|
|
8
|
+
* validated statically, so they're skipped rather than flagged (we never warn on what we can't check).
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
const PROBE = "-cssdoc-syntax-probe";
|
|
13
|
+
const CSS_WIDE = /^(?:initial|inherit|unset|revert|revert-layer)$/iu;
|
|
14
|
+
const SUBSTITUTION = /\b(?:var|env)\(/iu;
|
|
15
|
+
const lexerCache = /* @__PURE__ */ new Map();
|
|
16
|
+
function lexerFor(syntax) {
|
|
17
|
+
const cached = lexerCache.get(syntax);
|
|
18
|
+
if (cached !== void 0) return cached;
|
|
19
|
+
let lexer = null;
|
|
20
|
+
try {
|
|
21
|
+
lexer = fork({ properties: { [PROBE]: syntax } }).lexer;
|
|
22
|
+
} catch {
|
|
23
|
+
lexer = null;
|
|
24
|
+
}
|
|
25
|
+
lexerCache.set(syntax, lexer);
|
|
26
|
+
return lexer;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether `value` conforms to a `@property` `syntax` string (e.g. `<length>`, `<color> | none`,
|
|
30
|
+
* `<length>+`). Universal syntax (`*`), substitution values, and CSS-wide keywords are skipped.
|
|
31
|
+
*
|
|
32
|
+
* @param syntax - The `@property` `syntax` descriptor.
|
|
33
|
+
* @param value - The CSS value to check.
|
|
34
|
+
* @returns Whether it matches, and whether the check was skipped.
|
|
35
|
+
*/
|
|
36
|
+
function matchesSyntax(syntax, value) {
|
|
37
|
+
const s = syntax.trim();
|
|
38
|
+
const v = value.trim();
|
|
39
|
+
if (!s || s === "*") return {
|
|
40
|
+
ok: true,
|
|
41
|
+
skipped: true
|
|
42
|
+
};
|
|
43
|
+
if (!v || CSS_WIDE.test(v) || SUBSTITUTION.test(v)) return {
|
|
44
|
+
ok: true,
|
|
45
|
+
skipped: true
|
|
46
|
+
};
|
|
47
|
+
const lexer = lexerFor(s);
|
|
48
|
+
if (!lexer) return {
|
|
49
|
+
ok: true,
|
|
50
|
+
skipped: true
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
ok: lexer.matchProperty(PROBE, v).error === null,
|
|
54
|
+
skipped: false
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
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
|
+
const stripDot = (name) => name.replace(/^\./u, "");
|
|
68
|
+
const warn = (d) => ({
|
|
69
|
+
...d,
|
|
70
|
+
severity: "warning"
|
|
71
|
+
});
|
|
72
|
+
const record = {
|
|
73
|
+
model(index) {
|
|
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
|
+
}));
|
|
82
|
+
return out;
|
|
83
|
+
},
|
|
84
|
+
completions(index) {
|
|
85
|
+
return index.entries.map((entry) => ({
|
|
86
|
+
label: stripDot(entry.className),
|
|
87
|
+
kind: "component",
|
|
88
|
+
detail: entry.kind,
|
|
89
|
+
documentation: entry.summary,
|
|
90
|
+
deprecated: Boolean(entry.deprecated)
|
|
91
|
+
}));
|
|
92
|
+
},
|
|
93
|
+
hover(base, index) {
|
|
94
|
+
const entry = index.componentForClass(base);
|
|
95
|
+
if (!entry) return void 0;
|
|
96
|
+
const lines = [`\`${entry.className}\` — ${entry.kind}`];
|
|
97
|
+
if (entry.summary) lines.push("", entry.summary);
|
|
98
|
+
if (entry.remarks) lines.push("", entry.remarks);
|
|
99
|
+
const facets = [
|
|
100
|
+
entry.modifiers.length && `${entry.modifiers.length} modifiers`,
|
|
101
|
+
entry.parts.length && `${entry.parts.length} parts`,
|
|
102
|
+
entry.states.length && `${entry.states.length} states`,
|
|
103
|
+
entry.cssPropertiesDeclared.length && `${entry.cssPropertiesDeclared.length} custom properties`,
|
|
104
|
+
entry.functions.length && `${entry.functions.length} functions`,
|
|
105
|
+
entry.conditions.length && `${entry.conditions.length} conditions`
|
|
106
|
+
].filter(Boolean);
|
|
107
|
+
if (facets.length) lines.push("", facets.join(" · "));
|
|
108
|
+
if (entry.deprecated) lines.push("", `**Deprecated** — ${entry.deprecated}`);
|
|
109
|
+
return { contents: lines.join("\n") };
|
|
110
|
+
},
|
|
111
|
+
definition(base, index) {
|
|
112
|
+
const entry = index.componentForClass(base);
|
|
113
|
+
return entry ? index.location(entry.name, "record") : void 0;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const modifier = {
|
|
117
|
+
model(index) {
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const info of index.records) {
|
|
120
|
+
const name = info.entry.name;
|
|
121
|
+
for (const m of info.entry.modifiers) {
|
|
122
|
+
const span = info.memberSpans.get(memberKey("modifier", m.name)) ?? info.span;
|
|
123
|
+
if (!m.description?.trim() && !m.deprecated) out.push(warn({
|
|
124
|
+
aspect: "modifier",
|
|
125
|
+
rule: "undocumented-modifier",
|
|
126
|
+
message: `Modifier ".${m.name}" of "${name}" has no @modifier description.`,
|
|
127
|
+
record: name,
|
|
128
|
+
span
|
|
129
|
+
}));
|
|
130
|
+
if (m.deprecated && !m.deprecated.canonical && !m.deprecated.note?.trim()) out.push(warn({
|
|
131
|
+
aspect: "modifier",
|
|
132
|
+
rule: "deprecated-requires-canonical",
|
|
133
|
+
message: `Deprecated modifier ".${m.name}" of "${name}" needs a canonical replacement ({@link -x}) or a note.`,
|
|
134
|
+
record: name,
|
|
135
|
+
span
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
for (const authored of info.authoredModifiers) if (!info.selectorText.includes(`.${authored}`)) out.push(warn({
|
|
139
|
+
aspect: "modifier",
|
|
140
|
+
rule: "name-not-in-css",
|
|
141
|
+
message: `Documented modifier ".${authored}" of "${name}" is not defined by any selector.`,
|
|
142
|
+
record: name,
|
|
143
|
+
span: info.span
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
},
|
|
148
|
+
classUsage(usage, index) {
|
|
149
|
+
if (!usage.base || !usage.token.startsWith("-")) return [];
|
|
150
|
+
const entry = index.componentForClass(usage.base);
|
|
151
|
+
if (!entry) return [];
|
|
152
|
+
if (!index.isModifier(usage.base, usage.token)) return [warn({
|
|
153
|
+
aspect: "modifier",
|
|
154
|
+
rule: "unknown-modifier",
|
|
155
|
+
message: `".${usage.token}" is not a documented modifier of "${entry.name}".`,
|
|
156
|
+
record: entry.name,
|
|
157
|
+
span: usage.loc
|
|
158
|
+
})];
|
|
159
|
+
const dep = index.deprecationOf(usage.base, usage.token);
|
|
160
|
+
if (dep) {
|
|
161
|
+
const advice = dep.canonical ? `use ".${dep.canonical}"` : dep.note ?? "no replacement given";
|
|
162
|
+
return [warn({
|
|
163
|
+
aspect: "modifier",
|
|
164
|
+
rule: "deprecated-modifier",
|
|
165
|
+
message: `Modifier ".${usage.token}" of "${entry.name}" is deprecated — ${advice}.`,
|
|
166
|
+
record: entry.name,
|
|
167
|
+
span: usage.loc
|
|
168
|
+
})];
|
|
169
|
+
}
|
|
170
|
+
return [];
|
|
171
|
+
},
|
|
172
|
+
completions(base, index) {
|
|
173
|
+
const entry = index.componentForClass(base);
|
|
174
|
+
if (!entry) return [];
|
|
175
|
+
return entry.modifiers.map((m) => ({
|
|
176
|
+
label: `-${m.name.replace(/^-/u, "")}`,
|
|
177
|
+
kind: "modifier",
|
|
178
|
+
detail: m.prop,
|
|
179
|
+
documentation: m.description,
|
|
180
|
+
deprecated: Boolean(m.deprecated)
|
|
181
|
+
}));
|
|
182
|
+
},
|
|
183
|
+
hover(base, token, index) {
|
|
184
|
+
const entry = index.componentForClass(base);
|
|
185
|
+
const m = entry?.modifiers.find((x) => x.name === stripDot(token));
|
|
186
|
+
if (!m) return void 0;
|
|
187
|
+
const lines = [`\`.${m.name}\` — modifier of \`${entry.className}\``];
|
|
188
|
+
if (m.description) lines.push("", m.description);
|
|
189
|
+
if (m.deprecated) {
|
|
190
|
+
const advice = m.deprecated.canonical ? `use \`.${m.deprecated.canonical}\`` : m.deprecated.note ?? "";
|
|
191
|
+
lines.push("", `**Deprecated** — ${advice}`);
|
|
192
|
+
}
|
|
193
|
+
return { contents: lines.join("\n") };
|
|
194
|
+
},
|
|
195
|
+
definition(base, token, index) {
|
|
196
|
+
const entry = index.componentForClass(base);
|
|
197
|
+
return entry ? index.location(entry.name, memberKey("modifier", stripDot(token))) : void 0;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const part = { model(index) {
|
|
201
|
+
const out = [];
|
|
202
|
+
for (const info of index.records) {
|
|
203
|
+
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
|
+
}));
|
|
211
|
+
for (const authored of info.authoredParts) if (!info.selectorText.includes(`.${authored}`)) out.push(warn({
|
|
212
|
+
aspect: "part",
|
|
213
|
+
rule: "name-not-in-css",
|
|
214
|
+
message: `Documented part ".${authored}" of "${name}" is not defined by any selector.`,
|
|
215
|
+
record: name,
|
|
216
|
+
span: info.span
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
} };
|
|
221
|
+
function findProperty(index, name) {
|
|
222
|
+
for (const rec of index.records) {
|
|
223
|
+
const i = rec.entry.cssPropertiesDeclared.findIndex((p) => p.name === name);
|
|
224
|
+
if (i >= 0) return {
|
|
225
|
+
record: rec,
|
|
226
|
+
index: i
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const customProperty = {
|
|
231
|
+
/** Author-side: a registered property's default (`initial-value`/`@defaultValue`) must match its syntax. */
|
|
232
|
+
model(index) {
|
|
233
|
+
const out = [];
|
|
234
|
+
for (const { property, record } of index.allCustomProperties()) {
|
|
235
|
+
if (!property.syntax || property.defaultValue === void 0) continue;
|
|
236
|
+
const m = matchesSyntax(property.syntax, property.defaultValue);
|
|
237
|
+
if (m.skipped || m.ok) continue;
|
|
238
|
+
out.push(warn({
|
|
239
|
+
aspect: "custom-property",
|
|
240
|
+
rule: "invalid-default-value",
|
|
241
|
+
message: `Default \`${property.defaultValue}\` of \`${property.name}\` doesn't match its syntax \`${property.syntax}\`.`,
|
|
242
|
+
record,
|
|
243
|
+
span: index.location(record, memberKey("property", property.name))?.span
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
},
|
|
248
|
+
/** Consumer-side: an assignment `--name: value` must match the property's declared syntax. */
|
|
249
|
+
assignment(a, index) {
|
|
250
|
+
const found = findProperty(index, a.name);
|
|
251
|
+
if (!found) return [];
|
|
252
|
+
const property = found.record.entry.cssPropertiesDeclared[found.index];
|
|
253
|
+
if (!property.syntax) return [];
|
|
254
|
+
const m = matchesSyntax(property.syntax, a.value);
|
|
255
|
+
if (m.skipped || m.ok) return [];
|
|
256
|
+
return [warn({
|
|
257
|
+
aspect: "custom-property",
|
|
258
|
+
rule: "invalid-property-value",
|
|
259
|
+
message: `\`${a.value}\` doesn't match the declared syntax \`${property.syntax}\` of \`${a.name}\`.`,
|
|
260
|
+
record: found.record.entry.name,
|
|
261
|
+
span: a.loc
|
|
262
|
+
})];
|
|
263
|
+
},
|
|
264
|
+
propertyUsage(usage, index, options) {
|
|
265
|
+
const out = [];
|
|
266
|
+
if (usage.fallback) {
|
|
267
|
+
const found = findProperty(index, usage.name);
|
|
268
|
+
const property = found?.record.entry.cssPropertiesDeclared[found.index];
|
|
269
|
+
if (property?.syntax) {
|
|
270
|
+
const m = matchesSyntax(property.syntax, usage.fallback);
|
|
271
|
+
if (!m.skipped && !m.ok) out.push(warn({
|
|
272
|
+
aspect: "custom-property",
|
|
273
|
+
rule: "invalid-fallback-value",
|
|
274
|
+
message: `\`var(${usage.name}, …)\` fallback \`${usage.fallback}\` doesn't match the declared syntax \`${property.syntax}\`.`,
|
|
275
|
+
span: usage.loc
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (options.propertyPrefix && usage.name.startsWith(options.propertyPrefix) && !findProperty(index, usage.name)) out.push(warn({
|
|
280
|
+
aspect: "custom-property",
|
|
281
|
+
rule: "unknown-custom-property",
|
|
282
|
+
message: `\`${usage.name}\` is not a documented custom property.`,
|
|
283
|
+
span: usage.loc
|
|
284
|
+
}));
|
|
285
|
+
return out;
|
|
286
|
+
},
|
|
287
|
+
completions(index) {
|
|
288
|
+
return index.allCustomProperties().map(({ property }) => ({
|
|
289
|
+
label: property.name,
|
|
290
|
+
kind: "property",
|
|
291
|
+
detail: property.syntax,
|
|
292
|
+
documentation: property.description
|
|
293
|
+
}));
|
|
294
|
+
},
|
|
295
|
+
hover(name, index) {
|
|
296
|
+
const found = findProperty(index, name);
|
|
297
|
+
if (!found) return void 0;
|
|
298
|
+
const p = found.record.entry.cssPropertiesDeclared[found.index];
|
|
299
|
+
const lines = [`\`${p.name}\`${p.syntax ? ` — \`${p.syntax}\`` : ""}`];
|
|
300
|
+
if (p.defaultValue) lines.push("", `Default: \`${p.defaultValue}\``);
|
|
301
|
+
if (p.description) lines.push("", p.description);
|
|
302
|
+
return { contents: lines.join("\n") };
|
|
303
|
+
},
|
|
304
|
+
definition(name, index) {
|
|
305
|
+
const found = findProperty(index, name);
|
|
306
|
+
return found ? index.location(found.record.entry.name, memberKey("property", name)) : void 0;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const func = {
|
|
310
|
+
completions(index) {
|
|
311
|
+
return index.allFunctions().map(({ fn }) => ({
|
|
312
|
+
label: fn.name,
|
|
313
|
+
kind: "function",
|
|
314
|
+
detail: fn.result ? `(${fn.parameters.join(", ")}) → ${fn.result}` : `(${fn.parameters.join(", ")})`,
|
|
315
|
+
documentation: fn.description
|
|
316
|
+
}));
|
|
317
|
+
},
|
|
318
|
+
hover(name, index) {
|
|
319
|
+
const match = index.allFunctions().find(({ fn }) => fn.name === name);
|
|
320
|
+
if (!match) return void 0;
|
|
321
|
+
const { fn } = match;
|
|
322
|
+
const sig = `\`${fn.name}(${fn.parameters.join(", ")})\`${fn.result ? ` → \`${fn.result}\`` : ""}`;
|
|
323
|
+
return { contents: fn.description ? `${sig}\n\n${fn.description}` : sig };
|
|
324
|
+
},
|
|
325
|
+
definition(name, index) {
|
|
326
|
+
const match = index.allFunctions().find(({ fn }) => fn.name === name);
|
|
327
|
+
return match ? index.location(match.record, memberKey("function", name)) : void 0;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/index.ts
|
|
332
|
+
/** The aspect names covered, in a stable order (the extension point for future aspects). */
|
|
333
|
+
const ASPECTS = [
|
|
334
|
+
"record",
|
|
335
|
+
"modifier",
|
|
336
|
+
"part",
|
|
337
|
+
"custom-property",
|
|
338
|
+
"structure",
|
|
339
|
+
"function",
|
|
340
|
+
"state",
|
|
341
|
+
"condition"
|
|
342
|
+
];
|
|
343
|
+
/** 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),
|
|
347
|
+
...modifier.model(index),
|
|
348
|
+
...part.model(index),
|
|
349
|
+
...customProperty.model(index)
|
|
350
|
+
];
|
|
351
|
+
}
|
|
352
|
+
/** 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));
|
|
355
|
+
}
|
|
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));
|
|
359
|
+
}
|
|
360
|
+
/** 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));
|
|
363
|
+
}
|
|
364
|
+
/** Completions for a class attribute: modifiers of `base` when given, else the component classes. */
|
|
365
|
+
function completeClasses(base, index) {
|
|
366
|
+
return base ? modifier.completions(base, index) : record.completions(index);
|
|
367
|
+
}
|
|
368
|
+
/** Completions for `var(--…)`: the declared custom properties. */
|
|
369
|
+
function completeCustomProperties(index) {
|
|
370
|
+
return customProperty.completions(index);
|
|
371
|
+
}
|
|
372
|
+
/** Completions for a value position: the custom functions. */
|
|
373
|
+
function completeFunctions(index) {
|
|
374
|
+
return func.completions(index);
|
|
375
|
+
}
|
|
376
|
+
/** Hover for a class token: the modifier's docs, else the component's. */
|
|
377
|
+
function hoverForClass(base, token, index) {
|
|
378
|
+
return modifier.hover(base, token, index) ?? record.hover(base, index);
|
|
379
|
+
}
|
|
380
|
+
/** Hover for a `var(--…)` custom property. */
|
|
381
|
+
function hoverForCustomProperty(name, index) {
|
|
382
|
+
return customProperty.hover(name, index);
|
|
383
|
+
}
|
|
384
|
+
/** Hover for a custom function. */
|
|
385
|
+
function hoverForFunction(name, index) {
|
|
386
|
+
return func.hover(name, index);
|
|
387
|
+
}
|
|
388
|
+
/** Definition of a class token: the modifier's rule, else the component's. */
|
|
389
|
+
function definitionForClass(base, token, index) {
|
|
390
|
+
return modifier.definition(base, token, index) ?? record.definition(base, index);
|
|
391
|
+
}
|
|
392
|
+
/** Definition of a custom property (its `@property` rule). */
|
|
393
|
+
function definitionForCustomProperty(name, index) {
|
|
394
|
+
return customProperty.definition(name, index);
|
|
395
|
+
}
|
|
396
|
+
/** Definition of a custom function (its `@function` rule). */
|
|
397
|
+
function definitionForFunction(name, index) {
|
|
398
|
+
return func.definition(name, index);
|
|
399
|
+
}
|
|
400
|
+
//#endregion
|
|
401
|
+
export { ASPECTS, checkClassUsage, checkPropertyAssignments, checkPropertyUsage, completeClasses, completeCustomProperties, completeFunctions, customProperty, definitionForClass, definitionForCustomProperty, definitionForFunction, func, hoverForClass, hoverForCustomProperty, hoverForFunction, lintModel, matchesSyntax, modifier, part, record };
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cssdoc/providers",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"css",
|
|
7
|
+
"cssdoc",
|
|
8
|
+
"documentation",
|
|
9
|
+
"language-service"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://cssdoc.dev",
|
|
12
|
+
"bugs": "https://github.com/thedannywahl/cssdoc/issues",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/thedannywahl/cssdoc.git",
|
|
17
|
+
"directory": "packages/providers"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./dist/index.mjs",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"css-tree": "^3.2.1",
|
|
32
|
+
"@cssdoc/core": "0.1.0",
|
|
33
|
+
"@cssdoc/index": "0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/css-tree": "^2.3.11",
|
|
37
|
+
"@types/node": "^24.13.3",
|
|
38
|
+
"publint": "^0.3.21",
|
|
39
|
+
"typescript": "^6.0.3",
|
|
40
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.4",
|
|
41
|
+
"vite-plus": "0.2.4"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "vp pack",
|
|
45
|
+
"dev": "vp pack --watch",
|
|
46
|
+
"test": "vp test",
|
|
47
|
+
"check": "vp check",
|
|
48
|
+
"publint": "publint"
|
|
49
|
+
}
|
|
50
|
+
}
|