dogsbay 0.2.0-beta.119 → 0.2.0-beta.120

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.
@@ -1,4 +1,5 @@
1
1
  import type { FormatPlugin, ExportPage, NavItem } from "@dogsbay/types";
2
+ import { isNamespaceAxisActive, rewriteContentRefs } from "@dogsbay/site-files";
2
3
  import type { DogsbayConfig, SourceConfig } from "./config/index.js";
3
4
  /**
4
5
  * Effective version label for a source. Resolution chain:
@@ -16,17 +17,7 @@ import type { DogsbayConfig, SourceConfig } from "./config/index.js";
16
17
  * URLs in another.
17
18
  */
18
19
  export declare function effectiveVersion(s: SourceConfig, defaultVersion?: string): string | undefined;
19
- /**
20
- * Whether the namespace axis is "active" — i.e. ≥2 distinct
21
- * effective namespaces appear across the configured sources.
22
- * `undefined` (an unnamed source) counts as a distinct bucket,
23
- * so an unnamed + named pair activates the axis: the unnamed
24
- * source emits flat URLs, the named one gets `/<name>/<slug>`.
25
- *
26
- * See plans/multi-source-content.md → "Detecting which axes are
27
- * active".
28
- */
29
- export declare function isNamespaceAxisActive(sources: SourceConfig[]): boolean;
20
+ export { isNamespaceAxisActive };
30
21
  /**
31
22
  * Whether the version axis is "active" — i.e. ≥2 distinct
32
23
  * effective versions appear across the configured sources.
@@ -214,24 +205,7 @@ export declare function importContent(resolvedSources: string[], config: Dogsbay
214
205
  * naming the page and the version.
215
206
  */
216
207
  export declare function checkRootVersionCollisions(pages: ExportPage[], rootVersion: string, versionIds: (string | undefined)[]): void;
217
- /**
218
- * Prefix INTERNAL-ABSOLUTE refs in a page tree with the source's URL
219
- * segments, matching the slug rewrite — so a versioned/localised page's
220
- * links and images resolve under the same `/<segs>/...` prefix as the
221
- * pages themselves. Covers page links (`href`) and asset refs (`src`),
222
- * in both markdown nodes (`src`/`href` fields) and raw-HTML nodes
223
- * (`href="..."`/`src="..."` inside an `html` string).
224
- *
225
- * Refs at this pipeline stage are ROOT-RELATIVE and carry NO basePath
226
- * (importers resolve to `/foo`; basePath is composed downstream), so the
227
- * match anchor is `/`, not `<basePath>/`. A ref is prefixed iff it is an
228
- * internal absolute path (starts with `/`), and NOT: external
229
- * (`scheme:`, `//host`), an anchor (`#`), the site home (`/`), under a
230
- * GLOBAL prefix (taxonomy index paths — one `/tags/` for the whole site,
231
- * never version-bucketed), or ALREADY carrying this source's full
232
- * `/<segs>/` prefix (idempotent). Relative refs are left untouched.
233
- */
234
- export declare function rewriteContentRefs(tree: unknown, prefixSegs: string[], globalPrefixes: Set<string>): void;
208
+ export { rewriteContentRefs };
235
209
  /**
236
210
  * Build the options bag passed to the format plugin's import method.
237
211
  * The shape is intentionally loose (`Record<string, unknown>`) because
@@ -11,20 +11,13 @@
11
11
  */
12
12
  import { isAbsolute, join, resolve } from "node:path";
13
13
  import { normalizeBasePath, resolvePrefixes } from "@dogsbay/format-astro";
14
+ import { isNamespaceAxisActive, namespaceNavGroup, prefixNavHrefs, prefixSlug, rewriteContentRefs, sourceNamespace, } from "@dogsbay/site-files";
14
15
  // THE registry — shared with `dogsbay convert` (commands/convert.ts). This
15
16
  // module used to keep its own copy in a different order, so `site build` and
16
17
  // `convert` disagreed about the same directory. See formats.ts.
17
18
  import { FORMATS, DOCUSAURUS_ADAPTERS, loadAdapter, MDX_ADAPTERS, detectFormat, importerNames, } from "./formats.js";
18
- /**
19
- * Effective namespace key for a source. Returns `undefined` when
20
- * the source has no explicit `name:` — the source's URLs stay
21
- * flat (no namespace segment) even when paired with named
22
- * siblings. Avoids surprising "I added one namespace and now
23
- * everything else got a /docs/ prefix" rewrites.
24
- */
25
- function effectiveNamespace(s) {
26
- return s.name;
27
- }
19
+ /** A source's namespace (its `name:`); the rule lives in @dogsbay/site-files. */
20
+ const effectiveNamespace = sourceNamespace;
28
21
  /**
29
22
  * Effective version label for a source. Resolution chain:
30
23
  * 1. `source.version`
@@ -43,24 +36,9 @@ function effectiveNamespace(s) {
43
36
  export function effectiveVersion(s, defaultVersion) {
44
37
  return s.version ?? defaultVersion;
45
38
  }
46
- /**
47
- * Whether the namespace axis is "active" — i.e. ≥2 distinct
48
- * effective namespaces appear across the configured sources.
49
- * `undefined` (an unnamed source) counts as a distinct bucket,
50
- * so an unnamed + named pair activates the axis: the unnamed
51
- * source emits flat URLs, the named one gets `/<name>/<slug>`.
52
- *
53
- * See plans/multi-source-content.md → "Detecting which axes are
54
- * active".
55
- */
56
- export function isNamespaceAxisActive(sources) {
57
- if (sources.length < 2)
58
- return false;
59
- const seen = new Set();
60
- for (const s of sources)
61
- seen.add(effectiveNamespace(s));
62
- return seen.size >= 2;
63
- }
39
+ // Whether the namespace axis is active: shared with dogsbay-ssg, so both
40
+ // pipelines put a named source under the same /<name>/ URLs.
41
+ export { isNamespaceAxisActive };
64
42
  /**
65
43
  * Whether the version axis is "active" — i.e. ≥2 distinct
66
44
  * effective versions appear across the configured sources.
@@ -242,6 +220,17 @@ export async function importContent(resolvedSources, config, options = {}) {
242
220
  if (!firstImporter)
243
221
  firstImporter = importer;
244
222
  const opts = await buildImportOptions(config, sourceCfg, options);
223
+ // First URL segments a site-root link must not start with by accident:
224
+ // `/api/list-things` on a source published under `/api/` reads as
225
+ // already prefixed (rewriteContentRefs), and under base path `/api` as
226
+ // already based (format-astro). Importers that invent slugs (OpenAPI
227
+ // tags) avoid them.
228
+ const combinedPrefix = resolvePrefixes(config.site.url, config.site.basePath).combined;
229
+ opts.reservedSlugs = [
230
+ ...getPrefixSegments(sourceCfg, axes, defaults).slice(0, 1),
231
+ ...combinedPrefix.split("/").filter(Boolean).slice(0, 1),
232
+ ...globalPrefixes,
233
+ ];
245
234
  const result = await importer.import(resolved, opts);
246
235
  const { pages, nav } = result;
247
236
  const ownRoots = [...new Set([result.contentRoot, resolved].filter((r) => !!r))];
@@ -310,7 +299,7 @@ export async function importContent(resolvedSources, config, options = {}) {
310
299
  // emitter (page output paths, sitemap, llms.txt, agent-
311
300
  // readiness mirrors) reads slug.
312
301
  for (const page of pages) {
313
- page.slug = prefixSlugWithSegments(prefixSegs, page.slug);
302
+ page.slug = prefixSlug(prefixSegs, page.slug);
314
303
  }
315
304
  // Prefix in-content refs with the source's URL segments, matching
316
305
  // the slug rewrite: internal page links AND asset refs (`/foo` →
@@ -332,11 +321,9 @@ export async function importContent(resolvedSources, config, options = {}) {
332
321
  // unnamed in a namespace-active site → "default
333
322
  // section")
334
323
  const ns = axes.namespace ? effectiveNamespace(sourceCfg) : undefined;
335
- if (ns !== undefined) {
336
- allNav.push({
337
- label: prettyNamespaceLabel(ns),
338
- children: prefixedNav,
339
- });
324
+ const group = namespaceNavGroup(ns, prefixedNav);
325
+ if (group) {
326
+ allNav.push(group);
340
327
  }
341
328
  else {
342
329
  allNav.push(...prefixedNav);
@@ -391,133 +378,9 @@ export function checkRootVersionCollisions(pages, rootVersion, versionIds) {
391
378
  }
392
379
  }
393
380
  }
394
- /**
395
- * Prefix INTERNAL-ABSOLUTE refs in a page tree with the source's URL
396
- * segments, matching the slug rewrite — so a versioned/localised page's
397
- * links and images resolve under the same `/<segs>/...` prefix as the
398
- * pages themselves. Covers page links (`href`) and asset refs (`src`),
399
- * in both markdown nodes (`src`/`href` fields) and raw-HTML nodes
400
- * (`href="..."`/`src="..."` inside an `html` string).
401
- *
402
- * Refs at this pipeline stage are ROOT-RELATIVE and carry NO basePath
403
- * (importers resolve to `/foo`; basePath is composed downstream), so the
404
- * match anchor is `/`, not `<basePath>/`. A ref is prefixed iff it is an
405
- * internal absolute path (starts with `/`), and NOT: external
406
- * (`scheme:`, `//host`), an anchor (`#`), the site home (`/`), under a
407
- * GLOBAL prefix (taxonomy index paths — one `/tags/` for the whole site,
408
- * never version-bucketed), or ALREADY carrying this source's full
409
- * `/<segs>/` prefix (idempotent). Relative refs are left untouched.
410
- */
411
- export function rewriteContentRefs(tree, prefixSegs, globalPrefixes) {
412
- if (prefixSegs.length === 0)
413
- return;
414
- const seg = prefixSegs.join("/");
415
- const fullPrefix = `/${seg}/`;
416
- const prefix = (u) => {
417
- if (!u)
418
- return u;
419
- if (/^[a-z][a-z0-9+.-]*:/i.test(u))
420
- return u; // scheme (http:, mailto:)
421
- if (u.startsWith("//") || u.startsWith("#"))
422
- return u; // protocol-rel / anchor
423
- if (!u.startsWith("/"))
424
- return u; // relative
425
- if (u === "/" || u === "")
426
- return u; // site home — global
427
- if (u.startsWith(fullPrefix) || u === `/${seg}`)
428
- return u; // already prefixed
429
- // Global (taxonomy) prefixes live in one un-bucketed namespace.
430
- const firstSeg = u.slice(1).split(/[/#?]/)[0];
431
- if (globalPrefixes.has(firstSeg))
432
- return u;
433
- return `/${seg}${u}`;
434
- };
435
- // Raw HTML: rewrite the VALUE of each href="..."/src="..." attribute.
436
- const rewriteHtml = (html) => html.replace(/\b(href|src)=("|')(.*?)\2/gi, (_m, attr, q, val) => `${attr}=${q}${prefix(val)}${q}`);
437
- const walk = (node) => {
438
- if (Array.isArray(node)) {
439
- for (const item of node)
440
- walk(item);
441
- return;
442
- }
443
- if (!node || typeof node !== "object")
444
- return;
445
- const obj = node;
446
- if (typeof obj.src === "string")
447
- obj.src = prefix(obj.src);
448
- if (typeof obj.href === "string")
449
- obj.href = prefix(obj.href);
450
- if (typeof obj.html === "string")
451
- obj.html = rewriteHtml(obj.html);
452
- // `::child-links{folder=…}` is a site path too: on a versioned or
453
- // localized site it names a folder in THIS source's namespace.
454
- if (obj.type === "child-links" && obj.props && typeof obj.props.folder === "string") {
455
- const props = obj.props;
456
- props.folder = prefix(`/${String(props.folder).replace(/^\/+/, "")}`);
457
- }
458
- // `scope=section` has no folder to prefix: record the source namespace,
459
- // or the section resolves to the version segment (`/v2/`).
460
- if (obj.type === "child-links" && obj.props && obj.props.scope === "section") {
461
- obj.props.scopeBase = `/${seg}`;
462
- }
463
- for (const key of Object.keys(obj)) {
464
- const val = obj[key];
465
- if (val && typeof val === "object")
466
- walk(val);
467
- }
468
- };
469
- walk(tree);
470
- }
471
- function prefixSlugWithSegments(prefix, slug) {
472
- if (prefix.length === 0)
473
- return slug;
474
- if (!slug)
475
- return prefix.join("/");
476
- return `${prefix.join("/")}/${slug}`;
477
- }
478
- /**
479
- * Inject prefix segments into nav hrefs that match the
480
- * configured basePath prefix. Skips external URLs and hrefs that
481
- * don't start with basePath (the latter is unusual but worth
482
- * tolerating — never silently rewrite something we don't
483
- * recognise).
484
- */
485
- function prefixNavHrefs(items, prefix, basePath) {
486
- // basePath is normalised: "" or "/<seg>". For matching purposes
487
- // we always work with a string ending in "/".
488
- const matchPrefix = basePath ? `${basePath}/` : "/";
489
- return items.map((item) => {
490
- const next = { ...item };
491
- if (item.href !== undefined) {
492
- next.href = injectPrefixIntoHref(item.href, prefix, matchPrefix);
493
- }
494
- if (item.children && item.children.length > 0) {
495
- next.children = prefixNavHrefs(item.children, prefix, basePath);
496
- }
497
- return next;
498
- });
499
- }
500
- function injectPrefixIntoHref(href, prefix, matchPrefix) {
501
- // External URLs pass through untouched.
502
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(href) || href.startsWith("mailto:")) {
503
- return href;
504
- }
505
- if (!href.startsWith(matchPrefix)) {
506
- // Defensive: nav items not matching basePath stay as-is. In
507
- // practice all importer-produced hrefs include basePath since
508
- // we thread `hrefPrefix` through buildImportOptions.
509
- return href;
510
- }
511
- const rest = href.slice(matchPrefix.length);
512
- return `${matchPrefix}${prefix.join("/")}/${rest}`;
513
- }
514
- /** Title-case a namespace key for sidebar display. */
515
- function prettyNamespaceLabel(ns) {
516
- return ns
517
- .split("-")
518
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
519
- .join(" ");
520
- }
381
+ // rewriteContentRefs (a prefixed source's site-root links get its segments)
382
+ // lives in @dogsbay/site-files, shared with dogsbay-ssg.
383
+ export { rewriteContentRefs };
521
384
  function resolveImporter(source, fromName) {
522
385
  if (fromName && fromName !== "auto") {
523
386
  const explicit = FORMATS.find((f) => f.name === fromName && f.canImport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dogsbay",
3
- "version": "0.2.0-beta.119",
3
+ "version": "0.2.0-beta.120",
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": {
@@ -43,34 +43,35 @@
43
43
  "typescript": "^5.9.3",
44
44
  "valibot": "1.1.0",
45
45
  "yaml": "^2.9.0",
46
- "@dogsbay/adoc2md-modular": "0.2.0-beta.119",
47
- "@dogsbay/commands": "0.2.0-beta.119",
48
- "@dogsbay/format-asciidoc": "0.2.0-beta.119",
49
- "@dogsbay/autodoc-python": "0.2.0-beta.119",
50
- "@dogsbay/format-astro": "0.2.0-beta.119",
51
- "@dogsbay/format-dita": "0.2.0-beta.119",
52
- "@dogsbay/format-docusaurus": "0.2.0-beta.119",
53
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.119",
54
- "@dogsbay/format-fluidtopics-markdown": "0.2.0-beta.119",
55
- "@dogsbay/format-fluidtopics-rest": "0.2.0-beta.119",
56
- "@dogsbay/format-gatsby": "0.2.0-beta.119",
57
- "@dogsbay/format-gitbook": "0.2.0-beta.119",
58
- "@dogsbay/format-hugo": "0.2.0-beta.119",
59
- "@dogsbay/format-mdx": "0.2.0-beta.119",
60
- "@dogsbay/format-mkdocs": "0.2.0-beta.119",
61
- "@dogsbay/format-myst": "0.2.0-beta.119",
62
- "@dogsbay/format-obsidian": "0.2.0-beta.119",
63
- "@dogsbay/format-openapi": "0.2.0-beta.119",
64
- "@dogsbay/format-openknowledge": "0.2.0-beta.119",
65
- "@dogsbay/format-rst": "0.2.0-beta.119",
66
- "@dogsbay/format-starlight": "0.2.0-beta.119",
67
- "@dogsbay/format-topicmap": "0.2.0-beta.119",
68
- "@dogsbay/format-vitepress": "0.2.0-beta.119",
69
- "@dogsbay/minja": "0.2.0-beta.119",
70
- "@dogsbay/render": "0.2.0-beta.119",
71
- "@dogsbay/serialize-core": "0.2.0-beta.119",
72
- "@dogsbay/site-check": "0.2.0-beta.119",
73
- "@dogsbay/types": "0.2.0-beta.119"
46
+ "@dogsbay/adoc2md-modular": "0.2.0-beta.120",
47
+ "@dogsbay/format-asciidoc": "0.2.0-beta.120",
48
+ "@dogsbay/commands": "0.2.0-beta.120",
49
+ "@dogsbay/autodoc-python": "0.2.0-beta.120",
50
+ "@dogsbay/format-astro": "0.2.0-beta.120",
51
+ "@dogsbay/format-dita": "0.2.0-beta.120",
52
+ "@dogsbay/format-docusaurus": "0.2.0-beta.120",
53
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.120",
54
+ "@dogsbay/format-fluidtopics-markdown": "0.2.0-beta.120",
55
+ "@dogsbay/format-fluidtopics-rest": "0.2.0-beta.120",
56
+ "@dogsbay/format-gatsby": "0.2.0-beta.120",
57
+ "@dogsbay/format-gitbook": "0.2.0-beta.120",
58
+ "@dogsbay/format-hugo": "0.2.0-beta.120",
59
+ "@dogsbay/format-mdx": "0.2.0-beta.120",
60
+ "@dogsbay/format-mkdocs": "0.2.0-beta.120",
61
+ "@dogsbay/format-myst": "0.2.0-beta.120",
62
+ "@dogsbay/format-openapi": "0.2.0-beta.120",
63
+ "@dogsbay/format-obsidian": "0.2.0-beta.120",
64
+ "@dogsbay/format-openknowledge": "0.2.0-beta.120",
65
+ "@dogsbay/format-rst": "0.2.0-beta.120",
66
+ "@dogsbay/format-topicmap": "0.2.0-beta.120",
67
+ "@dogsbay/format-starlight": "0.2.0-beta.120",
68
+ "@dogsbay/format-vitepress": "0.2.0-beta.120",
69
+ "@dogsbay/minja": "0.2.0-beta.120",
70
+ "@dogsbay/render": "0.2.0-beta.120",
71
+ "@dogsbay/serialize-core": "0.2.0-beta.120",
72
+ "@dogsbay/site-check": "0.2.0-beta.120",
73
+ "@dogsbay/site-files": "0.2.0-beta.120",
74
+ "@dogsbay/types": "0.2.0-beta.120"
74
75
  },
75
76
  "devDependencies": {
76
77
  "@mdx-js/mdx": "^3.1.1",