@c2n/mcp 0.0.7
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 +59 -0
- package/data/registry.json +37555 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +17 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/installed.d.ts +8 -0
- package/dist/installed.js +55 -0
- package/dist/lib/css-var-name.d.ts +11 -0
- package/dist/lib/css-var-name.js +38 -0
- package/dist/lib/generate-code.d.ts +26 -0
- package/dist/lib/generate-code.js +72 -0
- package/dist/lib/group-css.d.ts +10 -0
- package/dist/lib/group-css.js +13 -0
- package/dist/registry-types.d.ts +133 -0
- package/dist/registry-types.js +1 -0
- package/dist/registry.d.ts +15 -0
- package/dist/registry.js +63 -0
- package/dist/render.d.ts +24 -0
- package/dist/render.js +181 -0
- package/dist/search.d.ts +8 -0
- package/dist/search.js +52 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +276 -0
- package/package.json +105 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** stdio entry point: `npx -y @c2n/mcp` or `node dist/cli.js`. */
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { createServer } from "./server.js";
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
8
|
+
console.log('c2n-mcp — c2n web-component documentation and generation tools over stdio\n\nUsage: c2n-mcp [--help] [--version]');
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
12
|
+
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
13
|
+
console.log(packageJson.version);
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
const server = createServer();
|
|
17
|
+
await server.connect(new StdioServerTransport());
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ElementEntry } from './registry-types.ts';
|
|
2
|
+
export interface InstalledInfo {
|
|
3
|
+
version: string;
|
|
4
|
+
/** Elements from the installed `custom-elements.json`, keyed by tag, when the package ships one. */
|
|
5
|
+
elements?: Map<string, Pick<ElementEntry, 'attributes' | 'slots' | 'events' | 'cssParts' | 'cssProperties'>>;
|
|
6
|
+
}
|
|
7
|
+
export declare function projectRoot(): string;
|
|
8
|
+
export declare function installedPackage(name: string): InstalledInfo | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects which `@c2n/*` packages the current project has installed (resolved from `C2N_PROJECT_ROOT` or the cwd,
|
|
3
|
+
* which is the project directory when an MCP client spawns the server) and reads their manifests so the API served
|
|
4
|
+
* for an installed component matches the installed version.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
const TTL = 10_000;
|
|
10
|
+
let cache;
|
|
11
|
+
export function projectRoot() {
|
|
12
|
+
return process.env.C2N_PROJECT_ROOT ?? process.cwd();
|
|
13
|
+
}
|
|
14
|
+
export function installedPackage(name) {
|
|
15
|
+
const root = projectRoot();
|
|
16
|
+
if (!cache || cache.root !== root || Date.now() - cache.at > TTL)
|
|
17
|
+
cache = { at: Date.now(), root, packages: new Map() };
|
|
18
|
+
if (cache.packages.has(name))
|
|
19
|
+
return cache.packages.get(name) ?? null;
|
|
20
|
+
let info;
|
|
21
|
+
try {
|
|
22
|
+
const req = createRequire(join(root, 'noop.js'));
|
|
23
|
+
const pkgJsonPath = req.resolve(`${name}/package.json`);
|
|
24
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
|
25
|
+
info = { version: pkg.version };
|
|
26
|
+
if (pkg.customElements) {
|
|
27
|
+
try {
|
|
28
|
+
const manifest = JSON.parse(readFileSync(join(pkgJsonPath, '..', pkg.customElements), 'utf8'));
|
|
29
|
+
const elements = new Map();
|
|
30
|
+
for (const mod of manifest.modules ?? []) {
|
|
31
|
+
for (const decl of mod.declarations ?? []) {
|
|
32
|
+
if (!decl.tagName)
|
|
33
|
+
continue;
|
|
34
|
+
elements.set(decl.tagName, {
|
|
35
|
+
attributes: (decl.attributes ?? []),
|
|
36
|
+
slots: (decl.slots ?? []),
|
|
37
|
+
events: (decl.events ?? []),
|
|
38
|
+
cssParts: (decl.cssParts ?? []),
|
|
39
|
+
cssProperties: (decl.cssProperties ?? []).filter((p) => p.name),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
info.elements = elements;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* manifest unreadable: keep the bundled API */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
info = null;
|
|
52
|
+
}
|
|
53
|
+
cache.packages.set(name, info);
|
|
54
|
+
return info;
|
|
55
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a c2 CSS variable name with the grammar `--<prefix>[__<part>[__<state>]]--<property>`.
|
|
3
|
+
* Ported from `apps/ui/src/utils/manifest-utils.ts` (`normalizeCssDeclaration`) and the theme generator.
|
|
4
|
+
*/
|
|
5
|
+
export interface ParsedCssVarName {
|
|
6
|
+
prefix: string;
|
|
7
|
+
parts: string[];
|
|
8
|
+
states: string[];
|
|
9
|
+
property: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseCssVarName(name: string): ParsedCssVarName | undefined;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const STATES = new Set([
|
|
2
|
+
'hover',
|
|
3
|
+
'active',
|
|
4
|
+
'focus',
|
|
5
|
+
'selected',
|
|
6
|
+
'disabled',
|
|
7
|
+
'open',
|
|
8
|
+
'error',
|
|
9
|
+
'read-only',
|
|
10
|
+
'unselected',
|
|
11
|
+
'copied',
|
|
12
|
+
'highlighted',
|
|
13
|
+
'over',
|
|
14
|
+
'online',
|
|
15
|
+
'away',
|
|
16
|
+
'busy',
|
|
17
|
+
'offline',
|
|
18
|
+
'expanded',
|
|
19
|
+
'running',
|
|
20
|
+
'checked',
|
|
21
|
+
'invalid',
|
|
22
|
+
]);
|
|
23
|
+
const NAME_PATTERN = /^--(c2-[a-z0-9-]+?)(?:__(.+?))?--(-?[a-z-]+)$/;
|
|
24
|
+
export function parseCssVarName(name) {
|
|
25
|
+
const match = NAME_PATTERN.exec(name);
|
|
26
|
+
if (!match)
|
|
27
|
+
return undefined;
|
|
28
|
+
const [, prefix, blocks = '', property] = match;
|
|
29
|
+
const parts = [];
|
|
30
|
+
const states = [];
|
|
31
|
+
for (const block of blocks.split('__').filter(Boolean)) {
|
|
32
|
+
if (STATES.has(block))
|
|
33
|
+
states.push(block);
|
|
34
|
+
else
|
|
35
|
+
parts.push(block);
|
|
36
|
+
}
|
|
37
|
+
return { prefix, parts, states, property };
|
|
38
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variant code generation, ported from `apps/ui/src/utils/playground.ts` (`generateCode`). The Lit output takes the
|
|
3
|
+
* base class and module from the registry instead of deriving them from the tag, which is wrong for `c2-tab`
|
|
4
|
+
* (`@c2n/tabs/tab.js`) and the icon elements.
|
|
5
|
+
*/
|
|
6
|
+
export type CodeFormat = 'html' | 'css' | 'lit' | 'json';
|
|
7
|
+
export interface VariantChanges {
|
|
8
|
+
css: Record<string, string>;
|
|
9
|
+
attributes?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export interface CodeInput {
|
|
12
|
+
tag: string;
|
|
13
|
+
/** Base markup the variant is applied to. */
|
|
14
|
+
html: string;
|
|
15
|
+
changes: VariantChanges;
|
|
16
|
+
/** Name of the generated class / custom element, e.g. `brand-checkbox`. */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Exported class of the base element, e.g. `Button`. */
|
|
19
|
+
className: string;
|
|
20
|
+
/** Module exporting that class, e.g. `@c2n/button`. */
|
|
21
|
+
modulePath: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function toPascalCase(value: string): string;
|
|
24
|
+
/** Rewrites the opening tag of the root element: adds a class and applies attribute changes. */
|
|
25
|
+
export declare function rewriteRootTag(html: string, tag: string, className: string, attributes?: Record<string, string>): string;
|
|
26
|
+
export declare function generateCode(format: CodeFormat, input: CodeInput): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function toPascalCase(value) {
|
|
2
|
+
return value
|
|
3
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
4
|
+
.filter(Boolean)
|
|
5
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
6
|
+
.join('');
|
|
7
|
+
}
|
|
8
|
+
function cssDeclarations(css, indent = ' ') {
|
|
9
|
+
return Object.entries(css)
|
|
10
|
+
.map(([name, value]) => `${indent}${name}: ${value};`)
|
|
11
|
+
.join('\n');
|
|
12
|
+
}
|
|
13
|
+
/** Rewrites the opening tag of the root element: adds a class and applies attribute changes. */
|
|
14
|
+
export function rewriteRootTag(html, tag, className, attributes = {}) {
|
|
15
|
+
const open = new RegExp(`<${tag}(\\s[^>]*)?>`, 'i');
|
|
16
|
+
return html.replace(open, (_match, attrs = '') => {
|
|
17
|
+
let rest = attrs;
|
|
18
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
19
|
+
rest = rest.replace(new RegExp(`\\s${name}(=("[^"]*"|'[^']*'|[^\\s>]+))?`, 'i'), '');
|
|
20
|
+
if (value === 'true' || value === '')
|
|
21
|
+
rest += ` ${name}`;
|
|
22
|
+
else if (value !== 'false')
|
|
23
|
+
rest += ` ${name}="${value}"`;
|
|
24
|
+
}
|
|
25
|
+
if (/\sclass=/.test(rest)) {
|
|
26
|
+
rest = rest.replace(/\sclass=(["'])([^"']*)\1/, (_m, quote, classes) => ` class=${quote}${classes.trim()} ${className}${quote}`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
rest = ` class="${className}"` + rest;
|
|
30
|
+
}
|
|
31
|
+
return `<${tag}${rest}>`;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function generateCode(format, input) {
|
|
35
|
+
const { tag, html, changes, name, className: baseClass, modulePath } = input;
|
|
36
|
+
const hasCss = Object.keys(changes.css).length > 0;
|
|
37
|
+
switch (format) {
|
|
38
|
+
case 'css':
|
|
39
|
+
return hasCss ? `.${name} {\n${cssDeclarations(changes.css)}\n}` : `/* No CSS variable changes. */`;
|
|
40
|
+
case 'html': {
|
|
41
|
+
const markup = rewriteRootTag(html.trim(), tag, name, changes.attributes);
|
|
42
|
+
return hasCss ? `${markup}\n\n<style>\n .${name} {\n${cssDeclarations(changes.css, ' ')}\n }\n</style>` : markup;
|
|
43
|
+
}
|
|
44
|
+
case 'lit': {
|
|
45
|
+
const className = toPascalCase(name);
|
|
46
|
+
const attributeLines = Object.entries(changes.attributes ?? {})
|
|
47
|
+
.map(([attr, value]) => ` this.setAttribute('${attr}', '${value === 'true' ? '' : value}')`)
|
|
48
|
+
.join('\n');
|
|
49
|
+
return [
|
|
50
|
+
`import { css } from 'lit'`,
|
|
51
|
+
`import { ${baseClass} } from '${modulePath}'`,
|
|
52
|
+
``,
|
|
53
|
+
`/** ${tag} with your theme baked in. Register once, then use <${name}> anywhere. */`,
|
|
54
|
+
`export class ${className} extends ${baseClass} {`,
|
|
55
|
+
` static override styles = [`,
|
|
56
|
+
` ${baseClass}.styles,`,
|
|
57
|
+
` css\``,
|
|
58
|
+
` :host {`,
|
|
59
|
+
hasCss ? cssDeclarations(changes.css, ' ') : ` /* your overrides */`,
|
|
60
|
+
` }`,
|
|
61
|
+
` \`,`,
|
|
62
|
+
` ]`,
|
|
63
|
+
...(attributeLines ? [``, ` override connectedCallback() {`, ` super.connectedCallback()`, attributeLines, ` }`] : []),
|
|
64
|
+
`}`,
|
|
65
|
+
``,
|
|
66
|
+
`if (!customElements.get('${name}')) customElements.define('${name}', ${className})`,
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
case 'json':
|
|
70
|
+
return JSON.stringify({ tag, ...changes }, null, 2);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Groups flat CSS variables by part, then by state, for compact rendering. */
|
|
2
|
+
import type { CssProperty } from '../registry-types.ts';
|
|
3
|
+
export interface CssGroup {
|
|
4
|
+
part: string;
|
|
5
|
+
states: {
|
|
6
|
+
state: string;
|
|
7
|
+
properties: CssProperty[];
|
|
8
|
+
}[];
|
|
9
|
+
}
|
|
10
|
+
export declare function groupCssProperties(properties: CssProperty[]): CssGroup[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function groupCssProperties(properties) {
|
|
2
|
+
const groups = new Map();
|
|
3
|
+
for (const property of properties) {
|
|
4
|
+
const byState = groups.get(property.part) ?? new Map();
|
|
5
|
+
const state = property.state ?? '';
|
|
6
|
+
byState.set(state, [...(byState.get(state) ?? []), property]);
|
|
7
|
+
groups.set(property.part, byState);
|
|
8
|
+
}
|
|
9
|
+
return [...groups.entries()].map(([part, byState]) => ({
|
|
10
|
+
part,
|
|
11
|
+
states: [...byState.entries()].map(([state, props]) => ({ state, properties: props })),
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape of `data/registry.json`: everything the MCP server knows about the c2n components, generated by
|
|
3
|
+
* `scripts/build-registry.ts` from the component manifests, the docs site content and the theme package.
|
|
4
|
+
*/
|
|
5
|
+
export interface Registry {
|
|
6
|
+
schemaVersion: 1;
|
|
7
|
+
/** lerna.json version of the published packages. */
|
|
8
|
+
c2nVersion: string;
|
|
9
|
+
categories: string[];
|
|
10
|
+
/** Keyed by component id (the docs page id: `button`, `feather-icons`, `chatbot`). */
|
|
11
|
+
components: Record<string, ComponentEntry>;
|
|
12
|
+
/** Tag → component id (`c2-tab` → `tabs`). Icon tags resolve through `tagPattern`. */
|
|
13
|
+
tagIndex: Record<string, string>;
|
|
14
|
+
/** npm package → component id. */
|
|
15
|
+
packageIndex: Record<string, string>;
|
|
16
|
+
theme: ThemeEntry;
|
|
17
|
+
guides: Record<GuideTopic, string>;
|
|
18
|
+
}
|
|
19
|
+
export type GuideTopic = 'workflow' | 'theming' | 'variant-components' | 'frameworks';
|
|
20
|
+
export interface ComponentEntry {
|
|
21
|
+
id: string;
|
|
22
|
+
package: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description: string;
|
|
25
|
+
intro?: string;
|
|
26
|
+
category: string;
|
|
27
|
+
/** `stable` has a docs page; `undocumented` is published and built but has no page yet. */
|
|
28
|
+
status: 'stable' | 'undocumented';
|
|
29
|
+
docsUrl: string;
|
|
30
|
+
/** One entry per custom element in the package (`tabs` has `c2-tabs` and `c2-tab`). */
|
|
31
|
+
elements: ElementEntry[];
|
|
32
|
+
/** Icon sets: the tag pattern (`c2-feather-{name}`) and the icon names. */
|
|
33
|
+
tagPattern?: string;
|
|
34
|
+
icons?: string[];
|
|
35
|
+
composition: {
|
|
36
|
+
internal: string[];
|
|
37
|
+
slotted: string[];
|
|
38
|
+
usedBy: string[];
|
|
39
|
+
};
|
|
40
|
+
install: {
|
|
41
|
+
npm: string;
|
|
42
|
+
import: string;
|
|
43
|
+
importClass: string;
|
|
44
|
+
};
|
|
45
|
+
presets?: {
|
|
46
|
+
html: string;
|
|
47
|
+
items: Preset[];
|
|
48
|
+
};
|
|
49
|
+
examples: Example[];
|
|
50
|
+
hasGallery: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface ElementEntry {
|
|
53
|
+
tag: string;
|
|
54
|
+
className: string;
|
|
55
|
+
/** Import specifier of the module defining the element, e.g. `@c2n/tabs/tab.js`. */
|
|
56
|
+
modulePath: string;
|
|
57
|
+
description: string;
|
|
58
|
+
attributes: {
|
|
59
|
+
name: string;
|
|
60
|
+
type: string;
|
|
61
|
+
default?: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
}[];
|
|
64
|
+
slots: {
|
|
65
|
+
name: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
}[];
|
|
68
|
+
events: {
|
|
69
|
+
name: string;
|
|
70
|
+
type?: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
}[];
|
|
73
|
+
cssParts: {
|
|
74
|
+
name: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
}[];
|
|
77
|
+
cssProperties: CssProperty[];
|
|
78
|
+
}
|
|
79
|
+
export interface CssProperty {
|
|
80
|
+
name: string;
|
|
81
|
+
type: string;
|
|
82
|
+
default?: string;
|
|
83
|
+
/** Part segment(s) joined with `/`, `''` for the root. */
|
|
84
|
+
part: string;
|
|
85
|
+
state?: string;
|
|
86
|
+
property: string;
|
|
87
|
+
/** `--c2-theme--*` token the base theme maps this variable to, when any. */
|
|
88
|
+
token?: string;
|
|
89
|
+
description?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface Example {
|
|
92
|
+
kind: 'usage' | 'gallery' | 'preview';
|
|
93
|
+
label: string;
|
|
94
|
+
section?: string;
|
|
95
|
+
/** What the example demonstrates. */
|
|
96
|
+
description?: string;
|
|
97
|
+
/** Situation in which this pattern is appropriate. */
|
|
98
|
+
useWhen?: string;
|
|
99
|
+
/** Accessibility behavior or responsibility worth preserving. */
|
|
100
|
+
accessibility?: string;
|
|
101
|
+
/** True only for a gallery baseline named Default with no c2n CSS-variable overrides. */
|
|
102
|
+
isDefault?: boolean;
|
|
103
|
+
/** c2 custom-element tags present in the example markup. */
|
|
104
|
+
tags?: string[];
|
|
105
|
+
html: string;
|
|
106
|
+
css?: string;
|
|
107
|
+
}
|
|
108
|
+
export interface Preset {
|
|
109
|
+
name: string;
|
|
110
|
+
description?: string;
|
|
111
|
+
css: Record<string, string>;
|
|
112
|
+
attributes?: Record<string, string>;
|
|
113
|
+
}
|
|
114
|
+
export interface ThemeToken {
|
|
115
|
+
name: string;
|
|
116
|
+
category: string;
|
|
117
|
+
light: string | null;
|
|
118
|
+
dark?: string;
|
|
119
|
+
description: string;
|
|
120
|
+
/** Number of component variables mapped to this token. */
|
|
121
|
+
usedBy: number;
|
|
122
|
+
}
|
|
123
|
+
export interface ThemeEntry {
|
|
124
|
+
package: string;
|
|
125
|
+
install: {
|
|
126
|
+
npm: string;
|
|
127
|
+
imports: string[];
|
|
128
|
+
};
|
|
129
|
+
tokens: ThemeToken[];
|
|
130
|
+
/** Component variable → token. */
|
|
131
|
+
mapping: Record<string, string>;
|
|
132
|
+
darkMode: string;
|
|
133
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ComponentEntry, ElementEntry, Registry } from './registry-types.ts';
|
|
2
|
+
export declare function loadRegistry(): Registry;
|
|
3
|
+
export interface ResolvedElement {
|
|
4
|
+
component: ComponentEntry;
|
|
5
|
+
element: ElementEntry;
|
|
6
|
+
/** Concrete tag (`c2-feather-arrow-right`), even when the element entry carries a pattern. */
|
|
7
|
+
tag: string;
|
|
8
|
+
/** Concrete module (`@c2n/feather-icons/icons/arrow-right.js`). */
|
|
9
|
+
modulePath: string;
|
|
10
|
+
className: string;
|
|
11
|
+
}
|
|
12
|
+
/** Accepts `c2-button`, `button`, `@c2n/button`, `c2-tab` or a concrete icon tag such as `c2-feather-arrow-right`. */
|
|
13
|
+
export declare function resolveElement(registry: Registry, input: string): ResolvedElement | undefined;
|
|
14
|
+
/** Closest names for "did you mean" hints. */
|
|
15
|
+
export declare function suggest(candidates: string[], input: string, max?: number): string[];
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Loads `data/registry.json` and resolves tags / packages / ids to components. */
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
export function loadRegistry() {
|
|
4
|
+
const url = new URL('../data/registry.json', import.meta.url);
|
|
5
|
+
return JSON.parse(readFileSync(url, 'utf8'));
|
|
6
|
+
}
|
|
7
|
+
/** Accepts `c2-button`, `button`, `@c2n/button`, `c2-tab` or a concrete icon tag such as `c2-feather-arrow-right`. */
|
|
8
|
+
export function resolveElement(registry, input) {
|
|
9
|
+
const query = input.trim().toLowerCase();
|
|
10
|
+
const byPackage = registry.packageIndex[query];
|
|
11
|
+
const byTag = registry.tagIndex[query];
|
|
12
|
+
const id = byPackage ?? byTag ?? (registry.components[query] ? query : registry.tagIndex[`c2-${query}`]);
|
|
13
|
+
if (id) {
|
|
14
|
+
const component = registry.components[id];
|
|
15
|
+
const element = component.elements.find((e) => e.tag === query) ?? component.elements[0];
|
|
16
|
+
if (component.tagPattern && element.tag.includes('{name}')) {
|
|
17
|
+
const name = component.icons?.[0] ?? 'name';
|
|
18
|
+
return concreteIcon(component, element, name);
|
|
19
|
+
}
|
|
20
|
+
return { component, element, tag: element.tag, modulePath: element.modulePath, className: element.className };
|
|
21
|
+
}
|
|
22
|
+
// Icon tags: match a pattern such as `c2-feather-{name}`.
|
|
23
|
+
for (const component of Object.values(registry.components)) {
|
|
24
|
+
if (!component.tagPattern)
|
|
25
|
+
continue;
|
|
26
|
+
const prefix = component.tagPattern.replace('{name}', '');
|
|
27
|
+
if (query.startsWith(prefix) && component.icons?.includes(query.slice(prefix.length))) {
|
|
28
|
+
return concreteIcon(component, component.elements[0], query.slice(prefix.length));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function concreteIcon(component, element, name) {
|
|
34
|
+
const pascal = name
|
|
35
|
+
.split('-')
|
|
36
|
+
.map((p) => p[0].toUpperCase() + p.slice(1))
|
|
37
|
+
.join('');
|
|
38
|
+
return {
|
|
39
|
+
component,
|
|
40
|
+
element,
|
|
41
|
+
tag: element.tag.replace('{name}', name),
|
|
42
|
+
modulePath: element.modulePath.replace('{name}', name),
|
|
43
|
+
className: element.className.replace('{Name}', pascal),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Closest names for "did you mean" hints. */
|
|
47
|
+
export function suggest(candidates, input, max = 3) {
|
|
48
|
+
const needle = input.toLowerCase();
|
|
49
|
+
const scored = candidates
|
|
50
|
+
.map((c) => ({ c, score: c.toLowerCase().includes(needle) || needle.includes(c.toLowerCase()) ? 2 : distance(c.toLowerCase(), needle) <= 4 ? 1 : 0 }))
|
|
51
|
+
.filter((s) => s.score > 0)
|
|
52
|
+
.sort((a, b) => b.score - a.score || a.c.localeCompare(b.c));
|
|
53
|
+
return scored.slice(0, max).map((s) => s.c);
|
|
54
|
+
}
|
|
55
|
+
function distance(a, b) {
|
|
56
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
57
|
+
for (let j = 1; j <= b.length; j++)
|
|
58
|
+
dp[0][j] = j;
|
|
59
|
+
for (let i = 1; i <= a.length; i++)
|
|
60
|
+
for (let j = 1; j <= b.length; j++)
|
|
61
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
62
|
+
return dp[a.length][b.length];
|
|
63
|
+
}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { InstalledInfo } from './installed.ts';
|
|
2
|
+
import type { ComponentEntry, CssProperty, ElementEntry, Example, Registry, ThemeEntry } from './registry-types.ts';
|
|
3
|
+
export declare const MAX_CHARS = 12000;
|
|
4
|
+
export declare function truncate(text: string, max?: number): string;
|
|
5
|
+
export declare function renderComponentList(components: ComponentEntry[], installed: (pkg: string) => InstalledInfo | null): string;
|
|
6
|
+
export interface ComponentRenderOptions {
|
|
7
|
+
include: Set<'attributes' | 'slots' | 'events' | 'parts' | 'css' | 'composition' | 'presets'>;
|
|
8
|
+
installed: InstalledInfo | null;
|
|
9
|
+
/** Concrete tag / module when the request named an icon. */
|
|
10
|
+
concrete?: {
|
|
11
|
+
tag: string;
|
|
12
|
+
modulePath: string;
|
|
13
|
+
className: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export declare function renderComponent(component: ComponentEntry, options: ComponentRenderOptions): string;
|
|
17
|
+
export declare function renderCssGroups(properties: CssProperty[]): string;
|
|
18
|
+
export declare function renderExamples(component: ComponentEntry, examples: Example[], total: number, offset: number): string;
|
|
19
|
+
export declare function renderPresets(component: ComponentEntry, names?: string): string;
|
|
20
|
+
export declare function renderTheme(theme: ThemeEntry, registry: Registry, component?: ComponentEntry): string;
|
|
21
|
+
export declare function elementApiNames(element: ElementEntry): {
|
|
22
|
+
css: string[];
|
|
23
|
+
attributes: string[];
|
|
24
|
+
};
|