dogsbay 0.2.0-beta.52 → 0.2.0-beta.54

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,11 +245,14 @@ 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
  }
@@ -263,6 +266,10 @@ function convertFragmentAdocs(sourceRoot, outputContentDir, scopedRootDirs) {
263
266
  basePath: srcPath,
264
267
  dlistFormat: "pandoc",
265
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,
266
273
  });
267
274
  mkdirSync(dstDir, { recursive: true });
268
275
  // No frontmatter — fragments are pure content. An earlier
@@ -459,6 +466,22 @@ export async function migrateAsciidoc(source, options) {
459
466
  process.exit(1);
460
467
  }
461
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;
462
485
  console.log();
463
486
  console.log(pc.bold(`Migrating AsciiDoc corpus to Dogsbay-MD: ${siteName}`));
464
487
  console.log(`Source: ${sourceDir}`);
@@ -467,6 +490,19 @@ export async function migrateAsciidoc(source, options) {
467
490
  // 1. Output skeleton.
468
491
  const contentDir = join(outputDir, "content");
469
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
+ }
470
506
  mkdirSync(contentDir, { recursive: true });
471
507
  mkdirSync(astroDir, { recursive: true });
472
508
  // 2. Import via the format-asciidoc plugin. Phase 9a moved corpus
@@ -478,7 +514,7 @@ export async function migrateAsciidoc(source, options) {
478
514
  throw new Error("format-asciidoc plugin has no import function");
479
515
  }
480
516
  const { pages: importedPages, nav } = await asciidocPlugin.import(sourceDir, {
481
- attributes,
517
+ attributes: engineAttributes,
482
518
  loader: options.loader,
483
519
  });
484
520
  if (importedPages.length === 0) {
@@ -503,12 +539,19 @@ export async function migrateAsciidoc(source, options) {
503
539
  // Fall back to treeToDogsbayMd for importers that don't produce
504
540
  // bodyMarkdown (e.g. mkdocs imports during a future
505
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();
506
548
  for (const page of importedPages) {
507
549
  const body = page.bodyMarkdown ?? treeToDogsbayMd(page.tree);
508
550
  const dst = join(contentDir, `${page.slug}.md`);
509
551
  mkdirSync(dirname(dst), { recursive: true });
510
552
  const frontmatter = `---\ntitle: ${yamlQuote(page.title)}\n---\n\n`;
511
553
  writeFileSync(dst, frontmatter + body);
554
+ pageWrittenPaths.add(dst);
512
555
  }
513
556
  console.log(pc.green(`Converted`) + ` ${importedPages.length} page(s) to ./content/`);
514
557
  // 3b. Compute the scoped set of root-level dirs we'll walk for
@@ -531,7 +574,11 @@ export async function migrateAsciidoc(source, options) {
531
574
  // skips routes for them via the generated config's
532
575
  // excludeFromRoutes. Adding them to scope just lets the walkers
533
576
  // descend; the route skip happens separately.
534
- 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"]) {
535
582
  scopedRootDirs.add(d);
536
583
  }
537
584
  // 3c. Fragment .adoc files (everything not topic-listed but in
@@ -542,7 +589,7 @@ export async function migrateAsciidoc(source, options) {
542
589
  // fragments. Fragment conversion is engine-only (no Minja,
543
590
  // no markdown-it) so directives inside fragments survive
544
591
  // verbatim and resolve in the parent page's context.
545
- const frag = convertFragmentAdocs(sourceDir, contentDir, scopedRootDirs);
592
+ const frag = convertFragmentAdocs(sourceDir, contentDir, scopedRootDirs, pageWrittenPaths, engineImagesdir);
546
593
  if (frag.converted > 0) {
547
594
  console.log(pc.green(`Converted`) +
548
595
  ` ${frag.converted} fragment(s)` +
@@ -563,6 +610,24 @@ export async function migrateAsciidoc(source, options) {
563
610
  parts.push(`${mirror.assets} asset(s)`);
564
611
  console.log(pc.green(`Mirrored`) + ` ${parts.join(" + ")}`);
565
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
+ }
566
631
  // 4. nav.yml — directly from the loader's NavItem[]. The loader
567
632
  // decides the shape (directory-mirror for plain, per-title
568
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.52",
3
+ "version": "0.2.0-beta.54",
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.52",
37
- "@dogsbay/format-mkdocs": "0.2.0-beta.52",
38
- "@dogsbay/format-astro": "0.2.0-beta.52",
39
- "@dogsbay/format-obsidian": "0.2.0-beta.52",
40
- "@dogsbay/format-mdx": "0.2.0-beta.52",
41
- "@dogsbay/format-starlight": "0.2.0-beta.52",
42
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.52",
43
- "@dogsbay/format-openapi": "0.2.0-beta.52",
44
- "@dogsbay/adoc2md-modular": "0.2.0-beta.52",
45
- "@dogsbay/format-asciidoc": "0.2.0-beta.52",
46
- "@dogsbay/minja": "0.2.0-beta.52",
47
- "@dogsbay/types": "0.2.0-beta.52"
36
+ "@dogsbay/autodoc-python": "0.2.0-beta.54",
37
+ "@dogsbay/format-mkdocs": "0.2.0-beta.54",
38
+ "@dogsbay/format-astro": "0.2.0-beta.54",
39
+ "@dogsbay/format-obsidian": "0.2.0-beta.54",
40
+ "@dogsbay/format-mdx": "0.2.0-beta.54",
41
+ "@dogsbay/format-starlight": "0.2.0-beta.54",
42
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.54",
43
+ "@dogsbay/format-openapi": "0.2.0-beta.54",
44
+ "@dogsbay/adoc2md-modular": "0.2.0-beta.54",
45
+ "@dogsbay/format-asciidoc": "0.2.0-beta.54",
46
+ "@dogsbay/minja": "0.2.0-beta.54",
47
+ "@dogsbay/types": "0.2.0-beta.54"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/markdown-it": "^14.1.0",