auigrid-mcp-server 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.
@@ -0,0 +1,95 @@
1
+ import { Project } from "ts-morph";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ function getDescription(node) {
7
+ const docs = node.getJsDocs();
8
+ if (docs.length === 0)
9
+ return "";
10
+ return docs[0]
11
+ .getDescription()
12
+ .replace(/\r\n/g, "\n")
13
+ .trim();
14
+ }
15
+ function getTag(node, tagName) {
16
+ for (const doc of node.getJsDocs()) {
17
+ for (const tag of doc.getTags()) {
18
+ if (tag.getTagName() === tagName) {
19
+ return tag.getCommentText()?.trim();
20
+ }
21
+ }
22
+ }
23
+ return undefined;
24
+ }
25
+ function classifyInterface(name) {
26
+ if (name === "Props")
27
+ return "grid-option";
28
+ if (name === "Column")
29
+ return "column";
30
+ if (name === "Footer")
31
+ return "footer";
32
+ if (name.endsWith("Renderer"))
33
+ return "renderer";
34
+ if (name.endsWith("Event"))
35
+ return "event";
36
+ return "other";
37
+ }
38
+ function propKindFor(ifaceKind) {
39
+ switch (ifaceKind) {
40
+ case "grid-option":
41
+ return "prop";
42
+ default:
43
+ return `${ifaceKind}-prop`;
44
+ }
45
+ }
46
+ const vendorPath = path.resolve(__dirname, "../vendor/auigrid-tsx.d.ts");
47
+ const outPath = path.resolve(__dirname, "../data/api-index.json");
48
+ const project = new Project();
49
+ const sourceFile = project.addSourceFileAtPath(vendorPath);
50
+ const entries = [];
51
+ for (const iface of sourceFile.getInterfaces()) {
52
+ const ifaceName = iface.getName();
53
+ const ifaceKind = classifyInterface(ifaceName);
54
+ entries.push({
55
+ name: ifaceName,
56
+ kind: ifaceKind,
57
+ description: getDescription(iface),
58
+ });
59
+ for (const prop of iface.getProperties()) {
60
+ entries.push({
61
+ name: prop.getName(),
62
+ kind: propKindFor(ifaceKind),
63
+ parent: ifaceName,
64
+ type: prop.getTypeNode()?.getText() ?? prop.getType().getText(),
65
+ description: getDescription(prop),
66
+ defaultValue: getTag(prop, "defaultValue"),
67
+ optional: prop.hasQuestionToken(),
68
+ });
69
+ }
70
+ }
71
+ for (const enumDecl of sourceFile.getEnums()) {
72
+ const enumName = enumDecl.getName();
73
+ entries.push({
74
+ name: enumName,
75
+ kind: "enum",
76
+ description: getDescription(enumDecl),
77
+ });
78
+ for (const member of enumDecl.getMembers()) {
79
+ entries.push({
80
+ name: member.getName(),
81
+ kind: "enum-member",
82
+ parent: enumName,
83
+ type: member.getValue()?.toString(),
84
+ description: getDescription(member),
85
+ });
86
+ }
87
+ }
88
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
89
+ fs.writeFileSync(outPath, JSON.stringify(entries, null, 2), "utf-8");
90
+ const byKind = new Map();
91
+ for (const e of entries)
92
+ byKind.set(e.kind, (byKind.get(e.kind) ?? 0) + 1);
93
+ console.error(`Wrote ${entries.length} entries to ${outPath}`);
94
+ for (const [kind, count] of byKind)
95
+ console.error(` ${kind}: ${count}`);
@@ -0,0 +1,14 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parseDocPage } from "./lib/docPageParser.js";
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const srcPath = path.resolve(__dirname, "../vendor/docs/DataGrid-Methods.html");
7
+ const outPath = path.resolve(__dirname, "../data/methods-index.json");
8
+ const html = fs.readFileSync(srcPath, "utf-8");
9
+ const entries = parseDocPage(html);
10
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
11
+ fs.writeFileSync(outPath, JSON.stringify(entries, null, 2), "utf-8");
12
+ console.error(`Wrote ${entries.length} method entries to ${outPath}`);
13
+ console.error(` with parameters: ${entries.filter((e) => e.parameters.length > 0).length}`);
14
+ console.error(` with example code: ${entries.filter((e) => e.example).length}`);
@@ -0,0 +1,46 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ const vendorRoot = path.resolve(__dirname, "../vendor/samples");
6
+ const outPath = path.resolve(__dirname, "../data/samples-index.json");
7
+ const CODE_EXT = new Set([".ts", ".tsx"]);
8
+ function walk(dir, fileList = []) {
9
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
10
+ const full = path.join(dir, entry.name);
11
+ if (entry.isDirectory()) {
12
+ walk(full, fileList);
13
+ }
14
+ else if (CODE_EXT.has(path.extname(entry.name))) {
15
+ fileList.push(full);
16
+ }
17
+ }
18
+ return fileList;
19
+ }
20
+ function categoryFromRelPath(relPath) {
21
+ const [top] = relPath.split(path.sep);
22
+ return top;
23
+ }
24
+ const entries = [];
25
+ for (const framework of fs.readdirSync(vendorRoot, { withFileTypes: true })) {
26
+ if (!framework.isDirectory())
27
+ continue;
28
+ const frameworkDir = path.join(vendorRoot, framework.name);
29
+ const files = walk(frameworkDir);
30
+ for (const file of files) {
31
+ const relPath = path.relative(frameworkDir, file);
32
+ entries.push({
33
+ name: path.basename(file, path.extname(file)),
34
+ framework: framework.name,
35
+ category: categoryFromRelPath(relPath),
36
+ relPath,
37
+ content: fs.readFileSync(file, "utf-8"),
38
+ });
39
+ }
40
+ }
41
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
42
+ fs.writeFileSync(outPath, JSON.stringify(entries, null, 2), "utf-8");
43
+ console.error(`Wrote ${entries.length} sample files to ${outPath}`);
44
+ for (const framework of new Set(entries.map((e) => e.framework))) {
45
+ console.error(` ${framework}: ${entries.filter((e) => e.framework === framework).length}`);
46
+ }
@@ -0,0 +1,12 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parseDocPage } from "./lib/docPageParser.js";
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const srcPath = path.resolve(__dirname, "../vendor/docs/DataGrid-StaticUtils.html");
7
+ const outPath = path.resolve(__dirname, "../data/static-utils-index.json");
8
+ const html = fs.readFileSync(srcPath, "utf-8");
9
+ const entries = parseDocPage(html);
10
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
11
+ fs.writeFileSync(outPath, JSON.stringify(entries, null, 2), "utf-8");
12
+ console.error(`Wrote ${entries.length} static util entries to ${outPath}`);
@@ -0,0 +1,50 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parseDocPage } from "./lib/docPageParser.js";
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const vendorDocs = path.resolve(__dirname, "../vendor/docs");
7
+ const apiIndexPath = path.resolve(__dirname, "../data/api-index.json");
8
+ const entries = JSON.parse(fs.readFileSync(apiIndexPath, "utf-8"));
9
+ function applyEnrichment(doc, match) {
10
+ const target = entries.find(match);
11
+ if (!target)
12
+ return false;
13
+ if (doc.description)
14
+ target.officialDescription = doc.description;
15
+ if (doc.example)
16
+ target.example = doc.example;
17
+ return true;
18
+ }
19
+ // Properties.html: ids are the exact grid-option prop names (Props interface).
20
+ const propertiesHtml = fs.readFileSync(path.join(vendorDocs, "DataGrid-Properties.html"), "utf-8");
21
+ const propertyDocs = parseDocPage(propertiesHtml);
22
+ let propMatched = 0;
23
+ for (const doc of propertyDocs) {
24
+ if (applyEnrichment(doc, (e) => e.kind === "prop" && e.name === doc.name))
25
+ propMatched++;
26
+ }
27
+ // Events.html: ids are camelCase event names (e.g. "cellClick"), which map to
28
+ // PascalCase "<Name>Event" interfaces in the .d.ts (e.g. "CellClickEvent").
29
+ // A couple of event names diverge between the doc site and the .d.ts itself
30
+ // (abbreviations, missing "Event" suffix) — handled as explicit aliases.
31
+ const EVENT_NAME_ALIASES = {
32
+ rowAllCheckClick: "RowAllChkClickEvent",
33
+ // RowNumHeaderClick's own .d.ts interface is missing the usual "Event" suffix
34
+ // (upstream inconsistency), so it was classified as kind "other" rather than "event".
35
+ rowNumHeaderClick: "RowNumHeaderClick",
36
+ };
37
+ const eventsHtml = fs.readFileSync(path.join(vendorDocs, "DataGrid-Events.html"), "utf-8");
38
+ const eventDocs = parseDocPage(eventsHtml);
39
+ let eventMatched = 0;
40
+ for (const doc of eventDocs) {
41
+ const pascalName = doc.name[0].toUpperCase() + doc.name.slice(1);
42
+ const ifaceName = EVENT_NAME_ALIASES[doc.name] ?? `${pascalName}Event`;
43
+ if (applyEnrichment(doc, (e) => (e.kind === "event" || e.kind === "other") && e.name === ifaceName))
44
+ eventMatched++;
45
+ else
46
+ console.error(` no .d.ts match for event "${doc.name}" (tried "${ifaceName}")`);
47
+ }
48
+ fs.writeFileSync(apiIndexPath, JSON.stringify(entries, null, 2), "utf-8");
49
+ console.error(`Enriched ${propMatched}/${propertyDocs.length} props from Properties.html`);
50
+ console.error(`Enriched ${eventMatched}/${eventDocs.length} events from Events.html`);
package/dist/index.js ADDED
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { searchEntries, getEntry } from "./lib/apiIndex.js";
6
+ import { searchSamples, getSample, listSamples } from "./lib/samplesIndex.js";
7
+ import { searchMethods, getMethod } from "./lib/methodsIndex.js";
8
+ import { searchStaticUtils, getStaticUtil } from "./lib/staticUtilsIndex.js";
9
+ const server = new McpServer({
10
+ name: "auigrid-mcp-server",
11
+ version: "0.1.0",
12
+ });
13
+ server.registerTool("ping", {
14
+ title: "Ping",
15
+ description: "Health check — returns pong if the server is running.",
16
+ inputSchema: {},
17
+ }, async () => ({
18
+ content: [{ type: "text", text: "pong" }],
19
+ }));
20
+ function formatEntry(e) {
21
+ const lines = [`### ${e.name}${e.parent ? ` (${e.parent})` : ""} [${e.kind}]`];
22
+ if (e.type)
23
+ lines.push(`type: ${e.type}`);
24
+ if (e.optional !== undefined)
25
+ lines.push(`optional: ${e.optional}`);
26
+ if (e.defaultValue !== undefined)
27
+ lines.push(`default: ${e.defaultValue}`);
28
+ if (e.description)
29
+ lines.push("", e.description);
30
+ if (e.officialDescription && e.officialDescription !== e.description) {
31
+ lines.push("", "Official docs (auisoft.net):", e.officialDescription);
32
+ }
33
+ if (e.example)
34
+ lines.push("", "Example:", "```javascript", e.example, "```");
35
+ return lines.join("\n");
36
+ }
37
+ const KIND_VALUES = [
38
+ "grid-option",
39
+ "prop",
40
+ "column",
41
+ "column-prop",
42
+ "renderer",
43
+ "renderer-prop",
44
+ "footer",
45
+ "footer-prop",
46
+ "event",
47
+ "event-prop",
48
+ "enum",
49
+ "enum-member",
50
+ "other",
51
+ "other-prop",
52
+ ];
53
+ server.registerTool("search_api", {
54
+ title: "Search AUIGrid API",
55
+ description: "Search the AUIGrid API reference (grid options/props, Column fields, cell/edit/header renderers, events, enums) by keyword. Matches against name, parent interface, and Korean description text.",
56
+ inputSchema: {
57
+ query: z.string().describe("Keyword to search for, e.g. 'rowHeight', 'tree grid', 'CalendarRenderer'"),
58
+ kind: z.enum(KIND_VALUES).optional().describe("Restrict results to one entry kind"),
59
+ limit: z.number().int().min(1).max(50).optional().describe("Max results to return (default 15)"),
60
+ },
61
+ }, async ({ query, kind, limit }) => {
62
+ const results = searchEntries(query, kind, limit ?? 15);
63
+ if (results.length === 0) {
64
+ return { content: [{ type: "text", text: `No matches for "${query}".` }] };
65
+ }
66
+ return {
67
+ content: [{ type: "text", text: results.map(formatEntry).join("\n\n---\n\n") }],
68
+ };
69
+ });
70
+ server.registerTool("get_api_entry", {
71
+ title: "Get AUIGrid API entry",
72
+ description: "Look up an exact AUIGrid API entry by name (a Props/Column field, renderer property, event property, enum member, etc). Optionally disambiguate by parent interface name when the same field name appears under multiple renderers/events.",
73
+ inputSchema: {
74
+ name: z.string().describe("Exact identifier, e.g. 'rowHeight' or 'onlyMonthMode'"),
75
+ parent: z
76
+ .string()
77
+ .optional()
78
+ .describe("Parent interface to disambiguate, e.g. 'CalendarRenderer'"),
79
+ },
80
+ }, async ({ name, parent }) => {
81
+ const results = getEntry(name, parent);
82
+ if (results.length === 0) {
83
+ return { content: [{ type: "text", text: `No entry found for "${name}".` }] };
84
+ }
85
+ return {
86
+ content: [{ type: "text", text: results.map(formatEntry).join("\n\n---\n\n") }],
87
+ };
88
+ });
89
+ server.registerTool("search_samples", {
90
+ title: "Search AUIGrid samples",
91
+ description: "Full-text search over real AUIGrid React/TSX sample and showcase source files (e.g. tree grid, custom editors, drag & drop, export). Returns a matching code snippet from each hit — use get_sample to fetch the full file.",
92
+ inputSchema: {
93
+ query: z.string().describe("Keyword or feature to search for, e.g. 'tree grid', 'dropdown', 'export'"),
94
+ limit: z.number().int().min(1).max(30).optional().describe("Max results to return (default 10)"),
95
+ },
96
+ }, async ({ query, limit }) => {
97
+ const results = searchSamples(query, limit ?? 10);
98
+ if (results.length === 0) {
99
+ return { content: [{ type: "text", text: `No sample matches for "${query}".` }] };
100
+ }
101
+ const text = results
102
+ .map((r) => `### ${r.name} (${r.framework}/${r.relPath})\n\`\`\`tsx\n${r.snippet}\n\`\`\``)
103
+ .join("\n\n---\n\n");
104
+ return { content: [{ type: "text", text }] };
105
+ });
106
+ server.registerTool("get_sample", {
107
+ title: "Get AUIGrid sample source",
108
+ description: "Fetch the full source of an AUIGrid sample or showcase file by exact name (e.g. 'SampleTreeGrid', 'EditDropDown', 'Showcase03').",
109
+ inputSchema: {
110
+ name: z.string().describe("Exact sample/showcase file name, e.g. 'SampleTreeGrid'"),
111
+ },
112
+ }, async ({ name }) => {
113
+ const results = getSample(name);
114
+ if (results.length === 0) {
115
+ return { content: [{ type: "text", text: `No sample found named "${name}".` }] };
116
+ }
117
+ const text = results
118
+ .map((r) => `### ${r.name} (${r.framework}/${r.relPath})\n\`\`\`tsx\n${r.content}\n\`\`\``)
119
+ .join("\n\n---\n\n");
120
+ return { content: [{ type: "text", text }] };
121
+ });
122
+ server.registerTool("list_samples", {
123
+ title: "List AUIGrid samples",
124
+ description: "List all indexed AUIGrid sample/showcase file names, grouped by category.",
125
+ inputSchema: {},
126
+ }, async () => {
127
+ const all = listSamples();
128
+ const byCategory = new Map();
129
+ for (const s of all) {
130
+ const list = byCategory.get(s.category) ?? [];
131
+ list.push(s.name);
132
+ byCategory.set(s.category, list);
133
+ }
134
+ const text = [...byCategory.entries()]
135
+ .map(([cat, names]) => `${cat}:\n ${names.join(", ")}`)
136
+ .join("\n\n");
137
+ return { content: [{ type: "text", text }] };
138
+ });
139
+ function formatParam(p, indent = "") {
140
+ const lines = [`${indent}- ${p.name}${p.type ? ` (${p.type})` : ""}: ${p.description}`];
141
+ if (p.subParameters?.length) {
142
+ for (const sub of p.subParameters)
143
+ lines.push(...formatParam(sub, indent + " "));
144
+ }
145
+ return lines;
146
+ }
147
+ function formatMethod(m) {
148
+ const lines = [`### ${m.name}()${m.version ? ` [since v${m.version}]` : ""}`];
149
+ if (m.description)
150
+ lines.push("", m.description);
151
+ if (m.parameters.length) {
152
+ lines.push("", "Parameters:", ...m.parameters.flatMap((p) => formatParam(p)));
153
+ }
154
+ if (m.example)
155
+ lines.push("", "Example:", "```javascript", m.example, "```");
156
+ if (m.notes)
157
+ lines.push("", m.notes);
158
+ return lines.join("\n");
159
+ }
160
+ server.registerTool("search_methods", {
161
+ title: "Search AUIGrid instance methods",
162
+ description: "Search the AUIGrid imperative instance-method API (e.g. AUIGrid.setGridData(gridId, data), insertRow, getRowById) by keyword. Sourced from the official auisoft.net method reference — covers methods not present in the React/Vue prop typings.",
163
+ inputSchema: {
164
+ query: z.string().describe("Keyword to search for, e.g. 'insertRow', 'export', 'grouping'"),
165
+ limit: z.number().int().min(1).max(50).optional().describe("Max results to return (default 15)"),
166
+ },
167
+ }, async ({ query, limit }) => {
168
+ const results = searchMethods(query, limit ?? 15);
169
+ if (results.length === 0) {
170
+ return { content: [{ type: "text", text: `No method matches for "${query}".` }] };
171
+ }
172
+ return {
173
+ content: [{ type: "text", text: results.map(formatMethod).join("\n\n---\n\n") }],
174
+ };
175
+ });
176
+ server.registerTool("get_method", {
177
+ title: "Get AUIGrid instance method",
178
+ description: "Look up the full reference (description, parameters, example code) for an exact AUIGrid instance method name, e.g. 'setGridData' or 'insertRow'.",
179
+ inputSchema: {
180
+ name: z.string().describe("Exact method name, e.g. 'setGridData'"),
181
+ },
182
+ }, async ({ name }) => {
183
+ const result = getMethod(name);
184
+ if (!result) {
185
+ return { content: [{ type: "text", text: `No method found named "${name}".` }] };
186
+ }
187
+ return { content: [{ type: "text", text: formatMethod(result) }] };
188
+ });
189
+ server.registerTool("search_static_utils", {
190
+ title: "Search AUIGrid static utilities",
191
+ description: "Search AUIGrid's static (non-instance) utility functions, e.g. AUIGrid.create(), formatDate(), formatNumber(), getActiveGrid(), isCreated(). These are called on the AUIGrid namespace itself, not a specific grid instance.",
192
+ inputSchema: {
193
+ query: z.string().describe("Keyword to search for, e.g. 'formatDate', 'create'"),
194
+ limit: z.number().int().min(1).max(50).optional().describe("Max results to return (default 15)"),
195
+ },
196
+ }, async ({ query, limit }) => {
197
+ const results = searchStaticUtils(query, limit ?? 15);
198
+ if (results.length === 0) {
199
+ return { content: [{ type: "text", text: `No static util matches for "${query}".` }] };
200
+ }
201
+ return {
202
+ content: [{ type: "text", text: results.map(formatMethod).join("\n\n---\n\n") }],
203
+ };
204
+ });
205
+ server.registerTool("get_static_util", {
206
+ title: "Get AUIGrid static utility",
207
+ description: "Look up the full reference for an exact AUIGrid static utility name, e.g. 'formatDate'.",
208
+ inputSchema: {
209
+ name: z.string().describe("Exact static utility name, e.g. 'formatDate'"),
210
+ },
211
+ }, async ({ name }) => {
212
+ const result = getStaticUtil(name);
213
+ if (!result) {
214
+ return { content: [{ type: "text", text: `No static util found named "${name}".` }] };
215
+ }
216
+ return { content: [{ type: "text", text: formatMethod(result) }] };
217
+ });
218
+ async function main() {
219
+ const transport = new StdioServerTransport();
220
+ await server.connect(transport);
221
+ console.error("auigrid-mcp-server running on stdio");
222
+ }
223
+ main().catch((err) => {
224
+ console.error("Fatal error starting auigrid-mcp-server:", err);
225
+ process.exit(1);
226
+ });
@@ -0,0 +1,40 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ const indexPath = path.resolve(__dirname, "../../data/api-index.json");
6
+ let entries = [];
7
+ try {
8
+ entries = JSON.parse(fs.readFileSync(indexPath, "utf-8"));
9
+ }
10
+ catch (err) {
11
+ console.error(`Failed to load API index at ${indexPath}. Run "npm run build:index" first.`, err);
12
+ }
13
+ export function getEntries() {
14
+ return entries;
15
+ }
16
+ export function searchEntries(query, kind, limit = 15) {
17
+ const q = query.toLowerCase();
18
+ const results = entries.filter((e) => {
19
+ if (kind && e.kind !== kind)
20
+ return false;
21
+ return (e.name.toLowerCase().includes(q) ||
22
+ (e.parent?.toLowerCase().includes(q) ?? false) ||
23
+ e.description.toLowerCase().includes(q));
24
+ });
25
+ // Exact name matches first, then by whether the query matched the name at all,
26
+ // so a search like "rowHeight" surfaces the property before unrelated description hits.
27
+ results.sort((a, b) => {
28
+ const aExact = a.name.toLowerCase() === q ? 0 : 1;
29
+ const bExact = b.name.toLowerCase() === q ? 0 : 1;
30
+ if (aExact !== bExact)
31
+ return aExact - bExact;
32
+ const aNameHit = a.name.toLowerCase().includes(q) ? 0 : 1;
33
+ const bNameHit = b.name.toLowerCase().includes(q) ? 0 : 1;
34
+ return aNameHit - bNameHit;
35
+ });
36
+ return results.slice(0, limit);
37
+ }
38
+ export function getEntry(name, parent) {
39
+ return entries.filter((e) => e.name === name && (!parent || e.parent === parent));
40
+ }
@@ -0,0 +1,101 @@
1
+ import * as cheerio from "cheerio";
2
+ function parseParamLi(text) {
3
+ const m = text.match(/^([^\s(]+)\s*\(([^)]*)\)\s*:\s*([\s\S]*)$/);
4
+ if (m) {
5
+ return { name: m[1].trim(), type: m[2].trim(), description: m[3].trim() };
6
+ }
7
+ return { name: "", description: text.trim() };
8
+ }
9
+ /**
10
+ * Parses the auisoft.net "summary table" doc page format shared by
11
+ * DataGrid/Methods.html, Properties.html, Events.html, StaticUtils.html:
12
+ * one <tr id="name"> per entry, full description/params/example inline
13
+ * in a single td.summaryTableDescription cell (no separate detail page).
14
+ */
15
+ export function parseDocPage(html) {
16
+ const $ = cheerio.load(html);
17
+ const entries = [];
18
+ $("tr[id]").each((_, tr) => {
19
+ const $tr = $(tr);
20
+ const name = $tr.attr("id");
21
+ if (!name)
22
+ return;
23
+ const cell = $tr.find("td.summaryTableDescription").first();
24
+ if (cell.length === 0)
25
+ return;
26
+ const versionMatch = cell.text().match(/Version\s*:\s*([\d.]+)/);
27
+ const version = versionMatch?.[1];
28
+ const descParts = [];
29
+ const notesParts = [];
30
+ const parameters = [];
31
+ let example;
32
+ let phase = "description";
33
+ let pendingSubParamTarget = null;
34
+ cell.contents().each((_, node) => {
35
+ const el = $(node);
36
+ const tag = node.tagName?.toLowerCase();
37
+ if (tag === "p") {
38
+ const text = el.text().trim();
39
+ if (!text)
40
+ return;
41
+ if (text === "파라메터 설명" || text === "파라미터 설명") {
42
+ phase = "params";
43
+ return;
44
+ }
45
+ const subHeader = text.match(/^■\s*(.+?)\s*파라(?:메터|미터)\s*설명\s*■$/);
46
+ if (subHeader) {
47
+ pendingSubParamTarget = subHeader[1].trim();
48
+ return;
49
+ }
50
+ if (phase === "description")
51
+ descParts.push(text);
52
+ else
53
+ notesParts.push(text);
54
+ }
55
+ else if (tag === "ul") {
56
+ if (phase === "params") {
57
+ el.find("li").each((_, li) => {
58
+ parameters.push(parseParamLi($(li).text().trim()));
59
+ });
60
+ phase = "notes";
61
+ }
62
+ else if (pendingSubParamTarget) {
63
+ const subParams = el
64
+ .find("li")
65
+ .toArray()
66
+ .map((li) => parseParamLi($(li).text().trim()));
67
+ const target = parameters.find((p) => p.name.toLowerCase() === pendingSubParamTarget.toLowerCase());
68
+ if (target) {
69
+ target.subParameters = subParams;
70
+ }
71
+ else {
72
+ parameters.push({
73
+ name: pendingSubParamTarget,
74
+ description: "(object properties)",
75
+ subParameters: subParams,
76
+ });
77
+ }
78
+ pendingSubParamTarget = null;
79
+ }
80
+ else {
81
+ el.find("li").each((_, li) => {
82
+ notesParts.push(`- ${$(li).text().trim()}`);
83
+ });
84
+ }
85
+ }
86
+ else if (tag === "pre") {
87
+ const code = el.find("code").first().text() || el.text();
88
+ example = example ? `${example}\n\n${code.trim()}` : code.trim();
89
+ }
90
+ });
91
+ entries.push({
92
+ name,
93
+ version,
94
+ description: descParts.join("\n"),
95
+ parameters,
96
+ notes: notesParts.length ? notesParts.join("\n") : undefined,
97
+ example,
98
+ });
99
+ });
100
+ return entries;
101
+ }
@@ -0,0 +1,32 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ const indexPath = path.resolve(__dirname, "../../data/methods-index.json");
6
+ let entries = [];
7
+ try {
8
+ entries = JSON.parse(fs.readFileSync(indexPath, "utf-8"));
9
+ }
10
+ catch (err) {
11
+ console.error(`Failed to load methods index at ${indexPath}. Run "npm run build:methods-index" first.`, err);
12
+ }
13
+ export function searchMethods(query, limit = 15) {
14
+ const q = query.toLowerCase();
15
+ const results = entries.filter((e) => e.name.toLowerCase().includes(q) ||
16
+ e.description.toLowerCase().includes(q) ||
17
+ e.parameters.some((p) => p.name.toLowerCase().includes(q)));
18
+ results.sort((a, b) => {
19
+ const aExact = a.name.toLowerCase() === q ? 0 : 1;
20
+ const bExact = b.name.toLowerCase() === q ? 0 : 1;
21
+ if (aExact !== bExact)
22
+ return aExact - bExact;
23
+ const aNameHit = a.name.toLowerCase().includes(q) ? 0 : 1;
24
+ const bNameHit = b.name.toLowerCase().includes(q) ? 0 : 1;
25
+ return aNameHit - bNameHit;
26
+ });
27
+ return results.slice(0, limit);
28
+ }
29
+ export function getMethod(name) {
30
+ const lower = name.toLowerCase();
31
+ return entries.find((e) => e.name.toLowerCase() === lower);
32
+ }
@@ -0,0 +1,52 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ const indexPath = path.resolve(__dirname, "../../data/samples-index.json");
6
+ let entries = [];
7
+ try {
8
+ entries = JSON.parse(fs.readFileSync(indexPath, "utf-8"));
9
+ }
10
+ catch (err) {
11
+ console.error(`Failed to load samples index at ${indexPath}. Run "npm run build:samples-index" first.`, err);
12
+ }
13
+ function snippetAround(content, query, contextLines = 3) {
14
+ const lines = content.split("\n");
15
+ const q = query.toLowerCase();
16
+ const hitIdx = lines.findIndex((l) => l.toLowerCase().includes(q));
17
+ if (hitIdx === -1)
18
+ return lines.slice(0, contextLines * 2).join("\n");
19
+ const start = Math.max(0, hitIdx - contextLines);
20
+ const end = Math.min(lines.length, hitIdx + contextLines + 1);
21
+ return lines.slice(start, end).join("\n");
22
+ }
23
+ export function searchSamples(query, limit = 10) {
24
+ const q = query.toLowerCase();
25
+ const matches = entries.filter((e) => e.name.toLowerCase().includes(q) ||
26
+ e.category.toLowerCase().includes(q) ||
27
+ e.content.toLowerCase().includes(q));
28
+ matches.sort((a, b) => {
29
+ const aNameHit = a.name.toLowerCase().includes(q) ? 0 : 1;
30
+ const bNameHit = b.name.toLowerCase().includes(q) ? 0 : 1;
31
+ return aNameHit - bNameHit;
32
+ });
33
+ return matches.slice(0, limit).map((e) => ({
34
+ name: e.name,
35
+ framework: e.framework,
36
+ category: e.category,
37
+ relPath: e.relPath,
38
+ snippet: snippetAround(e.content, query),
39
+ }));
40
+ }
41
+ export function getSample(name) {
42
+ const lower = name.toLowerCase();
43
+ return entries.filter((e) => e.name.toLowerCase() === lower);
44
+ }
45
+ export function listSamples() {
46
+ return entries.map(({ name, framework, category, relPath }) => ({
47
+ name,
48
+ framework,
49
+ category,
50
+ relPath,
51
+ }));
52
+ }