boltdocs 1.10.2 → 1.11.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.
- package/package.json +29 -7
- package/src/client/app/config-context.tsx +18 -0
- package/src/client/app/docs-layout.tsx +14 -0
- package/src/client/app/index.tsx +132 -260
- package/src/client/app/mdx-component.tsx +52 -0
- package/src/client/app/mdx-components-context.tsx +23 -0
- package/src/client/app/mdx-page.tsx +20 -0
- package/src/client/app/preload.tsx +38 -30
- package/src/client/app/router.tsx +30 -0
- package/src/client/app/scroll-handler.tsx +40 -0
- package/src/client/app/theme-context.tsx +75 -0
- package/src/client/components/default-layout.tsx +80 -0
- package/src/client/components/docs-layout.tsx +105 -0
- package/src/client/components/icons-dev.tsx +74 -0
- package/src/client/components/mdx/admonition.tsx +107 -0
- package/src/client/components/mdx/badge.tsx +41 -0
- package/src/client/components/mdx/button.tsx +35 -0
- package/src/client/components/mdx/card.tsx +124 -0
- package/src/client/components/mdx/code-block.tsx +119 -0
- package/src/client/components/mdx/component-preview.tsx +47 -0
- package/src/client/components/mdx/component-props.tsx +83 -0
- package/src/client/components/mdx/field.tsx +66 -0
- package/src/client/components/mdx/file-tree.tsx +287 -0
- package/src/client/components/mdx/hooks/use-code-block.ts +56 -0
- package/src/client/components/mdx/hooks/use-component-preview.ts +16 -0
- package/src/client/components/mdx/hooks/useTable.ts +74 -0
- package/src/client/components/mdx/hooks/useTabs.ts +68 -0
- package/src/client/components/mdx/image.tsx +23 -0
- package/src/client/components/mdx/index.ts +53 -0
- package/src/client/components/mdx/link.tsx +38 -0
- package/src/client/components/mdx/list.tsx +192 -0
- package/src/client/components/mdx/table.tsx +156 -0
- package/src/client/components/mdx/tabs.tsx +135 -0
- package/src/client/components/mdx/video.tsx +68 -0
- package/src/client/components/primitives/breadcrumbs.tsx +79 -0
- package/src/client/components/primitives/button-group.tsx +54 -0
- package/src/client/components/primitives/button.tsx +145 -0
- package/src/client/components/primitives/helpers/observer.ts +120 -0
- package/src/client/components/primitives/index.ts +17 -0
- package/src/client/components/primitives/link.tsx +122 -0
- package/src/client/components/primitives/menu.tsx +159 -0
- package/src/client/components/primitives/navbar.tsx +359 -0
- package/src/client/components/primitives/navigation-menu.tsx +116 -0
- package/src/client/components/primitives/on-this-page.tsx +461 -0
- package/src/client/components/primitives/page-nav.tsx +87 -0
- package/src/client/components/primitives/popover.tsx +47 -0
- package/src/client/components/primitives/search-dialog.tsx +183 -0
- package/src/client/components/primitives/sidebar.tsx +154 -0
- package/src/client/components/primitives/tabs.tsx +90 -0
- package/src/client/components/primitives/tooltip.tsx +83 -0
- package/src/client/components/primitives/types.ts +11 -0
- package/src/client/components/ui-base/breadcrumbs.tsx +42 -0
- package/src/client/components/ui-base/copy-markdown.tsx +112 -0
- package/src/client/components/ui-base/error-boundary.tsx +52 -0
- package/src/client/components/ui-base/github-stars.tsx +27 -0
- package/src/client/components/ui-base/head.tsx +69 -0
- package/src/client/components/ui-base/loading.tsx +87 -0
- package/src/client/components/ui-base/navbar.tsx +138 -0
- package/src/client/components/ui-base/not-found.tsx +24 -0
- package/src/client/components/ui-base/on-this-page.tsx +152 -0
- package/src/client/components/ui-base/page-nav.tsx +39 -0
- package/src/client/components/ui-base/powered-by.tsx +19 -0
- package/src/client/components/ui-base/progress-bar.tsx +67 -0
- package/src/client/components/ui-base/search-dialog.tsx +82 -0
- package/src/client/components/ui-base/sidebar.tsx +104 -0
- package/src/client/components/ui-base/tabs.tsx +65 -0
- package/src/client/components/ui-base/theme-toggle.tsx +32 -0
- package/src/client/hooks/index.ts +12 -0
- package/src/client/hooks/use-breadcrumbs.ts +22 -0
- package/src/client/hooks/use-i18n.ts +84 -0
- package/src/client/hooks/use-localized-to.ts +95 -0
- package/src/client/hooks/use-location.ts +5 -0
- package/src/client/hooks/use-navbar.ts +60 -0
- package/src/client/hooks/use-onthispage.ts +23 -0
- package/src/client/hooks/use-page-nav.ts +22 -0
- package/src/client/hooks/use-routes.ts +72 -0
- package/src/client/hooks/use-search.ts +71 -0
- package/src/client/hooks/use-sidebar.ts +49 -0
- package/src/client/hooks/use-tabs.ts +43 -0
- package/src/client/hooks/use-version.ts +78 -0
- package/src/client/index.ts +55 -17
- package/src/client/integrations/codesandbox.ts +179 -0
- package/src/client/ssr.tsx +27 -16
- package/src/client/theme/neutral.css +360 -0
- package/src/client/types.ts +131 -27
- package/src/client/utils/cn.ts +6 -0
- package/src/client/utils/copy-clipboard.ts +22 -0
- package/src/client/utils/get-base-file-path.ts +21 -0
- package/src/client/utils/github.ts +121 -0
- package/src/client/utils/use-on-change.ts +15 -0
- package/src/client/virtual.d.ts +24 -0
- package/src/node/cache.ts +156 -156
- package/src/node/config.ts +159 -103
- package/src/node/index.ts +13 -13
- package/src/node/mdx.ts +213 -61
- package/src/node/plugin/entry.ts +29 -18
- package/src/node/plugin/html.ts +11 -11
- package/src/node/plugin/index.ts +161 -83
- package/src/node/plugin/types.ts +2 -4
- package/src/node/routes/cache.ts +6 -6
- package/src/node/routes/index.ts +206 -113
- package/src/node/routes/parser.ts +106 -81
- package/src/node/routes/sorter.ts +15 -15
- package/src/node/routes/types.ts +24 -24
- package/src/node/ssg/index.ts +46 -46
- package/src/node/ssg/meta.ts +4 -4
- package/src/node/ssg/options.ts +5 -5
- package/src/node/ssg/sitemap.ts +14 -14
- package/src/node/utils.ts +31 -31
- package/tsconfig.json +25 -20
- package/tsup.config.ts +23 -14
- package/dist/PackageManagerTabs-NVT7G625.mjs +0 -99
- package/dist/SearchDialog-AGVF6JBO.mjs +0 -194
- package/dist/SearchDialog-YPDOM7Q6.css +0 -2847
- package/dist/Video-KNTY5BNO.mjs +0 -6
- package/dist/cache-KNL5B4EE.mjs +0 -12
- package/dist/chunk-7SFUJWTB.mjs +0 -211
- package/dist/chunk-FFBNU6IJ.mjs +0 -386
- package/dist/chunk-FMTOYQLO.mjs +0 -37
- package/dist/chunk-TKLQWU7H.mjs +0 -1920
- package/dist/chunk-Z7JHYNAS.mjs +0 -57
- package/dist/client/index.css +0 -2847
- package/dist/client/index.d.mts +0 -372
- package/dist/client/index.d.ts +0 -372
- package/dist/client/index.js +0 -3630
- package/dist/client/index.mjs +0 -697
- package/dist/client/ssr.css +0 -2847
- package/dist/client/ssr.d.mts +0 -27
- package/dist/client/ssr.d.ts +0 -27
- package/dist/client/ssr.js +0 -2928
- package/dist/client/ssr.mjs +0 -33
- package/dist/config-BsFQ-ErD.d.mts +0 -159
- package/dist/config-BsFQ-ErD.d.ts +0 -159
- package/dist/node/index.d.mts +0 -91
- package/dist/node/index.d.ts +0 -91
- package/dist/node/index.js +0 -1187
- package/dist/node/index.mjs +0 -762
- package/dist/types-Dj-bfnC3.d.mts +0 -74
- package/dist/types-Dj-bfnC3.d.ts +0 -74
- package/src/client/theme/components/CodeBlock/CodeBlock.tsx +0 -61
- package/src/client/theme/components/CodeBlock/index.ts +0 -1
- package/src/client/theme/components/PackageManagerTabs/PackageManagerTabs.tsx +0 -131
- package/src/client/theme/components/PackageManagerTabs/index.ts +0 -1
- package/src/client/theme/components/PackageManagerTabs/pkg-tabs.css +0 -64
- package/src/client/theme/components/Playground/Playground.tsx +0 -180
- package/src/client/theme/components/Playground/index.ts +0 -1
- package/src/client/theme/components/Playground/playground.css +0 -238
- package/src/client/theme/components/Video/Video.tsx +0 -84
- package/src/client/theme/components/Video/index.ts +0 -1
- package/src/client/theme/components/Video/video.css +0 -41
- package/src/client/theme/components/mdx/Admonition.tsx +0 -80
- package/src/client/theme/components/mdx/Badge.tsx +0 -31
- package/src/client/theme/components/mdx/Button.tsx +0 -50
- package/src/client/theme/components/mdx/Card.tsx +0 -80
- package/src/client/theme/components/mdx/Field.tsx +0 -60
- package/src/client/theme/components/mdx/FileTree.tsx +0 -229
- package/src/client/theme/components/mdx/List.tsx +0 -57
- package/src/client/theme/components/mdx/Table.tsx +0 -151
- package/src/client/theme/components/mdx/Tabs.tsx +0 -123
- package/src/client/theme/components/mdx/index.ts +0 -27
- package/src/client/theme/components/mdx/mdx-components.css +0 -764
- package/src/client/theme/icons/bun.tsx +0 -62
- package/src/client/theme/icons/deno.tsx +0 -20
- package/src/client/theme/icons/discord.tsx +0 -12
- package/src/client/theme/icons/github.tsx +0 -15
- package/src/client/theme/icons/npm.tsx +0 -13
- package/src/client/theme/icons/pnpm.tsx +0 -72
- package/src/client/theme/icons/twitter.tsx +0 -12
- package/src/client/theme/styles/markdown.css +0 -394
- package/src/client/theme/styles/variables.css +0 -175
- package/src/client/theme/styles.css +0 -39
- package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +0 -68
- package/src/client/theme/ui/Breadcrumbs/index.ts +0 -1
- package/src/client/theme/ui/CopyMarkdown/CopyMarkdown.tsx +0 -82
- package/src/client/theme/ui/CopyMarkdown/copy-markdown.css +0 -112
- package/src/client/theme/ui/CopyMarkdown/index.ts +0 -1
- package/src/client/theme/ui/ErrorBoundary/ErrorBoundary.tsx +0 -50
- package/src/client/theme/ui/ErrorBoundary/error-boundary.css +0 -55
- package/src/client/theme/ui/ErrorBoundary/index.ts +0 -1
- package/src/client/theme/ui/Footer/footer.css +0 -32
- package/src/client/theme/ui/Head/Head.tsx +0 -69
- package/src/client/theme/ui/Head/index.ts +0 -1
- package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +0 -125
- package/src/client/theme/ui/LanguageSwitcher/index.ts +0 -1
- package/src/client/theme/ui/LanguageSwitcher/language-switcher.css +0 -98
- package/src/client/theme/ui/Layout/Layout.tsx +0 -203
- package/src/client/theme/ui/Layout/base.css +0 -106
- package/src/client/theme/ui/Layout/index.ts +0 -2
- package/src/client/theme/ui/Layout/pagination.css +0 -72
- package/src/client/theme/ui/Layout/responsive.css +0 -47
- package/src/client/theme/ui/Link/Link.tsx +0 -392
- package/src/client/theme/ui/Link/LinkPreview.tsx +0 -59
- package/src/client/theme/ui/Link/index.ts +0 -2
- package/src/client/theme/ui/Link/link-preview.css +0 -48
- package/src/client/theme/ui/Loading/Loading.tsx +0 -10
- package/src/client/theme/ui/Loading/index.ts +0 -1
- package/src/client/theme/ui/Loading/loading.css +0 -30
- package/src/client/theme/ui/Navbar/GithubStars.tsx +0 -27
- package/src/client/theme/ui/Navbar/Navbar.tsx +0 -193
- package/src/client/theme/ui/Navbar/Tabs.tsx +0 -99
- package/src/client/theme/ui/Navbar/index.ts +0 -2
- package/src/client/theme/ui/Navbar/navbar.css +0 -347
- package/src/client/theme/ui/NotFound/NotFound.tsx +0 -19
- package/src/client/theme/ui/NotFound/index.ts +0 -1
- package/src/client/theme/ui/NotFound/not-found.css +0 -64
- package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +0 -244
- package/src/client/theme/ui/OnThisPage/index.ts +0 -1
- package/src/client/theme/ui/OnThisPage/toc.css +0 -152
- package/src/client/theme/ui/PoweredBy/PoweredBy.tsx +0 -18
- package/src/client/theme/ui/PoweredBy/index.ts +0 -1
- package/src/client/theme/ui/PoweredBy/powered-by.css +0 -76
- package/src/client/theme/ui/ProgressBar/ProgressBar.css +0 -17
- package/src/client/theme/ui/ProgressBar/ProgressBar.tsx +0 -51
- package/src/client/theme/ui/ProgressBar/index.ts +0 -1
- package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +0 -209
- package/src/client/theme/ui/SearchDialog/index.ts +0 -1
- package/src/client/theme/ui/SearchDialog/search.css +0 -152
- package/src/client/theme/ui/Sidebar/Sidebar.tsx +0 -244
- package/src/client/theme/ui/Sidebar/index.ts +0 -1
- package/src/client/theme/ui/Sidebar/sidebar.css +0 -230
- package/src/client/theme/ui/ThemeToggle/ThemeToggle.tsx +0 -69
- package/src/client/theme/ui/ThemeToggle/index.ts +0 -1
- package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +0 -136
- package/src/client/theme/ui/VersionSwitcher/index.ts +0 -1
- package/src/client/utils.ts +0 -49
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import GithubSlugger from
|
|
3
|
-
import { BoltdocsConfig } from
|
|
4
|
-
import { ParsedDocFile } from
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import GithubSlugger from 'github-slugger'
|
|
3
|
+
import type { BoltdocsConfig } from '../config'
|
|
4
|
+
import type { ParsedDocFile } from './types'
|
|
5
5
|
import {
|
|
6
6
|
normalizePath,
|
|
7
7
|
parseFrontmatter,
|
|
@@ -9,16 +9,19 @@ import {
|
|
|
9
9
|
capitalize,
|
|
10
10
|
stripNumberPrefix,
|
|
11
11
|
extractNumberPrefix,
|
|
12
|
-
|
|
13
|
-
} from "../utils";
|
|
12
|
+
} from '../utils'
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Parses a single Markdown/MDX file and extracts its metadata for routing.
|
|
17
16
|
* Checks frontmatter for explicit titles, descriptions, and sidebar positions.
|
|
18
17
|
*
|
|
18
|
+
* Also performs security validation to prevent path traversal and basic
|
|
19
|
+
* XSS sanitization for metadata and headings.
|
|
20
|
+
*
|
|
19
21
|
* @param file - The absolute path to the file
|
|
20
22
|
* @param docsDir - The root documentation directory (e.g., 'docs')
|
|
21
23
|
* @param basePath - The base URL path for the routes (default: '/docs')
|
|
24
|
+
* @param config - The Boltdocs configuration for versions and i18n
|
|
22
25
|
* @returns A parsed structure ready for route assembly and caching
|
|
23
26
|
*/
|
|
24
27
|
export function parseDocFile(
|
|
@@ -28,138 +31,138 @@ export function parseDocFile(
|
|
|
28
31
|
config?: BoltdocsConfig,
|
|
29
32
|
): ParsedDocFile {
|
|
30
33
|
// Security: Prevent path traversal
|
|
31
|
-
const decodedFile = decodeURIComponent(file)
|
|
32
|
-
const absoluteFile = path.resolve(decodedFile)
|
|
33
|
-
const absoluteDocsDir = path.resolve(docsDir)
|
|
34
|
+
const decodedFile = decodeURIComponent(file)
|
|
35
|
+
const absoluteFile = path.resolve(decodedFile)
|
|
36
|
+
const absoluteDocsDir = path.resolve(docsDir)
|
|
34
37
|
const relativePath = normalizePath(
|
|
35
38
|
path.relative(absoluteDocsDir, absoluteFile),
|
|
36
|
-
)
|
|
39
|
+
)
|
|
37
40
|
|
|
38
41
|
if (
|
|
39
|
-
relativePath.startsWith(
|
|
40
|
-
relativePath ===
|
|
41
|
-
absoluteFile.includes(
|
|
42
|
+
relativePath.startsWith('../') ||
|
|
43
|
+
relativePath === '..' ||
|
|
44
|
+
absoluteFile.includes('\0')
|
|
42
45
|
) {
|
|
43
46
|
throw new Error(
|
|
44
47
|
`Security breach: File is outside of docs directory or contains null bytes: ${file}`,
|
|
45
|
-
)
|
|
48
|
+
)
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
const { data, content } = parseFrontmatter(file)
|
|
49
|
-
let parts = relativePath.split(
|
|
51
|
+
const { data, content } = parseFrontmatter(file)
|
|
52
|
+
let parts = relativePath.split('/')
|
|
50
53
|
|
|
51
|
-
let locale: string | undefined
|
|
52
|
-
let version: string | undefined
|
|
54
|
+
let locale: string | undefined
|
|
55
|
+
let version: string | undefined
|
|
53
56
|
|
|
54
57
|
// Level 1: Check for version
|
|
55
58
|
if (config?.versions && parts.length > 0) {
|
|
56
|
-
const potentialVersion = parts[0]
|
|
59
|
+
const potentialVersion = parts[0]
|
|
57
60
|
if (config.versions.versions[potentialVersion]) {
|
|
58
|
-
version = potentialVersion
|
|
59
|
-
parts = parts.slice(1)
|
|
61
|
+
version = potentialVersion
|
|
62
|
+
parts = parts.slice(1)
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
// Level 2: Check for locale
|
|
64
67
|
if (config?.i18n && parts.length > 0) {
|
|
65
|
-
const potentialLocale = parts[0]
|
|
68
|
+
const potentialLocale = parts[0]
|
|
66
69
|
if (config.i18n.locales[potentialLocale]) {
|
|
67
|
-
locale = potentialLocale
|
|
68
|
-
parts = parts.slice(1)
|
|
70
|
+
locale = potentialLocale
|
|
71
|
+
parts = parts.slice(1)
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
// Level 3: Check for Tab hierarchy (name)
|
|
73
|
-
let inferredTab: string | undefined
|
|
76
|
+
let inferredTab: string | undefined
|
|
74
77
|
if (parts.length > 0) {
|
|
75
|
-
const tabMatch = parts[0].match(/^\((.+)\)$/)
|
|
78
|
+
const tabMatch = parts[0].match(/^\((.+)\)$/)
|
|
76
79
|
if (tabMatch) {
|
|
77
|
-
inferredTab = tabMatch[1].toLowerCase()
|
|
78
|
-
parts = parts.slice(1)
|
|
80
|
+
inferredTab = tabMatch[1].toLowerCase()
|
|
81
|
+
parts = parts.slice(1)
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
const cleanRelativePath = parts.join(
|
|
85
|
+
const cleanRelativePath = parts.join('/')
|
|
83
86
|
|
|
84
|
-
let cleanRoutePath: string
|
|
87
|
+
let cleanRoutePath: string
|
|
85
88
|
if (data.permalink) {
|
|
86
89
|
// If a permalink is specified, ensure it starts with a slash
|
|
87
|
-
cleanRoutePath = data.permalink.startsWith(
|
|
90
|
+
cleanRoutePath = data.permalink.startsWith('/')
|
|
88
91
|
? data.permalink
|
|
89
|
-
: `/${data.permalink}
|
|
92
|
+
: `/${data.permalink}`
|
|
90
93
|
} else {
|
|
91
|
-
cleanRoutePath = fileToRoutePath(cleanRelativePath ||
|
|
94
|
+
cleanRoutePath = fileToRoutePath(cleanRelativePath || 'index.md')
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
let finalPath = basePath
|
|
97
|
+
let finalPath = basePath
|
|
95
98
|
if (version) {
|
|
96
|
-
finalPath +=
|
|
99
|
+
finalPath += '/' + version
|
|
97
100
|
}
|
|
98
101
|
if (locale) {
|
|
99
|
-
finalPath +=
|
|
102
|
+
finalPath += '/' + locale
|
|
100
103
|
}
|
|
101
|
-
finalPath += cleanRoutePath ===
|
|
104
|
+
finalPath += cleanRoutePath === '/' ? '' : cleanRoutePath
|
|
102
105
|
|
|
103
|
-
if (!finalPath || finalPath ===
|
|
106
|
+
if (!finalPath || finalPath === '') finalPath = '/'
|
|
104
107
|
|
|
105
|
-
const rawFileName = parts[parts.length - 1]
|
|
106
|
-
const cleanFileName = stripNumberPrefix(rawFileName)
|
|
108
|
+
const rawFileName = parts[parts.length - 1]
|
|
109
|
+
const cleanFileName = stripNumberPrefix(rawFileName)
|
|
107
110
|
const inferredTitle = stripNumberPrefix(
|
|
108
111
|
path.basename(file, path.extname(file)),
|
|
109
|
-
)
|
|
112
|
+
)
|
|
110
113
|
const sidebarPosition =
|
|
111
|
-
data.sidebarPosition ?? extractNumberPrefix(rawFileName)
|
|
114
|
+
data.sidebarPosition ?? extractNumberPrefix(rawFileName)
|
|
112
115
|
|
|
113
|
-
const rawDirName = parts.length >= 2 ? parts[0] : undefined
|
|
114
|
-
const cleanDirName = rawDirName ? stripNumberPrefix(rawDirName) : undefined
|
|
116
|
+
const rawDirName = parts.length >= 2 ? parts[0] : undefined
|
|
117
|
+
const cleanDirName = rawDirName ? stripNumberPrefix(rawDirName) : undefined
|
|
115
118
|
|
|
116
|
-
const isGroupIndex = parts.length >= 2 && /^index\.mdx?$/.test(cleanFileName)
|
|
119
|
+
const isGroupIndex = parts.length >= 2 && /^index\.mdx?$/.test(cleanFileName)
|
|
117
120
|
|
|
118
|
-
const headings: { level: number; text: string; id: string }[] = []
|
|
119
|
-
const slugger = new GithubSlugger()
|
|
120
|
-
const headingsRegex = /^(#{2,4})\s+(.+)$/gm
|
|
121
|
-
let match
|
|
121
|
+
const headings: { level: number; text: string; id: string }[] = []
|
|
122
|
+
const slugger = new GithubSlugger()
|
|
123
|
+
const headingsRegex = /^(#{2,4})\s+(.+)$/gm
|
|
124
|
+
let match
|
|
122
125
|
while ((match = headingsRegex.exec(content)) !== null) {
|
|
123
|
-
const level = match[1].length
|
|
126
|
+
const level = match[1].length
|
|
124
127
|
// Strip simple markdown formatting specifically for the plain-text search index
|
|
125
128
|
const text = match[2]
|
|
126
|
-
.replace(/\[([^\]]+)\]\([^\)]+\)/g,
|
|
127
|
-
.replace(/[_*`]/g,
|
|
128
|
-
.trim()
|
|
129
|
-
const id = slugger.slug(text)
|
|
129
|
+
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
|
|
130
|
+
.replace(/[_*`]/g, '')
|
|
131
|
+
.trim()
|
|
132
|
+
const id = slugger.slug(text)
|
|
130
133
|
// Security: Sanitize heading text for XSS
|
|
131
|
-
|
|
134
|
+
const sanitizedText = text
|
|
135
|
+
.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gim, '')
|
|
136
|
+
.replace(/<[^>]+on\w+="[^"]*"/gim, '')
|
|
137
|
+
.replace(/<img[^>]+>/gim, '')
|
|
138
|
+
.trim()
|
|
139
|
+
headings.push({ level, text: sanitizedText, id })
|
|
132
140
|
}
|
|
133
141
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
const sanitize = (str: string) =>
|
|
143
|
+
str.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gim, '').trim()
|
|
144
|
+
|
|
145
|
+
const sanitizedTitle = data.title ? sanitize(data.title) : inferredTitle
|
|
146
|
+
let sanitizedDescription = data.description ? sanitize(data.description) : ''
|
|
138
147
|
|
|
139
148
|
// If no description is provided, extract a summary from the content
|
|
140
149
|
if (!sanitizedDescription && content) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
sanitizedDescription = sanitize(
|
|
151
|
+
content
|
|
152
|
+
.replace(/^#+.*$/gm, '') // Remove headers
|
|
153
|
+
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Simplify links
|
|
154
|
+
.replace(/[_*`]/g, '') // Remove formatting
|
|
155
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
156
|
+
.trim()
|
|
157
|
+
.slice(0, 160),
|
|
158
|
+
)
|
|
149
159
|
}
|
|
150
160
|
|
|
151
|
-
const sanitizedBadge = data.badge ? data.badge : undefined
|
|
152
|
-
const icon = data.icon ? String(data.icon) : undefined
|
|
161
|
+
const sanitizedBadge = data.badge ? sanitize(data.badge) : undefined
|
|
162
|
+
const icon = data.icon ? String(data.icon) : undefined
|
|
153
163
|
|
|
154
164
|
// Extract full content as plain text for search indexing
|
|
155
|
-
const plainText = content
|
|
156
|
-
.replace(/^#+.*$/gm, "") // Remove headers
|
|
157
|
-
.replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1") // Simplify links
|
|
158
|
-
.replace(/<[^>]+>/g, "") // Remove HTML/JSX tags
|
|
159
|
-
.replace(/\{[^\}]+\}/g, "") // Remove JS expressions/curly braces
|
|
160
|
-
.replace(/[_*`]/g, "") // Remove formatting
|
|
161
|
-
.replace(/\n+/g, " ") // Normalize whitespace
|
|
162
|
-
.trim();
|
|
165
|
+
const plainText = parseContentToPlainText(content)
|
|
163
166
|
|
|
164
167
|
return {
|
|
165
168
|
route: {
|
|
@@ -183,10 +186,10 @@ export function parseDocFile(
|
|
|
183
186
|
inferredTab,
|
|
184
187
|
groupMeta: isGroupIndex
|
|
185
188
|
? {
|
|
186
|
-
title:
|
|
189
|
+
title:
|
|
187
190
|
data.groupTitle ||
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
data.title ||
|
|
192
|
+
(cleanDirName ? capitalize(cleanDirName) : ''),
|
|
190
193
|
position:
|
|
191
194
|
data.groupPosition ??
|
|
192
195
|
data.sidebarPosition ??
|
|
@@ -197,5 +200,27 @@ export function parseDocFile(
|
|
|
197
200
|
inferredGroupPosition: rawDirName
|
|
198
201
|
? extractNumberPrefix(rawDirName)
|
|
199
202
|
: undefined,
|
|
200
|
-
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Sanitizes a string by removing script tags for basic XSS protection.
|
|
208
|
+
*/
|
|
209
|
+
function sanitize(str: string): string {
|
|
210
|
+
return str.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gim, '').trim()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Converts markdown content to plain text for search indexing.
|
|
215
|
+
* Strips headers, links, tags, and formatting.
|
|
216
|
+
*/
|
|
217
|
+
function parseContentToPlainText(content: string): string {
|
|
218
|
+
return content
|
|
219
|
+
.replace(/^#+.*$/gm, '') // Remove headers
|
|
220
|
+
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Simplify links
|
|
221
|
+
.replace(/<[^>]+>/g, '') // Remove HTML/JSX tags
|
|
222
|
+
.replace(/\{[^\}]+\}/g, '') // Remove JS expressions/curly braces
|
|
223
|
+
.replace(/[_*`]/g, '') // Remove formatting
|
|
224
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
225
|
+
.trim()
|
|
201
226
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RouteMeta } from
|
|
1
|
+
import type { RouteMeta } from './types'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Sorts an array of generated routes.
|
|
@@ -11,32 +11,32 @@ import { RouteMeta } from "./types";
|
|
|
11
11
|
export function sortRoutes(routes: RouteMeta[]): RouteMeta[] {
|
|
12
12
|
return routes.sort((a, b) => {
|
|
13
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
|
|
14
|
+
if (!a.group && !b.group) return compareByPosition(a, b)
|
|
15
|
+
if (!a.group) return -1
|
|
16
|
+
if (!b.group) return 1
|
|
17
17
|
|
|
18
18
|
// Different groups: sort by group position
|
|
19
19
|
if (a.group !== b.group) {
|
|
20
|
-
return compareByGroupPosition(a, b)
|
|
20
|
+
return compareByGroupPosition(a, b)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// Same group: sort by item position
|
|
24
|
-
return compareByPosition(a, b)
|
|
25
|
-
})
|
|
24
|
+
return compareByPosition(a, b)
|
|
25
|
+
})
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function compareByPosition(a: RouteMeta, b: RouteMeta): number {
|
|
29
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)
|
|
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
34
|
}
|
|
35
35
|
|
|
36
36
|
function compareByGroupPosition(a: RouteMeta, b: RouteMeta): number {
|
|
37
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!)
|
|
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
42
|
}
|
package/src/node/routes/types.ts
CHANGED
|
@@ -4,41 +4,41 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export interface RouteMeta {
|
|
6
6
|
/** The final URL path for the route (e.g., '/docs/guide/start') */
|
|
7
|
-
path: string
|
|
7
|
+
path: string
|
|
8
8
|
/** The absolute filesystem path to the source markdown/mdx file */
|
|
9
|
-
componentPath: string
|
|
9
|
+
componentPath: string
|
|
10
10
|
/** The title of the page, usually extracted from frontmatter or the filename */
|
|
11
|
-
title: string
|
|
11
|
+
title: string
|
|
12
12
|
/** The relative path from the docs directory, used for edit links */
|
|
13
|
-
filePath: string
|
|
13
|
+
filePath: string
|
|
14
14
|
/** Optional description of the page (for SEO/meta tags) */
|
|
15
|
-
description?: string
|
|
15
|
+
description?: string
|
|
16
16
|
/** Optional explicit position for ordering in the sidebar */
|
|
17
|
-
sidebarPosition?: number
|
|
17
|
+
sidebarPosition?: number
|
|
18
18
|
/** The group (directory) this route belongs to */
|
|
19
|
-
group?: string
|
|
19
|
+
group?: string
|
|
20
20
|
/** The display title for the route's group */
|
|
21
|
-
groupTitle?: string
|
|
21
|
+
groupTitle?: string
|
|
22
22
|
/** Optional explicit position for ordering the group itself */
|
|
23
|
-
groupPosition?: number
|
|
23
|
+
groupPosition?: number
|
|
24
24
|
/** Optional icon for the route's group */
|
|
25
|
-
groupIcon?: string
|
|
25
|
+
groupIcon?: string
|
|
26
26
|
/** Extracted markdown headings for search indexing */
|
|
27
|
-
headings?: { level: number; text: string; id: string }[]
|
|
27
|
+
headings?: { level: number; text: string; id: string }[]
|
|
28
28
|
/** The locale this route belongs to, if i18n is configured */
|
|
29
|
-
locale?: string
|
|
29
|
+
locale?: string
|
|
30
30
|
/** The version this route belongs to, if versioning is configured */
|
|
31
|
-
version?: string
|
|
31
|
+
version?: string
|
|
32
32
|
/** Optional badge to display next to the sidebar item (e.g., 'New', 'Experimental') */
|
|
33
|
-
badge?: string | { text: string; expires?: string }
|
|
33
|
+
badge?: string | { text: string; expires?: string }
|
|
34
34
|
/** Optional icon to display (Lucide icon name or raw SVG) */
|
|
35
|
-
icon?: string
|
|
35
|
+
icon?: string
|
|
36
36
|
/** The tab this route belongs to, if tabs are configured */
|
|
37
|
-
tab?: string
|
|
37
|
+
tab?: string
|
|
38
38
|
/** The extracted plain-text content of the page for search indexing */
|
|
39
|
-
_content?: string
|
|
39
|
+
_content?: string
|
|
40
40
|
/** The raw markdown content of the page */
|
|
41
|
-
_rawContent?: string
|
|
41
|
+
_rawContent?: string
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -47,15 +47,15 @@ export interface RouteMeta {
|
|
|
47
47
|
*/
|
|
48
48
|
export interface ParsedDocFile {
|
|
49
49
|
/** The core route metadata without group-level details (inferred later) */
|
|
50
|
-
route: Omit<RouteMeta,
|
|
50
|
+
route: Omit<RouteMeta, 'group' | 'groupTitle' | 'groupPosition'>
|
|
51
51
|
/** The base directory of the file (used to group files together) */
|
|
52
|
-
relativeDir?: string
|
|
52
|
+
relativeDir?: string
|
|
53
53
|
/** Whether this file is the index file for its directory group */
|
|
54
|
-
isGroupIndex: boolean
|
|
54
|
+
isGroupIndex: boolean
|
|
55
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 }
|
|
56
|
+
groupMeta?: { title: string; position?: number; icon?: string }
|
|
57
57
|
/** Extracted group position from the directory name if it has a numeric prefix */
|
|
58
|
-
inferredGroupPosition?: number
|
|
58
|
+
inferredGroupPosition?: number
|
|
59
59
|
/** Extracted tab name from the directory name if it follows the (tab-name) syntax */
|
|
60
|
-
inferredTab?: string
|
|
60
|
+
inferredTab?: string
|
|
61
61
|
}
|
package/src/node/ssg/index.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { generateRoutes } from
|
|
4
|
-
import { escapeHtml } from
|
|
5
|
-
import { fileURLToPath } from
|
|
6
|
-
import { createRequire } from
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { generateRoutes } from '../routes'
|
|
4
|
+
import { escapeHtml } from '../utils'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { createRequire } from 'module'
|
|
7
7
|
|
|
8
|
-
import { SSGOptions } from
|
|
9
|
-
import { replaceMetaTags } from
|
|
10
|
-
import { generateSitemap } from
|
|
8
|
+
import type { SSGOptions } from './options'
|
|
9
|
+
import { replaceMetaTags } from './meta'
|
|
10
|
+
import { generateSitemap } from './sitemap'
|
|
11
11
|
|
|
12
12
|
// Re-export options for consumers
|
|
13
|
-
export type { SSGOptions }
|
|
13
|
+
export type { SSGOptions }
|
|
14
14
|
|
|
15
15
|
// Polyfill __dirname and require for ESM
|
|
16
|
-
const _filename = fileURLToPath(import.meta.url)
|
|
17
|
-
const _dirname = path.dirname(_filename)
|
|
18
|
-
const _require = createRequire(import.meta.url)
|
|
16
|
+
const _filename = fileURLToPath(import.meta.url)
|
|
17
|
+
const _dirname = path.dirname(_filename)
|
|
18
|
+
const _require = createRequire(import.meta.url)
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Generates static HTML files and a \`sitemap.xml\` for all documentation routes.
|
|
@@ -24,40 +24,40 @@ const _require = createRequire(import.meta.url);
|
|
|
24
24
|
* @param options - Configuration for paths and site metadata
|
|
25
25
|
*/
|
|
26
26
|
export async function generateStaticPages(options: SSGOptions): Promise<void> {
|
|
27
|
-
const { docsDir, docsDirName, outDir, config } = options
|
|
28
|
-
const routes = await generateRoutes(docsDir, config)
|
|
29
|
-
const siteTitle = config?.themeConfig?.title ||
|
|
30
|
-
const siteDescription = config?.themeConfig?.description ||
|
|
27
|
+
const { docsDir, docsDirName, outDir, config } = options
|
|
28
|
+
const routes = await generateRoutes(docsDir, config)
|
|
29
|
+
const siteTitle = config?.themeConfig?.title || 'Boltdocs'
|
|
30
|
+
const siteDescription = config?.themeConfig?.description || ''
|
|
31
31
|
|
|
32
32
|
// Resolve the SSR module (compiled by tsup)
|
|
33
|
-
const ssrModulePath = path.resolve(_dirname,
|
|
33
|
+
const ssrModulePath = path.resolve(_dirname, '../client/ssr.js')
|
|
34
34
|
if (!fs.existsSync(ssrModulePath)) {
|
|
35
35
|
console.error(
|
|
36
|
-
|
|
36
|
+
'[boltdocs] SSR module not found at',
|
|
37
37
|
ssrModulePath,
|
|
38
|
-
|
|
39
|
-
)
|
|
40
|
-
return
|
|
38
|
+
'- Did you build the core package?',
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
41
|
}
|
|
42
|
-
const { render } = _require(ssrModulePath)
|
|
42
|
+
const { render } = _require(ssrModulePath)
|
|
43
43
|
|
|
44
44
|
// Read the built index.html as template
|
|
45
|
-
const templatePath = path.join(outDir,
|
|
45
|
+
const templatePath = path.join(outDir, 'index.html')
|
|
46
46
|
if (!fs.existsSync(templatePath)) {
|
|
47
|
-
console.warn(
|
|
48
|
-
return
|
|
47
|
+
console.warn('[boltdocs] No index.html found in outDir, skipping SSG.')
|
|
48
|
+
return
|
|
49
49
|
}
|
|
50
|
-
const template = fs.readFileSync(templatePath,
|
|
50
|
+
const template = fs.readFileSync(templatePath, 'utf-8')
|
|
51
51
|
|
|
52
52
|
// Generate an HTML file for each route concurrently
|
|
53
53
|
await Promise.all(
|
|
54
54
|
routes.map(async (route) => {
|
|
55
|
-
const pageTitle = `${route.title} | ${siteTitle}
|
|
56
|
-
const pageDescription = route.description || siteDescription
|
|
55
|
+
const pageTitle = `${route.title} | ${siteTitle}`
|
|
56
|
+
const pageDescription = route.description || siteDescription
|
|
57
57
|
|
|
58
58
|
// We mock the modules for SSR so it doesn't crash trying to dynamically import
|
|
59
|
-
const fakeModules: Record<string, any> = {}
|
|
60
|
-
fakeModules[route.componentPath] = { default: () => {} }
|
|
59
|
+
const fakeModules: Record<string, any> = {}
|
|
60
|
+
fakeModules[route.componentPath] = { default: () => {} } // Mock MDX component
|
|
61
61
|
|
|
62
62
|
try {
|
|
63
63
|
const appHtml = await render({
|
|
@@ -67,40 +67,40 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
|
|
|
67
67
|
docsDirName: docsDirName,
|
|
68
68
|
modules: fakeModules,
|
|
69
69
|
homePage: undefined, // No custom home page for now
|
|
70
|
-
})
|
|
70
|
+
})
|
|
71
71
|
|
|
72
72
|
const html = replaceMetaTags(template, {
|
|
73
73
|
title: escapeHtml(pageTitle),
|
|
74
74
|
description: escapeHtml(pageDescription),
|
|
75
75
|
})
|
|
76
|
-
.replace(
|
|
77
|
-
.replace(`<div id="root"></div>`, `<div id="root">${appHtml}</div>`)
|
|
76
|
+
.replace('<!--app-html-->', appHtml)
|
|
77
|
+
.replace(`<div id="root"></div>`, `<div id="root">${appHtml}</div>`)
|
|
78
78
|
|
|
79
|
-
const routeDir = path.join(outDir, route.path)
|
|
80
|
-
await fs.promises.mkdir(routeDir, { recursive: true })
|
|
79
|
+
const routeDir = path.join(outDir, route.path)
|
|
80
|
+
await fs.promises.mkdir(routeDir, { recursive: true })
|
|
81
81
|
await fs.promises.writeFile(
|
|
82
|
-
path.join(routeDir,
|
|
82
|
+
path.join(routeDir, 'index.html'),
|
|
83
83
|
html,
|
|
84
|
-
|
|
85
|
-
)
|
|
84
|
+
'utf-8',
|
|
85
|
+
)
|
|
86
86
|
} catch (e) {
|
|
87
|
-
console.error(`[boltdocs] Error SSR rendering route ${route.path}:`, e)
|
|
87
|
+
console.error(`[boltdocs] Error SSR rendering route ${route.path}:`, e)
|
|
88
88
|
}
|
|
89
89
|
}),
|
|
90
|
-
)
|
|
90
|
+
)
|
|
91
91
|
|
|
92
92
|
// Generate sitemap.xml
|
|
93
93
|
const sitemap = generateSitemap(
|
|
94
94
|
routes.map((r) => r.path),
|
|
95
95
|
config,
|
|
96
|
-
)
|
|
97
|
-
fs.writeFileSync(path.join(outDir,
|
|
96
|
+
)
|
|
97
|
+
fs.writeFileSync(path.join(outDir, 'sitemap.xml'), sitemap, 'utf-8')
|
|
98
98
|
|
|
99
99
|
console.log(
|
|
100
100
|
`[boltdocs] Generated ${routes.length} static pages + sitemap.xml`,
|
|
101
|
-
)
|
|
101
|
+
)
|
|
102
102
|
|
|
103
103
|
// Ensure all cache operations (like index persistence) are finished
|
|
104
|
-
const { flushCache } = await import(
|
|
105
|
-
await flushCache()
|
|
104
|
+
const { flushCache } = await import('../cache')
|
|
105
|
+
await flushCache()
|
|
106
106
|
}
|
package/src/node/ssg/meta.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeHtml } from
|
|
1
|
+
import { escapeHtml } from '../utils'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Replaces placeholder or default meta tags in the HTML template with page-specific values.
|
|
@@ -11,8 +11,8 @@ export function replaceMetaTags(
|
|
|
11
11
|
html: string,
|
|
12
12
|
meta: { title: string; description: string },
|
|
13
13
|
): string {
|
|
14
|
-
const title = escapeHtml(meta.title)
|
|
15
|
-
const description = escapeHtml(meta.description)
|
|
14
|
+
const title = escapeHtml(meta.title)
|
|
15
|
+
const description = escapeHtml(meta.description)
|
|
16
16
|
|
|
17
17
|
return html
|
|
18
18
|
.replace(/<title>.*?<\/title>/, `<title>${title}</title>`)
|
|
@@ -29,5 +29,5 @@ export function replaceMetaTags(
|
|
|
29
29
|
.replace(
|
|
30
30
|
/(<meta name="twitter:description" content=")[^"]*(")/,
|
|
31
31
|
`$1${description}$2`,
|
|
32
|
-
)
|
|
32
|
+
)
|
|
33
33
|
}
|
package/src/node/ssg/options.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { BoltdocsConfig } from
|
|
1
|
+
import type { BoltdocsConfig } from '../config'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for the Static Site Generation process.
|
|
5
5
|
*/
|
|
6
6
|
export interface SSGOptions {
|
|
7
7
|
/** The root directory containing markdown documentation files */
|
|
8
|
-
docsDir: string
|
|
8
|
+
docsDir: string
|
|
9
9
|
/** The name of the documentation directory (e.g. 'docs') */
|
|
10
|
-
docsDirName: string
|
|
10
|
+
docsDirName: string
|
|
11
11
|
/** The output directory where Vite placed the compiled `index.html` and assets */
|
|
12
|
-
outDir: string
|
|
12
|
+
outDir: string
|
|
13
13
|
/** Pre-resolved config (avoids re-resolving during the SSG phase) */
|
|
14
|
-
config?: BoltdocsConfig
|
|
14
|
+
config?: BoltdocsConfig
|
|
15
15
|
}
|