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.
Files changed (225) hide show
  1. package/package.json +29 -7
  2. package/src/client/app/config-context.tsx +18 -0
  3. package/src/client/app/docs-layout.tsx +14 -0
  4. package/src/client/app/index.tsx +132 -260
  5. package/src/client/app/mdx-component.tsx +52 -0
  6. package/src/client/app/mdx-components-context.tsx +23 -0
  7. package/src/client/app/mdx-page.tsx +20 -0
  8. package/src/client/app/preload.tsx +38 -30
  9. package/src/client/app/router.tsx +30 -0
  10. package/src/client/app/scroll-handler.tsx +40 -0
  11. package/src/client/app/theme-context.tsx +75 -0
  12. package/src/client/components/default-layout.tsx +80 -0
  13. package/src/client/components/docs-layout.tsx +105 -0
  14. package/src/client/components/icons-dev.tsx +74 -0
  15. package/src/client/components/mdx/admonition.tsx +107 -0
  16. package/src/client/components/mdx/badge.tsx +41 -0
  17. package/src/client/components/mdx/button.tsx +35 -0
  18. package/src/client/components/mdx/card.tsx +124 -0
  19. package/src/client/components/mdx/code-block.tsx +119 -0
  20. package/src/client/components/mdx/component-preview.tsx +47 -0
  21. package/src/client/components/mdx/component-props.tsx +83 -0
  22. package/src/client/components/mdx/field.tsx +66 -0
  23. package/src/client/components/mdx/file-tree.tsx +287 -0
  24. package/src/client/components/mdx/hooks/use-code-block.ts +56 -0
  25. package/src/client/components/mdx/hooks/use-component-preview.ts +16 -0
  26. package/src/client/components/mdx/hooks/useTable.ts +74 -0
  27. package/src/client/components/mdx/hooks/useTabs.ts +68 -0
  28. package/src/client/components/mdx/image.tsx +23 -0
  29. package/src/client/components/mdx/index.ts +53 -0
  30. package/src/client/components/mdx/link.tsx +38 -0
  31. package/src/client/components/mdx/list.tsx +192 -0
  32. package/src/client/components/mdx/table.tsx +156 -0
  33. package/src/client/components/mdx/tabs.tsx +135 -0
  34. package/src/client/components/mdx/video.tsx +68 -0
  35. package/src/client/components/primitives/breadcrumbs.tsx +79 -0
  36. package/src/client/components/primitives/button-group.tsx +54 -0
  37. package/src/client/components/primitives/button.tsx +145 -0
  38. package/src/client/components/primitives/helpers/observer.ts +120 -0
  39. package/src/client/components/primitives/index.ts +17 -0
  40. package/src/client/components/primitives/link.tsx +122 -0
  41. package/src/client/components/primitives/menu.tsx +159 -0
  42. package/src/client/components/primitives/navbar.tsx +359 -0
  43. package/src/client/components/primitives/navigation-menu.tsx +116 -0
  44. package/src/client/components/primitives/on-this-page.tsx +461 -0
  45. package/src/client/components/primitives/page-nav.tsx +87 -0
  46. package/src/client/components/primitives/popover.tsx +47 -0
  47. package/src/client/components/primitives/search-dialog.tsx +183 -0
  48. package/src/client/components/primitives/sidebar.tsx +154 -0
  49. package/src/client/components/primitives/tabs.tsx +90 -0
  50. package/src/client/components/primitives/tooltip.tsx +83 -0
  51. package/src/client/components/primitives/types.ts +11 -0
  52. package/src/client/components/ui-base/breadcrumbs.tsx +42 -0
  53. package/src/client/components/ui-base/copy-markdown.tsx +112 -0
  54. package/src/client/components/ui-base/error-boundary.tsx +52 -0
  55. package/src/client/components/ui-base/github-stars.tsx +27 -0
  56. package/src/client/components/ui-base/head.tsx +69 -0
  57. package/src/client/components/ui-base/loading.tsx +87 -0
  58. package/src/client/components/ui-base/navbar.tsx +138 -0
  59. package/src/client/components/ui-base/not-found.tsx +24 -0
  60. package/src/client/components/ui-base/on-this-page.tsx +152 -0
  61. package/src/client/components/ui-base/page-nav.tsx +39 -0
  62. package/src/client/components/ui-base/powered-by.tsx +19 -0
  63. package/src/client/components/ui-base/progress-bar.tsx +67 -0
  64. package/src/client/components/ui-base/search-dialog.tsx +82 -0
  65. package/src/client/components/ui-base/sidebar.tsx +104 -0
  66. package/src/client/components/ui-base/tabs.tsx +65 -0
  67. package/src/client/components/ui-base/theme-toggle.tsx +32 -0
  68. package/src/client/hooks/index.ts +12 -0
  69. package/src/client/hooks/use-breadcrumbs.ts +22 -0
  70. package/src/client/hooks/use-i18n.ts +84 -0
  71. package/src/client/hooks/use-localized-to.ts +95 -0
  72. package/src/client/hooks/use-location.ts +5 -0
  73. package/src/client/hooks/use-navbar.ts +60 -0
  74. package/src/client/hooks/use-onthispage.ts +23 -0
  75. package/src/client/hooks/use-page-nav.ts +22 -0
  76. package/src/client/hooks/use-routes.ts +72 -0
  77. package/src/client/hooks/use-search.ts +71 -0
  78. package/src/client/hooks/use-sidebar.ts +49 -0
  79. package/src/client/hooks/use-tabs.ts +43 -0
  80. package/src/client/hooks/use-version.ts +78 -0
  81. package/src/client/index.ts +55 -17
  82. package/src/client/integrations/codesandbox.ts +179 -0
  83. package/src/client/ssr.tsx +27 -16
  84. package/src/client/theme/neutral.css +360 -0
  85. package/src/client/types.ts +131 -27
  86. package/src/client/utils/cn.ts +6 -0
  87. package/src/client/utils/copy-clipboard.ts +22 -0
  88. package/src/client/utils/get-base-file-path.ts +21 -0
  89. package/src/client/utils/github.ts +121 -0
  90. package/src/client/utils/use-on-change.ts +15 -0
  91. package/src/client/virtual.d.ts +24 -0
  92. package/src/node/cache.ts +156 -156
  93. package/src/node/config.ts +159 -103
  94. package/src/node/index.ts +13 -13
  95. package/src/node/mdx.ts +213 -61
  96. package/src/node/plugin/entry.ts +29 -18
  97. package/src/node/plugin/html.ts +11 -11
  98. package/src/node/plugin/index.ts +161 -83
  99. package/src/node/plugin/types.ts +2 -4
  100. package/src/node/routes/cache.ts +6 -6
  101. package/src/node/routes/index.ts +206 -113
  102. package/src/node/routes/parser.ts +106 -81
  103. package/src/node/routes/sorter.ts +15 -15
  104. package/src/node/routes/types.ts +24 -24
  105. package/src/node/ssg/index.ts +46 -46
  106. package/src/node/ssg/meta.ts +4 -4
  107. package/src/node/ssg/options.ts +5 -5
  108. package/src/node/ssg/sitemap.ts +14 -14
  109. package/src/node/utils.ts +31 -31
  110. package/tsconfig.json +25 -20
  111. package/tsup.config.ts +23 -14
  112. package/dist/PackageManagerTabs-NVT7G625.mjs +0 -99
  113. package/dist/SearchDialog-AGVF6JBO.mjs +0 -194
  114. package/dist/SearchDialog-YPDOM7Q6.css +0 -2847
  115. package/dist/Video-KNTY5BNO.mjs +0 -6
  116. package/dist/cache-KNL5B4EE.mjs +0 -12
  117. package/dist/chunk-7SFUJWTB.mjs +0 -211
  118. package/dist/chunk-FFBNU6IJ.mjs +0 -386
  119. package/dist/chunk-FMTOYQLO.mjs +0 -37
  120. package/dist/chunk-TKLQWU7H.mjs +0 -1920
  121. package/dist/chunk-Z7JHYNAS.mjs +0 -57
  122. package/dist/client/index.css +0 -2847
  123. package/dist/client/index.d.mts +0 -372
  124. package/dist/client/index.d.ts +0 -372
  125. package/dist/client/index.js +0 -3630
  126. package/dist/client/index.mjs +0 -697
  127. package/dist/client/ssr.css +0 -2847
  128. package/dist/client/ssr.d.mts +0 -27
  129. package/dist/client/ssr.d.ts +0 -27
  130. package/dist/client/ssr.js +0 -2928
  131. package/dist/client/ssr.mjs +0 -33
  132. package/dist/config-BsFQ-ErD.d.mts +0 -159
  133. package/dist/config-BsFQ-ErD.d.ts +0 -159
  134. package/dist/node/index.d.mts +0 -91
  135. package/dist/node/index.d.ts +0 -91
  136. package/dist/node/index.js +0 -1187
  137. package/dist/node/index.mjs +0 -762
  138. package/dist/types-Dj-bfnC3.d.mts +0 -74
  139. package/dist/types-Dj-bfnC3.d.ts +0 -74
  140. package/src/client/theme/components/CodeBlock/CodeBlock.tsx +0 -61
  141. package/src/client/theme/components/CodeBlock/index.ts +0 -1
  142. package/src/client/theme/components/PackageManagerTabs/PackageManagerTabs.tsx +0 -131
  143. package/src/client/theme/components/PackageManagerTabs/index.ts +0 -1
  144. package/src/client/theme/components/PackageManagerTabs/pkg-tabs.css +0 -64
  145. package/src/client/theme/components/Playground/Playground.tsx +0 -180
  146. package/src/client/theme/components/Playground/index.ts +0 -1
  147. package/src/client/theme/components/Playground/playground.css +0 -238
  148. package/src/client/theme/components/Video/Video.tsx +0 -84
  149. package/src/client/theme/components/Video/index.ts +0 -1
  150. package/src/client/theme/components/Video/video.css +0 -41
  151. package/src/client/theme/components/mdx/Admonition.tsx +0 -80
  152. package/src/client/theme/components/mdx/Badge.tsx +0 -31
  153. package/src/client/theme/components/mdx/Button.tsx +0 -50
  154. package/src/client/theme/components/mdx/Card.tsx +0 -80
  155. package/src/client/theme/components/mdx/Field.tsx +0 -60
  156. package/src/client/theme/components/mdx/FileTree.tsx +0 -229
  157. package/src/client/theme/components/mdx/List.tsx +0 -57
  158. package/src/client/theme/components/mdx/Table.tsx +0 -151
  159. package/src/client/theme/components/mdx/Tabs.tsx +0 -123
  160. package/src/client/theme/components/mdx/index.ts +0 -27
  161. package/src/client/theme/components/mdx/mdx-components.css +0 -764
  162. package/src/client/theme/icons/bun.tsx +0 -62
  163. package/src/client/theme/icons/deno.tsx +0 -20
  164. package/src/client/theme/icons/discord.tsx +0 -12
  165. package/src/client/theme/icons/github.tsx +0 -15
  166. package/src/client/theme/icons/npm.tsx +0 -13
  167. package/src/client/theme/icons/pnpm.tsx +0 -72
  168. package/src/client/theme/icons/twitter.tsx +0 -12
  169. package/src/client/theme/styles/markdown.css +0 -394
  170. package/src/client/theme/styles/variables.css +0 -175
  171. package/src/client/theme/styles.css +0 -39
  172. package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +0 -68
  173. package/src/client/theme/ui/Breadcrumbs/index.ts +0 -1
  174. package/src/client/theme/ui/CopyMarkdown/CopyMarkdown.tsx +0 -82
  175. package/src/client/theme/ui/CopyMarkdown/copy-markdown.css +0 -112
  176. package/src/client/theme/ui/CopyMarkdown/index.ts +0 -1
  177. package/src/client/theme/ui/ErrorBoundary/ErrorBoundary.tsx +0 -50
  178. package/src/client/theme/ui/ErrorBoundary/error-boundary.css +0 -55
  179. package/src/client/theme/ui/ErrorBoundary/index.ts +0 -1
  180. package/src/client/theme/ui/Footer/footer.css +0 -32
  181. package/src/client/theme/ui/Head/Head.tsx +0 -69
  182. package/src/client/theme/ui/Head/index.ts +0 -1
  183. package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +0 -125
  184. package/src/client/theme/ui/LanguageSwitcher/index.ts +0 -1
  185. package/src/client/theme/ui/LanguageSwitcher/language-switcher.css +0 -98
  186. package/src/client/theme/ui/Layout/Layout.tsx +0 -203
  187. package/src/client/theme/ui/Layout/base.css +0 -106
  188. package/src/client/theme/ui/Layout/index.ts +0 -2
  189. package/src/client/theme/ui/Layout/pagination.css +0 -72
  190. package/src/client/theme/ui/Layout/responsive.css +0 -47
  191. package/src/client/theme/ui/Link/Link.tsx +0 -392
  192. package/src/client/theme/ui/Link/LinkPreview.tsx +0 -59
  193. package/src/client/theme/ui/Link/index.ts +0 -2
  194. package/src/client/theme/ui/Link/link-preview.css +0 -48
  195. package/src/client/theme/ui/Loading/Loading.tsx +0 -10
  196. package/src/client/theme/ui/Loading/index.ts +0 -1
  197. package/src/client/theme/ui/Loading/loading.css +0 -30
  198. package/src/client/theme/ui/Navbar/GithubStars.tsx +0 -27
  199. package/src/client/theme/ui/Navbar/Navbar.tsx +0 -193
  200. package/src/client/theme/ui/Navbar/Tabs.tsx +0 -99
  201. package/src/client/theme/ui/Navbar/index.ts +0 -2
  202. package/src/client/theme/ui/Navbar/navbar.css +0 -347
  203. package/src/client/theme/ui/NotFound/NotFound.tsx +0 -19
  204. package/src/client/theme/ui/NotFound/index.ts +0 -1
  205. package/src/client/theme/ui/NotFound/not-found.css +0 -64
  206. package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +0 -244
  207. package/src/client/theme/ui/OnThisPage/index.ts +0 -1
  208. package/src/client/theme/ui/OnThisPage/toc.css +0 -152
  209. package/src/client/theme/ui/PoweredBy/PoweredBy.tsx +0 -18
  210. package/src/client/theme/ui/PoweredBy/index.ts +0 -1
  211. package/src/client/theme/ui/PoweredBy/powered-by.css +0 -76
  212. package/src/client/theme/ui/ProgressBar/ProgressBar.css +0 -17
  213. package/src/client/theme/ui/ProgressBar/ProgressBar.tsx +0 -51
  214. package/src/client/theme/ui/ProgressBar/index.ts +0 -1
  215. package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +0 -209
  216. package/src/client/theme/ui/SearchDialog/index.ts +0 -1
  217. package/src/client/theme/ui/SearchDialog/search.css +0 -152
  218. package/src/client/theme/ui/Sidebar/Sidebar.tsx +0 -244
  219. package/src/client/theme/ui/Sidebar/index.ts +0 -1
  220. package/src/client/theme/ui/Sidebar/sidebar.css +0 -230
  221. package/src/client/theme/ui/ThemeToggle/ThemeToggle.tsx +0 -69
  222. package/src/client/theme/ui/ThemeToggle/index.ts +0 -1
  223. package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +0 -136
  224. package/src/client/theme/ui/VersionSwitcher/index.ts +0 -1
  225. package/src/client/utils.ts +0 -49
@@ -1,7 +1,7 @@
1
- import path from "path";
2
- import GithubSlugger from "github-slugger";
3
- import { BoltdocsConfig } from "../config";
4
- import { ParsedDocFile } from "./types";
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
- escapeHtml,
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("\0")
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 || "index.md");
94
+ cleanRoutePath = fileToRoutePath(cleanRelativePath || 'index.md')
92
95
  }
93
96
 
94
- let finalPath = basePath;
97
+ let finalPath = basePath
95
98
  if (version) {
96
- finalPath += "/" + version;
99
+ finalPath += '/' + version
97
100
  }
98
101
  if (locale) {
99
- finalPath += "/" + locale;
102
+ finalPath += '/' + locale
100
103
  }
101
- finalPath += cleanRoutePath === "/" ? "" : cleanRoutePath;
104
+ finalPath += cleanRoutePath === '/' ? '' : cleanRoutePath
102
105
 
103
- if (!finalPath || 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, "$1")
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
- headings.push({ level, text, id });
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 sanitizedTitle = data.title ? data.title : inferredTitle;
135
- let sanitizedDescription = data.description
136
- ? data.description
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
- const summary = content
142
- .replace(/^#+.*$/gm, "") // Remove headers
143
- .replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1") // Simplify links
144
- .replace(/[_*`]/g, "") // Remove formatting
145
- .replace(/\n+/g, " ") // Normalize whitespace
146
- .trim()
147
- .slice(0, 160);
148
- sanitizedDescription = summary;
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
- data.title ||
189
- (cleanDirName ? capitalize(cleanDirName) : ""),
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 "./types";
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
  }
@@ -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, "group" | "groupTitle" | "groupPosition">;
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
  }
@@ -1,21 +1,21 @@
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";
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 "./options";
9
- import { replaceMetaTags } from "./meta";
10
- import { generateSitemap } from "./sitemap";
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 || "Boltdocs";
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, "../client/ssr.js");
33
+ const ssrModulePath = path.resolve(_dirname, '../client/ssr.js')
34
34
  if (!fs.existsSync(ssrModulePath)) {
35
35
  console.error(
36
- "[boltdocs] SSR module not found at",
36
+ '[boltdocs] SSR module not found at',
37
37
  ssrModulePath,
38
- "- Did you build the core package?",
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, "index.html");
45
+ const templatePath = path.join(outDir, 'index.html')
46
46
  if (!fs.existsSync(templatePath)) {
47
- console.warn("[boltdocs] No index.html found in outDir, skipping SSG.");
48
- return;
47
+ console.warn('[boltdocs] No index.html found in outDir, skipping SSG.')
48
+ return
49
49
  }
50
- const template = fs.readFileSync(templatePath, "utf-8");
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: () => {} }; // Mock MDX component
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("<!--app-html-->", appHtml)
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, "index.html"),
82
+ path.join(routeDir, 'index.html'),
83
83
  html,
84
- "utf-8",
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, "sitemap.xml"), sitemap, "utf-8");
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("../cache");
105
- await flushCache();
104
+ const { flushCache } = await import('../cache')
105
+ await flushCache()
106
106
  }
@@ -1,4 +1,4 @@
1
- import { escapeHtml } from "../utils";
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
  }
@@ -1,15 +1,15 @@
1
- import { BoltdocsConfig } from "../config";
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
  }