dogsbay 0.2.0-beta.91 → 0.2.0-beta.93
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/add.js +1 -1
- package/dist/commands/migrate-asciidoc.js +62 -10
- package/dist/commands/migrate-fluidtopics-rest.js +149 -0
- package/dist/commands/migrate-fluidtopics.js +158 -0
- package/dist/config/load.js +10 -0
- package/dist/config/to-astro-options.js +1 -0
- package/dist/index.js +53 -4
- package/dist/utils/remote-includes.js +88 -0
- package/package.json +17 -15
package/dist/commands/add.js
CHANGED
|
@@ -22,7 +22,7 @@ function getSourceDir() {
|
|
|
22
22
|
return monorepo;
|
|
23
23
|
throw new Error("Could not find component source files.");
|
|
24
24
|
}
|
|
25
|
-
function collectDependencies(names) {
|
|
25
|
+
export function collectDependencies(names) {
|
|
26
26
|
const visited = new Set();
|
|
27
27
|
const components = [];
|
|
28
28
|
const npmDeps = [];
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* and runs `dogsbay site build`; the AsciiDoc source can be
|
|
9
9
|
* deleted.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* Corpus discovery is delegated to the `@dogsbay/format-asciidoc`
|
|
12
|
+
* CorpusLoader registry (see `asciidocPlugin.import` below), so the
|
|
13
|
+
* topology-aware loaders are live: detection auto-picks AsciiBinder
|
|
14
|
+
* (`_topic_maps/_topic_map.yml`), AAP (`master.adoc`), Antora component
|
|
15
|
+
* (`antora.yml`), or the plain `.adoc` walk, and topology files never
|
|
16
|
+
* accidentally become pages. Override with `--loader`.
|
|
17
|
+
* See plans/asciidoc-corpus-loaders.md.
|
|
16
18
|
*
|
|
17
19
|
* Output layout — same convention as `migrate-mkdocs`:
|
|
18
20
|
*
|
|
@@ -29,10 +31,11 @@ import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, r
|
|
|
29
31
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
30
32
|
import pc from "picocolors";
|
|
31
33
|
import { emitSiteScaffold } from "@dogsbay/format-astro";
|
|
32
|
-
import { plugin as asciidocPlugin } from "@dogsbay/format-asciidoc/cli";
|
|
34
|
+
import { plugin as asciidocPlugin, pickLoader } from "@dogsbay/format-asciidoc/cli";
|
|
33
35
|
import { asciidocToMarkdown } from "@dogsbay/adoc2md-modular";
|
|
34
36
|
import { treeToDogsbayMd } from "@dogsbay/format-dogsbay-md";
|
|
35
37
|
import { ensureGitignore } from "../utils/gitignore.js";
|
|
38
|
+
import { inlineRemoteIncludes, hasRemoteIncludes } from "../utils/remote-includes.js";
|
|
36
39
|
function parseAttributePairs(pairs) {
|
|
37
40
|
const out = {};
|
|
38
41
|
for (const raw of pairs ?? []) {
|
|
@@ -211,6 +214,9 @@ function convertFragmentAdocs(sourceRoot, outputContentDir, scopedRootDirs, page
|
|
|
211
214
|
let converted = 0;
|
|
212
215
|
let failed = 0;
|
|
213
216
|
let skipped = 0;
|
|
217
|
+
// Fragment .md files that carry a `{% include "https://…" %}` directive —
|
|
218
|
+
// resolved by an async fetch pass after the (sync) walk completes. (#007)
|
|
219
|
+
const remoteIncludePaths = [];
|
|
214
220
|
function walk(srcDir, dstDir, depth) {
|
|
215
221
|
let entries;
|
|
216
222
|
try {
|
|
@@ -302,6 +308,8 @@ function convertFragmentAdocs(sourceRoot, outputContentDir, scopedRootDirs, page
|
|
|
302
308
|
// pursue option A through a non-frontmatter signal that
|
|
303
309
|
// can't collide with Jinja whitespace control.
|
|
304
310
|
writeFileSync(mdPath, md, "utf-8");
|
|
311
|
+
if (hasRemoteIncludes(md))
|
|
312
|
+
remoteIncludePaths.push(mdPath);
|
|
305
313
|
converted += 1;
|
|
306
314
|
}
|
|
307
315
|
catch (err) {
|
|
@@ -312,7 +320,7 @@ function convertFragmentAdocs(sourceRoot, outputContentDir, scopedRootDirs, page
|
|
|
312
320
|
}
|
|
313
321
|
}
|
|
314
322
|
walk(sourceRoot, outputContentDir, 0);
|
|
315
|
-
return { converted, failed, skipped };
|
|
323
|
+
return { converted, failed, skipped, remoteIncludePaths };
|
|
316
324
|
}
|
|
317
325
|
/**
|
|
318
326
|
* Serialize a NavItem[] (from a CorpusLoader) to nav.yml syntax.
|
|
@@ -400,6 +408,21 @@ function buildDogsbayConfig(opts) {
|
|
|
400
408
|
return lines.join("\n");
|
|
401
409
|
}
|
|
402
410
|
function buildMigrationNotes(opts) {
|
|
411
|
+
// Report the loader that actually ran rather than assuming a plain
|
|
412
|
+
// walk. The non-plain loaders read a topology source for the nav and
|
|
413
|
+
// route only topology-declared pages; `modules/` / `snippets/` /
|
|
414
|
+
// `_attributes/` are pulled in as include targets, not pages.
|
|
415
|
+
const loaderDesc = {
|
|
416
|
+
asciibinder: "AsciiBinder topology (`_topic_maps/_topic_map.yml`)",
|
|
417
|
+
"master-adoc": "AAP `master.adoc` topology",
|
|
418
|
+
antora: "Antora component (`antora.yml`)",
|
|
419
|
+
plain: "plain AsciiDoc — flat `.adoc` walk",
|
|
420
|
+
};
|
|
421
|
+
const corpusDesc = loaderDesc[opts.loader] ?? `\`${opts.loader}\` loader`;
|
|
422
|
+
const distroNote = opts.distro ? `, distro \`${opts.distro}\`` : "";
|
|
423
|
+
const topologyDidnt = opts.loader === "plain"
|
|
424
|
+
? "- **Topology**: the plain loader walked every `.adoc` flat and derived the nav from the directory layout. If your source is actually AsciiBinder (`_topic_maps/_topic_map.yml`), Antora (`antora.yml`), or AAP (`master.adoc`), re-run with `--loader <name>` (or rely on auto-detection) to use the curated topology and per-page distro filtering instead.\n"
|
|
425
|
+
: "";
|
|
403
426
|
const hasAttrs = Object.keys(opts.attributes).length > 0;
|
|
404
427
|
const attrSummary = hasAttrs
|
|
405
428
|
? Object.entries(opts.attributes)
|
|
@@ -416,7 +439,7 @@ Generated by \`dogsbay migrate-asciidoc\`.
|
|
|
416
439
|
## Summary
|
|
417
440
|
|
|
418
441
|
- **Site name:** ${opts.siteName}
|
|
419
|
-
- **Source corpus:** \`${opts.sourceDir}\` (
|
|
442
|
+
- **Source corpus:** \`${opts.sourceDir}\` (${corpusDesc}${distroNote})
|
|
420
443
|
- **Output:** \`${opts.outputDir}\`
|
|
421
444
|
- **Pages migrated:** ${opts.pageCount}
|
|
422
445
|
|
|
@@ -455,8 +478,7 @@ See \`docs/asciidoc.md\` for the full preprocessor behaviour.
|
|
|
455
478
|
|
|
456
479
|
## What didn't (yet)
|
|
457
480
|
|
|
458
|
-
- **
|
|
459
|
-
- **Line-form admonition layout**: \`NOTE: foo\` lines always emit the HTML-shaped \`**📌 NOTE**\\\` prefix regardless of options. Block-form \`[NOTE]\\n====\\n…\\n====\` does respect the configured \`admonitionFormat\`.
|
|
481
|
+
${topologyDidnt}- **Line-form admonition layout**: \`NOTE: foo\` lines always emit the HTML-shaped \`**📌 NOTE**\\\` prefix regardless of options. Block-form \`[NOTE]\\n====\\n…\\n====\` does respect the configured \`admonitionFormat\`.
|
|
460
482
|
|
|
461
483
|
## Next steps
|
|
462
484
|
|
|
@@ -540,6 +562,7 @@ export async function migrateAsciidoc(source, options) {
|
|
|
540
562
|
attributes: engineAttributes,
|
|
541
563
|
loader: options.loader,
|
|
542
564
|
distro: options.distro,
|
|
565
|
+
topicMap: options.topicMap,
|
|
543
566
|
});
|
|
544
567
|
if (importedPages.length === 0) {
|
|
545
568
|
console.log(pc.red(`Error: no .adoc files found under ${sourceDir}`));
|
|
@@ -548,6 +571,13 @@ export async function migrateAsciidoc(source, options) {
|
|
|
548
571
|
}
|
|
549
572
|
console.log(`Pages: ${importedPages.length}`);
|
|
550
573
|
console.log();
|
|
574
|
+
// Which loader did the import auto-pick? Re-run detection (cheap —
|
|
575
|
+
// filesystem existence checks) so MIGRATION.md reports the topology
|
|
576
|
+
// that actually ran, not a hardcoded "plain walk" string. The import
|
|
577
|
+
// above already succeeded, so this won't throw on a no-match.
|
|
578
|
+
const loaderName = pickLoader(sourceDir, options.loader, {
|
|
579
|
+
topicMap: options.topicMap,
|
|
580
|
+
}).name;
|
|
551
581
|
// 3. Write each page to disk with title frontmatter.
|
|
552
582
|
//
|
|
553
583
|
// When the importer populated `page.bodyMarkdown` (currently only
|
|
@@ -620,6 +650,26 @@ export async function migrateAsciidoc(source, options) {
|
|
|
620
650
|
(frag.skipped > 0 ? ` (${frag.skipped} already-written page paths skipped)` : "") +
|
|
621
651
|
(frag.failed > 0 ? ` (${frag.failed} failed)` : ""));
|
|
622
652
|
}
|
|
653
|
+
// 3c-bis. Resolve remote `include::https://…[]` directives: fetch each URL
|
|
654
|
+
// once and bake it into the fragment as a fenced code block, so the build
|
|
655
|
+
// has no runtime network dependency and the content isn't dropped. (#007)
|
|
656
|
+
if (frag.remoteIncludePaths.length > 0) {
|
|
657
|
+
const cache = new Map();
|
|
658
|
+
let inlined = 0;
|
|
659
|
+
for (const mdPath of frag.remoteIncludePaths) {
|
|
660
|
+
const before = readFileSync(mdPath, "utf-8");
|
|
661
|
+
const after = await inlineRemoteIncludes(before, { cache });
|
|
662
|
+
if (after !== before) {
|
|
663
|
+
writeFileSync(mdPath, after, "utf-8");
|
|
664
|
+
inlined += 1;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const failures = [...cache.values()].filter((v) => v === null).length;
|
|
668
|
+
console.log(pc.green(`Inlined`) +
|
|
669
|
+
` remote includes in ${inlined} fragment(s) (${cache.size} URL(s)` +
|
|
670
|
+
(failures > 0 ? `, ${failures} unreachable` : "") +
|
|
671
|
+
`)`);
|
|
672
|
+
}
|
|
623
673
|
// 3d. Mirror non-`.adoc` filesystem entries within the scoped
|
|
624
674
|
// set: symlinks (critical for AsciiBinder include resolution
|
|
625
675
|
// where per-section dirs symlink back to root `_attributes/`,
|
|
@@ -695,6 +745,8 @@ export async function migrateAsciidoc(source, options) {
|
|
|
695
745
|
pageCount: importedPages.length,
|
|
696
746
|
attributes,
|
|
697
747
|
rerunCmd,
|
|
748
|
+
loader: loaderName,
|
|
749
|
+
distro: options.distro,
|
|
698
750
|
}));
|
|
699
751
|
console.log(pc.green(`Wrote`) + ` MIGRATION.md`);
|
|
700
752
|
if (!options.quiet) {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `migrate-fluidtopics-rest` — migrate a *live* Fluid Topics portal (via the
|
|
3
|
+
* khub REST API) into a scaffolded Dogsbay-MD site. Reverse-engineers DITA-OT
|
|
4
|
+
* HTML5 → TreeNode → Dogsbay-MD. Works on any public FT portal.
|
|
5
|
+
*
|
|
6
|
+
* Same output shape as `migrate-fluidtopics` (Option A, Git Markdown export);
|
|
7
|
+
* this is Option B (live REST).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join, resolve, dirname } from "node:path";
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
import YAML from "yaml";
|
|
13
|
+
import { emitSiteScaffold } from "@dogsbay/format-astro";
|
|
14
|
+
import { serializeConfig } from "../config/serialize.js";
|
|
15
|
+
export async function migrateFluidtopicsRest(source, options) {
|
|
16
|
+
if (!/^https?:\/\//i.test(source)) {
|
|
17
|
+
console.log(pc.red(`Error: source must be a portal URL (e.g. https://www.servicenow.com/docs)`));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const outputDir = resolve(options.output || "fluidtopics-rest-dogsbay");
|
|
21
|
+
const existingConfig = ["yml", "yaml", "json"]
|
|
22
|
+
.map((ext) => join(outputDir, `dogsbay.config.${ext}`))
|
|
23
|
+
.find(existsSync);
|
|
24
|
+
if (existingConfig && !options.force) {
|
|
25
|
+
console.log(pc.red(`Error: ${outputDir} already contains a Dogsbay site (${existingConfig}). Pass --force.`));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const siteName = options.siteName || "Documentation";
|
|
29
|
+
const basePath = options.basePath !== undefined ? options.basePath : "";
|
|
30
|
+
const hrefPrefix = basePath;
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(pc.bold(`Migrating Fluid Topics portal (REST) to Dogsbay-MD: ${siteName}`));
|
|
33
|
+
console.log(`Portal: ${source}`);
|
|
34
|
+
console.log(`Output: ${outputDir}`);
|
|
35
|
+
console.log();
|
|
36
|
+
const contentDir = join(outputDir, "content");
|
|
37
|
+
const astroDir = join(outputDir, "astro");
|
|
38
|
+
mkdirSync(contentDir, { recursive: true });
|
|
39
|
+
mkdirSync(astroDir, { recursive: true });
|
|
40
|
+
const { importFluidTopicsRest } = (await import(
|
|
41
|
+
/* @vite-ignore */ "@dogsbay/format-fluidtopics-rest"));
|
|
42
|
+
const { treeToDogsbayMd } = (await import(
|
|
43
|
+
/* @vite-ignore */ "@dogsbay/format-dogsbay-md"));
|
|
44
|
+
const { pages, nav, unresolved, mapsProcessed, assets } = await importFluidTopicsRest({
|
|
45
|
+
portalBase: source,
|
|
46
|
+
maps: options.map,
|
|
47
|
+
titleFilter: options.titleFilter,
|
|
48
|
+
groupBy: options.groupBy,
|
|
49
|
+
singlePageBooks: options.singlePageBooks,
|
|
50
|
+
keepDuplicateMaps: options.keepDuplicateMaps,
|
|
51
|
+
metadataAllow: options.metadata?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
52
|
+
fullTopicMetadata: options.fullTopicMetadata,
|
|
53
|
+
// Custom facets: editorialType (book/article), version, and subject (the
|
|
54
|
+
// DITA subjectScheme "Subject matter" / PBS facet, now lifted per-topic).
|
|
55
|
+
// category & audience are built-in. parseMeta lifts these into
|
|
56
|
+
// page.meta.taxonomies → DocsLayout emits data-pagefind-filter → Pagefind
|
|
57
|
+
// facets. (#search-facets)
|
|
58
|
+
taxonomyNames: ["editorialType", "version", "subject"],
|
|
59
|
+
lang: options.lang,
|
|
60
|
+
hrefPrefix,
|
|
61
|
+
throttleMs: options.throttle ? Number(options.throttle) : undefined,
|
|
62
|
+
singlePage: options.singlePage,
|
|
63
|
+
onProgress: (m) => console.log(pc.dim(` ${m}`)),
|
|
64
|
+
});
|
|
65
|
+
const unit = options.singlePage ? "page(s) (one per book)" : "topics";
|
|
66
|
+
console.log(pc.green(`Imported`) + ` ${pages.length} ${unit} from ${mapsProcessed} map(s)`);
|
|
67
|
+
// Write extracted images as real content-rooted assets (per _assets convention).
|
|
68
|
+
for (const a of assets) {
|
|
69
|
+
const dest = join(contentDir, a.path);
|
|
70
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
71
|
+
writeFileSync(dest, a.data);
|
|
72
|
+
}
|
|
73
|
+
if (assets.length)
|
|
74
|
+
console.log(pc.green(`Wrote`) + ` ${assets.length} images to content/_assets/`);
|
|
75
|
+
for (const page of pages) {
|
|
76
|
+
const dest = join(contentDir, `${page.slug || "index"}.md`);
|
|
77
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
78
|
+
const body = treeToDogsbayMd(page.tree, { frontmatter: page.frontmatter });
|
|
79
|
+
writeFileSync(dest, body.endsWith("\n") ? body : `${body}\n`);
|
|
80
|
+
}
|
|
81
|
+
console.log(pc.green(`Wrote`) + ` ${pages.length} pages to ./content/`);
|
|
82
|
+
writeFileSync(join(contentDir, "nav.yml"), YAML.stringify(navItemsToSingleKey(nav, basePath)));
|
|
83
|
+
console.log(pc.green(`Wrote`) + ` content/nav.yml (${nav.length} top-level items)`);
|
|
84
|
+
const config = {
|
|
85
|
+
schemaVersion: 1,
|
|
86
|
+
site: { name: siteName, url: options.siteUrl, basePath },
|
|
87
|
+
content: { sources: [{ path: "./content", from: "dogsbay-md" }] },
|
|
88
|
+
// FT faceted metadata → search filters. `category` / `audience` are
|
|
89
|
+
// built-in facets (no declaration needed); `editorialType` (book/article),
|
|
90
|
+
// `version`, and `subject` (the DITA subjectScheme "Subject matter" / PBS
|
|
91
|
+
// facet) are declared here so the build extracts them into each page's
|
|
92
|
+
// taxonomies, DocsLayout emits the data-pagefind-filter divs, and Pagefind
|
|
93
|
+
// surfaces them as facet columns. Also gives browseable term index pages
|
|
94
|
+
// (/format, /version, /subject). See docs-dev/fluidtopics-rest-importer.md.
|
|
95
|
+
taxonomies: {
|
|
96
|
+
editorialType: { indexPath: "/format", order: 2 },
|
|
97
|
+
version: { indexPath: "/version", order: 3 },
|
|
98
|
+
subject: { indexPath: "/subject", order: 4 },
|
|
99
|
+
},
|
|
100
|
+
agent: { llmsTxt: true, mdMirror: true },
|
|
101
|
+
};
|
|
102
|
+
writeFileSync(join(outputDir, "dogsbay.config.yml"), serializeConfig(config, "yaml"));
|
|
103
|
+
emitSiteScaffold(astroDir, siteName, { siteName, siteUrl: options.siteUrl, basePath, llmsTxt: true, mdMirror: true, local: options.local }, true);
|
|
104
|
+
const unresolvedCount = Object.values(unresolved).reduce((n, a) => n + a.length, 0);
|
|
105
|
+
writeFileSync(join(outputDir, "MIGRATION.md"), `# Migration notes — ${siteName} (Fluid Topics REST)\n\nMigrated live from \`${source}\` via the khub API; DITA-OT HTML5 reverse-engineered to Dogsbay-MD.\n\n- Maps: **${mapsProcessed}**\n- Pages: **${pages.length}**\n- Unresolved in-content links: **${unresolvedCount}**\n\nMedia and any conref/keyref/profiling were already resolved/flattened upstream by FT's DITA-OT run.\n`);
|
|
106
|
+
console.log(pc.green(`Scaffolded`) + ` Astro project + config + MIGRATION.md`);
|
|
107
|
+
if (!options.quiet) {
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(pc.bold("Done."));
|
|
110
|
+
console.log(` cd ${outputDir} && npx dogsbay site dev`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function navItemsToSingleKey(items, basePath) {
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const item of items) {
|
|
116
|
+
// A node carrying a single-page annotation uses the EXPLICIT nav form
|
|
117
|
+
// (`{ title, single-page, items }`) so the annotation survives into nav.yml;
|
|
118
|
+
// unannotated nodes keep the compact single-key form.
|
|
119
|
+
if (item.singlePage) {
|
|
120
|
+
const entry = { title: item.label, "single-page": item.singlePage };
|
|
121
|
+
if (item.children && item.children.length > 0) {
|
|
122
|
+
entry.items = navItemsToSingleKey(item.children, basePath);
|
|
123
|
+
}
|
|
124
|
+
else if (item.href) {
|
|
125
|
+
entry.file = hrefToNavPath(item.href, basePath);
|
|
126
|
+
}
|
|
127
|
+
out.push(entry);
|
|
128
|
+
}
|
|
129
|
+
else if (item.children && item.children.length > 0) {
|
|
130
|
+
out.push({ [item.label]: navItemsToSingleKey(item.children, basePath) });
|
|
131
|
+
}
|
|
132
|
+
else if (item.href) {
|
|
133
|
+
out.push({ [item.label]: hrefToNavPath(item.href, basePath) });
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
out.push({ [item.label]: [] });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
function hrefToNavPath(href, basePath) {
|
|
142
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href))
|
|
143
|
+
return href;
|
|
144
|
+
let s = href.split("#")[0];
|
|
145
|
+
if (basePath && s.startsWith(basePath))
|
|
146
|
+
s = s.slice(basePath.length);
|
|
147
|
+
s = s.replace(/^\/+/, "");
|
|
148
|
+
return s ? `${s}.md` : "index.md";
|
|
149
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `migrate-fluidtopics` — one-way migrate a Fluid Topics Markdown export
|
|
3
|
+
* (e.g. the ServiceNow/ServiceNowDocs repo) into a scaffolded Dogsbay-MD site.
|
|
4
|
+
*
|
|
5
|
+
* Canonical migration layout (see the migration-shape skill):
|
|
6
|
+
* <output>/
|
|
7
|
+
* content/ <- Dogsbay-MD, the new source of truth
|
|
8
|
+
* <pub>/<topic>.md
|
|
9
|
+
* nav.yml
|
|
10
|
+
* astro/ <- generated Astro project (emitSiteScaffold)
|
|
11
|
+
* dogsbay.config.yml
|
|
12
|
+
* MIGRATION.md
|
|
13
|
+
*
|
|
14
|
+
* Mirrors `migrate-mkdocs`, reusing the shared emitters
|
|
15
|
+
* (importer -> treeToDogsbayMd -> emitSiteScaffold + serializeConfig).
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { join, resolve, basename, dirname } from "node:path";
|
|
19
|
+
import pc from "picocolors";
|
|
20
|
+
import YAML from "yaml";
|
|
21
|
+
import { emitSiteScaffold } from "@dogsbay/format-astro";
|
|
22
|
+
import { serializeConfig } from "../config/serialize.js";
|
|
23
|
+
export async function migrateFluidtopics(source, options) {
|
|
24
|
+
const sourceDir = resolve(source);
|
|
25
|
+
const markdownDir = join(sourceDir, "markdown");
|
|
26
|
+
if (!existsSync(markdownDir)) {
|
|
27
|
+
console.log(pc.red(`Error: no markdown/ directory found in ${sourceDir}`));
|
|
28
|
+
console.log("Point this at a Fluid Topics Markdown export (e.g. a ServiceNowDocs checkout).");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const outputDir = resolve(options.output || `${basename(sourceDir)}-dogsbay`);
|
|
32
|
+
const existingConfig = ["yml", "yaml", "json"]
|
|
33
|
+
.map((ext) => join(outputDir, `dogsbay.config.${ext}`))
|
|
34
|
+
.find(existsSync);
|
|
35
|
+
if (existingConfig && !options.force) {
|
|
36
|
+
console.log(pc.red(`Error: ${outputDir} already contains a Dogsbay site (${basename(existingConfig)}).`));
|
|
37
|
+
console.log("Pass --force to overwrite.");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const siteName = options.siteName || "Documentation";
|
|
41
|
+
// A dedicated docs repo serves at the host root by default (matches the
|
|
42
|
+
// openshift-docs-dogsbay / GitHub Pages project-site convention).
|
|
43
|
+
const basePath = options.basePath !== undefined ? options.basePath : "";
|
|
44
|
+
const hrefPrefix = basePath; // root-relative slugs when basePath is ""
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(pc.bold(`Migrating Fluid Topics Markdown export to Dogsbay-MD: ${siteName}`));
|
|
47
|
+
console.log(`Source: ${sourceDir}`);
|
|
48
|
+
console.log(`Output: ${outputDir}`);
|
|
49
|
+
console.log();
|
|
50
|
+
const contentDir = join(outputDir, "content");
|
|
51
|
+
const astroDir = join(outputDir, "astro");
|
|
52
|
+
mkdirSync(contentDir, { recursive: true });
|
|
53
|
+
mkdirSync(astroDir, { recursive: true });
|
|
54
|
+
// 1. Import the export -> pages + nav.
|
|
55
|
+
const { importFluidTopicsProject } = (await import(
|
|
56
|
+
/* @vite-ignore */ "@dogsbay/format-fluidtopics-markdown"));
|
|
57
|
+
const { treeToDogsbayMd } = (await import(
|
|
58
|
+
/* @vite-ignore */ "@dogsbay/format-dogsbay-md"));
|
|
59
|
+
const { pages, nav, unresolved } = await importFluidTopicsProject(sourceDir, {
|
|
60
|
+
hrefPrefix,
|
|
61
|
+
publications: options.publication,
|
|
62
|
+
});
|
|
63
|
+
if (options.publication && options.publication.length > 0) {
|
|
64
|
+
console.log(pc.dim(`Scoped to publications: ${options.publication.join(", ")}`));
|
|
65
|
+
}
|
|
66
|
+
// 2. Write each page to ./content/<slug>.md as Dogsbay-MD.
|
|
67
|
+
for (const page of pages) {
|
|
68
|
+
const rel = `${page.slug || "index"}.md`;
|
|
69
|
+
const dest = join(contentDir, rel);
|
|
70
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
71
|
+
const body = treeToDogsbayMd(page.tree, { frontmatter: page.frontmatter });
|
|
72
|
+
writeFileSync(dest, body.endsWith("\n") ? body : `${body}\n`);
|
|
73
|
+
}
|
|
74
|
+
console.log(pc.green("Wrote") + ` ${pages.length} pages to ./content/ as Dogsbay-MD`);
|
|
75
|
+
// 3. nav.yml (single-key-map; file paths relative to content/).
|
|
76
|
+
const navYml = navItemsToSingleKey(nav, basePath);
|
|
77
|
+
writeFileSync(join(contentDir, "nav.yml"), YAML.stringify(navYml));
|
|
78
|
+
console.log(pc.green("Extracted") + ` navigation to content/nav.yml (${navYml.length} top-level items)`);
|
|
79
|
+
// 4. dogsbay.config.yml.
|
|
80
|
+
const dogsbayConfig = {
|
|
81
|
+
schemaVersion: 1,
|
|
82
|
+
site: { name: siteName, url: options.siteUrl, basePath },
|
|
83
|
+
content: { sources: [{ path: "./content", from: "dogsbay-md" }] },
|
|
84
|
+
agent: { llmsTxt: true, mdMirror: true },
|
|
85
|
+
};
|
|
86
|
+
writeFileSync(join(outputDir, "dogsbay.config.yml"), serializeConfig(dogsbayConfig, "yaml"));
|
|
87
|
+
console.log(pc.green("Wrote") + " dogsbay.config.yml");
|
|
88
|
+
// 5. Scaffold the Astro project under ./astro/.
|
|
89
|
+
emitSiteScaffold(astroDir, siteName, { siteName, siteUrl: options.siteUrl, basePath, llmsTxt: true, mdMirror: true, local: options.local }, true);
|
|
90
|
+
console.log(pc.green("Scaffolded") + " Astro project at ./astro/");
|
|
91
|
+
// 6. MIGRATION.md.
|
|
92
|
+
const unresolvedCount = Object.values(unresolved).reduce((n, a) => n + a.length, 0);
|
|
93
|
+
writeFileSync(join(outputDir, "MIGRATION.md"), buildMigrationNotes({ sourceDir, siteName, pageCount: pages.length, unresolvedCount, basePath }));
|
|
94
|
+
console.log(pc.green("Wrote") + " MIGRATION.md");
|
|
95
|
+
if (!options.quiet) {
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(pc.bold("Done! Your Dogsbay-MD site is ready."));
|
|
98
|
+
console.log();
|
|
99
|
+
console.log("Next steps:");
|
|
100
|
+
console.log(` cd ${outputDir}`);
|
|
101
|
+
console.log(" npx dogsbay site dev # live preview (auto-installs)");
|
|
102
|
+
console.log();
|
|
103
|
+
console.log("Edit ./content/*.md (canonical Dogsbay-MD) from here on.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function navItemsToSingleKey(items, basePath) {
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const item of items) {
|
|
109
|
+
if (item.children && item.children.length > 0) {
|
|
110
|
+
out.push({ [item.label]: navItemsToSingleKey(item.children, basePath) });
|
|
111
|
+
}
|
|
112
|
+
else if (item.href) {
|
|
113
|
+
out.push({ [item.label]: hrefToNavPath(item.href, basePath) });
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
out.push({ [item.label]: [] });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/** Convert a resolved route href back to a content-relative .md path. */
|
|
122
|
+
function hrefToNavPath(href, basePath) {
|
|
123
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href))
|
|
124
|
+
return href; // external
|
|
125
|
+
let s = href.split("#")[0];
|
|
126
|
+
if (basePath && s.startsWith(basePath))
|
|
127
|
+
s = s.slice(basePath.length);
|
|
128
|
+
s = s.replace(/^\/+/, "");
|
|
129
|
+
if (!s)
|
|
130
|
+
return "index.md";
|
|
131
|
+
return `${s}.md`;
|
|
132
|
+
}
|
|
133
|
+
function buildMigrationNotes(args) {
|
|
134
|
+
return [
|
|
135
|
+
`# Migration notes — ${args.siteName}`,
|
|
136
|
+
"",
|
|
137
|
+
`Migrated from a Fluid Topics Markdown export at \`${args.sourceDir}\`.`,
|
|
138
|
+
"",
|
|
139
|
+
`- Pages: **${args.pageCount}**`,
|
|
140
|
+
`- Base path: \`${args.basePath || "(root)"}\``,
|
|
141
|
+
`- Unresolved internal links: **${args.unresolvedCount}** (broken cross-references in the source export are left as-is).`,
|
|
142
|
+
"",
|
|
143
|
+
"## What this did",
|
|
144
|
+
"",
|
|
145
|
+
"- Converted each topic to Dogsbay-MD under `./content/` (DITA `topic_type` → frontmatter `type`).",
|
|
146
|
+
"- Embedded HTML tables → Markdown tables; DITA nav-card grids → `:::cards`.",
|
|
147
|
+
"- Resolved both link schemes (relative `.md` + `raw.githubusercontent` TOC URLs) to routes.",
|
|
148
|
+
"- Built `content/nav.yml` from each publication's `index.md` TOC + the README order.",
|
|
149
|
+
"",
|
|
150
|
+
"## Known gaps",
|
|
151
|
+
"",
|
|
152
|
+
"- **Media is omitted** in the source export — image references may be dangling. Pull assets from the live portal or replace as needed.",
|
|
153
|
+
"- Conref/keyref/profiling were already resolved/flattened by the export (not recoverable here).",
|
|
154
|
+
"",
|
|
155
|
+
"Edit `./content/*.md` from here on; re-run the migration to refresh.",
|
|
156
|
+
"",
|
|
157
|
+
].join("\n");
|
|
158
|
+
}
|
package/dist/config/load.js
CHANGED
|
@@ -104,6 +104,15 @@ function validate(parsed, sourcePath) {
|
|
|
104
104
|
const propSchema = validatePropSchema(obj.propSchema, sourcePath);
|
|
105
105
|
const plugins = validatePlugins(obj.plugins, sourcePath);
|
|
106
106
|
const attributes = validateAttributes(obj.attributes, "attributes", sourcePath);
|
|
107
|
+
if (obj.granularity !== undefined) {
|
|
108
|
+
if (typeof obj.granularity !== "object" || obj.granularity === null) {
|
|
109
|
+
throw new Error(`granularity must be an object in ${sourcePath}`);
|
|
110
|
+
}
|
|
111
|
+
const g = obj.granularity;
|
|
112
|
+
if (g.book !== undefined && typeof g.book !== "boolean") {
|
|
113
|
+
throw new Error(`granularity.book must be a boolean in ${sourcePath}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
107
116
|
// Pass through validated structure — applyDefaults handles missing
|
|
108
117
|
// optionals and contentSignal sub-fields.
|
|
109
118
|
return {
|
|
@@ -121,6 +130,7 @@ function validate(parsed, sourcePath) {
|
|
|
121
130
|
propSchema,
|
|
122
131
|
plugins,
|
|
123
132
|
attributes,
|
|
133
|
+
granularity: obj.granularity,
|
|
124
134
|
};
|
|
125
135
|
}
|
|
126
136
|
/**
|
|
@@ -29,6 +29,7 @@ export function configToAstroOptions(config) {
|
|
|
29
29
|
navMode: config.build?.navMode,
|
|
30
30
|
llmsTxt: config.agent?.llmsTxt,
|
|
31
31
|
mdMirror: config.agent?.mdMirror,
|
|
32
|
+
granularity: config.granularity,
|
|
32
33
|
aiTrain: config.agent?.contentSignal?.aiTrain,
|
|
33
34
|
aiInput: config.agent?.contentSignal?.aiInput,
|
|
34
35
|
aiSearch: config.agent?.contentSignal?.search,
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ import { add, list } from "./commands/add.js";
|
|
|
8
8
|
import { importMkdocs } from "./commands/import-mkdocs.js";
|
|
9
9
|
import { migrateMkdocs } from "./commands/migrate-mkdocs.js";
|
|
10
10
|
import { migrateAsciidoc } from "./commands/migrate-asciidoc.js";
|
|
11
|
+
import { migrateFluidtopics } from "./commands/migrate-fluidtopics.js";
|
|
12
|
+
import { migrateFluidtopicsRest } from "./commands/migrate-fluidtopics-rest.js";
|
|
11
13
|
import { preprocessVariants } from "./commands/preprocess-variants.js";
|
|
12
14
|
import { lighthouse } from "./commands/lighthouse.js";
|
|
13
15
|
import { convert } from "./commands/convert.js";
|
|
@@ -211,10 +213,11 @@ program
|
|
|
211
213
|
.action((source, options) => migrateMkdocs(source, options));
|
|
212
214
|
program
|
|
213
215
|
.command("migrate-asciidoc")
|
|
214
|
-
.description("One-way migrate
|
|
216
|
+
.description("One-way migrate an AsciiDoc corpus to a scaffolded Dogsbay-MD " +
|
|
215
217
|
"project. Source-of-truth becomes ./content/*.md; the AsciiDoc source " +
|
|
216
|
-
"can be deleted.
|
|
217
|
-
"
|
|
218
|
+
"can be deleted. Topology is auto-detected — AsciiBinder " +
|
|
219
|
+
"(_topic_maps/_topic_map.yml), AAP (master.adoc), Antora (antora.yml), " +
|
|
220
|
+
"or a plain .adoc directory walk; override with --loader.")
|
|
218
221
|
.argument("<source>", "Path to a directory containing .adoc files")
|
|
219
222
|
.option("-o, --output <dir>", "Output directory (default: {source}-dogsbay)")
|
|
220
223
|
.option("--force", "Overwrite an existing Dogsbay site. Clean re-import: clears the " +
|
|
@@ -226,13 +229,59 @@ program
|
|
|
226
229
|
.option("-a, --attribute <name=value>", "AsciiDoc attribute (repeatable). Resolved refs/conditionals get " +
|
|
227
230
|
"baked into the migrated Dogsbay-MD; unresolved ones survive as " +
|
|
228
231
|
"{{ var }} / {% if %} for a downstream Minja pass.", (val, prev = []) => [...prev, val])
|
|
229
|
-
.option("--loader <name>", "Force a corpus loader (plain, master-adoc).
|
|
232
|
+
.option("--loader <name>", "Force a corpus loader (plain, master-adoc, asciibinder, antora). " +
|
|
233
|
+
"Auto-detected when omitted.")
|
|
230
234
|
.option("--distro <name>", "AsciiBinder distro filter (e.g. 'openshift-enterprise'). Topic-map " +
|
|
231
235
|
"entries whose Distros: list excludes this value are dropped from " +
|
|
232
236
|
"the page set and nav. Entries without Distros: are kept as universal.")
|
|
237
|
+
.option("--topic-map <path>", "AsciiBinder topic-map path, relative to the source root " +
|
|
238
|
+
"(default: _topic_maps/_topic_map.yml). Drives loader detection and " +
|
|
239
|
+
"loading for corpora that name their topic map differently.")
|
|
233
240
|
.option("--local", "Use file: references to local monorepo packages (for development)")
|
|
234
241
|
.option("--quiet", "Suppress the 'Next steps' trailer")
|
|
235
242
|
.action((source, options) => migrateAsciidoc(source, options));
|
|
243
|
+
program
|
|
244
|
+
.command("migrate-fluidtopics")
|
|
245
|
+
.description("One-way migrate a Fluid Topics Markdown export (e.g. a ServiceNowDocs " +
|
|
246
|
+
"checkout: markdown/{publication}/**) to a scaffolded Dogsbay-MD " +
|
|
247
|
+
"project. Source-of-truth becomes ./content/*.md.")
|
|
248
|
+
.argument("<source>", "Path to the FT Markdown export (containing markdown/)")
|
|
249
|
+
.option("-o, --output <dir>", "Output directory (default: {source}-dogsbay)")
|
|
250
|
+
.option("--force", "Overwrite an existing Dogsbay site at the output dir")
|
|
251
|
+
.option("--site-name <name>", "Site name written into dogsbay.config.yml")
|
|
252
|
+
.option("--site-url <url>", "Canonical URL of the destination site. Drives robots.txt, sitemap, " +
|
|
253
|
+
"canonical tags, and llms.txt.")
|
|
254
|
+
.option("--base-path <path>", 'Path under the host where docs are served (default "" = host root, ' +
|
|
255
|
+
"the dedicated-repo convention).")
|
|
256
|
+
.option("--publication <name>", "Limit the migration to one or more publications (top-level markdown/{pub} " +
|
|
257
|
+
"folder). Repeatable. Useful for previewing a slice of a huge corpus.", (val, prev = []) => [...prev, val])
|
|
258
|
+
.option("--local", "Use file: references to local monorepo packages (for development)")
|
|
259
|
+
.option("--quiet", "Suppress the 'Next steps' trailer")
|
|
260
|
+
.action((source, options) => migrateFluidtopics(source, options));
|
|
261
|
+
program
|
|
262
|
+
.command("migrate-fluidtopics-rest")
|
|
263
|
+
.description("Migrate a LIVE Fluid Topics portal via the khub REST API (Option B): " +
|
|
264
|
+
"reverse-engineers DITA-OT HTML5 → Dogsbay-MD. Works on any public FT " +
|
|
265
|
+
"portal (ServiceNow, Johnson Controls, FT docs).")
|
|
266
|
+
.argument("<portal-url>", "FT portal root (e.g. https://www.servicenow.com/docs or https://docs.johnsoncontrols.com/bas)")
|
|
267
|
+
.option("-o, --output <dir>", "Output directory (default: fluidtopics-rest-dogsbay)")
|
|
268
|
+
.option("--force", "Overwrite an existing Dogsbay site at the output dir")
|
|
269
|
+
.option("--site-name <name>", "Site name written into dogsbay.config.yml")
|
|
270
|
+
.option("--site-url <url>", "Canonical URL of the destination site")
|
|
271
|
+
.option("--base-path <path>", 'Path under the host where docs are served (default "" = host root)')
|
|
272
|
+
.option("--map <id>", "Limit to one or more map ids (repeatable)", (v, p = []) => [...p, v])
|
|
273
|
+
.option("--title-filter <substr>", "Limit to maps whose title contains this substring (case-insensitive)")
|
|
274
|
+
.option("--lang <locale>", "Locale to request from the portal (e.g. en-US)")
|
|
275
|
+
.option("--throttle <ms>", "Delay between khub requests (default 150)")
|
|
276
|
+
.option("--single-page", "Roll each map into ONE page (whole book) instead of one page per topic; xrefs become in-page anchors")
|
|
277
|
+
.option("--group-by <facets>", "Group the nav by map metadata facet(s), comma-separated (e.g. Category or Product,Category). Default: auto (Category). Use 'none' to keep it flat")
|
|
278
|
+
.option("--single-page-books", "Mark book-type maps (editorialType=book) with single-page: yes — an additive 'Read as single page' rollup per guide; articles stay per-topic (size cap applies)")
|
|
279
|
+
.option("--keep-duplicate-maps", "Keep stale duplicate publications (same title+version). Default: keep only the freshest ft:lastEdition per guide, removing duplicate nav entries")
|
|
280
|
+
.option("--metadata <keys>", "Extra FT topic-metadata keys to lift into page frontmatter, comma-separated (beyond the curated subject/product/author/category/audience/version set)")
|
|
281
|
+
.option("--full-topic-metadata", "Fetch full per-topic metadata (one request per topic) for accurate per-topic ft:wordCount/ft:outputSize + 'updated' date. Roughly doubles requests; off by default (technical numerics emit at book level only)")
|
|
282
|
+
.option("--local", "Use file: references to local monorepo packages (for development)")
|
|
283
|
+
.option("--quiet", "Suppress the trailer")
|
|
284
|
+
.action((source, options) => migrateFluidtopicsRest(source, options));
|
|
236
285
|
program
|
|
237
286
|
.command("preprocess-variants")
|
|
238
287
|
.description("Add variant tabs to file includes (e.g. Python 3.10+ / 3.9+ tabs)")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote AsciiDoc include resolution (migrate time).
|
|
3
|
+
*
|
|
4
|
+
* AsciiDoc `include::https://…[]` inlines remote content at build time
|
|
5
|
+
* (AsciiBinder runs with `--allow-uri-read`). The adoc2md engine — which is
|
|
6
|
+
* pure and does no network I/O — emits the URL form as a Minja directive
|
|
7
|
+
* `{% include "./https://…" %}`. The build-time Minja loader is filesystem-only,
|
|
8
|
+
* so the directive would leak verbatim into a `<pre>` block, dropping the
|
|
9
|
+
* content (e.g. 6 CloudFormation templates on the AWS UPI install pages).
|
|
10
|
+
*
|
|
11
|
+
* This pass fetches each remote include ONCE (cached by URL) and inlines it as
|
|
12
|
+
* a fenced code block, baking the content into the migrated markdown so the
|
|
13
|
+
* build has no runtime network dependency. On fetch failure it leaves a visible
|
|
14
|
+
* link instead of a broken directive. Issue #007.
|
|
15
|
+
*/
|
|
16
|
+
// Code-fence language inferred from the URL's file extension.
|
|
17
|
+
const LANG_BY_EXT = {
|
|
18
|
+
yaml: "yaml",
|
|
19
|
+
yml: "yaml",
|
|
20
|
+
json: "json",
|
|
21
|
+
sh: "bash",
|
|
22
|
+
bash: "bash",
|
|
23
|
+
py: "python",
|
|
24
|
+
rb: "ruby",
|
|
25
|
+
go: "go",
|
|
26
|
+
toml: "toml",
|
|
27
|
+
ini: "ini",
|
|
28
|
+
xml: "xml",
|
|
29
|
+
tf: "hcl",
|
|
30
|
+
};
|
|
31
|
+
// `{% include "./https://…" %}` (the `./` is the engine's local-path prefix,
|
|
32
|
+
// spurious for URLs) on its own line, capturing leading indent + the URL.
|
|
33
|
+
const remoteIncludeRx = /^([ \t]*)\{%\s*include\s*"\.?\/?(https?:\/\/[^"]+)"\s*%\}[ \t]*$/gm;
|
|
34
|
+
function langForUrl(url) {
|
|
35
|
+
const clean = url.split(/[?#]/)[0];
|
|
36
|
+
const ext = clean.slice(clean.lastIndexOf(".") + 1).toLowerCase();
|
|
37
|
+
return LANG_BY_EXT[ext] ?? "";
|
|
38
|
+
}
|
|
39
|
+
/** Choose a backtick fence longer than any run inside the content. */
|
|
40
|
+
function pickFence(content) {
|
|
41
|
+
const longest = (content.match(/`+/g) ?? []).reduce((n, run) => Math.max(n, run.length), 0);
|
|
42
|
+
return "`".repeat(Math.max(3, longest + 1));
|
|
43
|
+
}
|
|
44
|
+
/** True if `md` contains at least one remote include directive. */
|
|
45
|
+
export function hasRemoteIncludes(md) {
|
|
46
|
+
remoteIncludeRx.lastIndex = 0;
|
|
47
|
+
return remoteIncludeRx.test(md);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Replace every `{% include "https://…" %}` directive in `md` with the fetched
|
|
51
|
+
* content as a fenced code block. Returns `md` unchanged if there are none.
|
|
52
|
+
*/
|
|
53
|
+
export async function inlineRemoteIncludes(md, options = {}) {
|
|
54
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
55
|
+
const cache = options.cache ?? new Map();
|
|
56
|
+
const onWarn = options.onWarn ?? ((m) => console.warn(m));
|
|
57
|
+
// Collect the distinct URLs first so each is fetched at most once.
|
|
58
|
+
const urls = new Set();
|
|
59
|
+
for (const m of md.matchAll(remoteIncludeRx))
|
|
60
|
+
urls.add(m[2]);
|
|
61
|
+
if (urls.size === 0)
|
|
62
|
+
return md;
|
|
63
|
+
for (const url of urls) {
|
|
64
|
+
if (cache.has(url))
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetchImpl(url);
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
throw new Error(`HTTP ${res.status}`);
|
|
70
|
+
cache.set(url, await res.text());
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
cache.set(url, null);
|
|
74
|
+
onWarn(`remote include: failed to fetch ${url} (${err instanceof Error ? err.message : String(err)}); leaving a link`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return md.replace(remoteIncludeRx, (_whole, indent, url) => {
|
|
78
|
+
const content = cache.get(url);
|
|
79
|
+
if (content == null) {
|
|
80
|
+
// Fetch failed (or not attempted): a visible link beats a broken directive.
|
|
81
|
+
return `${indent}See [${url}](${url}).`;
|
|
82
|
+
}
|
|
83
|
+
const body = content.replace(/\n$/, "");
|
|
84
|
+
const fence = pickFence(body);
|
|
85
|
+
const lang = langForUrl(url);
|
|
86
|
+
return `${indent}${fence}${lang}\n${body}\n${indent}${fence}`;
|
|
87
|
+
});
|
|
88
|
+
}
|
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.93",
|
|
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": {
|
|
@@ -33,20 +33,22 @@
|
|
|
33
33
|
"picocolors": "^1.1.0",
|
|
34
34
|
"prompts": "^2.4.2",
|
|
35
35
|
"yaml": "^2.8.3",
|
|
36
|
-
"@dogsbay/
|
|
37
|
-
"@dogsbay/
|
|
38
|
-
"@dogsbay/format-astro": "0.2.0-beta.
|
|
39
|
-
"@dogsbay/format-obsidian": "0.2.0-beta.
|
|
40
|
-
"@dogsbay/format-mdx": "0.2.0-beta.
|
|
41
|
-
"@dogsbay/format-starlight": "0.2.0-beta.
|
|
42
|
-
"@dogsbay/format-docusaurus": "0.2.0-beta.
|
|
43
|
-
"@dogsbay/docusaurus-adapter-tigera": "0.2.0-beta.
|
|
44
|
-
"@dogsbay/format-dogsbay-md": "0.2.0-beta.
|
|
45
|
-
"@dogsbay/format-
|
|
46
|
-
"@dogsbay/
|
|
47
|
-
"@dogsbay/format-
|
|
48
|
-
"@dogsbay/
|
|
49
|
-
"@dogsbay/
|
|
36
|
+
"@dogsbay/format-mkdocs": "0.2.0-beta.93",
|
|
37
|
+
"@dogsbay/autodoc-python": "0.2.0-beta.93",
|
|
38
|
+
"@dogsbay/format-astro": "0.2.0-beta.93",
|
|
39
|
+
"@dogsbay/format-obsidian": "0.2.0-beta.93",
|
|
40
|
+
"@dogsbay/format-mdx": "0.2.0-beta.93",
|
|
41
|
+
"@dogsbay/format-starlight": "0.2.0-beta.93",
|
|
42
|
+
"@dogsbay/format-docusaurus": "0.2.0-beta.93",
|
|
43
|
+
"@dogsbay/docusaurus-adapter-tigera": "0.2.0-beta.93",
|
|
44
|
+
"@dogsbay/format-dogsbay-md": "0.2.0-beta.93",
|
|
45
|
+
"@dogsbay/format-fluidtopics-markdown": "0.2.0-beta.93",
|
|
46
|
+
"@dogsbay/format-openapi": "0.2.0-beta.93",
|
|
47
|
+
"@dogsbay/format-fluidtopics-rest": "0.2.0-beta.93",
|
|
48
|
+
"@dogsbay/adoc2md-modular": "0.2.0-beta.93",
|
|
49
|
+
"@dogsbay/format-asciidoc": "0.2.0-beta.93",
|
|
50
|
+
"@dogsbay/minja": "0.2.0-beta.93",
|
|
51
|
+
"@dogsbay/types": "0.2.0-beta.93"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
52
54
|
"@types/markdown-it": "^14.1.0",
|