dogsbay 0.2.0-beta.76 → 0.2.0-beta.78

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.
@@ -5,7 +5,9 @@ import { plugin as astroPlugin } from "@dogsbay/format-astro/cli";
5
5
  import { plugin as obsidianPlugin } from "@dogsbay/format-obsidian/cli";
6
6
  import { plugin as mdxPlugin } from "@dogsbay/format-mdx/cli";
7
7
  import { plugin as starlightPlugin } from "@dogsbay/format-starlight/cli";
8
+ import { plugin as docusaurusPlugin } from "@dogsbay/format-docusaurus/cli";
8
9
  import { plugin as dogsbayMdPlugin } from "@dogsbay/format-dogsbay-md/cli";
10
+ import { tigeraAdapter } from "@dogsbay/docusaurus-adapter-tigera";
9
11
  // All registered format plugins
10
12
  const formats = [
11
13
  mkdocsPlugin,
@@ -13,8 +15,14 @@ const formats = [
13
15
  obsidianPlugin,
14
16
  mdxPlugin,
15
17
  starlightPlugin,
18
+ docusaurusPlugin,
16
19
  dogsbayMdPlugin,
17
20
  ];
21
+ // Docusaurus site adapters live in their own packages (the generic importer
22
+ // holds none). Resolve `--adapter <name>` here and pass the instance through.
23
+ const docusaurusAdapters = {
24
+ tigera: tigeraAdapter,
25
+ };
18
26
  /**
19
27
  * Auto-detect source format by checking each plugin's detectSource.
20
28
  */
@@ -77,6 +85,17 @@ export async function convert(source, options) {
77
85
  console.log(pc.red(`Error: ${exporter.name} export is not yet implemented.`));
78
86
  process.exit(1);
79
87
  }
88
+ // Resolve a Docusaurus site adapter (e.g. tigera) into an adapter object the
89
+ // generic importer can consume, keeping format-docusaurus free of site code.
90
+ if (importer.name === "docusaurus" && typeof options.adapter === "string") {
91
+ const adapter = docusaurusAdapters[options.adapter];
92
+ if (!adapter) {
93
+ console.log(pc.red(`Error: Unknown Docusaurus adapter "${options.adapter}"`));
94
+ console.log(`Available adapters: ${Object.keys(docusaurusAdapters).join(", ")}`);
95
+ process.exit(1);
96
+ }
97
+ options.adapterInstance = adapter;
98
+ }
80
99
  // Run the pipeline: import → export
81
100
  // The importer may attach metadata (siteName, repoUrl, etc.) to options
82
101
  // for the exporter to consume.
@@ -539,6 +539,7 @@ export async function migrateAsciidoc(source, options) {
539
539
  const { pages: importedPages, nav } = await asciidocPlugin.import(sourceDir, {
540
540
  attributes: engineAttributes,
541
541
  loader: options.loader,
542
+ distro: options.distro,
542
543
  });
543
544
  if (importedPages.length === 0) {
544
545
  console.log(pc.red(`Error: no .adoc files found under ${sourceDir}`));
@@ -329,6 +329,12 @@ export async function siteBuild(cwd, options) {
329
329
  // generalize this to copy assets per-source.
330
330
  const astroOpts = configToAstroOptions(config);
331
331
  astroOpts.sourceDir = resolvedSources[0];
332
+ // Asset mounts surfaced by importers (e.g. a Docusaurus `static/` dir) → copied
333
+ // into public/_assets by the astro emitter, matching the importer's rewritten
334
+ // `/_assets/...` refs.
335
+ if (importResult.assetMounts.length > 0) {
336
+ astroOpts.assetMounts = importResult.assetMounts;
337
+ }
332
338
  // siteRoot = repo root (where dogsbay.config.yml lives). Used by
333
339
  // emitDeployArtifacts to drop GH Actions workflows at
334
340
  // <siteRoot>/.github/ rather than inside the Astro subdir, where
@@ -523,6 +523,15 @@ function buildConfig(opts) {
523
523
  ...(opts.from && opts.from !== "auto"
524
524
  ? { from: opts.from }
525
525
  : { from: "auto" }),
526
+ // Docusaurus instance/adapter selection, nested under its format name.
527
+ ...(opts.from === "docusaurus" && (opts.instance?.trim() || opts.adapter?.trim())
528
+ ? {
529
+ docusaurus: {
530
+ ...(opts.instance?.trim() ? { instance: opts.instance.trim() } : {}),
531
+ ...(opts.adapter?.trim() ? { adapter: opts.adapter.trim() } : {}),
532
+ },
533
+ }
534
+ : {}),
526
535
  ...(opts.nav?.trim() ? { nav: opts.nav.trim() } : {}),
527
536
  };
528
537
  const content = {
@@ -356,6 +356,7 @@ const VALID_FROM = [
356
356
  "obsidian",
357
357
  "starlight",
358
358
  "mdx",
359
+ "docusaurus",
359
360
  "openapi",
360
361
  "asciidoc",
361
362
  ];
@@ -644,6 +645,18 @@ function validateSource(raw, index, sourcePath) {
644
645
  throw new Error(`${at}.mkdocs must be an object in ${sourcePath}`);
645
646
  }
646
647
  }
648
+ if (s.docusaurus !== undefined) {
649
+ if (typeof s.docusaurus !== "object" || s.docusaurus === null || Array.isArray(s.docusaurus)) {
650
+ throw new Error(`${at}.docusaurus must be an object in ${sourcePath}`);
651
+ }
652
+ const d = s.docusaurus;
653
+ if (d.instance !== undefined && typeof d.instance !== "string") {
654
+ throw new Error(`${at}.docusaurus.instance must be a string in ${sourcePath}`);
655
+ }
656
+ if (d.adapter !== undefined && typeof d.adapter !== "string") {
657
+ throw new Error(`${at}.docusaurus.adapter must be a string in ${sourcePath}`);
658
+ }
659
+ }
647
660
  if (s.primary !== undefined && typeof s.primary !== "boolean") {
648
661
  throw new Error(`${at}.primary must be a boolean in ${sourcePath}; got ${describe(s.primary)}`);
649
662
  }
@@ -677,6 +690,7 @@ function validateSource(raw, index, sourcePath) {
677
690
  nav: s.nav,
678
691
  subdir: s.subdir,
679
692
  mkdocs: s.mkdocs,
693
+ docusaurus: s.docusaurus,
680
694
  primary: s.primary,
681
695
  attributes,
682
696
  };
@@ -4,19 +4,27 @@ import { plugin as astroPlugin } from "@dogsbay/format-astro/cli";
4
4
  import { plugin as obsidianPlugin } from "@dogsbay/format-obsidian/cli";
5
5
  import { plugin as mdxPlugin } from "@dogsbay/format-mdx/cli";
6
6
  import { plugin as starlightPlugin } from "@dogsbay/format-starlight/cli";
7
+ import { plugin as docusaurusPlugin } from "@dogsbay/format-docusaurus/cli";
7
8
  import { plugin as dogsbayMdPlugin } from "@dogsbay/format-dogsbay-md/cli";
8
9
  import { plugin as openApiPlugin } from "@dogsbay/format-openapi/cli";
9
10
  import { plugin as asciidocPlugin } from "@dogsbay/format-asciidoc/cli";
11
+ import { tigeraAdapter } from "@dogsbay/docusaurus-adapter-tigera";
10
12
  const FORMATS = [
11
13
  mkdocsPlugin,
12
14
  astroPlugin,
13
15
  obsidianPlugin,
14
16
  mdxPlugin,
15
17
  starlightPlugin,
18
+ docusaurusPlugin,
16
19
  dogsbayMdPlugin,
17
20
  openApiPlugin,
18
21
  asciidocPlugin,
19
22
  ];
23
+ // Docusaurus site adapters live in their own packages (the generic importer
24
+ // holds none). Resolved by name from the source's `docusaurus.adapter`.
25
+ const DOCUSAURUS_ADAPTERS = {
26
+ tigera: tigeraAdapter,
27
+ };
20
28
  /**
21
29
  * Effective namespace key for a source. Returns `undefined` when
22
30
  * the source has no explicit `name:` — the source's URLs stay
@@ -202,6 +210,7 @@ export async function importContent(resolvedSources, config, options = {}) {
202
210
  const basePath = normalizeBasePath(config.site.basePath);
203
211
  const allPages = [];
204
212
  const allNav = [];
213
+ const allAssetMounts = [];
205
214
  let firstImporter;
206
215
  for (let i = 0; i < sources.length; i++) {
207
216
  const sourceCfg = sources[i];
@@ -214,6 +223,11 @@ export async function importContent(resolvedSources, config, options = {}) {
214
223
  firstImporter = importer;
215
224
  const opts = buildImportOptions(config, sourceCfg, options);
216
225
  const { pages, nav } = await importer.import(resolved, opts);
226
+ // Importers may attach asset mounts (e.g. Docusaurus signals its `static/`
227
+ // dir) for the exporter to copy into `_assets`.
228
+ if (Array.isArray(opts.assetMounts)) {
229
+ allAssetMounts.push(...opts.assetMounts);
230
+ }
217
231
  const anyAxisActive = axes.version || axes.namespace || axes.locale;
218
232
  const prefixSegs = getPrefixSegments(sourceCfg, axes, defaults);
219
233
  // Per-page axis metadata is stamped whenever ANY axis is
@@ -279,7 +293,7 @@ export async function importContent(resolvedSources, config, options = {}) {
279
293
  allNav.push(...nav);
280
294
  }
281
295
  }
282
- return { pages: allPages, nav: allNav, importer: firstImporter };
296
+ return { pages: allPages, nav: allNav, importer: firstImporter, assetMounts: allAssetMounts };
283
297
  }
284
298
  function prefixSlugWithSegments(prefix, slug) {
285
299
  if (prefix.length === 0)
@@ -380,6 +394,18 @@ function buildImportOptions(config, source, extra = {}) {
380
394
  if (source.mkdocs?.docusaurusAdmonitions) {
381
395
  opts.docusaurusAdmonitions = true;
382
396
  }
397
+ // Docusaurus-specific: instance selection + adapter resolution.
398
+ if (source.docusaurus?.instance) {
399
+ opts.instance = source.docusaurus.instance;
400
+ }
401
+ if (source.docusaurus?.adapter) {
402
+ const adapter = DOCUSAURUS_ADAPTERS[source.docusaurus.adapter];
403
+ if (!adapter) {
404
+ throw new Error(`Unknown Docusaurus adapter "${source.docusaurus.adapter}". ` +
405
+ `Available: ${Object.keys(DOCUSAURUS_ADAPTERS).join(", ")}`);
406
+ }
407
+ opts.adapterInstance = adapter;
408
+ }
383
409
  // Custom taxonomy names — importers pass these to parseMeta so
384
410
  // user-declared taxonomy keys in frontmatter get lifted into
385
411
  // meta.taxonomies. See plans/metadata-and-taxonomies.md Step 3.
package/dist/index.js CHANGED
@@ -67,7 +67,9 @@ site
67
67
  .option("--theme <name>", "Theme preset (default | material)")
68
68
  .option("--deploy <target>", "Deploy target (cloudflare-workers | github-pages)")
69
69
  .option("--content <path>", "Path to source markdown (relative to config)")
70
- .option("--from <format>", "Source format (auto | dogsbay-md | mkdocs | obsidian | starlight | mdx)")
70
+ .option("--from <format>", "Source format (auto | dogsbay-md | mkdocs | obsidian | starlight | mdx | docusaurus)")
71
+ .option("--instance <id>", "Docusaurus instance id (with --from docusaurus), e.g. calico")
72
+ .option("--adapter <name>", "Docusaurus site adapter (with --from docusaurus), e.g. tigera")
71
73
  .option("--nav <path>", "Path to explicit nav file (.json/.yml)")
72
74
  .option("--plausible-domain <domain>", "Plausible analytics domain")
73
75
  .option("--plausible-script-url <url>", "Override Plausible script URL")
@@ -225,6 +227,9 @@ program
225
227
  "baked into the migrated Dogsbay-MD; unresolved ones survive as " +
226
228
  "{{ var }} / {% if %} for a downstream Minja pass.", (val, prev = []) => [...prev, val])
227
229
  .option("--loader <name>", "Force a corpus loader (plain, master-adoc). Auto-detected when omitted.")
230
+ .option("--distro <name>", "AsciiBinder distro filter (e.g. 'openshift-enterprise'). Topic-map " +
231
+ "entries whose Distros: list excludes this value are dropped from " +
232
+ "the page set and nav. Entries without Distros: are kept as universal.")
228
233
  .option("--local", "Use file: references to local monorepo packages (for development)")
229
234
  .option("--quiet", "Suppress the 'Next steps' trailer")
230
235
  .action((source, options) => migrateAsciidoc(source, options));
@@ -265,7 +270,9 @@ program
265
270
  .option("--plugin <mode>", "Obsidian plugin: bundled (copy into vault), registry (list only), none", "bundled")
266
271
  .option("--optimize-images", "Enable Astro image optimization (WebP, width/height, lazy loading)")
267
272
  .option("--code-titles <mode>", "Code block title bar: always, auto (only with explicit title), never", "always")
268
- .option("--adapter <name>", "Site adapter for custom components (e.g. cloudflare)")
273
+ .option("--adapter <name>", "Site adapter for custom components (e.g. cloudflare, tigera)")
274
+ .option("--instance <id>", "Docusaurus plugin-content-docs instance id to import (e.g. calico)")
275
+ .option("--partial-mode <mode>", "Docusaurus partial handling: inline (splice in) or include (emit {% include %} + fragments; requires --to dogsbay-md)", "inline")
269
276
  .option("--section <name>", "Section name — pages go under this subfolder, nav gets a top-level entry")
270
277
  .option("--partials-dir <path>", "Directory containing partial MDX files for <Render> resolution")
271
278
  .option("--nav <path>", "Path to an explicit nav file (.json/.yml/.yaml). Overrides heuristic auto-detection in the content root.")
@@ -40,6 +40,61 @@ import { copyFileSync, linkSync, lstatSync, mkdirSync, readFileSync, readdirSync
40
40
  import { dirname, join } from "node:path";
41
41
  import matter from "gray-matter";
42
42
  import { render as minjaRender, FileSystemLoader, } from "@dogsbay/minja";
43
+ /**
44
+ * Sentinels that mask Minja openers before parsing, restored verbatim after
45
+ * rendering. Module-scoped so the page renderer AND the fragment loader share
46
+ * them, and so all masks are undone in one place.
47
+ *
48
+ * - HASH: `{#id}` heading anchors (markdown-it-attrs) — masked GLOBALLY, since
49
+ * Minja reads `{#` as a comment opener and aborts on the missing `#}`.
50
+ * - INTERP / BLOCK: `{{ … }}` / `{% … %}` that appear INSIDE code (fenced blocks
51
+ * and inline spans) — literal template examples (Go/kubectl/Helm:
52
+ * `{{range .Items}}`, `{% … %}`) that must never be evaluated. Masked only in
53
+ * code regions; outside code these still process (that's the point).
54
+ *
55
+ * This is the single, format-agnostic guard: every importer's output funnels
56
+ * through this build pass, so code examples are protected regardless of source.
57
+ */
58
+ const HASH_SENTINEL = " DBMINJA_HASH_OPEN ";
59
+ const INTERP_SENTINEL = " DBMINJA_INTERP_OPEN ";
60
+ const BLOCK_SENTINEL = " DBMINJA_BLOCK_OPEN ";
61
+ /** Mask `{{`/`{%` openers inside fenced code blocks and inline code spans. */
62
+ function maskCodeRegions(text) {
63
+ const maskOpeners = (s) => s.replace(/\{\{/g, INTERP_SENTINEL).replace(/\{%/g, BLOCK_SENTINEL);
64
+ const lines = text.split("\n");
65
+ let inFence = false;
66
+ let fenceMarker = "";
67
+ return lines
68
+ .map((line) => {
69
+ const fence = line.match(/^\s*(```+|~~~+)/);
70
+ if (fence) {
71
+ const marker = fence[1][0];
72
+ if (!inFence) {
73
+ inFence = true;
74
+ fenceMarker = marker;
75
+ }
76
+ else if (marker === fenceMarker) {
77
+ inFence = false;
78
+ }
79
+ return line; // the fence line itself (info string) — leave as-is
80
+ }
81
+ if (inFence)
82
+ return maskOpeners(line);
83
+ // Outside a fence: mask only within inline code spans (balanced backticks).
84
+ return line.replace(/(`+)([^`]*?)\1/g, (_m, ticks, inner) => ticks + maskOpeners(inner) + ticks);
85
+ })
86
+ .join("\n");
87
+ }
88
+ /** Restore every Minja-opener sentinel. */
89
+ function unmaskMinja(text) {
90
+ return text
91
+ .split(HASH_SENTINEL)
92
+ .join("#")
93
+ .split(INTERP_SENTINEL)
94
+ .join("{{")
95
+ .split(BLOCK_SENTINEL)
96
+ .join("{%");
97
+ }
43
98
  /**
44
99
  * Merge the three attribute layers into the effective per-source
45
100
  * context. Right-hand wins for duplicate keys (per-source overrides
@@ -102,7 +157,13 @@ class FragmentStrippingLoader {
102
157
  inner = new FileSystemLoader();
103
158
  async load(path, basePath) {
104
159
  const raw = await this.inner.load(path, basePath);
105
- return path.toLowerCase().endsWith(".md") ? stripFrontmatter(raw) : raw;
160
+ if (!path.toLowerCase().endsWith(".md"))
161
+ return raw;
162
+ // Apply the same masks to included fragments — code-region `{{`/`{%` and
163
+ // `{#id}` heading anchors — so Minja doesn't evaluate template examples or
164
+ // read anchors as comment openers and abort the include. All sentinels are
165
+ // restored together with the parent's after rendering (unmaskMinja).
166
+ return maskCodeRegions(stripFrontmatter(raw)).replace(/\{#/g, `{${HASH_SENTINEL}`);
106
167
  }
107
168
  }
108
169
  /**
@@ -165,8 +226,8 @@ async function renderMdToMirror(sourcePath, mirrorPath, attrs, result) {
165
226
  // when it doesn't find a `#}`. Mask the opener before parse, restore
166
227
  // after. Side effect: genuine Minja `{# comment #}` blocks pass
167
228
  // through as literal text (rare in our pipeline).
168
- const SENTINEL = " DBMINJA_HASH_OPEN ";
169
- const masked = raw.replace(/\{#/g, `{${SENTINEL}`);
229
+ // Protect literal template syntax in code examples, then mask heading anchors.
230
+ const masked = maskCodeRegions(raw).replace(/\{#/g, `{${HASH_SENTINEL}`);
170
231
  let resolved;
171
232
  try {
172
233
  const rendered = await minjaRender(masked, {
@@ -183,7 +244,7 @@ async function renderMdToMirror(sourcePath, mirrorPath, attrs, result) {
183
244
  basePath: dirname(sourcePath),
184
245
  loader: new FragmentStrippingLoader(),
185
246
  });
186
- resolved = rendered.split(SENTINEL).join("#");
247
+ resolved = unmaskMinja(rendered);
187
248
  }
188
249
  catch (err) {
189
250
  const msg = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dogsbay",
3
- "version": "0.2.0-beta.76",
3
+ "version": "0.2.0-beta.78",
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,18 +33,20 @@
33
33
  "picocolors": "^1.1.0",
34
34
  "prompts": "^2.4.2",
35
35
  "yaml": "^2.8.3",
36
- "@dogsbay/autodoc-python": "0.2.0-beta.76",
37
- "@dogsbay/format-mkdocs": "0.2.0-beta.76",
38
- "@dogsbay/format-astro": "0.2.0-beta.76",
39
- "@dogsbay/format-mdx": "0.2.0-beta.76",
40
- "@dogsbay/format-obsidian": "0.2.0-beta.76",
41
- "@dogsbay/format-starlight": "0.2.0-beta.76",
42
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.76",
43
- "@dogsbay/format-openapi": "0.2.0-beta.76",
44
- "@dogsbay/adoc2md-modular": "0.2.0-beta.76",
45
- "@dogsbay/format-asciidoc": "0.2.0-beta.76",
46
- "@dogsbay/minja": "0.2.0-beta.76",
47
- "@dogsbay/types": "0.2.0-beta.76"
36
+ "@dogsbay/autodoc-python": "0.2.0-beta.78",
37
+ "@dogsbay/format-mkdocs": "0.2.0-beta.78",
38
+ "@dogsbay/format-astro": "0.2.0-beta.78",
39
+ "@dogsbay/format-obsidian": "0.2.0-beta.78",
40
+ "@dogsbay/format-starlight": "0.2.0-beta.78",
41
+ "@dogsbay/format-mdx": "0.2.0-beta.78",
42
+ "@dogsbay/format-docusaurus": "0.2.0-beta.78",
43
+ "@dogsbay/docusaurus-adapter-tigera": "0.2.0-beta.78",
44
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.78",
45
+ "@dogsbay/format-openapi": "0.2.0-beta.78",
46
+ "@dogsbay/adoc2md-modular": "0.2.0-beta.78",
47
+ "@dogsbay/format-asciidoc": "0.2.0-beta.78",
48
+ "@dogsbay/minja": "0.2.0-beta.78",
49
+ "@dogsbay/types": "0.2.0-beta.78"
48
50
  },
49
51
  "devDependencies": {
50
52
  "@types/markdown-it": "^14.1.0",