sourcey 3.5.1 → 3.5.2

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.
Files changed (35) hide show
  1. package/README.md +90 -10
  2. package/dist/cli.js +122 -3
  3. package/dist/components/layout/Page.d.ts.map +1 -1
  4. package/dist/components/layout/Page.js +4 -1
  5. package/dist/config.d.ts +60 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +28 -3
  8. package/dist/core/godoc-introspector.d.ts +26 -0
  9. package/dist/core/godoc-introspector.d.ts.map +1 -0
  10. package/dist/core/godoc-introspector.js +144 -0
  11. package/dist/core/godoc-loader.d.ts +34 -0
  12. package/dist/core/godoc-loader.d.ts.map +1 -0
  13. package/dist/core/godoc-loader.js +491 -0
  14. package/dist/core/godoc-types.d.ts +109 -0
  15. package/dist/core/godoc-types.d.ts.map +1 -0
  16. package/dist/core/godoc-types.js +8 -0
  17. package/dist/core/markdown-loader.d.ts +10 -0
  18. package/dist/core/markdown-loader.d.ts.map +1 -1
  19. package/dist/core/search-indexer.d.ts.map +1 -1
  20. package/dist/core/search-indexer.js +9 -0
  21. package/dist/core/sourcey-godoc/cmd/sourcey-godoc/main.go +736 -0
  22. package/dist/core/sourcey-godoc/cmd/sourcey-godoc/site.go +497 -0
  23. package/dist/core/sourcey-godoc/cmd/sourcey-godoc/site_test.go +89 -0
  24. package/dist/core/sourcey-godoc/doc.go +11 -0
  25. package/dist/core/sourcey-godoc/go.mod +3 -0
  26. package/dist/dev-server.d.ts.map +1 -1
  27. package/dist/dev-server.js +42 -0
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -0
  31. package/dist/site-assembly.d.ts +3 -0
  32. package/dist/site-assembly.d.ts.map +1 -1
  33. package/dist/site-assembly.js +30 -1
  34. package/dist/themes/default/sourcey.css +244 -0
  35. package/package.json +13 -4
@@ -0,0 +1,144 @@
1
+ import { spawn } from "node:child_process";
2
+ import { access } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { GODOC_SCHEMA_VERSION } from "./godoc-types.js";
6
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
7
+ const PACKAGED_HELPER_DIR = join(MODULE_DIR, "sourcey-godoc");
8
+ const DEV_HELPER_DIR = resolve(MODULE_DIR, "../../go/sourcey-godoc");
9
+ const HELPER_ENTRY = "cmd/sourcey-godoc/main.go";
10
+ export class GodocIntrospectorError extends Error {
11
+ code;
12
+ cause;
13
+ constructor(code, message, cause) {
14
+ super(message);
15
+ this.name = "GodocIntrospectorError";
16
+ this.code = code;
17
+ this.cause = cause;
18
+ }
19
+ }
20
+ /**
21
+ * Run the Go introspector and return a parsed GodocSpec.
22
+ *
23
+ * Live mode invokes `go run` against the bundled helper, with `GOOS`/`GOARCH`
24
+ * and build tags pinned by `goEnv` when configured. Throws a structured
25
+ * `GodocIntrospectorError` on failure so callers can surface the right
26
+ * diagnostic kind.
27
+ */
28
+ export async function runIntrospector(opts) {
29
+ const helperDir = await resolveHelperDir();
30
+ const goBinary = opts.goBinary ?? "go";
31
+ const args = buildArgs(opts.config);
32
+ const env = buildEnv(opts.config);
33
+ const { stdout, stderr, code } = await runGo(goBinary, args, helperDir, env);
34
+ if (code === 2) {
35
+ throw new GodocIntrospectorError("GODOC_INTROSPECTOR_FAILED", `Go introspector exited with code 2:\n${stderr.trim()}`);
36
+ }
37
+ if (code !== 0 && code !== 1) {
38
+ throw new GodocIntrospectorError("GODOC_INTROSPECTOR_FAILED", `Go introspector exited with code ${code}:\n${stderr.trim()}`);
39
+ }
40
+ let snapshot;
41
+ try {
42
+ snapshot = JSON.parse(stdout);
43
+ }
44
+ catch (err) {
45
+ throw new GodocIntrospectorError("GODOC_INTROSPECTOR_BAD_JSON", `Could not parse introspector output as JSON: ${err.message}\n` +
46
+ `stdout (first 200 chars): ${stdout.slice(0, 200)}`, err);
47
+ }
48
+ if (snapshot.schema_version !== GODOC_SCHEMA_VERSION) {
49
+ throw new GodocIntrospectorError("GODOC_SCHEMA_MISMATCH", `Introspector emitted schema_version ${snapshot.schema_version}, ` +
50
+ `expected ${GODOC_SCHEMA_VERSION}. Rebuild Sourcey or update the helper.`);
51
+ }
52
+ return {
53
+ modulePath: snapshot.module_path,
54
+ moduleDir: opts.config.module,
55
+ generatedAt: snapshot.generated_at,
56
+ packages: snapshot.packages,
57
+ diagnostics: snapshot.diagnostics ?? [],
58
+ };
59
+ }
60
+ /** Locate the helper source in either the packaged dist tree or repo checkout. */
61
+ async function resolveHelperDir() {
62
+ const candidates = [PACKAGED_HELPER_DIR, DEV_HELPER_DIR];
63
+ for (const candidate of candidates) {
64
+ try {
65
+ await access(join(candidate, "go.mod"));
66
+ await access(join(candidate, HELPER_ENTRY));
67
+ return candidate;
68
+ }
69
+ catch {
70
+ // Try the next known layout.
71
+ }
72
+ }
73
+ throw new GodocIntrospectorError("GODOC_HELPER_MISSING", "Sourcey's Go documentation extractor is missing. Checked:\n" +
74
+ candidates.map((candidate) => `- ${join(candidate, HELPER_ENTRY)}`).join("\n") +
75
+ "\nReinstall sourcey or run `npm run build`.");
76
+ }
77
+ function buildArgs(cfg) {
78
+ const args = ["run", "./cmd/sourcey-godoc", "--module", cfg.module];
79
+ for (const pattern of cfg.packages)
80
+ args.push("--packages", pattern);
81
+ for (const exclude of cfg.exclude)
82
+ args.push("--exclude", exclude);
83
+ if (cfg.includeTests)
84
+ args.push("--include-tests=true");
85
+ else
86
+ args.push("--include-tests=false");
87
+ if (cfg.includeUnexported)
88
+ args.push("--include-unexported");
89
+ return args;
90
+ }
91
+ function buildEnv(cfg) {
92
+ const env = { ...process.env };
93
+ if (cfg.goEnv?.GOOS)
94
+ env.GOOS = cfg.goEnv.GOOS;
95
+ if (cfg.goEnv?.GOARCH)
96
+ env.GOARCH = cfg.goEnv.GOARCH;
97
+ if (cfg.goEnv?.tags?.length) {
98
+ env.GOFLAGS = [process.env.GOFLAGS, `-tags=${cfg.goEnv.tags.join(",")}`]
99
+ .filter(Boolean)
100
+ .join(" ");
101
+ }
102
+ return env;
103
+ }
104
+ function runGo(binary, args, cwd, env) {
105
+ return new Promise((resolvePromise, rejectPromise) => {
106
+ let child;
107
+ try {
108
+ child = spawn(binary, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
109
+ }
110
+ catch (err) {
111
+ rejectPromise(new GodocIntrospectorError("GO_NOT_FOUND", `Could not launch Go (${binary}). ` +
112
+ "Install Go from https://go.dev/dl, or use mode: \"snapshot\" with a committed godoc.json.", err));
113
+ return;
114
+ }
115
+ const stdoutChunks = [];
116
+ const stderrChunks = [];
117
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
118
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
119
+ child.on("error", (err) => {
120
+ if (err.code === "ENOENT") {
121
+ rejectPromise(new GodocIntrospectorError("GO_NOT_FOUND", `Could not launch Go (${binary}). ` +
122
+ "Install Go from https://go.dev/dl, or use mode: \"snapshot\" with a committed godoc.json.", err));
123
+ return;
124
+ }
125
+ rejectPromise(new GodocIntrospectorError("GODOC_INTROSPECTOR_FAILED", err.message, err));
126
+ });
127
+ child.on("close", (code) => {
128
+ resolvePromise({
129
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
130
+ stderr: Buffer.concat(stderrChunks).toString("utf8"),
131
+ code: code ?? -1,
132
+ });
133
+ });
134
+ });
135
+ }
136
+ /** Helper entry paths, exported for tests that pin the runtime. */
137
+ export const __helperEntryCandidatesForTests = [
138
+ join(PACKAGED_HELPER_DIR, HELPER_ENTRY),
139
+ join(DEV_HELPER_DIR, HELPER_ENTRY),
140
+ ];
141
+ /** Resolve the helper entry path relative to a given module directory. */
142
+ export function resolveHelperPath(fromDir) {
143
+ return resolve(fromDir, "sourcey-godoc", HELPER_ENTRY);
144
+ }
@@ -0,0 +1,34 @@
1
+ import type { ResolvedGodocConfig } from "../config.js";
2
+ import type { MarkdownPage } from "./markdown-loader.js";
3
+ import type { SiteTab } from "./navigation.js";
4
+ export interface GodocLoaderResult {
5
+ pages: Map<string, MarkdownPage>;
6
+ navTab: SiteTab;
7
+ diagnostics: GodocLoaderDiagnostic[];
8
+ }
9
+ export interface GodocLoaderDiagnostic {
10
+ severity: "error" | "warning" | "info";
11
+ code: string;
12
+ message: string;
13
+ package?: string;
14
+ file?: string;
15
+ line?: number;
16
+ }
17
+ export interface GodocSourceLinkOptions {
18
+ repo?: string;
19
+ editBranch?: string;
20
+ editBasePath?: string;
21
+ }
22
+ /**
23
+ * Resolve a configured godoc tab into pre-rendered Sourcey pages plus a
24
+ * navigation tab. Honours `mode: "live" | "snapshot" | "auto"`:
25
+ *
26
+ * - live: invoke the Go introspector. Requires Go on PATH.
27
+ * - snapshot: read the configured `godoc.json`. No Go required.
28
+ * - auto (default): use live when Go is available; fall back to snapshot.
29
+ *
30
+ * Source-of-truth contract: when both Go and a snapshot are present in
31
+ * "auto" mode, live wins. Snapshot is a fallback cache, not a pin.
32
+ */
33
+ export declare function loadGodocTab(config: ResolvedGodocConfig, tabSlug: string, tabLabel: string, sourceLinks?: GodocSourceLinkOptions): Promise<GodocLoaderResult>;
34
+ //# sourceMappingURL=godoc-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"godoc-loader.d.ts","sourceRoot":"","sources":["../../src/core/godoc-loader.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAiBxD,OAAO,KAAK,EAAE,YAAY,EAAmB,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,OAAO,EAA6B,MAAM,iBAAiB,CAAC;AAE1E,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,qBAAqB,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,WAAW,GAAE,sBAA2B,GACvC,OAAO,CAAC,iBAAiB,CAAC,CAG5B"}
@@ -0,0 +1,491 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { spawnSync } from "node:child_process";
3
+ import { GODOC_SCHEMA_VERSION } from "./godoc-types.js";
4
+ import { runIntrospector, GodocIntrospectorError } from "./godoc-introspector.js";
5
+ import { renderCodeBlock, renderMarkdown, } from "../utils/markdown.js";
6
+ /**
7
+ * Resolve a configured godoc tab into pre-rendered Sourcey pages plus a
8
+ * navigation tab. Honours `mode: "live" | "snapshot" | "auto"`:
9
+ *
10
+ * - live: invoke the Go introspector. Requires Go on PATH.
11
+ * - snapshot: read the configured `godoc.json`. No Go required.
12
+ * - auto (default): use live when Go is available; fall back to snapshot.
13
+ *
14
+ * Source-of-truth contract: when both Go and a snapshot are present in
15
+ * "auto" mode, live wins. Snapshot is a fallback cache, not a pin.
16
+ */
17
+ export async function loadGodocTab(config, tabSlug, tabLabel, sourceLinks = {}) {
18
+ const spec = await loadSpec(config);
19
+ return buildResult(spec, tabSlug, tabLabel, config, sourceLinks);
20
+ }
21
+ async function loadSpec(config) {
22
+ switch (config.mode) {
23
+ case "live":
24
+ return runIntrospector({ config });
25
+ case "snapshot":
26
+ if (!config.snapshot) {
27
+ throw new GodocIntrospectorError("GODOC_SNAPSHOT_MISSING", "godoc mode is 'snapshot' but no snapshot path was configured.");
28
+ }
29
+ return loadSnapshot(config.snapshot, config.module);
30
+ case "auto":
31
+ if (goAvailable()) {
32
+ return runIntrospector({ config });
33
+ }
34
+ if (config.snapshot) {
35
+ return loadSnapshot(config.snapshot, config.module);
36
+ }
37
+ throw new GodocIntrospectorError("GO_NOT_FOUND", "godoc mode is 'auto' and Go is not on PATH. Install Go or set " +
38
+ "mode: 'snapshot' with a committed godoc.json.");
39
+ default: {
40
+ const exhaustive = config.mode;
41
+ throw new Error(`Unknown godoc mode: ${exhaustive}`);
42
+ }
43
+ }
44
+ }
45
+ function goAvailable() {
46
+ try {
47
+ const result = spawnSync("go", ["version"], { stdio: "pipe" });
48
+ return result.status === 0;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ async function loadSnapshot(snapshotPath, moduleDir) {
55
+ let raw;
56
+ try {
57
+ raw = await readFile(snapshotPath, "utf8");
58
+ }
59
+ catch (err) {
60
+ throw new GodocIntrospectorError("GODOC_SNAPSHOT_UNREADABLE", `Could not read godoc snapshot at ${snapshotPath}: ${err.message}`, err);
61
+ }
62
+ let snapshot;
63
+ try {
64
+ snapshot = JSON.parse(raw);
65
+ }
66
+ catch (err) {
67
+ throw new GodocIntrospectorError("GODOC_SNAPSHOT_BAD_JSON", `Snapshot at ${snapshotPath} is not valid JSON: ${err.message}`, err);
68
+ }
69
+ if (snapshot.source !== "sourcey-godoc") {
70
+ throw new GodocIntrospectorError("GODOC_SNAPSHOT_BAD_SOURCE", `Snapshot at ${snapshotPath} has source="${snapshot.source}", expected "sourcey-godoc".`);
71
+ }
72
+ if (snapshot.schema_version !== GODOC_SCHEMA_VERSION) {
73
+ throw new GodocIntrospectorError("GODOC_SCHEMA_MISMATCH", `Snapshot at ${snapshotPath} has schema_version ${snapshot.schema_version}; ` +
74
+ `this build of sourcey supports ${GODOC_SCHEMA_VERSION}. Regenerate with \`sourcey godoc\`.`);
75
+ }
76
+ return {
77
+ modulePath: snapshot.module_path,
78
+ moduleDir,
79
+ generatedAt: snapshot.generated_at,
80
+ packages: snapshot.packages,
81
+ diagnostics: snapshot.diagnostics ?? [],
82
+ };
83
+ }
84
+ function buildResult(spec, tabSlug, tabLabel, config, sourceLinks) {
85
+ const pages = new Map();
86
+ const groups = new Map();
87
+ const filteredPackages = config.hideUndocumented
88
+ ? spec.packages.filter((p) => p.doc.trim().length > 0)
89
+ : spec.packages;
90
+ for (const pkg of filteredPackages) {
91
+ const slug = packageSlug(pkg.importPath, spec.modulePath);
92
+ const html = renderPackagePage(pkg, slug, sourceLinks);
93
+ const headings = collectHeadings(pkg);
94
+ const searchEntries = collectSearchEntries(pkg);
95
+ // Fallback description for undocumented packages — using the import
96
+ // path keeps llms.txt and search summaries clean instead of leaking
97
+ // raw HTML structure.
98
+ const description = pkg.synopsis || pkg.importPath;
99
+ pages.set(slug, {
100
+ kind: "markdown",
101
+ title: packageTitle(pkg, spec.modulePath),
102
+ description,
103
+ slug,
104
+ html,
105
+ headings,
106
+ sourcePath: `godoc/${slug}.md`,
107
+ editPath: packageEditPath(pkg),
108
+ editBasePath: sourceLinks.editBasePath ?? "",
109
+ searchEntries,
110
+ });
111
+ const groupKey = navGroupKey(pkg.importPath, spec.modulePath);
112
+ if (!groups.has(groupKey))
113
+ groups.set(groupKey, []);
114
+ groups.get(groupKey).push({ slug, label: packageNavLabel(pkg, spec.modulePath) });
115
+ }
116
+ const indexSlug = "index";
117
+ const indexHtml = renderIndexPage(spec, filteredPackages);
118
+ pages.set(indexSlug, {
119
+ kind: "markdown",
120
+ title: tabLabel,
121
+ description: spec.modulePath,
122
+ slug: indexSlug,
123
+ html: indexHtml,
124
+ headings: [],
125
+ sourcePath: `godoc/${indexSlug}.md`,
126
+ editPath: null,
127
+ editBasePath: sourceLinks.editBasePath ?? "",
128
+ });
129
+ const navGroups = [];
130
+ navGroups.push({
131
+ label: tabLabel,
132
+ items: [{ label: "Overview", href: `${tabSlug}/${indexSlug}.html`, id: indexSlug }],
133
+ });
134
+ const sortedKeys = [...groups.keys()].sort();
135
+ for (const key of sortedKeys) {
136
+ const items = groups.get(key).sort((a, b) => a.label.localeCompare(b.label));
137
+ navGroups.push({
138
+ label: key,
139
+ items: items.map((entry) => ({
140
+ label: entry.label,
141
+ href: `${tabSlug}/${entry.slug}.html`,
142
+ id: entry.slug,
143
+ })),
144
+ });
145
+ }
146
+ const firstItem = navGroups[0]?.items[0];
147
+ const diagnostics = spec.diagnostics.map((d) => ({
148
+ severity: d.severity,
149
+ code: d.code,
150
+ message: d.message,
151
+ package: d.package,
152
+ file: d.file,
153
+ line: d.line,
154
+ }));
155
+ return {
156
+ pages,
157
+ diagnostics,
158
+ navTab: {
159
+ label: tabLabel,
160
+ slug: tabSlug,
161
+ href: firstItem?.href ?? `${tabSlug}/`,
162
+ kind: "docs",
163
+ groups: navGroups,
164
+ },
165
+ };
166
+ }
167
+ function packageTitle(pkg, modulePath) {
168
+ const rel = relativeImportPath(pkg.importPath, modulePath);
169
+ if (rel === "." || rel === "")
170
+ return pkg.importPath;
171
+ return rel;
172
+ }
173
+ function packageNavLabel(pkg, modulePath) {
174
+ const rel = relativeImportPath(pkg.importPath, modulePath);
175
+ if (rel === "." || rel === "")
176
+ return pkg.name;
177
+ return rel;
178
+ }
179
+ function relativeImportPath(importPath, modulePath) {
180
+ if (!modulePath)
181
+ return importPath;
182
+ if (importPath === modulePath)
183
+ return ".";
184
+ if (importPath.startsWith(`${modulePath}/`)) {
185
+ return importPath.slice(modulePath.length + 1);
186
+ }
187
+ return importPath;
188
+ }
189
+ function packageSlug(importPath, modulePath) {
190
+ const rel = relativeImportPath(importPath, modulePath);
191
+ if (rel === "." || rel === "")
192
+ return "package-root";
193
+ return `pkg-${rel.replace(/[/]/g, "-").replace(/[^a-zA-Z0-9_-]/g, "")}`;
194
+ }
195
+ function navGroupKey(importPath, modulePath) {
196
+ const rel = relativeImportPath(importPath, modulePath);
197
+ if (rel === "." || rel === "")
198
+ return "Root";
199
+ const first = rel.split("/")[0];
200
+ return first.charAt(0).toUpperCase() + first.slice(1);
201
+ }
202
+ function collectHeadings(pkg) {
203
+ const headings = [];
204
+ if (pkg.consts.length > 0)
205
+ headings.push({ id: "constants", text: "Constants", level: 2 });
206
+ if (pkg.vars.length > 0)
207
+ headings.push({ id: "variables", text: "Variables", level: 2 });
208
+ if (pkg.funcs.length > 0) {
209
+ headings.push({ id: "functions", text: "Functions", level: 2 });
210
+ for (const f of pkg.funcs) {
211
+ headings.push({ id: funcAnchor(f.name), text: f.name, level: 3 });
212
+ }
213
+ }
214
+ if (pkg.types.length > 0) {
215
+ headings.push({ id: "types", text: "Types", level: 2 });
216
+ for (const t of pkg.types) {
217
+ headings.push({ id: typeAnchor(t.name), text: t.name, level: 3 });
218
+ }
219
+ }
220
+ if (pkg.examples.length > 0) {
221
+ headings.push({ id: "examples", text: "Examples", level: 2 });
222
+ }
223
+ return headings;
224
+ }
225
+ function collectSearchEntries(pkg) {
226
+ const entries = [];
227
+ for (const value of pkg.consts) {
228
+ entries.push(godocSearchEntry(`const ${value.name}`, value.doc, value.declaration, valueAnchor("const", value.name), "go constant"));
229
+ }
230
+ for (const value of pkg.vars) {
231
+ entries.push(godocSearchEntry(`var ${value.name}`, value.doc, value.declaration, valueAnchor("var", value.name), "go variable"));
232
+ }
233
+ for (const fn of pkg.funcs) {
234
+ entries.push(godocSearchEntry(fn.signature, fn.doc, fn.signature, funcAnchor(fn.name), "go function"));
235
+ for (const ex of fn.examples) {
236
+ entries.push(godocSearchEntry(exampleTitle(`Example ${fn.name}`, ex), ex.doc, ex.code, funcAnchor(fn.name), "go example"));
237
+ }
238
+ }
239
+ for (const t of pkg.types) {
240
+ entries.push(godocSearchEntry(`type ${t.name}`, t.doc, t.declaration, typeAnchor(t.name), "go type"));
241
+ for (const method of t.methods) {
242
+ entries.push(godocSearchEntry(`${t.name}.${method.name}`, method.doc, method.signature, methodAnchor(t.name, method.name), "go method"));
243
+ for (const ex of method.examples) {
244
+ entries.push(godocSearchEntry(exampleTitle(`Example ${t.name}.${method.name}`, ex), ex.doc, ex.code, methodAnchor(t.name, method.name), "go example"));
245
+ }
246
+ }
247
+ for (const ex of t.examples) {
248
+ entries.push(godocSearchEntry(exampleTitle(`Example ${t.name}`, ex), ex.doc, ex.code, typeAnchor(t.name), "go example"));
249
+ }
250
+ }
251
+ for (const ex of pkg.examples) {
252
+ entries.push(godocSearchEntry(exampleTitle("Example", ex), ex.doc, ex.code, "examples", "go example"));
253
+ }
254
+ return entries;
255
+ }
256
+ function godocSearchEntry(title, doc, declaration, anchor, category) {
257
+ return {
258
+ title,
259
+ content: [doc, declaration].filter(Boolean).join("\n\n"),
260
+ anchor,
261
+ category,
262
+ };
263
+ }
264
+ function exampleTitle(prefix, ex) {
265
+ return ex.suffix ? `${prefix} (${humaniseSuffix(ex.suffix)})` : prefix;
266
+ }
267
+ function packageEditPath(pkg) {
268
+ const firstFile = pkg.files[0];
269
+ if (!firstFile)
270
+ return null;
271
+ return firstFile;
272
+ }
273
+ function funcAnchor(name) {
274
+ return `func-${slugifySymbol(name)}`;
275
+ }
276
+ function typeAnchor(name) {
277
+ return `type-${slugifySymbol(name)}`;
278
+ }
279
+ function methodAnchor(typeName, methodName) {
280
+ return `method-${slugifySymbol(typeName)}-${slugifySymbol(methodName)}`;
281
+ }
282
+ function valueAnchor(prefix, name) {
283
+ return `${prefix}-${slugifySymbol(name)}`;
284
+ }
285
+ function slugifySymbol(name) {
286
+ return name.replace(/[^a-zA-Z0-9_]/g, "_");
287
+ }
288
+ // ---------------------------------------------------------------------------
289
+ // HTML rendering
290
+ // ---------------------------------------------------------------------------
291
+ function renderIndexPage(spec, packages) {
292
+ const parts = [];
293
+ if (spec.modulePath) {
294
+ parts.push(`<p class="godoc-import-path"><code>${escHtml(spec.modulePath)}</code></p>`);
295
+ }
296
+ const cards = [];
297
+ for (const pkg of packages) {
298
+ const slug = packageSlug(pkg.importPath, spec.modulePath);
299
+ const title = packageTitle(pkg, spec.modulePath);
300
+ const synopsis = pkg.synopsis ? `<p>${escHtml(pkg.synopsis)}</p>` : "";
301
+ cards.push(`<a href="${slug}.html" class="card-item">` +
302
+ `<div class="card-item-inner">` +
303
+ `<h3 class="card-item-title">${escHtml(title)}</h3>` +
304
+ `<div class="card-item-content">${synopsis}` +
305
+ `<p style="margin:0.5rem 0 0;font-size:0.8rem;opacity:0.5"><code>${escHtml(pkg.importPath)}</code></p>` +
306
+ `</div></div></a>`);
307
+ }
308
+ if (cards.length > 0) {
309
+ const cols = cards.length <= 2 ? "2" : "3";
310
+ parts.push(`<div class="card-group not-prose" data-cols="${cols}">\n${cards.join("\n")}\n</div>`);
311
+ }
312
+ else {
313
+ parts.push(`<p>No Go packages were resolved.</p>`);
314
+ }
315
+ return parts.join("\n");
316
+ }
317
+ function renderPackagePage(pkg, slug, sourceLinks) {
318
+ const parts = [];
319
+ parts.push(`<p class="godoc-import"><code>import "${escHtml(pkg.importPath)}"</code></p>`);
320
+ if (pkg.doc) {
321
+ parts.push(`<div class="godoc-doc">${renderDoc(pkg.doc)}</div>`);
322
+ }
323
+ parts.push(renderTableOfContents(pkg));
324
+ if (pkg.consts.length > 0) {
325
+ parts.push(`<h2 id="constants">Constants</h2>`);
326
+ parts.push(renderValueGroup(pkg.consts, "const", sourceLinks));
327
+ }
328
+ if (pkg.vars.length > 0) {
329
+ parts.push(`<h2 id="variables">Variables</h2>`);
330
+ parts.push(renderValueGroup(pkg.vars, "var", sourceLinks));
331
+ }
332
+ if (pkg.funcs.length > 0) {
333
+ parts.push(`<h2 id="functions">Functions</h2>`);
334
+ for (const fn of pkg.funcs) {
335
+ parts.push(renderFunc(fn, funcAnchor(fn.name), sourceLinks));
336
+ }
337
+ }
338
+ if (pkg.types.length > 0) {
339
+ parts.push(`<h2 id="types">Types</h2>`);
340
+ for (const t of pkg.types) {
341
+ parts.push(renderType(t, sourceLinks));
342
+ }
343
+ }
344
+ if (pkg.examples.length > 0) {
345
+ parts.push(`<h2 id="examples">Examples</h2>`);
346
+ for (const ex of pkg.examples) {
347
+ parts.push(renderExample(ex));
348
+ }
349
+ }
350
+ void slug;
351
+ return parts.join("\n");
352
+ }
353
+ function renderTableOfContents(pkg) {
354
+ const items = [];
355
+ if (pkg.consts.length > 0)
356
+ items.push(`<li><a href="#constants">Constants</a></li>`);
357
+ if (pkg.vars.length > 0)
358
+ items.push(`<li><a href="#variables">Variables</a></li>`);
359
+ if (pkg.funcs.length > 0) {
360
+ items.push(`<li><a href="#functions">Functions</a></li>`);
361
+ for (const fn of pkg.funcs) {
362
+ items.push(`<li class="godoc-toc-sub"><a href="#${funcAnchor(fn.name)}">${escHtml(fn.signature)}</a></li>`);
363
+ }
364
+ }
365
+ if (pkg.types.length > 0) {
366
+ items.push(`<li><a href="#types">Types</a></li>`);
367
+ for (const t of pkg.types) {
368
+ items.push(`<li class="godoc-toc-sub"><a href="#${typeAnchor(t.name)}">type ${escHtml(t.name)}</a></li>`);
369
+ }
370
+ }
371
+ if (pkg.examples.length > 0)
372
+ items.push(`<li><a href="#examples">Examples</a></li>`);
373
+ if (items.length === 0)
374
+ return "";
375
+ return `<nav class="godoc-toc not-prose"><h4>Index</h4><ul>${items.join("")}</ul></nav>`;
376
+ }
377
+ function renderValueGroup(values, kind, sourceLinks) {
378
+ const seenDeclarations = new Set();
379
+ const parts = [];
380
+ for (const v of values) {
381
+ const id = valueAnchor(kind, v.name);
382
+ if (!seenDeclarations.has(v.declaration)) {
383
+ seenDeclarations.add(v.declaration);
384
+ const docHtml = v.doc ? `<div class="godoc-doc">${renderDoc(v.doc)}</div>` : "";
385
+ parts.push(`<section class="godoc-value" id="${id}">` +
386
+ docHtml +
387
+ renderSourceLink(v.position, sourceLinks) +
388
+ renderCodeBlock(v.declaration, "go") +
389
+ `</section>`);
390
+ }
391
+ else {
392
+ // For the secondary names in a grouped declaration we still want
393
+ // anchor stability so external links don't break — emit an empty,
394
+ // styled anchor target.
395
+ parts.push(`<a id="${id}" class="godoc-anchor"></a>`);
396
+ }
397
+ }
398
+ return parts.join("\n");
399
+ }
400
+ function renderFunc(fn, anchorId, sourceLinks) {
401
+ const parts = [];
402
+ parts.push(`<section class="godoc-func" id="${anchorId}">`);
403
+ parts.push(`<h3 class="godoc-symbol">${escHtml(fn.signature)}</h3>`);
404
+ parts.push(renderSourceLink(fn.position, sourceLinks));
405
+ if (fn.doc)
406
+ parts.push(`<div class="godoc-doc">${renderDoc(fn.doc)}</div>`);
407
+ for (const ex of fn.examples)
408
+ parts.push(renderExample(ex));
409
+ parts.push(`</section>`);
410
+ return parts.join("\n");
411
+ }
412
+ function renderType(t, sourceLinks) {
413
+ const parts = [];
414
+ const id = typeAnchor(t.name);
415
+ parts.push(`<section class="godoc-type" id="${id}">`);
416
+ parts.push(`<h3 class="godoc-symbol">type ${escHtml(t.name)}</h3>`);
417
+ parts.push(renderSourceLink(t.position, sourceLinks));
418
+ if (t.doc)
419
+ parts.push(`<div class="godoc-doc">${renderDoc(t.doc)}</div>`);
420
+ parts.push(renderCodeBlock(t.declaration, "go"));
421
+ if (t.fields.length > 0 && (t.kind === "struct" || t.kind === "interface")) {
422
+ parts.push(renderFields(t));
423
+ }
424
+ for (const m of t.methods) {
425
+ parts.push(renderMethod(t.name, m, sourceLinks));
426
+ }
427
+ for (const ex of t.examples)
428
+ parts.push(renderExample(ex));
429
+ parts.push(`</section>`);
430
+ return parts.join("\n");
431
+ }
432
+ function renderFields(t) {
433
+ const heading = t.kind === "interface" ? "Methods" : "Fields";
434
+ const rows = [];
435
+ for (const f of t.fields) {
436
+ const docHtml = f.doc ? `<div class="godoc-field-doc">${renderDoc(f.doc)}</div>` : "";
437
+ const tagHtml = f.tag ? `<code class="godoc-tag">\`${escHtml(f.tag)}\`</code>` : "";
438
+ rows.push(`<li class="godoc-field"><code class="godoc-field-sig">${escHtml(f.name)}` +
439
+ (f.embedded ? "" : ` <span class="godoc-field-type">${escHtml(f.type)}</span>`) +
440
+ `</code> ${tagHtml}${docHtml}</li>`);
441
+ }
442
+ return `<details class="godoc-fields" open><summary>${heading}</summary><ul>${rows.join("")}</ul></details>`;
443
+ }
444
+ function renderMethod(typeName, m, sourceLinks) {
445
+ const id = methodAnchor(typeName, m.name);
446
+ return renderFunc(m, id, sourceLinks).replace(`id="${funcAnchor(m.name)}"`, `id="${id}"`);
447
+ }
448
+ function renderSourceLink(position, sourceLinks) {
449
+ const href = sourceURL(position, sourceLinks);
450
+ if (!href || !position)
451
+ return "";
452
+ return `<p class="godoc-source"><a href="${escAttr(href)}" target="_blank" rel="noopener noreferrer">Source: ${escHtml(position.file)}:${position.line}</a></p>`;
453
+ }
454
+ function sourceURL(position, sourceLinks) {
455
+ if (!position || !sourceLinks.repo || !sourceLinks.editBranch)
456
+ return undefined;
457
+ const repoBase = sourceLinks.repo.replace(/\/$/, "");
458
+ const basePath = sourceLinks.editBasePath ? `${sourceLinks.editBasePath.replace(/^\/|\/$/g, "")}/` : "";
459
+ return `${repoBase}/blob/${sourceLinks.editBranch}/${basePath}${position.file}#L${position.line}`;
460
+ }
461
+ function renderExample(ex) {
462
+ const title = escHtml(exampleTitle("Example", ex));
463
+ const parts = [`<details class="godoc-example"><summary>${title}</summary>`];
464
+ if (ex.doc)
465
+ parts.push(`<div class="godoc-doc">${renderDoc(ex.doc)}</div>`);
466
+ parts.push(renderCodeBlock(ex.code, "go"));
467
+ if (ex.output) {
468
+ parts.push(`<p class="godoc-example-output-label">Output:</p>`);
469
+ parts.push(renderCodeBlock(ex.output, "text"));
470
+ }
471
+ parts.push(`</details>`);
472
+ return parts.join("\n");
473
+ }
474
+ function humaniseSuffix(suffix) {
475
+ return suffix
476
+ .replace(/_/g, " ")
477
+ .replace(/\b\w/g, (c) => c.toUpperCase());
478
+ }
479
+ function renderDoc(input) {
480
+ return renderMarkdown(input).trim();
481
+ }
482
+ function escHtml(s) {
483
+ return s
484
+ .replace(/&/g, "&amp;")
485
+ .replace(/</g, "&lt;")
486
+ .replace(/>/g, "&gt;")
487
+ .replace(/"/g, "&quot;");
488
+ }
489
+ function escAttr(s) {
490
+ return escHtml(s);
491
+ }