@amcharts/amcharts5-mcp 1.2.0 → 1.4.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,84 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Node-only disk loader.
3
+ //
4
+ // Reads raw markdown off the filesystem into the { skillFiles, extendedFiles }
5
+ // shape consumed by buildContent(). Used by the stdio server (src/index.js) and
6
+ // by the Worker content build script (scripts/build-worker-content.js).
7
+ //
8
+ // NOTE: imports `fs` — must NOT be imported by the Worker bundle.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ import { readFileSync, readdirSync, existsSync } from "fs";
12
+ import { join, dirname } from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { buildContent } from "./content.js";
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const ROOT = join(__dirname, "..");
18
+
19
+ // Submodule path (local dev) or content/ fallback (npm install)
20
+ const SUBMODULE_DIR = join(ROOT, "amcharts5-skill", "amcharts5-skill");
21
+ const CONTENT_DIR = join(ROOT, "content");
22
+ const SKILL_DIR = existsSync(SUBMODULE_DIR) ? SUBMODULE_DIR : CONTENT_DIR;
23
+ const REFERENCES_DIR = existsSync(join(SKILL_DIR, "references"))
24
+ ? join(SKILL_DIR, "references")
25
+ : SKILL_DIR;
26
+ const EXTENDED_DIR = join(ROOT, "extended");
27
+
28
+ // Directories under extended/ that must never be served. `reference-generated/`
29
+ // is work-in-progress output from scripts/generate-reference.cjs: it duplicates
30
+ // every page in reference/ under a second key, which makes search return each
31
+ // class twice and advertises an unfinished section via get_doc. It is untracked,
32
+ // but untracked does NOT mean unpublished — `files: ["extended/"]` packs from the
33
+ // filesystem, so without this it also ships in the npm tarball.
34
+ const EXTENDED_SKIP_DIRS = new Set(["reference-generated"]);
35
+
36
+ /**
37
+ * Read all skill + extended markdown into the raw { skillFiles, extendedFiles }
38
+ * shape that buildContent() expects.
39
+ */
40
+ export function readRawFiles() {
41
+ const skillFiles = {};
42
+ const extendedFiles = {};
43
+
44
+ // Core SKILL.md
45
+ const skillPath = join(SKILL_DIR, "SKILL.md");
46
+ if (existsSync(skillPath)) {
47
+ skillFiles["SKILL"] = readFileSync(skillPath, "utf-8");
48
+ }
49
+
50
+ // Reference files (skip SKILL.md if it lives in the same dir as references)
51
+ if (existsSync(REFERENCES_DIR)) {
52
+ for (const file of readdirSync(REFERENCES_DIR)) {
53
+ if (!file.endsWith(".md") || file === "SKILL.md") continue;
54
+ skillFiles[file.replace(".md", "")] = readFileSync(join(REFERENCES_DIR, file), "utf-8");
55
+ }
56
+ }
57
+
58
+ // Extended docs + examples (recursive)
59
+ walkExtended(EXTENDED_DIR, "", extendedFiles);
60
+
61
+ return { skillFiles, extendedFiles };
62
+ }
63
+
64
+ function walkExtended(dir, prefix, out) {
65
+ if (!existsSync(dir)) return;
66
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
67
+ const fullPath = join(dir, entry.name);
68
+ if (entry.isDirectory()) {
69
+ // Only skip at the top level — a nested dir of the same name is fine.
70
+ if (!prefix && EXTENDED_SKIP_DIRS.has(entry.name)) continue;
71
+ walkExtended(fullPath, prefix ? `${prefix}/${entry.name}` : entry.name, out);
72
+ } else if (entry.name.endsWith(".md")) {
73
+ const key = prefix
74
+ ? `${prefix}/${entry.name.replace(".md", "")}`
75
+ : entry.name.replace(".md", "");
76
+ out[key] = readFileSync(fullPath, "utf-8");
77
+ }
78
+ }
79
+ }
80
+
81
+ /** Read from disk and build the in-memory content structures. */
82
+ export function loadContentFromDisk() {
83
+ return buildContent(readRawFiles());
84
+ }
package/src/content.js ADDED
@@ -0,0 +1,214 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared, transport-agnostic content layer.
3
+ //
4
+ // Pure logic only — NO filesystem, NO Node built-ins — so this module can be
5
+ // bundled into the Cloudflare Worker as-is. The stdio server reads files from
6
+ // disk (see content-fs.js) and the Worker imports a pre-generated bundle (see
7
+ // scripts/build-worker-content.js); both feed the same buildContent() here so
8
+ // the two deployments behave identically.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /** Chart type aliases → reference file mapping */
12
+ export const CHART_TYPE_MAP = {
13
+ // XY family
14
+ "xy": "xy", "line": "xy", "area": "xy", "bar": "xy", "column": "xy",
15
+ "candlestick": "xy", "ohlc": "xy", "scatter": "xy", "bubble": "xy",
16
+ "stacked": "xy", "stacked-bar": "xy", "stacked-column": "xy",
17
+ "stacked-area": "xy", "range": "xy", "waterfall": "xy",
18
+ // Pie family
19
+ "pie": "pie", "donut": "pie", "funnel": "pie", "pyramid": "pie",
20
+ "semi-circle": "pie", "pictorial": "pie", "sliced": "pie",
21
+ // Map
22
+ "map": "map", "choropleth": "map", "geo": "map", "geographic": "map",
23
+ "world": "map", "country": "map", "bubble-map": "map", "point-map": "map",
24
+ // Hierarchy
25
+ "hierarchy": "hierarchy", "treemap": "hierarchy", "sunburst": "hierarchy",
26
+ "force-directed": "hierarchy", "pack": "hierarchy", "partition": "hierarchy",
27
+ "tree": "hierarchy", "org-chart": "hierarchy",
28
+ // Flow
29
+ "flow": "flow", "sankey": "flow", "chord": "flow", "arc": "flow",
30
+ "alluvial": "flow",
31
+ // Radar
32
+ "radar": "radar", "spider": "radar", "polar": "radar", "gauge": "radar",
33
+ "speedometer": "radar", "meter": "radar",
34
+ // Stock
35
+ "stock": "stock", "financial": "stock", "trading": "stock",
36
+ // Timeline
37
+ "timeline": "timeline", "serpentine": "timeline", "spiral": "timeline",
38
+ "curve": "timeline",
39
+ // Gantt
40
+ "gantt": "gantt", "project-timeline": "gantt",
41
+ // Word cloud
42
+ "wordcloud": "wordcloud", "word-cloud": "wordcloud", "tag-cloud": "wordcloud",
43
+ // Venn
44
+ "venn": "venn",
45
+ // UI
46
+ "ui": "ui-elements", "ui-elements": "ui-elements", "button": "ui-elements",
47
+ "slider": "ui-elements", "controls": "ui-elements",
48
+ };
49
+
50
+ export function parseSections(content) {
51
+ const sections = [];
52
+ const lines = content.split("\n");
53
+ let currentHeading = "";
54
+ let currentBody = [];
55
+
56
+ for (const line of lines) {
57
+ const headingMatch = line.match(/^(#{1,3})\s+(.+)/);
58
+ if (headingMatch) {
59
+ const level = headingMatch[1].length;
60
+ const title = headingMatch[2].trim();
61
+ if (level <= 2) {
62
+ // Major section boundary (# or ##)
63
+ if (currentHeading || currentBody.length > 0) {
64
+ sections.push({ heading: currentHeading, body: currentBody.join("\n") });
65
+ }
66
+ currentHeading = title;
67
+ currentBody = [];
68
+ } else {
69
+ // Sub-heading (###) — include in current section body
70
+ currentBody.push(line);
71
+ }
72
+ } else {
73
+ currentBody.push(line);
74
+ }
75
+ }
76
+ if (currentHeading || currentBody.length > 0) {
77
+ sections.push({ heading: currentHeading, body: currentBody.join("\n") });
78
+ }
79
+ return sections;
80
+ }
81
+
82
+ /**
83
+ * Import sections carry two ### sub-blocks: one for ES modules, one for CDN /
84
+ * script tags. Return only the requested one (esm | html), or "" if not found.
85
+ */
86
+ export function extractImportBlock(body, format) {
87
+ const lines = body.split("\n");
88
+ const wantEsm = format === "esm";
89
+ let capturing = false;
90
+ let captured = [];
91
+ let sawSub = false;
92
+ for (const line of lines) {
93
+ const sub = line.match(/^###\s+(.+)/);
94
+ if (sub) {
95
+ sawSub = true;
96
+ const h = sub[1].toLowerCase();
97
+ const isEsm = /es module|esm|typescript|import/.test(h);
98
+ // Note: "typescript" contains "script", so match "script tag"/"cdn", not bare "script".
99
+ const isCdn = /cdn|script tag|<script/.test(h);
100
+ capturing = wantEsm ? isEsm : isCdn;
101
+ continue;
102
+ }
103
+ if (capturing) captured.push(line);
104
+ }
105
+ if (!sawSub) return ""; // no sub-blocks → caller falls back to full body
106
+ return captured.join("\n").trim();
107
+ }
108
+
109
+ /**
110
+ * Build the in-memory content structures from raw markdown files.
111
+ *
112
+ * @param {{ skillFiles: Record<string,string>, extendedFiles: Record<string,string> }} raw
113
+ * skillFiles — name (no .md) → raw markdown for SKILL.md + references/*.md
114
+ * extendedFiles — path key (no .md) → raw markdown for everything under extended/
115
+ * @returns {{ docs: Map, extendedDocs: Map, examples: Map }}
116
+ */
117
+ export function buildContent({ skillFiles = {}, extendedFiles = {} } = {}) {
118
+ /** @type {Map<string, {title: string, content: string, sections: {heading: string, body: string}[]}>} */
119
+ const docs = new Map();
120
+ /** @type {Map<string, {title: string, content: string, sections: {heading: string, body: string}[], path: string}>} */
121
+ const extendedDocs = new Map();
122
+ /** @type {Map<string, {title: string, category: string, content: string, source: string}>} */
123
+ const examples = new Map();
124
+
125
+ // Skill docs (SKILL.md + references/*.md)
126
+ for (const [name, content] of Object.entries(skillFiles)) {
127
+ const titleMatch = content.match(/^#\s+(.+)/m);
128
+ const title = titleMatch ? titleMatch[1] : name;
129
+ docs.set(name, { title, content, sections: parseSections(content) });
130
+ }
131
+
132
+ // Extended docs + examples (frontmatter-tagged markdown)
133
+ for (const [key, content] of Object.entries(extendedFiles)) {
134
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
135
+ const body = fmMatch ? fmMatch[2].trim() : content;
136
+ const fm = fmMatch ? fmMatch[1] : "";
137
+ const titleMatch = fm.match(/title:\s*"(.+?)"/);
138
+ const categoryMatch = fm.match(/category:\s*"(.+?)"/);
139
+ const sourceMatch = fm.match(/source:\s*"(.+?)"/);
140
+ const fallbackTitle = key.split("/").pop();
141
+ const title = titleMatch ? titleMatch[1] : fallbackTitle;
142
+
143
+ if (key.startsWith("examples/")) {
144
+ examples.set(key, {
145
+ title,
146
+ category: categoryMatch ? categoryMatch[1] : "",
147
+ content: body,
148
+ source: sourceMatch ? sourceMatch[1] : "",
149
+ });
150
+ } else {
151
+ extendedDocs.set(key, {
152
+ title,
153
+ content: body,
154
+ sections: parseSections(body),
155
+ path: key,
156
+ });
157
+ }
158
+ }
159
+
160
+ return { docs, extendedDocs, examples };
161
+ }
162
+
163
+ /**
164
+ * Keyword search over the skill docs and (optionally) the extended docs.
165
+ * @param {{docs: Map, extendedDocs: Map}} content
166
+ */
167
+ export function searchDocs(content, query, maxResults = 10, scope = "skill") {
168
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
169
+ const results = [];
170
+
171
+ // Skill docs (SKILL.md + references/*.md)
172
+ for (const [name, doc] of content.docs) {
173
+ for (const section of doc.sections) {
174
+ const text = (section.heading + " " + section.body).toLowerCase();
175
+ const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
176
+ if (score > 0) {
177
+ results.push({
178
+ file: name,
179
+ source: "skill",
180
+ docTitle: doc.title,
181
+ heading: section.heading,
182
+ body: section.body.trim(),
183
+ // Nudge skill results slightly ahead of extended on ties — the skill
184
+ // is the curated layer; extended is the exhaustive API reference.
185
+ score: score + 0.1,
186
+ });
187
+ }
188
+ }
189
+ }
190
+
191
+ // Extended docs (charts/, concepts/, getting-started/, reference/<class>) —
192
+ // this is where the per-class API + defaults live. Only scanned when scope=all.
193
+ if (scope === "all") {
194
+ for (const [key, doc] of content.extendedDocs) {
195
+ for (const section of doc.sections) {
196
+ const text = (section.heading + " " + section.body).toLowerCase();
197
+ const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
198
+ if (score > 0) {
199
+ results.push({
200
+ file: key,
201
+ source: "extended",
202
+ docTitle: doc.title,
203
+ heading: section.heading,
204
+ body: section.body.trim(),
205
+ score,
206
+ });
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ results.sort((a, b) => b.score - a.score);
213
+ return results.slice(0, maxResults);
214
+ }