dogsbay 0.2.0-beta.92 → 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 +37 -1
- package/dist/config/load.js +10 -0
- package/dist/config/to-astro-options.js +1 -0
- package/dist/index.js +14 -4
- package/dist/utils/remote-includes.js +88 -0
- package/package.json +17 -17
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) {
|
|
@@ -45,6 +45,17 @@ export async function migrateFluidtopicsRest(source, options) {
|
|
|
45
45
|
portalBase: source,
|
|
46
46
|
maps: options.map,
|
|
47
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"],
|
|
48
59
|
lang: options.lang,
|
|
49
60
|
hrefPrefix,
|
|
50
61
|
throttleMs: options.throttle ? Number(options.throttle) : undefined,
|
|
@@ -74,6 +85,18 @@ export async function migrateFluidtopicsRest(source, options) {
|
|
|
74
85
|
schemaVersion: 1,
|
|
75
86
|
site: { name: siteName, url: options.siteUrl, basePath },
|
|
76
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
|
+
},
|
|
77
100
|
agent: { llmsTxt: true, mdMirror: true },
|
|
78
101
|
};
|
|
79
102
|
writeFileSync(join(outputDir, "dogsbay.config.yml"), serializeConfig(config, "yaml"));
|
|
@@ -90,7 +113,20 @@ export async function migrateFluidtopicsRest(source, options) {
|
|
|
90
113
|
function navItemsToSingleKey(items, basePath) {
|
|
91
114
|
const out = [];
|
|
92
115
|
for (const item of items) {
|
|
93
|
-
|
|
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) {
|
|
94
130
|
out.push({ [item.label]: navItemsToSingleKey(item.children, basePath) });
|
|
95
131
|
}
|
|
96
132
|
else if (item.href) {
|
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
|
@@ -213,10 +213,11 @@ program
|
|
|
213
213
|
.action((source, options) => migrateMkdocs(source, options));
|
|
214
214
|
program
|
|
215
215
|
.command("migrate-asciidoc")
|
|
216
|
-
.description("One-way migrate
|
|
216
|
+
.description("One-way migrate an AsciiDoc corpus to a scaffolded Dogsbay-MD " +
|
|
217
217
|
"project. Source-of-truth becomes ./content/*.md; the AsciiDoc source " +
|
|
218
|
-
"can be deleted.
|
|
219
|
-
"
|
|
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.")
|
|
220
221
|
.argument("<source>", "Path to a directory containing .adoc files")
|
|
221
222
|
.option("-o, --output <dir>", "Output directory (default: {source}-dogsbay)")
|
|
222
223
|
.option("--force", "Overwrite an existing Dogsbay site. Clean re-import: clears the " +
|
|
@@ -228,10 +229,14 @@ program
|
|
|
228
229
|
.option("-a, --attribute <name=value>", "AsciiDoc attribute (repeatable). Resolved refs/conditionals get " +
|
|
229
230
|
"baked into the migrated Dogsbay-MD; unresolved ones survive as " +
|
|
230
231
|
"{{ var }} / {% if %} for a downstream Minja pass.", (val, prev = []) => [...prev, val])
|
|
231
|
-
.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.")
|
|
232
234
|
.option("--distro <name>", "AsciiBinder distro filter (e.g. 'openshift-enterprise'). Topic-map " +
|
|
233
235
|
"entries whose Distros: list excludes this value are dropped from " +
|
|
234
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.")
|
|
235
240
|
.option("--local", "Use file: references to local monorepo packages (for development)")
|
|
236
241
|
.option("--quiet", "Suppress the 'Next steps' trailer")
|
|
237
242
|
.action((source, options) => migrateAsciidoc(source, options));
|
|
@@ -269,6 +274,11 @@ program
|
|
|
269
274
|
.option("--lang <locale>", "Locale to request from the portal (e.g. en-US)")
|
|
270
275
|
.option("--throttle <ms>", "Delay between khub requests (default 150)")
|
|
271
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)")
|
|
272
282
|
.option("--local", "Use file: references to local monorepo packages (for development)")
|
|
273
283
|
.option("--quiet", "Suppress the trailer")
|
|
274
284
|
.action((source, options) => migrateFluidtopicsRest(source, options));
|
|
@@ -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,22 +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-
|
|
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-fluidtopics-markdown": "0.2.0-beta.
|
|
46
|
-
"@dogsbay/format-
|
|
47
|
-
"@dogsbay/format-
|
|
48
|
-
"@dogsbay/adoc2md-modular": "0.2.0-beta.
|
|
49
|
-
"@dogsbay/format-asciidoc": "0.2.0-beta.
|
|
50
|
-
"@dogsbay/minja": "0.2.0-beta.
|
|
51
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
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"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/markdown-it": "^14.1.0",
|