@doxbrix/doxloop 0.1.5 → 0.2.0

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.
Files changed (357) hide show
  1. package/CHANGELOG.md +1145 -0
  2. package/README.md +422 -227
  3. package/assets/doxbrix-preview.css +77 -15
  4. package/contracts/agent-events-v1.schema.json +1 -0
  5. package/contracts/coverage-v1.schema.json +59 -0
  6. package/contracts/drift-v1.schema.json +1 -0
  7. package/contracts/evaluation-v1.schema.json +62 -0
  8. package/contracts/quality-codes-v1.json +1 -0
  9. package/contracts/quality-report-v1.schema.json +1 -0
  10. package/contracts/validation-v1.schema.json +1 -0
  11. package/dist/agent-failure.d.ts +36 -0
  12. package/dist/agent-failure.js +98 -0
  13. package/dist/agent-isolation.d.ts +17 -0
  14. package/dist/agent-isolation.js +94 -0
  15. package/dist/agent-log.d.ts +197 -0
  16. package/dist/agent-log.js +1094 -0
  17. package/dist/agent-process.d.ts +63 -0
  18. package/dist/agent-process.js +128 -0
  19. package/dist/agent-reply.d.ts +47 -0
  20. package/dist/agent-reply.js +315 -0
  21. package/dist/agents.d.ts +24 -5
  22. package/dist/agents.js +60 -9
  23. package/dist/api-coverage.d.ts +11 -0
  24. package/dist/api-coverage.js +72 -0
  25. package/dist/application-probe.d.ts +28 -0
  26. package/dist/application-probe.js +93 -0
  27. package/dist/application-url.d.ts +14 -0
  28. package/dist/application-url.js +26 -0
  29. package/dist/args.js +15 -0
  30. package/dist/artifact-deploy.d.ts +9 -0
  31. package/dist/artifact-deploy.js +28 -8
  32. package/dist/assets.d.ts +72 -0
  33. package/dist/assets.js +382 -0
  34. package/dist/auth.d.ts +10 -0
  35. package/dist/auth.js +38 -9
  36. package/dist/author.d.ts +206 -4
  37. package/dist/author.js +1650 -52
  38. package/dist/authoring-batches.d.ts +190 -0
  39. package/dist/authoring-batches.js +489 -0
  40. package/dist/authoring-postpass.d.ts +79 -0
  41. package/dist/authoring-postpass.js +1289 -0
  42. package/dist/authoring-progress.d.ts +95 -0
  43. package/dist/authoring-progress.js +269 -0
  44. package/dist/autosync.d.ts +56 -0
  45. package/dist/autosync.js +442 -0
  46. package/dist/batch-artifacts.d.ts +81 -0
  47. package/dist/batch-artifacts.js +412 -0
  48. package/dist/batch-limits.d.ts +37 -0
  49. package/dist/batch-limits.js +80 -0
  50. package/dist/branding.d.ts +41 -0
  51. package/dist/branding.js +229 -0
  52. package/dist/bundle-upload.d.ts +26 -0
  53. package/dist/bundle-upload.js +99 -0
  54. package/dist/capture-auth.d.ts +95 -0
  55. package/dist/capture-auth.js +204 -0
  56. package/dist/capture.d.ts +48 -0
  57. package/dist/capture.js +20 -1
  58. package/dist/cli.js +839 -32
  59. package/dist/content-links.d.ts +10 -0
  60. package/dist/content-links.js +49 -0
  61. package/dist/contract-validation.d.ts +4 -0
  62. package/dist/contract-validation.js +25 -0
  63. package/dist/coverage-actions.d.ts +8 -0
  64. package/dist/coverage-actions.js +71 -0
  65. package/dist/coverage-resolutions.d.ts +16 -0
  66. package/dist/coverage-resolutions.js +58 -0
  67. package/dist/db.d.ts +37 -0
  68. package/dist/db.js +288 -0
  69. package/dist/demo.d.ts +12 -0
  70. package/dist/demo.js +122 -0
  71. package/dist/deploy-credentials.d.ts +7 -0
  72. package/dist/deploy-credentials.js +76 -0
  73. package/dist/deploy-targets/github-pages.d.ts +3 -0
  74. package/dist/deploy-targets/github-pages.js +100 -0
  75. package/dist/deploy-targets/index.d.ts +8 -0
  76. package/dist/deploy-targets/index.js +78 -0
  77. package/dist/deploy-targets/netlify.d.ts +3 -0
  78. package/dist/deploy-targets/netlify.js +49 -0
  79. package/dist/deploy-targets/types.d.ts +32 -0
  80. package/dist/deploy-targets/types.js +2 -0
  81. package/dist/deploy-targets/vercel.d.ts +3 -0
  82. package/dist/deploy-targets/vercel.js +95 -0
  83. package/dist/deploy.d.ts +9 -0
  84. package/dist/deploy.js +87 -13
  85. package/dist/deterministic-capture.d.ts +90 -0
  86. package/dist/deterministic-capture.js +435 -0
  87. package/dist/direct-edit.d.ts +40 -0
  88. package/dist/direct-edit.js +159 -0
  89. package/dist/docs-crawl.d.ts +86 -0
  90. package/dist/docs-crawl.js +536 -0
  91. package/dist/docs-site.d.ts +49 -0
  92. package/dist/docs-site.js +233 -0
  93. package/dist/doctor.js +8 -0
  94. package/dist/documentation-collections.d.ts +23 -0
  95. package/dist/documentation-collections.js +191 -0
  96. package/dist/documentation-plan.d.ts +253 -0
  97. package/dist/documentation-plan.js +2563 -0
  98. package/dist/doxbrix-build.d.ts +19 -0
  99. package/dist/doxbrix-build.js +167 -0
  100. package/dist/doxbrix-markdown.d.ts +9 -0
  101. package/dist/doxbrix-markdown.js +115 -14
  102. package/dist/drift.d.ts +10 -0
  103. package/dist/drift.js +164 -0
  104. package/dist/evaluation.d.ts +46 -0
  105. package/dist/evaluation.js +113 -0
  106. package/dist/evidence-pack.d.ts +47 -0
  107. package/dist/evidence-pack.js +358 -0
  108. package/dist/evidence.d.ts +25 -0
  109. package/dist/evidence.js +175 -0
  110. package/dist/fs.d.ts +8 -2
  111. package/dist/fs.js +41 -11
  112. package/dist/generator-api.d.ts +89 -0
  113. package/dist/generator-preflight.d.ts +27 -0
  114. package/dist/generator-preflight.js +105 -0
  115. package/dist/generator-runtime.d.ts +7 -0
  116. package/dist/generator-runtime.js +17 -1
  117. package/dist/generators.d.ts +24 -2
  118. package/dist/generators.js +54 -1
  119. package/dist/git-delivery.d.ts +17 -0
  120. package/dist/git-delivery.js +123 -0
  121. package/dist/globs.d.ts +16 -0
  122. package/dist/globs.js +65 -0
  123. package/dist/glossary.d.ts +26 -0
  124. package/dist/glossary.js +179 -0
  125. package/dist/history.d.ts +106 -0
  126. package/dist/history.js +600 -0
  127. package/dist/html-markdown.d.ts +46 -0
  128. package/dist/html-markdown.js +423 -0
  129. package/dist/interactive.js +3 -0
  130. package/dist/job-events.d.ts +74 -0
  131. package/dist/job-events.js +377 -0
  132. package/dist/keep-awake.d.ts +50 -0
  133. package/dist/keep-awake.js +123 -0
  134. package/dist/local-source-snapshot.d.ts +20 -0
  135. package/dist/local-source-snapshot.js +61 -0
  136. package/dist/mintlify-detect.d.ts +3 -0
  137. package/dist/mintlify-detect.js +18 -0
  138. package/dist/mintlify-import.d.ts +75 -0
  139. package/dist/mintlify-import.js +190 -0
  140. package/dist/navigation.d.ts +98 -0
  141. package/dist/navigation.js +310 -0
  142. package/dist/openapi.d.ts +60 -0
  143. package/dist/openapi.js +439 -0
  144. package/dist/page-editor-bridge.d.ts +3 -0
  145. package/dist/page-editor-bridge.js +109 -0
  146. package/dist/page-editor-preview.d.ts +10 -0
  147. package/dist/page-editor-preview.js +55 -0
  148. package/dist/page-extension.d.ts +9 -0
  149. package/dist/page-extension.js +15 -0
  150. package/dist/page-metadata.d.ts +28 -0
  151. package/dist/page-metadata.js +166 -0
  152. package/dist/page-operations.d.ts +34 -0
  153. package/dist/page-operations.js +215 -0
  154. package/dist/page-routes.d.ts +4 -0
  155. package/dist/page-routes.js +61 -0
  156. package/dist/pages.d.ts +20 -0
  157. package/dist/pages.js +184 -0
  158. package/dist/plan-generator.d.ts +3 -0
  159. package/dist/plan-generator.js +21 -0
  160. package/dist/plan-navigation.d.ts +11 -0
  161. package/dist/plan-navigation.js +30 -0
  162. package/dist/planning-captures.d.ts +20 -0
  163. package/dist/planning-captures.js +143 -0
  164. package/dist/planning-research.d.ts +135 -0
  165. package/dist/planning-research.js +472 -0
  166. package/dist/planning-triage.d.ts +23 -0
  167. package/dist/planning-triage.js +131 -0
  168. package/dist/preview.d.ts +24 -0
  169. package/dist/preview.js +280 -29
  170. package/dist/project-detect.d.ts +36 -0
  171. package/dist/project-detect.js +251 -0
  172. package/dist/project-import.d.ts +54 -0
  173. package/dist/project-import.js +157 -0
  174. package/dist/project-lock.d.ts +6 -0
  175. package/dist/project-lock.js +96 -0
  176. package/dist/project-registry.d.ts +25 -0
  177. package/dist/project-registry.js +79 -0
  178. package/dist/project.d.ts +35 -3
  179. package/dist/project.js +388 -38
  180. package/dist/prompts.d.ts +9 -0
  181. package/dist/prompts.js +25 -0
  182. package/dist/proposal-replay.d.ts +32 -0
  183. package/dist/proposal-replay.js +99 -0
  184. package/dist/quality-claims.d.ts +8 -0
  185. package/dist/quality-claims.js +168 -0
  186. package/dist/quality-config.d.ts +5 -0
  187. package/dist/quality-config.js +84 -0
  188. package/dist/quality-contract.d.ts +37 -0
  189. package/dist/quality-contract.js +46 -0
  190. package/dist/quality-examples.d.ts +4 -0
  191. package/dist/quality-examples.js +233 -0
  192. package/dist/quality-gates.d.ts +16 -0
  193. package/dist/quality-gates.js +192 -0
  194. package/dist/quality-links.d.ts +7 -0
  195. package/dist/quality-links.js +149 -0
  196. package/dist/quality-lint.d.ts +6 -0
  197. package/dist/quality-lint.js +124 -0
  198. package/dist/quality-rendered.d.ts +11 -0
  199. package/dist/quality-rendered.js +222 -0
  200. package/dist/quality-schema.d.ts +3 -0
  201. package/dist/quality-schema.js +71 -0
  202. package/dist/release-notes.d.ts +44 -0
  203. package/dist/release-notes.js +183 -0
  204. package/dist/remote-monitor.d.ts +16 -0
  205. package/dist/remote-monitor.js +74 -0
  206. package/dist/remote-source.d.ts +34 -0
  207. package/dist/remote-source.js +426 -0
  208. package/dist/review-diff.d.ts +82 -0
  209. package/dist/review-diff.js +400 -0
  210. package/dist/review-learning.d.ts +11 -0
  211. package/dist/review-learning.js +60 -0
  212. package/dist/review-render.d.ts +38 -0
  213. package/dist/review-render.js +224 -0
  214. package/dist/review-report.d.ts +9 -0
  215. package/dist/review-report.js +89 -0
  216. package/dist/review-ui.d.ts +14 -0
  217. package/dist/review-ui.js +1248 -0
  218. package/dist/schedule.d.ts +78 -0
  219. package/dist/schedule.js +480 -0
  220. package/dist/screen-capture-provider.d.ts +52 -0
  221. package/dist/screen-capture-provider.js +218 -0
  222. package/dist/screenshot-workflow.d.ts +167 -0
  223. package/dist/screenshot-workflow.js +1237 -0
  224. package/dist/settings.d.ts +1 -1
  225. package/dist/settings.js +95 -7
  226. package/dist/site-export.d.ts +18 -0
  227. package/dist/site-export.js +87 -0
  228. package/dist/source-connectors.d.ts +33 -0
  229. package/dist/source-connectors.js +268 -0
  230. package/dist/source-discovery.d.ts +132 -0
  231. package/dist/source-discovery.js +823 -0
  232. package/dist/source-intelligence.d.ts +9 -0
  233. package/dist/source-intelligence.js +306 -0
  234. package/dist/sync-review.d.ts +28 -0
  235. package/dist/sync-review.js +264 -0
  236. package/dist/sync-runs.d.ts +192 -0
  237. package/dist/sync-runs.js +2244 -0
  238. package/dist/sync.d.ts +35 -0
  239. package/dist/sync.js +298 -32
  240. package/dist/text-diff.d.ts +9 -0
  241. package/dist/text-diff.js +59 -0
  242. package/dist/types.d.ts +946 -1
  243. package/dist/ui/assets/doxloop-logo-light-De7Nx7j7.png +0 -0
  244. package/dist/ui/assets/index-BHBYU2aG.css +1 -0
  245. package/dist/ui/assets/index-Cq3RPQiC.js +33 -0
  246. package/dist/ui/index.html +18 -0
  247. package/dist/ui-server.d.ts +83 -0
  248. package/dist/ui-server.js +3532 -0
  249. package/dist/usage-budget.d.ts +28 -0
  250. package/dist/usage-budget.js +90 -0
  251. package/dist/validation.d.ts +25 -1
  252. package/dist/validation.js +312 -26
  253. package/dist/workspace-tools.d.ts +54 -0
  254. package/dist/workspace-tools.js +123 -0
  255. package/docs/agent-compatibility.md +49 -32
  256. package/docs/ci-and-automation.md +105 -66
  257. package/docs/doxbrix-http-api.md +8 -1
  258. package/docs/existing-documentation.md +80 -0
  259. package/docs/generation-performance.md +108 -0
  260. package/docs/generator-authoring.md +68 -5
  261. package/docs/generator-selection.md +50 -13
  262. package/docs/mintlify-import.md +71 -0
  263. package/docs/openapi-security.md +25 -0
  264. package/docs/project-format.md +331 -36
  265. package/docs/release-quality.md +158 -0
  266. package/docs/releasing.md +72 -0
  267. package/docs/review-workflows.md +51 -0
  268. package/docs/security-model.md +102 -39
  269. package/docs/troubleshooting.md +226 -93
  270. package/package.json +46 -15
  271. package/scripts/test-auto-screenshot.mjs +172 -0
  272. package/skills/doxloop-authoring/SKILL.md +242 -361
  273. package/skills/doxloop-authoring/references/existing-documentation.md +94 -0
  274. package/skills/doxloop-authoring/references/navigation-architecture.md +18 -7
  275. package/skills/doxloop-authoring/references/page-depth.md +169 -0
  276. package/skills/doxloop-authoring/references/project-format.md +106 -7
  277. package/skills/doxloop-authoring/references/quality.md +10 -0
  278. package/skills/doxloop-authoring/references/screenshot-manifest.md +113 -0
  279. package/skills/doxloop-authoring/references/screenshots.md +149 -235
  280. package/skills/doxloop-authoring/references/workflows.md +76 -0
  281. package/skills/doxloop-doxbrix/SKILL.md +38 -22
  282. package/skills/doxloop-doxbrix/references/api-endpoints.md +15 -14
  283. package/skills/doxloop-doxbrix/references/components.md +30 -3
  284. package/skills/doxloop-doxbrix/references/manifest.md +3 -2
  285. package/vendor/doxbrix-import/LICENSE +202 -0
  286. package/vendor/doxbrix-import/README.md +17 -0
  287. package/vendor/doxbrix-import/UPSTREAM.json +47 -0
  288. package/vendor/doxbrix-import/dist/docs/frontmatter.d.ts +13 -0
  289. package/vendor/doxbrix-import/dist/docs/frontmatter.js +83 -0
  290. package/vendor/doxbrix-import/dist/docs/import.d.ts +21 -0
  291. package/vendor/doxbrix-import/dist/docs/import.js +147 -0
  292. package/vendor/doxbrix-import/dist/docs/manifest.d.ts +163 -0
  293. package/vendor/doxbrix-import/dist/docs/manifest.js +64 -0
  294. package/vendor/doxbrix-import/dist/docs/project.d.ts +25 -0
  295. package/vendor/doxbrix-import/dist/docs/project.js +77 -0
  296. package/vendor/doxbrix-import/dist/docs/starter.d.ts +4 -0
  297. package/vendor/doxbrix-import/dist/docs/starter.js +11 -0
  298. package/vendor/doxbrix-import/dist/importer.d.ts +227 -0
  299. package/vendor/doxbrix-import/dist/importer.js +1567 -0
  300. package/vendor/doxbrix-import/dist/mintlify-openapi.d.ts +37 -0
  301. package/vendor/doxbrix-import/dist/mintlify-openapi.js +305 -0
  302. package/vendor/doxbrix-import/dist/safe-path.d.ts +9 -0
  303. package/vendor/doxbrix-import/dist/safe-path.js +47 -0
  304. package/dist/agents.d.ts.map +0 -1
  305. package/dist/agents.js.map +0 -1
  306. package/dist/args.d.ts.map +0 -1
  307. package/dist/args.js.map +0 -1
  308. package/dist/artifact-deploy.d.ts.map +0 -1
  309. package/dist/artifact-deploy.js.map +0 -1
  310. package/dist/auth.d.ts.map +0 -1
  311. package/dist/auth.js.map +0 -1
  312. package/dist/author.d.ts.map +0 -1
  313. package/dist/author.js.map +0 -1
  314. package/dist/capture.d.ts.map +0 -1
  315. package/dist/capture.js.map +0 -1
  316. package/dist/cli.d.ts.map +0 -1
  317. package/dist/cli.js.map +0 -1
  318. package/dist/deploy.d.ts.map +0 -1
  319. package/dist/deploy.js.map +0 -1
  320. package/dist/deployment-visibility.d.ts.map +0 -1
  321. package/dist/deployment-visibility.js.map +0 -1
  322. package/dist/doctor.d.ts.map +0 -1
  323. package/dist/doctor.js.map +0 -1
  324. package/dist/doxbrix-markdown.d.ts.map +0 -1
  325. package/dist/doxbrix-markdown.js.map +0 -1
  326. package/dist/errors.d.ts.map +0 -1
  327. package/dist/errors.js.map +0 -1
  328. package/dist/fs.d.ts.map +0 -1
  329. package/dist/fs.js.map +0 -1
  330. package/dist/generator-api.d.ts.map +0 -1
  331. package/dist/generator-api.js.map +0 -1
  332. package/dist/generator-manager.d.ts.map +0 -1
  333. package/dist/generator-manager.js.map +0 -1
  334. package/dist/generator-runtime.d.ts.map +0 -1
  335. package/dist/generator-runtime.js.map +0 -1
  336. package/dist/generators.d.ts.map +0 -1
  337. package/dist/generators.js.map +0 -1
  338. package/dist/interactive.d.ts.map +0 -1
  339. package/dist/interactive.js.map +0 -1
  340. package/dist/preview.d.ts.map +0 -1
  341. package/dist/preview.js.map +0 -1
  342. package/dist/progress.d.ts.map +0 -1
  343. package/dist/progress.js.map +0 -1
  344. package/dist/project.d.ts.map +0 -1
  345. package/dist/project.js.map +0 -1
  346. package/dist/prompts.d.ts.map +0 -1
  347. package/dist/prompts.js.map +0 -1
  348. package/dist/settings.d.ts.map +0 -1
  349. package/dist/settings.js.map +0 -1
  350. package/dist/sync.d.ts.map +0 -1
  351. package/dist/sync.js.map +0 -1
  352. package/dist/types.d.ts.map +0 -1
  353. package/dist/types.js.map +0 -1
  354. package/dist/validation.d.ts.map +0 -1
  355. package/dist/validation.js.map +0 -1
  356. package/dist/version.d.ts.map +0 -1
  357. package/dist/version.js.map +0 -1
package/dist/preview.d.ts CHANGED
@@ -7,16 +7,40 @@ interface PreviewOptions {
7
7
  open: boolean;
8
8
  }
9
9
  export declare function startPreview(options: PreviewOptions): Promise<void>;
10
+ /**
11
+ * A stored redirect is followed only when its target is a page the site can
12
+ * serve. Redirects are written when a plan is approved, before the pages are
13
+ * authored, so a target may name a planned path the author never used; falling
14
+ * through to the normal page lookup beats sending readers to "Page not found".
15
+ */
16
+ export declare function redirectTarget(redirect: string | undefined, site: DoxbrixSiteConfig, pages: Map<string, string>): string | undefined;
10
17
  export declare function doxbrixDocument(input: {
11
18
  site: DoxbrixSiteConfig;
12
19
  title: string;
13
20
  description?: string;
21
+ canonical?: string;
22
+ socialImage?: string;
14
23
  current: string;
24
+ docVersion?: string;
15
25
  rendered: {
16
26
  html: string;
17
27
  toc: TocEntry[];
18
28
  };
29
+ verification?: {
30
+ state: string;
31
+ verifiedOn?: string;
32
+ revisions: Record<string, string>;
33
+ locale: string;
34
+ };
35
+ editorUrl?: string;
36
+ embedded?: boolean;
37
+ workspace?: boolean;
38
+ /** URL prefix used by static exports hosted below an origin, such as GitHub project pages. */
39
+ basePath?: string;
40
+ /** Static builds have no preview event stream. */
41
+ liveReload?: boolean;
19
42
  }): string;
43
+ export declare function firstSitePage(site: DoxbrixSiteConfig): string | undefined;
20
44
  export declare function previewErrorPage(error: unknown): string;
21
45
  export {};
22
46
  //# sourceMappingURL=preview.d.ts.map
package/dist/preview.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { readRedirects } from './page-operations.js';
1
2
  import { spawn } from 'node:child_process';
2
3
  import { readFile } from 'node:fs/promises';
3
4
  import { createServer } from 'node:http';
@@ -5,9 +6,12 @@ import { extname, isAbsolute, relative, resolve } from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
6
7
  import chokidar from 'chokidar';
7
8
  import { renderMarkdown } from './doxbrix-markdown.js';
9
+ import { DoxloopError } from './errors.js';
8
10
  import { resolveContainedDirectory } from './fs.js';
11
+ import { loadQualityConfig } from './quality-config.js';
12
+ import { reverifyClaims } from './quality-claims.js';
9
13
  import { loadGeneratorAdapter } from './generators.js';
10
- import { loadPages, loadProject, loadSiteConfig, pageId, readPage, siteConfigPath, } from './project.js';
14
+ import { loadPages, loadProject, loadSiteConfig, pageId, readPage, ROOT_CONTENT_IGNORED_DIRECTORIES, siteConfigPath, } from './project.js';
11
15
  const PACKAGE_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
12
16
  const DOXBRIX_CSS = resolve(PACKAGE_ROOT, 'assets', 'doxbrix-preview.css');
13
17
  const ATLAS_LAYOUT = {
@@ -44,12 +48,19 @@ export async function startPreview(options) {
44
48
  await startDoxbrixPreview(options, project);
45
49
  }
46
50
  async function startDoxbrixPreview(options, project) {
47
- const contentRoot = await resolveContainedDirectory(options.root, project.contentDir, 'Preview content directory');
51
+ const contentRoot = await resolveContainedDirectory(options.root, project.contentDir, 'Preview content directory', { allowRoot: project.generator === 'doxbrix' });
48
52
  const css = await readFile(DOXBRIX_CSS, 'utf8');
49
53
  const clients = new Set();
54
+ const ignoredDirectories = contentRoot === resolve(options.root)
55
+ ? ROOT_CONTENT_IGNORED_DIRECTORIES
56
+ : undefined;
50
57
  const server = createServer(async (request, response) => {
51
58
  try {
52
59
  const url = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`);
60
+ if (url.pathname === '/__doxloop/identity') {
61
+ send(response, 200, 'application/json; charset=utf-8', JSON.stringify({ root: resolve(options.root) }));
62
+ return;
63
+ }
53
64
  if (url.pathname === '/__doxloop/events') {
54
65
  response.writeHead(200, {
55
66
  'Content-Type': 'text/event-stream',
@@ -65,7 +76,7 @@ async function startDoxbrixPreview(options, project) {
65
76
  send(response, 200, 'text/css; charset=utf-8', css);
66
77
  return;
67
78
  }
68
- const staticPath = safeStaticPath(contentRoot, url.pathname);
79
+ const staticPath = safeStaticPath(contentRoot, url.pathname, ignoredDirectories);
69
80
  if (staticPath && STATIC_TYPES[extname(staticPath).toLowerCase()]) {
70
81
  try {
71
82
  send(response, 200, STATIC_TYPES[extname(staticPath).toLowerCase()], await readFile(staticPath));
@@ -75,11 +86,19 @@ async function startDoxbrixPreview(options, project) {
75
86
  // A missing static asset may still be an extension-bearing documentation route.
76
87
  }
77
88
  }
78
- const [pages, site] = await Promise.all([
89
+ const [pages, site, qualityConfig, verificationMetadata] = await Promise.all([
79
90
  loadPages(options.root, project),
80
91
  loadSiteConfig(options.root, project),
92
+ loadQualityConfig(options.root),
93
+ loadQualityConfig(options.root).then((config) => config.readerVerification?.enabled ? reverifyClaims(options.root, project, false).then((result) => result.metadata) : undefined),
81
94
  ]);
82
95
  const pagesById = new Map(pages.map((path) => [pageId(contentRoot, path), path]));
96
+ const redirect = redirectTarget((await readRedirects(options.root))[url.pathname.replace(/\/$/, '') || '/'], site, pagesById);
97
+ if (redirect) {
98
+ response.writeHead(302, { Location: redirect });
99
+ response.end();
100
+ return;
101
+ }
83
102
  if (url.pathname === '/__doxloop/search-index') {
84
103
  const entries = await Promise.all([...pagesById].map(async ([id, path]) => {
85
104
  const page = await readPage(path);
@@ -104,8 +123,17 @@ async function startDoxbrixPreview(options, project) {
104
123
  site,
105
124
  title: page.title || labelFromId(requested),
106
125
  ...(page.description ? { description: page.description } : {}),
126
+ ...(page.canonical ? { canonical: page.canonical } : {}),
127
+ ...(page.socialImage ? { socialImage: page.socialImage } : {}),
107
128
  current: requested,
129
+ ...(url.searchParams.get('version') ? { docVersion: url.searchParams.get('version') } : {}),
130
+ ...(localEditorUrl(process.env.DOXLOOP_CONTROL_CENTER_URL, relative(options.root, pagesById.get(requested))) ? { editorUrl: localEditorUrl(process.env.DOXLOOP_CONTROL_CENTER_URL, relative(options.root, pagesById.get(requested))) } : {}),
108
131
  rendered,
132
+ ...(url.searchParams.get('embed') === 'page' ? { embedded: true } : {}),
133
+ ...(url.searchParams.get('workspace') === '1' ? { workspace: true } : {}),
134
+ ...(qualityConfig.readerVerification?.enabled && verificationMetadata?.pages[relative(options.root, pagesById.get(requested)).replace(/\\/g, '/')]
135
+ ? { verification: verificationMetadata.pages[relative(options.root, pagesById.get(requested)).replace(/\\/g, '/')] }
136
+ : {}),
109
137
  }));
110
138
  }
111
139
  catch (error) {
@@ -121,7 +149,12 @@ async function startDoxbrixPreview(options, project) {
121
149
  if (options.open)
122
150
  openBrowser(url);
123
151
  const configPath = await siteConfigPath(options.root, project);
124
- const watcher = chokidar.watch([contentRoot, configPath], { ignoreInitial: true });
152
+ const watcher = chokidar.watch([contentRoot, configPath], {
153
+ ignoreInitial: true,
154
+ ...(ignoredDirectories
155
+ ? { ignored: (path) => isIgnoredDirectoryPath(contentRoot, path, ignoredDirectories) }
156
+ : {}),
157
+ });
125
158
  watcher.on('all', () => {
126
159
  for (const client of clients)
127
160
  client.write('event: reload\ndata: now\n\n');
@@ -139,16 +172,40 @@ async function startDoxbrixPreview(options, project) {
139
172
  process.once('SIGINT', () => void stop());
140
173
  process.once('SIGTERM', () => void stop());
141
174
  }
175
+ /**
176
+ * A stored redirect is followed only when its target is a page the site can
177
+ * serve. Redirects are written when a plan is approved, before the pages are
178
+ * authored, so a target may name a planned path the author never used; falling
179
+ * through to the normal page lookup beats sending readers to "Page not found".
180
+ */
181
+ export function redirectTarget(redirect, site, pages) {
182
+ if (!redirect)
183
+ return undefined;
184
+ const target = requestedPage(redirect, site, pages);
185
+ return target !== undefined && pages.has(target) ? redirect : undefined;
186
+ }
142
187
  function requestedPage(pathname, site, pages) {
143
188
  if (pathname === '/' || pathname === '') {
144
189
  return firstSitePage(site) ?? pages.keys().next().value;
145
190
  }
146
- return decodeURIComponent(pathname.replace(/^\/+|\/+$/g, '')).replace(/\.(md|mdx)$/i, '');
191
+ const id = decodeURIComponent(pathname.replace(/^\/+|\/+$/g, '')).replace(/\.(md|mdx)$/i, '');
192
+ return pages.has(id) ? id : pages.has(`${id}/index`) ? `${id}/index` : id;
147
193
  }
148
194
  export function doxbrixDocument(input) {
149
- const visibleSpaces = input.site.spaces.filter((space) => hasVisibleNavigation(space.nav));
195
+ const versions = input.site.versions ?? [];
196
+ const defaultVersion = defaultDocVersion(input.site);
197
+ const contexts = (versions.length ? versions.map((entry) => entry.version) : [undefined]).map((version) => {
198
+ const spaces = input.site.spaces.filter((space) => (!versions.length || (space.version ?? defaultVersion) === version) && hasVisibleNavigation(space.nav));
199
+ return { version, spaces, matching: spaces.find((space) => containsPage(space.nav, input.current)) };
200
+ });
201
+ const context = contexts.find((entry) => entry.version === input.docVersion && entry.matching)
202
+ ?? contexts.find((entry) => entry.version === defaultVersion && entry.matching)
203
+ ?? contexts.find((entry) => entry.matching)
204
+ ?? contexts.find((entry) => entry.version === defaultVersion)
205
+ ?? contexts[0];
206
+ const visibleSpaces = context.spaces;
150
207
  const showTabs = visibleSpaces.length > 1;
151
- const activeSpace = visibleSpaces.find((space) => containsPage(space.nav, input.current)) ??
208
+ const activeSpace = context.matching ??
152
209
  visibleSpaces[0];
153
210
  const primary = themeColor(input.site, 'primaryColor', '#6366f1');
154
211
  const primaryLight = themeColor(input.site, 'lightColor', primary);
@@ -174,14 +231,14 @@ export function doxbrixDocument(input) {
174
231
  themeString(input.site, 'favicon', '') ||
175
232
  themeString(input.site, 'faviconDark', '');
176
233
  const siteName = input.site.name?.trim() || 'Documentation';
177
- const tabs = visibleSpaces
234
+ const tabsFor = (entry) => entry.spaces
178
235
  .map((space) => {
179
236
  const first = firstPage(space.nav);
180
- const active = space === activeSpace ? ' active' : '';
237
+ const active = space === (entry.matching ?? entry.spaces[0]) ? ' active' : '';
181
238
  const tabIcon = typeof space.icon === 'string' && space.icon.trim()
182
239
  ? space.icon
183
240
  : spaceIcon(space.name);
184
- const tabTag = typeof space.tag === 'string' && space.tag
241
+ const tabTag = space.tag
185
242
  ? `<span class="dxb-atlas-tab-tag">${escapeHtml(space.tag)}</span>`
186
243
  : '';
187
244
  const label = `<span class="dxb-atlas-tab-icon">${brandIcon(tabIcon, 16)}</span><span>${escapeHtml(space.name)}</span>${tabTag}`;
@@ -190,11 +247,16 @@ export function doxbrixDocument(input) {
190
247
  : `<span class="dxb-atlas-tab${active}">${label}</span>`;
191
248
  })
192
249
  .join('');
193
- const first = firstSitePage(input.site) ?? '';
250
+ const first = firstPageInVersion(input.site, context.version) ?? firstSitePage(input.site) ?? '';
194
251
  const logoHref = safeHref(themeString(input.site, 'logoHref', `/${first}`), `/${first}`);
195
- const leftnav = activeSpace
196
- ? `<aside class="dp-leftnav"><div class="dp-nav-tree">${navTree(activeSpace.nav, input.current)}</div></aside>`
197
- : '';
252
+ const leftnav = contexts.map((entry) => {
253
+ const space = entry.matching ?? entry.spaces[0];
254
+ if (!space)
255
+ return '';
256
+ const attrs = versions.length ? ` data-doc-version="${escapeAttr(entry.version)}" data-current-version-page="${Boolean(entry.matching)}" data-version-eyebrow="${escapeAttr(groupContainingPage(space.nav, input.current) ?? '')}"` : '';
257
+ return `<aside class="dp-leftnav" aria-label="Documentation navigation"${attrs}${entry !== context ? ' hidden' : ''}>${versionSwitcher(input.site, entry.version)}<div class="dp-nav-tree">${navTree(space.nav, input.current)}</div></aside>`;
258
+ }).join('');
259
+ const tabs = contexts.filter((entry) => entry.spaces.length > 1).map((entry) => `<div class="dxb-atlas-tabs-bar"${versions.length ? ` data-doc-version="${escapeAttr(entry.version)}"` : ''}${entry !== context ? ' hidden' : ''}><nav class="dxb-atlas-tabs" aria-label="Documentation spaces">${tabsFor(entry)}</nav></div>`).join('');
198
260
  const toc = tocHtml(input.rendered.toc);
199
261
  const groupLabel = activeSpace
200
262
  ? groupContainingPage(activeSpace.nav, input.current)
@@ -204,6 +266,11 @@ export function doxbrixDocument(input) {
204
266
  const faviconLink = favicon
205
267
  ? `<link rel="icon" href="${escapeAttr(favicon)}">`
206
268
  : '';
269
+ const metadata = [
270
+ input.description ? `<meta name="description" content="${escapeAttr(input.description)}">` : '',
271
+ input.canonical ? `<link rel="canonical" href="${escapeAttr(input.canonical)}">` : '',
272
+ input.socialImage ? `<meta property="og:image" content="${escapeAttr(input.socialImage)}">` : '',
273
+ ].filter(Boolean).join('\n ');
207
274
  const logo = logoLight || logoDark
208
275
  ? `<span class="dp-topnav-logo-img-wrap">${logoLight ? `<img class="dp-topnav-logo-img dp-topnav-logo-img--light${logoDark ? ' has-dark' : ''}" src="${escapeAttr(logoLight)}" alt="${escapeAttr(siteName)}">` : ''}${logoDark ? `<img class="dp-topnav-logo-img dp-topnav-logo-img--dark${logoLight ? ' has-light' : ''}" src="${escapeAttr(logoDark)}" alt="${escapeAttr(siteName)}">` : ''}</span>`
209
276
  : `<span class="dp-topnav-logo-mark" aria-hidden="true">${icon('book', 16)}</span><span class="dp-topnav-logo-text">${escapeHtml(siteName)}</span>`;
@@ -213,12 +280,14 @@ export function doxbrixDocument(input) {
213
280
  const eyebrow = groupLabel
214
281
  ? `<div class="dxb-atlas-eyebrow">${escapeHtml(groupLabel)}</div>`
215
282
  : '';
216
- return `<!doctype html>
283
+ const verification = input.verification ? verificationBadge(input.verification) : '';
284
+ const document = `<!doctype html>
217
285
  <html lang="en">
218
286
  <head>
219
287
  <meta charset="utf-8">
220
288
  <meta name="viewport" content="width=device-width, initial-scale=1">
221
289
  <title>${escapeHtml(input.title)} · ${escapeHtml(siteName)}</title>
290
+ ${metadata}
222
291
  <link rel="preconnect" href="https://fonts.googleapis.com">
223
292
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
224
293
  ${fontStylesheet}
@@ -227,6 +296,7 @@ export function doxbrixDocument(input) {
227
296
  <style>
228
297
  ${fontFaces}
229
298
  body{margin:0}.dp-tab-panel{display:none}.dp-tab-panel.active{display:block}
299
+ .dxb-verification{display:inline-flex;align-items:center;gap:8px;margin-top:12px;padding:6px 9px;border:1px solid #cbd5e1;border-radius:999px;font-size:12px;color:#475569;background:#f8fafc}.dxb-verification strong{font-size:12px}.dxb-verification--verified{border-color:#99e1cb;color:#08745d;background:#effcf8}.dxb-verification--contradicted,.dxb-verification--needs-human{border-color:#fecaca;color:#a61b29;background:#fff5f5}
230
300
  .dp-nav-chevron{transition:transform .15s}.dp-nav-chevron:not(.open){transform:rotate(-90deg)}
231
301
  .dp-root--published{--project-primary:${escapeAttr(primary)};--project-primary-hover:${escapeAttr(primary)};
232
302
  --project-primary-rgb:${hexToRgb(primary)};--primary:${escapeAttr(primary)};--primary-rgb:${hexToRgb(primary)};
@@ -243,7 +313,7 @@ export function doxbrixDocument(input) {
243
313
  </style>
244
314
  </head>
245
315
  <body>
246
- <div class="dp-root dp-root--published" data-color-theme="${resolvedMode}" data-project-color-theme="${escapeAttr(mode)}" data-code-theme="${codeTheme}" data-shell-theme="atlas">
316
+ <div class="dp-root dp-root--published${input.embedded ? ' dp-root--embedded' : ''}" data-color-theme="${resolvedMode}" data-project-color-theme="${escapeAttr(mode)}" data-code-theme="${codeTheme}" data-shell-theme="atlas">
247
317
  <header class="dxb-atlas-header">
248
318
  <div class="dxb-atlas-header-main"><div class="dxb-atlas-header-inner">
249
319
  <a class="dp-topnav-logo" href="${escapeAttr(logoHref)}">${logo}</a>
@@ -262,13 +332,13 @@ export function doxbrixDocument(input) {
262
332
  <button class="dp-theme-toggle" type="button" aria-label="Switch color theme" aria-pressed="${resolvedMode === 'dark'}" data-theme-toggle>${icon(resolvedMode === 'dark' ? 'sun' : 'moon', 17)}</button>
263
333
  </div>
264
334
  </div></div>
265
- ${showTabs ? `<div class="dxb-atlas-tabs-bar"><nav class="dxb-atlas-tabs" aria-label="Documentation spaces">${tabs}</nav></div>` : ''}
335
+ ${tabs}
266
336
  </header>
267
337
  <div class="dp-body">
268
338
  ${leftnav}
269
- <main class="dp-main"><div class="dp-content-wrap">
339
+ <main class="dp-main"><div class="dp-content-wrap">${input.editorUrl ? `<a class="dp-edit-page" href="${escapeHtml(input.editorUrl)}" target="_blank" rel="noopener">Edit this page in Doxloop</a>` : ''}
270
340
  <div class="dxb-atlas-title-row">
271
- <div class="dxb-atlas-title-copy">${eyebrow}<h1 class="dp-page-title">${escapeHtml(input.title)}</h1>${description}</div>
341
+ <div class="dxb-atlas-title-copy">${eyebrow}<h1 class="dp-page-title">${escapeHtml(input.title)}</h1>${description}${verification}</div>
272
342
  <button class="dxb-atlas-copy-page" type="button" data-copy-page>${icon('copy', 16)}<span>Copy page</span>${icon('chevron-down', 14)}</button>
273
343
  </div>
274
344
  <div class="dp-blocks">${input.rendered.html}</div>
@@ -289,6 +359,14 @@ export function doxbrixDocument(input) {
289
359
  </div>
290
360
  </div>
291
361
  <script>
362
+ ${input.embedded && input.workspace ? `document.addEventListener('click', function(event) {
363
+ const link = event.target.closest('a[href]');
364
+ if (!link) return;
365
+ const target = new URL(link.href, location.href);
366
+ if (target.origin !== location.origin || (target.pathname === location.pathname && target.hash)) return;
367
+ event.preventDefault();
368
+ parent.postMessage({ type: 'doxloop:preview-navigate', href: target.href }, '*');
369
+ });` : ''}
292
370
  const root = document.querySelector('.dp-root--published');
293
371
  let colorTheme = root?.dataset.projectColorTheme || 'system';
294
372
  try {
@@ -346,8 +424,21 @@ export function doxbrixDocument(input) {
346
424
  updateActiveToc();
347
425
  addEventListener('scroll', scheduleTocUpdate, { passive: true });
348
426
  addEventListener('resize', scheduleTocUpdate);
349
- const events = new EventSource('/__doxloop/events');
350
- events.addEventListener('reload', () => location.reload());
427
+ ${input.liveReload === false ? 'const events = null;' : "const events = new EventSource('/__doxloop/events');"}
428
+ events?.addEventListener('reload', () => location.reload());
429
+ function closePreviewEvents() {
430
+ events?.close();
431
+ }
432
+ addEventListener('pagehide', closePreviewEvents, { once: true });
433
+ addEventListener('beforeunload', closePreviewEvents, { once: true });
434
+ document.addEventListener('click', (event) => {
435
+ const link = event.target instanceof Element ? event.target.closest('a[href]') : null;
436
+ if (!link || event.defaultPrevented || link.target === '_blank' || link.hasAttribute('download')) return;
437
+ const target = new URL(link.href, location.href);
438
+ const currentWithoutHash = location.origin + location.pathname + location.search;
439
+ const targetWithoutHash = target.origin + target.pathname + target.search;
440
+ if (target.origin === location.origin && targetWithoutHash !== currentWithoutHash) closePreviewEvents();
441
+ }, { capture: true });
351
442
  const previewNotice = document.querySelector('[data-preview-notice]');
352
443
  const previewAssistant = document.querySelector('[data-preview-assistant]');
353
444
  let previewNoticeTimer;
@@ -733,13 +824,70 @@ export function doxbrixDocument(input) {
733
824
  const active = searchResults?.querySelector('.dxb-preview-search-result.active');
734
825
  if (active) {
735
826
  event.preventDefault();
827
+ closePreviewEvents();
736
828
  location.href = active.href;
737
829
  }
738
830
  }
739
831
  });
740
832
  </script>
833
+ ${MERMAID_PREVIEW_SCRIPT}
834
+ ${versions.length ? versionContextScript(layout.tabsHeight) : ''}
741
835
  </body>
742
836
  </html>`;
837
+ return prefixStaticReferences(document, input.basePath);
838
+ }
839
+ /** Prefix root-relative reader URLs without touching external or fragment links. */
840
+ function prefixStaticReferences(document, rawBasePath) {
841
+ if (rawBasePath === undefined)
842
+ return document;
843
+ const basePath = normalizeBasePath(rawBasePath);
844
+ if (!basePath)
845
+ return document;
846
+ return document
847
+ .replaceAll('href="/', `href="${basePath}/`)
848
+ .replaceAll('src="/', `src="${basePath}/`)
849
+ .replaceAll("fetch('/", `fetch('${basePath}/`)
850
+ .replaceAll("new EventSource('/", `new EventSource('${basePath}/`);
851
+ }
852
+ function normalizeBasePath(value) {
853
+ const trimmed = value.trim();
854
+ if (!trimmed || trimmed === '/')
855
+ return '';
856
+ if (/^[a-z][a-z\d+.-]*:/i.test(trimmed) || trimmed.startsWith('//')) {
857
+ throw new DoxloopError('Static build base path must be an origin-relative URL path.');
858
+ }
859
+ const normalized = `/${trimmed.replace(/^\/+|\/+$/g, '')}`;
860
+ if (normalized.split('/').some((part) => part === '..')) {
861
+ throw new DoxloopError('Static build base path cannot leave its URL root.');
862
+ }
863
+ return normalized;
864
+ }
865
+ /**
866
+ * Render `<Mermaid>` blocks in the local preview. The published site renders
867
+ * diagrams itself; without this the preview showed the diagram source as
868
+ * plain text, so a concept page looked broken before it was ever published.
869
+ * Loaded only when a page contains a diagram, and a failed load (offline)
870
+ * leaves the readable source in place.
871
+ */
872
+ const MERMAID_PREVIEW_SCRIPT = `<script type="module">
873
+ const diagrams = document.querySelectorAll('pre.mermaid');
874
+ if (diagrams.length > 0) {
875
+ try {
876
+ const { default: mermaid } = await import('https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs');
877
+ const dark = document.querySelector('.dp-root--published')?.dataset.colorTheme === 'dark';
878
+ mermaid.initialize({ startOnLoad: false, theme: dark ? 'dark' : 'neutral', securityLevel: 'strict' });
879
+ await mermaid.run({ nodes: diagrams });
880
+ } catch (error) {
881
+ console.warn('Doxloop preview could not render Mermaid diagrams; the published site renders them.', error);
882
+ }
883
+ }
884
+ </script>`;
885
+ function verificationBadge(verification) {
886
+ const label = verification.state === 'verified' ? 'Verified' : verification.state === 'inferred' ? 'Evidence inferred' : verification.state === 'contradicted' ? 'Contradicted — review required' : 'Needs review';
887
+ const revisions = Object.entries(verification.revisions).map(([source, revision]) => `${source} ${revision.slice(0, 12)}`).join(', ');
888
+ const date = verification.verifiedOn ? new Intl.DateTimeFormat(verification.locale, { dateStyle: 'medium', timeZone: 'UTC' }).format(new Date(verification.verifiedOn)) : '';
889
+ const detail = [date, revisions].filter(Boolean).join(' · ');
890
+ return `<aside class="dxb-verification dxb-verification--${escapeAttr(verification.state)}" aria-label="Documentation verification"><strong>${escapeHtml(label)}</strong>${detail ? `<span>${escapeHtml(detail)}</span>` : ''}</aside>`;
743
891
  }
744
892
  function navTree(nodes, current, depth = 0) {
745
893
  return nodes
@@ -755,9 +903,12 @@ function navTree(nodes, current, depth = 0) {
755
903
  return `<a class="dp-nav-item${depthClass(depth)}" href="${escapeAttr(safeNavigationHref(node.href))}">${navIcon(node.icon)}<span class="dp-nav-item-label">${escapeHtml(node.title)}</span></a>`;
756
904
  }
757
905
  if (node.type === 'api') {
758
- return `<div class="dp-nav-item${depthClass(depth)}">${navIcon(node.icon)}<span class="dp-nav-item-label">${escapeHtml(node.title)}</span></div>`;
906
+ return `<a class="dp-nav-item${depthClass(depth)}" href="${escapeAttr(apiSpecHref(node.spec))}" target="_blank" rel="noreferrer">${navIcon(node.icon)}<span class="dp-nav-item-label">${escapeHtml(node.title)}</span></a>`;
759
907
  }
760
908
  if (node.type === 'group') {
909
+ // An empty header is nothing to click into; hide it rather than render a bare label.
910
+ if (!hasVisibleNavigation(node.items))
911
+ return '';
761
912
  const children = navTree(node.items, current, depth + 1);
762
913
  const activeBranch = containsPage(node.items, current) ? ' active-branch' : '';
763
914
  const groupIcon = navIcon(node.icon);
@@ -773,8 +924,8 @@ function navTree(nodes, current, depth = 0) {
773
924
  }
774
925
  function tocHtml(entries) {
775
926
  if (entries.length === 0)
776
- return '<aside class="dp-toc"></aside>';
777
- return `<aside class="dp-toc"><div class="dp-toc-header">${icon('list', 16)} On this page</div>${entries
927
+ return '<aside class="dp-toc" aria-label="On this page"></aside>';
928
+ return `<aside class="dp-toc" aria-label="On this page"><div class="dp-toc-header">${icon('list', 16)} On this page</div>${entries
778
929
  .map((entry, index) => `<a class="dp-toc-entry level-${entry.level}${index === 0 ? ' active' : ''}" href="#${escapeAttr(entry.id)}">${escapeHtml(entry.title)}</a>`)
779
930
  .join('')}</aside>`;
780
931
  }
@@ -804,7 +955,68 @@ function firstPage(nodes) {
804
955
  }
805
956
  return undefined;
806
957
  }
807
- function firstSitePage(site) {
958
+ function defaultDocVersion(site) {
959
+ const versions = site.versions ?? [];
960
+ return (versions.find((entry) => entry.default === true || entry.isDefault === true) ?? versions[0])?.version;
961
+ }
962
+ function firstPageInVersion(site, version) {
963
+ for (const space of site.spaces) {
964
+ if (site.versions?.length && (space.version ?? defaultDocVersion(site)) !== version)
965
+ continue;
966
+ const first = firstPage(space.nav);
967
+ if (first)
968
+ return first;
969
+ }
970
+ return undefined;
971
+ }
972
+ // Mirrors the Doxbrix CLI reader: default/isDefault and a sidebar dropdown.
973
+ function versionSwitcher(site, version) {
974
+ const versions = site.versions ?? [];
975
+ if (versions.length < 2)
976
+ return '';
977
+ const active = versions.find((entry) => entry.version === version) ?? versions[0];
978
+ const label = (entry) => `<span class="dp-version-label">${escapeHtml(entry.label ?? entry.version)}</span>${entry.tag ? `<span class="dp-version-tag">${escapeHtml(entry.tag)}</span>` : ''}`;
979
+ const options = versions.map((entry) => {
980
+ const first = firstPageInVersion(site, entry.version);
981
+ return first
982
+ ? `<a class="dp-version-option${entry === active ? ' active' : ''}" href="/${escapeAttr(first)}?version=${escapeAttr(encodeURIComponent(entry.version))}"${entry === active ? ' aria-current="true"' : ''}>${label(entry)}</a>`
983
+ : `<span class="dp-version-option" aria-disabled="true">${label(entry)}</span>`;
984
+ }).join('');
985
+ return `<details class="dp-version-switcher"><summary class="dp-version-trigger" aria-label="Documentation version: ${escapeAttr(active.label ?? active.version)}">${label(active)}${icon('chevron-down', 12)}</summary><nav class="dp-version-menu" aria-label="Documentation versions">${options}</nav></details>`;
986
+ }
987
+ function versionContextScript(tabsHeight) {
988
+ // Shared pages have one static HTML file. Keep the selected version in their URL
989
+ // and select the matching pre-rendered navigation for previews and static hosts.
990
+ return `<script>
991
+ (() => {
992
+ const sides = [...document.querySelectorAll('aside[data-doc-version]')];
993
+ const requested = new URL(location.href).searchParams.get('version');
994
+ const side = sides.find((entry) => entry.dataset.docVersion === requested && entry.dataset.currentVersionPage === 'true') || sides.find((entry) => !entry.hidden);
995
+ if (!side) return;
996
+ const version = side.dataset.docVersion;
997
+ document.querySelectorAll('[data-doc-version]').forEach((entry) => { entry.hidden = entry.dataset.docVersion !== version; });
998
+ const tabs = [...document.querySelectorAll('.dxb-atlas-tabs-bar')].find((entry) => !entry.hidden);
999
+ document.querySelector('.dp-root')?.style.setProperty('--dxb-tabs-height', tabs ? '${tabsHeight}px' : '0px');
1000
+ const eyebrow = document.querySelector('.dxb-atlas-eyebrow');
1001
+ if (eyebrow) eyebrow.textContent = side.dataset.versionEyebrow || '';
1002
+ const logo = document.querySelector('.dp-topnav-logo');
1003
+ const home = side.querySelector('.dp-version-option.active');
1004
+ if (logo && home) logo.href = home.href;
1005
+ const preserve = (link) => {
1006
+ if (!link || link.closest('.dp-version-switcher') || !link.getAttribute('href') || link.getAttribute('href').startsWith('#')) return;
1007
+ const url = new URL(link.href, location.href);
1008
+ if (url.origin !== location.origin || /\\.[a-z0-9]+$/i.test(url.pathname) && !/\\.mdx?$/i.test(url.pathname)) return;
1009
+ if (!url.searchParams.has('version')) { url.searchParams.set('version', version); link.href = url.href; }
1010
+ };
1011
+ document.querySelectorAll('a[href]').forEach(preserve);
1012
+ document.addEventListener('click', (event) => preserve(event.target.closest('a[href]')), true);
1013
+ })();
1014
+ </script>`;
1015
+ }
1016
+ export function firstSitePage(site) {
1017
+ const defaultFirst = firstPageInVersion(site, defaultDocVersion(site));
1018
+ if (defaultFirst)
1019
+ return defaultFirst;
808
1020
  for (const space of site.spaces) {
809
1021
  const first = firstPage(space.nav);
810
1022
  if (first)
@@ -1014,7 +1226,9 @@ function googleFontStylesheet(families) {
1014
1226
  return '';
1015
1227
  const query = unique
1016
1228
  .slice(0, 3)
1017
- .map((family) => `family=${encodeURIComponent(family)}:wght@400;500;600;700;800`)
1229
+ .map((family) => family.toLowerCase() === 'inter'
1230
+ ? 'family=Inter:ital,wght@0,100..900;1,100..900'
1231
+ : `family=${encodeURIComponent(family)}:wght@400;500;600;700;800`)
1018
1232
  .join('&');
1019
1233
  return `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?${query}&display=swap">`;
1020
1234
  }
@@ -1042,6 +1256,19 @@ function safeNavigationHref(value) {
1042
1256
  }
1043
1257
  return '#';
1044
1258
  }
1259
+ function apiSpecHref(value) {
1260
+ const trimmed = value.trim();
1261
+ if (/^https?:\/\//i.test(trimmed))
1262
+ return trimmed;
1263
+ if (trimmed === '' ||
1264
+ trimmed.startsWith('//') ||
1265
+ trimmed.includes('\\') ||
1266
+ trimmed.split(/[/?#]/).includes('..')) {
1267
+ return '#';
1268
+ }
1269
+ const local = trimmed.replace(/^\.\//, '');
1270
+ return local.startsWith('/') ? local : `/${local}`;
1271
+ }
1045
1272
  function depthClass(depth) {
1046
1273
  return depth === 0 ? '' : ` dp-nav-d${Math.min(depth, 3)}`;
1047
1274
  }
@@ -1052,7 +1279,7 @@ function labelFromId(id) {
1052
1279
  .map((word) => `${word[0]?.toUpperCase() ?? ''}${word.slice(1)}`)
1053
1280
  .join(' ');
1054
1281
  }
1055
- function safeStaticPath(contentRoot, pathname) {
1282
+ function safeStaticPath(contentRoot, pathname, ignoredDirectories) {
1056
1283
  let decoded;
1057
1284
  try {
1058
1285
  decoded = decodeURIComponent(pathname);
@@ -1062,7 +1289,16 @@ function safeStaticPath(contentRoot, pathname) {
1062
1289
  }
1063
1290
  const path = resolve(contentRoot, decoded.replace(/^\/+/, ''));
1064
1291
  const rel = relative(contentRoot, path);
1065
- return rel === '' || rel.startsWith('..') || isAbsolute(rel) ? undefined : path;
1292
+ return rel === '' || rel.startsWith('..') || isAbsolute(rel)
1293
+ || isIgnoredDirectoryPath(contentRoot, path, ignoredDirectories)
1294
+ ? undefined
1295
+ : path;
1296
+ }
1297
+ function isIgnoredDirectoryPath(contentRoot, path, ignoredDirectories) {
1298
+ if (!ignoredDirectories)
1299
+ return false;
1300
+ const first = relative(contentRoot, path).split(/[\\/]/)[0];
1301
+ return first !== undefined && ignoredDirectories.has(first);
1066
1302
  }
1067
1303
  function errorPage(requested, site) {
1068
1304
  return `<!doctype html><meta charset="utf-8"><title>Page not found</title><body style="font-family:system-ui;padding:40px"><h1>Page not found</h1><p>No Doxbrix page matches <code>/${escapeHtml(requested)}</code> in ${escapeHtml(site.name ?? 'this project')}.</p></body>`;
@@ -1146,4 +1382,19 @@ function openBrowser(url) {
1146
1382
  const child = spawn(command, args, { detached: true, stdio: 'ignore' });
1147
1383
  child.unref();
1148
1384
  }
1385
+ function localEditorUrl(origin, path) {
1386
+ if (!origin)
1387
+ return undefined;
1388
+ try {
1389
+ const url = new URL(origin);
1390
+ if (url.protocol !== 'http:' || !['127.0.0.1', 'localhost'].includes(url.hostname) || url.username || url.password)
1391
+ return undefined;
1392
+ url.pathname = '/pages';
1393
+ url.search = new URLSearchParams({ path: path.replace(/\\/g, '/'), edit: '1' }).toString();
1394
+ return url.toString();
1395
+ }
1396
+ catch {
1397
+ return undefined;
1398
+ }
1399
+ }
1149
1400
  //# sourceMappingURL=preview.js.map
@@ -0,0 +1,36 @@
1
+ import type { GeneratorName } from './types.js';
2
+ /**
3
+ * One way the folder could be read. `contentDir` is the directory Doxloop
4
+ * would treat as documentation content, relative to the folder; an empty
5
+ * string means the folder itself, which only Doxbrix allows.
6
+ */
7
+ export interface GeneratorCandidate {
8
+ generator: GeneratorName;
9
+ contentDir: string;
10
+ markers: string[];
11
+ title?: string;
12
+ }
13
+ export interface GeneratorDetection {
14
+ /** Most specific marker first. Empty when nothing recognizable was found. */
15
+ candidates: GeneratorCandidate[];
16
+ recommended?: GeneratorCandidate;
17
+ }
18
+ /**
19
+ * Page extensions per generator when the adapter package cannot be loaded,
20
+ * so a folder can be inspected before anything is installed.
21
+ */
22
+ export declare const DEFAULT_PAGE_EXTENSIONS: Record<GeneratorName, readonly string[]>;
23
+ /**
24
+ * Recognize an existing documentation site from the files its generator
25
+ * needs. Detection only reads; it never writes into the folder. Order matters
26
+ * where markers overlap: a Docusaurus or MkDocs site may also carry a Jekyll
27
+ * style `_config.yml`, so the more specific generator is listed first.
28
+ */
29
+ export declare function detectDocumentationGenerator(root: string): Promise<GeneratorDetection>;
30
+ /**
31
+ * Project-relative page paths under a content directory, using the
32
+ * generator's default extensions. Missing directories yield no pages instead
33
+ * of an error so an inspection can report "0 pages" for a wrong guess.
34
+ */
35
+ export declare function listDocumentationPageFiles(root: string, generator: GeneratorName, contentDir: string): Promise<string[]>;
36
+ //# sourceMappingURL=project-detect.d.ts.map