dogsbay 0.2.0-beta.39 → 0.2.0-beta.40
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/dist/commands/migrate-mkdocs.js +333 -32
- package/dist/commands/site-build.js +22 -0
- package/dist/index.js +3 -0
- package/dist/resolve-autodoc.js +218 -0
- package/package.json +11 -9
|
@@ -99,7 +99,7 @@ export async function migrateMkdocs(source, options) {
|
|
|
99
99
|
// snippets, macros, autodoc, variants — see @dogsbay/format-
|
|
100
100
|
// mkdocs's CLAUDE.md) runs identically to import-mkdocs; only
|
|
101
101
|
// the final write target differs.
|
|
102
|
-
const { pageCount, lossy } = await collectAndWriteContent(sourceDir, fullDocsDir, outputDir, config);
|
|
102
|
+
const { pageCount, lossy } = await collectAndWriteContent(sourceDir, fullDocsDir, outputDir, config, { inlineAutodoc: options.inlineAutodoc ?? false });
|
|
103
103
|
console.log(pc.green(`Wrote`) + ` ${pageCount} pages to ./content/ as Dogsbay-MD`);
|
|
104
104
|
// 3. Convert MkDocs nav: → ./content/nav.yml in canonical
|
|
105
105
|
// single-key-map shape (- Label: file.md / nested children).
|
|
@@ -120,6 +120,12 @@ export async function migrateMkdocs(source, options) {
|
|
|
120
120
|
if (assetCount > 0) {
|
|
121
121
|
console.log(pc.green(`Copied`) + ` ${assetCount} asset files to content/_assets/`);
|
|
122
122
|
}
|
|
123
|
+
// 4b. Collect extra_css from mkdocs.yml. Paths are relative to
|
|
124
|
+
// docs_dir (MkDocs convention). emitSiteScaffold below
|
|
125
|
+
// copies the files to astro/src/styles/custom/ AND appends
|
|
126
|
+
// `@import "./custom/<file>";` to global.css so they load on
|
|
127
|
+
// every page.
|
|
128
|
+
const extraCss = extractExtraCssRel(config, fullDocsDir);
|
|
123
129
|
// 5. Build dogsbay.config.yml. agent.{llmsTxt, mdMirror} default
|
|
124
130
|
// to true in config/defaults.ts — listing them explicitly
|
|
125
131
|
// makes the migrated config self-documenting. NO `output:`
|
|
@@ -159,7 +165,16 @@ export async function migrateMkdocs(source, options) {
|
|
|
159
165
|
llmsTxt: true,
|
|
160
166
|
mdMirror: true,
|
|
161
167
|
local: options.local,
|
|
168
|
+
// Extra CSS from the source mkdocs.yml. emitSiteScaffold
|
|
169
|
+
// copies the files into astro/src/styles/custom/ and
|
|
170
|
+
// appends imports to global.css so they apply site-wide.
|
|
171
|
+
extraCss,
|
|
172
|
+
sourceDir: fullDocsDir,
|
|
162
173
|
}, true);
|
|
174
|
+
if (extraCss.length > 0) {
|
|
175
|
+
console.log(pc.green(`Copied`) +
|
|
176
|
+
` ${extraCss.length} extra_css file(s) to astro/src/styles/custom/`);
|
|
177
|
+
}
|
|
163
178
|
console.log(pc.green(`Scaffolded`) + ` Astro project at ./astro/ (theme, components, package.json)`);
|
|
164
179
|
// 7. MIGRATION.md — humans-only summary of the conversion.
|
|
165
180
|
// Captures lossy items the parser flagged so the user can
|
|
@@ -186,7 +201,7 @@ export async function migrateMkdocs(source, options) {
|
|
|
186
201
|
console.log("Review MIGRATION.md for what survived and what didn't.");
|
|
187
202
|
}
|
|
188
203
|
}
|
|
189
|
-
async function collectAndWriteContent(sourceDir, fullDocsDir, outputDir, mkdocsConfig) {
|
|
204
|
+
async function collectAndWriteContent(sourceDir, fullDocsDir, outputDir, mkdocsConfig, opts) {
|
|
190
205
|
// Dynamic imports for the parser + serializer — same pattern
|
|
191
206
|
// used in import-mkdocs to avoid pulling these into the CLI's
|
|
192
207
|
// type-checking graph (they're workspace deps, not direct).
|
|
@@ -206,6 +221,11 @@ async function collectAndWriteContent(sourceDir, fullDocsDir, outputDir, mkdocsC
|
|
|
206
221
|
// mapping form — without that tolerance PyMdownX Blocks like
|
|
207
222
|
// `/// note` fall through to raw paragraphs in the output).
|
|
208
223
|
const detected = configureFromMkdocs(mkdocsConfig.markdown_extensions);
|
|
224
|
+
// Extract macros include_yaml + autodoc paths from mkdocs.yml so
|
|
225
|
+
// the parser expands them at migration time. Native Dogsbay-MD
|
|
226
|
+
// templating + includes are deferred — see
|
|
227
|
+
// plans/dogsbay-md-templating.md.
|
|
228
|
+
const macrosData = extractMacrosData(mkdocsConfig, sourceDir);
|
|
209
229
|
md.use(mkdocsPlugin, {
|
|
210
230
|
...detected,
|
|
211
231
|
snippets: {
|
|
@@ -220,21 +240,52 @@ async function collectAndWriteContent(sourceDir, fullDocsDir, outputDir, mkdocsC
|
|
|
220
240
|
// See packages/cli/skills/platform/migration-shape/SKILL.md
|
|
221
241
|
// → "Internal links — root-relative absolute slugs".
|
|
222
242
|
linkRewrite: { baseUrl: "" },
|
|
243
|
+
// fileInclude: `{* ../../docs_src/foo.py *}` in MkDocs sources
|
|
244
|
+
// resolves relative to the mkdocs project root (where
|
|
245
|
+
// mkdocs.yml lives). Setting docsDir = sourceDir makes
|
|
246
|
+
// resolve() walk paths from the project root.
|
|
247
|
+
fileInclude: { root: sourceDir, docsDir: sourceDir },
|
|
248
|
+
// variants: auto-generate `//// tab` blocks from sibling files
|
|
249
|
+
// (foo.md + foo_py310.md → tabs with "Python 3.10+" / current).
|
|
250
|
+
// Pointed at the actual docs_dir so the sibling walk reads the
|
|
251
|
+
// right tree.
|
|
252
|
+
variants: { docsDir: fullDocsDir },
|
|
253
|
+
// templates (mkdocs-macros plugin): Jinja2/nunjucks expressions
|
|
254
|
+
// like `{{ contributors }}` / `{% for %}`. The parser reads
|
|
255
|
+
// the YAMLs at parse time, expands the templates, and the
|
|
256
|
+
// output is plain Dogsbay-MD prose.
|
|
257
|
+
...(macrosData ? { templates: { dataFiles: macrosData } } : {}),
|
|
223
258
|
});
|
|
224
|
-
// Auto-detect mkdocstrings sourceRoot. Phase
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
259
|
+
// Auto-detect mkdocstrings sourceRoot. Phase 4 default = preserve:
|
|
260
|
+
// `:::` directives stay as paragraph text after parse, then a
|
|
261
|
+
// post-walk converts them to Dogsbay-MD `autodoc-ref` TreeNodes
|
|
262
|
+
// which serialize as `:::autodoc{ref="..."}`. site-build resolves
|
|
263
|
+
// them on every build via @dogsbay/autodoc-python — Python signature
|
|
264
|
+
// changes propagate without a re-migration. The `--inline-autodoc`
|
|
265
|
+
// flag opts into the older snapshot behaviour (autodoc resolved at
|
|
266
|
+
// migration time, written as `:::api-*` directives).
|
|
267
|
+
const autodocSourceRootAbs = detectAutodocSourceRoot(sourceDir, mkdocsConfig);
|
|
268
|
+
// For the directive form we store the sourceRoot RELATIVE to the
|
|
269
|
+
// migrated site's config dir (= outputDir). That way the directives
|
|
270
|
+
// survive moving the migrated project around the filesystem; site
|
|
271
|
+
// build resolves them against the config location.
|
|
272
|
+
const autodocSourceRootRel = autodocSourceRootAbs
|
|
273
|
+
? toRelativeFromConfig(autodocSourceRootAbs, outputDir)
|
|
274
|
+
: null;
|
|
230
275
|
const parseOpts = {
|
|
231
276
|
collapse: false,
|
|
232
277
|
youtubeEmbed: true,
|
|
233
278
|
diagrams: true,
|
|
234
279
|
};
|
|
235
|
-
if (
|
|
236
|
-
|
|
280
|
+
if (opts.inlineAutodoc && autodocSourceRootAbs) {
|
|
281
|
+
// Snapshot mode: resolve at migration time. The resolved api-*
|
|
282
|
+
// TreeNodes serialize to Dogsbay-MD `:::api-*` directives via
|
|
283
|
+
// format-dogsbay-md's Phase 4a cases.
|
|
284
|
+
parseOpts.autodoc = { sourceRoot: autodocSourceRootAbs };
|
|
237
285
|
}
|
|
286
|
+
// Preserve mode (default): no `autodoc` in parseOpts. The `:::`
|
|
287
|
+
// paragraphs stay as raw text; we walk them in post-process and
|
|
288
|
+
// convert to autodoc-ref TreeNodes.
|
|
238
289
|
const mdFiles = findMarkdownFiles(fullDocsDir);
|
|
239
290
|
const lossy = [];
|
|
240
291
|
let pageCount = 0;
|
|
@@ -248,6 +299,14 @@ async function collectAndWriteContent(sourceDir, fullDocsDir, outputDir, mkdocsC
|
|
|
248
299
|
...parseOpts,
|
|
249
300
|
env: { filePath: relPath },
|
|
250
301
|
});
|
|
302
|
+
// Preserve mode (default): convert any mkdocstrings `:::` text
|
|
303
|
+
// paragraphs in the parsed tree to Dogsbay-MD `autodoc-ref`
|
|
304
|
+
// TreeNodes. The serializer emits these as `:::autodoc{...}`
|
|
305
|
+
// directives; site-build resolves them on every build. Skipped
|
|
306
|
+
// when --inline-autodoc resolved them at parse time already.
|
|
307
|
+
if (!opts.inlineAutodoc && autodocSourceRootRel) {
|
|
308
|
+
rewriteMkdocstringsDirectivesToAutodocRefs(tree, autodocSourceRootRel);
|
|
309
|
+
}
|
|
251
310
|
// Rewrite image references to /_assets/<rel>/... canonical
|
|
252
311
|
// form. format-mkdocs's linkRewrite (configured above) already
|
|
253
312
|
// resolved relative paths to root-relative `/img/...` form;
|
|
@@ -264,16 +323,21 @@ async function collectAndWriteContent(sourceDir, fullDocsDir, outputDir, mkdocsC
|
|
|
264
323
|
// anyway — no behavioural difference.
|
|
265
324
|
const body = treeToDogsbayMd(tree);
|
|
266
325
|
writeFileSync(destPath, body.endsWith("\n") ? body : body + "\n");
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
326
|
+
// Note pages with mkdocstrings directives in MIGRATION.md.
|
|
327
|
+
// Default mode emits :::autodoc directives that site-build
|
|
328
|
+
// resolves on every build (live). --inline-autodoc snapshots
|
|
329
|
+
// at migration time (frozen).
|
|
271
330
|
if (/^:::\s+\S/m.test(source)) {
|
|
272
331
|
lossy.push({
|
|
273
332
|
file: relPath,
|
|
274
|
-
reason:
|
|
275
|
-
"
|
|
276
|
-
|
|
333
|
+
reason: opts.inlineAutodoc
|
|
334
|
+
? "Contains mkdocstrings ::: directives. --inline-autodoc " +
|
|
335
|
+
"resolved them to a snapshot at migration time — Python " +
|
|
336
|
+
"signature changes will NOT propagate until you re-migrate."
|
|
337
|
+
: "Contains mkdocstrings ::: directives. Converted to " +
|
|
338
|
+
":::autodoc{ref=\"...\"} directives. site-build resolves " +
|
|
339
|
+
"them on every build via @dogsbay/autodoc-python — Python " +
|
|
340
|
+
"signature changes propagate automatically.",
|
|
277
341
|
});
|
|
278
342
|
}
|
|
279
343
|
pageCount++;
|
|
@@ -288,27 +352,162 @@ async function collectAndWriteContent(sourceDir, fullDocsDir, outputDir, mkdocsC
|
|
|
288
352
|
}
|
|
289
353
|
return { pageCount, lossy };
|
|
290
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Auto-detect the Python source root for mkdocstrings autodoc.
|
|
357
|
+
*
|
|
358
|
+
* mkdocs.yml's `plugins:` can be either a list (canonical) or a
|
|
359
|
+
* mapping (FastAPI-style) — `mkdocstrings` is the key in both. We
|
|
360
|
+
* extract its config and walk:
|
|
361
|
+
*
|
|
362
|
+
* 1. mkdocstrings.handlers.python.paths (explicit declaration)
|
|
363
|
+
* 2. mkdocstrings.paths (alternate location seen in some sites)
|
|
364
|
+
* 3. Walk up from sourceDir looking for a directory that contains
|
|
365
|
+
* ONE OF: pyproject.toml, setup.py, or a Python package
|
|
366
|
+
* matching the autodoc identifiers (e.g. `fastapi/__init__.py`).
|
|
367
|
+
* Tops out 4 levels up; if nothing found, returns null and
|
|
368
|
+
* the migration warns + skips autodoc.
|
|
369
|
+
*
|
|
370
|
+
* Returns null when mkdocstrings isn't declared at all.
|
|
371
|
+
*/
|
|
291
372
|
function detectAutodocSourceRoot(sourceDir, mkdocsConfig) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
373
|
+
const mkdocstrings = extractMkdocstringsConfig(mkdocsConfig);
|
|
374
|
+
if (!mkdocstrings)
|
|
375
|
+
return null;
|
|
376
|
+
// Explicit `paths:` declaration.
|
|
377
|
+
const explicit = mkdocstrings.handlers?.python?.paths ?? mkdocstrings.paths;
|
|
378
|
+
if (typeof explicit === "string")
|
|
379
|
+
return resolve(sourceDir, explicit);
|
|
380
|
+
if (Array.isArray(explicit) && typeof explicit[0] === "string") {
|
|
381
|
+
return resolve(sourceDir, explicit[0]);
|
|
382
|
+
}
|
|
383
|
+
// Walk up looking for a Python project marker. FastAPI's layout
|
|
384
|
+
// puts mkdocs.yml at docs/en/mkdocs.yml and the package at
|
|
385
|
+
// ../../fastapi/, so we check up to 4 levels.
|
|
386
|
+
let dir = sourceDir;
|
|
387
|
+
for (let i = 0; i < 4; i++) {
|
|
388
|
+
if (existsSync(join(dir, "pyproject.toml")) ||
|
|
389
|
+
existsSync(join(dir, "setup.py"))) {
|
|
390
|
+
return dir;
|
|
391
|
+
}
|
|
392
|
+
const parent = dirname(dir);
|
|
393
|
+
if (parent === dir)
|
|
394
|
+
break;
|
|
395
|
+
dir = parent;
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Pull the `mkdocstrings:` block out of mkdocs.yml's plugins field,
|
|
401
|
+
* which can be either a list-of-entries or a mapping. Returns
|
|
402
|
+
* null if mkdocstrings isn't declared.
|
|
403
|
+
*/
|
|
404
|
+
function extractMkdocstringsConfig(mkdocsConfig) {
|
|
295
405
|
const plugins = mkdocsConfig.plugins;
|
|
296
|
-
if (
|
|
406
|
+
if (Array.isArray(plugins)) {
|
|
407
|
+
for (const entry of plugins) {
|
|
408
|
+
if (typeof entry === "string" && entry === "mkdocstrings")
|
|
409
|
+
return {};
|
|
410
|
+
if (entry && typeof entry === "object" && "mkdocstrings" in entry) {
|
|
411
|
+
const value = entry.mkdocstrings;
|
|
412
|
+
if (value && typeof value === "object") {
|
|
413
|
+
return value;
|
|
414
|
+
}
|
|
415
|
+
return {};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
if (plugins && typeof plugins === "object") {
|
|
421
|
+
// Mapping form (FastAPI uses this).
|
|
422
|
+
const map = plugins;
|
|
423
|
+
if (!("mkdocstrings" in map))
|
|
424
|
+
return null;
|
|
425
|
+
const value = map.mkdocstrings;
|
|
426
|
+
if (value && typeof value === "object")
|
|
427
|
+
return value;
|
|
428
|
+
return {};
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Extract macros plugin data file mappings from mkdocs.yml.
|
|
434
|
+
* Returns { varName: absolutePath } for each `include_yaml` entry,
|
|
435
|
+
* or null if the macros plugin isn't configured.
|
|
436
|
+
*
|
|
437
|
+
* Mirrors import-mkdocs's extractMacrosData. The `plugins:` block
|
|
438
|
+
* can be either a list (canonical) or a mapping (FastAPI-style);
|
|
439
|
+
* accept both. The include_yaml entries are themselves a list of
|
|
440
|
+
* single-key maps:
|
|
441
|
+
*
|
|
442
|
+
* plugins:
|
|
443
|
+
* macros:
|
|
444
|
+
* include_yaml:
|
|
445
|
+
* - github_sponsors: ../en/data/github_sponsors.yml
|
|
446
|
+
* - people: ../en/data/people.yml
|
|
447
|
+
*/
|
|
448
|
+
function extractMacrosData(mkdocsConfig, sourceDir) {
|
|
449
|
+
const plugins = mkdocsConfig.plugins;
|
|
450
|
+
if (!plugins)
|
|
451
|
+
return null;
|
|
452
|
+
let macrosConfig = null;
|
|
453
|
+
if (Array.isArray(plugins)) {
|
|
454
|
+
for (const p of plugins) {
|
|
455
|
+
if (typeof p === "object" && p !== null && "macros" in p) {
|
|
456
|
+
macrosConfig = p.macros;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
if (p === "macros") {
|
|
460
|
+
macrosConfig = {};
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
else if (typeof plugins === "object") {
|
|
466
|
+
if ("macros" in plugins) {
|
|
467
|
+
macrosConfig =
|
|
468
|
+
plugins.macros || {};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (!macrosConfig)
|
|
472
|
+
return null;
|
|
473
|
+
const includeYaml = macrosConfig.include_yaml;
|
|
474
|
+
if (!includeYaml || !Array.isArray(includeYaml))
|
|
297
475
|
return null;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
476
|
+
const result = {};
|
|
477
|
+
for (const entry of includeYaml) {
|
|
478
|
+
if (typeof entry === "string") {
|
|
479
|
+
const name = entry.replace(/\.[^.]+$/, "").split("/").pop() || entry;
|
|
480
|
+
result[name] = resolve(sourceDir, entry);
|
|
481
|
+
}
|
|
482
|
+
else if (typeof entry === "object" && entry !== null) {
|
|
483
|
+
const [name, path] = Object.entries(entry)[0];
|
|
484
|
+
if (typeof path === "string") {
|
|
485
|
+
result[name] = resolve(sourceDir, path);
|
|
306
486
|
}
|
|
307
487
|
}
|
|
308
488
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
489
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Read mkdocs.yml's `extra_css:` list — returns paths verbatim
|
|
493
|
+
* (relative to docs_dir, the MkDocs convention) for the entries
|
|
494
|
+
* whose files exist on disk. The list is passed through to
|
|
495
|
+
* emitSiteScaffold's `extraCss` + `sourceDir` options, which
|
|
496
|
+
* copies the files into astro/src/styles/custom/ and appends
|
|
497
|
+
* `@import` statements to the generated global.css.
|
|
498
|
+
*/
|
|
499
|
+
function extractExtraCssRel(mkdocsConfig, fullDocsDir) {
|
|
500
|
+
const raw = mkdocsConfig.extra_css;
|
|
501
|
+
if (!Array.isArray(raw))
|
|
502
|
+
return [];
|
|
503
|
+
const out = [];
|
|
504
|
+
for (const entry of raw) {
|
|
505
|
+
if (typeof entry !== "string")
|
|
506
|
+
continue;
|
|
507
|
+
if (existsSync(resolve(fullDocsDir, entry)))
|
|
508
|
+
out.push(entry);
|
|
509
|
+
}
|
|
510
|
+
return out;
|
|
312
511
|
}
|
|
313
512
|
function findMarkdownFiles(dir) {
|
|
314
513
|
const results = [];
|
|
@@ -390,6 +589,108 @@ export function rewriteAssetHref(src) {
|
|
|
390
589
|
const trimmed = src.replace(/^\/+/, "").replace(/^\.\/+/, "");
|
|
391
590
|
return `/_assets/${posix.normalize(trimmed)}`;
|
|
392
591
|
}
|
|
592
|
+
// ── mkdocstrings ::: → :::autodoc{ref="..."} (preserve mode) ──
|
|
593
|
+
/**
|
|
594
|
+
* After parseMkdocsMarkdown returns (without the autodoc option),
|
|
595
|
+
* `::: identifier` directives appear as paragraph nodes whose first
|
|
596
|
+
* prose child's html starts with `:::` text. Walk the tree, find
|
|
597
|
+
* them, and replace each with an autodoc-ref TreeNode that
|
|
598
|
+
* serializes to a Dogsbay-MD `:::autodoc{ref="..."}` directive.
|
|
599
|
+
*
|
|
600
|
+
* Site-build later resolves these via @dogsbay/autodoc-python on
|
|
601
|
+
* every build, so the rendered API ref stays in sync with the
|
|
602
|
+
* Python source.
|
|
603
|
+
*
|
|
604
|
+
* The mkdocstrings YAML option block (indented after the directive)
|
|
605
|
+
* is parsed and merged into the autodoc-ref props.
|
|
606
|
+
*/
|
|
607
|
+
function rewriteMkdocstringsDirectivesToAutodocRefs(nodes, sourceRootRel) {
|
|
608
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
609
|
+
const node = nodes[i];
|
|
610
|
+
if (node.type === "paragraph" && node.children) {
|
|
611
|
+
const proseChild = node.children.find((c) => c.type === "prose" && typeof c.html === "string" && c.html);
|
|
612
|
+
if (proseChild?.html) {
|
|
613
|
+
const directive = parseMkdocstringsDirective(proseChild.html.trim());
|
|
614
|
+
if (directive) {
|
|
615
|
+
nodes[i] = {
|
|
616
|
+
type: "autodoc-ref",
|
|
617
|
+
props: {
|
|
618
|
+
ref: directive.ref,
|
|
619
|
+
sourceRoot: sourceRootRel,
|
|
620
|
+
...directive.options,
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (node.children) {
|
|
628
|
+
rewriteMkdocstringsDirectivesToAutodocRefs(node.children, sourceRootRel);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const MKDOCSTRINGS_FIRST_LINE_RE = /^:::\s+(\S+)\s*$/;
|
|
633
|
+
/**
|
|
634
|
+
* Parse the mkdocstrings directive text (`::: identifier` +
|
|
635
|
+
* optional indented YAML body). Returns null if the text doesn't
|
|
636
|
+
* look like a directive.
|
|
637
|
+
*
|
|
638
|
+
* ::: fastapi.FastAPI
|
|
639
|
+
* options:
|
|
640
|
+
* show_source: true
|
|
641
|
+
* members:
|
|
642
|
+
* - get
|
|
643
|
+
* - post
|
|
644
|
+
*
|
|
645
|
+
* Becomes:
|
|
646
|
+
*
|
|
647
|
+
* { ref: "fastapi.FastAPI",
|
|
648
|
+
* options: { showSource: true, members: ["get", "post"] } }
|
|
649
|
+
*
|
|
650
|
+
* The handler-options snake_case → camelCase mapping mirrors the
|
|
651
|
+
* one in @dogsbay/format-mkdocs's loader so the autodoc engine
|
|
652
|
+
* receives the same keys regardless of mode (preserve vs inline).
|
|
653
|
+
*/
|
|
654
|
+
function parseMkdocstringsDirective(text) {
|
|
655
|
+
const firstLine = text.split("\n")[0].trim();
|
|
656
|
+
const match = MKDOCSTRINGS_FIRST_LINE_RE.exec(firstLine);
|
|
657
|
+
if (!match)
|
|
658
|
+
return null;
|
|
659
|
+
const ref = match[1];
|
|
660
|
+
const remainder = text.split("\n").slice(1).join("\n").trim();
|
|
661
|
+
const options = {};
|
|
662
|
+
if (remainder) {
|
|
663
|
+
try {
|
|
664
|
+
const parsed = YAML.parse(remainder);
|
|
665
|
+
if (parsed && typeof parsed === "object" && parsed.options) {
|
|
666
|
+
for (const [snake, value] of Object.entries(parsed.options)) {
|
|
667
|
+
options[snakeToCamel(snake)] = value;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
// Malformed YAML — drop the options, keep the directive
|
|
673
|
+
// pointing at the bare ref. Better than failing the whole
|
|
674
|
+
// page migration.
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return { ref, options };
|
|
678
|
+
}
|
|
679
|
+
function snakeToCamel(s) {
|
|
680
|
+
return s.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Compute the sourceRoot to bake into `:::autodoc{sourceRoot="..."}`
|
|
684
|
+
* directives. The migrated site lives at `outputDir`; we want a
|
|
685
|
+
* path relative to that so the directives travel with the site if
|
|
686
|
+
* it's moved.
|
|
687
|
+
*/
|
|
688
|
+
function toRelativeFromConfig(absSourcePath, outputDir) {
|
|
689
|
+
const rel = relative(outputDir, absSourcePath);
|
|
690
|
+
// Ensure forward slashes in the emitted string regardless of
|
|
691
|
+
// platform — the directives are read by site-build cross-platform.
|
|
692
|
+
return rel.split(/[\\/]/).join("/") || ".";
|
|
693
|
+
}
|
|
393
694
|
function copyAssets(srcDocsDir, destPublicDir) {
|
|
394
695
|
const exts = new Set([
|
|
395
696
|
".png",
|
|
@@ -165,6 +165,28 @@ export async function siteBuild(cwd, options) {
|
|
|
165
165
|
if (resolvedPlugins.some((p) => p.plugin.transformNav)) {
|
|
166
166
|
nav = await runTransformNav(resolvedPlugins, baseCtx, nav);
|
|
167
167
|
}
|
|
168
|
+
// 7c.5. Resolve :::autodoc directives — Phase 4 of
|
|
169
|
+
// plans/mkdocs-import-architecture.md. Walk every page tree for
|
|
170
|
+
// `autodoc-ref` placeholders left by format-dogsbay-md's parser,
|
|
171
|
+
// call @dogsbay/autodoc-python (resolveIdentifier + symbolToTreeNode),
|
|
172
|
+
// and splice the rendered api-* tree in place. Failures log a
|
|
173
|
+
// warning and leave the placeholder intact (its fallback body
|
|
174
|
+
// renders). Done BEFORE strict ref validation (7d) so structural
|
|
175
|
+
// checks see the resolved tree, not the placeholder.
|
|
176
|
+
const { resolveAutodocRefs } = await import("../resolve-autodoc.js");
|
|
177
|
+
const autodocResult = await resolveAutodocRefs(pages, {
|
|
178
|
+
configDir: dirname(configPath),
|
|
179
|
+
});
|
|
180
|
+
if (autodocResult.resolved > 0 || autodocResult.failed > 0) {
|
|
181
|
+
const parts = [];
|
|
182
|
+
if (autodocResult.resolved > 0) {
|
|
183
|
+
parts.push(`${autodocResult.resolved} resolved`);
|
|
184
|
+
}
|
|
185
|
+
if (autodocResult.failed > 0) {
|
|
186
|
+
parts.push(`${autodocResult.failed} failed`);
|
|
187
|
+
}
|
|
188
|
+
console.log(` Autodoc: ${parts.join(", ")}`);
|
|
189
|
+
}
|
|
168
190
|
console.log(` Imported ${pages.length} pages from ${importer.name}` +
|
|
169
191
|
(draftCount > 0 && !options.includeDrafts
|
|
170
192
|
? ` (${draftCount} draft${draftCount === 1 ? "" : "s"} excluded)`
|
package/dist/index.js
CHANGED
|
@@ -184,6 +184,9 @@ program
|
|
|
184
184
|
.argument("<source>", "Path to MkDocs project (containing mkdocs.yml)")
|
|
185
185
|
.option("-o, --output <dir>", "Output directory (default: {source}-dogsbay)")
|
|
186
186
|
.option("--force", "Overwrite an existing Dogsbay site at the output dir")
|
|
187
|
+
.option("--inline-autodoc", "Resolve mkdocstrings ::: directives at migration time (snapshot). " +
|
|
188
|
+
"Default is :::autodoc directives that site-build resolves on " +
|
|
189
|
+
"every build (live re-rendering).")
|
|
187
190
|
.option("--local", "Use file: references to local monorepo packages (for development)")
|
|
188
191
|
.action((source, options) => migrateMkdocs(source, options));
|
|
189
192
|
program
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time resolver for the `:::autodoc` directive.
|
|
3
|
+
*
|
|
4
|
+
* Phase 4 of plans/mkdocs-import-architecture.md.
|
|
5
|
+
*
|
|
6
|
+
* format-dogsbay-md parses `:::autodoc{ref="..."}` blocks into
|
|
7
|
+
* `autodoc-ref` TreeNode placeholders. site-build calls
|
|
8
|
+
* `resolveAutodocRefs` between the importer's content load and
|
|
9
|
+
* format-astro's emission to substitute the placeholders with
|
|
10
|
+
* rendered api-* TreeNodes (api-class, api-member, api-doc, etc.).
|
|
11
|
+
*
|
|
12
|
+
* Resolution flow per ref:
|
|
13
|
+
* 1. Read `ref` + `sourceRoot` (+ render options) from props.
|
|
14
|
+
* 2. Call @dogsbay/autodoc-python.resolveIdentifier → SymbolInfo.
|
|
15
|
+
* 3. Call symbolToTreeNode → TreeNode.
|
|
16
|
+
* 4. Replace the autodoc-ref node in the page tree.
|
|
17
|
+
*
|
|
18
|
+
* Failures (missing source, unresolvable identifier, parse error)
|
|
19
|
+
* leave the autodoc-ref node intact and log a warning. The
|
|
20
|
+
* directive's fallback body (if any) renders in its place.
|
|
21
|
+
*/
|
|
22
|
+
import { resolve as resolvePath } from "node:path";
|
|
23
|
+
import MarkdownIt from "markdown-it";
|
|
24
|
+
import pc from "picocolors";
|
|
25
|
+
import { resolveIdentifier, symbolToTreeNode, } from "@dogsbay/autodoc-python";
|
|
26
|
+
/**
|
|
27
|
+
* Default render options for autodoc directives. Mirrors
|
|
28
|
+
* mkdocstrings' Python handler defaults — these are the same
|
|
29
|
+
* values format-mkdocs/src/loader.ts baked in during the legacy
|
|
30
|
+
* import flow, kept here so migrate-mkdocs + site-build produce
|
|
31
|
+
* output that matches the legacy import path by default. A
|
|
32
|
+
* directive's own props override any default it doesn't like
|
|
33
|
+
* (e.g. `showSource: false` to skip the collapsible source).
|
|
34
|
+
*/
|
|
35
|
+
const AUTODOC_RENDER_DEFAULTS = {
|
|
36
|
+
showRootHeading: false,
|
|
37
|
+
showRootFullPath: true,
|
|
38
|
+
showIfNoDocstring: false,
|
|
39
|
+
showBases: true,
|
|
40
|
+
showSource: true,
|
|
41
|
+
showSignature: true,
|
|
42
|
+
mergeInitIntoClass: false,
|
|
43
|
+
separateSignature: false,
|
|
44
|
+
unwrapAnnotated: false,
|
|
45
|
+
groupByCategory: true,
|
|
46
|
+
inheritedMembers: false,
|
|
47
|
+
filters: ["!^_[^_]"],
|
|
48
|
+
membersOrder: "alphabetical",
|
|
49
|
+
headingLevel: 2,
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Lightweight slugifier matching format-mkdocs's behaviour so the
|
|
53
|
+
* heading slugs we generate here line up with what hrefs against
|
|
54
|
+
* these symbols expect.
|
|
55
|
+
*/
|
|
56
|
+
function slugify(text) {
|
|
57
|
+
return text
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/[^\w\s-]/g, "")
|
|
60
|
+
.replace(/\s+/g, "-")
|
|
61
|
+
.replace(/-+/g, "-")
|
|
62
|
+
.replace(/^-|-$/g, "");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Walk every page's TreeNode tree and substitute `autodoc-ref`
|
|
66
|
+
* placeholders with rendered api-* trees.
|
|
67
|
+
*
|
|
68
|
+
* Pages are mutated in place — caller owns the array. Returns a
|
|
69
|
+
* tally of resolved / failed refs for the build log.
|
|
70
|
+
*/
|
|
71
|
+
export async function resolveAutodocRefs(pages, options) {
|
|
72
|
+
let resolved = 0;
|
|
73
|
+
let failed = 0;
|
|
74
|
+
const md = new MarkdownIt({ html: true, linkify: true });
|
|
75
|
+
for (const page of pages) {
|
|
76
|
+
await walkAndResolve(page.tree, {
|
|
77
|
+
configDir: options.configDir,
|
|
78
|
+
md,
|
|
79
|
+
onResolved: () => {
|
|
80
|
+
resolved++;
|
|
81
|
+
},
|
|
82
|
+
onFailed: (ref, reason) => {
|
|
83
|
+
failed++;
|
|
84
|
+
console.warn(pc.yellow(` Warning: autodoc ref "${ref}" failed to resolve — ${reason}. ` +
|
|
85
|
+
`Page: ${page.slug}`));
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
// After substitution, walk the resolved tree and append heading
|
|
89
|
+
// entries for every api-class / api-member. Without this, the
|
|
90
|
+
// TOC component only sees the page's prose H1 — the rendered
|
|
91
|
+
// API ref pages have no method index in the sidebar TOC.
|
|
92
|
+
//
|
|
93
|
+
// Mirrors format-mkdocs/src/loader.ts → extractHeadings.
|
|
94
|
+
if (page.tree.some(treeHasApiHeading)) {
|
|
95
|
+
appendApiHeadings(page);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { resolved, failed };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* True if the subtree carries at least one api-class / api-member —
|
|
102
|
+
* cheap pre-check before we walk to collect headings.
|
|
103
|
+
*/
|
|
104
|
+
function treeHasApiHeading(node) {
|
|
105
|
+
if (node.type === "api-class" || node.type === "api-member")
|
|
106
|
+
return true;
|
|
107
|
+
if (node.children) {
|
|
108
|
+
for (const child of node.children) {
|
|
109
|
+
if (treeHasApiHeading(child))
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Walk the page's TreeNode tree for api-class / api-member nodes
|
|
117
|
+
* and append a Heading entry for each. Depth follows format-
|
|
118
|
+
* mkdocs's convention: api-class is depth 2 (top-level symbol),
|
|
119
|
+
* api-member is depth 3 (method / attribute under a class). The
|
|
120
|
+
* `kind` prop is preserved on the heading so the TOC component
|
|
121
|
+
* can render type-colored badges (class blue, method green, etc.).
|
|
122
|
+
*
|
|
123
|
+
* Slug allocation respects existing entries in page.headings to
|
|
124
|
+
* avoid collisions with handwritten prose headings.
|
|
125
|
+
*/
|
|
126
|
+
function appendApiHeadings(page) {
|
|
127
|
+
const seen = new Map();
|
|
128
|
+
for (const h of page.headings ?? []) {
|
|
129
|
+
seen.set(h.slug, (seen.get(h.slug) ?? 0) + 1);
|
|
130
|
+
}
|
|
131
|
+
const headings = [];
|
|
132
|
+
function walk(nodes) {
|
|
133
|
+
for (const node of nodes) {
|
|
134
|
+
if (node.type === "api-class" || node.type === "api-member") {
|
|
135
|
+
const name = node.props?.name || "";
|
|
136
|
+
const kind = node.props?.kind || "";
|
|
137
|
+
const depth = node.type === "api-class" ? 2 : 3;
|
|
138
|
+
const baseSlug = slugify(name);
|
|
139
|
+
const count = seen.get(baseSlug) ?? 0;
|
|
140
|
+
const slug = count === 0 ? baseSlug : `${baseSlug}-${count}`;
|
|
141
|
+
seen.set(baseSlug, count + 1);
|
|
142
|
+
headings.push({ depth, slug, text: name, kind });
|
|
143
|
+
// Persist the assigned slug on the node so format-astro's
|
|
144
|
+
// emitter can render the same id on the corresponding
|
|
145
|
+
// anchor — TOC links and on-page ids stay in sync.
|
|
146
|
+
if (!node.props)
|
|
147
|
+
node.props = {};
|
|
148
|
+
node.props.slug = slug;
|
|
149
|
+
}
|
|
150
|
+
if (node.children)
|
|
151
|
+
walk(node.children);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
walk(page.tree);
|
|
155
|
+
if (headings.length > 0) {
|
|
156
|
+
page.headings = [...(page.headings ?? []), ...headings];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function walkAndResolve(nodes, ctx) {
|
|
160
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
161
|
+
const node = nodes[i];
|
|
162
|
+
if (node.type === "autodoc-ref") {
|
|
163
|
+
const replacement = await resolveOne(node, ctx);
|
|
164
|
+
if (replacement) {
|
|
165
|
+
nodes[i] = replacement;
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (node.children) {
|
|
170
|
+
await walkAndResolve(node.children, ctx);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function resolveOne(node, ctx) {
|
|
175
|
+
const props = (node.props ?? {});
|
|
176
|
+
const ref = typeof props.ref === "string" ? props.ref : "";
|
|
177
|
+
const sourceRootRaw = typeof props.sourceRoot === "string" ? props.sourceRoot : "";
|
|
178
|
+
if (!ref) {
|
|
179
|
+
ctx.onFailed("(empty)", "no `ref` prop on :::autodoc directive");
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (!sourceRootRaw) {
|
|
183
|
+
ctx.onFailed(ref, "no `sourceRoot` prop on :::autodoc directive — needed to locate Python source");
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
// sourceRoot may be a path relative to dogsbay.config.yml.
|
|
187
|
+
// Resolve against configDir so the resolver doesn't depend on
|
|
188
|
+
// process.cwd().
|
|
189
|
+
const sourceRoot = resolvePath(ctx.configDir, sourceRootRaw);
|
|
190
|
+
// Pull rendering options from props, layered on top of the
|
|
191
|
+
// mkdocstrings-compatible defaults. tree-builder.ts uses
|
|
192
|
+
// truthy-checks for several of these (e.g. `if (options.showSource
|
|
193
|
+
// && symbol.sourceText) { ... }`) — without the defaults the
|
|
194
|
+
// collapsible source-code block, attribute signatures, and class
|
|
195
|
+
// bases all silently drop. The defaults mirror format-mkdocs/
|
|
196
|
+
// src/loader.ts → processAutodocDirectives so the migrated
|
|
197
|
+
// output matches the legacy import-mkdocs output by default.
|
|
198
|
+
const renderOptions = {
|
|
199
|
+
...AUTODOC_RENDER_DEFAULTS,
|
|
200
|
+
...props,
|
|
201
|
+
};
|
|
202
|
+
delete renderOptions.ref;
|
|
203
|
+
delete renderOptions.sourceRoot;
|
|
204
|
+
try {
|
|
205
|
+
const symbol = await resolveIdentifier(ref, sourceRoot);
|
|
206
|
+
if (!symbol) {
|
|
207
|
+
ctx.onFailed(ref, `identifier not found under ${sourceRoot}`);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const tree = symbolToTreeNode(symbol, renderOptions, ctx.md, ref, sourceRoot);
|
|
211
|
+
ctx.onResolved();
|
|
212
|
+
return tree;
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
ctx.onFailed(ref, err.message);
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dogsbay",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.40",
|
|
4
4
|
"description": "CLI for Dogsbay — scaffold, build, and serve documentation sites with markdown / MkDocs / Obsidian / OpenAPI sources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,16 +32,18 @@
|
|
|
32
32
|
"picocolors": "^1.1.0",
|
|
33
33
|
"prompts": "^2.4.2",
|
|
34
34
|
"yaml": "^2.8.3",
|
|
35
|
-
"@dogsbay/
|
|
36
|
-
"@dogsbay/format-
|
|
37
|
-
"@dogsbay/format-astro": "0.2.0-beta.
|
|
38
|
-
"@dogsbay/format-
|
|
39
|
-
"@dogsbay/format-starlight": "0.2.0-beta.
|
|
40
|
-
"@dogsbay/format-
|
|
41
|
-
"@dogsbay/format-
|
|
42
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
35
|
+
"@dogsbay/autodoc-python": "0.2.0-beta.38",
|
|
36
|
+
"@dogsbay/format-mkdocs": "0.2.0-beta.40",
|
|
37
|
+
"@dogsbay/format-astro": "0.2.0-beta.40",
|
|
38
|
+
"@dogsbay/format-obsidian": "0.2.0-beta.40",
|
|
39
|
+
"@dogsbay/format-starlight": "0.2.0-beta.40",
|
|
40
|
+
"@dogsbay/format-mdx": "0.2.0-beta.40",
|
|
41
|
+
"@dogsbay/format-dogsbay-md": "0.2.0-beta.40",
|
|
42
|
+
"@dogsbay/types": "0.2.0-beta.40",
|
|
43
|
+
"@dogsbay/format-openapi": "0.2.0-beta.40"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
46
|
+
"@types/markdown-it": "^14.1.0",
|
|
45
47
|
"@types/node": "^22.0.0",
|
|
46
48
|
"@types/prompts": "^2.4.9",
|
|
47
49
|
"typescript": "^5.9.0",
|