specra 0.1.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 (142) hide show
  1. package/LICENSE.MD +21 -0
  2. package/README.md +157 -0
  3. package/dist/app/api/mdx-watch/route.d.mts +12 -0
  4. package/dist/app/api/mdx-watch/route.d.ts +12 -0
  5. package/dist/app/api/mdx-watch/route.js +98 -0
  6. package/dist/app/api/mdx-watch/route.js.map +1 -0
  7. package/dist/app/api/mdx-watch/route.mjs +71 -0
  8. package/dist/app/api/mdx-watch/route.mjs.map +1 -0
  9. package/dist/app/docs-page.d.mts +32 -0
  10. package/dist/app/docs-page.d.ts +32 -0
  11. package/dist/app/docs-page.js +4072 -0
  12. package/dist/app/docs-page.js.map +1 -0
  13. package/dist/app/docs-page.mjs +14 -0
  14. package/dist/app/docs-page.mjs.map +1 -0
  15. package/dist/app/layout.css +297 -0
  16. package/dist/app/layout.css.map +1 -0
  17. package/dist/app/layout.d.mts +19 -0
  18. package/dist/app/layout.d.ts +19 -0
  19. package/dist/app/layout.js +112 -0
  20. package/dist/app/layout.js.map +1 -0
  21. package/dist/app/layout.mjs +13 -0
  22. package/dist/app/layout.mjs.map +1 -0
  23. package/dist/chunk-DR4EPLMT.mjs +1013 -0
  24. package/dist/chunk-DR4EPLMT.mjs.map +1 -0
  25. package/dist/chunk-INL2EC72.mjs +170 -0
  26. package/dist/chunk-INL2EC72.mjs.map +1 -0
  27. package/dist/chunk-IZFGEAD6.mjs +61 -0
  28. package/dist/chunk-IZFGEAD6.mjs.map +1 -0
  29. package/dist/chunk-KTRWWAGL.mjs +50 -0
  30. package/dist/chunk-KTRWWAGL.mjs.map +1 -0
  31. package/dist/chunk-MZJHJ6BV.mjs +21 -0
  32. package/dist/chunk-MZJHJ6BV.mjs.map +1 -0
  33. package/dist/chunk-NXRIAL7T.mjs +3119 -0
  34. package/dist/chunk-NXRIAL7T.mjs.map +1 -0
  35. package/dist/components/index.d.mts +822 -0
  36. package/dist/components/index.d.ts +822 -0
  37. package/dist/components/index.js +3738 -0
  38. package/dist/components/index.js.map +1 -0
  39. package/dist/components/index.mjs +3627 -0
  40. package/dist/components/index.mjs.map +1 -0
  41. package/dist/index.css +297 -0
  42. package/dist/index.css.map +1 -0
  43. package/dist/index.d.mts +545 -0
  44. package/dist/index.d.ts +545 -0
  45. package/dist/index.js +4648 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/index.mjs +347 -0
  48. package/dist/index.mjs.map +1 -0
  49. package/dist/lib/index.d.mts +798 -0
  50. package/dist/lib/index.d.ts +798 -0
  51. package/dist/lib/index.js +1301 -0
  52. package/dist/lib/index.js.map +1 -0
  53. package/dist/lib/index.mjs +89 -0
  54. package/dist/lib/index.mjs.map +1 -0
  55. package/package.json +119 -0
  56. package/src/app/api/mdx-watch/route.ts +86 -0
  57. package/src/app/docs-page.tsx +212 -0
  58. package/src/app/layout.tsx +74 -0
  59. package/src/components/docs/accordion.tsx +53 -0
  60. package/src/components/docs/api/api-endpoint.tsx +59 -0
  61. package/src/components/docs/api/api-params.tsx +43 -0
  62. package/src/components/docs/api/api-playground.tsx +233 -0
  63. package/src/components/docs/api/api-reference.tsx +291 -0
  64. package/src/components/docs/api/api-response.tsx +48 -0
  65. package/src/components/docs/api/index.ts +5 -0
  66. package/src/components/docs/badge.tsx +22 -0
  67. package/src/components/docs/breadcrumb.tsx +51 -0
  68. package/src/components/docs/callout.tsx +109 -0
  69. package/src/components/docs/card.tsx +84 -0
  70. package/src/components/docs/category-index.tsx +112 -0
  71. package/src/components/docs/code-block.tsx +129 -0
  72. package/src/components/docs/columns.tsx +45 -0
  73. package/src/components/docs/componentTextProps.ts +85 -0
  74. package/src/components/docs/dev-mode-badge.tsx +35 -0
  75. package/src/components/docs/doc-layout-wrapper.tsx +54 -0
  76. package/src/components/docs/doc-layout.tsx +111 -0
  77. package/src/components/docs/doc-loading.tsx +15 -0
  78. package/src/components/docs/doc-metadata.tsx +55 -0
  79. package/src/components/docs/doc-navigation.tsx +62 -0
  80. package/src/components/docs/doc-tags.tsx +25 -0
  81. package/src/components/docs/draft-badge.tsx +10 -0
  82. package/src/components/docs/footer.tsx +47 -0
  83. package/src/components/docs/frame.tsx +22 -0
  84. package/src/components/docs/header.tsx +122 -0
  85. package/src/components/docs/hot-reload-indicator.tsx +77 -0
  86. package/src/components/docs/icon.tsx +70 -0
  87. package/src/components/docs/image-card.tsx +95 -0
  88. package/src/components/docs/image.tsx +73 -0
  89. package/src/components/docs/index.ts +48 -0
  90. package/src/components/docs/math.tsx +46 -0
  91. package/src/components/docs/mdx-components.tsx +166 -0
  92. package/src/components/docs/mdx-hot-reload.tsx +37 -0
  93. package/src/components/docs/mermaid.tsx +77 -0
  94. package/src/components/docs/mobile-doc-layout.tsx +115 -0
  95. package/src/components/docs/not-found-content.tsx +55 -0
  96. package/src/components/docs/search-highlight.tsx +127 -0
  97. package/src/components/docs/search-modal.tsx +223 -0
  98. package/src/components/docs/sidebar-skeleton.tsx +39 -0
  99. package/src/components/docs/sidebar.tsx +323 -0
  100. package/src/components/docs/site-banner.tsx +92 -0
  101. package/src/components/docs/steps.tsx +29 -0
  102. package/src/components/docs/tab-context.tsx +28 -0
  103. package/src/components/docs/tab-groups.tsx +50 -0
  104. package/src/components/docs/table-of-contents.tsx +104 -0
  105. package/src/components/docs/tabs.tsx +63 -0
  106. package/src/components/docs/theme-toggle.tsx +39 -0
  107. package/src/components/docs/tooltip.tsx +37 -0
  108. package/src/components/docs/version-switcher.tsx +52 -0
  109. package/src/components/docs/video.tsx +80 -0
  110. package/src/components/global/index.ts +3 -0
  111. package/src/components/global/version-not-found.tsx +26 -0
  112. package/src/components/index.ts +8 -0
  113. package/src/components/theme-provider.tsx +11 -0
  114. package/src/components/ui/badge.tsx +46 -0
  115. package/src/components/ui/button.tsx +60 -0
  116. package/src/components/ui/dialog.tsx +143 -0
  117. package/src/components/ui/index.ts +6 -0
  118. package/src/components/ui/input.tsx +21 -0
  119. package/src/components/ui/textarea.tsx +18 -0
  120. package/src/index.ts +41 -0
  121. package/src/lib/api-parser.types.ts +78 -0
  122. package/src/lib/api.types.ts +202 -0
  123. package/src/lib/category.ts +71 -0
  124. package/src/lib/config.server.ts +170 -0
  125. package/src/lib/config.ts +20 -0
  126. package/src/lib/config.types.ts +295 -0
  127. package/src/lib/dev-utils.ts +75 -0
  128. package/src/lib/index.ts +27 -0
  129. package/src/lib/mdx-cache.ts +200 -0
  130. package/src/lib/mdx.ts +402 -0
  131. package/src/lib/parsers/base-parser.ts +16 -0
  132. package/src/lib/parsers/index.ts +69 -0
  133. package/src/lib/parsers/openapi-parser.ts +251 -0
  134. package/src/lib/parsers/postman-parser.ts +301 -0
  135. package/src/lib/parsers/specra-parser.ts +24 -0
  136. package/src/lib/redirects.ts +40 -0
  137. package/src/lib/remark-code-meta.ts +23 -0
  138. package/src/lib/sidebar-utils.ts +188 -0
  139. package/src/lib/toc.ts +24 -0
  140. package/src/lib/utils.ts +36 -0
  141. package/src/specra.config.json +124 -0
  142. package/src/styles/globals.css +427 -0
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Caching layer for MDX operations to improve development performance
3
+ *
4
+ * This module provides in-memory caching for expensive file system operations
5
+ * that occur during static generation. In development mode, caches are
6
+ * invalidated automatically when files change.
7
+ */
8
+
9
+ import { Doc, getVersions, getAllDocs, getDocBySlug } from './mdx'
10
+ import { watch } from 'fs'
11
+ import { join } from 'path'
12
+ import { PerfTimer, logCacheOperation } from './dev-utils'
13
+
14
+ const isDevelopment = process.env.NODE_ENV === 'development'
15
+
16
+ // Cache stores
17
+ const versionsCache = {
18
+ data: null as string[] | null,
19
+ timestamp: 0,
20
+ }
21
+
22
+ const allDocsCache = new Map<string, {
23
+ data: Doc[]
24
+ timestamp: number
25
+ }>()
26
+
27
+ const docBySlugCache = new Map<string, {
28
+ data: Doc | null
29
+ timestamp: number
30
+ }>()
31
+
32
+ // Cache TTL (time to live) in milliseconds
33
+ const CACHE_TTL = isDevelopment ? 5000 : 60000 // 5s in dev, 60s in prod
34
+
35
+ // Track if we've set up file watchers
36
+ let watchersInitialized = false
37
+
38
+ /**
39
+ * Initialize file watchers to invalidate cache on changes
40
+ * Only runs in development mode
41
+ */
42
+ function initializeWatchers() {
43
+ if (!isDevelopment || watchersInitialized) return
44
+
45
+ watchersInitialized = true
46
+ const docsPath = join(process.cwd(), 'docs')
47
+
48
+ try {
49
+ watch(docsPath, { recursive: true }, (eventType, filename) => {
50
+ if (!filename) return
51
+
52
+ // Invalidate relevant caches when MDX or JSON files change
53
+ if (filename.endsWith('.mdx') || filename.endsWith('.json')) {
54
+ // Extract version from path
55
+ const parts = filename.split(/[/\\]/)
56
+ const version = parts[0]
57
+
58
+ // Clear all docs cache for this version
59
+ allDocsCache.delete(version)
60
+
61
+ // Clear individual doc caches for this version
62
+ const cacheKeysToDelete: string[] = []
63
+ docBySlugCache.forEach((_, key) => {
64
+ if (key.startsWith(`${version}:`)) {
65
+ cacheKeysToDelete.push(key)
66
+ }
67
+ })
68
+ cacheKeysToDelete.forEach(key => docBySlugCache.delete(key))
69
+
70
+ // Clear versions cache if directory structure changed
71
+ if (eventType === 'rename') {
72
+ versionsCache.data = null
73
+ }
74
+
75
+ console.log(`[MDX Cache] Invalidated cache for: ${filename}`)
76
+ }
77
+ })
78
+
79
+ console.log('[MDX Cache] File watchers initialized')
80
+ } catch (error) {
81
+ console.error('[MDX Cache] Failed to initialize watchers:', error)
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Check if a cache entry is still valid
87
+ */
88
+ function isCacheValid(timestamp: number): boolean {
89
+ return Date.now() - timestamp < CACHE_TTL
90
+ }
91
+
92
+ /**
93
+ * Cached version of getVersions()
94
+ */
95
+ export function getCachedVersions(): string[] {
96
+ // Initialize watchers on first use
97
+ initializeWatchers()
98
+
99
+ if (versionsCache.data && isCacheValid(versionsCache.timestamp)) {
100
+ logCacheOperation('hit', 'versions')
101
+ return versionsCache.data
102
+ }
103
+
104
+ logCacheOperation('miss', 'versions')
105
+ const timer = new PerfTimer('getVersions')
106
+ const versions = getVersions()
107
+ timer.end()
108
+
109
+ versionsCache.data = versions
110
+ versionsCache.timestamp = Date.now()
111
+
112
+ return versions
113
+ }
114
+
115
+ /**
116
+ * Cached version of getAllDocs()
117
+ */
118
+ export async function getCachedAllDocs(version = 'v1.0.0'): Promise<Doc[]> {
119
+ // Initialize watchers on first use
120
+ initializeWatchers()
121
+
122
+ const cached = allDocsCache.get(version)
123
+ if (cached && isCacheValid(cached.timestamp)) {
124
+ logCacheOperation('hit', `getAllDocs:${version}`)
125
+ return cached.data
126
+ }
127
+
128
+ logCacheOperation('miss', `getAllDocs:${version}`)
129
+ const timer = new PerfTimer(`getAllDocs(${version})`)
130
+ const docs = await getAllDocs(version)
131
+ timer.end()
132
+
133
+ allDocsCache.set(version, {
134
+ data: docs,
135
+ timestamp: Date.now(),
136
+ })
137
+
138
+ return docs
139
+ }
140
+
141
+ /**
142
+ * Cached version of getDocBySlug()
143
+ */
144
+ export async function getCachedDocBySlug(
145
+ slug: string,
146
+ version = 'v1.0.0'
147
+ ): Promise<Doc | null> {
148
+ // Initialize watchers on first use
149
+ initializeWatchers()
150
+
151
+ const cacheKey = `${version}:${slug}`
152
+ const cached = docBySlugCache.get(cacheKey)
153
+
154
+ if (cached && isCacheValid(cached.timestamp)) {
155
+ logCacheOperation('hit', `getDocBySlug:${cacheKey}`)
156
+ return cached.data
157
+ }
158
+
159
+ logCacheOperation('miss', `getDocBySlug:${cacheKey}`)
160
+ const timer = new PerfTimer(`getDocBySlug(${slug})`)
161
+ const doc = await getDocBySlug(slug, version)
162
+ timer.end()
163
+
164
+ docBySlugCache.set(cacheKey, {
165
+ data: doc,
166
+ timestamp: Date.now(),
167
+ })
168
+
169
+ return doc
170
+ }
171
+
172
+ /**
173
+ * Manually clear all caches
174
+ * Useful for testing or when you want to force a refresh
175
+ */
176
+ export function clearAllCaches() {
177
+ versionsCache.data = null
178
+ allDocsCache.clear()
179
+ docBySlugCache.clear()
180
+ console.log('[MDX Cache] All caches cleared')
181
+ }
182
+
183
+ /**
184
+ * Get cache statistics for debugging
185
+ */
186
+ export function getCacheStats() {
187
+ return {
188
+ versions: {
189
+ cached: versionsCache.data !== null,
190
+ age: versionsCache.timestamp ? Date.now() - versionsCache.timestamp : 0,
191
+ },
192
+ allDocs: {
193
+ entries: allDocsCache.size,
194
+ versions: Array.from(allDocsCache.keys()),
195
+ },
196
+ docBySlug: {
197
+ entries: docBySlugCache.size,
198
+ },
199
+ }
200
+ }
package/src/lib/mdx.ts ADDED
@@ -0,0 +1,402 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import matter from "gray-matter"
4
+ import { getAllCategoryConfigs } from "./category"
5
+ import { sortSidebarItems, sortSidebarGroups, buildSidebarStructure, type SidebarGroup } from "./sidebar-utils"
6
+
7
+ const DOCS_DIR = path.join(process.cwd(), "docs")
8
+
9
+ /**
10
+ * Calculate reading time based on word count
11
+ * Average reading speed: 200 words per minute
12
+ */
13
+ function calculateReadingTime(content: string): { minutes: number; words: number } {
14
+ const words = content.trim().split(/\s+/).length
15
+ const minutes = Math.ceil(words / 200)
16
+ return { minutes, words }
17
+ }
18
+
19
+ export interface DocMeta {
20
+ title: string
21
+ description?: string
22
+ slug?: string
23
+ section?: string
24
+ group?: string
25
+ sidebar?: string
26
+ order?: number
27
+ sidebar_position?: number
28
+ content?: string
29
+ last_updated?: string
30
+ draft?: boolean
31
+ authors?: Array<{ id: string; name?: string }>
32
+ tags?: string[]
33
+ redirect_from?: string[]
34
+ reading_time?: number
35
+ word_count?: number
36
+ icon?: string // Icon name for sidebar display (Lucide icon name)
37
+ tab_group?: string // Tab group ID for organizing docs into tabs
38
+ }
39
+
40
+ export interface Doc {
41
+ slug: string
42
+ filePath: string // Original file path for sidebar grouping
43
+ title: string
44
+ meta: DocMeta
45
+ content: string
46
+ categoryLabel?: string // Label from _category_.json
47
+ categoryPosition?: number // Position from _category_.json
48
+ categoryCollapsible?: boolean // Collapsible from _category_.json
49
+ categoryCollapsed?: boolean // Default collapsed state from _category_.json
50
+ categoryIcon?: string // Icon from _category_.json
51
+ categoryTabGroup?: string // Tab group from _category_.json
52
+ }
53
+
54
+ export interface TocItem {
55
+ id: string
56
+ title: string
57
+ level: number
58
+ }
59
+
60
+ export function getVersions(): string[] {
61
+ try {
62
+ const versions = fs.readdirSync(DOCS_DIR)
63
+ return versions.filter((v) => fs.statSync(path.join(DOCS_DIR, v)).isDirectory())
64
+ } catch (error) {
65
+ return ["v1.0.0"]
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Recursively find all MDX files in a directory
71
+ */
72
+ function findMdxFiles(dir: string, baseDir: string = dir): string[] {
73
+ const files: string[] = []
74
+
75
+ try {
76
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
77
+
78
+ for (const entry of entries) {
79
+ const fullPath = path.join(dir, entry.name)
80
+
81
+ if (entry.isDirectory()) {
82
+ files.push(...findMdxFiles(fullPath, baseDir))
83
+ } else if (entry.isFile() && entry.name.endsWith(".mdx")) {
84
+ // Get relative path from base directory and normalize to forward slashes
85
+ const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/')
86
+ files.push(relativePath)
87
+ }
88
+ }
89
+ } catch (error) {
90
+ console.error(`Error reading directory ${dir}:`, error)
91
+ }
92
+
93
+ return files
94
+ }
95
+
96
+ /**
97
+ * Internal function to read a doc from file path
98
+ */
99
+ function readDocFromFile(filePath: string, originalSlug: string): Doc | null {
100
+ try {
101
+ if (!fs.existsSync(filePath)) {
102
+ return null
103
+ }
104
+
105
+ const fileContents = fs.readFileSync(filePath, "utf8")
106
+ const { data, content } = matter(fileContents)
107
+
108
+ // Calculate reading time
109
+ const { minutes, words } = calculateReadingTime(content)
110
+
111
+ // If custom slug provided, replace only the filename part, keep the folder structure
112
+ let finalSlug = originalSlug
113
+ if (data.slug) {
114
+ const customSlug = data.slug.replace(/^\//, '')
115
+ const parts = originalSlug.split("/")
116
+
117
+ if (parts.length > 1) {
118
+ // Keep folder structure, replace only filename
119
+ parts[parts.length - 1] = customSlug
120
+ finalSlug = parts.join("/")
121
+ } else {
122
+ // Root level file, use custom slug as-is
123
+ finalSlug = customSlug
124
+ }
125
+ }
126
+
127
+ return {
128
+ slug: finalSlug,
129
+ filePath: originalSlug, // Keep original file path for sidebar
130
+ title: data.title || originalSlug,
131
+ meta: {
132
+ ...data,
133
+ content,
134
+ reading_time: minutes,
135
+ word_count: words,
136
+ } as DocMeta,
137
+ content,
138
+ }
139
+ } catch (error) {
140
+ console.error(`Error reading file ${filePath}:`, error)
141
+ return null
142
+ }
143
+ }
144
+
145
+ export async function getDocBySlug(slug: string, version = "v1.0.0"): Promise<Doc | null> {
146
+ try {
147
+ // Try direct file first
148
+ let filePath = path.join(DOCS_DIR, version, `${slug}.mdx`)
149
+ let doc = readDocFromFile(filePath, slug)
150
+
151
+ if (doc) return doc
152
+
153
+ // If not found, try index.mdx in the folder
154
+ filePath = path.join(DOCS_DIR, version, slug, "index.mdx")
155
+ doc = readDocFromFile(filePath, slug)
156
+
157
+ if (doc) return doc
158
+
159
+ // If still not found, search all docs for a matching custom slug
160
+ const versionDir = path.join(DOCS_DIR, version)
161
+ if (!fs.existsSync(versionDir)) {
162
+ return null
163
+ }
164
+
165
+ const mdxFiles = findMdxFiles(versionDir)
166
+
167
+ for (const file of mdxFiles) {
168
+ const fileSlug = file.replace(/\.mdx$/, "")
169
+ const testPath = path.join(versionDir, file.endsWith("index.mdx") ? file : `${fileSlug}.mdx`)
170
+ const testDoc = readDocFromFile(testPath, fileSlug)
171
+
172
+ if (testDoc && testDoc.slug === slug) {
173
+ return testDoc
174
+ }
175
+ }
176
+
177
+ return null
178
+ } catch (error) {
179
+ console.error(`Error reading doc ${slug}:`, error)
180
+ return null
181
+ }
182
+ }
183
+
184
+ export async function getAllDocs(version = "v1.0.0"): Promise<Doc[]> {
185
+ try {
186
+ const versionDir = path.join(DOCS_DIR, version)
187
+
188
+ if (!fs.existsSync(versionDir)) {
189
+ return []
190
+ }
191
+
192
+ const mdxFiles = findMdxFiles(versionDir)
193
+ const categoryConfigs = getAllCategoryConfigs(version)
194
+
195
+ const docs = await Promise.all(
196
+ mdxFiles.map(async (file) => {
197
+ const originalFilePath = file.replace(/\.mdx$/, "")
198
+ let slug = originalFilePath
199
+
200
+ // If this is an index.mdx, use the directory path as the slug
201
+ if (file.endsWith("/index.mdx") || file === "index.mdx") {
202
+ slug = path.dirname(file).replace(/\\/g, '/')
203
+ if (slug === ".") slug = "" // Root index
204
+ }
205
+
206
+ const doc = await getDocBySlug(slug, version)
207
+
208
+ // Override filePath to preserve the original file structure
209
+ if (doc) {
210
+ doc.filePath = originalFilePath
211
+
212
+ // Apply category config if exists
213
+ const folderPath = path.dirname(originalFilePath).replace(/\\/g, '/')
214
+ if (folderPath !== ".") {
215
+ const categoryConfig = categoryConfigs.get(folderPath)
216
+ if (categoryConfig) {
217
+ doc.categoryLabel = categoryConfig.label
218
+ // Use position if available, otherwise fall back to sidebar_position
219
+ doc.categoryPosition = categoryConfig.position ?? categoryConfig.sidebar_position
220
+ doc.categoryCollapsible = categoryConfig.collapsible
221
+ doc.categoryCollapsed = categoryConfig.collapsed
222
+ doc.categoryIcon = categoryConfig.icon
223
+ doc.categoryTabGroup = categoryConfig.tab_group
224
+ }
225
+ }
226
+ }
227
+
228
+ return doc
229
+ }),
230
+ )
231
+
232
+ const isDevelopment = process.env.NODE_ENV === "development"
233
+
234
+ // Create a map to track unique slugs and avoid duplicates
235
+ const uniqueDocs = new Map<string, Doc>()
236
+
237
+ docs
238
+ .filter((doc): doc is Doc => doc !== null)
239
+ // Filter out drafts in production
240
+ .filter((doc) => isDevelopment || !doc.meta.draft)
241
+ .forEach((doc) => {
242
+ // Use the doc's slug (which may be custom from frontmatter) as the key
243
+ uniqueDocs.set(doc.slug, doc)
244
+ })
245
+
246
+ return Array.from(uniqueDocs.values()).sort((a, b) => {
247
+ const orderA = a.meta.sidebar_position ?? a.meta.order ?? 999
248
+ const orderB = b.meta.sidebar_position ?? b.meta.order ?? 999
249
+ return orderA - orderB
250
+ })
251
+ } catch (error) {
252
+ console.error(`Error getting all docs for version ${version}:`, error)
253
+ return []
254
+ }
255
+ }
256
+
257
+ // export function getAdjacentDocs(currentSlug: string, allDocs: Doc[]): { previous?: Doc; next?: Doc } {
258
+ // const currentIndex = allDocs.findIndex((doc) => doc.slug === currentSlug)
259
+
260
+ // if (currentIndex === -1) {
261
+ // return {}
262
+ // }
263
+
264
+ // return {
265
+ // previous: currentIndex > 0 ? allDocs[currentIndex - 1] : undefined,
266
+ // next: currentIndex < allDocs.length - 1 ? allDocs[currentIndex + 1] : undefined,
267
+ // }
268
+ // }
269
+
270
+ // Flatten the sidebar structure into a linear order
271
+ function flattenSidebarOrder(
272
+ rootGroups: Record<string, SidebarGroup>,
273
+ standalone: Doc[]
274
+ ): Doc[] {
275
+ const flatDocs: Doc[] = []
276
+
277
+ // Recursively flatten groups - intermix folders and files by position
278
+ const flattenGroup = (group: SidebarGroup) => {
279
+ const sortedChildren = sortSidebarGroups(group.children)
280
+ const sortedItems = sortSidebarItems(group.items)
281
+
282
+ // Merge child groups and items, then sort by position
283
+ const merged: Array<{type: 'group', group: SidebarGroup, position: number} | {type: 'item', doc: Doc, position: number}> = [
284
+ ...sortedChildren.map(([, childGroup]) => ({
285
+ type: 'group' as const,
286
+ group: childGroup,
287
+ position: childGroup.position
288
+ })),
289
+ ...sortedItems.map((doc) => ({
290
+ type: 'item' as const,
291
+ doc,
292
+ position: doc.meta.sidebar_position ?? doc.meta.order ?? 999
293
+ }))
294
+ ]
295
+
296
+ // Sort by position
297
+ merged.sort((a, b) => a.position - b.position)
298
+
299
+ // Process in sorted order
300
+ merged.forEach((item) => {
301
+ if (item.type === 'group') {
302
+ flattenGroup(item.group)
303
+ } else {
304
+ flatDocs.push(item.doc)
305
+ }
306
+ })
307
+ }
308
+
309
+ // Add standalone items first
310
+ sortSidebarItems(standalone).forEach((doc) => {
311
+ flatDocs.push(doc)
312
+ })
313
+
314
+ // Then add all grouped items
315
+ const sortedRootGroups = sortSidebarGroups(rootGroups)
316
+ sortedRootGroups.forEach(([, group]) => {
317
+ flattenGroup(group)
318
+ })
319
+
320
+ return flatDocs
321
+ }
322
+
323
+ export function getAdjacentDocs(currentSlug: string, allDocs: Doc[]): { previous?: Doc; next?: Doc } {
324
+ // Build the same sidebar structure
325
+ const { rootGroups, standalone } = buildSidebarStructure(allDocs)
326
+
327
+ // Flatten into the same order as shown in the sidebar
328
+ const orderedDocs = flattenSidebarOrder(rootGroups, standalone)
329
+
330
+ // Find current doc in the ordered list
331
+ const currentIndex = orderedDocs.findIndex((doc) => doc.slug === currentSlug)
332
+
333
+ if (currentIndex === -1) {
334
+ return {}
335
+ }
336
+
337
+ const currentDoc = orderedDocs[currentIndex]
338
+
339
+ // Get current doc's tab group (from meta or category)
340
+ const currentTabGroup = currentDoc.meta?.tab_group || currentDoc.categoryTabGroup
341
+
342
+ // Filter docs to match the current doc's tab group status
343
+ // If current has a tab group, only show docs in the same tab group
344
+ // If current has NO tab group, only show docs with NO tab group
345
+ const filteredDocs = orderedDocs.filter((doc) => {
346
+ const docTabGroup = doc.meta?.tab_group || doc.categoryTabGroup
347
+
348
+ // If current doc has a tab group, only include docs with the same tab group
349
+ if (currentTabGroup) {
350
+ return docTabGroup === currentTabGroup
351
+ }
352
+
353
+ // If current doc has no tab group, only include docs with no tab group
354
+ return !docTabGroup
355
+ })
356
+
357
+ // Find current doc's index within the filtered list
358
+ const filteredIndex = filteredDocs.findIndex((doc) => doc.slug === currentSlug)
359
+
360
+ if (filteredIndex === -1) {
361
+ return {}
362
+ }
363
+
364
+ return {
365
+ previous: filteredIndex > 0 ? filteredDocs[filteredIndex - 1] : undefined,
366
+ next: filteredIndex < filteredDocs.length - 1 ? filteredDocs[filteredIndex + 1] : undefined,
367
+ }
368
+ }
369
+
370
+ export function extractTableOfContents(content: string): TocItem[] {
371
+ const headingRegex = /^(#{2,3})\s+(.+)$/gm
372
+ const toc: TocItem[] = []
373
+ let match
374
+
375
+ while ((match = headingRegex.exec(content)) !== null) {
376
+ const level = match[1].length
377
+ const text = match[2]
378
+ // Generate ID the same way rehype-slug does
379
+ const id = text
380
+ .toLowerCase()
381
+ .replace(/\s+/g, "-") // Replace spaces with hyphens first
382
+ .replace(/[^a-z0-9-]/g, "") // Remove special chars (dots, slashes, etc)
383
+ .replace(/^-|-$/g, "") // Remove leading/trailing hyphens
384
+
385
+ toc.push({ id, title: text, level })
386
+ }
387
+
388
+ return toc
389
+ }
390
+
391
+ /**
392
+ * Check if a slug represents a category (has child documents)
393
+ */
394
+ export function isCategoryPage(slug: string, allDocs: Doc[]): boolean {
395
+ return allDocs.some((doc) => {
396
+ const parts = doc.slug.split("/")
397
+ const docParent = parts.slice(0, -1).join("/")
398
+ return docParent === slug && doc.slug !== slug
399
+ })
400
+ }
401
+
402
+
@@ -0,0 +1,16 @@
1
+ import type { SpecraApiSpec } from "../api-parser.types"
2
+
3
+ /**
4
+ * Base interface for all API spec parsers
5
+ */
6
+ export interface ApiSpecParser {
7
+ /**
8
+ * Parse the input spec and convert to Specra format
9
+ */
10
+ parse(input: any): SpecraApiSpec
11
+
12
+ /**
13
+ * Validate if the input is in the expected format
14
+ */
15
+ validate(input: any): boolean
16
+ }
@@ -0,0 +1,69 @@
1
+ import type { SpecraApiSpec } from "../api-parser.types"
2
+ import type { ApiSpecParser } from "./base-parser"
3
+ import { SpecraParser } from "./specra-parser"
4
+ import { OpenApiParser } from "./openapi-parser"
5
+ import { PostmanParser } from "./postman-parser"
6
+
7
+ export type ParserType = "auto" | "specra" | "openapi" | "postman"
8
+
9
+ /**
10
+ * Registry of all available parsers
11
+ */
12
+ const parsers: Map<string, ApiSpecParser> = new Map([
13
+ ["specra", new SpecraParser()],
14
+ ["openapi", new OpenApiParser()],
15
+ ["postman", new PostmanParser()],
16
+ ])
17
+
18
+ /**
19
+ * Auto-detect the parser type based on the input structure
20
+ */
21
+ export function detectParserType(input: any): ParserType {
22
+ if (!input || typeof input !== "object") {
23
+ throw new Error("Invalid API spec: input must be an object")
24
+ }
25
+
26
+ // Check for Postman Collection
27
+ if (input.info?.schema?.includes("v2")) {
28
+ return "postman"
29
+ }
30
+
31
+ // Check for OpenAPI/Swagger
32
+ if (input.openapi || input.swagger) {
33
+ return "openapi"
34
+ }
35
+
36
+ // Check for Specra format
37
+ if (input.endpoints && Array.isArray(input.endpoints)) {
38
+ return "specra"
39
+ }
40
+
41
+ throw new Error(
42
+ "Unable to auto-detect API spec format. Supported formats: Specra, OpenAPI 3.x, Postman Collection v2.x"
43
+ )
44
+ }
45
+
46
+ /**
47
+ * Parse an API spec using the specified or auto-detected parser
48
+ */
49
+ export function parseApiSpec(input: any, parserType: ParserType = "auto"): SpecraApiSpec {
50
+ // Auto-detect if needed
51
+ const actualType = parserType === "auto" ? detectParserType(input) : parserType
52
+
53
+ // Get the parser
54
+ const parser = parsers.get(actualType)
55
+ if (!parser) {
56
+ throw new Error(`Unknown parser type: ${actualType}`)
57
+ }
58
+
59
+ // Validate and parse
60
+ if (!parser.validate(input)) {
61
+ throw new Error(`Input does not match ${actualType} format`)
62
+ }
63
+
64
+ return parser.parse(input)
65
+ }
66
+
67
+ // Export parsers for direct use
68
+ export { SpecraParser, OpenApiParser, PostmanParser }
69
+ export type { ApiSpecParser }