@cssdoc/index 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 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,28 @@
1
+ # @cssdoc/index
2
+
3
+ A queryable semantic index over the [`@cssdoc/core`](../core) model — the shared data layer every
4
+ cssdoc linter and the language server query. It adds the two cross-cutting concerns those tools need:
5
+
6
+ - a host-agnostic **`Usage`** abstraction (`ClassUsage`, `PropertyUsage`) so HTML, JSX, template
7
+ literals, and CSS selectors all feed the same providers; and
8
+ - optional **source spans**, built in a dedicated PostCSS pass (so `@cssdoc/core` stays position-free),
9
+ powering diagnostics locations, hover ranges, and go-to-definition.
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { createIndex } from "@cssdoc/index";
15
+
16
+ const index = createIndex(css, { file: "components.css" });
17
+ index.componentForClass(".button"); // → the button record
18
+ index.isModifier(".button", "-color-x"); // → boolean
19
+ index.deprecationOf(".button", "-old"); // → { canonical } | undefined
20
+ index.location("button", "modifier:-color-secondary"); // → { file, span } | undefined
21
+ ```
22
+
23
+ `createIndex(css)` includes spans; `indexFromEntries(entries)` builds a lookup-only index from a model
24
+ snapshot (`index.toManifest()`), without spans.
25
+
26
+ ## License
27
+
28
+ MIT
@@ -0,0 +1,143 @@
1
+ import { CssAnimation, CssCondition, CssDocConfiguration, CssDocEntry, CssFunction, CssModifier, CssPart, CssPropertyDeclared, CssState, StructureNode } from "@cssdoc/core";
2
+
3
+ //#region src/index.d.ts
4
+ /** A 1-based line/column position (matching PostCSS). */
5
+ interface Position {
6
+ line: number;
7
+ column: number;
8
+ }
9
+ /** A source range. */
10
+ interface SourceSpan {
11
+ start: Position;
12
+ end: Position;
13
+ }
14
+ /** A location: a source range, optionally in a named file. */
15
+ interface Location {
16
+ file?: string;
17
+ span: SourceSpan;
18
+ }
19
+ /**
20
+ * A class-attribute usage: the classes on one element, the specific `token` under inspection, and the
21
+ * resolved `base` component class (when one of the tokens names a documented component). Producers for
22
+ * HTML, JSX, and CSS selectors all emit this shape.
23
+ */
24
+ interface ClassUsage {
25
+ /** The base component class among the tokens, if any (e.g. `.button` → `button`). */
26
+ base?: string;
27
+ /** Every class token on the element. */
28
+ tokens: string[];
29
+ /** The specific token this usage is about (e.g. a `-modifier`). */
30
+ token: string;
31
+ /** Where the token sits in the host document. */
32
+ loc?: SourceSpan;
33
+ }
34
+ /** A `var(--name)` custom-property reference. */
35
+ interface PropertyUsage {
36
+ name: string;
37
+ /** The fallback in `var(--name, fallback)`, when present. */
38
+ fallback?: string;
39
+ loc?: SourceSpan;
40
+ }
41
+ /** A custom-property assignment: `--name: value`. */
42
+ interface PropertyAssignment {
43
+ name: string;
44
+ value: string;
45
+ loc?: SourceSpan;
46
+ }
47
+ /** The value sites a stylesheet contains: custom-property assignments and `var(--…)` references. */
48
+ interface CssValueSites {
49
+ assignments: PropertyAssignment[];
50
+ usages: PropertyUsage[];
51
+ }
52
+ /** The kinds of record member a span can be keyed to. */
53
+ type MemberKind = "record" | "modifier" | "part" | "property" | "function" | "animation" | "layer" | "state" | "condition";
54
+ /** Build a stable span key for a member. */
55
+ declare function memberKey(kind: MemberKind, id?: string): string;
56
+ /** One record in the index: its model entry plus source spans and the facts drift checks need. */
57
+ interface RecordInfo {
58
+ entry: CssDocEntry;
59
+ /** The record's definition span (its base-class rule, else its doc comment). */
60
+ span?: SourceSpan;
61
+ /** Spans keyed by {@link memberKey}, e.g. `modifier:-color-secondary`, `property:--value`. */
62
+ memberSpans: Map<string, SourceSpan>;
63
+ /** Modifier names authored via `@modifier` (used for drift detection). */
64
+ authoredModifiers: Set<string>;
65
+ /** Part names authored via `@part`/`@csspart` (used for drift detection). */
66
+ authoredParts: Set<string>;
67
+ /** The concatenated selector text of the record's rules (used for drift detection). */
68
+ selectorText: string;
69
+ }
70
+ /** A serializable snapshot of the index (the model plus its file), for tools that consume JSON. */
71
+ interface CssDocManifest {
72
+ version: 1;
73
+ file?: string;
74
+ entries: CssDocEntry[];
75
+ }
76
+ /** A queryable view over the parsed records. */
77
+ declare class CssDocIndex {
78
+ readonly file?: string;
79
+ readonly records: readonly RecordInfo[];
80
+ private readonly byName;
81
+ private readonly byClass;
82
+ constructor(records: RecordInfo[], file?: string);
83
+ /** Every documented record's model entry. */
84
+ get entries(): CssDocEntry[];
85
+ /** The record whose base class matches `className` (with or without a leading dot). */
86
+ componentForClass(className: string): CssDocEntry | undefined;
87
+ /** The full {@link RecordInfo} for a record name. */
88
+ recordInfo(name: string): RecordInfo | undefined;
89
+ modifiersFor(name: string): CssModifier[];
90
+ partsFor(name: string): CssPart[];
91
+ customPropertiesFor(name: string): CssPropertyDeclared[];
92
+ functionsFor(name: string): CssFunction[];
93
+ statesFor(name: string): CssState[];
94
+ conditionsFor(name: string): CssCondition[];
95
+ animationsFor(name: string): CssAnimation[];
96
+ structureFor(name: string): StructureNode[] | undefined;
97
+ /** Whether `modifier` (with or without a leading dot) is a documented modifier of `base`. */
98
+ isModifier(base: string, modifier: string): boolean;
99
+ /** The deprecation of a modifier on `base`, if it is deprecated. */
100
+ deprecationOf(base: string, modifier: string): {
101
+ canonical?: string;
102
+ note?: string;
103
+ } | undefined;
104
+ /** Every declared custom property, paired with the record that declares it (for `var(...)` completion). */
105
+ allCustomProperties(): {
106
+ property: CssPropertyDeclared;
107
+ record: string;
108
+ }[];
109
+ /** Every custom function, paired with its record. */
110
+ allFunctions(): {
111
+ fn: CssFunction;
112
+ record: string;
113
+ }[];
114
+ /** The definition location of a record member (or the record itself), if a span is known. */
115
+ location(name: string, key?: string): Location | undefined;
116
+ /** A stable, serializable snapshot. */
117
+ toManifest(): CssDocManifest;
118
+ }
119
+ /** Build an index from a model snapshot (no source spans). */
120
+ declare function indexFromEntries(entries: CssDocEntry[], file?: string): CssDocIndex;
121
+ /**
122
+ * Build an index from CSS, with source spans. Parses the model via `@cssdoc/core`, then makes a second
123
+ * PostCSS pass to locate each record and member.
124
+ *
125
+ * @param css - The CSS source.
126
+ * @param options - `file` (attached to locations) and `configuration` (custom tags).
127
+ * @returns The index.
128
+ */
129
+ declare function createIndex(css: string, options?: {
130
+ file?: string;
131
+ configuration?: CssDocConfiguration;
132
+ }): CssDocIndex;
133
+ /**
134
+ * Extract custom-property assignments (`--name: value`) and `var(--name, fallback)` references from
135
+ * CSS. Parsing lives here — the CSS-parsing package — so the linters and the language server share one
136
+ * extractor for the value-validation rules.
137
+ *
138
+ * @param css - The CSS source.
139
+ * @returns The assignments and `var(…)` usages found.
140
+ */
141
+ declare function cssValueSites(css: string): CssValueSites;
142
+ //#endregion
143
+ export { ClassUsage, CssDocIndex, CssDocManifest, CssValueSites, Location, MemberKind, Position, PropertyAssignment, PropertyUsage, RecordInfo, SourceSpan, createIndex, cssValueSites, indexFromEntries, memberKey };
package/dist/index.mjs ADDED
@@ -0,0 +1,250 @@
1
+ import { parseCssDocs, parseDocComment, recordNameOf } from "@cssdoc/core";
2
+ import postcss from "postcss";
3
+ import valueParser from "postcss-value-parser";
4
+ //#region src/index.ts
5
+ /**
6
+ * `@cssdoc/index` — a queryable semantic index over the `@cssdoc/core` model, plus the two
7
+ * cross-cutting concerns every downstream tool shares: a host-agnostic usage abstraction (so
8
+ * HTML, JSX, template literals, and CSS selectors all feed the same providers) and optional source
9
+ * spans (so diagnostics, hover ranges, and go-to-definition can point back into the CSS).
10
+ *
11
+ * `@cssdoc/core` stays position-free; the spans are built here, in a dedicated PostCSS pass, and
12
+ * carried alongside the model. Build with {@link createIndex} (from CSS, with spans) or
13
+ * {@link indexFromEntries} (from a model snapshot, without spans).
14
+ *
15
+ * @module @cssdoc/index
16
+ */
17
+ /** Build a stable span key for a member. */
18
+ function memberKey(kind, id = "") {
19
+ return id ? `${kind}:${id}` : kind;
20
+ }
21
+ const escapeRe = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
22
+ const stripDot = (name) => name.replace(/^\./u, "");
23
+ const spanOf = (node) => {
24
+ const { source } = node;
25
+ if (!source?.start) return void 0;
26
+ const end = source.end ?? source.start;
27
+ return {
28
+ start: {
29
+ line: source.start.line,
30
+ column: source.start.column
31
+ },
32
+ end: {
33
+ line: end.line,
34
+ column: end.column
35
+ }
36
+ };
37
+ };
38
+ /** A queryable view over the parsed records. */
39
+ var CssDocIndex = class {
40
+ file;
41
+ records;
42
+ byName = /* @__PURE__ */ new Map();
43
+ byClass = /* @__PURE__ */ new Map();
44
+ constructor(records, file) {
45
+ this.records = records;
46
+ this.file = file;
47
+ for (const record of records) {
48
+ this.byName.set(record.entry.name, record);
49
+ this.byClass.set(stripDot(record.entry.className), record);
50
+ }
51
+ }
52
+ /** Every documented record's model entry. */
53
+ get entries() {
54
+ return this.records.map((r) => r.entry);
55
+ }
56
+ /** The record whose base class matches `className` (with or without a leading dot). */
57
+ componentForClass(className) {
58
+ return this.byClass.get(stripDot(className))?.entry;
59
+ }
60
+ /** The full {@link RecordInfo} for a record name. */
61
+ recordInfo(name) {
62
+ return this.byName.get(name);
63
+ }
64
+ modifiersFor(name) {
65
+ return this.byName.get(name)?.entry.modifiers ?? [];
66
+ }
67
+ partsFor(name) {
68
+ return this.byName.get(name)?.entry.parts ?? [];
69
+ }
70
+ customPropertiesFor(name) {
71
+ return this.byName.get(name)?.entry.cssPropertiesDeclared ?? [];
72
+ }
73
+ functionsFor(name) {
74
+ return this.byName.get(name)?.entry.functions ?? [];
75
+ }
76
+ statesFor(name) {
77
+ return this.byName.get(name)?.entry.states ?? [];
78
+ }
79
+ conditionsFor(name) {
80
+ return this.byName.get(name)?.entry.conditions ?? [];
81
+ }
82
+ animationsFor(name) {
83
+ return this.byName.get(name)?.entry.animations ?? [];
84
+ }
85
+ structureFor(name) {
86
+ return this.byName.get(name)?.entry.structure;
87
+ }
88
+ /** Whether `modifier` (with or without a leading dot) is a documented modifier of `base`. */
89
+ isModifier(base, modifier) {
90
+ const wanted = stripDot(modifier);
91
+ return this.byClass.get(stripDot(base))?.entry.modifiers.some((m) => m.name === wanted) ?? false;
92
+ }
93
+ /** The deprecation of a modifier on `base`, if it is deprecated. */
94
+ deprecationOf(base, modifier) {
95
+ const wanted = stripDot(modifier);
96
+ return this.byClass.get(stripDot(base))?.entry.modifiers.find((m) => m.name === wanted)?.deprecated;
97
+ }
98
+ /** Every declared custom property, paired with the record that declares it (for `var(...)` completion). */
99
+ allCustomProperties() {
100
+ return this.records.flatMap((r) => r.entry.cssPropertiesDeclared.map((property) => ({
101
+ property,
102
+ record: r.entry.name
103
+ })));
104
+ }
105
+ /** Every custom function, paired with its record. */
106
+ allFunctions() {
107
+ return this.records.flatMap((r) => r.entry.functions.map((fn) => ({
108
+ fn,
109
+ record: r.entry.name
110
+ })));
111
+ }
112
+ /** The definition location of a record member (or the record itself), if a span is known. */
113
+ location(name, key = "record") {
114
+ const record = this.byName.get(name);
115
+ if (!record) return void 0;
116
+ const span = key === "record" ? record.span : record.memberSpans.get(key);
117
+ return span ? {
118
+ file: this.file,
119
+ span
120
+ } : void 0;
121
+ }
122
+ /** A stable, serializable snapshot. */
123
+ toManifest() {
124
+ return {
125
+ version: 1,
126
+ file: this.file,
127
+ entries: this.entries
128
+ };
129
+ }
130
+ };
131
+ /** Build an index from a model snapshot (no source spans). */
132
+ function indexFromEntries(entries, file) {
133
+ return new CssDocIndex(entries.map((entry) => ({
134
+ entry,
135
+ memberSpans: /* @__PURE__ */ new Map(),
136
+ authoredModifiers: /* @__PURE__ */ new Set(),
137
+ authoredParts: /* @__PURE__ */ new Set(),
138
+ selectorText: ""
139
+ })), file);
140
+ }
141
+ /** Scan a record's nodes for member spans, recording each member's first occurrence. */
142
+ function scanNodes(nodes, build, base) {
143
+ const modRe = new RegExp(`(?:${escapeRe(base)}|:scope)((?:\\.-[\\w-]+)+)`, "gu");
144
+ const set = (key, node) => {
145
+ const span = spanOf(node);
146
+ if (span && !build.memberSpans.has(key)) build.memberSpans.set(key, span);
147
+ };
148
+ for (const node of nodes) if (node.type === "rule") {
149
+ build.selectorText += ` ${node.selector}`;
150
+ if (`.${stripDot(node.selector.trim())}` === base && !build.span) build.span = spanOf(node);
151
+ for (const selector of node.selector.split(",")) {
152
+ for (const m of selector.matchAll(modRe)) for (const mod of m[1].matchAll(/\.(-[\w-]+)/gu)) set(memberKey("modifier", mod[1]), node);
153
+ for (const s of selector.matchAll(/:state\(\s*([\w-]+)\s*\)/gu)) set(memberKey("state", s[1]), node);
154
+ const bare = selector.replace(/::?[\w-]+(\([^)]*\))?/gu, "");
155
+ for (const p of bare.matchAll(/\.([a-z][\w-]*)/gu)) set(memberKey("part", p[1]), node);
156
+ }
157
+ } else if (node.type === "atrule") {
158
+ if (node.name === "property") set(memberKey("property", node.params.trim()), node);
159
+ else if (node.name === "function") {
160
+ const fn = node.params.trim().match(/^(--[\w-]+)/u);
161
+ if (fn) set(memberKey("function", fn[1]), node);
162
+ } else if (node.name === "keyframes") set(memberKey("animation", node.params.trim()), node);
163
+ else if (node.name === "layer") for (const raw of node.params.split(",")) {
164
+ const layer = raw.trim();
165
+ if (layer) set(memberKey("layer", layer), node);
166
+ }
167
+ else if (node.name === "container" || node.name === "supports" || node.name === "media") set(memberKey("condition", `${node.name}:${node.params.trim()}`), node);
168
+ if (node.nodes) scanNodes(node.nodes, build, base);
169
+ }
170
+ }
171
+ /**
172
+ * Build an index from CSS, with source spans. Parses the model via `@cssdoc/core`, then makes a second
173
+ * PostCSS pass to locate each record and member.
174
+ *
175
+ * @param css - The CSS source.
176
+ * @param options - `file` (attached to locations) and `configuration` (custom tags).
177
+ * @returns The index.
178
+ */
179
+ function createIndex(css, options = {}) {
180
+ const entries = parseCssDocs(css, { configuration: options.configuration });
181
+ const byName = new Map(entries.map((e) => [e.name, e]));
182
+ const builds = /* @__PURE__ */ new Map();
183
+ const root = postcss.parse(css);
184
+ let current;
185
+ for (const node of root.nodes) {
186
+ if (node.type === "comment") {
187
+ const name = recordNameOf(node.text, options.configuration);
188
+ const entry = name ? byName.get(name) : void 0;
189
+ if (name && entry) {
190
+ const doc = parseDocComment(node.text, options.configuration);
191
+ current = {
192
+ entry,
193
+ span: spanOf(node),
194
+ memberSpans: /* @__PURE__ */ new Map(),
195
+ authoredModifiers: new Set(doc.modifiers.keys()),
196
+ authoredParts: new Set(doc.parts.keys()),
197
+ selectorText: ""
198
+ };
199
+ builds.set(name, current);
200
+ continue;
201
+ }
202
+ }
203
+ if (current) scanNodes([node], current, current.entry.className);
204
+ }
205
+ return new CssDocIndex(entries.map((entry) => builds.get(entry.name) ?? {
206
+ entry,
207
+ memberSpans: /* @__PURE__ */ new Map(),
208
+ authoredModifiers: /* @__PURE__ */ new Set(),
209
+ authoredParts: /* @__PURE__ */ new Set(),
210
+ selectorText: ""
211
+ }), options.file);
212
+ }
213
+ /**
214
+ * Extract custom-property assignments (`--name: value`) and `var(--name, fallback)` references from
215
+ * CSS. Parsing lives here — the CSS-parsing package — so the linters and the language server share one
216
+ * extractor for the value-validation rules.
217
+ *
218
+ * @param css - The CSS source.
219
+ * @returns The assignments and `var(…)` usages found.
220
+ */
221
+ function cssValueSites(css) {
222
+ const assignments = [];
223
+ const usages = [];
224
+ postcss.parse(css).walkDecls((decl) => {
225
+ const loc = spanOf(decl);
226
+ if (decl.prop.startsWith("--")) assignments.push({
227
+ name: decl.prop,
228
+ value: decl.value,
229
+ loc
230
+ });
231
+ if (decl.value.includes("var(")) valueParser(decl.value).walk((node) => {
232
+ if (node.type !== "function" || node.value !== "var") return;
233
+ const name = node.nodes.find((n) => n.type === "word")?.value;
234
+ if (!name?.startsWith("--")) return;
235
+ const comma = node.nodes.findIndex((n) => n.type === "div" && n.value === ",");
236
+ const fallback = comma >= 0 ? valueParser.stringify(node.nodes.slice(comma + 1)).trim() : void 0;
237
+ usages.push({
238
+ name,
239
+ fallback: fallback || void 0,
240
+ loc
241
+ });
242
+ });
243
+ });
244
+ return {
245
+ assignments,
246
+ usages
247
+ };
248
+ }
249
+ //#endregion
250
+ export { CssDocIndex, createIndex, cssValueSites, indexFromEntries, memberKey };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@cssdoc/index",
3
+ "version": "0.1.0",
4
+ "description": "A queryable semantic index over the @cssdoc/core model, plus a host-agnostic Usage abstraction and source spans — the shared data layer for cssdoc's linters and language server.",
5
+ "keywords": [
6
+ "css",
7
+ "cssdoc",
8
+ "documentation",
9
+ "index"
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/index"
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
+ "postcss": "^8.5.16",
32
+ "postcss-value-parser": "^4.2.0",
33
+ "@cssdoc/core": "0.1.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^24.13.3",
37
+ "publint": "^0.3.21",
38
+ "typescript": "^6.0.3",
39
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.2.4",
40
+ "vite-plus": "0.2.4"
41
+ },
42
+ "scripts": {
43
+ "build": "vp pack",
44
+ "dev": "vp pack --watch",
45
+ "test": "vp test",
46
+ "check": "vp check",
47
+ "publint": "publint"
48
+ }
49
+ }