@dogsbay/ssg 0.2.0-beta.141 → 0.2.0-beta.143

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,31 +1,31 @@
1
1
  Third-party software in @dogsbay/ssg (dist/package/)
2
2
 
3
3
  ========================================================================
4
- @dogsbay/elements@0.2.0-beta.141 (MIT)
4
+ @dogsbay/elements@0.2.0-beta.143 (MIT)
5
5
  https://github.com/dogsbay/dogsbay.git
6
6
 
7
7
  Licensed under MIT; the package ships no licence file.
8
8
 
9
9
  ========================================================================
10
- @dogsbay/primitives@0.2.0-beta.141 (MIT)
10
+ @dogsbay/primitives@0.2.0-beta.143 (MIT)
11
11
  https://github.com/dogsbay/dogsbay.git
12
12
 
13
13
  Licensed under MIT; the package ships no licence file.
14
14
 
15
15
  ========================================================================
16
- @dogsbay/render-html@0.2.0-beta.141 (MIT)
16
+ @dogsbay/render-html@0.2.0-beta.143 (MIT)
17
17
  https://github.com/dogsbay/dogsbay.git
18
18
 
19
19
  Licensed under MIT; the package ships no licence file.
20
20
 
21
21
  ========================================================================
22
- @dogsbay/search@0.2.0-beta.141 (MIT)
22
+ @dogsbay/search@0.2.0-beta.143 (MIT)
23
23
  https://github.com/dogsbay/dogsbay.git
24
24
 
25
25
  Licensed under MIT; the package ships no licence file.
26
26
 
27
27
  ========================================================================
28
- @dogsbay/ui@0.2.0-beta.141 (MIT)
28
+ @dogsbay/ui@0.2.0-beta.143 (MIT)
29
29
  https://github.com/dogsbay/dogsbay.git
30
30
 
31
31
  Licensed under MIT; the package ships no licence file.
@@ -18,7 +18,7 @@ import {
18
18
 
19
19
  // src/core/build.ts
20
20
  import { dogsbayMdToTree, splitFrontmatter as splitFrontmatter2, treeToDogsbayMd } from "@dogsbay/format-dogsbay-md/browser";
21
- import { buildHreflangLinks, missingTranslations, namespaceNavGroup, prefixSlug, rewriteContentRefs } from "@dogsbay/site-files";
21
+ import { buildHreflangLinks, isMountRoot, missingTranslations, mountRefs, mountRootNote, namespaceNavGroup, placeMountedNav, prefixSlug as prefixSlug2, rewriteContentRefs } from "@dogsbay/site-files";
22
22
 
23
23
  // src/core/canonical.ts
24
24
  import { decideCanonical } from "@dogsbay/site-files";
@@ -26,7 +26,7 @@ import { declaresParams, splitFrontmatter } from "@dogsbay/format-dogsbay-md/bro
26
26
 
27
27
  // src/core/config.ts
28
28
  import { scriptUrl } from "@dogsbay/render-html";
29
- import { assertLeafSitemap, checkVersionSeo, isNamespaceAxisActive, namespaceSegments } from "@dogsbay/site-files";
29
+ import { assertLeafSitemap, checkVersionSeo, isNamespaceAxisActive, namespaceSegments, parseMount } from "@dogsbay/site-files";
30
30
 
31
31
  // src/core/yaml.ts
32
32
  import { load } from "js-yaml";
@@ -67,6 +67,14 @@ function unitId(kind, v) {
67
67
  if (!/^[a-z0-9][a-z0-9._-]*$/i.test(id)) throw new ConfigError(`${kind}: id "${id.slice(0, 80)}" must be one URL segment of letters, digits, dots, dashes and underscores`);
68
68
  return id;
69
69
  }
70
+ function sourceMount(s) {
71
+ const at = `content.sources[${String(s.path)}]`;
72
+ if (s.from !== "openapi") throw new ConfigError(`${at}: mount is only for a "from: openapi" source`);
73
+ if (s.name != null) throw new ConfigError(`${at} has both name and mount: a mounted source goes inside its version, so it takes no name`);
74
+ const parsed = parseMount(s.mount);
75
+ if ("error" in parsed) throw new ConfigError(`${at}: ${parsed.error}`);
76
+ return parsed.segments.join("/");
77
+ }
70
78
  function sourceFrom(v, path) {
71
79
  if (v === "dogsbay-md" || v === "openapi") return v;
72
80
  throw new ConfigError(`content.sources[${path}].from must be "dogsbay-md" or "openapi"; got ${JSON.stringify(v)}`);
@@ -139,6 +147,7 @@ function parseConfig(text, overrides = {}) {
139
147
  path: sourcePath(s),
140
148
  ...s.name != null ? { name: sourceName(s.name) } : {},
141
149
  ...s.from != null ? { from: sourceFrom(s.from, String(s.path)) } : {},
150
+ ...s.mount != null ? { mount: sourceMount(s) } : {},
142
151
  version: s.version != null ? unitId("version", s.version) : void 0,
143
152
  locale: s.locale != null ? unitId("locale", s.locale) : void 0,
144
153
  attributes: s.attributes ?? void 0,
@@ -414,29 +423,41 @@ function buildUnits(cfg) {
414
423
  throw new ConfigError(`source "${u.source.path}" has locale "${u.locale}" which is not declared in locales`);
415
424
  }
416
425
  }
417
- const namespaces = isNamespaceAxisActive(cfg.sources);
426
+ const namespaces = isNamespaceAxisActive(cfg.sources.filter((s) => !s.mount));
418
427
  if (namespaces && (versionAxis || localeAxis)) {
419
428
  throw new ConfigError("named sources (content.sources name:) cannot be combined with versions or locales yet (plans/openapi-sources.md)");
420
429
  }
421
430
  for (const s of cfg.sources) {
422
- if (s.from === "openapi" && cfg.sources.length > 1 && !s.name) throw new ConfigError(`content.sources[${s.path}]: an OpenAPI source beside other sources needs a name:, so its pages do not land on theirs`);
431
+ if (s.from === "openapi" && cfg.sources.length > 1 && !s.name && !s.mount) throw new ConfigError(`content.sources[${s.path}]: an OpenAPI source beside other sources needs a name: or a mount:, so its pages do not land on theirs`);
423
432
  }
424
433
  const units = /* @__PURE__ */ new Map();
425
434
  for (const u of withDefaults2) {
426
435
  const atRoot = cfg.versions.find((v) => v.id === u.version)?.path === "";
427
436
  const prefix = [localeAxis ? u.locale : void 0, versionAxis && !atRoot ? u.version : void 0].filter(Boolean).join("/");
428
- const member = { source: u.source, namespace: namespaceSegments(u.source, cfg.sources).join("/") };
437
+ const member = u.source.mount ? { source: u.source, namespace: "", mount: u.source.mount } : { source: u.source, namespace: namespaceSegments(u.source, cfg.sources).join("/") };
429
438
  const unit = units.get(prefix);
430
439
  if (!unit) {
431
440
  units.set(prefix, { source: u.source, members: [member], version: versionAxis ? u.version : void 0, locale: localeAxis ? u.locale : void 0, prefix });
432
441
  continue;
433
442
  }
434
- if (!namespaces) throw new ConfigError(`two sources publish to the same prefix "${prefix || "/"}"`);
435
- if (unit.members.some((m) => m.namespace === member.namespace)) {
436
- throw new ConfigError(member.namespace ? `two sources are named "${member.namespace}"` : `two unnamed sources publish to the same prefix "${prefix || "/"}"`);
443
+ if (member.mount) {
444
+ const taken = unit.members.find((m) => m.mount === member.mount);
445
+ if (taken) throw new ConfigError(`two sources are mounted at "${member.mount}" in the same version and locale (${taken.source.path}, ${u.source.path})`);
446
+ } else if (unit.members.some((m) => !m.mount)) {
447
+ if (!namespaces) throw new ConfigError(`two sources publish to the same prefix "${prefix || "/"}"`);
448
+ if (unit.members.some((m) => !m.mount && m.namespace === member.namespace)) {
449
+ throw new ConfigError(member.namespace ? `two sources are named "${member.namespace}"` : `two unnamed sources publish to the same prefix "${prefix || "/"}"`);
450
+ }
437
451
  }
438
452
  unit.members.push(member);
439
- if (!member.namespace && unit.members[0].namespace) unit.source = u.source;
453
+ const main = unit.members.find((m) => m.source === unit.source);
454
+ if (!member.namespace && !member.mount && (main.namespace || main.mount)) unit.source = u.source;
455
+ }
456
+ for (const unit of units.values()) {
457
+ for (const m of unit.members) {
458
+ const ns = m.mount && unit.members.find((o) => o.namespace && m.mount.split("/")[0] === o.namespace);
459
+ if (ns) throw new ConfigError(`content.sources[${m.source.path}]: mount "${m.mount}" is inside the folder of the source named "${ns.namespace}"; mount it elsewhere`);
460
+ }
440
461
  }
441
462
  return [...units.values()];
442
463
  }
@@ -454,8 +475,7 @@ function projectPath(unit, path) {
454
475
  for (const m of unit.members) {
455
476
  if (m.namespace && m.source.from !== "openapi" && (path === m.namespace || path.startsWith(m.namespace + "/"))) return sourceRoot(m.source) + path.slice(m.namespace.length + 1);
456
477
  }
457
- const main = unit.members.find((m) => !m.namespace) ?? unit.members[0];
458
- return sourceRoot(main.source) + path;
478
+ return sourceRoot(unit.source) + path;
459
479
  }
460
480
  var TOPIC_MAP = /\.topicmap\.ya?ml$/;
461
481
  function sourceNav(v, path, from) {
@@ -559,6 +579,209 @@ function createResolver(table, page) {
559
579
  };
560
580
  }
561
581
 
582
+ // src/core/openapi-pages.ts
583
+ import { mountSlug, prefixSlug } from "@dogsbay/site-files";
584
+
585
+ // src/core/resolve.ts
586
+ import { inlineText } from "@dogsbay/render-html";
587
+ import { slugify } from "@dogsbay/render-html/html";
588
+ import { CORE_SCHEMA, load as loadYaml2 } from "js-yaml";
589
+ function hasPlaceholder(nodes, types) {
590
+ return (nodes ?? []).some((n) => types.has(n.type) || hasPlaceholder(n.children, types));
591
+ }
592
+ async function resolvePlaceholders(tree, resolvers, ctx) {
593
+ const byType = new Map(resolvers.map((r) => [r.type, r]));
594
+ if (!hasPlaceholder(tree, new Set(byType.keys()))) return void 0;
595
+ const usedIds = headingIds(tree);
596
+ const warnings = [];
597
+ const walk = async (nodes) => {
598
+ for (let i = 0; i < nodes.length; i++) {
599
+ const r = byType.get(nodes[i].type);
600
+ if (!r) {
601
+ if (nodes[i].children) await walk(nodes[i].children);
602
+ continue;
603
+ }
604
+ let got;
605
+ try {
606
+ got = await r.resolve(nodes[i], { ...ctx, usedIds });
607
+ } catch (e) {
608
+ got = { warning: e.message || String(e) };
609
+ }
610
+ if ("warning" in got) {
611
+ warnings.push(got.warning);
612
+ nodes.splice(i, 1, ...nodes[i].children ?? []);
613
+ i--;
614
+ continue;
615
+ }
616
+ headingIds(got.nodes, usedIds);
617
+ nodes.splice(i, 1, ...got.nodes);
618
+ i += got.nodes.length - 1;
619
+ }
620
+ };
621
+ await walk(tree);
622
+ return warnings;
623
+ }
624
+ function headingIds(nodes, ids = /* @__PURE__ */ new Set()) {
625
+ const take = (raw) => {
626
+ const base = RESERVED_ID.test(raw) ? `h-${raw}` : raw;
627
+ let id = base || "section";
628
+ for (let n = 2; ids.has(id); n++) id = `${base}-${n}`;
629
+ ids.add(id);
630
+ };
631
+ const walk = (list) => {
632
+ for (const n of list) {
633
+ if (n.type === "heading") {
634
+ const p = n.props ?? {};
635
+ take(typeof p.id === "string" ? p.id : typeof p.slug === "string" ? p.slug : slugify(inlineText(n.inline)));
636
+ } else if ((n.type === "prose" || n.type === "html") && typeof n.html === "string") {
637
+ for (const m of n.html.matchAll(/<h[1-6]\b[^>]*\sid="([^"]*)"/gi)) take(m[1]);
638
+ }
639
+ if (n.children) walk(n.children);
640
+ }
641
+ };
642
+ walk(nodes);
643
+ return ids;
644
+ }
645
+ var RESERVED_ID = /^(db-|tab-\d|tabpanel-\d|acc-\d|accpanel-\d|fn-|fnref-)/;
646
+ var autodocUnresolved = {
647
+ type: "autodoc-ref",
648
+ async resolve(node) {
649
+ const ref = typeof node.props?.ref === "string" ? node.props.ref : "";
650
+ return { warning: `:::autodoc${ref ? `{ref="${ref}"}` : ""} is not resolved in this build: resolve it at migration time (dogsbay migrate-mkdocs --inline-autodoc)` };
651
+ }
652
+ };
653
+ var coreLoad;
654
+ var loadCore = () => coreLoad ??= import("@dogsbay/format-openapi/core");
655
+ function openApiResolver(read, maxChars = 8e6) {
656
+ let specs;
657
+ return {
658
+ type: "openapi-ref",
659
+ async resolve(node, ctx) {
660
+ const spec = typeof node.props?.spec === "string" ? node.props.spec : "";
661
+ const core = await loadCore();
662
+ const selected = core.directiveOperation(node.props);
663
+ if ("error" in selected) return { warning: `:::openapi: ${selected.error}` };
664
+ const slug = ctx.source.replace(/\.(md|markdown)$/i, "").replace(/(^|\/)index$/, "");
665
+ const found = core.findSpec(spec, slug, (p) => {
666
+ const at = ctx.scope ? ctx.scope(p) : p;
667
+ return ctx.has(at) ? { path: at } : void 0;
668
+ });
669
+ if ("error" in found) return { warning: `:::openapi: ${found.error}` };
670
+ specs ??= core.createSpecCache(async (p) => {
671
+ const text = await read(p);
672
+ if (text === void 0) throw new Error("not found");
673
+ return text;
674
+ }, (text) => loadYaml2(text, { schema: CORE_SCHEMA }));
675
+ let prepared;
676
+ try {
677
+ prepared = await specs.get(found.path);
678
+ } catch (e) {
679
+ return { warning: `:::openapi: cannot read ${spec}: ${e.message}` };
680
+ }
681
+ let tree;
682
+ try {
683
+ tree = core.embedSpec(prepared, { usedIds: ctx.usedIds, operation: selected.operation }).tree;
684
+ } catch (e) {
685
+ return { warning: `:::openapi: cannot use ${spec}: ${e.message}` };
686
+ }
687
+ const size = boundedSize(tree, maxChars);
688
+ if (size === "cycle") return { warning: `:::openapi: ${spec} has a cyclic example or schema, which cannot be shown` };
689
+ if (size === "over") return { warning: `:::openapi: ${spec} is too large to embed in one page (over ${Math.round(maxChars / 1e6)}M characters)` };
690
+ return { nodes: tree };
691
+ }
692
+ };
693
+ }
694
+ async function openApiSourcePages(read, specPath, reservedSlugs) {
695
+ const core = await loadCore();
696
+ const specs = core.createSpecCache(async (p) => {
697
+ const text = await read(p);
698
+ if (text === void 0) throw new Error("not found");
699
+ return text;
700
+ }, (text) => loadYaml2(text, { schema: CORE_SCHEMA }));
701
+ try {
702
+ return core.specToPages(await specs.get(specPath), { reservedSlugs });
703
+ } catch (e) {
704
+ return { error: `cannot read ${specPath}: ${e.message}` };
705
+ }
706
+ }
707
+ async function specFileOf(path, has, read) {
708
+ const p = path.replace(/^\.\//, "").replace(/\/+$/, "");
709
+ if (/\.(ya?ml|json)$/i.test(p)) return has(p) ? p : void 0;
710
+ const dir = p === "." || p === "" ? "" : p + "/";
711
+ const found = ["openapi", "swagger", "spec"].flatMap((b) => ["yaml", "yml", "json"].map((e) => `${dir}${b}.${e}`)).filter(has);
712
+ for (const f of found) {
713
+ const head = (await read(f))?.slice(0, 2048) ?? "";
714
+ if (/(?:^|[{,])\s*"?(openapi|swagger)"?\s*[:=]/m.test(head)) return f;
715
+ }
716
+ return found[0];
717
+ }
718
+ function boundedSize(value, max) {
719
+ let total = 0;
720
+ const path = /* @__PURE__ */ new Set();
721
+ const walk = (v) => {
722
+ if (typeof v === "string") total += v.length;
723
+ else if (v && typeof v === "object") {
724
+ if (path.has(v)) return "cycle";
725
+ path.add(v);
726
+ total += 2;
727
+ for (const x of Array.isArray(v) ? v : Object.values(v)) {
728
+ const r = walk(x);
729
+ if (r) return r;
730
+ if (total > max) return "over";
731
+ }
732
+ path.delete(v);
733
+ } else total += 4;
734
+ return total > max ? "over" : void 0;
735
+ };
736
+ return walk(value) ?? total;
737
+ }
738
+
739
+ // src/core/openapi-pages.ts
740
+ function urlPathOf(url) {
741
+ if (!url) return [];
742
+ try {
743
+ return new URL(url).pathname.split("/").filter(Boolean);
744
+ } catch {
745
+ return [];
746
+ }
747
+ }
748
+ async function placeApiPages(cfg, unit, m, files, readText2) {
749
+ const read = async (p) => {
750
+ const c = files.get(p);
751
+ return c == null ? void 0 : readText2(c);
752
+ };
753
+ const spec = await specFileOf(m.source.path, (p) => files.has(p), read);
754
+ if (!spec) return { missing: true };
755
+ const ns = m.namespace;
756
+ const mount = m.mount?.split("/");
757
+ const segs = mount ?? (ns ? [ns] : []);
758
+ const first = ns || unit.prefix.split("/")[0];
759
+ const combined = [...urlPathOf(cfg.url), ...cfg.basePath.split("/").filter(Boolean)];
760
+ const taxonomies = new Set(cfg.taxonomies.map((t) => t.path.split("/")[0]).filter(Boolean));
761
+ const got = await openApiSourcePages(read, spec, [...first ? [first] : [], ...combined.slice(0, 1), ...taxonomies]);
762
+ if ("error" in got) return { spec, error: got.error };
763
+ const pages = got.pages.map((page) => {
764
+ const slug = mount ? mountSlug(mount, page.slug) : prefixSlug(segs, page.slug === "index" ? "" : page.slug);
765
+ const isIndex = page.slug === "index" || got.pages.some((q) => q.slug.startsWith(page.slug + "/"));
766
+ return { slug, out: outFileFor(slug, isIndex && slug !== ""), isIndex, page };
767
+ });
768
+ return { spec, pages, nav: got.nav, segs };
769
+ }
770
+ var outsCache = /* @__PURE__ */ new WeakMap();
771
+ async function apiPageOuts(cfg, unit, m, files, readText2) {
772
+ let cache = outsCache.get(files);
773
+ if (!cache) outsCache.set(files, cache = /* @__PURE__ */ new Map());
774
+ const id = `${unit.prefix}\0${m.source.path}`;
775
+ const key = JSON.stringify([m.namespace, m.mount, cfg.url, cfg.basePath, cfg.taxonomies.map((t) => t.path)]);
776
+ const hit = cache.get(id);
777
+ const specFile = /\.(ya?ml|json)$/i.test(m.source.path) ? files.get(m.source.path.replace(/^\.\//, "")) : void 0;
778
+ if (hit && hit.key === key && specFile !== void 0 && hit.spec === specFile) return hit.outs;
779
+ const placed = await placeApiPages(cfg, unit, m, files, readText2);
780
+ const outs = "pages" in placed ? placed.pages.map((p) => [p.slug, p.out]) : [];
781
+ cache.set(id, { spec: specFile, key, outs });
782
+ return outs;
783
+ }
784
+
562
785
  // src/core/nav.ts
563
786
  import { scriptUrl as scriptUrl2 } from "@dogsbay/render-html";
564
787
  function slugFromPath(p) {
@@ -851,7 +1074,7 @@ async function defaultTables(cfg, units, built, files, read, warn = () => {
851
1074
  const tables = /* @__PURE__ */ new Map();
852
1075
  if (!cfg.defaultVersion || !built.some((u) => u.version && u.version !== cfg.defaultVersion)) return tables;
853
1076
  for (const unit of units.filter((u) => u.version === cfg.defaultVersion)) {
854
- const t = await scan(cfg, unit, units, files, fm ?? frontMatterCache(read));
1077
+ const t = await scan(cfg, unit, units, files, fm ?? frontMatterCache(read), read);
855
1078
  if (!t.pages.size && built.some((u) => u.version !== cfg.defaultVersion && u.locale === unit.locale)) {
856
1079
  warn(`[canonical] the default version "${cfg.defaultVersion}" has no pages under ${unit.source.path}, so older versions' pages keep themselves as canonical`);
857
1080
  }
@@ -864,17 +1087,17 @@ async function languageTables(cfg, units, built, files, read, fm) {
864
1087
  if (new Set(units.map((u) => u.locale).filter(Boolean)).size < 2) return tables;
865
1088
  const versions = new Set(built.map((u) => u.version));
866
1089
  const fmOf = fm ?? frontMatterCache(read);
867
- for (const unit of units) if (versions.has(unit.version)) tables.set(unit.prefix, (await scan(cfg, unit, units, files, fmOf)).pages);
1090
+ for (const unit of units) if (versions.has(unit.version)) tables.set(unit.prefix, (await scan(cfg, unit, units, files, fmOf, read)).pages);
868
1091
  return tables;
869
1092
  }
870
1093
  async function translationTables(cfg, units, files, read, includeDrafts, fm) {
871
1094
  const tables = /* @__PURE__ */ new Map();
872
1095
  if (new Set(units.map((u) => u.locale).filter(Boolean)).size < 2) return tables;
873
1096
  const fmOf = fm ?? frontMatterCache(read);
874
- for (const unit of units) tables.set(unit.prefix, await scan(cfg, unit, units, files, fmOf, { kind: "built", includeDrafts }));
1097
+ for (const unit of units) tables.set(unit.prefix, await scan(cfg, unit, units, files, fmOf, read, { kind: "built", includeDrafts }));
875
1098
  return tables;
876
1099
  }
877
- async function scan(cfg, unit, units, files, frontMatterOf, mode = { kind: "targets" }) {
1100
+ async function scan(cfg, unit, units, files, frontMatterOf, read, mode = { kind: "targets" }) {
878
1101
  const rootOf = (u) => u.source.path === "." ? "" : u.source.path.replace(/^\.\//, "").replace(/\/+$/, "") + "/";
879
1102
  const root = rootOf(unit);
880
1103
  const otherRoots = units.filter((u) => u !== unit).map(rootOf).filter((r) => r && r.startsWith(root));
@@ -912,6 +1135,10 @@ async function scan(cfg, unit, units, files, frontMatterOf, mode = { kind: "targ
912
1135
  for (const a of fm.aliases) if (typeof a === "string" && inUnit(a) !== void 0) setFirst(inUnit(a), detach(key(slug)));
913
1136
  }
914
1137
  }
1138
+ for (const m of unit.members) {
1139
+ if (m.source.from !== "openapi") continue;
1140
+ for (const [slug, out] of await apiPageOuts(cfg, unit, m, files, read)) if (!pages.has(key(slug))) pages.set(detach(key(slug)), out);
1141
+ }
915
1142
  for (const r of cfg.redirects) {
916
1143
  const from = sitePath(r.from, cfg.basePath);
917
1144
  if (!mine(from)) continue;
@@ -1085,31 +1312,31 @@ var CLIENT_CANDIDATES = ['""', '"&"', '"\\\\$&")).join("|")})', '"\\\\$&")}"', '
1085
1312
  var SITE_NOTICES = `Third-party software in this site's _dogsbay/ files (dogsbay.js, site.css) and pages
1086
1313
 
1087
1314
  ========================================================================
1088
- @dogsbay/elements@0.2.0-beta.141 (MIT)
1315
+ @dogsbay/elements@0.2.0-beta.143 (MIT)
1089
1316
  https://github.com/dogsbay/dogsbay.git
1090
1317
 
1091
1318
  Licensed under MIT; the package ships no licence file.
1092
1319
 
1093
1320
  ========================================================================
1094
- @dogsbay/primitives@0.2.0-beta.141 (MIT)
1321
+ @dogsbay/primitives@0.2.0-beta.143 (MIT)
1095
1322
  https://github.com/dogsbay/dogsbay.git
1096
1323
 
1097
1324
  Licensed under MIT; the package ships no licence file.
1098
1325
 
1099
1326
  ========================================================================
1100
- @dogsbay/render-html@0.2.0-beta.141 (MIT)
1327
+ @dogsbay/render-html@0.2.0-beta.143 (MIT)
1101
1328
  https://github.com/dogsbay/dogsbay.git
1102
1329
 
1103
1330
  Licensed under MIT; the package ships no licence file.
1104
1331
 
1105
1332
  ========================================================================
1106
- @dogsbay/search@0.2.0-beta.141 (MIT)
1333
+ @dogsbay/search@0.2.0-beta.143 (MIT)
1107
1334
  https://github.com/dogsbay/dogsbay.git
1108
1335
 
1109
1336
  Licensed under MIT; the package ships no licence file.
1110
1337
 
1111
1338
  ========================================================================
1112
- @dogsbay/ui@0.2.0-beta.141 (MIT)
1339
+ @dogsbay/ui@0.2.0-beta.143 (MIT)
1113
1340
  https://github.com/dogsbay/dogsbay.git
1114
1341
 
1115
1342
  Licensed under MIT; the package ships no licence file.
@@ -1560,160 +1787,6 @@ import { FAVICON_SVG } from "@dogsbay/render-html/favicon";
1560
1787
  import { codeRequest, createRenderContext, highlightInputs, inlineText as inlineText2, renderBlocks, scriptUrl as scriptUrl3 } from "@dogsbay/render-html";
1561
1788
  import { apiPageLayout } from "@dogsbay/serialize-core";
1562
1789
 
1563
- // src/core/resolve.ts
1564
- import { inlineText } from "@dogsbay/render-html";
1565
- import { slugify } from "@dogsbay/render-html/html";
1566
- import { CORE_SCHEMA, load as loadYaml2 } from "js-yaml";
1567
- function hasPlaceholder(nodes, types) {
1568
- return (nodes ?? []).some((n) => types.has(n.type) || hasPlaceholder(n.children, types));
1569
- }
1570
- async function resolvePlaceholders(tree, resolvers, ctx) {
1571
- const byType = new Map(resolvers.map((r) => [r.type, r]));
1572
- if (!hasPlaceholder(tree, new Set(byType.keys()))) return void 0;
1573
- const usedIds = headingIds(tree);
1574
- const warnings = [];
1575
- const walk = async (nodes) => {
1576
- for (let i = 0; i < nodes.length; i++) {
1577
- const r = byType.get(nodes[i].type);
1578
- if (!r) {
1579
- if (nodes[i].children) await walk(nodes[i].children);
1580
- continue;
1581
- }
1582
- let got;
1583
- try {
1584
- got = await r.resolve(nodes[i], { ...ctx, usedIds });
1585
- } catch (e) {
1586
- got = { warning: e.message || String(e) };
1587
- }
1588
- if ("warning" in got) {
1589
- warnings.push(got.warning);
1590
- nodes.splice(i, 1, ...nodes[i].children ?? []);
1591
- i--;
1592
- continue;
1593
- }
1594
- headingIds(got.nodes, usedIds);
1595
- nodes.splice(i, 1, ...got.nodes);
1596
- i += got.nodes.length - 1;
1597
- }
1598
- };
1599
- await walk(tree);
1600
- return warnings;
1601
- }
1602
- function headingIds(nodes, ids = /* @__PURE__ */ new Set()) {
1603
- const take = (raw) => {
1604
- const base = RESERVED_ID.test(raw) ? `h-${raw}` : raw;
1605
- let id = base || "section";
1606
- for (let n = 2; ids.has(id); n++) id = `${base}-${n}`;
1607
- ids.add(id);
1608
- };
1609
- const walk = (list) => {
1610
- for (const n of list) {
1611
- if (n.type === "heading") {
1612
- const p = n.props ?? {};
1613
- take(typeof p.id === "string" ? p.id : typeof p.slug === "string" ? p.slug : slugify(inlineText(n.inline)));
1614
- } else if ((n.type === "prose" || n.type === "html") && typeof n.html === "string") {
1615
- for (const m of n.html.matchAll(/<h[1-6]\b[^>]*\sid="([^"]*)"/gi)) take(m[1]);
1616
- }
1617
- if (n.children) walk(n.children);
1618
- }
1619
- };
1620
- walk(nodes);
1621
- return ids;
1622
- }
1623
- var RESERVED_ID = /^(db-|tab-\d|tabpanel-\d|acc-\d|accpanel-\d|fn-|fnref-)/;
1624
- var autodocUnresolved = {
1625
- type: "autodoc-ref",
1626
- async resolve(node) {
1627
- const ref = typeof node.props?.ref === "string" ? node.props.ref : "";
1628
- return { warning: `:::autodoc${ref ? `{ref="${ref}"}` : ""} is not resolved in this build: resolve it at migration time (dogsbay migrate-mkdocs --inline-autodoc)` };
1629
- }
1630
- };
1631
- var coreLoad;
1632
- var loadCore = () => coreLoad ??= import("@dogsbay/format-openapi/core");
1633
- function openApiResolver(read, maxChars = 8e6) {
1634
- let specs;
1635
- return {
1636
- type: "openapi-ref",
1637
- async resolve(node, ctx) {
1638
- const spec = typeof node.props?.spec === "string" ? node.props.spec : "";
1639
- const core = await loadCore();
1640
- const selected = core.directiveOperation(node.props);
1641
- if ("error" in selected) return { warning: `:::openapi: ${selected.error}` };
1642
- const slug = ctx.source.replace(/\.(md|markdown)$/i, "").replace(/(^|\/)index$/, "");
1643
- const found = core.findSpec(spec, slug, (p) => {
1644
- const at = ctx.scope ? ctx.scope(p) : p;
1645
- return ctx.has(at) ? { path: at } : void 0;
1646
- });
1647
- if ("error" in found) return { warning: `:::openapi: ${found.error}` };
1648
- specs ??= core.createSpecCache(async (p) => {
1649
- const text = await read(p);
1650
- if (text === void 0) throw new Error("not found");
1651
- return text;
1652
- }, (text) => loadYaml2(text, { schema: CORE_SCHEMA }));
1653
- let prepared;
1654
- try {
1655
- prepared = await specs.get(found.path);
1656
- } catch (e) {
1657
- return { warning: `:::openapi: cannot read ${spec}: ${e.message}` };
1658
- }
1659
- let tree;
1660
- try {
1661
- tree = core.embedSpec(prepared, { usedIds: ctx.usedIds, operation: selected.operation }).tree;
1662
- } catch (e) {
1663
- return { warning: `:::openapi: cannot use ${spec}: ${e.message}` };
1664
- }
1665
- const size = boundedSize(tree, maxChars);
1666
- if (size === "cycle") return { warning: `:::openapi: ${spec} has a cyclic example or schema, which cannot be shown` };
1667
- if (size === "over") return { warning: `:::openapi: ${spec} is too large to embed in one page (over ${Math.round(maxChars / 1e6)}M characters)` };
1668
- return { nodes: tree };
1669
- }
1670
- };
1671
- }
1672
- async function openApiSourcePages(read, specPath, reservedSlugs) {
1673
- const core = await loadCore();
1674
- const specs = core.createSpecCache(async (p) => {
1675
- const text = await read(p);
1676
- if (text === void 0) throw new Error("not found");
1677
- return text;
1678
- }, (text) => loadYaml2(text, { schema: CORE_SCHEMA }));
1679
- try {
1680
- return core.specToPages(await specs.get(specPath), { reservedSlugs });
1681
- } catch (e) {
1682
- return { error: `cannot read ${specPath}: ${e.message}` };
1683
- }
1684
- }
1685
- async function specFileOf(path, has, read) {
1686
- const p = path.replace(/^\.\//, "").replace(/\/+$/, "");
1687
- if (/\.(ya?ml|json)$/i.test(p)) return has(p) ? p : void 0;
1688
- const dir = p === "." || p === "" ? "" : p + "/";
1689
- const found = ["openapi", "swagger", "spec"].flatMap((b) => ["yaml", "yml", "json"].map((e) => `${dir}${b}.${e}`)).filter(has);
1690
- for (const f of found) {
1691
- const head = (await read(f))?.slice(0, 2048) ?? "";
1692
- if (/(?:^|[{,])\s*"?(openapi|swagger)"?\s*[:=]/m.test(head)) return f;
1693
- }
1694
- return found[0];
1695
- }
1696
- function boundedSize(value, max) {
1697
- let total = 0;
1698
- const path = /* @__PURE__ */ new Set();
1699
- const walk = (v) => {
1700
- if (typeof v === "string") total += v.length;
1701
- else if (v && typeof v === "object") {
1702
- if (path.has(v)) return "cycle";
1703
- path.add(v);
1704
- total += 2;
1705
- for (const x of Array.isArray(v) ? v : Object.values(v)) {
1706
- const r = walk(x);
1707
- if (r) return r;
1708
- if (total > max) return "over";
1709
- }
1710
- path.delete(v);
1711
- } else total += 4;
1712
- return total > max ? "over" : void 0;
1713
- };
1714
- return walk(value) ?? total;
1715
- }
1716
-
1717
1790
  // src/theme/regions.ts
1718
1791
  import { attrs, esc } from "@dogsbay/render-html/html";
1719
1792
  var svg = (body, cls, size = 16, extra = "") => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="${cls}" aria-hidden="true" focusable="false"${extra}>${body}</svg>`;
@@ -3134,7 +3207,7 @@ async function build(input, hooks = {}) {
3134
3207
  phase.init = now() - t0;
3135
3208
  let pages = 0;
3136
3209
  let assets = 0;
3137
- const state = { redirects: [], navCache: navCache(), written: /* @__PURE__ */ new Set(), stubs: [], pageOut: /* @__PURE__ */ new Map(), sitemaps: /* @__PURE__ */ new Map(), supplied: /* @__PURE__ */ new Set(), usedMath: false, diagrams: diagramStore(input, cfg), scan: hooks.keep || hooks.lazy ? new ScanCache() : void 0, parseCache: input.parseCacheBytes === 0 ? false : new ParseCache(input.parseCacheBytes ?? DEFAULT_PARSE_CACHE_BYTES), templateBudget: templateBudget(input.untrusted === true, input.templateMs), dataCache: new DataCache(input.dataCacheBytes ?? DEFAULT_DATA_CACHE_BYTES) };
3210
+ const state = { redirects: [], notes: /* @__PURE__ */ new Map(), navCache: navCache(), written: /* @__PURE__ */ new Set(), stubs: [], pageOut: /* @__PURE__ */ new Map(), sitemaps: /* @__PURE__ */ new Map(), supplied: /* @__PURE__ */ new Set(), usedMath: false, diagrams: diagramStore(input, cfg), scan: hooks.keep || hooks.lazy ? new ScanCache() : void 0, parseCache: input.parseCacheBytes === 0 ? false : new ParseCache(input.parseCacheBytes ?? DEFAULT_PARSE_CACHE_BYTES), templateBudget: templateBudget(input.untrusted === true, input.templateMs), dataCache: new DataCache(input.dataCacheBytes ?? DEFAULT_DATA_CACHE_BYTES) };
3138
3211
  for (const r of cfg.redirects) {
3139
3212
  const from = sitePath(r.from, cfg.basePath);
3140
3213
  if (!from) {
@@ -3211,6 +3284,7 @@ async function build(input, hooks = {}) {
3211
3284
  files: out,
3212
3285
  warnings,
3213
3286
  problems,
3287
+ notes: [...state.notes.values()].flat(),
3214
3288
  config: cfg,
3215
3289
  stats: { pages, assets, units: units.length, ms: Math.round(now() - t0), msByPhase: roundAll(phase), highlightCache: highlighter.cacheStats(), ...state.parseCache ? { parseCache: state.parseCache.stats() } : {} }
3216
3290
  };
@@ -3523,7 +3597,7 @@ ${marker}${title}${marker}`, path, attributesOf(path), includesFor(path, read2),
3523
3597
  if (templatesOf(p).isPartial(fm, p)) continue;
3524
3598
  const inUnit = (s) => unit.prefix && (s === unit.prefix || s.startsWith(unit.prefix + "/")) ? s : [unit.prefix, s].filter(Boolean).join("/");
3525
3599
  const pageNs = named.length ? namespaceOf(p) : "";
3526
- const inScope = (s) => inUnit(pageNs && !(s === pageNs || s.startsWith(pageNs + "/")) ? prefixSlug([pageNs], s) : s);
3600
+ const inScope = (s) => inUnit(pageNs && !(s === pageNs || s.startsWith(pageNs + "/")) ? prefixSlug2([pageNs], s) : s);
3527
3601
  const target = (raw) => isExternal(raw) ? raw : inScope(sitePath(raw, cfg.basePath));
3528
3602
  const isIndexFile = /(^|\/)index\.(md|markdown)$/i.test(p) && slug !== "";
3529
3603
  if (typeof fm.redirect === "string" && fm.redirect.trim()) {
@@ -3595,36 +3669,48 @@ ${marker}${title}${marker}`, path, attributesOf(path), includesFor(path, read2),
3595
3669
  if (activeFiles.length) warn("html", `content.html is sanitize, so ${activeFiles.length} HTML, XML or script file(s) are not copied: ${some(activeFiles)}`);
3596
3670
  if (untrustedFiles.length) report("untrusted", `[untrusted] ${untrustedFiles.length} file(s) not copied: only images, fonts, media, stylesheets, documents and data are, and no SVG that runs script or host configuration (_headers, _redirects): ${some(untrustedFiles)}`);
3597
3671
  const specNavs = /* @__PURE__ */ new Map();
3598
- const combinedPrefix = [...urlPathOf(cfg.url), ...cfg.basePath.split("/").filter(Boolean)];
3672
+ const mountedNavs = /* @__PURE__ */ new Map();
3673
+ const mountedSlugs = /* @__PURE__ */ new Set();
3674
+ const notes = [];
3675
+ state.notes.set(unit.prefix, notes);
3599
3676
  for (const m of unit.members) {
3600
3677
  if (m.source.from !== "openapi") continue;
3601
- const readProject = async (p) => {
3602
- const c = input.files.get(p);
3603
- return c == null ? void 0 : readText(c);
3604
- };
3605
- const spec = await specFileOf(m.source.path, (p) => input.files.has(p), readProject);
3606
- if (!spec) {
3678
+ const placed = await placeApiPages(cfg, unit, m, input.files, readText);
3679
+ if ("missing" in placed) {
3607
3680
  warn("openapi", `${m.source.path}: no OpenAPI spec there (a .yaml, .yml or .json file, or a folder holding openapi.*, swagger.* or spec.*)`);
3608
3681
  continue;
3609
3682
  }
3610
- const ns = m.namespace;
3611
- const segs = ns ? [ns] : [];
3612
- const reserved = [...segs, ...combinedPrefix.slice(0, 1), ...globalPrefixes];
3613
- const got = await openApiSourcePages(readProject, spec, reserved);
3614
- if ("error" in got) {
3615
- warnFile("openapi", m.source.path, spec, got.error);
3683
+ const spec = placed.spec;
3684
+ if ("error" in placed) {
3685
+ warnFile("openapi", m.source.path, spec, placed.error);
3616
3686
  continue;
3617
3687
  }
3688
+ const ns = m.namespace;
3689
+ const mount = m.mount?.split("/");
3690
+ const segs = placed.segs;
3618
3691
  const limit = input.openApiChars ?? 8e6;
3619
- for (const page of got.pages) {
3620
- const slug = prefixSlug(segs, page.slug === "index" ? "" : page.slug);
3621
- if (infos.has(slug)) {
3692
+ const own = new Set(placed.pages.map((p) => p.page.slug));
3693
+ let label2 = placed.pages.find((p) => isMountRoot(p.page.slug))?.page.title ?? m.mount ?? "";
3694
+ for (const { slug, out, isIndex, page } of placed.pages) {
3695
+ if (mount) {
3696
+ const there = infos.get(slug);
3697
+ if (there && isMountRoot(page.slug) && !there.virtual) {
3698
+ notes.push(mountRootNote(m.mount, prefixSlug2(unit.prefix ? unit.prefix.split("/") : [], slug)));
3699
+ label2 = there.title;
3700
+ continue;
3701
+ }
3702
+ if (there) throw mountClash(slug, m.mount, srcOf(there), there.virtual ? `a page of ${srcOf(there)}` : `another page (${srcOf(there)})`);
3703
+ mountRefs(page.tree, mount, own);
3704
+ mountedSlugs.add(slug);
3705
+ } else if (mountedSlugs.has(slug)) {
3706
+ const by = unit.members.find((o) => o.mount && (slug === o.mount || slug.startsWith(o.mount + "/")))?.mount ?? "";
3707
+ throw mountClash(slug, by, spec, `a page of ${spec}`);
3708
+ } else if (infos.has(slug)) {
3622
3709
  warn("duplicate-page", `two sources map to the page "${slug || "/"}"; keeping ${srcOf(infos.get(slug))}, ignoring ${spec}'s`, { file: spec });
3623
3710
  continue;
3624
3711
  }
3625
- const isIndex = page.slug === "index" || got.pages.some((q) => q.slug.startsWith(page.slug + "/"));
3626
3712
  let tree = page.tree;
3627
- if (ns) rewriteContentRefs(tree, segs, globalPrefixes);
3713
+ if (ns && !mount) rewriteContentRefs(tree, segs, globalPrefixes);
3628
3714
  const size = boundedSize(tree, limit);
3629
3715
  if (typeof size !== "number") {
3630
3716
  warnFile("openapi", spec, spec, `the page ${slug || "/"} ${size === "cycle" ? "has a cyclic example or schema, which cannot be shown" : `is too large to build (over ${Math.round(limit / 1e6 * 10) / 10}M characters)`}`);
@@ -3633,13 +3719,17 @@ ${marker}${title}${marker}`, path, attributesOf(path), includesFor(path, read2),
3633
3719
  infos.set(slug, {
3634
3720
  slug,
3635
3721
  source: isIndex ? slug ? `${slug}/index.md` : "index.md" : `${slug}.md`,
3636
- out: outFileFor(slug, isIndex && slug !== ""),
3722
+ out,
3637
3723
  title: detach(page.title),
3638
3724
  draft: false,
3639
3725
  virtual: { tree, project: spec }
3640
3726
  });
3641
3727
  }
3642
- specNavs.set(ns, specNav(got.nav, segs));
3728
+ if (mount) {
3729
+ mountedNavs.set(m, { nav: specNav(placed.nav, segs), label: label2 });
3730
+ continue;
3731
+ }
3732
+ specNavs.set(ns, specNav(placed.nav, segs));
3643
3733
  const strays = [...infos.values()].filter((i) => !i.virtual && ns && (i.slug === ns || i.slug.startsWith(ns + "/")));
3644
3734
  if (strays.length) warn("openapi", `${strays.length} page(s) are in the folder of the OpenAPI source "${ns}", and are left out of the nav: ${strays.slice(0, 5).map(srcOf).join(", ")}${strays.length > 5 ? ", \u2026" : ""}`);
3645
3735
  }
@@ -3647,16 +3737,16 @@ ${marker}${title}${marker}`, path, attributesOf(path), includesFor(path, read2),
3647
3737
  const navLoad = state.scan ? (text) => state.scan.yaml(text, loadYaml) : void 0;
3648
3738
  const memberNav = async (ns) => {
3649
3739
  const dir2 = ns ? ns + "/" : "";
3650
- const member = unit.members.find((m) => m.namespace === ns);
3740
+ const member = unit.members.find((m) => m.namespace === ns && !m.mount);
3651
3741
  if (member?.source.nav) return ownNav(member.source, ns);
3652
3742
  const file = NAV_FILES.map((f) => dir2 + f).find((f) => files.has(f));
3653
3743
  const others = unit.members.filter((m) => m.namespace);
3654
- const mine = [...infos.values()].filter((i) => ns ? i.slug === ns || i.slug.startsWith(dir2) : !others.some((m) => i.slug === m.namespace || i.slug.startsWith(m.namespace + "/")));
3744
+ const mine = [...infos.values()].filter((i) => !mountedSlugs.has(i.slug) && (ns ? i.slug === ns || i.slug.startsWith(dir2) : !others.some((m) => i.slug === m.namespace || i.slug.startsWith(m.namespace + "/"))));
3655
3745
  const fromPages = () => ns ? prefixNav(navFromPages(mine.map((i) => ({ ...i, slug: i.slug === ns ? "" : i.slug.slice(dir2.length) }))), ns) : navFromPages(mine);
3656
3746
  if (!file) return { nav: fromPages() };
3657
3747
  try {
3658
3748
  const text = await read(file);
3659
- return { nav: file.endsWith(".json") ? prefixNav(parseNavJson(text, (slug) => labelFor(ns ? prefixSlug([ns], slug) : slug)), ns) : parseNavYaml(text, labelFor, ns, navLoad), file, project: abs(file) };
3749
+ return { nav: file.endsWith(".json") ? prefixNav(parseNavJson(text, (slug) => labelFor(ns ? prefixSlug2([ns], slug) : slug)), ns) : parseNavYaml(text, labelFor, ns, navLoad), file, project: abs(file) };
3660
3750
  } catch (e) {
3661
3751
  const mark = e.mark;
3662
3752
  warn("nav", `could not read ${file}: ${e.message}; using the directory layout`, {
@@ -3668,7 +3758,7 @@ ${marker}${title}${marker}`, path, attributesOf(path), includesFor(path, read2),
3668
3758
  };
3669
3759
  const ownNav = async (src, ns) => {
3670
3760
  const path = src.nav;
3671
- const nsLabel = (slug) => labelFor(ns ? prefixSlug([ns], slug) : slug);
3761
+ const nsLabel = (slug) => labelFor(ns ? prefixSlug2([ns], slug) : slug);
3672
3762
  const map = topicMaps.get(path);
3673
3763
  if (map) return { nav: prefixNav(parseNavJson(JSON.stringify(map.entries), nsLabel), ns), file: path, project: path, files: map.maps };
3674
3764
  const c = input.files.get(path);
@@ -3679,7 +3769,13 @@ ${marker}${title}${marker}`, path, attributesOf(path), includesFor(path, read2),
3679
3769
  let nav = [];
3680
3770
  const navFiles = [];
3681
3771
  const navProjectFiles = [];
3772
+ const host = unit.members.find((m) => !m.namespace && !m.mount && m.source.from !== "openapi");
3682
3773
  for (const m of unit.members) {
3774
+ const placed = mountedNavs.get(m);
3775
+ if (placed) {
3776
+ if (!host) nav.push(...placed.nav);
3777
+ continue;
3778
+ }
3683
3779
  if (m.source.from === "openapi") {
3684
3780
  const items = specNavs.get(m.namespace) ?? [];
3685
3781
  const group2 = namespaceNavGroup(m.namespace || void 0, items);
@@ -3688,6 +3784,7 @@ ${marker}${title}${marker}`, path, attributesOf(path), includesFor(path, read2),
3688
3784
  continue;
3689
3785
  }
3690
3786
  const got = await memberNav(m.namespace);
3787
+ if (m === host) for (const [mm, { nav: items, label: label2 }] of mountedNavs) got.nav = placeMounted(got.nav, items, mm.mount, label2);
3691
3788
  if (got.file) navFiles.push(got.file);
3692
3789
  if (got.project) navProjectFiles.push(got.project);
3693
3790
  const group = namespaceNavGroup(m.namespace || void 0, got.nav);
@@ -4596,22 +4693,28 @@ function postHeader(p, lang, t, asset) {
4596
4693
  function specNav(items, segs) {
4597
4694
  return items.map((it) => ({
4598
4695
  label: it.label,
4599
- ...it.href != null ? { slug: prefixSlug(segs, it.href.replace(/^\/+|\/+$/g, "")) } : {},
4696
+ ...it.href != null ? { slug: prefixSlug2(segs, it.href.replace(/^\/+|\/+$/g, "")) } : {},
4600
4697
  children: specNav(it.children ?? [], segs)
4601
4698
  }));
4602
4699
  }
4700
+ var PageClashError = class extends Error {
4701
+ constructor(message, file) {
4702
+ super(message);
4703
+ this.file = file;
4704
+ }
4705
+ };
4706
+ function mountClash(slug, mount, file, other) {
4707
+ return new PageClashError(`"${slug}" is both a page of the source mounted at "${mount}" and ${other}; move the page, or mount the source somewhere else`, file);
4708
+ }
4709
+ function placeMounted(host, mounted, mount, label) {
4710
+ const to = (nodes) => nodes.map((n) => ({ label: n.label, href: n.slug != null ? `/${n.slug}` : n.href, children: to(n.children), node: n }));
4711
+ const back = (items) => items.map((i) => i.node ? { ...i.node, children: back(i.children ?? []) } : { label: i.label, slug: i.href.replace(/^\/+|\/+$/g, ""), children: back(i.children ?? []) });
4712
+ return back(placeMountedNav(to(host), to(mounted), `/${mount}`, label, mount).nav);
4713
+ }
4603
4714
  function prefixNav(nodes, ns) {
4604
4715
  if (!ns) return nodes;
4605
4716
  return nodes.map((n) => ({ ...n, ...n.slug != null ? { slug: n.slug ? `${ns}/${n.slug}` : ns } : {}, children: prefixNav(n.children, ns) }));
4606
4717
  }
4607
- function urlPathOf(url) {
4608
- if (!url) return [];
4609
- try {
4610
- return new URL(url).pathname.split("/").filter(Boolean);
4611
- } catch {
4612
- return [];
4613
- }
4614
- }
4615
4718
  function dropMount(tree, mounts) {
4616
4719
  if (!mounts.length) return tree;
4617
4720
  const strip = (u) => {
@@ -5043,6 +5146,9 @@ async function openDevSession(input, o = {}) {
5043
5146
  sourceFor(path) {
5044
5147
  return unitForSitePath(path)?.handle.sourceForPage(path);
5045
5148
  },
5149
+ notes() {
5150
+ return [...ctx?.state.notes.values() ?? []].flat();
5151
+ },
5046
5152
  takeWarnings() {
5047
5153
  const all = ctx?.warnings ?? [];
5048
5154
  const fresh = all.slice(taken);
@@ -5287,7 +5393,7 @@ function inConfig(e, file) {
5287
5393
  function problemOf(e, text) {
5288
5394
  const err = e ?? {};
5289
5395
  const config = err.inConfig === true || e instanceof ConfigError;
5290
- const file = err.configFile;
5396
+ const file = err.configFile ?? (e instanceof PageClashError ? e.file : void 0);
5291
5397
  const at = typeof err.mark?.line === "number" ? { line: err.mark.line + 1, column: (err.mark.column ?? 0) + 1 } : {};
5292
5398
  return { code: config ? "config" : "build", ...file ? { file } : {}, ...at, message: err.reason || text.split("\n")[0], text };
5293
5399
  }
@@ -5366,6 +5472,7 @@ function openDevSession2(input, o) {
5366
5472
  }
5367
5473
  export {
5368
5474
  ConfigError,
5475
+ PageClashError,
5369
5476
  build2 as build,
5370
5477
  composePage,
5371
5478
  indexNav,
@@ -204,6 +204,8 @@ export interface BuiltPage {
204
204
  /** What the site-level files need from every unit. Small: a few strings per page. */
205
205
  export interface SiteState {
206
206
  redirects: RedirectInput[];
207
+ /** What the build says about the content, not a problem, by unit prefix (replaced when a unit is built again). */
208
+ notes: Map<string, string[]>;
207
209
  /** The sidebar items kept between pages (src/theme/regions.ts). */
208
210
  navCache: NavCache;
209
211
  /** Site path ("en/latest/guide/x") → output file relative to the site root. */
@@ -262,6 +264,11 @@ export interface BuildResult {
262
264
  warnings: string[];
263
265
  /** The warnings as data, one per warning, in the same order. */
264
266
  problems: BuildProblem[];
267
+ /**
268
+ * What the build says that is not a problem: a content page standing as a mounted API's root
269
+ * (@dogsbay/site-files' `mountRootNote`), once per mount. The CLI prints them as the Astro pipeline does.
270
+ */
271
+ notes: string[];
265
272
  stats: BuildStats;
266
273
  config: SiteConfig;
267
274
  }
@@ -402,6 +409,14 @@ export declare function selectUnits(allUnits: BuildUnit[], names: string[] | und
402
409
  * yes only loads KaTeX early; code is skipped, its `$` is not math.
403
410
  */
404
411
  export declare function mayHaveMath(v: unknown, inProps?: boolean): boolean;
412
+ /** A page an API mounted beside it would land on: the build stops, on that page's file. */
413
+ export declare class PageClashError extends Error {
414
+ /** The project path of the other page (or spec). */
415
+ readonly file: string;
416
+ constructor(message: string,
417
+ /** The project path of the other page (or spec). */
418
+ file: string);
419
+ }
405
420
  export interface LoadedTheme {
406
421
  templates: ThemeTemplates;
407
422
  themeCss?: string;
@@ -32,6 +32,12 @@ export interface SourceDef {
32
32
  name?: string;
33
33
  /** What the source holds: Dogsbay-MD pages (the default) or an OpenAPI spec, a page per operation. */
34
34
  from?: "dogsbay-md" | "openapi";
35
+ /**
36
+ * Where an OpenAPI source's pages go inside its version and locale (`reference/rest-api`), beside
37
+ * the Markdown it belongs to: its root page is the mount, and a content page there stands in for it
38
+ * (@dogsbay/site-files mounts.ts, plans/openapi-mount.md). Not with `name:`, which puts a source outside its version.
39
+ */
40
+ mount?: string;
35
41
  version?: string;
36
42
  locale?: string;
37
43
  attributes?: Record<string, unknown>;
@@ -258,6 +264,8 @@ export declare function unitOfPath<U extends {
258
264
  export interface UnitMember {
259
265
  source: SourceDef;
260
266
  namespace: string;
267
+ /** A mounted OpenAPI source's mount (`source.mount`): its pages go there, inside the unit. */
268
+ mount?: string;
261
269
  }
262
270
  /** One independent build: one or more content roots published under a locale/version prefix. */
263
271
  export interface BuildUnit {
@@ -43,6 +43,8 @@ export interface DevSession {
43
43
  change(paths: string[]): Promise<ChangeResult>;
44
44
  /** Warnings not handed out yet (the start's, then those of each change and page render). */
45
45
  takeWarnings(): string[];
46
+ /** What the site's build says that is not a problem (BuildResult.notes: a content page standing as a mounted API's root), as it stands now. */
47
+ notes(): string[];
46
48
  /**
47
49
  * The same warnings as data, not handed out yet (its own count: taking one kind leaves the other):
48
50
  * the file each is about as a project path, with its line when known, and a code.
@@ -0,0 +1,31 @@
1
+ import type { BuildUnit, SiteConfig, UnitMember } from "./config.js";
2
+ import type { FileContent } from "./build.js";
3
+ import { openApiSourcePages } from "./resolve.js";
4
+ type Got = Exclude<Awaited<ReturnType<typeof openApiSourcePages>>, {
5
+ error: string;
6
+ }>;
7
+ export interface PlacedApiPage {
8
+ /** In the unit: under the namespace or the mount (the root page is the mount itself). */
9
+ slug: string;
10
+ /** Output file relative to the unit: the root and the tag pages are folders of the pages under them (`api/pets/`). */
11
+ out: string;
12
+ isIndex: boolean;
13
+ page: Got["pages"][number];
14
+ }
15
+ export type PlacedApi = {
16
+ missing: true;
17
+ } | {
18
+ spec: string;
19
+ error: string;
20
+ } | {
21
+ spec: string;
22
+ pages: PlacedApiPage[];
23
+ nav: Got["nav"];
24
+ segs: string[];
25
+ };
26
+ /** The path segments of `site.url` (`https://x.github.io/docs/` → ["docs"]). */
27
+ export declare function urlPathOf(url: string | undefined): string[];
28
+ /** An OpenAPI member's pages, placed in its unit. */
29
+ export declare function placeApiPages(cfg: SiteConfig, unit: BuildUnit, m: UnitMember, files: Map<string, FileContent>, readText: (c: FileContent) => Promise<string>): Promise<PlacedApi>;
30
+ export declare function apiPageOuts(cfg: SiteConfig, unit: BuildUnit, m: UnitMember, files: Map<string, FileContent>, readText: (c: FileContent) => Promise<string>): Promise<[string, string][]>;
31
+ export {};
@@ -20,6 +20,7 @@ export declare function openDevSession(input: BuildInput & {
20
20
  }, o?: DevOptions): Promise<DevSession>;
21
21
  export { prebuiltSiteCss };
22
22
  export type { BuildInput, BuildResult, FileContent, OutputContent, SinkContent, SiteCssBuilder, BuiltPage, HighlightPool, BuildProblem, PageDiagnostic } from "../core/build.js";
23
+ export { PageClashError } from "../core/build.js";
23
24
  export type { ChangeResult, DevOptions, DevSession } from "../core/dev.js";
24
25
  export { parseConfig, projectRoots, ConfigError, type SiteConfig, type DiagramEngine } from "../core/config.js";
25
26
  export type { DiagramRenderer, DiagramRenderers, DiagramRequest, DiagramResult } from "../core/diagrams.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/ssg",
3
- "version": "0.2.0-beta.141",
3
+ "version": "0.2.0-beta.143",
4
4
  "type": "module",
5
5
  "description": "Documentation static site generator for Dogsbay-MD that builds inside Cloudflare Workers and publishes sites that work offline",
6
6
  "license": "Apache-2.0",
@@ -49,38 +49,38 @@
49
49
  "tailwindcss": "^4.3.2"
50
50
  },
51
51
  "peerDependencies": {
52
- "@dogsbay/elements": "0.2.0-beta.141",
53
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.141",
54
- "@dogsbay/format-openapi": "0.2.0-beta.141",
55
- "@dogsbay/format-topicmap": "0.2.0-beta.141",
56
- "@dogsbay/highlight": "0.2.0-beta.141",
57
- "@dogsbay/minja": "0.2.0-beta.141",
58
- "@dogsbay/primitives": "0.2.0-beta.141",
59
- "@dogsbay/render-html": "0.2.0-beta.141",
60
- "@dogsbay/search": "0.2.0-beta.141",
61
- "@dogsbay/serialize-core": "0.2.0-beta.141",
62
- "@dogsbay/site-check": "0.2.0-beta.141",
63
- "@dogsbay/site-files": "0.2.0-beta.141",
64
- "@dogsbay/types": "0.2.0-beta.141"
52
+ "@dogsbay/elements": "0.2.0-beta.143",
53
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.143",
54
+ "@dogsbay/format-openapi": "0.2.0-beta.143",
55
+ "@dogsbay/format-topicmap": "0.2.0-beta.143",
56
+ "@dogsbay/highlight": "0.2.0-beta.143",
57
+ "@dogsbay/minja": "0.2.0-beta.143",
58
+ "@dogsbay/primitives": "0.2.0-beta.143",
59
+ "@dogsbay/render-html": "0.2.0-beta.143",
60
+ "@dogsbay/search": "0.2.0-beta.143",
61
+ "@dogsbay/serialize-core": "0.2.0-beta.143",
62
+ "@dogsbay/site-check": "0.2.0-beta.143",
63
+ "@dogsbay/site-files": "0.2.0-beta.143",
64
+ "@dogsbay/types": "0.2.0-beta.143"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@axe-core/playwright": "^4.12.1",
68
68
  "@cloudflare/workers-types": "^5.20260921.1",
69
- "@dogsbay/elements": "0.2.0-beta.141",
70
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.141",
71
- "@dogsbay/format-openapi": "0.2.0-beta.141",
72
- "@dogsbay/format-topicmap": "0.2.0-beta.141",
73
- "@dogsbay/highlight": "0.2.0-beta.141",
74
- "@dogsbay/icons": "0.2.0-beta.141",
75
- "@dogsbay/minja": "0.2.0-beta.141",
76
- "@dogsbay/primitives": "0.2.0-beta.141",
77
- "@dogsbay/render-html": "0.2.0-beta.141",
78
- "@dogsbay/search": "0.2.0-beta.141",
79
- "@dogsbay/serialize-core": "0.2.0-beta.141",
80
- "@dogsbay/site-check": "0.2.0-beta.141",
81
- "@dogsbay/site-files": "0.2.0-beta.141",
82
- "@dogsbay/types": "0.2.0-beta.141",
83
- "@dogsbay/ui": "0.2.0-beta.141",
69
+ "@dogsbay/elements": "0.2.0-beta.143",
70
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.143",
71
+ "@dogsbay/format-openapi": "0.2.0-beta.143",
72
+ "@dogsbay/format-topicmap": "0.2.0-beta.143",
73
+ "@dogsbay/highlight": "0.2.0-beta.143",
74
+ "@dogsbay/icons": "0.2.0-beta.143",
75
+ "@dogsbay/minja": "0.2.0-beta.143",
76
+ "@dogsbay/primitives": "0.2.0-beta.143",
77
+ "@dogsbay/render-html": "0.2.0-beta.143",
78
+ "@dogsbay/search": "0.2.0-beta.143",
79
+ "@dogsbay/serialize-core": "0.2.0-beta.143",
80
+ "@dogsbay/site-check": "0.2.0-beta.143",
81
+ "@dogsbay/site-files": "0.2.0-beta.143",
82
+ "@dogsbay/types": "0.2.0-beta.143",
83
+ "@dogsbay/ui": "0.2.0-beta.143",
84
84
  "@mermanjs/web": "0.7.0",
85
85
  "@playwright/test": "1.61.1",
86
86
  "@types/node": "^22.10.0",