blodemd 0.0.4 → 0.0.6

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 (185) hide show
  1. package/README.md +12 -1
  2. package/dev-server/app/[[...slug]]/page.tsx +139 -0
  3. package/dev-server/app/blodemd-dev/invalidate/route.ts +12 -0
  4. package/dev-server/app/blodemd-dev/static/[...path]/route.ts +32 -0
  5. package/dev-server/app/blodemd-dev/version/route.ts +14 -0
  6. package/dev-server/app/blodemd-internal/proxy/route.ts +86 -0
  7. package/dev-server/app/error.tsx +24 -0
  8. package/dev-server/app/globals.css +4 -0
  9. package/dev-server/app/layout.tsx +38 -0
  10. package/dev-server/app/not-found.tsx +18 -0
  11. package/dev-server/app/search/route.ts +17 -0
  12. package/dev-server/components/dev-reload-script.tsx +86 -0
  13. package/dev-server/components/providers.tsx +15 -0
  14. package/dev-server/lib/dev-state.ts +8 -0
  15. package/dev-server/lib/local-content-source.ts +103 -0
  16. package/dev-server/lib/local-runtime.tsx +558 -0
  17. package/dev-server/next.config.js +46 -0
  18. package/dev-server/package.json +57 -0
  19. package/dev-server/postcss.config.mjs +7 -0
  20. package/dev-server/public/glide-variable.woff2 +0 -0
  21. package/dev-server/tsconfig.json +49 -0
  22. package/dist/cli.mjs +299 -26
  23. package/dist/cli.mjs.map +1 -1
  24. package/docs/app/globals.css +455 -0
  25. package/docs/components/api/api-playground.tsx +295 -0
  26. package/docs/components/api/api-reference.tsx +121 -0
  27. package/docs/components/content/collection-index.tsx +114 -0
  28. package/docs/components/docs/contextual-menu.tsx +406 -0
  29. package/docs/components/docs/copy-page-menu.tsx +255 -0
  30. package/docs/components/docs/doc-header.tsx +192 -0
  31. package/docs/components/docs/doc-shell.tsx +289 -0
  32. package/docs/components/docs/doc-sidebar.tsx +206 -0
  33. package/docs/components/docs/doc-toc.tsx +45 -0
  34. package/docs/components/docs/mobile-nav.tsx +207 -0
  35. package/docs/components/mdx/accordion.tsx +83 -0
  36. package/docs/components/mdx/badge.tsx +79 -0
  37. package/docs/components/mdx/callout.tsx +88 -0
  38. package/docs/components/mdx/card.tsx +104 -0
  39. package/docs/components/mdx/code-block.tsx +75 -0
  40. package/docs/components/mdx/code-group.tsx +94 -0
  41. package/docs/components/mdx/color.tsx +87 -0
  42. package/docs/components/mdx/columns.tsx +25 -0
  43. package/docs/components/mdx/expandable.tsx +45 -0
  44. package/docs/components/mdx/field-layout.tsx +77 -0
  45. package/docs/components/mdx/frame.tsx +23 -0
  46. package/docs/components/mdx/get-text-content.ts +18 -0
  47. package/docs/components/mdx/icon.tsx +56 -0
  48. package/docs/components/mdx/index.tsx +96 -0
  49. package/docs/components/mdx/installer.tsx +20 -0
  50. package/docs/components/mdx/panel.tsx +11 -0
  51. package/docs/components/mdx/param-field.tsx +56 -0
  52. package/docs/components/mdx/preview.tsx +36 -0
  53. package/docs/components/mdx/prompt.tsx +63 -0
  54. package/docs/components/mdx/request-example.tsx +27 -0
  55. package/docs/components/mdx/response-field.tsx +42 -0
  56. package/docs/components/mdx/steps.tsx +92 -0
  57. package/docs/components/mdx/tabs.tsx +88 -0
  58. package/docs/components/mdx/tile.tsx +43 -0
  59. package/docs/components/mdx/tooltip.tsx +71 -0
  60. package/docs/components/mdx/tree.tsx +120 -0
  61. package/docs/components/mdx/type-table.tsx +71 -0
  62. package/docs/components/mdx/update.tsx +44 -0
  63. package/docs/components/mdx/video.tsx +12 -0
  64. package/docs/components/mdx/view.tsx +66 -0
  65. package/docs/components/providers.tsx +15 -0
  66. package/docs/components/ui/breadcrumb.tsx +92 -0
  67. package/docs/components/ui/button.tsx +90 -0
  68. package/docs/components/ui/card.tsx +92 -0
  69. package/docs/components/ui/command.tsx +139 -0
  70. package/docs/components/ui/dialog.tsx +97 -0
  71. package/docs/components/ui/field.tsx +237 -0
  72. package/docs/components/ui/input.tsx +105 -0
  73. package/docs/components/ui/label.tsx +22 -0
  74. package/docs/components/ui/popover.tsx +72 -0
  75. package/docs/components/ui/search.tsx +380 -0
  76. package/docs/components/ui/separator.tsx +26 -0
  77. package/docs/components/ui/sheet.tsx +104 -0
  78. package/docs/components/ui/sidebar.tsx +433 -0
  79. package/docs/components/ui/theme-toggle.tsx +62 -0
  80. package/docs/components/ui/tooltip.tsx +53 -0
  81. package/docs/lib/contextual-options.ts +193 -0
  82. package/docs/lib/docs-collection.ts +22 -0
  83. package/docs/lib/mdx.ts +90 -0
  84. package/docs/lib/navigation.ts +288 -0
  85. package/docs/lib/openapi.ts +158 -0
  86. package/docs/lib/routes.ts +10 -0
  87. package/docs/lib/server-cache.ts +83 -0
  88. package/docs/lib/shiki.ts +35 -0
  89. package/docs/lib/theme.ts +29 -0
  90. package/docs/lib/toc.ts +2 -0
  91. package/docs/lib/utils.ts +5 -0
  92. package/package.json +34 -3
  93. package/packages/@repo/common/dist/index.d.ts +9 -0
  94. package/packages/@repo/common/dist/index.d.ts.map +1 -0
  95. package/packages/@repo/common/dist/index.js +42 -0
  96. package/packages/@repo/common/package.json +34 -0
  97. package/packages/@repo/common/src/common.unit.test.ts +55 -0
  98. package/packages/@repo/common/src/index.ts +51 -0
  99. package/packages/@repo/contracts/dist/api-key.d.ts +30 -0
  100. package/packages/@repo/contracts/dist/api-key.d.ts.map +1 -0
  101. package/packages/@repo/contracts/dist/api-key.js +20 -0
  102. package/packages/@repo/contracts/dist/dates.d.ts +4 -0
  103. package/packages/@repo/contracts/dist/dates.d.ts.map +1 -0
  104. package/packages/@repo/contracts/dist/dates.js +2 -0
  105. package/packages/@repo/contracts/dist/deployment.d.ts +71 -0
  106. package/packages/@repo/contracts/dist/deployment.d.ts.map +1 -0
  107. package/packages/@repo/contracts/dist/deployment.js +46 -0
  108. package/packages/@repo/contracts/dist/domain.d.ts +94 -0
  109. package/packages/@repo/contracts/dist/domain.d.ts.map +1 -0
  110. package/packages/@repo/contracts/dist/domain.js +36 -0
  111. package/packages/@repo/contracts/dist/ids.d.ts +14 -0
  112. package/packages/@repo/contracts/dist/ids.d.ts.map +1 -0
  113. package/packages/@repo/contracts/dist/ids.js +10 -0
  114. package/packages/@repo/contracts/dist/index.d.ts +10 -0
  115. package/packages/@repo/contracts/dist/index.d.ts.map +1 -0
  116. package/packages/@repo/contracts/dist/index.js +11 -0
  117. package/packages/@repo/contracts/dist/pagination.d.ts +23 -0
  118. package/packages/@repo/contracts/dist/pagination.d.ts.map +1 -0
  119. package/packages/@repo/contracts/dist/pagination.js +15 -0
  120. package/packages/@repo/contracts/dist/project.d.ts +25 -0
  121. package/packages/@repo/contracts/dist/project.d.ts.map +1 -0
  122. package/packages/@repo/contracts/dist/project.js +23 -0
  123. package/packages/@repo/contracts/dist/tenant.d.ts +99 -0
  124. package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -0
  125. package/packages/@repo/contracts/dist/tenant.js +36 -0
  126. package/packages/@repo/contracts/dist/user.d.ts +9 -0
  127. package/packages/@repo/contracts/dist/user.d.ts.map +1 -0
  128. package/packages/@repo/contracts/dist/user.js +9 -0
  129. package/packages/@repo/contracts/package.json +37 -0
  130. package/packages/@repo/contracts/src/api-key.ts +27 -0
  131. package/packages/@repo/contracts/src/dates.ts +4 -0
  132. package/packages/@repo/contracts/src/deployment.ts +73 -0
  133. package/packages/@repo/contracts/src/domain.ts +51 -0
  134. package/packages/@repo/contracts/src/ids.ts +22 -0
  135. package/packages/@repo/contracts/src/index.ts +11 -0
  136. package/packages/@repo/contracts/src/pagination.ts +21 -0
  137. package/packages/@repo/contracts/src/project.ts +30 -0
  138. package/packages/@repo/contracts/src/tenant.ts +54 -0
  139. package/packages/@repo/contracts/src/user.ts +12 -0
  140. package/packages/@repo/models/dist/docs-config.d.ts +985 -0
  141. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -0
  142. package/packages/@repo/models/dist/docs-config.js +548 -0
  143. package/packages/@repo/models/dist/index.d.ts +3 -0
  144. package/packages/@repo/models/dist/index.d.ts.map +1 -0
  145. package/packages/@repo/models/dist/index.js +3 -0
  146. package/packages/@repo/models/dist/tenant.d.ts +25 -0
  147. package/packages/@repo/models/dist/tenant.d.ts.map +1 -0
  148. package/packages/@repo/models/dist/tenant.js +1 -0
  149. package/packages/@repo/models/package.json +37 -0
  150. package/packages/@repo/models/src/docs-config.ts +648 -0
  151. package/packages/@repo/models/src/index.ts +3 -0
  152. package/packages/@repo/models/src/tenant.ts +29 -0
  153. package/packages/@repo/prebuild/dist/index.d.ts +2 -0
  154. package/packages/@repo/prebuild/dist/index.d.ts.map +1 -0
  155. package/packages/@repo/prebuild/dist/index.js +2 -0
  156. package/packages/@repo/prebuild/dist/openapi.d.ts +43 -0
  157. package/packages/@repo/prebuild/dist/openapi.d.ts.map +1 -0
  158. package/packages/@repo/prebuild/dist/openapi.js +58 -0
  159. package/packages/@repo/prebuild/package.json +39 -0
  160. package/packages/@repo/prebuild/src/index.ts +2 -0
  161. package/packages/@repo/prebuild/src/openapi.ts +116 -0
  162. package/packages/@repo/previewing/dist/blob-source.d.ts +16 -0
  163. package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -0
  164. package/packages/@repo/previewing/dist/blob-source.js +110 -0
  165. package/packages/@repo/previewing/dist/content-source.d.ts +12 -0
  166. package/packages/@repo/previewing/dist/content-source.d.ts.map +1 -0
  167. package/packages/@repo/previewing/dist/content-source.js +1 -0
  168. package/packages/@repo/previewing/dist/fs-source.d.ts +11 -0
  169. package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -0
  170. package/packages/@repo/previewing/dist/fs-source.js +79 -0
  171. package/packages/@repo/previewing/dist/index.d.ts +120 -0
  172. package/packages/@repo/previewing/dist/index.d.ts.map +1 -0
  173. package/packages/@repo/previewing/dist/index.js +984 -0
  174. package/packages/@repo/previewing/package.json +41 -0
  175. package/packages/@repo/previewing/src/blob-source.ts +167 -0
  176. package/packages/@repo/previewing/src/content-source.ts +12 -0
  177. package/packages/@repo/previewing/src/fs-source.ts +111 -0
  178. package/packages/@repo/previewing/src/index.ts +1490 -0
  179. package/packages/@repo/previewing/src/index.unit.test.ts +290 -0
  180. package/packages/@repo/validation/dist/index.d.ts +12 -0
  181. package/packages/@repo/validation/dist/index.d.ts.map +1 -0
  182. package/packages/@repo/validation/dist/index.js +30 -0
  183. package/packages/@repo/validation/package.json +37 -0
  184. package/packages/@repo/validation/src/index.ts +59 -0
  185. package/packages/@repo/validation/src/mintlify-docs-schema.json +5016 -0
@@ -0,0 +1,193 @@
1
+ import type {
2
+ ContextualBuiltinOption,
3
+ ContextualCustomOption,
4
+ } from "@repo/models";
5
+
6
+ interface BuiltinOptionDefinition {
7
+ title: string;
8
+ description: string;
9
+ iconName: string;
10
+ type: "action" | "link";
11
+ }
12
+
13
+ export const builtinOptions: Record<
14
+ ContextualBuiltinOption,
15
+ BuiltinOptionDefinition
16
+ > = {
17
+ "add-mcp": {
18
+ description: "Copy the npx add-mcp command",
19
+ iconName: "CodeBracketsIcon",
20
+ title: "Copy MCP install command",
21
+ type: "action",
22
+ },
23
+ aistudio: {
24
+ description: "Ask questions about this page",
25
+ iconName: "GoogleColoredIcon",
26
+ title: "Open in Google AI Studio",
27
+ type: "link",
28
+ },
29
+ assistant: {
30
+ description: "Open the assistant with this page as context",
31
+ iconName: "SparkleIcon",
32
+ title: "Ask assistant",
33
+ type: "action",
34
+ },
35
+ chatgpt: {
36
+ description: "Ask questions about this page",
37
+ iconName: "OpenaiIcon",
38
+ title: "Open in ChatGPT",
39
+ type: "link",
40
+ },
41
+ claude: {
42
+ description: "Ask questions about this page",
43
+ iconName: "ClaudeaiIcon",
44
+ title: "Open in Claude",
45
+ type: "link",
46
+ },
47
+ copy: {
48
+ description: "Copy page as Markdown for LLMs",
49
+ iconName: "CopySimpleIcon",
50
+ title: "Copy page",
51
+ type: "action",
52
+ },
53
+ cursor: {
54
+ description: "Install MCP server in Cursor",
55
+ iconName: "CodeIcon",
56
+ title: "Connect to Cursor",
57
+ type: "link",
58
+ },
59
+ devin: {
60
+ description: "Create a Devin session with this page",
61
+ iconName: "CodeAssistantIcon",
62
+ title: "Open in Devin",
63
+ type: "link",
64
+ },
65
+ "devin-mcp": {
66
+ description: "Install MCP server in Devin",
67
+ iconName: "CodeAssistantIcon",
68
+ title: "Connect to Devin",
69
+ type: "link",
70
+ },
71
+ grok: {
72
+ description: "Ask questions about this page",
73
+ iconName: "GrokIcon",
74
+ title: "Open in Grok",
75
+ type: "link",
76
+ },
77
+ mcp: {
78
+ description: "Copy your MCP server URL to clipboard",
79
+ iconName: "Plugin1Icon",
80
+ title: "Copy MCP server URL",
81
+ type: "action",
82
+ },
83
+ perplexity: {
84
+ description: "Ask questions about this page",
85
+ iconName: "PerplexityIcon",
86
+ title: "Open in Perplexity",
87
+ type: "link",
88
+ },
89
+ view: {
90
+ description: "Open the current page as Markdown",
91
+ iconName: "MarkdownIcon",
92
+ title: "View as Markdown",
93
+ type: "link",
94
+ },
95
+ vscode: {
96
+ description: "Install MCP server in VS Code",
97
+ iconName: "CodeLinesIcon",
98
+ title: "Connect to VS Code",
99
+ type: "link",
100
+ },
101
+ windsurf: {
102
+ description: "Open Windsurf Cascade with this page",
103
+ iconName: "WindIcon",
104
+ title: "Open in Windsurf",
105
+ type: "link",
106
+ },
107
+ };
108
+
109
+ interface UrlContext {
110
+ pageUrl: string;
111
+ pagePath: string;
112
+ pageContent: string;
113
+ mcpServerUrl?: string;
114
+ }
115
+
116
+ const askPrompt = (url: string) =>
117
+ `Read from ${url} so I can ask questions about it.`;
118
+
119
+ const encoded = (text: string) => encodeURIComponent(text);
120
+
121
+ export const buildBuiltinUrl = (
122
+ id: ContextualBuiltinOption,
123
+ context: UrlContext
124
+ ): string | null => {
125
+ const { pageUrl, pagePath, mcpServerUrl } = context;
126
+
127
+ switch (id) {
128
+ case "chatgpt": {
129
+ return `https://chatgpt.com/?hints=search&q=${encoded(askPrompt(pageUrl))}`;
130
+ }
131
+ case "claude": {
132
+ return `https://claude.ai/new?q=${encoded(askPrompt(pageUrl))}`;
133
+ }
134
+ case "perplexity": {
135
+ return `https://www.perplexity.ai/?q=${encoded(askPrompt(pageUrl))}`;
136
+ }
137
+ case "grok": {
138
+ return `https://grok.com/?q=${encoded(askPrompt(pageUrl))}`;
139
+ }
140
+ case "aistudio": {
141
+ return `https://aistudio.google.com/prompts/new_chat?q=${encoded(askPrompt(pageUrl))}`;
142
+ }
143
+ case "devin": {
144
+ return `https://app.devin.ai/sessions?url=${encoded(pageUrl)}`;
145
+ }
146
+ case "windsurf": {
147
+ return `windsurf://cascade?url=${encoded(pageUrl)}`;
148
+ }
149
+ case "view": {
150
+ const mdxPath = pagePath === "index" ? "/index" : `/${pagePath}`;
151
+ return `${mdxPath}.mdx`;
152
+ }
153
+ case "cursor": {
154
+ return mcpServerUrl
155
+ ? `cursor://anysphere.cursor-deeplink/mcp/install?url=${encoded(mcpServerUrl)}`
156
+ : null;
157
+ }
158
+ case "vscode": {
159
+ return mcpServerUrl
160
+ ? `vscode://anysphere.open-mcp?url=${encoded(mcpServerUrl)}`
161
+ : null;
162
+ }
163
+ case "devin-mcp": {
164
+ return mcpServerUrl
165
+ ? `https://app.devin.ai/mcp/install?url=${encoded(mcpServerUrl)}`
166
+ : null;
167
+ }
168
+ default: {
169
+ return null;
170
+ }
171
+ }
172
+ };
173
+
174
+ export const resolveCustomHref = (
175
+ href: ContextualCustomOption["href"],
176
+ context: UrlContext
177
+ ): string => {
178
+ const substitute = (value: string) =>
179
+ value
180
+ .replaceAll("$page", context.pageContent)
181
+ .replaceAll("$path", context.pagePath)
182
+ .replaceAll("$mcp", context.mcpServerUrl ?? "");
183
+
184
+ if (typeof href === "string") {
185
+ return substitute(href);
186
+ }
187
+
188
+ const params = new URLSearchParams();
189
+ for (const item of href.query) {
190
+ params.set(item.key, substitute(item.value));
191
+ }
192
+ return `${href.base}?${params.toString()}`;
193
+ };
@@ -0,0 +1,22 @@
1
+ import type { SiteConfig } from "@repo/models";
2
+
3
+ export const getDocsCollection = (
4
+ config: SiteConfig
5
+ ): SiteConfig["collections"][number] | undefined =>
6
+ config.collections.find((collection) => collection.type === "docs");
7
+
8
+ export const getDocsNavigation = (config: SiteConfig) =>
9
+ getDocsCollection(config)?.navigation ?? config.navigation;
10
+
11
+ export const getDocsCollectionWithNavigation = (
12
+ config: SiteConfig
13
+ ): SiteConfig["collections"][number] | undefined => {
14
+ const docsCollection = getDocsCollection(config);
15
+ const docsNavigation = getDocsNavigation(config);
16
+
17
+ return docsCollection &&
18
+ docsNavigation &&
19
+ docsCollection.navigation !== docsNavigation
20
+ ? { ...docsCollection, navigation: docsNavigation }
21
+ : docsCollection;
22
+ };
@@ -0,0 +1,90 @@
1
+ import { run } from "@mdx-js/mdx";
2
+ import rehypeShikiFromHighlighter from "@shikijs/rehype/core";
3
+ import { compileMDX } from "next-mdx-remote/rsc";
4
+ import { createElement } from "react";
5
+ import type { ComponentType } from "react";
6
+ import * as jsxRuntime from "react/jsx-runtime";
7
+ import rehypeAutolinkHeadings from "rehype-autolink-headings";
8
+ import rehypeSlug from "rehype-slug";
9
+ import remarkGfm from "remark-gfm";
10
+
11
+ import { mdxComponents } from "@/components/mdx";
12
+
13
+ import { getHighlighter } from "./shiki";
14
+
15
+ const FRONTMATTER_REGEX = /^---\s*\n[\s\S]*?\n---\s*\n?/;
16
+
17
+ const stripFrontmatter = (source: string) => ({
18
+ body: source.replace(FRONTMATTER_REGEX, ""),
19
+ frontmatter: {} as Record<string, unknown>,
20
+ });
21
+
22
+ /**
23
+ * Renders pre-compiled MDX content. The `compiledSource` is a JS function body
24
+ * produced by `@repo/mdx-compiler` at deploy time via `compile()` with
25
+ * `outputFormat: 'function-body'`. This is the fast path — no parsing,
26
+ * no plugin execution, no Shiki. Sub-millisecond execution.
27
+ */
28
+ export const renderFromCompiled = async (compiledSource: string) => {
29
+ const mdxModule = await run(compiledSource, {
30
+ ...jsxRuntime,
31
+ baseUrl: import.meta.url,
32
+ });
33
+ const Content = mdxModule.default as ComponentType<{
34
+ components?: typeof mdxComponents;
35
+ }>;
36
+
37
+ return {
38
+ content: createElement(Content, { components: mdxComponents }),
39
+ frontmatter: {} as Record<string, unknown>,
40
+ };
41
+ };
42
+
43
+ /**
44
+ * Full MDX compilation + rendering. Used as fallback for local development
45
+ * (FsContentSource) and any content that wasn't pre-compiled at deploy time.
46
+ */
47
+ export const renderMdx = async (source: string) => {
48
+ const { body, frontmatter } = stripFrontmatter(source);
49
+ const highlighter = await getHighlighter();
50
+ const shikiTransformer = rehypeShikiFromHighlighter(highlighter, {
51
+ defaultColor: false,
52
+ themes: {
53
+ dark: "github-dark",
54
+ light: "github-light",
55
+ },
56
+ });
57
+ const shikiPlugin = () => shikiTransformer;
58
+
59
+ const result = await compileMDX({
60
+ components: mdxComponents,
61
+ options: {
62
+ blockDangerousJS: false,
63
+ blockJS: false,
64
+ mdxOptions: {
65
+ rehypePlugins: [
66
+ rehypeSlug,
67
+ [
68
+ rehypeAutolinkHeadings,
69
+ {
70
+ behavior: "append",
71
+ properties: {
72
+ className: [
73
+ "ml-2 text-[0.9em] opacity-0 transition-opacity hover:opacity-100",
74
+ ],
75
+ },
76
+ },
77
+ ],
78
+ shikiPlugin,
79
+ ],
80
+ remarkPlugins: [remarkGfm],
81
+ },
82
+ },
83
+ source: body,
84
+ });
85
+
86
+ return {
87
+ content: result.content,
88
+ frontmatter,
89
+ };
90
+ };
@@ -0,0 +1,288 @@
1
+ import { normalizePath } from "@repo/common";
2
+ import type { DocsNavigation } from "@repo/models";
3
+ import type { PageMetadata } from "@repo/previewing";
4
+
5
+ import type { OpenApiRegistry } from "./openapi";
6
+ import { toDocHref } from "./routes";
7
+
8
+ export interface NavPage {
9
+ type: "page";
10
+ title: string;
11
+ path: string;
12
+ source: "mdx" | "openapi";
13
+ identifier?: string;
14
+ sidebarTitle?: string;
15
+ icon?: string;
16
+ iconType?: string;
17
+ tag?: string;
18
+ hidden?: boolean;
19
+ deprecated?: boolean;
20
+ url?: string;
21
+ hideApiMarker?: boolean;
22
+ }
23
+
24
+ export interface NavGroup {
25
+ type: "group";
26
+ title: string;
27
+ items: NavPage[];
28
+ expanded?: boolean;
29
+ hidden?: boolean;
30
+ }
31
+
32
+ export type NavEntry = NavGroup | NavPage;
33
+
34
+ export const getNavPageTitle = (page: NavPage): string =>
35
+ page.sidebarTitle ?? page.title;
36
+
37
+ export const getNavPageHref = (page: NavPage, basePath: string): string =>
38
+ page.url ?? toDocHref(page.path, basePath);
39
+
40
+ const titleFromSlug = (slug: string) => {
41
+ const clean = slug.replaceAll("-", " ").split("/").pop() ?? slug;
42
+ if (clean === "index") {
43
+ return "Overview";
44
+ }
45
+ return clean.replaceAll(/\b\w/g, (char) => char.toUpperCase());
46
+ };
47
+
48
+ const createPageItem = (
49
+ page: string,
50
+ registry: OpenApiRegistry,
51
+ slugPrefix: string
52
+ ): NavPage => {
53
+ const entry = registry.byIdentifier.get(page);
54
+ if (entry) {
55
+ return {
56
+ identifier: entry.identifier,
57
+ path: entry.slug,
58
+ source: "openapi",
59
+ title: entry.operation.summary ?? entry.identifier,
60
+ type: "page",
61
+ };
62
+ }
63
+
64
+ const normalized = normalizePath(page);
65
+ const path = slugPrefix
66
+ ? normalizePath(`${slugPrefix}/${normalized}`)
67
+ : normalized;
68
+
69
+ return {
70
+ path,
71
+ source: "mdx",
72
+ title: titleFromSlug(page),
73
+ type: "page",
74
+ };
75
+ };
76
+
77
+ // oxlint-disable-next-line eslint/complexity
78
+ export const buildNavigation = (
79
+ navigation: DocsNavigation | undefined,
80
+ registry: OpenApiRegistry,
81
+ slugPrefix = ""
82
+ ) => {
83
+ const entries: NavEntry[] = [];
84
+ const groups = navigation?.groups ?? [];
85
+ const hiddenPages = new Set(navigation?.hidden);
86
+
87
+ for (const group of groups) {
88
+ const title = group.group ?? "Untitled";
89
+ const items: NavPage[] = [];
90
+ const groupHidden = group.hidden === true;
91
+
92
+ if (group.pages?.length) {
93
+ for (const page of group.pages) {
94
+ const item = createPageItem(page, registry, slugPrefix);
95
+ if (groupHidden || hiddenPages.has(page)) {
96
+ item.hidden = true;
97
+ }
98
+ items.push(item);
99
+ }
100
+ } else if (group.openapi) {
101
+ const sourceKey =
102
+ typeof group.openapi === "string"
103
+ ? `${group.openapi}::::`
104
+ : `${group.openapi.source}::${group.openapi.directory ?? ""}::${(
105
+ group.openapi.include ?? []
106
+ ).join("|")}`;
107
+ const sourceEntries = registry.bySource.get(sourceKey) ?? [];
108
+ for (const entry of sourceEntries) {
109
+ items.push({
110
+ hidden: groupHidden || undefined,
111
+ identifier: entry.identifier,
112
+ path: entry.slug,
113
+ source: "openapi",
114
+ title: entry.operation.summary ?? entry.identifier,
115
+ type: "page",
116
+ });
117
+ }
118
+ }
119
+
120
+ if (items.length) {
121
+ entries.push({
122
+ expanded: group.expanded,
123
+ hidden: groupHidden || undefined,
124
+ items,
125
+ title,
126
+ type: "group",
127
+ });
128
+ }
129
+ }
130
+
131
+ const topPages = navigation?.pages ?? [];
132
+ for (const page of topPages) {
133
+ const item = createPageItem(page, registry, slugPrefix);
134
+ if (hiddenPages.has(page)) {
135
+ item.hidden = true;
136
+ }
137
+ entries.push(item);
138
+ }
139
+
140
+ return entries;
141
+ };
142
+
143
+ const enrichPage = (
144
+ page: NavPage,
145
+ metadataMap: Map<string, PageMetadata>
146
+ ): NavPage => {
147
+ const meta = metadataMap.get(page.path);
148
+ if (!meta) {
149
+ return page;
150
+ }
151
+ return {
152
+ ...page,
153
+ deprecated: meta.deprecated ?? page.deprecated,
154
+ hidden: meta.hidden ?? page.hidden,
155
+ hideApiMarker: meta.hideApiMarker ?? page.hideApiMarker,
156
+ icon: meta.icon ?? page.icon,
157
+ iconType: meta.iconType ?? page.iconType,
158
+ sidebarTitle: meta.sidebarTitle ?? page.sidebarTitle,
159
+ tag: meta.tag ?? page.tag,
160
+ title: meta.title ?? page.title,
161
+ url: meta.url ?? page.url,
162
+ };
163
+ };
164
+
165
+ export const enrichNavWithMetadata = (
166
+ entries: NavEntry[],
167
+ metadataMap: Map<string, PageMetadata>
168
+ ): NavEntry[] =>
169
+ entries.map((entry) => {
170
+ if (entry.type === "page") {
171
+ return enrichPage(entry, metadataMap);
172
+ }
173
+ return {
174
+ ...entry,
175
+ items: entry.items.map((item) => enrichPage(item, metadataMap)),
176
+ };
177
+ });
178
+
179
+ export const getVisibleNavigation = (entries: NavEntry[]): NavEntry[] => {
180
+ const visible: NavEntry[] = [];
181
+ for (const entry of entries) {
182
+ if (entry.hidden) {
183
+ continue;
184
+ }
185
+ if (entry.type === "group") {
186
+ const visibleItems = entry.items.filter((item) => !item.hidden);
187
+ if (visibleItems.length) {
188
+ visible.push({ ...entry, items: visibleItems });
189
+ }
190
+ } else {
191
+ visible.push(entry);
192
+ }
193
+ }
194
+ return visible;
195
+ };
196
+
197
+ export const flattenNav = (entries: NavEntry[]): NavPage[] => {
198
+ const pages: NavPage[] = [];
199
+ for (const entry of entries) {
200
+ if (entry.type === "page") {
201
+ pages.push(entry);
202
+ } else {
203
+ pages.push(...entry.items);
204
+ }
205
+ }
206
+ return pages;
207
+ };
208
+
209
+ export interface NavTab {
210
+ label: string;
211
+ icon?: string;
212
+ href?: string;
213
+ entries: NavEntry[];
214
+ slugPrefix?: string;
215
+ }
216
+
217
+ export const buildTabbedNavigation = (
218
+ navigation: DocsNavigation | undefined,
219
+ registry: OpenApiRegistry,
220
+ slugPrefix = ""
221
+ ): NavTab[] | null => {
222
+ if (!navigation?.tabs?.length) {
223
+ return null;
224
+ }
225
+ const { tabs } = navigation;
226
+
227
+ return tabs.map((tab) => {
228
+ if (tab.href) {
229
+ return {
230
+ entries: [],
231
+ href: tab.href,
232
+ icon: tab.icon,
233
+ label: tab.label,
234
+ };
235
+ }
236
+
237
+ const tabNav: DocsNavigation = {
238
+ groups: tab.groups,
239
+ hidden: navigation.hidden,
240
+ pages: tab.pages,
241
+ };
242
+ const entries = buildNavigation(tabNav, registry, slugPrefix);
243
+ const [firstPage] = flattenNav(entries);
244
+
245
+ return {
246
+ entries,
247
+ icon: tab.icon,
248
+ label: tab.label,
249
+ slugPrefix: firstPage?.path,
250
+ };
251
+ });
252
+ };
253
+
254
+ export const findActiveTabIndex = (
255
+ tabs: NavTab[],
256
+ currentPath: string
257
+ ): number => {
258
+ const normalized = normalizePath(currentPath);
259
+ for (const [index, tab] of tabs.entries()) {
260
+ if (tab.href) {
261
+ continue;
262
+ }
263
+ const pages = flattenNav(tab.entries);
264
+ if (pages.some((page) => page.path === normalized)) {
265
+ return index;
266
+ }
267
+ }
268
+ return 0;
269
+ };
270
+
271
+ export const findBreadcrumbs = (entries: NavEntry[], path: string) => {
272
+ const normalized = normalizePath(path);
273
+ for (const entry of entries) {
274
+ if (entry.type === "page" && entry.path === normalized) {
275
+ return [{ label: getNavPageTitle(entry), path: entry.path }];
276
+ }
277
+ if (entry.type === "group") {
278
+ const found = entry.items.find((item) => item.path === normalized);
279
+ if (found) {
280
+ return [
281
+ { label: entry.title, path: "" },
282
+ { label: getNavPageTitle(found), path: found.path },
283
+ ];
284
+ }
285
+ }
286
+ }
287
+ return [] as { label: string; path: string }[];
288
+ };