@geenius/docs 0.1.0 → 0.4.1
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.
- package/CHANGELOG.md +13 -0
- package/README.md +53 -1
- package/package.json +101 -13
- package/packages/convex/dist/index.d.ts +503 -0
- package/packages/convex/dist/index.js +482 -0
- package/packages/convex/dist/index.js.map +1 -0
- package/packages/react/dist/index.d.ts +439 -0
- package/packages/react/dist/index.js +4954 -0
- package/packages/react/dist/index.js.map +1 -0
- package/packages/react-css/{src/styles.css → dist/index.css} +183 -223
- package/packages/react-css/dist/index.css.map +1 -0
- package/packages/react-css/dist/index.d.ts +443 -0
- package/packages/react-css/dist/index.js +5058 -0
- package/packages/react-css/dist/index.js.map +1 -0
- package/packages/shared/dist/index.d.ts +684 -0
- package/packages/shared/dist/index.js +788 -0
- package/packages/shared/dist/index.js.map +1 -0
- package/packages/solidjs/dist/index.d.ts +435 -0
- package/packages/solidjs/dist/index.js +4584 -0
- package/packages/solidjs/dist/index.js.map +1 -0
- package/packages/solidjs-css/{src/styles.css → dist/index.css} +183 -223
- package/packages/solidjs-css/dist/index.css.map +1 -0
- package/packages/solidjs-css/dist/index.d.ts +432 -0
- package/packages/solidjs-css/dist/index.js +4934 -0
- package/packages/solidjs-css/dist/index.js.map +1 -0
- package/.changeset/config.json +0 -11
- package/.github/CODEOWNERS +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -16
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -11
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -10
- package/.github/dependabot.yml +0 -11
- package/.github/workflows/ci.yml +0 -23
- package/.github/workflows/release.yml +0 -29
- package/.nvmrc +0 -1
- package/.project/ACCOUNT.yaml +0 -4
- package/.project/IDEAS.yaml +0 -7
- package/.project/PROJECT.yaml +0 -11
- package/.project/ROADMAP.yaml +0 -15
- package/CODE_OF_CONDUCT.md +0 -16
- package/CONTRIBUTING.md +0 -26
- package/SECURITY.md +0 -15
- package/SUPPORT.md +0 -8
- package/packages/convex/README.md +0 -1
- package/packages/convex/package.json +0 -12
- package/packages/convex/src/convex.config.ts +0 -3
- package/packages/convex/src/index.ts +0 -3
- package/packages/convex/src/mutations.ts +0 -270
- package/packages/convex/src/queries.ts +0 -175
- package/packages/convex/src/schema.ts +0 -55
- package/packages/react/README.md +0 -1
- package/packages/react/package.json +0 -36
- package/packages/react/src/DocsLayout.tsx +0 -116
- package/packages/react/src/DocsProvider.tsx +0 -93
- package/packages/react/src/RouterDocsContent.tsx +0 -148
- package/packages/react/src/RouterDocsLayout.tsx +0 -161
- package/packages/react/src/components/Breadcrumbs.tsx +0 -34
- package/packages/react/src/components/DocPage.tsx +0 -191
- package/packages/react/src/components/DocSearch.tsx +0 -140
- package/packages/react/src/components/DocSidebar.tsx +0 -86
- package/packages/react/src/components/DocsLayout.tsx +0 -62
- package/packages/react/src/components/EditButton.tsx +0 -26
- package/packages/react/src/components/PageNavigation.tsx +0 -45
- package/packages/react/src/components/TableOfContents.tsx +0 -46
- package/packages/react/src/components/VersionSelector.tsx +0 -60
- package/packages/react/src/components/index.ts +0 -9
- package/packages/react/src/hooks/index.ts +0 -8
- package/packages/react/src/hooks/useDocSearch.ts +0 -55
- package/packages/react/src/hooks/useDocs.ts +0 -57
- package/packages/react/src/hooks/useDocsAdmin.ts +0 -151
- package/packages/react/src/hooks/useTableOfContents.ts +0 -66
- package/packages/react/src/index.ts +0 -38
- package/packages/react/src/pages/DocSearchPage.tsx +0 -129
- package/packages/react/src/pages/DocViewPage.tsx +0 -158
- package/packages/react/src/pages/DocsAdminPage.tsx +0 -330
- package/packages/react/src/pages/DocsIndexPage.tsx +0 -172
- package/packages/react/src/pages/index.ts +0 -4
- package/packages/react/src/useDocs.ts +0 -58
- package/packages/react/tsup.config.ts +0 -12
- package/packages/react-css/README.md +0 -1
- package/packages/react-css/package.json +0 -37
- package/packages/react-css/src/DocsLayout.tsx +0 -117
- package/packages/react-css/src/DocsProvider.tsx +0 -93
- package/packages/react-css/src/RouterDocsContent.tsx +0 -60
- package/packages/react-css/src/RouterDocsLayout.tsx +0 -101
- package/packages/react-css/src/components/DocPage.tsx +0 -21
- package/packages/react-css/src/components/DocSearch.tsx +0 -55
- package/packages/react-css/src/components/DocSidebar.tsx +0 -56
- package/packages/react-css/src/components/DocsLayout.tsx +0 -28
- package/packages/react-css/src/components/common.tsx +0 -93
- package/packages/react-css/src/components/index.ts +0 -5
- package/packages/react-css/src/hooks/index.ts +0 -2
- package/packages/react-css/src/index.ts +0 -6
- package/packages/react-css/src/index.tsx +0 -3
- package/packages/react-css/src/pages/DocViewPage.tsx +0 -78
- package/packages/react-css/src/pages/DocsAdminPage.tsx +0 -101
- package/packages/react-css/src/pages/DocsIndexPage.tsx +0 -68
- package/packages/react-css/src/pages/index.ts +0 -3
- package/packages/react-css/src/useDocs.ts +0 -58
- package/packages/react-css/tsconfig.json +0 -19
- package/packages/react-css/tsup.config.ts +0 -10
- package/packages/shared/README.md +0 -1
- package/packages/shared/package.json +0 -31
- package/packages/shared/src/__tests__/docs.test.ts +0 -69
- package/packages/shared/src/config.ts +0 -80
- package/packages/shared/src/index.ts +0 -179
- package/packages/shared/src/providers/astro.ts +0 -94
- package/packages/shared/src/providers/fumadocs.ts +0 -116
- package/packages/shared/src/providers/internal.ts +0 -80
- package/packages/shared/src/types.ts +0 -73
- package/packages/shared/tsconfig.json +0 -18
- package/packages/shared/tsup.config.ts +0 -12
- package/packages/shared/vitest.config.ts +0 -4
- package/packages/solidjs/README.md +0 -1
- package/packages/solidjs/package.json +0 -33
- package/packages/solidjs/src/DocsLayout.tsx +0 -87
- package/packages/solidjs/src/DocsProvider.tsx +0 -95
- package/packages/solidjs/src/RouterDocsContent.tsx +0 -147
- package/packages/solidjs/src/RouterDocsLayout.tsx +0 -161
- package/packages/solidjs/src/components/Breadcrumbs.tsx +0 -27
- package/packages/solidjs/src/components/DocPage.tsx +0 -110
- package/packages/solidjs/src/components/DocSearch.tsx +0 -81
- package/packages/solidjs/src/components/DocSidebar.tsx +0 -92
- package/packages/solidjs/src/components/DocsLayout.tsx +0 -38
- package/packages/solidjs/src/components/EditButton.tsx +0 -15
- package/packages/solidjs/src/components/PageNavigation.tsx +0 -31
- package/packages/solidjs/src/components/TableOfContents.tsx +0 -41
- package/packages/solidjs/src/components/VersionSelector.tsx +0 -30
- package/packages/solidjs/src/components/index.ts +0 -9
- package/packages/solidjs/src/createDocs.ts +0 -62
- package/packages/solidjs/src/index.ts +0 -28
- package/packages/solidjs/src/pages/DocSearchPage.tsx +0 -72
- package/packages/solidjs/src/pages/DocViewPage.tsx +0 -80
- package/packages/solidjs/src/pages/DocsAdminPage.tsx +0 -123
- package/packages/solidjs/src/pages/DocsIndexPage.tsx +0 -85
- package/packages/solidjs/src/pages/index.ts +0 -4
- package/packages/solidjs/src/primitives/createDocSearch.ts +0 -42
- package/packages/solidjs/src/primitives/createDocs.ts +0 -35
- package/packages/solidjs/src/primitives/createDocsAdmin.ts +0 -63
- package/packages/solidjs/src/primitives/createTableOfContents.ts +0 -51
- package/packages/solidjs/src/primitives/index.ts +0 -4
- package/packages/solidjs/tsup.config.ts +0 -12
- package/packages/solidjs-css/README.md +0 -1
- package/packages/solidjs-css/package.json +0 -36
- package/packages/solidjs-css/src/DocsLayout.tsx +0 -106
- package/packages/solidjs-css/src/DocsProvider.tsx +0 -95
- package/packages/solidjs-css/src/RouterDocsContent.tsx +0 -54
- package/packages/solidjs-css/src/RouterDocsLayout.tsx +0 -104
- package/packages/solidjs-css/src/createDocs.ts +0 -62
- package/packages/solidjs-css/src/index.ts +0 -7
- package/packages/solidjs-css/src/index.tsx +0 -17
- package/packages/solidjs-css/src/pages/DocViewPage.tsx +0 -111
- package/packages/solidjs-css/src/pages/DocsAdminPage.tsx +0 -332
- package/packages/solidjs-css/src/pages/DocsIndexPage.tsx +0 -116
- package/packages/solidjs-css/src/pages/index.ts +0 -3
- package/packages/solidjs-css/src/primitives/index.ts +0 -1
- package/packages/solidjs-css/tsconfig.json +0 -20
- package/packages/solidjs-css/tsup.config.ts +0 -10
- package/pnpm-workspace.yaml +0 -2
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@geenius-docs/shared",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "Framework-agnostic docs types, providers, and utilities",
|
|
6
|
-
"main": "./src/index.ts",
|
|
7
|
-
"types": "./src/index.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": "./src/index.ts"
|
|
10
|
-
},
|
|
11
|
-
"dependencies": {
|
|
12
|
-
"gray-matter": "^4.0.3"
|
|
13
|
-
},
|
|
14
|
-
"scripts": {
|
|
15
|
-
"type-check": "tsc --noEmit",
|
|
16
|
-
"test": "vitest run",
|
|
17
|
-
"test:watch": "vitest",
|
|
18
|
-
"test:coverage": "vitest run --coverage"
|
|
19
|
-
},
|
|
20
|
-
"author": "Antigravity HQ",
|
|
21
|
-
"license": "MIT",
|
|
22
|
-
"engines": {
|
|
23
|
-
"node": ">=20.0.0"
|
|
24
|
-
},
|
|
25
|
-
"publishConfig": {
|
|
26
|
-
"access": "public"
|
|
27
|
-
},
|
|
28
|
-
"devDependencies": {
|
|
29
|
-
"vitest": "^4.0.0"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
calcWordCount, slugify, highlightMatch,
|
|
4
|
-
searchDocs, buildDocsIndex, defaultDocsConfig,
|
|
5
|
-
} from '../index'
|
|
6
|
-
|
|
7
|
-
describe('calcWordCount', () => {
|
|
8
|
-
it('counts words and estimates reading time', () => {
|
|
9
|
-
const result = calcWordCount('The quick brown fox jumps over the lazy dog')
|
|
10
|
-
expect(result.wordCount).toBe(9)
|
|
11
|
-
expect(result.readingTime).toBeGreaterThanOrEqual(1)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('handles empty content', () => {
|
|
15
|
-
const result = calcWordCount('')
|
|
16
|
-
expect(result.wordCount).toBe(0)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('handles long content with realistic reading time', () => {
|
|
20
|
-
const words = Array(500).fill('word').join(' ')
|
|
21
|
-
const result = calcWordCount(words)
|
|
22
|
-
expect(result.wordCount).toBe(500)
|
|
23
|
-
expect(result.readingTime).toBeGreaterThanOrEqual(2)
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
describe('slugify', () => {
|
|
28
|
-
it('converts to lowercase with hyphens', () => {
|
|
29
|
-
expect(slugify('Getting Started')).toBe('getting-started')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('removes special characters', () => {
|
|
33
|
-
expect(slugify('API Reference (v2)')).toBe('api-reference-v2')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('trims leading/trailing hyphens', () => {
|
|
37
|
-
expect(slugify(' Hello World! ')).toBe('hello-world')
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe('highlightMatch', () => {
|
|
42
|
-
it('returns snippet around match', () => {
|
|
43
|
-
const text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
|
|
44
|
-
const result = highlightMatch(text, 'dolor')
|
|
45
|
-
expect(result).toContain('dolor')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('returns start of text when no match', () => {
|
|
49
|
-
const result = highlightMatch('Hello world', 'xyz', 160)
|
|
50
|
-
expect(result.length).toBeLessThanOrEqual(160)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('returns text slice when query is empty', () => {
|
|
54
|
-
const result = highlightMatch('Hello world', '')
|
|
55
|
-
expect(result).toBe('Hello world')
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
describe('Search & Config', () => {
|
|
60
|
-
it('searchDocs returns empty for blank query', () => {
|
|
61
|
-
expect(searchDocs('', [])).toEqual([])
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('defaultDocsConfig has expected fields', () => {
|
|
65
|
-
expect(defaultDocsConfig.siteName).toBe('Docs')
|
|
66
|
-
expect(defaultDocsConfig.cmdKEnabled).toBe(true)
|
|
67
|
-
expect(defaultDocsConfig.showReadingTime).toBe(true)
|
|
68
|
-
})
|
|
69
|
-
})
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// @geenius-docs/shared — src/config.ts
|
|
2
|
-
|
|
3
|
-
import type { DocsConfig } from './types'
|
|
4
|
-
|
|
5
|
-
let _config: DocsConfig | null = null
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Initialize the docs system with a configuration.
|
|
9
|
-
* Call this once at app startup before using any docs features.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```ts
|
|
13
|
-
* import { configureDocs } from '@geenius-docs/shared'
|
|
14
|
-
*
|
|
15
|
-
* configureDocs({
|
|
16
|
-
* baseUrl: '/docs',
|
|
17
|
-
* siteName: 'My Docs',
|
|
18
|
-
* versionsEnabled: true,
|
|
19
|
-
* })
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
export function configureDocs(config: DocsConfig): void {
|
|
23
|
-
_config = config
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Get the current docs configuration.
|
|
28
|
-
* Throws if `configureDocs()` has not been called.
|
|
29
|
-
*/
|
|
30
|
-
export function getDocsConfig(): DocsConfig {
|
|
31
|
-
if (!_config) {
|
|
32
|
-
throw new Error('Docs not configured. Call configureDocs() first.')
|
|
33
|
-
}
|
|
34
|
-
return _config
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Check if docs have been configured.
|
|
39
|
-
*/
|
|
40
|
-
export function isDocsConfigured(): boolean {
|
|
41
|
-
return _config !== null
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Reset docs configuration (useful for testing).
|
|
46
|
-
*/
|
|
47
|
-
export function resetDocsConfig(): void {
|
|
48
|
-
_config = null
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Create a fully resolved docs config with defaults.
|
|
53
|
-
*/
|
|
54
|
-
export function defineDocsConfig(
|
|
55
|
-
overrides: Partial<DocsConfig>
|
|
56
|
-
): DocsConfig {
|
|
57
|
-
return {
|
|
58
|
-
baseUrl: '/docs',
|
|
59
|
-
defaultAccess: 'public',
|
|
60
|
-
versionsEnabled: false,
|
|
61
|
-
cmdKEnabled: true,
|
|
62
|
-
showReadingTime: true,
|
|
63
|
-
showLastEdited: true,
|
|
64
|
-
printModeEnabled: true,
|
|
65
|
-
...overrides,
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Merge an existing config with partial overrides.
|
|
71
|
-
*/
|
|
72
|
-
export function mergeDocsConfig(
|
|
73
|
-
base: DocsConfig,
|
|
74
|
-
overrides: Partial<DocsConfig>
|
|
75
|
-
): DocsConfig {
|
|
76
|
-
return {
|
|
77
|
-
...base,
|
|
78
|
-
...overrides,
|
|
79
|
-
}
|
|
80
|
-
}
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
export * from './types'
|
|
2
|
-
|
|
3
|
-
import type { DocPage, DocSection, TocItem, BreadcrumbItem, SearchResult, DocsConfig } from './types'
|
|
4
|
-
|
|
5
|
-
// ─── Utility: extract ToC from MDX heading lines ─────────────
|
|
6
|
-
export function extractToc(mdxContent: string): TocItem[] {
|
|
7
|
-
const headingRegex = /^(#{2,4})\s+(.+)$/gm
|
|
8
|
-
const items: TocItem[] = []
|
|
9
|
-
let match: RegExpExecArray | null
|
|
10
|
-
|
|
11
|
-
while ((match = headingRegex.exec(mdxContent)) !== null) {
|
|
12
|
-
const level = match[1].length as 2 | 3 | 4
|
|
13
|
-
const text = match[2].trim()
|
|
14
|
-
const id = slugify(text)
|
|
15
|
-
items.push({ id, text, level, children: [] })
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Nest h3 under h2, h4 under h3
|
|
19
|
-
const nested: TocItem[] = []
|
|
20
|
-
let currentH2: TocItem | null = null
|
|
21
|
-
let currentH3: TocItem | null = null
|
|
22
|
-
|
|
23
|
-
for (const item of items) {
|
|
24
|
-
if (item.level === 2) {
|
|
25
|
-
currentH2 = item
|
|
26
|
-
currentH3 = null
|
|
27
|
-
nested.push(item)
|
|
28
|
-
} else if (item.level === 3) {
|
|
29
|
-
currentH3 = item
|
|
30
|
-
if (currentH2) {
|
|
31
|
-
currentH2.children.push(item)
|
|
32
|
-
} else {
|
|
33
|
-
nested.push(item)
|
|
34
|
-
}
|
|
35
|
-
} else if (item.level === 4) {
|
|
36
|
-
if (currentH3) {
|
|
37
|
-
currentH3.children.push(item)
|
|
38
|
-
} else if (currentH2) {
|
|
39
|
-
currentH2.children.push(item)
|
|
40
|
-
} else {
|
|
41
|
-
nested.push(item)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return nested
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ─── Utility: count words and reading time ───────────────────
|
|
50
|
-
export function calcWordCount(content: string): { wordCount: number; readingTime: number } {
|
|
51
|
-
const stripped = content
|
|
52
|
-
.replace(/```[\s\S]*?```/g, '')
|
|
53
|
-
.replace(/`[^`]+`/g, '')
|
|
54
|
-
.replace(/!\[.*?\]\(.*?\)/g, '')
|
|
55
|
-
.replace(/\[([^\]]+)\]\(.*?\)/g, '$1')
|
|
56
|
-
.replace(/[#*_~>|-]/g, '')
|
|
57
|
-
.trim()
|
|
58
|
-
|
|
59
|
-
const words = stripped.split(/\s+/).filter(Boolean)
|
|
60
|
-
const wordCount = words.length
|
|
61
|
-
const readingTime = Math.max(1, Math.ceil(wordCount / 225))
|
|
62
|
-
|
|
63
|
-
return { wordCount, readingTime }
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ─── Utility: build breadcrumbs from section tree ────────────
|
|
67
|
-
export function buildBreadcrumbs(
|
|
68
|
-
sections: DocSection[],
|
|
69
|
-
sectionId: string,
|
|
70
|
-
slug: string
|
|
71
|
-
): BreadcrumbItem[] {
|
|
72
|
-
const crumbs: BreadcrumbItem[] = [{ title: 'Docs', href: '/docs' }]
|
|
73
|
-
const sectionMap = new Map(sections.map(s => [s.id, s]))
|
|
74
|
-
|
|
75
|
-
const trail: DocSection[] = []
|
|
76
|
-
let current = sectionMap.get(sectionId)
|
|
77
|
-
while (current) {
|
|
78
|
-
trail.unshift(current)
|
|
79
|
-
current = current.parentId ? sectionMap.get(current.parentId) : undefined
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
for (const section of trail) {
|
|
83
|
-
crumbs.push({ title: section.title, href: `/docs/${section.slug}` })
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (slug) {
|
|
87
|
-
crumbs.push({ title: slug, href: `/docs/${trail[trail.length - 1]?.slug ?? ''}/${slug}` })
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return crumbs
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ─── Utility: full-text search index ─────────────────────────
|
|
94
|
-
export function buildDocsIndex(pages: DocPage[], sections: DocSection[]): SearchResult[] {
|
|
95
|
-
const sectionMap = new Map(sections.map(s => [s.id, s]))
|
|
96
|
-
|
|
97
|
-
return pages
|
|
98
|
-
.filter(p => p.status === 'published')
|
|
99
|
-
.map(page => {
|
|
100
|
-
const section = sectionMap.get(page.sectionId)
|
|
101
|
-
return {
|
|
102
|
-
pageId: page.id,
|
|
103
|
-
pageTitle: page.title,
|
|
104
|
-
sectionTitle: section?.title ?? '',
|
|
105
|
-
sectionSlug: section?.slug ?? '',
|
|
106
|
-
slug: page.slug,
|
|
107
|
-
highlight: page.excerpt ?? page.content.slice(0, 160),
|
|
108
|
-
score: 0,
|
|
109
|
-
tags: page.tags,
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function searchDocs(query: string, index: SearchResult[]): SearchResult[] {
|
|
115
|
-
if (!query.trim()) return []
|
|
116
|
-
|
|
117
|
-
const terms = query.toLowerCase().split(/\s+/).filter(Boolean)
|
|
118
|
-
|
|
119
|
-
const scored = index
|
|
120
|
-
.map(entry => {
|
|
121
|
-
let score = 0
|
|
122
|
-
const titleLower = entry.pageTitle.toLowerCase()
|
|
123
|
-
const highlightLower = entry.highlight.toLowerCase()
|
|
124
|
-
const tagsLower = entry.tags.map(t => t.toLowerCase())
|
|
125
|
-
|
|
126
|
-
for (const term of terms) {
|
|
127
|
-
if (titleLower.includes(term)) score += 10
|
|
128
|
-
if (tagsLower.some(t => t.includes(term))) score += 5
|
|
129
|
-
if (highlightLower.includes(term)) score += 2
|
|
130
|
-
if (entry.sectionTitle.toLowerCase().includes(term)) score += 3
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return { ...entry, score }
|
|
134
|
-
})
|
|
135
|
-
.filter(r => r.score > 0)
|
|
136
|
-
.sort((a, b) => b.score - a.score)
|
|
137
|
-
.slice(0, 20)
|
|
138
|
-
|
|
139
|
-
return scored
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ─── Utility: slugify for doc pages ──────────────────────────
|
|
143
|
-
export function slugify(title: string): string {
|
|
144
|
-
return title
|
|
145
|
-
.toLowerCase()
|
|
146
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
147
|
-
.replace(/(^-|-$)/g, '')
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ─── Utility: highlight search term in text ──────────────────
|
|
151
|
-
export function highlightMatch(text: string, query: string, maxLen = 160): string {
|
|
152
|
-
if (!query.trim()) return text.slice(0, maxLen)
|
|
153
|
-
|
|
154
|
-
const lowerText = text.toLowerCase()
|
|
155
|
-
const lowerQuery = query.toLowerCase()
|
|
156
|
-
const idx = lowerText.indexOf(lowerQuery)
|
|
157
|
-
|
|
158
|
-
if (idx === -1) return text.slice(0, maxLen)
|
|
159
|
-
|
|
160
|
-
const start = Math.max(0, idx - 40)
|
|
161
|
-
const end = Math.min(text.length, idx + query.length + (maxLen - 80))
|
|
162
|
-
let snippet = text.slice(start, end)
|
|
163
|
-
|
|
164
|
-
if (start > 0) snippet = '…' + snippet
|
|
165
|
-
if (end < text.length) snippet = snippet + '…'
|
|
166
|
-
|
|
167
|
-
return snippet
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ─── Default config ──────────────────────────────────────────
|
|
171
|
-
export const defaultDocsConfig: DocsConfig = {
|
|
172
|
-
siteName: 'Docs',
|
|
173
|
-
cmdKEnabled: true,
|
|
174
|
-
showReadingTime: true,
|
|
175
|
-
showLastEdited: true,
|
|
176
|
-
versionsEnabled: false,
|
|
177
|
-
defaultAccess: 'team',
|
|
178
|
-
printModeEnabled: true,
|
|
179
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import type { DocsProvider, DocsPage, DocsSidebar, DocsSection, DocsSearchResult, TocEntry } from '../types'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Astro docs provider — reads MDX files from filesystem at build time.
|
|
5
|
-
* Designed for Astro Starlight or custom Astro docs sites.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* const provider = new AstroDocsProvider()
|
|
9
|
-
* await provider.loadFromGlob(import.meta.glob('./content/docs/** /*.mdx', { as: 'raw' }))
|
|
10
|
-
*/
|
|
11
|
-
export class AstroDocsProvider implements DocsProvider {
|
|
12
|
-
readonly name = 'astro' as const
|
|
13
|
-
private pages: DocsPage[] = []
|
|
14
|
-
|
|
15
|
-
async loadFromGlob(globResult: Record<string, () => Promise<string> | string>): Promise<void> {
|
|
16
|
-
const entries = Object.entries(globResult)
|
|
17
|
-
const parsed = await Promise.all(entries.map(async ([path, loader]) => {
|
|
18
|
-
const raw = typeof loader === 'function' ? await loader() : loader
|
|
19
|
-
return this.parseDoc(path, raw)
|
|
20
|
-
}))
|
|
21
|
-
this.pages = this.linkPages(parsed)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async loadFromFiles(files: { path: string; content: string }[]): Promise<void> {
|
|
25
|
-
const parsed = files.map(f => this.parseDoc(f.path, f.content))
|
|
26
|
-
this.pages = this.linkPages(parsed)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private parseDoc(path: string, raw: string): DocsPage {
|
|
30
|
-
let frontmatter: Record<string, any> = {}
|
|
31
|
-
let content = raw
|
|
32
|
-
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
|
|
33
|
-
if (fmMatch) {
|
|
34
|
-
// Simple YAML parser for common fields
|
|
35
|
-
frontmatter = Object.fromEntries(fmMatch[1].split('\n').map(line => { const [k, ...v] = line.split(':'); return [k.trim(), v.join(':').trim()] }).filter(([k]) => k))
|
|
36
|
-
content = fmMatch[2]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const pathParts = path.replace(/.*content\/docs\//, '').replace(/\.(mdx?|md)$/, '').split('/')
|
|
40
|
-
const slug = pathParts.join('/')
|
|
41
|
-
const section = pathParts.length > 1 ? pathParts[0].replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) : 'General'
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
slug, title: frontmatter.title ?? pathParts.pop() ?? slug,
|
|
45
|
-
description: frontmatter.description, content, section,
|
|
46
|
-
order: frontmatter.order ? parseInt(frontmatter.order) : undefined,
|
|
47
|
-
icon: frontmatter.icon, draft: frontmatter.draft === 'true',
|
|
48
|
-
toc: this.extractToc(content),
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private extractToc(content: string): TocEntry[] {
|
|
53
|
-
const entries: TocEntry[] = []
|
|
54
|
-
const regex = /^(#{2,4})\s+(.+)$/gm
|
|
55
|
-
let match
|
|
56
|
-
while ((match = regex.exec(content)) !== null) {
|
|
57
|
-
const title = match[2].trim()
|
|
58
|
-
entries.push({ id: title.toLowerCase().replace(/[^a-z0-9]+/g, '-'), title, depth: match[1].length })
|
|
59
|
-
}
|
|
60
|
-
return entries
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private linkPages(pages: DocsPage[]): DocsPage[] {
|
|
64
|
-
const sorted = [...pages].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
65
|
-
return sorted.map((p, i) => ({
|
|
66
|
-
...p,
|
|
67
|
-
prev: i > 0 ? { slug: sorted[i - 1].slug, title: sorted[i - 1].title } : undefined,
|
|
68
|
-
next: i < sorted.length - 1 ? { slug: sorted[i + 1].slug, title: sorted[i + 1].title } : undefined,
|
|
69
|
-
}))
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async getSidebar(): Promise<DocsSidebar> {
|
|
73
|
-
const sectionMap = new Map<string, DocsPage[]>()
|
|
74
|
-
for (const p of this.pages.filter(p => !p.draft)) {
|
|
75
|
-
if (!sectionMap.has(p.section)) sectionMap.set(p.section, [])
|
|
76
|
-
sectionMap.get(p.section)!.push(p)
|
|
77
|
-
}
|
|
78
|
-
return { sections: [...sectionMap.entries()].map(([title, pages]) => ({ title, slug: title.toLowerCase().replace(/\s+/g, '-'), pages })) }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async getPage(slug: string): Promise<DocsPage | null> { return this.pages.find(p => p.slug === slug) ?? null }
|
|
82
|
-
async getAllPages(): Promise<DocsPage[]> { return this.pages.filter(p => !p.draft) }
|
|
83
|
-
|
|
84
|
-
async search(query: string): Promise<DocsSearchResult[]> {
|
|
85
|
-
const q = query.toLowerCase()
|
|
86
|
-
return this.pages.filter(p => !p.draft && (p.title.toLowerCase().includes(q) || p.content.toLowerCase().includes(q)))
|
|
87
|
-
.map(page => ({ page, matches: [{ field: 'content', snippet: page.title }], score: 1 }))
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async getNavigation(slug: string): Promise<{ prev: DocsPage | null; next: DocsPage | null }> {
|
|
91
|
-
const page = this.pages.find(p => p.slug === slug)
|
|
92
|
-
return { prev: page?.prev ? this.pages.find(p => p.slug === page.prev!.slug) ?? null : null, next: page?.next ? this.pages.find(p => p.slug === page.next!.slug) ?? null : null }
|
|
93
|
-
}
|
|
94
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import type { DocsProvider, DocsPage, DocsSidebar, DocsSection, DocsSearchResult, TocEntry } from '../types'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Fumadocs docs provider — wraps Fumadocs content source API.
|
|
5
|
-
* Use when docs are managed by Fumadocs in a Next.js app.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* import { FumadocsDocsProvider } from '@geenius-docs/shared/providers/fumadocs'
|
|
9
|
-
* import { docs } from '@/lib/source'
|
|
10
|
-
* const provider = new FumadocsDocsProvider()
|
|
11
|
-
* provider.loadFromSource(docs.getPages(), docs.pageTree)
|
|
12
|
-
*/
|
|
13
|
-
export class FumadocsDocsProvider implements DocsProvider {
|
|
14
|
-
readonly name = 'fumadocs' as const
|
|
15
|
-
private pages: DocsPage[] = []
|
|
16
|
-
private tree: FumadocsTreeNode[] = []
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Load from Fumadocs source pages and page tree.
|
|
20
|
-
*/
|
|
21
|
-
loadFromSource(pages: FumadocsSourcePage[], tree?: FumadocsTreeNode[]): void {
|
|
22
|
-
this.tree = tree ?? []
|
|
23
|
-
this.pages = this.linkPages(pages.map(p => this.normalizePage(p)))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private normalizePage(page: FumadocsSourcePage): DocsPage {
|
|
27
|
-
const slugParts = page.slugs ?? page.slug?.split('/') ?? []
|
|
28
|
-
return {
|
|
29
|
-
slug: slugParts.join('/'),
|
|
30
|
-
title: page.data?.title ?? slugParts[slugParts.length - 1] ?? '',
|
|
31
|
-
description: page.data?.description,
|
|
32
|
-
content: page.data?.body ?? '',
|
|
33
|
-
section: slugParts.length > 1
|
|
34
|
-
? slugParts[0].replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
|
35
|
-
: 'General',
|
|
36
|
-
order: page.data?.order ?? undefined,
|
|
37
|
-
icon: page.data?.icon,
|
|
38
|
-
lastUpdated: page.data?.lastModified ?? page.data?.lastUpdated,
|
|
39
|
-
toc: (page.data?.toc as TocEntry[]) ?? this.extractToc(page.data?.body ?? ''),
|
|
40
|
-
draft: page.data?.draft ?? false,
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private extractToc(content: string): TocEntry[] {
|
|
45
|
-
const entries: TocEntry[] = []
|
|
46
|
-
const regex = /^(#{2,4})\s+(.+)$/gm
|
|
47
|
-
let match
|
|
48
|
-
while ((match = regex.exec(content)) !== null) {
|
|
49
|
-
const title = match[2].trim()
|
|
50
|
-
entries.push({ id: title.toLowerCase().replace(/[^a-z0-9]+/g, '-'), title, depth: match[1].length })
|
|
51
|
-
}
|
|
52
|
-
return entries
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private linkPages(pages: DocsPage[]): DocsPage[] {
|
|
56
|
-
const sorted = [...pages].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
57
|
-
return sorted.map((p, i) => ({
|
|
58
|
-
...p,
|
|
59
|
-
prev: i > 0 ? { slug: sorted[i - 1].slug, title: sorted[i - 1].title } : undefined,
|
|
60
|
-
next: i < sorted.length - 1 ? { slug: sorted[i + 1].slug, title: sorted[i + 1].title } : undefined,
|
|
61
|
-
}))
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async getSidebar(): Promise<DocsSidebar> {
|
|
65
|
-
// If we have a Fumadocs tree, use it for sidebar structure
|
|
66
|
-
if (this.tree.length > 0) {
|
|
67
|
-
return { sections: this.treeToSections(this.tree) }
|
|
68
|
-
}
|
|
69
|
-
// Fallback: group by section
|
|
70
|
-
const sectionMap = new Map<string, DocsPage[]>()
|
|
71
|
-
for (const p of this.pages.filter(p => !p.draft)) {
|
|
72
|
-
if (!sectionMap.has(p.section)) sectionMap.set(p.section, [])
|
|
73
|
-
sectionMap.get(p.section)!.push(p)
|
|
74
|
-
}
|
|
75
|
-
return { sections: [...sectionMap.entries()].map(([title, pages]) => ({ title, slug: title.toLowerCase().replace(/\s+/g, '-'), pages })) }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private treeToSections(nodes: FumadocsTreeNode[]): DocsSection[] {
|
|
79
|
-
return nodes.filter(n => n.type === 'folder').map(folder => ({
|
|
80
|
-
title: folder.name ?? '',
|
|
81
|
-
slug: folder.name?.toLowerCase().replace(/\s+/g, '-') ?? '',
|
|
82
|
-
icon: folder.icon,
|
|
83
|
-
pages: (folder.children ?? []).filter(c => c.type === 'page').map(c => this.pages.find(p => p.slug === c.slug) ?? { slug: c.slug ?? '', title: c.name ?? '', content: '', section: folder.name ?? '' }),
|
|
84
|
-
}))
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async getPage(slug: string): Promise<DocsPage | null> { return this.pages.find(p => p.slug === slug) ?? null }
|
|
88
|
-
async getAllPages(): Promise<DocsPage[]> { return this.pages.filter(p => !p.draft) }
|
|
89
|
-
|
|
90
|
-
async search(query: string): Promise<DocsSearchResult[]> {
|
|
91
|
-
const q = query.toLowerCase()
|
|
92
|
-
return this.pages.filter(p => !p.draft && (p.title.toLowerCase().includes(q) || p.content.toLowerCase().includes(q) || p.description?.toLowerCase().includes(q)))
|
|
93
|
-
.map(page => ({ page, matches: [{ field: 'title', snippet: page.title }], score: 1 }))
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async getNavigation(slug: string): Promise<{ prev: DocsPage | null; next: DocsPage | null }> {
|
|
97
|
-
const page = this.pages.find(p => p.slug === slug)
|
|
98
|
-
return { prev: page?.prev ? this.pages.find(p => p.slug === page.prev!.slug) ?? null : null, next: page?.next ? this.pages.find(p => p.slug === page.next!.slug) ?? null : null }
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ─── Fumadocs Source Types ───────────────────────────────────
|
|
103
|
-
export interface FumadocsSourcePage {
|
|
104
|
-
slug?: string
|
|
105
|
-
slugs?: string[]
|
|
106
|
-
url?: string
|
|
107
|
-
data: { title?: string; description?: string; body?: string; icon?: string; order?: number; toc?: unknown; draft?: boolean; lastModified?: string; lastUpdated?: string; [key: string]: unknown }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export interface FumadocsTreeNode {
|
|
111
|
-
type: 'page' | 'folder' | 'separator'
|
|
112
|
-
name?: string
|
|
113
|
-
slug?: string
|
|
114
|
-
icon?: string
|
|
115
|
-
children?: FumadocsTreeNode[]
|
|
116
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import type { DocsProvider, DocsPage, DocsSidebar, DocsSection, DocsSearchResult, TocEntry } from '../types'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Internal docs provider — renders docs from an in-memory page array.
|
|
5
|
-
* Use when loading docs from CMS, API, or static JSON.
|
|
6
|
-
*/
|
|
7
|
-
export class InternalDocsProvider implements DocsProvider {
|
|
8
|
-
readonly name = 'internal' as const
|
|
9
|
-
private pages: DocsPage[] = []
|
|
10
|
-
|
|
11
|
-
constructor(pages?: DocsPage[]) { if (pages) this.pages = pages }
|
|
12
|
-
|
|
13
|
-
setPages(pages: DocsPage[]): void { this.pages = this.linkPages(pages) }
|
|
14
|
-
|
|
15
|
-
private linkPages(pages: DocsPage[]): DocsPage[] {
|
|
16
|
-
const sorted = [...pages].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
17
|
-
return sorted.map((page, i) => ({
|
|
18
|
-
...page,
|
|
19
|
-
prev: i > 0 ? { slug: sorted[i - 1].slug, title: sorted[i - 1].title } : undefined,
|
|
20
|
-
next: i < sorted.length - 1 ? { slug: sorted[i + 1].slug, title: sorted[i + 1].title } : undefined,
|
|
21
|
-
toc: page.toc ?? this.extractToc(page.content),
|
|
22
|
-
}))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
private extractToc(content: string): TocEntry[] {
|
|
26
|
-
const regex = /^(#{2,4})\s+(.+)$/gm
|
|
27
|
-
const entries: TocEntry[] = []
|
|
28
|
-
let match
|
|
29
|
-
while ((match = regex.exec(content)) !== null) {
|
|
30
|
-
const title = match[2].trim()
|
|
31
|
-
entries.push({ id: title.toLowerCase().replace(/[^a-z0-9]+/g, '-'), title, depth: match[1].length })
|
|
32
|
-
}
|
|
33
|
-
return entries
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async getSidebar(): Promise<DocsSidebar> {
|
|
37
|
-
const sectionMap = new Map<string, DocsPage[]>()
|
|
38
|
-
for (const page of this.pages.filter(p => !p.draft)) {
|
|
39
|
-
const section = page.section || 'General'
|
|
40
|
-
if (!sectionMap.has(section)) sectionMap.set(section, [])
|
|
41
|
-
sectionMap.get(section)!.push(page)
|
|
42
|
-
}
|
|
43
|
-
const sections: DocsSection[] = [...sectionMap.entries()].map(([title, pages]) => ({
|
|
44
|
-
title, slug: title.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
|
45
|
-
pages: pages.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
|
46
|
-
}))
|
|
47
|
-
return { sections }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async getPage(slug: string): Promise<DocsPage | null> {
|
|
51
|
-
return this.pages.find(p => p.slug === slug) ?? null
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async getAllPages(): Promise<DocsPage[]> {
|
|
55
|
-
return this.pages.filter(p => !p.draft)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async search(query: string): Promise<DocsSearchResult[]> {
|
|
59
|
-
const q = query.toLowerCase()
|
|
60
|
-
return this.pages.filter(p => !p.draft).map(page => {
|
|
61
|
-
const matches: DocsSearchResult['matches'] = []
|
|
62
|
-
if (page.title.toLowerCase().includes(q)) matches.push({ field: 'title', snippet: page.title })
|
|
63
|
-
if (page.description?.toLowerCase().includes(q)) matches.push({ field: 'description', snippet: page.description })
|
|
64
|
-
if (page.content.toLowerCase().includes(q)) {
|
|
65
|
-
const idx = page.content.toLowerCase().indexOf(q)
|
|
66
|
-
matches.push({ field: 'content', snippet: page.content.slice(Math.max(0, idx - 40), idx + q.length + 40) })
|
|
67
|
-
}
|
|
68
|
-
return matches.length > 0 ? { page, matches, score: matches.length } : null
|
|
69
|
-
}).filter(Boolean) as DocsSearchResult[]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async getNavigation(slug: string): Promise<{ prev: DocsPage | null; next: DocsPage | null }> {
|
|
73
|
-
const page = this.pages.find(p => p.slug === slug)
|
|
74
|
-
if (!page) return { prev: null, next: null }
|
|
75
|
-
return {
|
|
76
|
-
prev: page.prev ? (this.pages.find(p => p.slug === page.prev!.slug) ?? null) : null,
|
|
77
|
-
next: page.next ? (this.pages.find(p => p.slug === page.next!.slug) ?? null) : null,
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|