boltdocs 2.5.4 → 2.5.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 (96) hide show
  1. package/bin/boltdocs.js +1 -1
  2. package/dist/cache-Cr8W2zgZ.cjs +6 -0
  3. package/dist/cache-DFdakSmR.mjs +6 -0
  4. package/dist/client/index.d.mts +1276 -861
  5. package/dist/client/index.d.ts +1276 -861
  6. package/dist/client/index.js +6 -1
  7. package/dist/client/index.mjs +6 -1
  8. package/dist/client/ssr.cjs +6 -0
  9. package/dist/client/ssr.d.cts +80 -0
  10. package/dist/client/ssr.d.mts +63 -61
  11. package/dist/client/ssr.mjs +6 -1
  12. package/dist/client/theme/neutral.css +388 -0
  13. package/dist/node/cli-entry.cjs +8 -0
  14. package/dist/node/cli-entry.d.cts +2 -0
  15. package/dist/node/cli-entry.d.mts +2 -1
  16. package/dist/node/cli-entry.mjs +7 -5
  17. package/dist/node/index.cjs +6 -0
  18. package/dist/node/index.d.cts +574 -0
  19. package/dist/node/index.d.mts +385 -378
  20. package/dist/node/index.mjs +6 -1
  21. package/dist/node-CWXme96p.mjs +73 -0
  22. package/dist/node-VYfhzGrh.cjs +73 -0
  23. package/dist/package-BY8Jd2j4.cjs +6 -0
  24. package/dist/package-OFZf0s2j.mjs +6 -0
  25. package/dist/search-dialog-BeNyI_KQ.mjs +6 -0
  26. package/dist/search-dialog-dYsCAk5S.js +6 -0
  27. package/dist/use-search-D25n0PrV.mjs +6 -0
  28. package/dist/use-search-WuzdH1cJ.js +6 -0
  29. package/package.json +16 -12
  30. package/src/client/app/index.tsx +15 -12
  31. package/src/client/components/default-layout.tsx +21 -19
  32. package/src/client/hooks/use-i18n.ts +1 -1
  33. package/src/client/hooks/use-routes.ts +1 -1
  34. package/src/client/hooks/use-version.ts +1 -1
  35. package/src/client/store/boltdocs-context.tsx +119 -0
  36. package/CHANGELOG.md +0 -92
  37. package/dist/cache-3FOEPC2P.mjs +0 -1
  38. package/dist/chunk-IMEKU5U3.mjs +0 -75
  39. package/dist/chunk-J2PTDWZM.mjs +0 -1
  40. package/dist/chunk-TP5KMRD3.mjs +0 -1
  41. package/dist/chunk-Y4RE5KI7.mjs +0 -1
  42. package/dist/client/ssr.d.ts +0 -78
  43. package/dist/client/ssr.js +0 -1
  44. package/dist/node/cli-entry.d.ts +0 -1
  45. package/dist/node/cli-entry.js +0 -80
  46. package/dist/node/index.d.ts +0 -567
  47. package/dist/node/index.js +0 -75
  48. package/dist/package-KCTE4HFV.mjs +0 -1
  49. package/dist/search-dialog-O6VLVSOA.mjs +0 -1
  50. package/src/client/store/use-boltdocs-store.ts +0 -44
  51. package/src/node/cache.ts +0 -408
  52. package/src/node/cli/build.ts +0 -53
  53. package/src/node/cli/dev.ts +0 -22
  54. package/src/node/cli/doctor.ts +0 -243
  55. package/src/node/cli/index.ts +0 -9
  56. package/src/node/cli/ui.ts +0 -54
  57. package/src/node/cli-entry.ts +0 -24
  58. package/src/node/config.ts +0 -382
  59. package/src/node/errors.ts +0 -44
  60. package/src/node/index.ts +0 -84
  61. package/src/node/mdx/cache.ts +0 -12
  62. package/src/node/mdx/highlighter.ts +0 -47
  63. package/src/node/mdx/index.ts +0 -122
  64. package/src/node/mdx/rehype-shiki.ts +0 -62
  65. package/src/node/mdx/remark-code-meta.ts +0 -35
  66. package/src/node/mdx/remark-shiki.ts +0 -61
  67. package/src/node/plugin/entry.ts +0 -87
  68. package/src/node/plugin/html.ts +0 -99
  69. package/src/node/plugin/index.ts +0 -464
  70. package/src/node/plugin/types.ts +0 -9
  71. package/src/node/plugins/index.ts +0 -17
  72. package/src/node/plugins/plugin-errors.ts +0 -62
  73. package/src/node/plugins/plugin-lifecycle.ts +0 -117
  74. package/src/node/plugins/plugin-sandbox.ts +0 -59
  75. package/src/node/plugins/plugin-store.ts +0 -54
  76. package/src/node/plugins/plugin-types.ts +0 -107
  77. package/src/node/plugins/plugin-validator.ts +0 -105
  78. package/src/node/routes/cache.ts +0 -28
  79. package/src/node/routes/index.ts +0 -293
  80. package/src/node/routes/parser.ts +0 -262
  81. package/src/node/routes/sorter.ts +0 -42
  82. package/src/node/routes/types.ts +0 -61
  83. package/src/node/schema/config.ts +0 -195
  84. package/src/node/schema/frontmatter.ts +0 -17
  85. package/src/node/search/index.ts +0 -55
  86. package/src/node/security/constants/index.ts +0 -10
  87. package/src/node/security/csp.ts +0 -31
  88. package/src/node/security/headers.ts +0 -27
  89. package/src/node/ssg/index.ts +0 -205
  90. package/src/node/ssg/meta.ts +0 -33
  91. package/src/node/ssg/options.ts +0 -15
  92. package/src/node/ssg/robots.ts +0 -53
  93. package/src/node/ssg/sitemap.ts +0 -55
  94. package/src/node/utils.ts +0 -349
  95. package/tsconfig.json +0 -26
  96. package/tsup.config.ts +0 -56
@@ -1,262 +0,0 @@
1
- import path from 'path'
2
- import GithubSlugger from 'github-slugger'
3
- import type { BoltdocsConfig } from '../config'
4
- import type { ParsedDocFile } from './types'
5
- import {
6
- normalizePath,
7
- parseFrontmatter,
8
- fileToRoutePath,
9
- capitalize,
10
- stripNumberPrefix,
11
- extractNumberPrefix,
12
- sanitizeHtml,
13
- stripHtmlTags,
14
- logSecurityEvent,
15
- } from '../utils'
16
- import { MAX_PATH_LENGTH, ALLOWED_PATH_CHARS } from '../security/constants'
17
- import { EncodingSecurityError, PathTraversalError } from '../errors'
18
-
19
- /**
20
- * Parses a single Markdown/MDX file and extracts its metadata for routing.
21
- * Checks frontmatter for explicit titles, descriptions, and sidebar positions.
22
- *
23
- * Also performs security validation to prevent path traversal and basic
24
- * XSS sanitization for metadata and headings.
25
- *
26
- * @param file - The absolute path to the file
27
- * @param docsDir - The root documentation directory (e.g., 'docs')
28
- * @param basePath - The base URL path for the routes (default: '/docs')
29
- * @param config - The Boltdocs configuration for versions and i18n
30
- * @returns A parsed structure ready for route assembly and caching
31
- */
32
- export function parseDocFile(
33
- file: string,
34
- docsDir: string,
35
- basePath: string,
36
- config?: BoltdocsConfig,
37
- ): ParsedDocFile {
38
- // Security: Prevent path traversal and malicious encoding
39
- let decodedFile: string
40
- try {
41
- decodedFile = decodeURIComponent(file)
42
- } catch {
43
- const fileName = path.basename(file)
44
- logSecurityEvent('ENCODING_ERROR', 'Invalid character encoding', { file: fileName })
45
- throw new EncodingSecurityError(
46
- `Security breach: Invalid characters or encoding in path: ${fileName}`,
47
- )
48
- }
49
-
50
- // Validation: Path length
51
- if (decodedFile.length > MAX_PATH_LENGTH) {
52
- const fileName = path.basename(decodedFile)
53
- logSecurityEvent('PATH_TOO_LONG', 'Path length exceeds limit', {
54
- length: decodedFile.length,
55
- file: fileName,
56
- })
57
- throw new PathTraversalError(
58
- `Security breach: Path length exceeds limit of ${MAX_PATH_LENGTH} characters: ${fileName}`,
59
- )
60
- }
61
-
62
- const absoluteFile = path.resolve(decodedFile)
63
- const absoluteDocsDir = path.resolve(docsDir)
64
- const relativePath = normalizePath(
65
- path.relative(absoluteDocsDir, absoluteFile),
66
- )
67
-
68
- if (
69
- relativePath.startsWith('../') ||
70
- relativePath === '..' ||
71
- absoluteFile.includes('\0') ||
72
- !ALLOWED_PATH_CHARS.test(relativePath)
73
- ) {
74
- const fileName = path.basename(file)
75
- logSecurityEvent('PATH_TRAVERSAL_ATTEMPT', 'Path traversal or invalid characters detected', {
76
- path: relativePath,
77
- })
78
- throw new PathTraversalError(
79
- `Security breach: File is outside of docs directory, contains null bytes, or invalid characters: ${fileName}`,
80
- )
81
- }
82
-
83
- const { data, content } = parseFrontmatter(file)
84
- let parts = relativePath.split('/')
85
-
86
- let locale: string | undefined
87
- let version: string | undefined
88
-
89
- // Level 1: Check for version
90
- if (config?.versions && parts.length > 0) {
91
- const potentialVersion = parts[0]
92
- const prefix = config.versions.prefix || ''
93
-
94
- const versionMatch = config.versions.versions.find((v) => {
95
- const fullPath = prefix + v.path
96
- return potentialVersion === fullPath || potentialVersion === v.path
97
- })
98
-
99
- if (versionMatch) {
100
- version = versionMatch.path
101
- parts = parts.slice(1)
102
- }
103
- }
104
-
105
- // Level 2: Check for locale
106
- if (config?.i18n && parts.length > 0) {
107
- const potentialLocale = parts[0]
108
- if (config.i18n.locales[potentialLocale]) {
109
- locale = potentialLocale
110
- parts = parts.slice(1)
111
- }
112
- }
113
-
114
- // Level 3: Check for Tab hierarchy (name)
115
- let inferredTab: string | undefined
116
- if (parts.length > 0) {
117
- const tabMatch = parts[0].match(/^\((.+)\)$/)
118
- if (tabMatch) {
119
- inferredTab = tabMatch[1].toLowerCase()
120
- parts = parts.slice(1)
121
- }
122
- }
123
-
124
- const cleanRelativePath = parts.join('/')
125
-
126
- let cleanRoutePath: string
127
- if (data.permalink) {
128
- // If a permalink is specified, ensure it starts with a slash
129
- cleanRoutePath = data.permalink.startsWith('/')
130
- ? data.permalink
131
- : `/${data.permalink}`
132
- } else {
133
- cleanRoutePath = fileToRoutePath(cleanRelativePath || 'index.md')
134
- }
135
-
136
- let finalPath = basePath
137
- if (version) {
138
- finalPath += '/' + version
139
- }
140
- if (locale) {
141
- finalPath += '/' + locale
142
- }
143
- if (inferredTab) {
144
- finalPath += '/' + inferredTab
145
- }
146
- finalPath += cleanRoutePath === '/' ? '' : cleanRoutePath
147
-
148
- if (!finalPath || finalPath === '') finalPath = '/'
149
-
150
- const rawFileName = parts[parts.length - 1]
151
- const cleanFileName = stripNumberPrefix(rawFileName)
152
- const inferredTitle = stripNumberPrefix(
153
- path.basename(file, path.extname(file)),
154
- )
155
- const sidebarPosition =
156
- data.sidebarPosition ?? extractNumberPrefix(rawFileName)
157
-
158
- const rawDirName = parts.length >= 2 ? parts[0] : undefined
159
- const cleanDirName = rawDirName ? stripNumberPrefix(rawDirName) : undefined
160
-
161
- const isGroupIndex = parts.length >= 2 && /^index\.mdx?$/.test(cleanFileName)
162
-
163
- const slugger = new GithubSlugger()
164
- const headings: { level: number; text: string; id: string }[] = []
165
- const headingsRegex = /^(#{2,4})\s+(.+)$/gm
166
-
167
- for (const match of content.matchAll(headingsRegex)) {
168
- const level = match[1].length
169
- const rawText = match[2]
170
- .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Strip markdown links
171
- .replace(/[_*`]/g, '') // Strip markdown formatting
172
- .trim()
173
-
174
- const sanitizedText = sanitizeHtml(rawText).trim()
175
- const id = slugger.slug(sanitizedText)
176
-
177
- headings.push({ level, text: sanitizedText, id })
178
- }
179
-
180
- const sanitizedTitle = data.title
181
- ? sanitizeHtml(String(data.title))
182
- : inferredTitle
183
- let sanitizedDescription = data.description
184
- ? sanitizeHtml(String(data.description))
185
- : ''
186
-
187
- // If no description is provided, extract a summary from the content
188
- if (!sanitizedDescription && content) {
189
- const plainExcerpt = stripHtmlTags(
190
- content
191
- .replace(/^#+.*$/gm, '') // Remove headers
192
- .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Simplify links
193
- .replace(/[_*`]/g, '') // Remove formatting
194
- .replace(/\s+/g, ' '), // Normalize whitespace
195
- )
196
- .trim()
197
- .slice(0, 160)
198
-
199
- sanitizedDescription = plainExcerpt
200
- }
201
-
202
- const sanitizedBadge = data.badge
203
- ? sanitizeHtml(String(data.badge))
204
- : undefined
205
- const icon = data.icon ? String(data.icon) : undefined
206
-
207
- // Extract full content as plain text for search indexing
208
- const plainText = parseContentToPlainText(content)
209
-
210
- return {
211
- route: {
212
- path: finalPath,
213
- componentPath: file,
214
- filePath: relativePath,
215
- title: sanitizedTitle,
216
- description: sanitizedDescription,
217
- sidebarPosition,
218
- headings,
219
- locale,
220
- version,
221
- badge: sanitizedBadge,
222
- icon,
223
- tab: inferredTab,
224
- _content: plainText,
225
- _rawContent: content,
226
- },
227
- relativeDir: cleanDirName,
228
- isGroupIndex,
229
- inferredTab,
230
- groupMeta: isGroupIndex
231
- ? {
232
- title:
233
- data.groupTitle ||
234
- data.title ||
235
- (cleanDirName ? capitalize(cleanDirName) : ''),
236
- position:
237
- data.groupPosition ??
238
- data.sidebarPosition ??
239
- (rawDirName ? extractNumberPrefix(rawDirName) : undefined),
240
- icon,
241
- }
242
- : undefined,
243
- inferredGroupPosition: rawDirName
244
- ? extractNumberPrefix(rawDirName)
245
- : undefined,
246
- }
247
- }
248
-
249
- /**
250
- * Converts markdown content to plain text for search indexing.
251
- * Strips headers, links, tags, and formatting.
252
- */
253
- function parseContentToPlainText(content: string): string {
254
- const plainText = content
255
- .replace(/^#+.*$/gm, '') // Remove headers
256
- .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Simplify links
257
- .replace(/\{[^\}]+\}/g, '') // Remove JS expressions/curly braces
258
- .replace(/[_*`]/g, '') // Remove formatting
259
- .replace(/\s+/g, ' ') // Normalize whitespace
260
-
261
- return stripHtmlTags(plainText).trim()
262
- }
@@ -1,42 +0,0 @@
1
- import type { RouteMeta } from './types'
2
-
3
- /**
4
- * Sorts an array of generated routes.
5
- * Ungrouped items come first. Items within the same group are sorted by position, then alphabetically.
6
- * Groups are sorted relative to each other by their group position, then alphabetically.
7
- *
8
- * @param routes - The unsorted routes
9
- * @returns A new array of sorted routes suitable for sidebar generation
10
- */
11
- export function sortRoutes(routes: RouteMeta[]): RouteMeta[] {
12
- return routes.sort((a, b) => {
13
- // Ungrouped first
14
- if (!a.group && !b.group) return compareByPosition(a, b)
15
- if (!a.group) return -1
16
- if (!b.group) return 1
17
-
18
- // Different groups: sort by group position
19
- if (a.group !== b.group) {
20
- return compareByGroupPosition(a, b)
21
- }
22
-
23
- // Same group: sort by item position
24
- return compareByPosition(a, b)
25
- })
26
- }
27
-
28
- function compareByPosition(a: RouteMeta, b: RouteMeta): number {
29
- if (a.sidebarPosition !== undefined && b.sidebarPosition !== undefined)
30
- return a.sidebarPosition - b.sidebarPosition
31
- if (a.sidebarPosition !== undefined) return -1
32
- if (b.sidebarPosition !== undefined) return 1
33
- return a.title.localeCompare(b.title)
34
- }
35
-
36
- function compareByGroupPosition(a: RouteMeta, b: RouteMeta): number {
37
- if (a.groupPosition !== undefined && b.groupPosition !== undefined)
38
- return a.groupPosition - b.groupPosition
39
- if (a.groupPosition !== undefined) return -1
40
- if (b.groupPosition !== undefined) return 1
41
- return (a.groupTitle || a.group!).localeCompare(b.groupTitle || b.group!)
42
- }
@@ -1,61 +0,0 @@
1
- /**
2
- * Metadata representing a single documentation route.
3
- * This information is used to build the client-side router and the sidebar navigation.
4
- */
5
- export interface RouteMeta {
6
- /** The final URL path for the route (e.g., '/docs/guide/start') */
7
- path: string
8
- /** The absolute filesystem path to the source markdown/mdx file */
9
- componentPath: string
10
- /** The title of the page, usually extracted from frontmatter or the filename */
11
- title: string
12
- /** The relative path from the docs directory, used for edit links */
13
- filePath: string
14
- /** Optional description of the page (for SEO/meta tags) */
15
- description?: string
16
- /** Optional explicit position for ordering in the sidebar */
17
- sidebarPosition?: number
18
- /** The group (directory) this route belongs to */
19
- group?: string
20
- /** The display title for the route's group */
21
- groupTitle?: string
22
- /** Optional explicit position for ordering the group itself */
23
- groupPosition?: number
24
- /** Optional icon for the route's group */
25
- groupIcon?: string
26
- /** Extracted markdown headings for search indexing */
27
- headings?: { level: number; text: string; id: string }[]
28
- /** The locale this route belongs to, if i18n is configured */
29
- locale?: string
30
- /** The version this route belongs to, if versioning is configured */
31
- version?: string
32
- /** Optional badge to display next to the sidebar item (e.g., 'New', 'Experimental') */
33
- badge?: string | { text: string; expires?: string }
34
- /** Optional icon to display (Lucide icon name or raw SVG) */
35
- icon?: string
36
- /** The tab this route belongs to, if tabs are configured */
37
- tab?: string
38
- /** The extracted plain-text content of the page for search indexing */
39
- _content?: string
40
- /** The raw markdown content of the page */
41
- _rawContent?: string
42
- }
43
-
44
- /**
45
- * Internal representation of a parsed documentation file before finalizing groups.
46
- * Stored in the file cache to avoid re-parsing unchanged files.
47
- */
48
- export interface ParsedDocFile {
49
- /** The core route metadata without group-level details (inferred later) */
50
- route: Omit<RouteMeta, 'group' | 'groupTitle' | 'groupPosition'>
51
- /** The base directory of the file (used to group files together) */
52
- relativeDir?: string
53
- /** Whether this file is the index file for its directory group */
54
- isGroupIndex: boolean
55
- /** If this is a group index, any specific frontmatter metadata dictating the group's title and position */
56
- groupMeta?: { title: string; position?: number; icon?: string }
57
- /** Extracted group position from the directory name if it has a numeric prefix */
58
- inferredGroupPosition?: number
59
- /** Extracted tab name from the directory name if it follows the (tab-name) syntax */
60
- inferredTab?: string
61
- }
@@ -1,195 +0,0 @@
1
- import { z } from 'zod'
2
-
3
- /**
4
- * Zod schema for a single social link.
5
- */
6
- export const SocialLinkSchema = z.object({
7
- icon: z.string().max(50),
8
- link: z.string().url(),
9
- })
10
-
11
- /**
12
- * Zod schema for footer configuration.
13
- */
14
- export const FooterConfigSchema = z.object({
15
- text: z.string().max(2000).optional(),
16
- })
17
-
18
- /**
19
- * Zod schema for plugin permissions.
20
- */
21
- export const PluginPermissionSchema = z.enum([
22
- 'fs:read',
23
- 'fs:write',
24
- 'vite:config',
25
- 'mdx:remark',
26
- 'mdx:rehype',
27
- 'components',
28
- 'hooks:build',
29
- 'hooks:dev',
30
- ])
31
-
32
- /**
33
- * Zod schema for a Boltdocs plugin.
34
- */
35
- export const BoltdocsPluginSchema = z.object({
36
- name: z.string(),
37
- enforce: z.enum(['pre', 'post']).optional(),
38
- version: z.string().optional(),
39
- boltdocsVersion: z.string().optional(),
40
- permissions: z.array(PluginPermissionSchema).optional(),
41
- remarkPlugins: z.array(z.any()).optional(),
42
- rehypePlugins: z.array(z.any()).optional(),
43
- vitePlugins: z.array(z.any()).optional(),
44
- components: z.record(z.string(), z.string()).optional(),
45
- hooks: z.record(z.string(), z.any()).optional(),
46
- })
47
-
48
- /**
49
- * Zod schema for theme configuration.
50
- */
51
- export const ThemeConfigSchema = z.object({
52
- title: z.union([z.string(), z.record(z.string(), z.string())]).optional(),
53
- description: z.union([z.string(), z.record(z.string(), z.string())]).optional(),
54
- logo: z
55
- .union([
56
- z.string(),
57
- z.object({
58
- dark: z.string(),
59
- light: z.string(),
60
- alt: z.string().optional(),
61
- width: z.number().optional(),
62
- height: z.number().optional(),
63
- }),
64
- ])
65
- .optional(),
66
- navbar: z
67
- .array(
68
- z.object({
69
- label: z.union([z.string(), z.record(z.string(), z.string())]),
70
- href: z.string(),
71
- }),
72
- )
73
- .optional(),
74
- sidebar: z
75
- .record(
76
- z.string(),
77
- z.array(
78
- z.object({
79
- text: z.string(),
80
- link: z.string(),
81
- }),
82
- ),
83
- )
84
- .optional(),
85
- socialLinks: z.array(SocialLinkSchema).optional(),
86
- footer: FooterConfigSchema.optional(),
87
- breadcrumbs: z.boolean().optional(),
88
- editLink: z.string().refine(val => !val || val.includes(':path'), {
89
- message: "editLink must contain ':path' placeholder if specified"
90
- }).optional(),
91
- communityHelp: z.string().url().optional(),
92
- version: z.string().max(50).optional(),
93
- githubRepo: z.string().max(100).optional(),
94
- favicon: z.string().optional(),
95
- ogImage: z.string().optional(),
96
- poweredBy: z.boolean().optional(),
97
- tabs: z
98
- .array(
99
- z.object({
100
- id: z.string(),
101
- text: z.union([z.string(), z.record(z.string(), z.string())]),
102
- icon: z.string().optional(),
103
- }),
104
- )
105
- .optional(),
106
- codeTheme: z
107
- .union([z.string(), z.object({ light: z.string(), dark: z.string() })])
108
- .optional(),
109
- copyMarkdown: z
110
- .union([
111
- z.boolean(),
112
- z.object({
113
- text: z.string().optional(),
114
- icon: z.string().optional(),
115
- }),
116
- ])
117
- .optional(),
118
- })
119
-
120
- /**
121
- * Zod schema for robots.txt configuration.
122
- */
123
- export const RobotsConfigSchema = z.union([
124
- z.string(),
125
- z.object({
126
- rules: z
127
- .array(
128
- z.object({
129
- userAgent: z.string(),
130
- allow: z.union([z.string(), z.array(z.string())]).optional(),
131
- disallow: z.union([z.string(), z.array(z.string())]).optional(),
132
- }),
133
- )
134
- .optional(),
135
- sitemaps: z.array(z.string().url()).optional(),
136
- }),
137
- ])
138
-
139
- /**
140
- * Zod schema for internationalization configuration.
141
- */
142
- export const I18nConfigSchema = z.object({
143
- defaultLocale: z.string(),
144
- locales: z.record(z.string(), z.string()),
145
- localeConfigs: z
146
- .record(
147
- z.string(),
148
- z.object({
149
- label: z.string().optional(),
150
- direction: z.enum(['ltr', 'rtl']).optional(),
151
- htmlLang: z.string().optional(),
152
- calendar: z.string().optional(),
153
- }),
154
- )
155
- .optional(),
156
- })
157
-
158
- /**
159
- * Zod schema for versioning configuration.
160
- */
161
- export const VersionsConfigSchema = z.object({
162
- defaultVersion: z.string(),
163
- prefix: z.string().optional(),
164
- versions: z.array(
165
- z.object({
166
- label: z.string(),
167
- path: z.string(),
168
- }),
169
- ),
170
- })
171
-
172
- /**
173
- * Zod schema for security configuration.
174
- */
175
- export const SecurityConfigSchema = z.object({
176
- headers: z.record(z.string(), z.string()).optional(),
177
- enableCSP: z.boolean().optional(),
178
- customHeaders: z.record(z.string(), z.string()).optional(),
179
- })
180
-
181
- /**
182
- * Root Zod schema for Boltdocs project configuration.
183
- */
184
- export const BoltdocsConfigSchema = z.object({
185
- siteUrl: z.string().url().optional(),
186
- docsDir: z.string().optional(),
187
- homePage: z.string().optional(),
188
- theme: ThemeConfigSchema.optional(),
189
- i18n: I18nConfigSchema.optional(),
190
- versions: VersionsConfigSchema.optional(),
191
- plugins: z.array(BoltdocsPluginSchema).optional(),
192
- robots: RobotsConfigSchema.optional(),
193
- security: SecurityConfigSchema.optional(),
194
- vite: z.record(z.string(), z.unknown()).optional(),
195
- })
@@ -1,17 +0,0 @@
1
- import { z } from 'zod'
2
-
3
- /**
4
- * Schema for strict frontmatter validation.
5
- */
6
- export const FrontmatterSchema = z.object({
7
- title: z.string().max(200).optional(),
8
- description: z.string().max(500).optional(),
9
- sidebarPosition: z.number().optional(),
10
- sidebarLabel: z.string().max(100).optional(),
11
- category: z.string().max(50).optional(),
12
- order: z.number().optional(),
13
- badge: z.string().max(50).optional(),
14
- icon: z.string().max(50).optional(),
15
- })
16
-
17
- export type FrontmatterData = z.infer<typeof FrontmatterSchema>
@@ -1,55 +0,0 @@
1
- import type { RouteMeta } from '../routes/types'
2
-
3
- export interface SearchDocument {
4
- id: string
5
- title: string
6
- content: string
7
- url: string
8
- display: string
9
- locale?: string
10
- version?: string
11
- }
12
-
13
- /**
14
- * Generates a flat list of searchable documents from the route metadata.
15
- * Each page is indexed as a primary document, and its sections (headings)
16
- * are indexed as secondary documents to provide granular search results.
17
- */
18
- export function generateSearchData(routes: RouteMeta[]): SearchDocument[] {
19
- const documents: SearchDocument[] = []
20
-
21
- for (const route of routes) {
22
- // 1. Index the main page
23
- documents.push({
24
- id: route.path,
25
- title: route.title,
26
- content: route._content || '',
27
- url: route.path,
28
- display: route.groupTitle
29
- ? `${route.groupTitle} > ${route.title}`
30
- : route.title,
31
- locale: route.locale,
32
- version: route.version,
33
- })
34
-
35
- // 2. Index headings as sub-documents for deep linking
36
- if (route.headings) {
37
- for (const heading of route.headings) {
38
- // We find the content belonging to this heading?
39
- // For now, indexing just the heading text and a bit of context is standard.
40
- // Deep full-text mapping to specific headings is more complex.
41
- documents.push({
42
- id: `${route.path}#${heading.id}`,
43
- title: heading.text,
44
- content: `${heading.text} in ${route.title}`,
45
- url: `${route.path}#${heading.id}`,
46
- display: `${route.title} > ${heading.text}`,
47
- locale: route.locale,
48
- version: route.version,
49
- })
50
- }
51
- }
52
- }
53
-
54
- return documents
55
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * Security limits for file system operations.
3
- */
4
- export const MAX_PATH_LENGTH = 260
5
- export const ALLOWED_PATH_CHARS = /^[a-zA-Z0-9\-_\/\.\(\)]+$/
6
-
7
- /**
8
- * Security limits for document metadata (frontmatter).
9
- */
10
- export const MAX_FRONTMATTER_SIZE = 10 * 1024 // 10KB
@@ -1,31 +0,0 @@
1
- import type { BoltdocsConfig } from '../config'
2
-
3
- /**
4
- * Generates a Content Security Policy (CSP) header string based on the configuration.
5
- * Automatically adapts to the environment (development vs production).
6
- *
7
- * @param config - The Boltdocs configuration object.
8
- * @returns The CSP header value.
9
- */
10
- export function getCSPHeader(_config: BoltdocsConfig): string {
11
- const isDev = process.env.NODE_ENV === 'development'
12
-
13
- const directives: Record<string, string[]> = {
14
- 'default-src': ["'self'"],
15
- 'script-src': ["'self'", "'unsafe-inline'"],
16
- 'style-src': ["'self'", "'unsafe-inline'"],
17
- 'img-src': ["'self'", "data:", "https:"],
18
- 'font-src': ["'self'"],
19
- 'connect-src': ["'self'"],
20
- }
21
-
22
- // Relax policies in development to support Vite's HMR (eval-based)
23
- if (isDev) {
24
- directives['script-src'] = ["'self'", "'unsafe-eval'", "'unsafe-inline'"]
25
- directives['style-src'] = ["'self'", "'unsafe-inline'"]
26
- }
27
-
28
- return Object.entries(directives)
29
- .map(([key, values]) => `${key} ${values.join(' ')}`)
30
- .join('; ')
31
- }