docs-i18n 0.6.3 → 0.7.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 (169) hide show
  1. package/{src/admin/ui → admin/app}/components/JobDialog.tsx +21 -2
  2. package/{src/admin/ui → admin/app}/components/JobPanel.tsx +1 -1
  3. package/{src/admin/ui → admin/app}/components/Preview.tsx +2 -5
  4. package/{src/admin/ui → admin/app}/lib/api.ts +18 -39
  5. package/admin/app/routeTree.gen.ts +68 -0
  6. package/admin/app/router.tsx +23 -0
  7. package/admin/app/routes/__root.tsx +55 -0
  8. package/admin/app/routes/index.tsx +416 -0
  9. package/{src/admin/ui → admin/app}/styles.css +36 -3
  10. package/admin/package.json +27 -0
  11. package/admin/server/functions/jobs.ts +53 -0
  12. package/admin/server/functions/misc.ts +84 -0
  13. package/{src/admin/server/routes → admin/server/functions}/models.ts +16 -29
  14. package/admin/server/functions/status.ts +61 -0
  15. package/admin/server/index.ts +35 -0
  16. package/admin/server/init.ts +46 -0
  17. package/{src/admin → admin}/server/services/job-manager.ts +39 -10
  18. package/{src/admin → admin}/server/services/status.ts +6 -6
  19. package/admin/tsconfig.json +19 -0
  20. package/{src/admin → admin}/vite.config.ts +8 -2
  21. package/dist/{assemble-7H4QCW35.js → assemble-CP2BRYQJ.js} +6 -4
  22. package/dist/{chunk-A3YQNPKZ.js → chunk-CLYUAWZE.js} +1 -1
  23. package/dist/{chunk-YN4VJHCQ.js → chunk-JHBSHTXC.js} +1 -1
  24. package/dist/chunk-L64GJ4OB.js +32 -0
  25. package/dist/{chunk-SKKZIV3L.js → chunk-PNKVD2UK.js} +1 -29
  26. package/dist/{chunk-XEOYZUHS.js → chunk-QKIR7RKQ.js} +4 -31
  27. package/dist/chunk-TRURQFP4.js +31 -0
  28. package/dist/cli.js +108 -7
  29. package/dist/index.d.ts +41 -1
  30. package/dist/index.js +92 -3
  31. package/dist/{rescan-O5D3CYC2.js → rescan-HXMWFAOC.js} +5 -3
  32. package/dist/{status-F4MYIAAY.js → status-AGZDXOTZ.js} +4 -2
  33. package/dist/{translate-ZIVKNAC4.js → translate-A5X6MX4Y.js} +14 -7
  34. package/dist/upload-XL6KG6S2.js +132 -0
  35. package/package.json +17 -15
  36. package/template/app/components/BlogArticle.tsx +159 -0
  37. package/template/app/components/BlogList.tsx +88 -0
  38. package/template/app/components/Breadcrumbs.tsx +81 -0
  39. package/template/app/components/Card.tsx +31 -0
  40. package/template/app/components/Doc.tsx +191 -0
  41. package/template/app/components/DocBreadcrumb.tsx +60 -0
  42. package/template/app/components/DocContainer.tsx +13 -0
  43. package/template/app/components/DocTitle.tsx +11 -0
  44. package/template/app/components/DocsLayout.tsx +715 -0
  45. package/template/app/components/Dropdown.tsx +116 -0
  46. package/template/app/components/FallbackBanner.tsx +36 -0
  47. package/template/app/components/Footer.tsx +29 -0
  48. package/template/app/components/FrameworkSelect.tsx +150 -0
  49. package/template/app/components/LibraryCard.tsx +178 -0
  50. package/template/app/components/LocaleSwitcher.tsx +43 -0
  51. package/template/app/components/Navbar.tsx +430 -0
  52. package/template/app/components/PostNotFound.tsx +20 -0
  53. package/template/app/components/SearchButton.tsx +32 -0
  54. package/template/app/components/Select.tsx +103 -0
  55. package/template/app/components/Spinner.tsx +18 -0
  56. package/template/app/components/ThemeProvider.tsx +141 -0
  57. package/template/app/components/ThemeToggle.tsx +31 -0
  58. package/template/app/components/Toc.tsx +86 -0
  59. package/template/app/components/VersionSelect.tsx +118 -0
  60. package/template/app/components/icons/BSkyIcon.tsx +27 -0
  61. package/template/app/components/icons/BaseballCapIcon.tsx +25 -0
  62. package/template/app/components/icons/BrandXIcon.tsx +28 -0
  63. package/template/app/components/icons/CheckCircleIcon.tsx +28 -0
  64. package/template/app/components/icons/CogsIcon.tsx +25 -0
  65. package/template/app/components/icons/DiscordIcon.tsx +24 -0
  66. package/template/app/components/icons/GithubIcon.tsx +24 -0
  67. package/template/app/components/icons/GoogleIcon.tsx +24 -0
  68. package/template/app/components/icons/InstagramIcon.tsx +24 -0
  69. package/template/app/components/icons/NpmIcon.tsx +26 -0
  70. package/template/app/components/icons/YinYangIcon.tsx +26 -0
  71. package/template/app/components/icons/YouTubeIcon.tsx +24 -0
  72. package/template/app/components/markdown/CodeBlock.tsx +254 -0
  73. package/template/app/components/markdown/FileTabs.tsx +58 -0
  74. package/template/app/components/markdown/FrameworkContent.tsx +76 -0
  75. package/template/app/components/markdown/Markdown.tsx +216 -0
  76. package/template/app/components/markdown/MarkdownContent.tsx +89 -0
  77. package/template/app/components/markdown/MarkdownFrameworkHandler.tsx +66 -0
  78. package/template/app/components/markdown/MarkdownHeadingContext.tsx +35 -0
  79. package/template/app/components/markdown/MarkdownLink.tsx +46 -0
  80. package/template/app/components/markdown/MarkdownTabsHandler.tsx +109 -0
  81. package/template/app/components/markdown/PackageManagerTabs.tsx +95 -0
  82. package/template/app/components/markdown/Tabs.tsx +139 -0
  83. package/template/app/components/markdown/index.ts +15 -0
  84. package/template/app/components/ui/Button.tsx +141 -0
  85. package/template/app/components/ui/InlineCode.tsx +16 -0
  86. package/template/app/components/ui/MarkdownImg.tsx +21 -0
  87. package/template/app/config/frameworks.ts +93 -0
  88. package/template/app/contexts/SearchContext.tsx +36 -0
  89. package/template/app/db/index.ts +17 -0
  90. package/template/app/db/schema.ts +74 -0
  91. package/template/app/hooks/useClickOutside.ts +106 -0
  92. package/template/app/routeTree.gen.ts +584 -0
  93. package/template/app/router.tsx +29 -0
  94. package/template/app/routes/$lang.$project.$version.docs.$.tsx +128 -0
  95. package/template/app/routes/$lang.$project.$version.docs.framework.$framework.$.tsx +106 -0
  96. package/template/app/routes/$lang.$project.$version.docs.framework.$framework.index.tsx +27 -0
  97. package/template/app/routes/$lang.$project.$version.docs.framework.index.tsx +44 -0
  98. package/template/app/routes/$lang.$project.$version.docs.index.tsx +27 -0
  99. package/template/app/routes/$lang.$project.$version.docs.tsx +70 -0
  100. package/template/app/routes/$lang.$project.$version.tsx +69 -0
  101. package/template/app/routes/$lang.$project.docs.$.tsx +104 -0
  102. package/template/app/routes/$lang.$project.docs.index.tsx +20 -0
  103. package/template/app/routes/$lang.$project.docs.tsx +79 -0
  104. package/template/app/routes/$lang.$project.tsx +89 -0
  105. package/template/app/routes/$lang.blog.$.tsx +82 -0
  106. package/template/app/routes/$lang.blog.index.tsx +56 -0
  107. package/template/app/routes/$lang.blog.tsx +26 -0
  108. package/template/app/routes/$lang.docs.$.tsx +100 -0
  109. package/template/app/routes/$lang.docs.framework.$framework.$.tsx +104 -0
  110. package/template/app/routes/$lang.docs.framework.$framework.index.tsx +32 -0
  111. package/template/app/routes/$lang.docs.framework.index.tsx +47 -0
  112. package/template/app/routes/$lang.docs.index.tsx +20 -0
  113. package/template/app/routes/$lang.docs.tsx +90 -0
  114. package/template/app/routes/$lang.tsx +16 -0
  115. package/template/app/routes/__root.tsx +180 -0
  116. package/template/app/routes/index.tsx +89 -0
  117. package/template/app/site.config.ts +182 -0
  118. package/template/app/styles/app.css +1029 -0
  119. package/template/app/types/index.ts +77 -0
  120. package/template/app/utils/blog.server.ts +193 -0
  121. package/template/app/utils/blog.ts +42 -0
  122. package/template/app/utils/config.ts +120 -0
  123. package/template/app/utils/content-loader.ts +400 -0
  124. package/template/app/utils/dates.ts +29 -0
  125. package/template/app/utils/docs.server.ts +150 -0
  126. package/template/app/utils/markdown/filterFrameworkContent.ts +233 -0
  127. package/template/app/utils/markdown/index.ts +2 -0
  128. package/template/app/utils/markdown/installCommand.ts +143 -0
  129. package/template/app/utils/markdown/plugins/collectHeadings.ts +104 -0
  130. package/template/app/utils/markdown/plugins/extractCodeMeta.ts +57 -0
  131. package/template/app/utils/markdown/plugins/helpers.ts +33 -0
  132. package/template/app/utils/markdown/plugins/index.ts +8 -0
  133. package/template/app/utils/markdown/plugins/parseCommentComponents.ts +103 -0
  134. package/template/app/utils/markdown/plugins/transformCommentComponents.ts +23 -0
  135. package/template/app/utils/markdown/plugins/transformFrameworkComponent.ts +217 -0
  136. package/template/app/utils/markdown/plugins/transformTabsComponent.ts +359 -0
  137. package/template/app/utils/markdown/processor.ts +75 -0
  138. package/template/app/utils/site-config.tsx +11 -0
  139. package/template/app/utils/upload.ts +232 -0
  140. package/template/app/utils/useLocalStorage.ts +65 -0
  141. package/template/app/utils/utils.ts +23 -0
  142. package/template/package.json +54 -0
  143. package/template/public/favicon.svg +1 -0
  144. package/template/public/fonts/Inter-latin-ext.woff2 +0 -0
  145. package/template/public/fonts/Inter-latin.woff2 +0 -0
  146. package/template/public/images/frameworks/angular-logo.svg +1 -0
  147. package/template/public/images/frameworks/js-logo.svg +1 -0
  148. package/template/public/images/frameworks/lit-logo.svg +1 -0
  149. package/template/public/images/frameworks/preact-logo.svg +6 -0
  150. package/template/public/images/frameworks/qwik-logo.svg +1 -0
  151. package/template/public/images/frameworks/react-logo.svg +1 -0
  152. package/template/public/images/frameworks/solid-logo.svg +1 -0
  153. package/template/public/images/frameworks/svelte-logo.svg +1 -0
  154. package/template/public/images/frameworks/vue-logo.svg +4 -0
  155. package/template/tsconfig.json +24 -0
  156. package/template/vite.config.ts +43 -0
  157. package/template/wrangler.jsonc +16 -0
  158. package/README.md +0 -161
  159. package/dist/server-73AVSOL5.js +0 -598
  160. package/src/admin/index.html +0 -13
  161. package/src/admin/server/index.ts +0 -138
  162. package/src/admin/server/routes/jobs.ts +0 -113
  163. package/src/admin/server/routes/status.ts +0 -57
  164. package/src/admin/ui/App.tsx +0 -332
  165. package/src/admin/ui/main.tsx +0 -19
  166. /package/{src/admin/ui → admin/app}/components/FileList.tsx +0 -0
  167. /package/{src/admin/ui → admin/app}/components/LangGrid.tsx +0 -0
  168. /package/{src/admin/ui → admin/app}/components/ProgressBar.tsx +0 -0
  169. /package/{src/admin/ui → admin/app}/lib/flags.ts +0 -0
@@ -0,0 +1,217 @@
1
+ import { toString } from 'hast-util-to-string'
2
+ import { visit } from 'unist-util-visit'
3
+
4
+ import { normalizeComponentName } from './helpers'
5
+
6
+ type HastNode = {
7
+ type: string
8
+ tagName?: string
9
+ properties?: Record<string, unknown>
10
+ children?: HastNode[]
11
+ value?: string
12
+ }
13
+
14
+ type FrameworkCodeBlock = {
15
+ title: string
16
+ code: string
17
+ language: string
18
+ }
19
+
20
+ type FrameworkExtraction = {
21
+ codeBlocksByFramework: Record<string, FrameworkCodeBlock[]>
22
+ contentByFramework: Record<string, HastNode[]>
23
+ }
24
+
25
+ /**
26
+ * Extract code block data (language, title, code) from a <pre> element.
27
+ */
28
+ function extractCodeBlockData(preNode: HastNode): {
29
+ language: string
30
+ title: string
31
+ code: string
32
+ } | null {
33
+ const codeNode = preNode.children?.find(
34
+ (c: HastNode) => c.type === 'element' && c.tagName === 'code',
35
+ )
36
+
37
+ if (!codeNode) return null
38
+
39
+ // Extract language from className
40
+ let language = 'plaintext'
41
+ const className = codeNode.properties?.className
42
+ if (Array.isArray(className)) {
43
+ const langClass = className.find((c) => String(c).startsWith('language-'))
44
+ if (langClass) {
45
+ language = String(langClass).replace('language-', '')
46
+ }
47
+ }
48
+
49
+ // Extract title from data attributes
50
+ let title = ''
51
+ const props = preNode.properties || {}
52
+ if (typeof props['dataCodeTitle'] === 'string') {
53
+ title = props['dataCodeTitle'] as string
54
+ } else if (typeof props['data-code-title'] === 'string') {
55
+ title = props['data-code-title']
56
+ } else if (typeof props['dataFilename'] === 'string') {
57
+ title = props['dataFilename'] as string
58
+ } else if (typeof props['data-filename'] === 'string') {
59
+ title = props['data-filename']
60
+ }
61
+
62
+ // Extract code text
63
+ const extractText = (nodes: HastNode[]): string => {
64
+ let text = ''
65
+ for (const node of nodes) {
66
+ if (node.type === 'text' && node.value) {
67
+ text += node.value
68
+ } else if (node.type === 'element' && node.children) {
69
+ text += extractText(node.children)
70
+ }
71
+ }
72
+ return text
73
+ }
74
+ const code = extractText(codeNode.children || [])
75
+
76
+ return { language, title, code }
77
+ }
78
+
79
+ function extractFrameworkData(node: HastNode): FrameworkExtraction | null {
80
+ const children = node.children ?? []
81
+ const codeBlocksByFramework: Record<string, FrameworkCodeBlock[]> = {}
82
+ const contentByFramework: Record<string, HastNode[]> = {}
83
+
84
+ // First pass: find the first H1 to determine the first framework
85
+ let firstFramework: string | null = null
86
+ for (const child of children) {
87
+ if (child.type === 'element' && child.tagName === 'h1') {
88
+ firstFramework = toString(child as any)
89
+ .trim()
90
+ .toLowerCase()
91
+ break
92
+ }
93
+ }
94
+
95
+ // If no H1 found at all, return null
96
+ if (!firstFramework) {
97
+ return null
98
+ }
99
+
100
+ // Second pass: collect content
101
+ let currentFramework: string | null = firstFramework // Start with first framework for content before first H1
102
+
103
+ // Initialize the first framework
104
+ contentByFramework[firstFramework] = []
105
+ codeBlocksByFramework[firstFramework] = []
106
+
107
+ for (const child of children) {
108
+ // Check if this is an H1 heading (framework divider)
109
+ if (child.type === 'element' && child.tagName === 'h1') {
110
+ // Extract framework name from H1 text
111
+ currentFramework = toString(child as any)
112
+ .trim()
113
+ .toLowerCase()
114
+
115
+ // Initialize arrays for this framework
116
+ if (currentFramework && !contentByFramework[currentFramework]) {
117
+ contentByFramework[currentFramework] = []
118
+ codeBlocksByFramework[currentFramework] = []
119
+ }
120
+ // Don't include the H1 itself in content - it's just a divider
121
+ continue
122
+ }
123
+
124
+ if (!currentFramework) continue
125
+
126
+ // Create a shallow copy of the node
127
+ const contentNode = Object.assign({}, child) as HastNode
128
+
129
+ // Mark all headings (h2-h6) with framework attribute so they appear in TOC only for this framework
130
+ if (
131
+ contentNode.type === 'element' &&
132
+ contentNode.tagName &&
133
+ /^h[2-6]$/.test(contentNode.tagName)
134
+ ) {
135
+ contentNode.properties = (contentNode.properties || {}) as Record<
136
+ string,
137
+ unknown
138
+ >
139
+ contentNode.properties['data-framework'] = currentFramework
140
+ }
141
+
142
+ contentByFramework[currentFramework].push(contentNode)
143
+
144
+ // Extract code blocks for this framework
145
+ if (contentNode.type === 'element' && contentNode.tagName === 'pre') {
146
+ const codeBlockData = extractCodeBlockData(contentNode)
147
+ if (codeBlockData) {
148
+ codeBlocksByFramework[currentFramework].push(codeBlockData)
149
+ }
150
+ }
151
+ }
152
+
153
+ // Return null if no frameworks found
154
+ if (Object.keys(contentByFramework).length === 0) {
155
+ return null
156
+ }
157
+
158
+ return { codeBlocksByFramework, contentByFramework }
159
+ }
160
+
161
+ export function transformFrameworkComponent(node: HastNode) {
162
+ const result = extractFrameworkData(node)
163
+
164
+ if (!result) {
165
+ return
166
+ }
167
+
168
+ node.properties = node.properties || {}
169
+ node.properties['data-framework-meta'] = JSON.stringify({
170
+ codeBlocksByFramework: Object.fromEntries(
171
+ Object.entries(result.codeBlocksByFramework).map(([fw, blocks]) => [
172
+ fw,
173
+ blocks.map((b) => ({
174
+ title: b.title,
175
+ code: b.code,
176
+ language: b.language,
177
+ })),
178
+ ]),
179
+ ),
180
+ })
181
+
182
+ // Store available frameworks for the component
183
+ const availableFrameworks = Object.keys(result.contentByFramework)
184
+ node.properties['data-available-frameworks'] =
185
+ JSON.stringify(availableFrameworks)
186
+
187
+ node.children = availableFrameworks.map((fw) => {
188
+ const content = result.contentByFramework[fw] || []
189
+ return {
190
+ type: 'element',
191
+ tagName: 'md-framework-panel',
192
+ properties: {
193
+ 'data-framework': fw,
194
+ },
195
+ children: content,
196
+ }
197
+ })
198
+ }
199
+
200
+ /**
201
+ * Rehype plugin to transform framework components in the AST.
202
+ * Visits the tree and calls transformFrameworkComponent for each framework component found.
203
+ */
204
+ export const rehypeTransformFrameworkComponents = () => {
205
+ return (tree: any) => {
206
+ visit(tree, 'element', (node) => {
207
+ if (node.tagName !== 'md-comment-component') {
208
+ return
209
+ }
210
+
211
+ const component = String(node.properties?.['data-component'] ?? '')
212
+ if (normalizeComponentName(component) === 'framework') {
213
+ transformFrameworkComponent(node)
214
+ }
215
+ })
216
+ }
217
+ }
@@ -0,0 +1,359 @@
1
+ import { toString } from 'hast-util-to-string'
2
+
3
+ import { headingLevel, isHeading, slugify } from './helpers'
4
+
5
+ export type VariantHandler = (
6
+ node: HastNode,
7
+ attributes: Record<string, string>,
8
+ ) => boolean
9
+
10
+ type InstallMode = 'install' | 'dev-install' | 'local-install'
11
+
12
+ type HastNode = {
13
+ type: string
14
+ tagName: string
15
+ properties?: Record<string, unknown>
16
+ children?: HastNode[]
17
+ }
18
+
19
+ type TabDescriptor = {
20
+ slug: string
21
+ name: string
22
+ }
23
+
24
+ type TabExtraction = {
25
+ tabs: TabDescriptor[]
26
+ panels: HastNode[][]
27
+ }
28
+
29
+ type PackageManagerExtraction = {
30
+ packagesByFramework: Record<string, string[][]>
31
+ mode: InstallMode
32
+ }
33
+
34
+ type FilesExtraction = {
35
+ files: Array<{
36
+ title: string
37
+ code: string
38
+ language: string
39
+ preNode: HastNode
40
+ }>
41
+ }
42
+
43
+ function parseAttributes(node: HastNode): Record<string, string> {
44
+ const rawAttributes = node.properties?.['data-attributes']
45
+ if (typeof rawAttributes === 'string') {
46
+ try {
47
+ return JSON.parse(rawAttributes)
48
+ } catch {
49
+ return {}
50
+ }
51
+ }
52
+ return {}
53
+ }
54
+
55
+ function resolveMode(attributes: Record<string, string>): InstallMode {
56
+ const mode = attributes.mode?.toLowerCase()
57
+ if (mode === 'dev-install') return 'dev-install'
58
+ if (mode === 'local-install') return 'local-install'
59
+ return 'install'
60
+ }
61
+
62
+ function normalizeFrameworkKey(key: string): string {
63
+ return key.trim().toLowerCase()
64
+ }
65
+
66
+ // Helper to extract text from nodes (used for code content)
67
+ function extractText(nodes: any[]): string {
68
+ let text = ''
69
+ for (const node of nodes) {
70
+ if (node.type === 'text') {
71
+ text += node.value
72
+ } else if (node.type === 'element' && node.children) {
73
+ text += extractText(node.children)
74
+ }
75
+ }
76
+ return text
77
+ }
78
+
79
+ /**
80
+ * Parse a line like "react: @tanstack/react-query @tanstack/react-query-devtools"
81
+ * Returns { framework: 'react', packages: '@tanstack/react-query @tanstack/react-query-devtools' }
82
+ */
83
+ function parseFrameworkLine(text: string): {
84
+ framework: string
85
+ packages: string[]
86
+ } | null {
87
+ const colonIndex = text.indexOf(':')
88
+ if (colonIndex === -1) {
89
+ return null
90
+ }
91
+
92
+ const framework = normalizeFrameworkKey(text.slice(0, colonIndex))
93
+ const packagesStr = text.slice(colonIndex + 1).trim()
94
+ const packages = packagesStr.split(/\s+/).filter(Boolean)
95
+
96
+ if (!framework || packages.length === 0) {
97
+ return null
98
+ }
99
+
100
+ return { framework, packages }
101
+ }
102
+
103
+ function extractPackageManagerData(
104
+ node: HastNode,
105
+ mode: InstallMode,
106
+ ): PackageManagerExtraction | null {
107
+ const children = node.children ?? []
108
+ const packagesByFramework: Record<string, string[][]> = {}
109
+
110
+ const allText = extractText(children)
111
+ const lines = allText.split('\n')
112
+
113
+ for (const line of lines) {
114
+ const trimmed = line.trim()
115
+ if (!trimmed) continue
116
+
117
+ const parsed = parseFrameworkLine(trimmed)
118
+ if (parsed) {
119
+ // Each line becomes a separate entry (array of packages)
120
+ // Multiple packages on same line = install together
121
+ // Multiple lines = install separately
122
+ if (packagesByFramework[parsed.framework]) {
123
+ packagesByFramework[parsed.framework].push(parsed.packages)
124
+ } else {
125
+ packagesByFramework[parsed.framework] = [parsed.packages]
126
+ }
127
+ }
128
+ }
129
+
130
+ if (Object.keys(packagesByFramework).length === 0) {
131
+ return null
132
+ }
133
+
134
+ return { packagesByFramework, mode }
135
+ }
136
+
137
+ /**
138
+ * Extract code block data (language, title, code) from a <pre> element.
139
+ * Extracts title from data-code-title (set by rehypeCodeMeta).
140
+ */
141
+ function extractCodeBlockData(preNode: HastNode): {
142
+ language: string
143
+ title: string
144
+ code: string
145
+ } | null {
146
+ // Find the <code> child
147
+ const codeNode = preNode.children?.find(
148
+ (c: HastNode) => c.type === 'element' && c.tagName === 'code',
149
+ )
150
+
151
+ if (!codeNode) return null
152
+
153
+ // Extract language from className
154
+ let language = 'plaintext'
155
+ const className = codeNode.properties?.className
156
+ if (Array.isArray(className)) {
157
+ const langClass = className.find((c) => String(c).startsWith('language-'))
158
+ if (langClass) {
159
+ language = String(langClass).replace('language-', '')
160
+ }
161
+ }
162
+
163
+ let title = ''
164
+ const props = preNode.properties || {}
165
+ if (typeof props['dataCodeTitle'] === 'string') {
166
+ title = props['dataCodeTitle'] as string
167
+ } else if (typeof props['data-code-title'] === 'string') {
168
+ title = props['data-code-title']
169
+ } else if (typeof props['dataFilename'] === 'string') {
170
+ title = props['dataFilename'] as string
171
+ } else if (typeof props['data-filename'] === 'string') {
172
+ title = props['data-filename']
173
+ }
174
+
175
+ // Extract code content
176
+ const code = extractText(codeNode.children || [])
177
+
178
+ return { language, title, code }
179
+ }
180
+
181
+ /**
182
+ * Extract files data for variant="files" tabs.
183
+ * Parses consecutive code blocks and creates file tabs.
184
+ */
185
+ function extractFilesData(node: HastNode): FilesExtraction | null {
186
+ const children = node.children ?? []
187
+ const files: FilesExtraction['files'] = []
188
+
189
+ for (const child of children) {
190
+ if (child.type === 'element' && child.tagName === 'pre') {
191
+ const codeBlockData = extractCodeBlockData(child)
192
+ if (!codeBlockData) continue
193
+
194
+ files.push({
195
+ title: codeBlockData.title || 'Untitled',
196
+ code: codeBlockData.code,
197
+ language: codeBlockData.language,
198
+ preNode: child,
199
+ })
200
+ }
201
+ }
202
+
203
+ if (files.length === 0) {
204
+ return null
205
+ }
206
+
207
+ return { files }
208
+ }
209
+
210
+ function extractTabPanels(node: HastNode): TabExtraction | null {
211
+ const children = node.children ?? []
212
+ const headings = children.filter(isHeading)
213
+
214
+ let sectionStarted = false
215
+ let largestHeadingLevel = Infinity
216
+ headings.forEach((heading: HastNode) => {
217
+ largestHeadingLevel = Math.min(largestHeadingLevel, headingLevel(heading))
218
+ })
219
+
220
+ const tabs: TabDescriptor[] = []
221
+ const panels: HastNode[][] = []
222
+ let currentPanel: HastNode[] | null = null
223
+
224
+ children.forEach((child: any) => {
225
+ if (isHeading(child)) {
226
+ const level = headingLevel(child)
227
+ if (!sectionStarted) {
228
+ if (level !== largestHeadingLevel) {
229
+ return
230
+ }
231
+ sectionStarted = true
232
+ }
233
+
234
+ if (level === largestHeadingLevel) {
235
+ if (currentPanel) {
236
+ panels.push(currentPanel)
237
+ }
238
+
239
+ const headingId =
240
+ typeof child.properties?.id === 'string'
241
+ ? child.properties.id
242
+ : slugify(toString(child as any), `tab-${tabs.length + 1}`)
243
+
244
+ tabs.push({
245
+ slug: headingId,
246
+ name: toString(child as any),
247
+ })
248
+
249
+ currentPanel = []
250
+ return
251
+ }
252
+ }
253
+
254
+ if (sectionStarted) {
255
+ if (!currentPanel) {
256
+ currentPanel = []
257
+ }
258
+ currentPanel.push(child)
259
+ }
260
+ })
261
+
262
+ if (currentPanel) {
263
+ panels.push(currentPanel)
264
+ }
265
+
266
+ if (!tabs.length) {
267
+ return null
268
+ }
269
+
270
+ return { tabs, panels }
271
+ }
272
+
273
+ export function transformTabsComponent(node: HastNode) {
274
+ const attributes = parseAttributes(node)
275
+ const variant = attributes.variant?.toLowerCase()
276
+
277
+ // Handle package-manager variant
278
+ if (variant === 'package-manager' || variant === 'package-managers') {
279
+ const mode = resolveMode(attributes)
280
+ const result = extractPackageManagerData(node, mode)
281
+
282
+ if (!result) {
283
+ return
284
+ }
285
+
286
+ // Remove children so package managers don't show up in TOC
287
+ node.children = []
288
+
289
+ // Store metadata for the React component
290
+ node.properties = node.properties || {}
291
+ node.properties['data-package-manager-meta'] = JSON.stringify({
292
+ packagesByFramework: result.packagesByFramework,
293
+ mode: result.mode,
294
+ })
295
+ return
296
+ }
297
+
298
+ // Handle files variant
299
+ if (variant === 'files') {
300
+ const result = extractFilesData(node)
301
+
302
+ if (!result) {
303
+ return
304
+ }
305
+
306
+ // Store metadata for the React component (without preNodes to avoid circular refs)
307
+ node.properties = node.properties || {}
308
+ node.properties['data-files-meta'] = JSON.stringify({
309
+ files: result.files.map((f) => ({
310
+ title: f.title,
311
+ code: f.code,
312
+ language: f.language,
313
+ })),
314
+ })
315
+
316
+ // Create tab headings from file titles
317
+ const tabs = result.files.map((file, index) => ({
318
+ slug: `file-${index}`,
319
+ name: file.title,
320
+ }))
321
+
322
+ node.properties['data-attributes'] = JSON.stringify({ tabs })
323
+
324
+ // Create panel elements with original preNodes
325
+ node.children = result.files.map((file, index) => ({
326
+ type: 'element',
327
+ tagName: 'md-tab-panel',
328
+ properties: {
329
+ 'data-tab-slug': `file-${index}`,
330
+ 'data-tab-index': String(index),
331
+ },
332
+ // Use the original preNode which already has data-code-title from rehypeCodeMeta
333
+ children: [file.preNode],
334
+ }))
335
+ return
336
+ }
337
+
338
+ // Handle default tabs variant
339
+ const result = extractTabPanels(node)
340
+ if (!result) {
341
+ return
342
+ }
343
+
344
+ const panelElements = result.panels.map((panelChildren, index) => ({
345
+ type: 'element',
346
+ tagName: 'md-tab-panel',
347
+ properties: {
348
+ 'data-tab-slug': result.tabs[index]?.slug ?? `tab-${index + 1}`,
349
+ 'data-tab-index': String(index),
350
+ },
351
+ children: panelChildren,
352
+ }))
353
+
354
+ node.properties = {
355
+ ...node.properties,
356
+ 'data-attributes': JSON.stringify({ tabs: result.tabs }),
357
+ }
358
+ node.children = panelElements
359
+ }
@@ -0,0 +1,75 @@
1
+ import { unified } from 'unified'
2
+ import remarkParse from 'remark-parse'
3
+ import remarkGfm from 'remark-gfm'
4
+ import remarkRehype from 'remark-rehype'
5
+ import rehypeCallouts from 'rehype-callouts'
6
+ import rehypeRaw from 'rehype-raw'
7
+ import rehypeSlug from 'rehype-slug'
8
+ import rehypeAutolinkHeadings from 'rehype-autolink-headings'
9
+ import rehypeStringify from 'rehype-stringify'
10
+
11
+ import {
12
+ rehypeCollectHeadings,
13
+ rehypeParseCommentComponents,
14
+ rehypeTransformCommentComponents,
15
+ rehypeTransformFrameworkComponents,
16
+ type MarkdownHeading,
17
+ } from '~/utils/markdown/plugins'
18
+ import { extractCodeMeta } from '~/utils/markdown/plugins/extractCodeMeta'
19
+
20
+ export type { MarkdownHeading } from '~/utils/markdown/plugins'
21
+
22
+ export type MarkdownRenderResult = {
23
+ markup: string
24
+ headings: MarkdownHeading[]
25
+ }
26
+
27
+ export function renderMarkdown(content: string): MarkdownRenderResult {
28
+ const headings: MarkdownHeading[] = []
29
+
30
+ const processor = unified()
31
+ .use(remarkParse)
32
+ .use(remarkGfm)
33
+ .use(remarkRehype, { allowDangerousHtml: true })
34
+ .use(extractCodeMeta)
35
+ .use(rehypeRaw)
36
+ .use(rehypeParseCommentComponents)
37
+ .use(rehypeCallouts, {
38
+ theme: 'github',
39
+ props: {
40
+ containerProps: (_node: any, type: string) => ({
41
+ className: `markdown-alert markdown-alert-${type}`,
42
+ }),
43
+ titleIconProps: () => ({
44
+ className: 'octicon octicon-info mr-2',
45
+ }),
46
+ titleProps: () => ({
47
+ className: 'markdown-alert-title',
48
+ }),
49
+ titleTextProps: () => ({
50
+ className: 'markdown-alert-title',
51
+ }),
52
+ contentProps: () => ({
53
+ className: 'markdown-alert-content',
54
+ }),
55
+ },
56
+ } as any)
57
+ .use(rehypeSlug)
58
+ .use(rehypeTransformFrameworkComponents)
59
+ .use(rehypeTransformCommentComponents)
60
+ .use(rehypeAutolinkHeadings, {
61
+ behavior: 'wrap',
62
+ properties: {
63
+ className: ['anchor-heading'],
64
+ },
65
+ })
66
+ .use(() => rehypeCollectHeadings(headings))
67
+ .use(rehypeStringify)
68
+
69
+ const file = processor.processSync(content)
70
+
71
+ return {
72
+ markup: String(file),
73
+ headings,
74
+ }
75
+ }
@@ -0,0 +1,11 @@
1
+ import { createContext, useContext } from 'react'
2
+ import type { SiteConfig } from '~/types'
3
+
4
+ const SiteConfigContext = createContext<SiteConfig | null>(null)
5
+ export const SiteConfigProvider = SiteConfigContext.Provider
6
+
7
+ export function useSiteConfig(): SiteConfig {
8
+ const config = useContext(SiteConfigContext)
9
+ if (!config) throw new Error('SiteConfig not provided')
10
+ return config
11
+ }