docs-i18n 0.6.2 → 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.
- package/{src/admin/ui → admin/app}/components/JobDialog.tsx +21 -2
- package/{src/admin/ui → admin/app}/components/JobPanel.tsx +1 -1
- package/{src/admin/ui → admin/app}/components/Preview.tsx +2 -5
- package/{src/admin/ui → admin/app}/lib/api.ts +18 -39
- package/admin/app/routeTree.gen.ts +68 -0
- package/admin/app/router.tsx +23 -0
- package/admin/app/routes/__root.tsx +55 -0
- package/admin/app/routes/index.tsx +416 -0
- package/{src/admin/ui → admin/app}/styles.css +36 -3
- package/admin/package.json +27 -0
- package/admin/server/functions/jobs.ts +53 -0
- package/admin/server/functions/misc.ts +84 -0
- package/{src/admin/server/routes → admin/server/functions}/models.ts +16 -29
- package/admin/server/functions/status.ts +61 -0
- package/admin/server/index.ts +35 -0
- package/admin/server/init.ts +46 -0
- package/{src/admin → admin}/server/services/job-manager.ts +39 -10
- package/{src/admin → admin}/server/services/status.ts +6 -6
- package/admin/tsconfig.json +19 -0
- package/{src/admin → admin}/vite.config.ts +8 -2
- package/dist/{assemble-7H4QCW35.js → assemble-CP2BRYQJ.js} +6 -4
- package/dist/{chunk-A3YQNPKZ.js → chunk-CLYUAWZE.js} +1 -1
- package/dist/{chunk-YN4VJHCQ.js → chunk-JHBSHTXC.js} +1 -1
- package/dist/chunk-L64GJ4OB.js +32 -0
- package/dist/{chunk-SKKZIV3L.js → chunk-PNKVD2UK.js} +1 -29
- package/dist/{chunk-XEOYZUHS.js → chunk-QKIR7RKQ.js} +4 -31
- package/dist/chunk-TRURQFP4.js +31 -0
- package/dist/cli.js +108 -23
- package/dist/index.d.ts +41 -1
- package/dist/index.js +92 -3
- package/dist/{rescan-O5D3CYC2.js → rescan-HXMWFAOC.js} +5 -3
- package/dist/{status-F4MYIAAY.js → status-AGZDXOTZ.js} +4 -2
- package/dist/{translate-ZIVKNAC4.js → translate-A5X6MX4Y.js} +14 -7
- package/dist/upload-XL6KG6S2.js +132 -0
- package/package.json +17 -15
- package/template/app/components/BlogArticle.tsx +159 -0
- package/template/app/components/BlogList.tsx +88 -0
- package/template/app/components/Breadcrumbs.tsx +81 -0
- package/template/app/components/Card.tsx +31 -0
- package/template/app/components/Doc.tsx +191 -0
- package/template/app/components/DocBreadcrumb.tsx +60 -0
- package/template/app/components/DocContainer.tsx +13 -0
- package/template/app/components/DocTitle.tsx +11 -0
- package/template/app/components/DocsLayout.tsx +715 -0
- package/template/app/components/Dropdown.tsx +116 -0
- package/template/app/components/FallbackBanner.tsx +36 -0
- package/template/app/components/Footer.tsx +29 -0
- package/template/app/components/FrameworkSelect.tsx +150 -0
- package/template/app/components/LibraryCard.tsx +178 -0
- package/template/app/components/LocaleSwitcher.tsx +43 -0
- package/template/app/components/Navbar.tsx +430 -0
- package/template/app/components/PostNotFound.tsx +20 -0
- package/template/app/components/SearchButton.tsx +32 -0
- package/template/app/components/Select.tsx +103 -0
- package/template/app/components/Spinner.tsx +18 -0
- package/template/app/components/ThemeProvider.tsx +141 -0
- package/template/app/components/ThemeToggle.tsx +31 -0
- package/template/app/components/Toc.tsx +86 -0
- package/template/app/components/VersionSelect.tsx +118 -0
- package/template/app/components/icons/BSkyIcon.tsx +27 -0
- package/template/app/components/icons/BaseballCapIcon.tsx +25 -0
- package/template/app/components/icons/BrandXIcon.tsx +28 -0
- package/template/app/components/icons/CheckCircleIcon.tsx +28 -0
- package/template/app/components/icons/CogsIcon.tsx +25 -0
- package/template/app/components/icons/DiscordIcon.tsx +24 -0
- package/template/app/components/icons/GithubIcon.tsx +24 -0
- package/template/app/components/icons/GoogleIcon.tsx +24 -0
- package/template/app/components/icons/InstagramIcon.tsx +24 -0
- package/template/app/components/icons/NpmIcon.tsx +26 -0
- package/template/app/components/icons/YinYangIcon.tsx +26 -0
- package/template/app/components/icons/YouTubeIcon.tsx +24 -0
- package/template/app/components/markdown/CodeBlock.tsx +254 -0
- package/template/app/components/markdown/FileTabs.tsx +58 -0
- package/template/app/components/markdown/FrameworkContent.tsx +76 -0
- package/template/app/components/markdown/Markdown.tsx +216 -0
- package/template/app/components/markdown/MarkdownContent.tsx +89 -0
- package/template/app/components/markdown/MarkdownFrameworkHandler.tsx +66 -0
- package/template/app/components/markdown/MarkdownHeadingContext.tsx +35 -0
- package/template/app/components/markdown/MarkdownLink.tsx +46 -0
- package/template/app/components/markdown/MarkdownTabsHandler.tsx +109 -0
- package/template/app/components/markdown/PackageManagerTabs.tsx +95 -0
- package/template/app/components/markdown/Tabs.tsx +139 -0
- package/template/app/components/markdown/index.ts +15 -0
- package/template/app/components/ui/Button.tsx +141 -0
- package/template/app/components/ui/InlineCode.tsx +16 -0
- package/template/app/components/ui/MarkdownImg.tsx +21 -0
- package/template/app/config/frameworks.ts +93 -0
- package/template/app/contexts/SearchContext.tsx +36 -0
- package/template/app/db/index.ts +17 -0
- package/template/app/db/schema.ts +74 -0
- package/template/app/hooks/useClickOutside.ts +106 -0
- package/template/app/routeTree.gen.ts +584 -0
- package/template/app/router.tsx +29 -0
- package/template/app/routes/$lang.$project.$version.docs.$.tsx +128 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.$framework.$.tsx +106 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.$framework.index.tsx +27 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.index.tsx +44 -0
- package/template/app/routes/$lang.$project.$version.docs.index.tsx +27 -0
- package/template/app/routes/$lang.$project.$version.docs.tsx +70 -0
- package/template/app/routes/$lang.$project.$version.tsx +69 -0
- package/template/app/routes/$lang.$project.docs.$.tsx +104 -0
- package/template/app/routes/$lang.$project.docs.index.tsx +20 -0
- package/template/app/routes/$lang.$project.docs.tsx +79 -0
- package/template/app/routes/$lang.$project.tsx +89 -0
- package/template/app/routes/$lang.blog.$.tsx +82 -0
- package/template/app/routes/$lang.blog.index.tsx +56 -0
- package/template/app/routes/$lang.blog.tsx +26 -0
- package/template/app/routes/$lang.docs.$.tsx +100 -0
- package/template/app/routes/$lang.docs.framework.$framework.$.tsx +104 -0
- package/template/app/routes/$lang.docs.framework.$framework.index.tsx +32 -0
- package/template/app/routes/$lang.docs.framework.index.tsx +47 -0
- package/template/app/routes/$lang.docs.index.tsx +20 -0
- package/template/app/routes/$lang.docs.tsx +90 -0
- package/template/app/routes/$lang.tsx +16 -0
- package/template/app/routes/__root.tsx +180 -0
- package/template/app/routes/index.tsx +89 -0
- package/template/app/site.config.ts +182 -0
- package/template/app/styles/app.css +1029 -0
- package/template/app/types/index.ts +77 -0
- package/template/app/utils/blog.server.ts +193 -0
- package/template/app/utils/blog.ts +42 -0
- package/template/app/utils/config.ts +120 -0
- package/template/app/utils/content-loader.ts +400 -0
- package/template/app/utils/dates.ts +29 -0
- package/template/app/utils/docs.server.ts +150 -0
- package/template/app/utils/markdown/filterFrameworkContent.ts +233 -0
- package/template/app/utils/markdown/index.ts +2 -0
- package/template/app/utils/markdown/installCommand.ts +143 -0
- package/template/app/utils/markdown/plugins/collectHeadings.ts +104 -0
- package/template/app/utils/markdown/plugins/extractCodeMeta.ts +57 -0
- package/template/app/utils/markdown/plugins/helpers.ts +33 -0
- package/template/app/utils/markdown/plugins/index.ts +8 -0
- package/template/app/utils/markdown/plugins/parseCommentComponents.ts +103 -0
- package/template/app/utils/markdown/plugins/transformCommentComponents.ts +23 -0
- package/template/app/utils/markdown/plugins/transformFrameworkComponent.ts +217 -0
- package/template/app/utils/markdown/plugins/transformTabsComponent.ts +359 -0
- package/template/app/utils/markdown/processor.ts +75 -0
- package/template/app/utils/site-config.tsx +11 -0
- package/template/app/utils/upload.ts +232 -0
- package/template/app/utils/useLocalStorage.ts +65 -0
- package/template/app/utils/utils.ts +23 -0
- package/template/package.json +54 -0
- package/template/public/favicon.svg +1 -0
- package/template/public/fonts/Inter-latin-ext.woff2 +0 -0
- package/template/public/fonts/Inter-latin.woff2 +0 -0
- package/template/public/images/frameworks/angular-logo.svg +1 -0
- package/template/public/images/frameworks/js-logo.svg +1 -0
- package/template/public/images/frameworks/lit-logo.svg +1 -0
- package/template/public/images/frameworks/preact-logo.svg +6 -0
- package/template/public/images/frameworks/qwik-logo.svg +1 -0
- package/template/public/images/frameworks/react-logo.svg +1 -0
- package/template/public/images/frameworks/solid-logo.svg +1 -0
- package/template/public/images/frameworks/svelte-logo.svg +1 -0
- package/template/public/images/frameworks/vue-logo.svg +4 -0
- package/template/tsconfig.json +24 -0
- package/template/vite.config.ts +43 -0
- package/template/wrangler.jsonc +16 -0
- package/README.md +0 -161
- package/dist/server-73AVSOL5.js +0 -598
- package/src/admin/index.html +0 -13
- package/src/admin/server/index.ts +0 -138
- package/src/admin/server/routes/jobs.ts +0 -113
- package/src/admin/server/routes/status.ts +0 -57
- package/src/admin/ui/App.tsx +0 -332
- package/src/admin/ui/main.tsx +0 -19
- /package/{src/admin/ui → admin/app}/components/FileList.tsx +0 -0
- /package/{src/admin/ui → admin/app}/components/LangGrid.tsx +0 -0
- /package/{src/admin/ui → admin/app}/components/ProgressBar.tsx +0 -0
- /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
|
+
}
|