dogsbay 0.2.0-beta.51 → 0.2.0-beta.53

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.
@@ -25,7 +25,7 @@
25
25
  * astro/ ← scaffolded by emitSiteScaffold
26
26
  * MIGRATION.md ← what survived + re-run command
27
27
  */
28
- import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
28
+ import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
29
29
  import { basename, dirname, join, relative, resolve } from "node:path";
30
30
  import pc from "picocolors";
31
31
  import { emitSiteScaffold } from "@dogsbay/format-astro";
@@ -207,7 +207,7 @@ function mirrorSourceFs(sourceRoot, outputContentDir, scopedRootDirs) {
207
207
  *
208
208
  * Returns counts for the summary.
209
209
  */
210
- function convertFragmentAdocs(sourceRoot, outputContentDir, scopedRootDirs) {
210
+ function convertFragmentAdocs(sourceRoot, outputContentDir, scopedRootDirs, pageWrittenPaths, imagesdir) {
211
211
  let converted = 0;
212
212
  let failed = 0;
213
213
  let skipped = 0;
@@ -245,17 +245,32 @@ function convertFragmentAdocs(sourceRoot, outputContentDir, scopedRootDirs) {
245
245
  continue;
246
246
  const mdName = name.replace(/\.adoc$/i, ".md");
247
247
  const mdPath = join(dstDir, mdName);
248
- // A topic page already wrote this destination — don't
249
- // overwrite with the engine-only fragment output (the page
250
- // version has frontmatter, headings, and went through
251
- // markdown-it + serialize for canonical Dogsbay-MD shape).
252
- if (existsSync(mdPath)) {
248
+ // A topic page wrote this destination THIS run — don't overwrite
249
+ // it with the engine-only fragment output (the page version has
250
+ // frontmatter, headings, and went through markdown-it + serialize
251
+ // for canonical Dogsbay-MD shape). We check the known set of
252
+ // this-run page writes, NOT existsSync: a stale .md left by a
253
+ // prior --force run must be re-converted, not mistaken for a page
254
+ // and skipped (#364).
255
+ if (pageWrittenPaths.has(mdPath)) {
253
256
  skipped += 1;
254
257
  continue;
255
258
  }
256
259
  try {
257
260
  const content = readFileSync(srcPath, "utf-8");
258
- const md = asciidocToMarkdown(content, { basePath: srcPath });
261
+ // Same Dogsbay house defaults as the topic-page path
262
+ // (format-asciidoc parse.ts): 'pandoc' dlists + 'docusaurus'
263
+ // admonitions — the shapes Dogsbay's renderer renders natively.
264
+ // Fragments are included into pages, so their shape must match.
265
+ const md = asciidocToMarkdown(content, {
266
+ basePath: srcPath,
267
+ dlistFormat: "pandoc",
268
+ admonitionFormat: "docusaurus",
269
+ // imagesdir = "/_assets/<dir>" makes the engine emit image
270
+ // srcs already content-rooted (/_assets/images/foo.png), so
271
+ // they resolve regardless of fragment depth. See #363.
272
+ attributes: imagesdir ? { imagesdir } : undefined,
273
+ });
259
274
  mkdirSync(dstDir, { recursive: true });
260
275
  // No frontmatter — fragments are pure content. An earlier
261
276
  // attempt stamped `_fragment: true` here as a loader-driven
@@ -451,6 +466,22 @@ export async function migrateAsciidoc(source, options) {
451
466
  process.exit(1);
452
467
  }
453
468
  const attributes = parseAttributePairs(options.attribute);
469
+ // Image handling (#363). AsciiDoc keeps images in an `:imagesdir:`
470
+ // (OpenShift / AsciiBinder / Antora convention: a top-level `images/`
471
+ // dir). The engine's image() prepends imagesdir to every src, so
472
+ // supplying `imagesdir = "/_assets/<dir>"` makes refs emit
473
+ // already-rooted (`/_assets/images/foo.png`) — matching the Dogsbay
474
+ // `_assets` convention (docs/images.md). We copy the dir into
475
+ // content/_assets/<dir>/ below. Detected, NOT persisted to config
476
+ // (a migrate-time rendering concern, not a content attribute).
477
+ const srcImagesDir = existsSync(join(sourceDir, "images")) &&
478
+ statSync(join(sourceDir, "images")).isDirectory()
479
+ ? "images"
480
+ : undefined;
481
+ const engineImagesdir = srcImagesDir ? `/_assets/${srcImagesDir}` : undefined;
482
+ const engineAttributes = engineImagesdir
483
+ ? { ...attributes, imagesdir: engineImagesdir }
484
+ : attributes;
454
485
  console.log();
455
486
  console.log(pc.bold(`Migrating AsciiDoc corpus to Dogsbay-MD: ${siteName}`));
456
487
  console.log(`Source: ${sourceDir}`);
@@ -459,6 +490,19 @@ export async function migrateAsciidoc(source, options) {
459
490
  // 1. Output skeleton.
460
491
  const contentDir = join(outputDir, "content");
461
492
  const astroDir = join(outputDir, "astro");
493
+ // Clean slate for the regenerated content tree (#364). We only reach
494
+ // here on a fresh output or with --force (the existing-site guard
495
+ // above already exited otherwise), so a pre-existing content/ is a
496
+ // prior migration's output. Clear it so stale files don't linger
497
+ // across re-migrations — renamed/removed dirs (e.g. content/images/
498
+ // after #363 moved images to _assets) and orphaned pages. content/ is
499
+ // entirely migration-generated; --force is a clean re-import and
500
+ // discards any hand edits under content/ (see the --force help text).
501
+ // astro/ is left intact: emitSiteScaffold overwrites its files in
502
+ // place, and clearing it would drop a warm node_modules.
503
+ if (existsSync(contentDir)) {
504
+ rmSync(contentDir, { recursive: true, force: true });
505
+ }
462
506
  mkdirSync(contentDir, { recursive: true });
463
507
  mkdirSync(astroDir, { recursive: true });
464
508
  // 2. Import via the format-asciidoc plugin. Phase 9a moved corpus
@@ -470,7 +514,7 @@ export async function migrateAsciidoc(source, options) {
470
514
  throw new Error("format-asciidoc plugin has no import function");
471
515
  }
472
516
  const { pages: importedPages, nav } = await asciidocPlugin.import(sourceDir, {
473
- attributes,
517
+ attributes: engineAttributes,
474
518
  loader: options.loader,
475
519
  });
476
520
  if (importedPages.length === 0) {
@@ -495,12 +539,19 @@ export async function migrateAsciidoc(source, options) {
495
539
  // Fall back to treeToDogsbayMd for importers that don't produce
496
540
  // bodyMarkdown (e.g. mkdocs imports during a future
497
541
  // dual-format migration). No behaviour change for those.
542
+ // Track the .md paths the page step wrote THIS run. The fragment
543
+ // converter skips these so it can't clobber a topic page with its
544
+ // headerless engine-only output. Using a known set (not existsSync)
545
+ // means a stale .md left over from a prior --force run is NOT mistaken
546
+ // for a this-run page — it gets re-converted. See #364.
547
+ const pageWrittenPaths = new Set();
498
548
  for (const page of importedPages) {
499
549
  const body = page.bodyMarkdown ?? treeToDogsbayMd(page.tree);
500
550
  const dst = join(contentDir, `${page.slug}.md`);
501
551
  mkdirSync(dirname(dst), { recursive: true });
502
552
  const frontmatter = `---\ntitle: ${yamlQuote(page.title)}\n---\n\n`;
503
553
  writeFileSync(dst, frontmatter + body);
554
+ pageWrittenPaths.add(dst);
504
555
  }
505
556
  console.log(pc.green(`Converted`) + ` ${importedPages.length} page(s) to ./content/`);
506
557
  // 3b. Compute the scoped set of root-level dirs we'll walk for
@@ -523,7 +574,11 @@ export async function migrateAsciidoc(source, options) {
523
574
  // skips routes for them via the generated config's
524
575
  // excludeFromRoutes. Adding them to scope just lets the walkers
525
576
  // descend; the route skip happens separately.
526
- for (const d of ["_attributes", "modules", "snippets", "includes", "images"]) {
577
+ // Note: the imagesdir (`images`) is intentionally NOT here — it's
578
+ // copied to content/_assets/<dir>/ below and referenced as
579
+ // /_assets/<dir>/... (#363), so the generic mirror must not also
580
+ // copy it to content/images/ (dead weight; refs don't point there).
581
+ for (const d of ["_attributes", "modules", "snippets", "includes"]) {
527
582
  scopedRootDirs.add(d);
528
583
  }
529
584
  // 3c. Fragment .adoc files (everything not topic-listed but in
@@ -534,7 +589,7 @@ export async function migrateAsciidoc(source, options) {
534
589
  // fragments. Fragment conversion is engine-only (no Minja,
535
590
  // no markdown-it) so directives inside fragments survive
536
591
  // verbatim and resolve in the parent page's context.
537
- const frag = convertFragmentAdocs(sourceDir, contentDir, scopedRootDirs);
592
+ const frag = convertFragmentAdocs(sourceDir, contentDir, scopedRootDirs, pageWrittenPaths, engineImagesdir);
538
593
  if (frag.converted > 0) {
539
594
  console.log(pc.green(`Converted`) +
540
595
  ` ${frag.converted} fragment(s)` +
@@ -555,6 +610,24 @@ export async function migrateAsciidoc(source, options) {
555
610
  parts.push(`${mirror.assets} asset(s)`);
556
611
  console.log(pc.green(`Mirrored`) + ` ${parts.join(" + ")}`);
557
612
  }
613
+ // 3e. Copy the imagesdir tree into content/_assets/<dir>/ so the
614
+ // rooted image refs (/_assets/<dir>/foo.png, emitted by the
615
+ // engine via the supplied imagesdir) resolve. format-astro's
616
+ // copyAssets serves content/_assets/** at /_assets/** on every
617
+ // build. (#363)
618
+ if (srcImagesDir) {
619
+ const from = join(sourceDir, srcImagesDir);
620
+ const to = join(contentDir, "_assets", srcImagesDir);
621
+ try {
622
+ mkdirSync(dirname(to), { recursive: true });
623
+ cpSync(from, to, { recursive: true });
624
+ console.log(pc.green(`Copied`) + ` images → content/_assets/${srcImagesDir}/`);
625
+ }
626
+ catch (err) {
627
+ const msg = err instanceof Error ? err.message : String(err);
628
+ console.warn(`images: could not copy ${from} → ${to}: ${msg}`);
629
+ }
630
+ }
558
631
  // 4. nav.yml — directly from the loader's NavItem[]. The loader
559
632
  // decides the shape (directory-mirror for plain, per-title
560
633
  // grouping for master.adoc, etc.). We just serialize.
package/dist/index.js CHANGED
@@ -212,7 +212,8 @@ program
212
212
  "Antora, and AAP master.adoc loaders are planned for a later release.")
213
213
  .argument("<source>", "Path to a directory containing .adoc files")
214
214
  .option("-o, --output <dir>", "Output directory (default: {source}-dogsbay)")
215
- .option("--force", "Overwrite an existing Dogsbay site at the output dir")
215
+ .option("--force", "Overwrite an existing Dogsbay site. Clean re-import: clears the " +
216
+ "content/ tree first, discarding any edits under it (astro/ kept).")
216
217
  .option("--site-name <name>", "Site name written into dogsbay.config.yml")
217
218
  .option("--site-url <url>", "Canonical URL of the destination site (e.g. " +
218
219
  "https://you.github.io/repo). Drives robots.txt, sitemap, " +
@@ -57,6 +57,21 @@ export function mergeAttributes(siteWide, perSource, cliOverrides) {
57
57
  ...(perSource ?? {}),
58
58
  ...(cliOverrides ?? {}),
59
59
  };
60
+ // AsciiDoc attribute names are hyphenated by convention (product-title,
61
+ // openshift-enterprise), but the engine emits underscored Minja vars
62
+ // ({{ product_title }}) and minja's context lookup is keyed on the
63
+ // underscored form (hyphenToUnderscore normalizes the *lookup*, not the
64
+ // stored keys). So a supplied `product-title` would never match. Add an
65
+ // underscored alias for every hyphenated key so users can pass either
66
+ // form. The original is kept; explicit underscored keys win over an
67
+ // alias derived from a hyphenated one.
68
+ for (const [key, value] of Object.entries({ ...merged })) {
69
+ if (key.includes("-")) {
70
+ const underscored = key.replace(/-/g, "_");
71
+ if (!(underscored in merged))
72
+ merged[underscored] = value;
73
+ }
74
+ }
60
75
  return Object.keys(merged).length > 0 ? merged : undefined;
61
76
  }
62
77
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dogsbay",
3
- "version": "0.2.0-beta.51",
3
+ "version": "0.2.0-beta.53",
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,18 @@
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.51",
37
- "@dogsbay/format-astro": "0.2.0-beta.51",
38
- "@dogsbay/format-mkdocs": "0.2.0-beta.51",
39
- "@dogsbay/format-obsidian": "0.2.0-beta.51",
40
- "@dogsbay/format-mdx": "0.2.0-beta.51",
41
- "@dogsbay/format-starlight": "0.2.0-beta.51",
42
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.51",
43
- "@dogsbay/format-openapi": "0.2.0-beta.51",
44
- "@dogsbay/format-asciidoc": "0.2.0-beta.51",
45
- "@dogsbay/adoc2md-modular": "0.2.0-beta.51",
46
- "@dogsbay/types": "0.2.0-beta.51",
47
- "@dogsbay/minja": "0.2.0-beta.51"
36
+ "@dogsbay/autodoc-python": "0.2.0-beta.53",
37
+ "@dogsbay/format-mkdocs": "0.2.0-beta.53",
38
+ "@dogsbay/format-astro": "0.2.0-beta.53",
39
+ "@dogsbay/format-obsidian": "0.2.0-beta.53",
40
+ "@dogsbay/format-mdx": "0.2.0-beta.53",
41
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.53",
42
+ "@dogsbay/format-starlight": "0.2.0-beta.53",
43
+ "@dogsbay/format-openapi": "0.2.0-beta.53",
44
+ "@dogsbay/adoc2md-modular": "0.2.0-beta.53",
45
+ "@dogsbay/format-asciidoc": "0.2.0-beta.53",
46
+ "@dogsbay/minja": "0.2.0-beta.53",
47
+ "@dogsbay/types": "0.2.0-beta.53"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/markdown-it": "^14.1.0",