@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
@@ -0,0 +1,1567 @@
1
+ import { materializeMintlifyOpenApiPages, materializeOpenApiNavigationDocument } from './mintlify-openapi.js';
2
+ const PAGE_EXTENSIONS = ['.md', '.mdx', '.markdown'];
3
+ const CONFIG_NAMES = ['mint.json', 'docs.json'];
4
+ const NON_PAGE_DIRS = new Set([
5
+ 'snippets', '_snippets', 'includes', '_includes', 'partials', '_partials',
6
+ 'node_modules', '.git', '.github', '.doxbrix', '.agents', '.claude', '.gemini', '.idea', '.vscode',
7
+ ]);
8
+ const ROOT_REPOSITORY_FILES = new Set(['readme', 'contributing', 'agents', 'code_of_conduct', 'code-of-conduct', 'security', 'license']);
9
+ function normalizePath(path) {
10
+ const out = [];
11
+ for (const part of path.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '').split('/')) {
12
+ if (!part || part === '.')
13
+ continue;
14
+ if (part === '..')
15
+ out.pop();
16
+ else
17
+ out.push(part);
18
+ }
19
+ return out.join('/');
20
+ }
21
+ function dirname(path) {
22
+ const normalized = normalizePath(path);
23
+ const index = normalized.lastIndexOf('/');
24
+ return index < 0 ? '' : normalized.slice(0, index);
25
+ }
26
+ function basename(path) {
27
+ const normalized = normalizePath(path);
28
+ return normalized.slice(normalized.lastIndexOf('/') + 1);
29
+ }
30
+ function joinPath(base, path) {
31
+ return normalizePath(base ? `${base}/${path}` : path);
32
+ }
33
+ function isPageFile(path) {
34
+ const lower = path.toLowerCase();
35
+ return PAGE_EXTENSIONS.some((ext) => lower.endsWith(ext));
36
+ }
37
+ function stripPageExtension(path) {
38
+ for (const ext of PAGE_EXTENSIONS)
39
+ if (path.toLowerCase().endsWith(ext))
40
+ return path.slice(0, -ext.length);
41
+ return path;
42
+ }
43
+ function asRecord(value) {
44
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
45
+ }
46
+ function asString(value) {
47
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
48
+ }
49
+ function asNumber(value) {
50
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
51
+ }
52
+ function normalizeHex(value) {
53
+ const raw = asString(value);
54
+ if (!raw || !/^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i.test(raw))
55
+ return undefined;
56
+ if (raw.length === 7)
57
+ return raw;
58
+ return `#${raw.slice(1).split('').map((char) => `${char}${char}`).join('')}`;
59
+ }
60
+ function slug(value) {
61
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'docs';
62
+ }
63
+ function mergeObjects(base, override) {
64
+ if (!base || typeof base !== 'object' || Array.isArray(base))
65
+ return override;
66
+ if (!override || typeof override !== 'object' || Array.isArray(override))
67
+ return override;
68
+ const out = { ...base };
69
+ for (const [key, value] of Object.entries(override)) {
70
+ if (key === '$ref')
71
+ continue;
72
+ out[key] = key in out ? mergeObjects(out[key], value) : value;
73
+ }
74
+ return out;
75
+ }
76
+ /** Resolve Mintlify's JSON `$ref` files without executing code or leaving the source tree. */
77
+ function resolveJsonRefs(value, currentPath, files, stack = new Set()) {
78
+ if (Array.isArray(value))
79
+ return value.map((item) => resolveJsonRefs(item, currentPath, files, stack));
80
+ if (!value || typeof value !== 'object')
81
+ return value;
82
+ const record = value;
83
+ let resolved = record;
84
+ const ref = asString(record.$ref);
85
+ if (ref && !ref.startsWith('#')) {
86
+ const refPath = joinPath(dirname(currentPath), ref.split('#')[0]);
87
+ if (!stack.has(refPath)) {
88
+ const source = files.get(refPath);
89
+ if (source) {
90
+ try {
91
+ const nextStack = new Set(stack).add(refPath);
92
+ resolved = mergeObjects(resolveJsonRefs(JSON.parse(source), refPath, files, nextStack), record);
93
+ }
94
+ catch {
95
+ // Keep the sibling configuration when the referenced JSON is invalid.
96
+ }
97
+ }
98
+ }
99
+ }
100
+ const out = {};
101
+ for (const [key, child] of Object.entries(asRecord(resolved))) {
102
+ if (key !== '$ref')
103
+ out[key] = resolveJsonRefs(child, currentPath, files, stack);
104
+ }
105
+ return out;
106
+ }
107
+ function pageNode(entry) {
108
+ if (typeof entry === 'string')
109
+ return [{ type: 'page', file: stripPageExtension(normalizePath(entry)) }];
110
+ const obj = asRecord(entry);
111
+ const page = asString(obj.page);
112
+ if (page) {
113
+ return [{
114
+ type: 'page',
115
+ file: stripPageExtension(normalizePath(page)),
116
+ ...(asString(obj.title) ? { title: asString(obj.title) } : {}),
117
+ ...(asString(obj.icon) ? { icon: asString(obj.icon) } : {}),
118
+ ...(obj.hidden === true ? { hidden: true } : {}),
119
+ }];
120
+ }
121
+ const href = asString(obj.href ?? obj.url);
122
+ const linkLabel = asString(obj.label ?? obj.anchor ?? obj.name);
123
+ if (href && linkLabel && !Array.isArray(obj.pages) && !Array.isArray(obj.groups)) {
124
+ return [{ type: 'link', title: linkLabel, href, ...(asString(obj.icon) ? { icon: asString(obj.icon) } : {}) }];
125
+ }
126
+ const root = asString(obj.root);
127
+ const children = [
128
+ ...(root ? pageNode(root) : []),
129
+ ...(Array.isArray(obj.pages) ? obj.pages.flatMap(pageNode) : []),
130
+ ...apiNode(obj),
131
+ ];
132
+ const group = asString(obj.group);
133
+ if (Object.hasOwn(obj, 'group')) {
134
+ if (!group)
135
+ return children;
136
+ return [{ type: 'group', label: group, ...(asString(obj.icon) ? { icon: asString(obj.icon) } : {}), items: children }];
137
+ }
138
+ return children;
139
+ }
140
+ function normalizeSpecSource(source) {
141
+ return /^https?:\/\//i.test(source) ? source : normalizePath(source);
142
+ }
143
+ function openApiSource(value) {
144
+ return asString(value) ?? asString(asRecord(value).source);
145
+ }
146
+ function apiNode(container) {
147
+ const openapi = openApiSource(container.openapi);
148
+ return openapi
149
+ ? [{
150
+ type: 'api',
151
+ title: asString(container.apiTitle ?? container.api_title) ?? 'API Reference',
152
+ spec: normalizeSpecSource(openapi),
153
+ ...(asString(container.icon) ? { icon: asString(container.icon) } : {}),
154
+ }]
155
+ : [];
156
+ }
157
+ function directNav(container) {
158
+ const groups = Array.isArray(container.groups) ? container.groups.flatMap(pageNode) : [];
159
+ const pages = Array.isArray(container.pages) ? container.pages.flatMap(pageNode) : [];
160
+ return [...groups, ...pages, ...apiNode(container)];
161
+ }
162
+ function childContainers(value) {
163
+ return Array.isArray(value) ? value.map(asRecord).filter((entry) => Object.keys(entry).length > 0) : [];
164
+ }
165
+ /** Preserve every language/version/product/tab instead of silently selecting the first branch. */
166
+ function collectSpaces(navigation, initialName) {
167
+ const spaces = [];
168
+ const importVersions = [];
169
+ const globalLinks = [];
170
+ const visit = (raw, ctx, depth) => {
171
+ if (depth > 12)
172
+ return;
173
+ if (Array.isArray(raw))
174
+ raw = { groups: raw };
175
+ const nav = asRecord(raw);
176
+ if (!Object.keys(nav).length)
177
+ return;
178
+ const global = asRecord(nav.global);
179
+ for (const entry of [...childContainers(global.anchors), ...childContainers(global.links)]) {
180
+ globalLinks.push(...pageNode(entry));
181
+ }
182
+ const languages = childContainers(nav.languages);
183
+ if (languages.length) {
184
+ for (const language of languages) {
185
+ const locale = asString(language.language ?? language.locale);
186
+ visit(language, { ...ctx, locale: locale ?? ctx.locale, suffixes: locale ? [...ctx.suffixes, locale.toUpperCase()] : ctx.suffixes }, depth + 1);
187
+ }
188
+ return;
189
+ }
190
+ // Versions become first-class ProjectVersion entries, NOT name-suffixed
191
+ // sibling spaces: each branch's spaces carry ctx.version and the catalog
192
+ // records label/tag/default (first entry is default unless one opts in).
193
+ const versions = childContainers(nav.versions);
194
+ if (versions.length) {
195
+ for (const version of versions) {
196
+ const label = asString(version.version ?? version.name ?? version.label);
197
+ if (!label) {
198
+ visit(version, ctx, depth + 1);
199
+ continue;
200
+ }
201
+ const key = slug(label);
202
+ if (!importVersions.some((entry) => entry.version === key)) {
203
+ const tag = asString(version.tag);
204
+ importVersions.push({
205
+ version: key,
206
+ label,
207
+ ...(tag ? { tag } : {}),
208
+ ...(version.default === true ? { isDefault: true } : {}),
209
+ });
210
+ }
211
+ visit(version, { ...ctx, version: key }, depth + 1);
212
+ }
213
+ return;
214
+ }
215
+ const structured = [
216
+ ...childContainers(nav.products).map((entry) => ({ entry, label: asString(entry.product ?? entry.name ?? entry.label) })),
217
+ ...childContainers(nav.tabs).map((entry) => ({ entry, label: asString(entry.tab ?? entry.name ?? entry.label) })),
218
+ ...childContainers(nav.dropdowns).map((entry) => ({ entry, label: asString(entry.dropdown ?? entry.name ?? entry.label) })),
219
+ ];
220
+ if (structured.length) {
221
+ for (const { entry, label } of structured) {
222
+ visit(entry, {
223
+ ...ctx,
224
+ name: label ?? ctx.name,
225
+ suffixes: ctx.suffixes,
226
+ icon: asString(entry.icon) ?? ctx.icon,
227
+ tag: asString(entry.tag) ?? ctx.tag,
228
+ groupAnchors: true,
229
+ }, depth + 1);
230
+ }
231
+ }
232
+ const anchorLinks = [];
233
+ for (const anchor of childContainers(nav.anchors)) {
234
+ const nested = directNav(anchor).length > 0 || ['groups', 'pages', 'tabs', 'products', 'dropdowns'].some((key) => Array.isArray(anchor[key]));
235
+ if (nested) {
236
+ const label = asString(anchor.anchor ?? anchor.name) ?? ctx.name;
237
+ if (ctx.groupAnchors) {
238
+ const items = directNav(anchor);
239
+ if (items.length) {
240
+ anchorLinks.push({
241
+ type: 'group',
242
+ label,
243
+ ...(asString(anchor.icon) ? { icon: asString(anchor.icon) } : {}),
244
+ items,
245
+ });
246
+ }
247
+ }
248
+ else {
249
+ visit(anchor, { ...ctx, name: label }, depth + 1);
250
+ }
251
+ }
252
+ else {
253
+ anchorLinks.push(...pageNode(anchor));
254
+ }
255
+ }
256
+ const own = directNav(nav);
257
+ if (own.length || anchorLinks.length) {
258
+ const suffix = ctx.suffixes.length ? ` ${ctx.suffixes.join(' · ')}` : '';
259
+ const name = `${ctx.name}${suffix}`;
260
+ spaces.push({
261
+ name,
262
+ slug: slug(name),
263
+ ...(ctx.locale ? { locale: ctx.locale } : {}),
264
+ ...(ctx.version ? { version: ctx.version } : {}),
265
+ ...(ctx.icon ? { icon: ctx.icon } : {}),
266
+ ...(ctx.tag ? { tag: ctx.tag } : {}),
267
+ nav: [...own, ...anchorLinks],
268
+ });
269
+ }
270
+ };
271
+ visit(navigation, { name: initialName, suffixes: [] }, 0);
272
+ if (globalLinks.length) {
273
+ for (const space of spaces)
274
+ space.nav.push(...globalLinks);
275
+ }
276
+ if (importVersions.length > 0 && !importVersions.some((entry) => entry.isDefault)) {
277
+ importVersions[0].isDefault = true;
278
+ }
279
+ return { spaces: dedupeSpaces(spaces), versions: importVersions };
280
+ }
281
+ function dedupeSpaces(spaces) {
282
+ const seen = new Map();
283
+ return spaces.map((space) => {
284
+ // v1 and v2 both legitimately contain a "Getting Started" space, so the
285
+ // dedupe key spans (locale, version, slug).
286
+ const key = `${space.locale ?? ''}:${space.version ?? ''}:${space.slug ?? slug(space.name)}`;
287
+ const count = seen.get(key) ?? 0;
288
+ seen.set(key, count + 1);
289
+ return count === 0 ? space : { ...space, slug: `${space.slug ?? slug(space.name)}-${count + 1}` };
290
+ });
291
+ }
292
+ function fontConfig(value) {
293
+ if (typeof value === 'string')
294
+ return { family: value };
295
+ const record = asRecord(value);
296
+ return {
297
+ family: asString(record.family),
298
+ source: asString(record.source),
299
+ format: asString(record.format),
300
+ weight: asNumber(record.weight),
301
+ };
302
+ }
303
+ function mintlifyLayout(preset) {
304
+ // Mintlify themes differ mainly in shell density. These tokens keep the same
305
+ // proportions in Doxbrix while remaining responsive at the renderer layer.
306
+ switch (preset) {
307
+ // Mint's current max-w-8xl canvas is 92rem (1472px). With the reader's
308
+ // 32px inline padding this produces the 1408px body used by the source.
309
+ case 'mint': return { headerHeight: 64, tabsHeight: 48, shellMaxWidth: 1472, sidebarWidth: 288, tocWidth: 264, contentMaxWidth: 920, contentPadding: 32, navPadding: 32 };
310
+ case 'willow': return { headerHeight: 64, tabsHeight: 48, shellMaxWidth: 1400, sidebarWidth: 272, tocWidth: 248, contentMaxWidth: 760, contentPadding: 40, navPadding: 22 };
311
+ case 'linden': return { headerHeight: 64, sidebarWidth: 288, tocWidth: 256, contentMaxWidth: 820, contentPadding: 42, navPadding: 24 };
312
+ case 'maple':
313
+ case 'almond': return { headerHeight: 68, sidebarWidth: 304, tocWidth: 272, contentMaxWidth: 880, contentPadding: 48, navPadding: 26 };
314
+ case 'palm':
315
+ case 'aspen':
316
+ case 'sequoia': return { headerHeight: 68, sidebarWidth: 320, tocWidth: 288, contentMaxWidth: 920, contentPadding: 48, navPadding: 28 };
317
+ default: return { headerHeight: 64, sidebarWidth: 288, tocWidth: 272, contentMaxWidth: 840, contentPadding: 44, navPadding: 24 };
318
+ }
319
+ }
320
+ function mintlifyTypography(preset) {
321
+ if (preset !== 'mint')
322
+ return {};
323
+ // Mintlify does not serialize its platform typography defaults into docs.json.
324
+ // Materialize them during conversion so Doxbrix does not accidentally fall
325
+ // back to the host application's heavier title and lower-contrast prose.
326
+ return {
327
+ font: 'Inter',
328
+ headingFont: 'Inter',
329
+ typography: {
330
+ titleWeight: 600,
331
+ titleSize: 36,
332
+ titleSizeMobile: 30,
333
+ titleLineHeight: 1.11111,
334
+ titleLineHeightMobile: 1.2,
335
+ descriptionSize: 18,
336
+ descriptionLineHeight: 1.55556,
337
+ bodyWeight: 400,
338
+ bodySize: 16,
339
+ bodyLineHeight: 1.75,
340
+ fontFeatures: ['cv02', 'cv03', 'cv04', 'cv11'],
341
+ fontVariationSettings: 'normal',
342
+ fontSmoothing: 'antialiased',
343
+ headingWeight: 600,
344
+ h2Size: 24,
345
+ h2LineHeight: 1.33333,
346
+ h2MarginTop: 48,
347
+ h2MarginBottom: 16,
348
+ h3Size: 20,
349
+ h3LineHeight: 1.4,
350
+ h3MarginTop: 48,
351
+ h3MarginBottom: 12,
352
+ h4Size: 18,
353
+ h4LineHeight: 1.5,
354
+ h4MarginTop: 36,
355
+ h4MarginBottom: 9,
356
+ strongWeight: 600,
357
+ apiFieldWeight: 600,
358
+ },
359
+ textColors: {
360
+ // Mint's current neutral scale: gray-900/700/600 in light mode and
361
+ // gray-200/300/400 in dark mode.
362
+ light: { heading: '#171717', body: '#3f3f3f', muted: '#505050' },
363
+ dark: { heading: '#dfdfdf', body: '#cecece', muted: '#9f9f9f' },
364
+ },
365
+ };
366
+ }
367
+ function parseTheme(config) {
368
+ const colors = asRecord(config.colors);
369
+ const logo = config.logo;
370
+ const logoRecord = asRecord(logo);
371
+ const favicon = config.favicon;
372
+ const faviconRecord = asRecord(favicon);
373
+ const appearance = asRecord(config.appearance);
374
+ const fonts = asRecord(config.fonts);
375
+ const baseFont = fontConfig(config.fonts);
376
+ const bodyFont = fontConfig(fonts.body);
377
+ const headingFont = fontConfig(fonts.heading);
378
+ const codeFont = fontConfig(fonts.code);
379
+ const background = asRecord(config.background);
380
+ const bgColor = background.color;
381
+ const bgRecord = asRecord(bgColor);
382
+ const styling = asRecord(config.styling);
383
+ const codeblocks = styling.codeblocks;
384
+ const codeblockConfig = asRecord(codeblocks);
385
+ const codeblockTheme = codeblockConfig.theme;
386
+ const codeblockModes = asRecord(codeblockTheme);
387
+ const preset = asString(config.theme);
388
+ const presetTypography = mintlifyTypography(preset);
389
+ const fontSources = [];
390
+ for (const [role, font] of [['body', Object.keys(bodyFont).length ? bodyFont : baseFont], ['heading', headingFont], ['code', codeFont]]) {
391
+ if (font.family && font.source)
392
+ fontSources.push({ family: font.family, source: font.source, ...(font.format ? { format: font.format } : {}), ...(font.weight ? { weight: font.weight } : {}), role });
393
+ }
394
+ const mode = appearance.default === 'light' || appearance.default === 'dark' ? appearance.default : 'system';
395
+ return {
396
+ source: 'doxbrix-import',
397
+ ...(preset ? { preset } : {}),
398
+ ...(normalizeHex(colors.primary) ? { primaryColor: normalizeHex(colors.primary) } : {}),
399
+ ...(normalizeHex(colors.light) ? { lightColor: normalizeHex(colors.light) } : {}),
400
+ ...(normalizeHex(colors.dark) ? { darkColor: normalizeHex(colors.dark) } : {}),
401
+ mode,
402
+ strictMode: appearance.strict === true,
403
+ ...presetTypography,
404
+ ...(bodyFont.family ?? baseFont.family ? { font: bodyFont.family ?? baseFont.family } : {}),
405
+ ...(headingFont.family ?? bodyFont.family ?? baseFont.family ? { headingFont: headingFont.family ?? bodyFont.family ?? baseFont.family } : {}),
406
+ ...(codeFont.family ? { codeFont: codeFont.family } : {}),
407
+ ...(fontSources.length ? { fontSources } : {}),
408
+ ...(typeof logo === 'string' ? { logoLight: logo, logoDark: logo } : {}),
409
+ ...(asString(logoRecord.light) ? { logoLight: asString(logoRecord.light) } : {}),
410
+ ...(asString(logoRecord.dark) ? { logoDark: asString(logoRecord.dark) } : {}),
411
+ ...(asString(logoRecord.href) ? { logoHref: asString(logoRecord.href) } : {}),
412
+ ...(typeof favicon === 'string' ? { favicon } : {}),
413
+ ...(asString(faviconRecord.light) ? { faviconLight: asString(faviconRecord.light) } : {}),
414
+ ...(asString(faviconRecord.dark) ? { faviconDark: asString(faviconRecord.dark) } : {}),
415
+ ...(normalizeHex(bgRecord.light ?? bgColor) ? { backgroundColorLight: normalizeHex(bgRecord.light ?? bgColor) } : {}),
416
+ ...(normalizeHex(bgRecord.dark) ? { backgroundColorDark: normalizeHex(bgRecord.dark) } : {}),
417
+ ...(asString(background.image) ? { backgroundImage: asString(background.image) } : {}),
418
+ ...(asString(background.decoration) ? { backgroundDecoration: asString(background.decoration) } : {}),
419
+ ...(asString(codeblocks) ? { codeTheme: asString(codeblocks) } : {}),
420
+ ...(asString(codeblockTheme) ? { codeTheme: asString(codeblockTheme) } : {}),
421
+ ...(asString(codeblockModes.light) ? { codeThemeLight: asString(codeblockModes.light) } : {}),
422
+ ...(asString(codeblockModes.dark) ? { codeThemeDark: asString(codeblockModes.dark) } : {}),
423
+ ...(asString(styling.eyebrows) ? { eyebrow: asString(styling.eyebrows) } : {}),
424
+ layout: mintlifyLayout(preset),
425
+ };
426
+ }
427
+ function parseSiteChrome(config) {
428
+ const navbar = asRecord(config.navbar);
429
+ const links = Array.isArray(navbar.links) ? navbar.links.map(asRecord).map((link) => ({
430
+ label: asString(link.label ?? link.type) ?? 'Link',
431
+ href: asString(link.href) ?? '',
432
+ ...(asString(link.icon) ? { icon: asString(link.icon) } : {}),
433
+ })).filter((link) => link.href) : [];
434
+ const primaryRaw = asRecord(navbar.primary);
435
+ const primaryHref = asString(primaryRaw.href);
436
+ const primary = primaryHref ? { label: asString(primaryRaw.label ?? primaryRaw.type) ?? 'Open', href: primaryHref, ...(asString(primaryRaw.type) ? { type: asString(primaryRaw.type) } : {}) } : undefined;
437
+ const footer = asRecord(config.footer);
438
+ const groups = Array.isArray(footer.links) ? footer.links.map(asRecord).map((group) => ({
439
+ title: asString(group.header ?? group.title) ?? 'Links',
440
+ links: (Array.isArray(group.items) ? group.items : []).map(asRecord).map((link) => ({ label: asString(link.label) ?? 'Link', url: asString(link.href ?? link.url) ?? '' })).filter((link) => link.url),
441
+ })).filter((group) => group.links.length) : [];
442
+ const socials = Object.entries(asRecord(footer.socials)).map(([platform, url]) => ({ platform, label: platform.replace(/(^|-)(\w)/g, (_m, _p, c) => ` ${c.toUpperCase()}`).trim(), url: asString(url) ?? '' })).filter((entry) => entry.url);
443
+ const bannerRaw = asRecord(config.banner);
444
+ const bannerContent = asString(bannerRaw.content);
445
+ const bannerColor = bannerRaw.color;
446
+ const bannerColors = asRecord(bannerColor);
447
+ const bannerType = bannerRaw.type === 'warning' || bannerRaw.type === 'critical' ? bannerRaw.type : 'info';
448
+ const banner = bannerContent ? {
449
+ content: bannerContent,
450
+ dismissible: bannerRaw.dismissible === true,
451
+ type: bannerType,
452
+ ...(normalizeHex(bannerColors.light ?? bannerColor) ? { colorLight: normalizeHex(bannerColors.light ?? bannerColor) } : {}),
453
+ ...(normalizeHex(bannerColors.dark) ? { colorDark: normalizeHex(bannerColors.dark) } : {}),
454
+ } : undefined;
455
+ if (!links.length && !primary && !groups.length && !socials.length && !banner)
456
+ return undefined;
457
+ return {
458
+ ...((links.length || primary) ? { navbar: { links, ...(primary ? { primary } : {}) } } : {}),
459
+ ...((groups.length || socials.length) ? { footer: { groups, socials } } : {}),
460
+ ...(banner ? { banner } : {}),
461
+ };
462
+ }
463
+ function parseRedirects(value) {
464
+ return Array.isArray(value) ? value.map(asRecord).map((redirect) => ({ from: asString(redirect.source ?? redirect.from) ?? '', to: asString(redirect.destination ?? redirect.to) ?? '' })).filter((redirect) => redirect.from && redirect.to) : [];
465
+ }
466
+ /** Mintlify accepts one or many OpenAPI documents in `api.openapi`. Object
467
+ * entries use `source`; strings can be repository paths or public URLs. */
468
+ function parseOpenApiSources(config) {
469
+ const raw = asRecord(config.api).openapi;
470
+ const values = Array.isArray(raw) ? raw : raw === undefined ? [] : [raw];
471
+ const sources = values
472
+ .map((value) => asString(value) ?? asString(asRecord(value).source))
473
+ .filter((value) => Boolean(value));
474
+ return [...new Set(sources)];
475
+ }
476
+ function navigationOpenApiSources(value, sources = []) {
477
+ if (Array.isArray(value)) {
478
+ for (const child of value)
479
+ navigationOpenApiSources(child, sources);
480
+ return sources;
481
+ }
482
+ const node = asRecord(value);
483
+ if (!Object.keys(node).length)
484
+ return sources;
485
+ const source = openApiSource(node.openapi);
486
+ if (source)
487
+ sources.push(normalizeSpecSource(source));
488
+ for (const key of ['languages', 'versions', 'products', 'tabs', 'dropdowns', 'anchors', 'groups', 'pages']) {
489
+ navigationOpenApiSources(node[key], sources);
490
+ }
491
+ return sources;
492
+ }
493
+ function parseApiBaseUrl(config) {
494
+ const server = asRecord(asRecord(config.api).mdx).server;
495
+ if (Array.isArray(server))
496
+ return server.map(asString).find(Boolean);
497
+ return asString(server);
498
+ }
499
+ function collectManifestPages(manifest) {
500
+ const files = new Set();
501
+ const walk = (nodes) => {
502
+ for (const node of nodes) {
503
+ if (node.type === 'page')
504
+ files.add(stripPageExtension(normalizePath(node.file)));
505
+ else if (node.type === 'group')
506
+ walk(node.items);
507
+ }
508
+ };
509
+ for (const space of manifest.spaces)
510
+ walk(space.nav);
511
+ return files;
512
+ }
513
+ function likelyPage(path) {
514
+ const parts = normalizePath(path).split('/');
515
+ if (parts.some((part) => NON_PAGE_DIRS.has(part.toLowerCase())))
516
+ return false;
517
+ return parts.length !== 1 || !ROOT_REPOSITORY_FILES.has(stripPageExtension(parts[0]).toLowerCase());
518
+ }
519
+ function preserveUnlistedPages(manifest, pagePaths) {
520
+ const listed = collectManifestPages(manifest);
521
+ const missing = pagePaths.map(stripPageExtension).filter((file) => likelyPage(file) && !listed.has(file));
522
+ if (!missing.length)
523
+ return;
524
+ if (!manifest.spaces[0])
525
+ manifest.spaces.push({ name: manifest.name ?? 'Documentation', slug: 'docs', nav: [] });
526
+ manifest.spaces[0].nav.push({
527
+ type: 'group',
528
+ label: 'Imported pages',
529
+ hidden: true,
530
+ items: missing.sort().map((file) => ({ type: 'page', file, hidden: true })),
531
+ });
532
+ }
533
+ function splitFrontmatter(markdown) {
534
+ if (!/^---\s*\r?\n/.test(markdown))
535
+ return { head: '', body: markdown };
536
+ const match = /^---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n?/.exec(markdown);
537
+ return match ? { head: match[0], body: markdown.slice(match[0].length) } : { head: '', body: markdown };
538
+ }
539
+ function attr(raw, name) {
540
+ const match = new RegExp(`\\b${name}\\s*=\\s*(?:["']([^"']*)["']|\\{([^{}]*)\\}|([^\\s/>]+))`, 'i').exec(raw);
541
+ return match?.[1] ?? match?.[2] ?? match?.[3];
542
+ }
543
+ function stringAttr(raw, name) {
544
+ const value = attr(raw, name)?.trim();
545
+ if (!value)
546
+ return undefined;
547
+ const quoted = /^(['"])([\s\S]*)\1$/.exec(value);
548
+ return quoted ? quoted[2] : value;
549
+ }
550
+ function decodeMdxEntities(value) {
551
+ return value
552
+ .replace(/&#x([0-9a-f]+);/gi, (_match, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
553
+ .replace(/&#(\d+);/g, (_match, decimal) => String.fromCodePoint(Number.parseInt(decimal, 10)))
554
+ .replace(/&quot;/gi, '"')
555
+ .replace(/&apos;|&#39;/gi, "'")
556
+ .replace(/&lt;/gi, '<')
557
+ .replace(/&gt;/gi, '>')
558
+ .replace(/&amp;/gi, '&');
559
+ }
560
+ function mintlifyFieldInline(rawAttrs) {
561
+ const name = decodeMdxEntities(attr(rawAttrs, 'name') ?? 'field').replace(/`/g, '\\`').replace(/\|/g, '\\|');
562
+ const type = decodeMdxEntities(attr(rawAttrs, 'type') ?? '').replace(/`/g, '\\`').replace(/\|/g, '\\|');
563
+ const required = /(?:^|\s)required(?:\s|=|$)/i.test(rawAttrs);
564
+ const recommended = /(?:^|\s)recommended(?:\s|=|$)/i.test(rawAttrs);
565
+ return [`\`${name}\``, required ? '*' : recommended ? '●' : '', type ? `\`(${type})\`` : '']
566
+ .filter(Boolean)
567
+ .join(' ');
568
+ }
569
+ function exportedSnippetBody(source, exportedName) {
570
+ const content = splitFrontmatter(source).body.trim();
571
+ if (!exportedName)
572
+ return content;
573
+ const declaration = new RegExp(`export\\s+const\\s+${exportedName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).exec(content);
574
+ if (!declaration)
575
+ return content;
576
+ const arrow = content.indexOf('=>', declaration.index);
577
+ if (arrow < 0)
578
+ return content;
579
+ const returned = content.indexOf('return', arrow);
580
+ const open = content.indexOf('(', returned >= 0 ? returned : arrow + 2);
581
+ const close = content.lastIndexOf(');');
582
+ if (open < 0 || close <= open)
583
+ return content;
584
+ return content.slice(open + 1, close).trim().replace(/^<>\s*/, '').replace(/\s*<\/>$/, '');
585
+ }
586
+ function expandSnippetInvocation(output, name, snippet) {
587
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
588
+ const invocation = new RegExp(`<${escaped}\\b((?:"[^"]*"|'[^']*'|\\{[^{}]*\\}|[^>])*)(?:\\/\\s*>|>([\\s\\S]*?)<\\/${escaped}>)`, 'g');
589
+ return output.replace(invocation, (_match, rawAttrs, children) => {
590
+ let expanded = snippet;
591
+ for (const attribute of rawAttrs.matchAll(/\b([A-Za-z_$][\w$-]*)\s*=\s*(?:["']([^"']*)["']|\{\s*(["'])([\s\S]*?)\3\s*\}|\{([^{}]*)\})/g)) {
592
+ const key = attribute[1];
593
+ const value = attribute[2] ?? attribute[4] ?? attribute[5] ?? '';
594
+ expanded = expanded.replace(new RegExp(`\\{\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*}`, 'g'), JSON.stringify(value.trim()));
595
+ }
596
+ return children?.trim() ? `${expanded}\n${children.trim()}` : expanded;
597
+ });
598
+ }
599
+ function inlineMdxSnippets(body, pagePath, files, stack = new Set()) {
600
+ const imports = new Map();
601
+ let output = body.replace(/^\s*import\s+([A-Za-z_$][A-Za-z0-9_$]*)\s+from\s+["']([^"']+\.(?:md|mdx))["']\s*;?\s*$/gm, (_match, name, source) => {
602
+ const target = normalizePath(source.startsWith('/') ? source : joinPath(dirname(pagePath), source));
603
+ const snippet = files.get(target);
604
+ if (snippet !== undefined && !stack.has(target)) {
605
+ imports.set(name, inlineMdxSnippets(exportedSnippetBody(snippet), target, files, new Set(stack).add(target)));
606
+ }
607
+ return '';
608
+ });
609
+ output = output.replace(/^\s*import\s*\{([^}]+)}\s*from\s*["']([^"']+\.(?:md|mdx|js|jsx|ts|tsx))["']\s*;?\s*$/gm, (_match, bindings, source) => {
610
+ const target = normalizePath(source.startsWith('/') ? source : joinPath(dirname(pagePath), source));
611
+ const snippet = files.get(target);
612
+ if (snippet !== undefined) {
613
+ const isCodeModule = /\.(?:js|jsx|ts|tsx)$/i.test(target);
614
+ for (const binding of bindings.split(',')) {
615
+ const [exported, local = exported] = binding.trim().split(/\s+as\s+/);
616
+ if (exported && local && local !== 'IntegrationTile' && !stack.has(target)) {
617
+ const exportedBody = exportedSnippetBody(snippet, exported);
618
+ // Mintlify repositories commonly keep brand icons as tiny React
619
+ // components. Inline only modules whose selected export is a single
620
+ // SVG tree; arbitrary JavaScript components must never be executed or
621
+ // copied into the destination MDX.
622
+ if (!isCodeModule || /^<svg\b[\s\S]*<\/svg\s*>$/i.test(exportedBody.trim())) {
623
+ imports.set(local, isCodeModule
624
+ ? exportedBody
625
+ : inlineMdxSnippets(exportedBody, target, files, new Set(stack).add(target)));
626
+ }
627
+ }
628
+ }
629
+ }
630
+ return '';
631
+ });
632
+ // Mintlify also permits reusable snippets to be globally available. Resolve
633
+ // simple PascalCase filename components so pages do not lose content merely
634
+ // because an explicit import is absent.
635
+ for (const [path, source] of files) {
636
+ if (!/(?:^|\/)snippets\/[^/]+\.(?:md|mdx)$/i.test(path))
637
+ continue;
638
+ const base = path.split('/').at(-1).replace(/\.(?:md|mdx)$/i, '');
639
+ const name = base.split(/[-_]+/).map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`).join('');
640
+ if (name && name !== 'IntegrationTile' && output.includes(`<${name}`) && !imports.has(name) && !stack.has(path)) {
641
+ imports.set(name, inlineMdxSnippets(exportedSnippetBody(source), path, files, new Set(stack).add(path)));
642
+ }
643
+ }
644
+ for (const [name, snippet] of imports) {
645
+ output = expandSnippetInvocation(output, name, snippet);
646
+ }
647
+ return output;
648
+ }
649
+ function replaceBalancedTag(body, tag, map) {
650
+ const re = new RegExp(`<${tag}\\b([^>]*)>([\\s\\S]*?)</${tag}>`, 'gi');
651
+ let prior = '';
652
+ let next = body;
653
+ while (next !== prior) {
654
+ prior = next;
655
+ next = next.replace(re, (_match, attrs, inner) => map(attrs, inner));
656
+ }
657
+ return next;
658
+ }
659
+ function normalizeFileTreeInner(inner) {
660
+ return inner
661
+ .replace(/<Tree\.Folder\b/gi, '<Folder')
662
+ .replace(/<\/Tree\.Folder>/gi, '</Folder>')
663
+ .replace(/<Tree\.File\b/gi, '<FileNode')
664
+ .replace(/<\/Tree\.File>/gi, '</FileNode>');
665
+ }
666
+ function mintlifyImageTheme(rawAttrs) {
667
+ const className = stringAttr(rawAttrs, 'className') ?? stringAttr(rawAttrs, 'class') ?? '';
668
+ if (/(?:^|\s)dark:hidden(?:\s|$)/i.test(className))
669
+ return 'light';
670
+ if (/(?:^|\s)dark:block(?:\s|$)/i.test(className) && /(?:^|\s)hidden(?:\s|$)/i.test(className))
671
+ return 'dark';
672
+ return undefined;
673
+ }
674
+ /**
675
+ * Mintlify commonly authors one light-theme image followed by one dark-theme
676
+ * image and controls visibility with Tailwind classes. Preserve that pair as a
677
+ * single portable Image block so stripping source CSS does not show both.
678
+ */
679
+ function normalizeMintlifyThemeImagePairs(segment) {
680
+ return segment.replace(/<img\b([^>]*)\/?\s*>\s*<img\b([^>]*)\/?\s*>/gi, (match, firstAttrs, secondAttrs) => {
681
+ const firstTheme = mintlifyImageTheme(firstAttrs);
682
+ const secondTheme = mintlifyImageTheme(secondAttrs);
683
+ if (!firstTheme || !secondTheme || firstTheme === secondTheme)
684
+ return match;
685
+ const lightAttrs = firstTheme === 'light' ? firstAttrs : secondAttrs;
686
+ const darkAttrs = firstTheme === 'dark' ? firstAttrs : secondAttrs;
687
+ const lightSrc = stringAttr(lightAttrs, 'src');
688
+ const darkSrc = stringAttr(darkAttrs, 'src');
689
+ if (!lightSrc || !darkSrc)
690
+ return match;
691
+ const alt = stringAttr(lightAttrs, 'alt') ?? stringAttr(darkAttrs, 'alt') ?? '';
692
+ return `\n<Image src=${JSON.stringify(lightSrc)} darkSrc=${JSON.stringify(darkSrc)} alt=${JSON.stringify(alt)} />\n`;
693
+ });
694
+ }
695
+ const SAFE_SVG_PRESENTATION_PROPERTIES = new Set([
696
+ 'fill',
697
+ 'fill-opacity',
698
+ 'opacity',
699
+ 'stroke',
700
+ 'stroke-linecap',
701
+ 'stroke-linejoin',
702
+ 'stroke-opacity',
703
+ 'stroke-width',
704
+ ]);
705
+ function svgStyleText(value) {
706
+ const expression = /^\s*\{\s*("(?:\\.|[^"\\])*")\s*}\s*$/.exec(value)?.[1];
707
+ if (!expression)
708
+ return value;
709
+ try {
710
+ return JSON.parse(expression);
711
+ }
712
+ catch {
713
+ return '';
714
+ }
715
+ }
716
+ /** Convert safe class-based SVG presentation rules to attributes before style
717
+ * elements are removed. This preserves icons generated by design tools without
718
+ * allowing their CSS to escape the SVG or affect the surrounding docs page. */
719
+ function inlineSafeSvgClassStyles(svg) {
720
+ const classRules = new Map();
721
+ const withoutStyles = svg.replace(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi, (_match, raw) => {
722
+ const css = svgStyleText(raw);
723
+ for (const rule of css.matchAll(/\.([A-Za-z_][\w-]*)\s*\{([^{}]*)}/g)) {
724
+ const declarations = new Map();
725
+ for (const declaration of (rule[2] ?? '').split(';')) {
726
+ const separator = declaration.indexOf(':');
727
+ if (separator < 1)
728
+ continue;
729
+ const property = declaration.slice(0, separator).trim().toLowerCase();
730
+ const value = declaration.slice(separator + 1).trim();
731
+ if (SAFE_SVG_PRESENTATION_PROPERTIES.has(property)
732
+ && /^(?:currentColor|none|transparent|#[0-9a-f]{3,8}|-?(?:\d+(?:\.\d+)?|\.\d+)(?:px|%)?|url\(#[A-Za-z_][\w:.-]*\))$/i.test(value))
733
+ declarations.set(property, value);
734
+ }
735
+ if (declarations.size)
736
+ classRules.set(rule[1], declarations);
737
+ }
738
+ return '';
739
+ });
740
+ if (!classRules.size)
741
+ return withoutStyles;
742
+ return withoutStyles.replace(/<([A-Za-z][\w:.-]*)\b([^>]*)>/g, (match, tag, attrs) => {
743
+ const classes = /\b(?:class|className)\s*=\s*(["'])(.*?)\1/i.exec(attrs)?.[2]?.split(/\s+/) ?? [];
744
+ const additions = new Map();
745
+ for (const className of classes) {
746
+ for (const [property, value] of classRules.get(className) ?? [])
747
+ additions.set(property, value);
748
+ }
749
+ let nextAttrs = attrs;
750
+ for (const [property, value] of additions) {
751
+ if (!new RegExp(`\\b${property.replace('-', '[-A-Z]')}\\s*=`, 'i').test(nextAttrs)) {
752
+ nextAttrs += ` ${property}="${value}"`;
753
+ }
754
+ }
755
+ return `<${tag}${nextAttrs}>`;
756
+ });
757
+ }
758
+ function sanitizeInlineSvg(svg) {
759
+ return inlineSafeSvgClassStyles(svg)
760
+ .replace(/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi, '')
761
+ .replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, '')
762
+ .replace(/<foreignObject\b[^>]*>[\s\S]*?<\/foreignObject\s*>/gi, '')
763
+ .replace(/<(?:animate|animateMotion|animateTransform|set)\b[^>]*>[\s\S]*?<\/(?:animate|animateMotion|animateTransform|set)\s*>/gi, '')
764
+ .replace(/<(?:animate|animateMotion|animateTransform|set)\b[^>]*\/\s*>/gi, '')
765
+ .replace(/\s+style\s*=\s*\{\{[\s\S]*?}}/gi, '')
766
+ .replace(/\s+\{\.\.\.[^}]+}/g, '')
767
+ .replace(/\s+on[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|\{[^}]*\}|[^\s>]+)/gi, '')
768
+ .replace(/\s+(?:href|xlink:href)\s*=\s*(["'])\s*javascript:[\s\S]*?\1/gi, '')
769
+ .replace(/=\{\s*(-?(?:\d+(?:\.\d+)?|\.\d+))\s*}/g, '="$1"')
770
+ .replace(/=\{\s*(["'])([\s\S]*?)\1\s*}/g, '="$2"')
771
+ .replace(/\bclassName=/g, 'class=')
772
+ .replace(/\bfillRule=/g, 'fill-rule=')
773
+ .replace(/\bclipRule=/g, 'clip-rule=')
774
+ .replace(/\bclipPath=/g, 'clip-path=')
775
+ .replace(/\bfillOpacity=/g, 'fill-opacity=')
776
+ .replace(/\bstrokeWidth=/g, 'stroke-width=')
777
+ .replace(/\bstrokeLinecap=/g, 'stroke-linecap=')
778
+ .replace(/\bstrokeLinejoin=/g, 'stroke-linejoin=')
779
+ .replace(/\bstrokeMiterlimit=/g, 'stroke-miterlimit=')
780
+ .replace(/\bstrokeOpacity=/g, 'stroke-opacity=')
781
+ .replace(/\bstopColor=/g, 'stop-color=')
782
+ .replace(/\bstopOpacity=/g, 'stop-opacity=')
783
+ .replace(/\bxmlSpace=/g, 'xml:space=')
784
+ .replace(/\bxlinkHref=/g, 'xlink:href=')
785
+ .replace(/\s+/g, ' ')
786
+ .trim();
787
+ }
788
+ function markForcedCurrentColorCardIcons(input) {
789
+ return input.replace(/\bicon\s*=\s*\{\s*(<svg\b)/gi, 'icon={$1 data-doxbrix-force-current-color="true"');
790
+ }
791
+ function sourceForcesCurrentColorCardIcons(files) {
792
+ return [...files].some(([path, content]) => (/\.css$/i.test(path)
793
+ && /\[data-component-part=["']card-icon["']]\s+svg\s+(?:path|g|circle|rect)/i.test(content)
794
+ && /fill\s*:\s*#18171a\s*!important/i.test(content)
795
+ && /html\.dark[\s\S]*?fill\s*:\s*#(?:fff|ffffff)\s*!important/i.test(content)));
796
+ }
797
+ /**
798
+ * Decode an SVG icon produced by the importer for safe inline rendering.
799
+ *
800
+ * SVGs that use `currentColor` must remain in the document tree to inherit the
801
+ * card or navigation color. Rendering the same data URL through `<img>` creates
802
+ * an isolated document and silently turns those icons black. Only the encoded
803
+ * SVG form emitted by this importer is accepted, and it is sanitized again at
804
+ * the rendering boundary before callers use it as markup.
805
+ */
806
+ export function decodeImportedSvgIconDataUrl(value) {
807
+ const prefix = 'data:image/svg+xml,';
808
+ if (!value.toLowerCase().startsWith(prefix))
809
+ return null;
810
+ try {
811
+ const decoded = decodeURIComponent(value.slice(prefix.length));
812
+ const sanitized = sanitizeInlineSvg(decoded);
813
+ return /^<svg\b[^>]*>[\s\S]*<\/svg\s*>$/i.test(sanitized) ? sanitized : null;
814
+ }
815
+ catch {
816
+ return null;
817
+ }
818
+ }
819
+ /** Preserve JSX `icon={<svg>...</svg>}` values without retaining executable JSX. */
820
+ function normalizeMintlifyInlineSvgIcons(segment) {
821
+ return segment.replace(/\bicon\s*=\s*\{\s*(<svg\b[\s\S]*?<\/svg\s*>)\s*\}/gi, (_match, svg) => {
822
+ const sanitized = sanitizeInlineSvg(svg);
823
+ if (!sanitized)
824
+ return '';
825
+ return `icon=${JSON.stringify(`data:image/svg+xml,${encodeURIComponent(sanitized)}`)}`;
826
+ });
827
+ }
828
+ /**
829
+ * Degrade ordinary HTML/JSX used inside Mintlify MDX to the portable component
830
+ * vocabulary understood by the block importer. Documentation platforms permit
831
+ * raw HTML and JSX, so otherwise tags such as `<div>` and `<li>` survive as
832
+ * reader-visible text.
833
+ *
834
+ * This is intentionally a small, non-executing tokenizer rather than an HTML
835
+ * parser: it handles the common authoring tags while leaving unknown tags alone.
836
+ * Flex wrappers become native Columns; their direct child divs become Column items.
837
+ */
838
+ function transformPortableHtmlSegment(segment) {
839
+ const inlineCode = [];
840
+ // Raw HTML-like text inside Markdown code spans is documentation content,
841
+ // not a live construct. Protect it before the HTML/JSX tokenizer runs. Work
842
+ // line-by-line because CommonMark code spans authored this way do not need a
843
+ // cross-line placeholder for the Mintlify content we import.
844
+ const protectedSegment = segment.split('\n').map((line) => line.replace(/(`+)([^`]*?)\1/g, (value) => {
845
+ const index = inlineCode.push(value) - 1;
846
+ return `DOCSINLINECODE${index}END`;
847
+ })).join('\n');
848
+ // Media embeds are commonly authored as paired, multiline HTML. Collapse the
849
+ // whole element first so its closing tag cannot leak into a separate block.
850
+ // `url` is used for Video because that is the portable block vocabulary;
851
+ // Embed keeps `src` for iframe-compatible content.
852
+ const normalized = normalizeMintlifyThemeImagePairs(normalizeMintlifyInlineSvgIcons(protectedSegment))
853
+ .replace(/<iframe\b([^>]*)>[\s\S]*?<\/iframe\s*>/gi, (_match, attrs) => {
854
+ const src = attr(attrs, 'src');
855
+ return src ? `\n<Embed src=${JSON.stringify(src)} />\n` : '';
856
+ })
857
+ .replace(/<video\b([^>]*)>([\s\S]*?)<\/video\s*>/gi, (_match, attrs, inner) => {
858
+ const sourceAttrs = /<source\b([^>]*)\/?\s*>/i.exec(inner)?.[1] ?? '';
859
+ const src = attr(attrs, 'src') ?? attr(sourceAttrs, 'src');
860
+ return src ? `\n<Video url=${JSON.stringify(src)} />\n` : inner;
861
+ });
862
+ const divStack = [];
863
+ const listStack = [];
864
+ const linkStack = [];
865
+ const token = /\{\/\*[\s\S]*?\*\/\}|\{\s*(['"])\s*\1\s*\}|<([a-z][a-z0-9]*)\b([^>]*?)(\/?)>|<\/([a-z][a-z0-9]*)\s*>/gi;
866
+ const transformed = normalized.replace(token, (match, _spaceQuote, openName, rawAttrs, _selfClose, closeName) => {
867
+ if (match.startsWith('{/*'))
868
+ return '';
869
+ if (match.startsWith('{'))
870
+ return ' ';
871
+ if (openName) {
872
+ // Portable MDX components are capitalized. They already belong to the
873
+ // destination vocabulary and must not be mistaken for lowercase HTML.
874
+ if (/^[A-Z]/.test(openName))
875
+ return match;
876
+ const name = openName.toLowerCase();
877
+ const attrs = rawAttrs ?? '';
878
+ if (/^h[1-6]$/.test(name))
879
+ return `\n${'#'.repeat(Number(name[1]))} `;
880
+ switch (name) {
881
+ case 'div': {
882
+ const parent = divStack.at(-1);
883
+ const isFlex = /\bdisplay\s*:\s*['"]?flex\b/i.test(attrs);
884
+ const kind = isFlex ? 'columns' : parent === 'columns' ? 'column' : 'strip';
885
+ divStack.push(kind);
886
+ if (kind === 'columns')
887
+ return '\n<Columns>\n';
888
+ if (kind === 'column')
889
+ return '\n<Column>\n';
890
+ return '\n';
891
+ }
892
+ case 'p':
893
+ return '\n';
894
+ case 'ul':
895
+ case 'ol':
896
+ listStack.push({ tag: name, next: 1, items: 0 });
897
+ return '\n';
898
+ case 'li': {
899
+ const list = listStack.at(-1);
900
+ const prefix = list && list.items++ > 0 ? '\n' : '';
901
+ if (!list || list.tag === 'ul')
902
+ return `${prefix}- `;
903
+ return `${prefix}${list.next++}. `;
904
+ }
905
+ case 'a': {
906
+ const href = attr(attrs, 'href') ?? '#';
907
+ linkStack.push(href);
908
+ return '[';
909
+ }
910
+ case 'img': {
911
+ const cleanAttrs = attrs
912
+ .replace(/\bclassName=(['"])[\s\S]*?\1/gi, '')
913
+ .replace(/\s*\/$/, '')
914
+ .trim();
915
+ return `\n<Image${cleanAttrs ? ` ${cleanAttrs}` : ''} />\n`;
916
+ }
917
+ case 'iframe': {
918
+ const src = attr(attrs, 'src');
919
+ return src ? `\n<Embed src=${JSON.stringify(src)} />\n` : '';
920
+ }
921
+ case 'source': {
922
+ const src = attr(attrs, 'src');
923
+ return src ? `\n<Video url=${JSON.stringify(src)} />\n` : '';
924
+ }
925
+ case 'video': {
926
+ const src = attr(attrs, 'src');
927
+ return src ? `\n<Video url=${JSON.stringify(src)} />\n` : '\n';
928
+ }
929
+ case 'br':
930
+ return '\n';
931
+ case 'hr':
932
+ return '\n---\n';
933
+ case 'strong':
934
+ case 'b':
935
+ return '**';
936
+ case 'em':
937
+ case 'i':
938
+ return '*';
939
+ case 'span':
940
+ case 'button':
941
+ return '';
942
+ case 'section':
943
+ case 'article':
944
+ case 'main':
945
+ case 'header':
946
+ case 'footer':
947
+ case 'figure':
948
+ case 'figcaption':
949
+ return '\n';
950
+ default:
951
+ return match;
952
+ }
953
+ }
954
+ if (closeName) {
955
+ if (/^[A-Z]/.test(closeName))
956
+ return match;
957
+ const name = closeName.toLowerCase();
958
+ if (/^h[1-6]$/.test(name))
959
+ return '\n\n';
960
+ switch (name) {
961
+ case 'div': {
962
+ const kind = divStack.pop();
963
+ if (kind === 'columns')
964
+ return '\n</Columns>\n';
965
+ if (kind === 'column')
966
+ return '\n</Column>\n';
967
+ return '\n';
968
+ }
969
+ case 'p':
970
+ return '\n\n';
971
+ case 'ul':
972
+ case 'ol':
973
+ listStack.pop();
974
+ return '\n';
975
+ case 'li':
976
+ return '';
977
+ case 'a':
978
+ return `](${linkStack.pop() ?? '#'})`;
979
+ case 'strong':
980
+ case 'b':
981
+ return '**';
982
+ case 'em':
983
+ case 'i':
984
+ return '*';
985
+ case 'span':
986
+ case 'button':
987
+ return '';
988
+ case 'iframe':
989
+ case 'source':
990
+ case 'video':
991
+ case 'section':
992
+ case 'article':
993
+ case 'main':
994
+ case 'header':
995
+ case 'footer':
996
+ case 'figure':
997
+ case 'figcaption':
998
+ return '\n';
999
+ default:
1000
+ return match;
1001
+ }
1002
+ }
1003
+ return match;
1004
+ });
1005
+ // JSX commonly puts each <li> on its own indented line. The replacement plus
1006
+ // source whitespace can otherwise insert blank lines and split one list into
1007
+ // several one-item blocks.
1008
+ return transformed
1009
+ // An image used as the sole content of an HTML heading is media, not an
1010
+ // empty Markdown heading followed by media.
1011
+ .replace(/^\s*#{1,6}\s*(?=<Image\b)/gm, '')
1012
+ .replace(/^([ \t]*(?:[-*+]|\d+\.)[ \t]+.*)\n(?:[ \t]*\n)+(?=[ \t]*(?:[-*+]|\d+\.)[ \t])/gm, '$1\n')
1013
+ .replace(/DOCSINLINECODE(\d+)END/g, (_match, index) => inlineCode[Number(index)] ?? '');
1014
+ }
1015
+ /** Apply raw-HTML normalization only outside fenced code examples. */
1016
+ function transformPortableHtml(body) {
1017
+ const lines = body.split('\n');
1018
+ const out = [];
1019
+ let pending = [];
1020
+ let inFence = false;
1021
+ const flush = () => {
1022
+ if (!pending.length)
1023
+ return;
1024
+ out.push(transformPortableHtmlSegment(pending.join('\n')));
1025
+ pending = [];
1026
+ };
1027
+ for (const line of lines) {
1028
+ if (/^\s*(```|~~~)/.test(line)) {
1029
+ if (!inFence)
1030
+ flush();
1031
+ out.push(line);
1032
+ inFence = !inFence;
1033
+ }
1034
+ else if (inFence) {
1035
+ out.push(line);
1036
+ }
1037
+ else {
1038
+ pending.push(line);
1039
+ }
1040
+ }
1041
+ flush();
1042
+ return out.join('\n');
1043
+ }
1044
+ function protectFencedCodeBlocks(body) {
1045
+ const blocks = [];
1046
+ const output = [];
1047
+ let current;
1048
+ let fenceCharacter = '';
1049
+ let fenceLength = 0;
1050
+ for (const line of body.split('\n')) {
1051
+ if (!current) {
1052
+ const opening = /^\s*(`{3,}|~{3,})/.exec(line)?.[1];
1053
+ if (!opening) {
1054
+ output.push(line);
1055
+ continue;
1056
+ }
1057
+ current = [line];
1058
+ fenceCharacter = opening[0];
1059
+ fenceLength = opening.length;
1060
+ continue;
1061
+ }
1062
+ current.push(line);
1063
+ const closing = new RegExp(`^\\s*${fenceCharacter === '`' ? '`' : '~'}{${fenceLength},}\\s*$`);
1064
+ if (!closing.test(line))
1065
+ continue;
1066
+ const index = blocks.push(current.join('\n')) - 1;
1067
+ output.push(`DOCSFENCEDCODE${index}END`);
1068
+ current = undefined;
1069
+ }
1070
+ // Preserve an incomplete fence as authored. Validation can report the
1071
+ // unbalanced fence without transformations corrupting its contents.
1072
+ if (current)
1073
+ output.push(current.join('\n'));
1074
+ return {
1075
+ body: output.join('\n'),
1076
+ restore: (value) => value.replace(/DOCSFENCEDCODE(\d+)END/g, (_match, index) => blocks[Number(index)] ?? ''),
1077
+ };
1078
+ }
1079
+ /** Normalize current Mintlify components into Doxbrix's supported MDX vocabulary. */
1080
+ export function transformMintlifyMdx(input) {
1081
+ let body = input;
1082
+ const importedComponents = new Set();
1083
+ for (const line of body.split('\n')) {
1084
+ for (const statement of line.matchAll(/\bimport\s+(.+?)\s+from\s+["'][^"']+["']/g)) {
1085
+ const bindings = statement[1] ?? '';
1086
+ const defaultName = /^([A-Z][A-Za-z0-9_$]*)/.exec(bindings)?.[1];
1087
+ if (defaultName)
1088
+ importedComponents.add(defaultName);
1089
+ const named = /\{([^}]+)}/.exec(bindings)?.[1];
1090
+ for (const binding of named?.split(',') ?? []) {
1091
+ const local = binding.trim().split(/\s+as\s+/).at(-1);
1092
+ if (local && /^[A-Z][A-Za-z0-9_$]*$/.test(local))
1093
+ importedComponents.add(local);
1094
+ }
1095
+ }
1096
+ }
1097
+ let inFence = false;
1098
+ body = body.split('\n').map((line) => {
1099
+ if (/^\s*(```|~~~)/.test(line))
1100
+ inFence = !inFence;
1101
+ if (inFence || !/^\s*(?:import|export)\b/.test(line))
1102
+ return line;
1103
+ return '';
1104
+ }).join('\n');
1105
+ const fencedCode = protectFencedCodeBlocks(body);
1106
+ body = fencedCode.body;
1107
+ body = transformPortableHtml(body);
1108
+ // Imported presentation components are normally removed because their code
1109
+ // cannot execute in a portable docs project. Field is semantic content,
1110
+ // though: dropping it erases whole table cells. Flatten it to Markdown that
1111
+ // both the cloud reader and CLI preview can render.
1112
+ body = body.replace(/<Field\b((?:"[^"]*"|'[^']*'|[^>])*)\/\s*>/gi, (_match, attrs) => mintlifyFieldInline(attrs));
1113
+ // The destination reader already builds a page TOC. Avoid retaining an
1114
+ // imported client-only TOC placeholder or an empty Panel/Info around it.
1115
+ body = body.replace(/<TableOfContents\b((?:"[^"]*"|'[^']*'|[^>])*)\/\s*>/gi, '');
1116
+ body = body.replace(/<Tab\b([^>]*?)\btitle=(['"])(.*?)\2/gi, '<Tab$1label=$2$3$2');
1117
+ body = body.replace(/<Video\b([^>]*?)\bsrc=(['"])(.*?)\2/gi, '<Video$1url=$2$3$2');
1118
+ body = body.replace(/<Embed\b([^>]*?)\burl=(['"])(.*?)\2/gi, '<Embed$1src=$2$3$2');
1119
+ body = body.replace(/<File\b([^>]*?)\bsrc=(['"])(.*?)\2/gi, '<File$1url=$2$3$2');
1120
+ body = replaceBalancedTag(body, 'Panel', (attrs, inner) => inner.trim() ? `<SidePanel${attrs}>\n${inner.trim()}\n</SidePanel>` : '');
1121
+ body = replaceBalancedTag(body, 'Prompt', (attrs, inner) => `<PromptCard${attrs}>\n${inner.trim()}\n</PromptCard>`);
1122
+ // Tile is already the Doxbrix-native name. Tiles becomes a Columns layout so
1123
+ // every child remains an independently editable Tile block.
1124
+ body = replaceBalancedTag(body, 'Tiles', (attrs, inner) => `<Columns${attrs}>\n${inner.trim()}\n</Columns>`);
1125
+ body = replaceBalancedTag(body, 'View', (attrs, inner) => `<ViewPane${attrs}>\n${inner.trim()}\n</ViewPane>`);
1126
+ // Adjacent Mintlify Views are one selector; keep the grouping explicit.
1127
+ body = body.replace(/(?:\s*<ViewPane\b[^>]*>[\s\S]*?<\/ViewPane>\s*)+/gi, (run) => `<ViewSwitcher>\n${run.trim()}\n</ViewSwitcher>`);
1128
+ body = replaceBalancedTag(body, 'Visibility', (attrs, inner) => `<Audience${attrs}>\n${inner.trim()}\n</Audience>`);
1129
+ body = replaceBalancedTag(body, 'Banner', (attrs, inner) => {
1130
+ const type = attr(attrs, 'type');
1131
+ const tag = type === 'warning' || type === 'critical' ? 'Warning' : 'Info';
1132
+ return `<${tag}>\n${inner.trim()}\n</${tag}>`;
1133
+ });
1134
+ body = replaceBalancedTag(body, 'Tree', (_attrs, inner) => `<FileTree>\n${normalizeFileTreeInner(inner).trim()}\n</FileTree>`);
1135
+ body = replaceBalancedTag(body, 'FileTree', (_attrs, inner) => `<FileTree>\n${normalizeFileTreeInner(inner).trim()}\n</FileTree>`);
1136
+ body = replaceBalancedTag(body, 'Color', (attrs, inner) => {
1137
+ const normalized = inner
1138
+ .replace(/<Color\.Row\b/gi, '<ColorGroup')
1139
+ .replace(/<\/Color\.Row>/gi, '</ColorGroup>')
1140
+ .replace(/<Color\.Item\b/gi, '<ColorSwatch')
1141
+ .replace(/<\/Color\.Item>/gi, '</ColorSwatch>');
1142
+ // Mintlify permits Color.Item directly inside Color; give those swatches a
1143
+ // canonical group so the destination schema never needs two shapes.
1144
+ const grouped = /<ColorGroup\b/i.test(normalized) ? normalized : `<ColorGroup>\n${normalized.trim()}\n</ColorGroup>`;
1145
+ return `<ColorPalette${attrs}>\n${grouped.trim()}\n</ColorPalette>`;
1146
+ });
1147
+ // Imported JSX components that are simple image/CTA cards degrade to portable Markdown.
1148
+ body = body.replace(/<([A-Z][A-Za-z0-9_$]*)\b([^>]*?)\/>/g, (match, name, attrs) => {
1149
+ // These are already portable media blocks. In particular, treating an
1150
+ // iframe-derived Embed's `src` as an image would silently change its type.
1151
+ if (['Embed', 'Excalidraw', 'File', 'PDF', 'Video', 'Tile', 'Icon', 'ColorSwatch', 'FileNode'].includes(name))
1152
+ return match;
1153
+ // A paired light/dark source must remain one theme-aware image block.
1154
+ // Ordinary Image components still degrade to portable Markdown below.
1155
+ if (name === 'Image' && stringAttr(attrs, 'darkSrc'))
1156
+ return match;
1157
+ const image = stringAttr(attrs, 'logo') ?? stringAttr(attrs, 'img') ?? stringAttr(attrs, 'image') ?? stringAttr(attrs, 'src');
1158
+ const alt = stringAttr(attrs, 'alt') ?? stringAttr(attrs, 'title') ?? name;
1159
+ const href = stringAttr(attrs, 'link') ?? stringAttr(attrs, 'ctaHref') ?? stringAttr(attrs, 'href');
1160
+ const label = stringAttr(attrs, 'ctaText') ?? stringAttr(attrs, 'title');
1161
+ if (href && label && name === 'IntegrationTile') {
1162
+ return `<Tile title=${JSON.stringify(label)} href=${JSON.stringify(href)}${image ? ` image=${JSON.stringify(image)}` : ''} />`;
1163
+ }
1164
+ if (image && href)
1165
+ return `[![${alt}](${image})](${href})${label ? `\n\n[${label}](${href})` : ''}`;
1166
+ if (image)
1167
+ return `![${alt}](${image})`;
1168
+ if (importedComponents.has(name))
1169
+ return '';
1170
+ return match;
1171
+ });
1172
+ return fencedCode.restore(body.replace(/\n{3,}/g, '\n\n'));
1173
+ }
1174
+ function unmappedConstructs(body) {
1175
+ const known = new Set(['Note', 'Tip', 'Warning', 'Info', 'Check', 'Danger', 'Callout', 'Steps', 'Step', 'Tabs', 'Tab', 'Accordion', 'AccordionItem', 'AccordionGroup', 'CodeGroup', 'CodeTab', 'Card', 'CardGroup', 'Frame', 'Image', 'Video', 'Embed', 'File', 'Columns', 'Column', 'Expandable', 'Mermaid', 'Math', 'Terminal', 'Badge', 'Icon', 'Update', 'ApiEndpoint', 'ParameterTable', 'Param', 'ParamField', 'Response', 'ResponseExample', 'ResponseField', 'RequestExample', 'Excalidraw', 'ColorPalette', 'ColorGroup', 'ColorSwatch', 'SidePanel', 'PromptCard', 'Tile', 'Tooltip', 'FileTree', 'Folder', 'FileNode', 'ViewSwitcher', 'ViewPane', 'Audience']);
1176
+ const found = new Set();
1177
+ // Component-like strings inside code examples (TypeScript generics, shell
1178
+ // placeholders, JSX tutorials) are examples, not unmapped page components.
1179
+ let inFence = false;
1180
+ const visible = body.split('\n').map((line) => {
1181
+ if (/^\s*(```|~~~)/.test(line)) {
1182
+ inFence = !inFence;
1183
+ return '';
1184
+ }
1185
+ // Inline-code spans cannot cross a Markdown line. Removing them before
1186
+ // joining prevents an unmatched backtick from swallowing multiple lines
1187
+ // and exposing JSX or generic types from otherwise fenced examples.
1188
+ return inFence ? '' : line.replace(/`+[^`]*`+/g, '');
1189
+ }).join('\n')
1190
+ .replace(/<!--[^]*?-->/g, '');
1191
+ for (const match of visible.matchAll(/(?<!\\)<([A-Z][A-Za-z0-9_.]+)/g))
1192
+ if (!known.has(match[1]))
1193
+ found.add(`<${match[1]}>`);
1194
+ // Lowercase tags are HTML, SVG, or presentation-layer custom elements. They
1195
+ // may degrade visually, but their presence must never stop an otherwise safe
1196
+ // repository transformation. Capitalized MDX components remain blocking in
1197
+ // strict mode because a self-closing component can carry all semantic content
1198
+ // in its props and silently dropping it would lose documentation.
1199
+ for (const match of visible.matchAll(/\{%\s*([a-z-]+)/gi))
1200
+ found.add(`{% ${match[1]} %}`);
1201
+ return [...found];
1202
+ }
1203
+ function replaceVariables(body, variables) {
1204
+ return body.replace(/\{([A-Za-z_][A-Za-z0-9_.-]*)}/g, (match, key) => {
1205
+ const value = key.split('.').reduce((current, part) => asRecord(current)[part], variables);
1206
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? String(value) : match;
1207
+ });
1208
+ }
1209
+ function frontmatterValue(head, key) {
1210
+ const match = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:\\s*(.*?)\\s*$`, 'm').exec(head);
1211
+ return match?.[1]?.trim();
1212
+ }
1213
+ function inferredDescription(body) {
1214
+ const prose = body
1215
+ .replace(/```[\s\S]*?```|~~~[\s\S]*?~~~/g, '')
1216
+ .replace(/<!--[^]*?-->/g, '')
1217
+ .replace(/!\[[^\]]*]\([^)]*\)/g, '')
1218
+ .replace(/\[([^\]]+)]\([^)]*\)/g, '$1')
1219
+ .replace(/<[^>]+>/g, ' ')
1220
+ .replace(/^#{1,6}\s+/gm, '')
1221
+ .replace(/[*_`]/g, '');
1222
+ const paragraph = prose
1223
+ .split(/\n\s*\n/)
1224
+ .map((value) => value.replace(/^\s*(?:[-+] |\d+\. )/gm, '').replace(/\s+/g, ' ').trim())
1225
+ .find((value) => value.length >= 20 && !value.startsWith('|'));
1226
+ if (!paragraph)
1227
+ return undefined;
1228
+ if (paragraph.length <= 200)
1229
+ return paragraph;
1230
+ const shortened = paragraph.slice(0, 197).replace(/\s+\S*$/, '').trimEnd();
1231
+ return `${shortened}…`;
1232
+ }
1233
+ function normalizeMintlifyPage(head, body) {
1234
+ let normalizedBody = body
1235
+ // Mintlify treats its root `images/` directory as site-absolute even when
1236
+ // an authored Markdown image omits the slash.
1237
+ .replace(/(!\[[^\]]*]\()images\//g, '$1/images/');
1238
+ if (frontmatterValue(head, 'title')) {
1239
+ // The Doxbrix reader renders the frontmatter title as the one page H1.
1240
+ normalizedBody = normalizedBody.replace(/^#\s+/gm, '## ');
1241
+ }
1242
+ if (!head || frontmatterValue(head, 'description'))
1243
+ return { head, body: normalizedBody };
1244
+ const description = inferredDescription(normalizedBody);
1245
+ if (!description)
1246
+ return { head, body: normalizedBody };
1247
+ const closing = head.lastIndexOf('---');
1248
+ return closing < 3
1249
+ ? { head, body: normalizedBody }
1250
+ : { head: `${head.slice(0, closing)}description: ${JSON.stringify(description)}\n${head.slice(closing)}`, body: normalizedBody };
1251
+ }
1252
+ function inferManifest(paths, name = 'Documentation') {
1253
+ const root = [];
1254
+ const groups = new Map();
1255
+ for (const path of paths.filter(likelyPage).sort()) {
1256
+ const file = stripPageExtension(path);
1257
+ const [top] = file.split('/');
1258
+ if (!top || !file.includes('/'))
1259
+ root.push({ type: 'page', file });
1260
+ else {
1261
+ if (!groups.has(top))
1262
+ groups.set(top, []);
1263
+ groups.get(top).push({ type: 'page', file });
1264
+ }
1265
+ }
1266
+ for (const [folder, items] of groups)
1267
+ root.push({ type: 'group', label: folder.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()), items });
1268
+ return { version: 1, name, spaces: [{ name, slug: 'docs', nav: root }] };
1269
+ }
1270
+ function parseGitbookSummary(summary) {
1271
+ const nav = [];
1272
+ const stack = [{ indent: -1, items: nav }];
1273
+ for (const raw of summary.split('\n')) {
1274
+ const heading = /^#{1,6}\s+(.+)$/.exec(raw.trim());
1275
+ if (heading) {
1276
+ nav.push({ type: 'label', text: heading[1].trim() });
1277
+ continue;
1278
+ }
1279
+ const bullet = /^(\s*)[-*+]\s+\[([^\]]+)]\(([^)]+)\)/.exec(raw);
1280
+ if (!bullet)
1281
+ continue;
1282
+ const indent = bullet[1].replace(/\t/g, ' ').length;
1283
+ while (stack.length > 1 && stack.at(-1).indent >= indent)
1284
+ stack.pop();
1285
+ const node = { type: 'page', file: stripPageExtension(normalizePath(bullet[3])), title: bullet[2].trim() };
1286
+ stack.at(-1).items.push(node);
1287
+ }
1288
+ return { version: 1, name: 'Documentation', spaces: [{ name: 'Documentation', slug: 'docs', nav }] };
1289
+ }
1290
+ function transformGitbook(body) {
1291
+ return body
1292
+ .replace(/\{%\s*hint\s+style=["']?(warning|danger|info|success)["']?\s*%}([\s\S]*?)\{%\s*endhint\s*%}/gi, (_m, style, inner) => `<${style === 'danger' ? 'Danger' : style === 'warning' ? 'Warning' : style === 'success' ? 'Check' : 'Info'}>\n${inner.trim()}\n</${style === 'danger' ? 'Danger' : style === 'warning' ? 'Warning' : style === 'success' ? 'Check' : 'Info'}>`)
1293
+ .replace(/<details>\s*<summary>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gi, (_m, title, inner) => `<Expandable title="${title.trim()}">\n${inner.trim()}\n</Expandable>`);
1294
+ }
1295
+ export function convertSourceTree(inputFiles) {
1296
+ const files = new Map(inputFiles.map((file) => [normalizePath(file.path), file.content]));
1297
+ const paths = [...files.keys()];
1298
+ const pagePaths = paths.filter(isPageFile);
1299
+ // Prefer the repository-root config. A prior local migration may have left a
1300
+ // nested Doxbrix docs.json; choosing that would incorrectly detect the source
1301
+ // as native on the next import.
1302
+ const configPath = paths
1303
+ .filter((path) => CONFIG_NAMES.includes(basename(path).toLowerCase()))
1304
+ .sort((a, b) => a.split('/').length - b.split('/').length || a.localeCompare(b))[0];
1305
+ let dialect = 'generic';
1306
+ let manifest;
1307
+ let redirects = [];
1308
+ let transform = (body) => body;
1309
+ let variables = {};
1310
+ if (configPath) {
1311
+ let raw = {};
1312
+ try {
1313
+ raw = asRecord(resolveJsonRefs(JSON.parse(files.get(configPath)), configPath, files));
1314
+ }
1315
+ catch {
1316
+ raw = {};
1317
+ }
1318
+ if (raw.navigation && !Array.isArray(raw.spaces)) {
1319
+ dialect = 'mintlify';
1320
+ const name = asString(raw.name) ?? 'Documentation';
1321
+ const openapiSources = [...new Set([
1322
+ ...parseOpenApiSources(raw),
1323
+ ...navigationOpenApiSources(raw.navigation),
1324
+ ])];
1325
+ const apiBaseUrl = parseApiBaseUrl(raw);
1326
+ const collected = collectSpaces(raw.navigation, name);
1327
+ manifest = {
1328
+ version: 1,
1329
+ name,
1330
+ ...(asString(raw.description) ? { description: asString(raw.description) } : {}),
1331
+ ...(openapiSources.length ? { openapiSources } : {}),
1332
+ ...(apiBaseUrl ? { apiBaseUrl } : {}),
1333
+ spaces: collected.spaces,
1334
+ ...(collected.versions.length ? { versions: collected.versions } : {}),
1335
+ theme: parseTheme(raw),
1336
+ ...(parseSiteChrome(raw) ? { site: parseSiteChrome(raw) } : {}),
1337
+ };
1338
+ redirects = parseRedirects(raw.redirects);
1339
+ variables = asRecord(raw.variables);
1340
+ transform = transformMintlifyMdx;
1341
+ preserveUnlistedPages(manifest, pagePaths);
1342
+ }
1343
+ else if (Array.isArray(raw.spaces)) {
1344
+ dialect = 'native';
1345
+ manifest = raw;
1346
+ }
1347
+ else {
1348
+ manifest = inferManifest(pagePaths);
1349
+ }
1350
+ }
1351
+ else {
1352
+ const summaryPath = paths.find((path) => basename(path).toLowerCase() === 'summary.md');
1353
+ if (summaryPath) {
1354
+ dialect = 'gitbook';
1355
+ manifest = parseGitbookSummary(files.get(summaryPath));
1356
+ transform = transformGitbook;
1357
+ preserveUnlistedPages(manifest, pagePaths.filter((path) => path !== summaryPath));
1358
+ }
1359
+ else {
1360
+ manifest = inferManifest(pagePaths);
1361
+ }
1362
+ }
1363
+ const unmapped = new Set();
1364
+ const forceCurrentColorCardIcons = dialect === 'mintlify' && sourceForcesCurrentColorCardIcons(files);
1365
+ let pages = pagePaths
1366
+ .filter((path) => basename(path).toLowerCase() !== 'summary.md')
1367
+ .filter(likelyPage)
1368
+ .map((path) => {
1369
+ const { head, body } = splitFrontmatter(files.get(path));
1370
+ const expanded = dialect === 'mintlify' ? inlineMdxSnippets(body, path, files) : body;
1371
+ const prepared = forceCurrentColorCardIcons ? markForcedCurrentColorCardIcons(expanded) : expanded;
1372
+ const dialectTransformed = transform(replaceVariables(prepared, variables));
1373
+ // Raw HTML and JSX are supported by several documentation systems, not
1374
+ // only Mintlify. Normalize their safe structural subset for every source
1375
+ // dialect so presentational elements such as <button> cannot block a
1376
+ // strict import or leak into the destination as reader-visible markup.
1377
+ // The normalizer protects fenced and inline code before touching tags.
1378
+ const transformed = dialect === 'mintlify'
1379
+ ? dialectTransformed
1380
+ : transformPortableHtml(dialectTransformed);
1381
+ const normalized = dialect === 'mintlify' ? normalizeMintlifyPage(head, transformed) : { head, body: transformed };
1382
+ const markdown = `${normalized.head}${normalized.body}`;
1383
+ for (const item of unmappedConstructs(markdown))
1384
+ unmapped.add(item);
1385
+ return { path, markdown };
1386
+ });
1387
+ if (dialect === 'mintlify') {
1388
+ pages = materializeMintlifyOpenApiPages({
1389
+ pages,
1390
+ files: inputFiles,
1391
+ configuredSources: manifest.openapiSources,
1392
+ fallbackBaseUrl: manifest.apiBaseUrl,
1393
+ });
1394
+ }
1395
+ return { dialect, manifest, pages, redirects, unmapped: [...unmapped].sort() };
1396
+ }
1397
+ /** Materialize navigation-level OpenAPI markers into generated endpoint pages.
1398
+ * Local documents are read from the source tree; callers may provide a guarded
1399
+ * loader for public URL sources. Unavailable specs remain as API markers and
1400
+ * are reported without dropping their navigation entry. */
1401
+ export async function materializeMintlifyOpenApiNavigation(params) {
1402
+ if (params.result.dialect !== 'mintlify')
1403
+ return { result: params.result, warnings: [] };
1404
+ const files = new Map(params.files.map((file) => [normalizePath(file.path), file.content]));
1405
+ const pages = [...params.result.pages];
1406
+ const usedFiles = new Set(pages.map((page) => stripPageExtension(normalizePath(page.path))));
1407
+ const addedPages = new Set(usedFiles);
1408
+ const warnings = [];
1409
+ const loaded = new Map();
1410
+ const materializedSpecs = new Map();
1411
+ const load = (source) => {
1412
+ const cached = loaded.get(source);
1413
+ if (cached)
1414
+ return cached;
1415
+ const pending = /^https?:\/\//i.test(source)
1416
+ ? params.loadRemote?.(source) ?? Promise.resolve(null)
1417
+ : Promise.resolve(files.get(normalizePath(source)) ?? null);
1418
+ loaded.set(source, pending);
1419
+ return pending;
1420
+ };
1421
+ const operationRef = (value) => {
1422
+ const match = /^(?:(\S+)\s+)?(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE)\s+(\/\S*)$/i.exec(value.trim());
1423
+ if (!match)
1424
+ return null;
1425
+ return {
1426
+ ...(match[1] ? { source: normalizeSpecSource(match[1]) } : {}),
1427
+ operation: `${match[2].toUpperCase()} ${match[3]}`,
1428
+ };
1429
+ };
1430
+ const containsOperationRef = (nodes) => nodes.some((node) => (node.type === 'page' && Boolean(operationRef(node.file)))
1431
+ || (node.type === 'group' && containsOperationRef(node.items)));
1432
+ const materialize = (node) => {
1433
+ const key = `${node.spec}\0${node.title}`;
1434
+ const cached = materializedSpecs.get(key);
1435
+ if (cached)
1436
+ return cached;
1437
+ const pending = (async () => {
1438
+ let sourceText = null;
1439
+ try {
1440
+ sourceText = await load(node.spec);
1441
+ }
1442
+ catch (error) {
1443
+ warnings.push(`Could not load OpenAPI spec ${node.spec}: ${error instanceof Error ? error.message : String(error)}`);
1444
+ }
1445
+ if (!sourceText) {
1446
+ if (!warnings.some((warning) => warning.includes(node.spec)))
1447
+ warnings.push(`Could not load OpenAPI spec ${node.spec}.`);
1448
+ return null;
1449
+ }
1450
+ const result = materializeOpenApiNavigationDocument({
1451
+ text: sourceText,
1452
+ title: node.title,
1453
+ usedFiles,
1454
+ fallbackBaseUrl: params.result.manifest.apiBaseUrl,
1455
+ });
1456
+ if (!result)
1457
+ warnings.push(`OpenAPI spec ${node.spec} did not contain any importable operations.`);
1458
+ return result;
1459
+ })();
1460
+ materializedSpecs.set(key, pending);
1461
+ return pending;
1462
+ };
1463
+ const expand = async (nodes) => {
1464
+ // Mintlify permits an OpenAPI source beside authored navigation groups whose
1465
+ // page strings are operation selectors (for example, "GET /v1/apps"). Those
1466
+ // selectors are not file paths. Resolve them against the sibling document
1467
+ // while preserving the source's grouping and order.
1468
+ const localApis = nodes.filter((node) => node.type === 'api');
1469
+ if (localApis.length === 1 && containsOperationRef(nodes)) {
1470
+ const api = localApis[0];
1471
+ const document = await materialize(api);
1472
+ const itemsByOperation = new Map(document?.groups.flatMap((group) => group.items).map((item) => [item.operation, item]) ?? []);
1473
+ const pagesByFile = new Map(document?.pages.map((page) => [stripPageExtension(normalizePath(page.path)), page]) ?? []);
1474
+ const missing = new Set();
1475
+ const rewrite = async (entries) => {
1476
+ const rewritten = [];
1477
+ for (const entry of entries) {
1478
+ if (entry.type === 'group') {
1479
+ rewritten.push({ ...entry, items: await rewrite(entry.items) });
1480
+ continue;
1481
+ }
1482
+ if (entry === api) {
1483
+ // The marker supplied the document for the explicit operation list;
1484
+ // retaining it would append a duplicate, fully generated API tree.
1485
+ if (!document)
1486
+ rewritten.push(entry);
1487
+ continue;
1488
+ }
1489
+ if (entry.type === 'page') {
1490
+ const ref = operationRef(entry.file);
1491
+ if (ref) {
1492
+ const belongsToApi = !ref.source || ref.source === api.spec;
1493
+ const item = belongsToApi ? itemsByOperation.get(ref.operation) : undefined;
1494
+ const generatedPage = item ? pagesByFile.get(item.file) : undefined;
1495
+ if (item && generatedPage) {
1496
+ if (!addedPages.has(item.file)) {
1497
+ pages.push(generatedPage);
1498
+ addedPages.add(item.file);
1499
+ }
1500
+ rewritten.push({ ...entry, file: item.file, title: entry.title ?? item.title });
1501
+ }
1502
+ else {
1503
+ missing.add(entry.file);
1504
+ // Never serialize an HTTP operation selector as a page path. In
1505
+ // non-strict imports this label plus the unresolved API marker is
1506
+ // a safe, lintable fallback; strict imports stop on the warning.
1507
+ rewritten.push({ type: 'label', text: entry.title ?? ref.operation });
1508
+ }
1509
+ continue;
1510
+ }
1511
+ }
1512
+ if (entry.type === 'api')
1513
+ rewritten.push(...await expand([entry]));
1514
+ else
1515
+ rewritten.push(entry);
1516
+ }
1517
+ return rewritten;
1518
+ };
1519
+ const rewritten = await rewrite(nodes);
1520
+ if (document && missing.size) {
1521
+ warnings.push(`OpenAPI spec ${api.spec} did not contain ${missing.size} referenced operation(s): ${[...missing].slice(0, 5).join(', ')}${missing.size > 5 ? '…' : ''}`);
1522
+ }
1523
+ return rewritten;
1524
+ }
1525
+ const out = [];
1526
+ for (const node of nodes) {
1527
+ if (node.type === 'group') {
1528
+ out.push({ ...node, items: await expand(node.items) });
1529
+ continue;
1530
+ }
1531
+ if (node.type !== 'api') {
1532
+ out.push(node);
1533
+ continue;
1534
+ }
1535
+ const materialized = await materialize(node);
1536
+ if (!materialized) {
1537
+ out.push(node);
1538
+ continue;
1539
+ }
1540
+ for (const page of materialized.pages) {
1541
+ const file = stripPageExtension(normalizePath(page.path));
1542
+ if (!addedPages.has(file)) {
1543
+ pages.push(page);
1544
+ addedPages.add(file);
1545
+ }
1546
+ }
1547
+ out.push(...materialized.groups.map((group) => ({
1548
+ type: 'group',
1549
+ label: group.label,
1550
+ items: group.items.map((item) => ({ type: 'page', file: item.file, title: item.title })),
1551
+ })));
1552
+ }
1553
+ return out;
1554
+ };
1555
+ const spaces = [];
1556
+ for (const space of params.result.manifest.spaces) {
1557
+ spaces.push({ ...space, nav: await expand(space.nav) });
1558
+ }
1559
+ return {
1560
+ result: {
1561
+ ...params.result,
1562
+ manifest: { ...params.result.manifest, spaces },
1563
+ pages,
1564
+ },
1565
+ warnings,
1566
+ };
1567
+ }