sourcey 3.5.10 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -96
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +98 -0
- package/dist/adapters/mkdocs.d.ts +5 -0
- package/dist/adapters/mkdocs.d.ts.map +1 -0
- package/dist/adapters/mkdocs.js +367 -0
- package/dist/adapters/shared.d.ts +12 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +134 -0
- package/dist/adapters/types.d.ts +68 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +1 -0
- package/dist/client/search.js +77 -8
- package/dist/components/layout/Head.js +16 -3
- package/dist/components/layout/TableOfContents.d.ts.map +1 -1
- package/dist/components/layout/TableOfContents.js +4 -2
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +79 -141
- package/dist/core/doxygen-loader.d.ts.map +1 -1
- package/dist/core/doxygen-loader.js +2 -0
- package/dist/core/markdown-loader.d.ts +14 -0
- package/dist/core/markdown-loader.d.ts.map +1 -1
- package/dist/core/markdown-loader.js +63 -22
- package/dist/core/navigation.d.ts.map +1 -1
- package/dist/core/navigation.js +8 -7
- package/dist/core/search-indexer.d.ts +10 -0
- package/dist/core/search-indexer.d.ts.map +1 -1
- package/dist/core/search-indexer.js +5 -0
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +60 -33
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +19 -14
- package/dist/site-assembly.d.ts.map +1 -1
- package/dist/site-assembly.js +107 -25
- package/dist/utils/markdown.d.ts.map +1 -1
- package/dist/utils/markdown.js +15 -4
- package/dist/vite-plugin.d.ts +2 -0
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +71 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -27,8 +27,9 @@ npx sourcey init
|
|
|
27
27
|
- **API reference from OpenAPI**: endpoints, parameters, request/response schemas, auto-generated code samples in 10 languages (cURL, JavaScript, TypeScript, Python, Go, Ruby, Java, PHP, Rust, C#)
|
|
28
28
|
- **MCP server documentation**: tools, resources, prompts rendered as browsable reference with JSON-RPC, TypeScript, and Python code samples. Color-coded method types, annotation badges, connection config cards
|
|
29
29
|
- **Rich guides**: markdown pages with steps, cards, accordions, syntax-highlighted code blocks, and prose alongside your API reference
|
|
30
|
+
- **MkDocs source import**: point a tab at `mkdocs.yml`; Sourcey reads `docs_dir` and `nav` so existing MkDocs markdown sites can render without hand-copying the sidebar structure
|
|
30
31
|
- **Product story pages**: changelog, roadmap pages, examples, reference material, search, and portable context exports in one source-owned site
|
|
31
|
-
- **C++ and Doxygen**: feed Doxygen XML output, get modern searchable API docs. No new parser, no four-tool Breathe/Exhale/Sphinx pipeline
|
|
32
|
+
- **C++ and Doxygen**: feed Doxygen XML output, get modern searchable API docs with exact member search, source links, templates, qualifiers, examples, inherited members, and relationship sections. No new parser, no four-tool Breathe/Exhale/Sphinx pipeline
|
|
32
33
|
- **Go and godoc**: native package documentation extracted from Go source via the toolchain. Render Go modules as Sourcey tabs, generate standalone static Go docs sites, or commit `godoc.json` snapshots for JS-only docs hosts. No Doxygen detour
|
|
33
34
|
- **Context exports**: auto-generate llms.txt and llms-full.txt alongside your HTML as alternate views of the same documentation graph
|
|
34
35
|
- **TypeScript config**: `sourcey.config.ts` with `defineConfig()` autocomplete; theme, navbar, CTA buttons, footer
|
|
@@ -45,12 +46,12 @@ npx sourcey init
|
|
|
45
46
|
|
|
46
47
|
The full Sourcey binary handles OpenAPI, Doxygen, godoc, MCP, and Markdown sources.
|
|
47
48
|
|
|
48
|
-
| Path | Command
|
|
49
|
-
|
|
50
|
-
| npm | `npm install -g sourcey`
|
|
51
|
-
| Homebrew | `brew tap sourcey/tap && brew install sourcey`
|
|
52
|
-
| Docker | `docker run -v "$PWD":/docs sourcey/sourcey`
|
|
53
|
-
| Nix | `nix run github:sourcey/sourcey`
|
|
49
|
+
| Path | Command | Requires |
|
|
50
|
+
| -------- | ---------------------------------------------- | ----------------- |
|
|
51
|
+
| npm | `npm install -g sourcey` | Node 20+ |
|
|
52
|
+
| Homebrew | `brew tap sourcey/tap && brew install sourcey` | macOS / Linuxbrew |
|
|
53
|
+
| Docker | `docker run -v "$PWD":/docs sourcey/sourcey` | Docker |
|
|
54
|
+
| Nix | `nix run github:sourcey/sourcey` | Nix (flakes) |
|
|
54
55
|
|
|
55
56
|
Then `sourcey init` to scaffold a new project, or `sourcey build` against an existing one. See [docs/install.md](docs/install.md) for full Docker invocations (`init` / `dev` / `build`), the `--host` flag for containerized dev, Linuxbrew notes, and Nix profile install.
|
|
56
57
|
|
|
@@ -58,10 +59,10 @@ Then `sourcey init` to scaffold a new project, or `sourcey build` against an exi
|
|
|
58
59
|
|
|
59
60
|
For Go-only consumers without a JavaScript toolchain, `sourcey-godoc` ships as a separate native binary. It produces static Go docs sites or portable `godoc.json` snapshots.
|
|
60
61
|
|
|
61
|
-
| Path | Command
|
|
62
|
-
|
|
63
|
-
| Go | `go install github.com/sourcey/sourcey/go/sourcey-godoc/cmd/sourcey-godoc@latest`
|
|
64
|
-
| Homebrew | `brew install sourcey/tap/sourcey-godoc`
|
|
62
|
+
| Path | Command |
|
|
63
|
+
| -------- | ------------------------------------------------------------------------------------------------- |
|
|
64
|
+
| Go | `go install github.com/sourcey/sourcey/go/sourcey-godoc/cmd/sourcey-godoc@latest` |
|
|
65
|
+
| Homebrew | `brew install sourcey/tap/sourcey-godoc` |
|
|
65
66
|
| Scoop | `scoop bucket add sourcey https://github.com/sourcey/scoop-bucket && scoop install sourcey-godoc` |
|
|
66
67
|
|
|
67
68
|
## Quick start
|
|
@@ -82,101 +83,29 @@ sourcey dev
|
|
|
82
83
|
Create `sourcey.config.ts` in your project root:
|
|
83
84
|
|
|
84
85
|
```typescript
|
|
85
|
-
import { defineConfig } from "sourcey";
|
|
86
|
+
import { defineConfig, openapi } from "sourcey";
|
|
86
87
|
|
|
87
88
|
export default defineConfig({
|
|
88
89
|
name: "My API",
|
|
89
|
-
theme: {
|
|
90
|
-
preset: "default", // "default" | "minimal" | "api-first"
|
|
91
|
-
colors: {
|
|
92
|
-
primary: "#6366F1",
|
|
93
|
-
light: "#818CF8",
|
|
94
|
-
dark: "#4F46E5",
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
logo: "./logo.png",
|
|
98
90
|
navigation: {
|
|
99
|
-
tabs: [
|
|
100
|
-
{
|
|
101
|
-
tab: "Documentation",
|
|
102
|
-
groups: [
|
|
103
|
-
{
|
|
104
|
-
group: "Getting Started",
|
|
105
|
-
pages: ["introduction", "quickstart", "authentication"],
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
tab: "API Reference",
|
|
111
|
-
openapi: "./openapi.yaml",
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
tab: "MCP Server",
|
|
115
|
-
mcp: "./mcp.json",
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
},
|
|
119
|
-
navbar: {
|
|
120
|
-
links: [{ type: "github", href: "https://github.com/you/repo" }],
|
|
121
|
-
primary: { type: "button", label: "Dashboard", href: "https://app.example.com" },
|
|
122
|
-
},
|
|
123
|
-
footer: {
|
|
124
|
-
socials: { github: "https://github.com/you/repo" },
|
|
91
|
+
tabs: [{ tab: "API Reference", source: openapi("./openapi.yaml") }],
|
|
125
92
|
},
|
|
126
93
|
});
|
|
127
94
|
```
|
|
128
95
|
|
|
129
|
-
Each tab
|
|
96
|
+
Each tab has one `source`, usually created with `markdown()`, `mkdocs()`, `openapi()`, `mcp()`, `doxygen()`, or `godoc()`. Pages are referenced by slug (e.g. `"quickstart"` resolves to `quickstart.md`). See [docs/configuration.md](docs/configuration.md) for theme, navbar, footer, logo, and full tab options.
|
|
130
97
|
|
|
131
98
|
### Go documentation (godoc)
|
|
132
99
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
```typescript
|
|
136
|
-
{
|
|
137
|
-
tab: "Go API",
|
|
138
|
-
godoc: {
|
|
139
|
-
module: ".",
|
|
140
|
-
packages: ["./internal/core/...", "./cmd/..."],
|
|
141
|
-
// sourceBasePath: "go/my-module" // repo-relative prefix for source links when module is not at repo root
|
|
142
|
-
// mode: "auto" // default; live when Go is available, snapshot otherwise
|
|
143
|
-
// includeTests: true // examples from *_test.go (default)
|
|
144
|
-
// includeUnexported: false // hide unexported symbols (default)
|
|
145
|
-
},
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
The string shorthand is `godoc: "."` and expands to `{ module: ".", packages: ["./..."], mode: "auto", includeTests: true }`.
|
|
150
|
-
|
|
151
|
-
`editBasePath` remains the global base for Markdown "Edit this page" links.
|
|
152
|
-
Use `godoc.sourceBasePath` only when Go source files live under a different
|
|
153
|
-
repository prefix than the docs source.
|
|
100
|
+
Render Go package docs as a tab inside your Sourcey site, branded and styled with the rest of your documentation. Native toolchain extraction (`go list` + `go/parser` + `go/doc`) feeds the same renderer as your OpenAPI, MCP, and Markdown tabs, so signatures, examples from `*_test.go`, and source links sit alongside your guides instead of bouncing readers to pkg.go.dev.
|
|
154
101
|
|
|
155
|
-
Live
|
|
156
|
-
|
|
157
|
-
Snapshot mode reads a committed `godoc.json` and needs no Go on the build host. Generate it with:
|
|
158
|
-
|
|
159
|
-
```bash
|
|
160
|
-
sourcey godoc --module . --packages './...' --out docs/godoc.json
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
then point the tab at it:
|
|
102
|
+
**[Live: scafld's Go API reference →](https://0state.com/scafld/docs/go-api)**
|
|
164
103
|
|
|
165
104
|
```typescript
|
|
166
|
-
{
|
|
167
|
-
tab: "Go API",
|
|
168
|
-
godoc: { mode: "snapshot", snapshot: "./docs/godoc.json" },
|
|
169
|
-
}
|
|
105
|
+
{ tab: "Go API", source: godoc(".") }
|
|
170
106
|
```
|
|
171
107
|
|
|
172
|
-
The
|
|
173
|
-
docs generator without installing the full Sourcey npm package:
|
|
174
|
-
|
|
175
|
-
```bash
|
|
176
|
-
go install github.com/sourcey/sourcey/go/sourcey-godoc/cmd/sourcey-godoc@latest
|
|
177
|
-
sourcey-godoc generate --module . --packages './...' --out site
|
|
178
|
-
sourcey-godoc snapshot --module . --packages './...' --out docs/godoc.json
|
|
179
|
-
```
|
|
108
|
+
The shorthand expands to `{ module: ".", packages: ["./..."], mode: "auto", includeTests: true }`. Live mode uses the host Go toolchain; snapshot mode reads a committed `godoc.json` and needs no Go on the build host. See [docs/configuration.md](docs/configuration.md) for `packages`, `mode`, `goEnv`, `sourceBasePath`, and `includeUnexported`.
|
|
180
109
|
|
|
181
110
|
### Markdown components
|
|
182
111
|
|
|
@@ -226,12 +155,7 @@ sourcey godoc --out godoc.json Snapshot a Go module's docs to JSON
|
|
|
226
155
|
sourcey-godoc generate --out site Standalone Go CLI for static godoc sites
|
|
227
156
|
```
|
|
228
157
|
|
|
229
|
-
|
|
230
|
-
| --- | --- | --- |
|
|
231
|
-
| `build` | `--output, -o` | Output directory (default: `dist`) |
|
|
232
|
-
| `build` | `--embed, -e` | Embeddable output (no html/body wrapper) |
|
|
233
|
-
| `build` | `--quiet, -q` | Suppress output |
|
|
234
|
-
| `dev` | `--port, -p` | Dev server port (default: `4400`) |
|
|
158
|
+
Run `sourcey <command> --help` for flags.
|
|
235
159
|
|
|
236
160
|
## Development
|
|
237
161
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DoxygenSourceOptions, GodocSourceOptions, MarkdownSourceOptions, McpSourceOptions, MkDocsSourceOptions, OpenApiSourceOptions, SourceAdapter } from "./types.js";
|
|
2
|
+
import { assertLocalPath } from "./shared.js";
|
|
3
|
+
export declare function openapi(specOrOptions: string | OpenApiSourceOptions): SourceAdapter;
|
|
4
|
+
export declare function mcp(specOrOptions: string | McpSourceOptions): SourceAdapter;
|
|
5
|
+
export declare function markdown(options: MarkdownSourceOptions): SourceAdapter;
|
|
6
|
+
export declare function mkdocs(configOrOptions: string | MkDocsSourceOptions): SourceAdapter;
|
|
7
|
+
export declare function doxygen(options: DoxygenSourceOptions): SourceAdapter;
|
|
8
|
+
export declare function godoc(configOrPath: GodocSourceOptions): SourceAdapter;
|
|
9
|
+
export { assertLocalPath };
|
|
10
|
+
export type { DoxygenSourceOptions, GodocSourceOptions, MarkdownSourceOptions, McpSourceOptions, MkDocsSourceOptions, OpenApiSourceOptions, PageMarkdownOptions, ResolvedTabSource, SourceAdapter, SourceAdapterContext, } from "./types.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB,EAChB,mBAAmB,EACnB,oBAAoB,EACpB,aAAa,EACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAuC,MAAM,aAAa,CAAC;AAGnF,wBAAgB,OAAO,CAAC,aAAa,EAAE,MAAM,GAAG,oBAAoB,GAAG,aAAa,CAanF;AAED,wBAAgB,GAAG,CAAC,aAAa,EAAE,MAAM,GAAG,gBAAgB,GAAG,aAAa,CAa3E;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,qBAAqB,GAAG,aAAa,CAWtE;AAED,wBAAgB,MAAM,CAAC,eAAe,EAAE,MAAM,GAAG,mBAAmB,GAAG,aAAa,CAInF;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAsBpE;AAED,wBAAgB,KAAK,CAAC,YAAY,EAAE,kBAAkB,GAAG,aAAa,CA6BrE;AAMD,OAAO,EAAE,eAAe,EAAE,CAAC;AAC3B,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB,EAChB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,aAAa,EACb,oBAAoB,GACrB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { assertLocalPath, resolveMarkdownGroups, toWatchPaths } from "./shared.js";
|
|
3
|
+
import { mkdocs as mkdocsAdapter } from "./mkdocs.js";
|
|
4
|
+
export function openapi(specOrOptions) {
|
|
5
|
+
const spec = typeof specOrOptions === "string" ? specOrOptions : specOrOptions.spec;
|
|
6
|
+
return {
|
|
7
|
+
name: "openapi",
|
|
8
|
+
async resolve(ctx) {
|
|
9
|
+
if (ctx.isUrl(spec)) {
|
|
10
|
+
return { kind: "openapi", spec };
|
|
11
|
+
}
|
|
12
|
+
const absPath = ctx.resolvePath(spec);
|
|
13
|
+
await ctx.assertExists(absPath, `OpenAPI spec "${spec}" in tab "${ctx.tabName}"`);
|
|
14
|
+
return { kind: "openapi", spec: absPath, watchPaths: [absPath] };
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function mcp(specOrOptions) {
|
|
19
|
+
const spec = typeof specOrOptions === "string" ? specOrOptions : specOrOptions.spec;
|
|
20
|
+
return {
|
|
21
|
+
name: "mcp",
|
|
22
|
+
async resolve(ctx) {
|
|
23
|
+
if (ctx.isUrl(spec)) {
|
|
24
|
+
return { kind: "mcp", spec };
|
|
25
|
+
}
|
|
26
|
+
const absPath = ctx.resolvePath(spec);
|
|
27
|
+
await ctx.assertExists(absPath, `MCP spec "${spec}" in tab "${ctx.tabName}"`);
|
|
28
|
+
return { kind: "mcp", spec: absPath, watchPaths: [absPath] };
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function markdown(options) {
|
|
33
|
+
return {
|
|
34
|
+
name: "markdown",
|
|
35
|
+
async resolve(ctx) {
|
|
36
|
+
return {
|
|
37
|
+
kind: "markdown",
|
|
38
|
+
adapter: "markdown",
|
|
39
|
+
groups: await resolveMarkdownGroups(options.groups, ctx.tabName, ctx.configDir),
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function mkdocs(configOrOptions) {
|
|
45
|
+
const options = typeof configOrOptions === "string" ? { config: configOrOptions } : configOrOptions;
|
|
46
|
+
return mkdocsAdapter(options);
|
|
47
|
+
}
|
|
48
|
+
export function doxygen(options) {
|
|
49
|
+
return {
|
|
50
|
+
name: "doxygen",
|
|
51
|
+
async resolve(ctx) {
|
|
52
|
+
const absXml = ctx.resolvePath(options.xml);
|
|
53
|
+
await ctx.assertExists(absXml, `Doxygen XML directory "${options.xml}" in tab "${ctx.tabName}"`);
|
|
54
|
+
return {
|
|
55
|
+
kind: "doxygen",
|
|
56
|
+
config: {
|
|
57
|
+
xml: absXml,
|
|
58
|
+
language: options.language ?? "cpp",
|
|
59
|
+
groups: options.groups ?? false,
|
|
60
|
+
index: options.index === false ? "none" : (options.index ?? "auto"),
|
|
61
|
+
sourceUrl: options.sourceUrl,
|
|
62
|
+
},
|
|
63
|
+
watchPaths: [absXml],
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function godoc(configOrPath) {
|
|
69
|
+
return {
|
|
70
|
+
name: "godoc",
|
|
71
|
+
async resolve(ctx) {
|
|
72
|
+
const cfg = typeof configOrPath === "string" ? { module: configOrPath } : configOrPath;
|
|
73
|
+
const moduleAbs = resolve(ctx.configDir, cfg.module ?? ".");
|
|
74
|
+
await ctx.assertExists(moduleAbs, `Go module directory "${cfg.module ?? "."}" in tab "${ctx.tabName}"`);
|
|
75
|
+
const snapshotAbs = cfg.snapshot ? ctx.resolvePath(cfg.snapshot) : undefined;
|
|
76
|
+
return {
|
|
77
|
+
kind: "godoc",
|
|
78
|
+
config: {
|
|
79
|
+
module: moduleAbs,
|
|
80
|
+
packages: cfg.packages?.length ? cfg.packages : ["./..."],
|
|
81
|
+
snapshot: snapshotAbs,
|
|
82
|
+
mode: cfg.mode ?? "auto",
|
|
83
|
+
includeTests: cfg.includeTests ?? true,
|
|
84
|
+
includeUnexported: cfg.includeUnexported ?? false,
|
|
85
|
+
hideUndocumented: cfg.hideUndocumented ?? false,
|
|
86
|
+
exclude: cfg.exclude ?? [],
|
|
87
|
+
goEnv: cfg.goEnv,
|
|
88
|
+
sourceBasePath: trimSlashes(cfg.sourceBasePath ?? ""),
|
|
89
|
+
},
|
|
90
|
+
watchPaths: toWatchPaths([snapshotAbs]),
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function trimSlashes(value) {
|
|
96
|
+
return value.replace(/^\/+|\/+$/g, "");
|
|
97
|
+
}
|
|
98
|
+
export { assertLocalPath };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MarkdownPreprocessor } from "../core/markdown-loader.js";
|
|
2
|
+
import type { MkDocsSourceOptions, SourceAdapter } from "./types.js";
|
|
3
|
+
export declare function mkdocs(options: MkDocsSourceOptions): SourceAdapter;
|
|
4
|
+
export declare const preprocessMkDocsMarkdown: MarkdownPreprocessor;
|
|
5
|
+
//# sourceMappingURL=mkdocs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mkdocs.d.ts","sourceRoot":"","sources":["../../src/adapters/mkdocs.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAcrE,wBAAgB,MAAM,CAAC,OAAO,EAAE,mBAAmB,GAAG,aAAa,CAoBlE;AAiLD,eAAO,MAAM,wBAAwB,EAAE,oBAWtC,CAAC"}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { access, readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { load as parseYaml } from "js-yaml";
|
|
5
|
+
import { assertLocalPath, collectStaticAssets } from "./shared.js";
|
|
6
|
+
export function mkdocs(options) {
|
|
7
|
+
return {
|
|
8
|
+
name: "mkdocs",
|
|
9
|
+
async resolve(ctx) {
|
|
10
|
+
const configPath = assertLocalPath(ctx, options.config, "MkDocs config");
|
|
11
|
+
await ctx.assertExists(configPath, `MkDocs config "${options.config}" in tab "${ctx.tabName}"`);
|
|
12
|
+
const { docsDir, groups } = await resolveMkDocsGroups(configPath);
|
|
13
|
+
return {
|
|
14
|
+
kind: "markdown",
|
|
15
|
+
adapter: "mkdocs",
|
|
16
|
+
configPath,
|
|
17
|
+
groups,
|
|
18
|
+
assets: await collectStaticAssets(docsDir),
|
|
19
|
+
watchPaths: [configPath, docsDir],
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function resolveMkDocsGroups(configPath) {
|
|
25
|
+
const raw = await readFile(configPath, "utf-8");
|
|
26
|
+
const parsed = parseYaml(sanitizeMkDocsYaml(raw));
|
|
27
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
28
|
+
throw new Error(`MkDocs config must be a YAML object: ${configPath}`);
|
|
29
|
+
}
|
|
30
|
+
const configDir = dirname(configPath);
|
|
31
|
+
const docsDirName = typeof parsed.docs_dir === "string" && parsed.docs_dir.trim() ? parsed.docs_dir.trim() : "docs";
|
|
32
|
+
const docsDir = resolve(configDir, docsDirName);
|
|
33
|
+
const nav = parsed.nav ?? parsed.pages;
|
|
34
|
+
const groups = Array.isArray(nav)
|
|
35
|
+
? mkDocsGroupsFromNav(nav, docsDir)
|
|
36
|
+
: [{ label: "Pages", pages: await discoverMkDocsPages(docsDir) }];
|
|
37
|
+
const nonEmptyGroups = groups.filter((group) => group.pages.length > 0);
|
|
38
|
+
if (nonEmptyGroups.length === 0) {
|
|
39
|
+
throw new Error(`MkDocs config did not resolve any markdown pages: ${configPath}`);
|
|
40
|
+
}
|
|
41
|
+
for (const group of nonEmptyGroups) {
|
|
42
|
+
for (const page of group.pages) {
|
|
43
|
+
await access(page.file).catch(() => {
|
|
44
|
+
throw new Error(`MkDocs nav page "${relative(docsDir, page.file).replace(/\\/g, "/")}" not found`);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { docsDir, groups: nonEmptyGroups };
|
|
49
|
+
}
|
|
50
|
+
function sanitizeMkDocsYaml(raw) {
|
|
51
|
+
return raw.replace(/!!python\/name:[^\s\]]+/g, "").replace(/!ENV\b/g, "");
|
|
52
|
+
}
|
|
53
|
+
function mkDocsGroupsFromNav(nav, docsDir) {
|
|
54
|
+
const groups = [];
|
|
55
|
+
const loosePages = [];
|
|
56
|
+
const seenPaths = new Set();
|
|
57
|
+
for (const entry of nav) {
|
|
58
|
+
if (typeof entry === "string") {
|
|
59
|
+
const page = resolveMkDocsNavPage({ path: entry }, docsDir, seenPaths);
|
|
60
|
+
if (page)
|
|
61
|
+
loosePages.push(page);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!isPlainRecord(entry))
|
|
65
|
+
continue;
|
|
66
|
+
for (const [label, value] of Object.entries(entry)) {
|
|
67
|
+
if (typeof value === "string") {
|
|
68
|
+
const page = resolveMkDocsNavPage({ path: value, label }, docsDir, seenPaths);
|
|
69
|
+
if (page)
|
|
70
|
+
loosePages.push(page);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const pages = collectMkDocsNavPages(value, [])
|
|
74
|
+
.map((page) => resolveMkDocsNavPage(page, docsDir, seenPaths))
|
|
75
|
+
.filter((page) => Boolean(page));
|
|
76
|
+
if (pages.length > 0) {
|
|
77
|
+
groups.push({ label, pages });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return loosePages.length > 0 ? [{ label: "Pages", pages: loosePages }, ...groups] : groups;
|
|
82
|
+
}
|
|
83
|
+
function collectMkDocsNavPages(value, labelParts) {
|
|
84
|
+
if (typeof value === "string") {
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
path: value,
|
|
88
|
+
label: isMkDocsIndexPage(value) ? labelParts.join(" / ") || undefined : undefined,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return value.flatMap((entry) => collectMkDocsNavPages(entry, labelParts));
|
|
94
|
+
}
|
|
95
|
+
if (!isPlainRecord(value)) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
return Object.entries(value).flatMap(([label, nested]) => {
|
|
99
|
+
if (typeof nested === "string") {
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
path: nested,
|
|
103
|
+
label: [...labelParts, label].join(" / "),
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
return collectMkDocsNavPages(nested, [...labelParts, label]);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function resolveMkDocsNavPage(page, docsDir, seenPaths) {
|
|
111
|
+
const navPath = normalizeMkDocsPagePath(page.path);
|
|
112
|
+
if (!navPath || seenPaths.has(navPath)) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const file = resolveWithinRoot(docsDir, navPath);
|
|
116
|
+
if (!file) {
|
|
117
|
+
throw new Error(`MkDocs nav page "${page.path}" escapes docs_dir: ${docsDir}`);
|
|
118
|
+
}
|
|
119
|
+
seenPaths.add(navPath);
|
|
120
|
+
return {
|
|
121
|
+
slug: slugFromMkDocsPath(navPath),
|
|
122
|
+
file,
|
|
123
|
+
label: page.label,
|
|
124
|
+
sourceRoot: docsDir,
|
|
125
|
+
preprocess: [preprocessMkDocsMarkdown],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function normalizeMkDocsPagePath(value) {
|
|
129
|
+
const text = value.trim();
|
|
130
|
+
if (!text || isExternalNavTarget(text) || !/\.(?:md|mdx)$/i.test(text)) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return text.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
134
|
+
}
|
|
135
|
+
function isExternalNavTarget(value) {
|
|
136
|
+
return /^(?:https?:|mailto:|tel:|#)/i.test(value);
|
|
137
|
+
}
|
|
138
|
+
function slugFromMkDocsPath(value) {
|
|
139
|
+
return value.replace(/\.(?:md|mdx)$/i, "");
|
|
140
|
+
}
|
|
141
|
+
function isMkDocsIndexPage(value) {
|
|
142
|
+
return /(^|\/)index\.(?:md|mdx)$/i.test(value.trim());
|
|
143
|
+
}
|
|
144
|
+
async function discoverMkDocsPages(docsDir) {
|
|
145
|
+
const pages = [];
|
|
146
|
+
async function walk(dir) {
|
|
147
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
148
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
149
|
+
if (entry.name.startsWith("."))
|
|
150
|
+
continue;
|
|
151
|
+
const absolutePath = join(dir, entry.name);
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
await walk(absolutePath);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!entry.isFile() || !/\.(?:md|mdx)$/i.test(entry.name))
|
|
157
|
+
continue;
|
|
158
|
+
const relPath = relative(docsDir, absolutePath).replace(/\\/g, "/");
|
|
159
|
+
pages.push({
|
|
160
|
+
slug: slugFromMkDocsPath(relPath),
|
|
161
|
+
file: absolutePath,
|
|
162
|
+
sourceRoot: docsDir,
|
|
163
|
+
preprocess: [preprocessMkDocsMarkdown],
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
await walk(docsDir);
|
|
168
|
+
return pages;
|
|
169
|
+
}
|
|
170
|
+
export const preprocessMkDocsMarkdown = (body, context) => {
|
|
171
|
+
if (!context.sourceRoot)
|
|
172
|
+
return body;
|
|
173
|
+
return normalizeMkDocsAdmonitions(normalizeMkDocsTabs(escapeMkDocsCppTemplateAngles(stripMkDocsInlineLanguageMarkers(expandMkDocsSnippets(body, context.filePath, context.sourceRoot)))));
|
|
174
|
+
};
|
|
175
|
+
function expandMkDocsSnippets(body, filePath, sourceRoot) {
|
|
176
|
+
return body
|
|
177
|
+
.split("\n")
|
|
178
|
+
.map((line) => {
|
|
179
|
+
const match = line.match(/^(\s*)--8<--\s+"([^"]+)"\s*$/);
|
|
180
|
+
if (!match)
|
|
181
|
+
return line;
|
|
182
|
+
const [, indent, includePath] = match;
|
|
183
|
+
const included = readMkDocsSnippet(includePath, filePath, sourceRoot);
|
|
184
|
+
if (included === undefined)
|
|
185
|
+
return line;
|
|
186
|
+
return included
|
|
187
|
+
.replace(/\r\n?/g, "\n")
|
|
188
|
+
.split("\n")
|
|
189
|
+
.map((includedLine) => (includedLine ? `${indent}${includedLine}` : includedLine))
|
|
190
|
+
.join("\n");
|
|
191
|
+
})
|
|
192
|
+
.join("\n");
|
|
193
|
+
}
|
|
194
|
+
function readMkDocsSnippet(includePath, filePath, sourceRoot) {
|
|
195
|
+
const candidates = uniquePaths([
|
|
196
|
+
isAbsolute(includePath) ? includePath : resolve(sourceRoot, includePath),
|
|
197
|
+
isAbsolute(includePath) ? includePath : resolve(dirname(filePath), includePath),
|
|
198
|
+
]);
|
|
199
|
+
const safeCandidates = candidates.filter((candidate) => isWithinRoot(sourceRoot, candidate));
|
|
200
|
+
if (safeCandidates.length === 0) {
|
|
201
|
+
throw new Error(`MkDocs snippet "${includePath}" escapes docs_dir: ${sourceRoot}`);
|
|
202
|
+
}
|
|
203
|
+
for (const candidate of safeCandidates) {
|
|
204
|
+
try {
|
|
205
|
+
return readFileSync(candidate, "utf-8");
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (err.code !== "ENOENT")
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
function stripMkDocsInlineLanguageMarkers(body) {
|
|
215
|
+
return body
|
|
216
|
+
.replace(/`#!([A-Za-z0-9_+.-]+)\s+([^`]+)`/g, "`$2`")
|
|
217
|
+
.replace(/`#!([A-Za-z0-9_+.-]+)\s*`/g, "");
|
|
218
|
+
}
|
|
219
|
+
function escapeMkDocsCppTemplateAngles(body) {
|
|
220
|
+
return body.replace(/<([A-Za-z_][A-Za-z0-9_:]*)(\\?)>/g, (match, name, escaped) => {
|
|
221
|
+
if (!escaped && HTML_TAG_NAMES.has(name.toLowerCase()))
|
|
222
|
+
return match;
|
|
223
|
+
return `<${name}>`;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const HTML_TAG_NAMES = new Set([
|
|
227
|
+
"a",
|
|
228
|
+
"abbr",
|
|
229
|
+
"article",
|
|
230
|
+
"aside",
|
|
231
|
+
"b",
|
|
232
|
+
"blockquote",
|
|
233
|
+
"br",
|
|
234
|
+
"button",
|
|
235
|
+
"caption",
|
|
236
|
+
"code",
|
|
237
|
+
"dd",
|
|
238
|
+
"del",
|
|
239
|
+
"details",
|
|
240
|
+
"div",
|
|
241
|
+
"dl",
|
|
242
|
+
"dt",
|
|
243
|
+
"em",
|
|
244
|
+
"figcaption",
|
|
245
|
+
"figure",
|
|
246
|
+
"h1",
|
|
247
|
+
"h2",
|
|
248
|
+
"h3",
|
|
249
|
+
"h4",
|
|
250
|
+
"h5",
|
|
251
|
+
"h6",
|
|
252
|
+
"hr",
|
|
253
|
+
"i",
|
|
254
|
+
"iframe",
|
|
255
|
+
"img",
|
|
256
|
+
"input",
|
|
257
|
+
"ins",
|
|
258
|
+
"kbd",
|
|
259
|
+
"li",
|
|
260
|
+
"mark",
|
|
261
|
+
"ol",
|
|
262
|
+
"p",
|
|
263
|
+
"pre",
|
|
264
|
+
"s",
|
|
265
|
+
"section",
|
|
266
|
+
"small",
|
|
267
|
+
"span",
|
|
268
|
+
"strong",
|
|
269
|
+
"sub",
|
|
270
|
+
"summary",
|
|
271
|
+
"sup",
|
|
272
|
+
"table",
|
|
273
|
+
"tbody",
|
|
274
|
+
"td",
|
|
275
|
+
"th",
|
|
276
|
+
"thead",
|
|
277
|
+
"tr",
|
|
278
|
+
"u",
|
|
279
|
+
"ul",
|
|
280
|
+
]);
|
|
281
|
+
function normalizeMkDocsTabs(body) {
|
|
282
|
+
const lines = body.split("\n");
|
|
283
|
+
const out = [];
|
|
284
|
+
for (let i = 0; i < lines.length;) {
|
|
285
|
+
const tab = lines[i].match(/^(\s*)===\s+"([^"]+)"\s*$/);
|
|
286
|
+
if (!tab) {
|
|
287
|
+
out.push(lines[i]);
|
|
288
|
+
i += 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const [, indent, title] = tab;
|
|
292
|
+
out.push(`${indent}**${title}**`);
|
|
293
|
+
i += 1;
|
|
294
|
+
while (i < lines.length) {
|
|
295
|
+
if (lines[i].match(new RegExp(`^${escapeRegExp(indent)}===\\s+"[^"]+"\\s*$`))) {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
if (/^\s*$/.test(lines[i])) {
|
|
299
|
+
out.push(lines[i]);
|
|
300
|
+
i += 1;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (!lines[i].startsWith(`${indent} `)) {
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
out.push(indent + lines[i].slice(indent.length + 4));
|
|
307
|
+
i += 1;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return out.join("\n");
|
|
311
|
+
}
|
|
312
|
+
function normalizeMkDocsAdmonitions(body) {
|
|
313
|
+
const lines = body.split("\n");
|
|
314
|
+
const out = [];
|
|
315
|
+
for (let i = 0; i < lines.length;) {
|
|
316
|
+
const admonition = lines[i].match(/^(\s*)(!!!|\?\?\?)\s+([A-Za-z0-9_-]+)(?:\s+"([^"]+)")?\s*$/);
|
|
317
|
+
if (!admonition) {
|
|
318
|
+
out.push(lines[i]);
|
|
319
|
+
i += 1;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const [, indent, , type, title] = admonition;
|
|
323
|
+
out.push(`${indent}> **${formatMkDocsAdmonitionTitle(type, title)}**`);
|
|
324
|
+
i += 1;
|
|
325
|
+
const content = [];
|
|
326
|
+
while (i < lines.length) {
|
|
327
|
+
if (/^\s*$/.test(lines[i])) {
|
|
328
|
+
content.push("");
|
|
329
|
+
i += 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (!lines[i].startsWith(`${indent} `)) {
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
content.push(lines[i].slice(indent.length + 4));
|
|
336
|
+
i += 1;
|
|
337
|
+
}
|
|
338
|
+
for (const line of normalizeMkDocsAdmonitions(content.join("\n")).split("\n")) {
|
|
339
|
+
out.push(line ? `${indent}> ${line}` : `${indent}>`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return out.join("\n");
|
|
343
|
+
}
|
|
344
|
+
function formatMkDocsAdmonitionTitle(type, title) {
|
|
345
|
+
if (title?.trim())
|
|
346
|
+
return title.trim();
|
|
347
|
+
return type.slice(0, 1).toUpperCase() + type.slice(1).replace(/-/g, " ");
|
|
348
|
+
}
|
|
349
|
+
function escapeRegExp(value) {
|
|
350
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
351
|
+
}
|
|
352
|
+
function isPlainRecord(value) {
|
|
353
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
354
|
+
}
|
|
355
|
+
function resolveWithinRoot(root, path) {
|
|
356
|
+
const candidate = resolve(root, path);
|
|
357
|
+
return isWithinRoot(root, candidate) ? candidate : undefined;
|
|
358
|
+
}
|
|
359
|
+
function isWithinRoot(root, candidate) {
|
|
360
|
+
const rootAbs = resolve(root);
|
|
361
|
+
const candidateAbs = resolve(candidate);
|
|
362
|
+
const rel = relative(rootAbs, candidateAbs);
|
|
363
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
364
|
+
}
|
|
365
|
+
function uniquePaths(paths) {
|
|
366
|
+
return [...new Set(paths)];
|
|
367
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ResolvedGroup, ResolvedPage } from "../config.js";
|
|
2
|
+
import type { PageMarkdownOptions, ResolvedSourceAsset, SourceAdapterContext } from "./types.js";
|
|
3
|
+
export declare const STATIC_ASSET_EXTENSIONS: Set<string>;
|
|
4
|
+
export declare function resolveMarkdownGroups(groups: {
|
|
5
|
+
group: string;
|
|
6
|
+
pages: string[];
|
|
7
|
+
}[], tabName: string, configDir: string, markdown?: PageMarkdownOptions): Promise<ResolvedGroup[]>;
|
|
8
|
+
export declare function collectStaticAssets(sourceRoot: string): Promise<ResolvedSourceAsset[]>;
|
|
9
|
+
export declare function toWatchPaths(paths: Array<string | undefined>): string[];
|
|
10
|
+
export declare function assertLocalPath(ctx: SourceAdapterContext, source: string, label: string): string;
|
|
11
|
+
export declare function outputSlugForPage(page: ResolvedPage): string;
|
|
12
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/adapters/shared.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEhE,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEjG,eAAO,MAAM,uBAAuB,aAsBlC,CAAC;AAEH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,EAAE,EAC5C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,aAAa,EAAE,CAAC,CA0B1B;AAED,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAuB5F;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,EAAE,CAEvE;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKhG;AA0DD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAE5D"}
|