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,24 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
export function YouTubeIcon({
|
|
4
|
+
className,
|
|
5
|
+
width = '1em',
|
|
6
|
+
height = '1em',
|
|
7
|
+
...props
|
|
8
|
+
}: React.SVGProps<SVGSVGElement>) {
|
|
9
|
+
return (
|
|
10
|
+
<svg
|
|
11
|
+
viewBox="0 0 461 461"
|
|
12
|
+
fill="currentColor"
|
|
13
|
+
width={width}
|
|
14
|
+
height={height}
|
|
15
|
+
role="img"
|
|
16
|
+
aria-hidden={props['aria-label'] ? undefined : true}
|
|
17
|
+
className={className}
|
|
18
|
+
{...props}
|
|
19
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
20
|
+
>
|
|
21
|
+
<path d="M365.257,67.393H95.744C42.866,67.393,0,110.259,0,163.137v134.728 c0,52.878,42.866,95.744,95.744,95.744h269.513c52.878,0,95.744-42.866,95.744-95.744V163.137 C461.001,110.259,418.135,67.393,365.257,67.393z M300.506,237.056l-126.06,60.123c-3.359,1.602-7.239-0.847-7.239-4.568V168.607 c0-3.774,3.982-6.22,7.348-4.514l126.06,63.881C304.363,229.873,304.298,235.248,300.506,237.056z" />
|
|
22
|
+
</svg>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { twMerge } from 'tailwind-merge'
|
|
3
|
+
import { Copy } from 'lucide-react'
|
|
4
|
+
import type { Mermaid } from 'mermaid'
|
|
5
|
+
import { transformerNotationDiff } from '@shikijs/transformers'
|
|
6
|
+
import type { HighlighterGeneric } from 'shiki'
|
|
7
|
+
import { createHighlighter } from 'shiki'
|
|
8
|
+
import { Button } from '../ui/Button'
|
|
9
|
+
|
|
10
|
+
// Language aliases mapping
|
|
11
|
+
const LANG_ALIASES: Record<string, string> = {
|
|
12
|
+
ts: 'typescript',
|
|
13
|
+
js: 'javascript',
|
|
14
|
+
sh: 'bash',
|
|
15
|
+
shell: 'bash',
|
|
16
|
+
console: 'bash',
|
|
17
|
+
zsh: 'bash',
|
|
18
|
+
cmd: 'bash',
|
|
19
|
+
md: 'markdown',
|
|
20
|
+
txt: 'plaintext',
|
|
21
|
+
text: 'plaintext',
|
|
22
|
+
yml: 'yaml',
|
|
23
|
+
json5: 'jsonc',
|
|
24
|
+
eslintrc: 'jsonc',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Lazy highlighter singleton
|
|
28
|
+
let highlighterPromise: Promise<HighlighterGeneric<any, any>> | null = null
|
|
29
|
+
let mermaidInstance: Mermaid | null = null
|
|
30
|
+
const genSvgMap = new Map<string, string>()
|
|
31
|
+
const failedLanguages = new Set<string>()
|
|
32
|
+
|
|
33
|
+
async function getHighlighter(language: string): Promise<{
|
|
34
|
+
highlighter: HighlighterGeneric<any, any>
|
|
35
|
+
effectiveLang: string
|
|
36
|
+
}> {
|
|
37
|
+
if (!highlighterPromise) {
|
|
38
|
+
highlighterPromise = createHighlighter({
|
|
39
|
+
themes: ['github-light', 'aurora-x'],
|
|
40
|
+
langs: [
|
|
41
|
+
'typescript',
|
|
42
|
+
'javascript',
|
|
43
|
+
'tsx',
|
|
44
|
+
'jsx',
|
|
45
|
+
'bash',
|
|
46
|
+
'json',
|
|
47
|
+
'html',
|
|
48
|
+
'css',
|
|
49
|
+
'markdown',
|
|
50
|
+
'plaintext',
|
|
51
|
+
'toml',
|
|
52
|
+
'yaml',
|
|
53
|
+
'sql',
|
|
54
|
+
'diff',
|
|
55
|
+
'vue',
|
|
56
|
+
'svelte',
|
|
57
|
+
'scss',
|
|
58
|
+
'jsonc',
|
|
59
|
+
'vue-html',
|
|
60
|
+
'angular-html',
|
|
61
|
+
'angular-ts',
|
|
62
|
+
],
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const highlighter = await highlighterPromise
|
|
67
|
+
const normalizedLang = LANG_ALIASES[language] || language
|
|
68
|
+
const langToLoad = normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang
|
|
69
|
+
|
|
70
|
+
// Return plaintext for known failed languages
|
|
71
|
+
if (failedLanguages.has(langToLoad)) {
|
|
72
|
+
return { highlighter, effectiveLang: 'plaintext' }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Load language if not already loaded
|
|
76
|
+
if (!highlighter.getLoadedLanguages().includes(langToLoad as any)) {
|
|
77
|
+
try {
|
|
78
|
+
await highlighter.loadLanguage(langToLoad as any)
|
|
79
|
+
} catch {
|
|
80
|
+
console.warn(`Shiki: Language "${langToLoad}" not found, using plaintext`)
|
|
81
|
+
failedLanguages.add(langToLoad)
|
|
82
|
+
return { highlighter, effectiveLang: 'plaintext' }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { highlighter, effectiveLang: langToLoad }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Lazy load mermaid only when needed
|
|
90
|
+
async function getMermaid(): Promise<Mermaid> {
|
|
91
|
+
if (!mermaidInstance) {
|
|
92
|
+
const { default: mermaid } = await import('mermaid')
|
|
93
|
+
mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' })
|
|
94
|
+
mermaidInstance = mermaid
|
|
95
|
+
}
|
|
96
|
+
return mermaidInstance
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function extractPreAttributes(html: string): {
|
|
100
|
+
class: string | null
|
|
101
|
+
style: string | null
|
|
102
|
+
} {
|
|
103
|
+
const match = html.match(/<pre\b([^>]*)>/i)
|
|
104
|
+
if (!match) {
|
|
105
|
+
return { class: null, style: null }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const attributes = match[1]
|
|
109
|
+
|
|
110
|
+
const classMatch = attributes.match(/\bclass\s*=\s*["']([^"']*)["']/i)
|
|
111
|
+
const styleMatch = attributes.match(/\bstyle\s*=\s*["']([^"']*)["']/i)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
class: classMatch ? classMatch[1] : null,
|
|
115
|
+
style: styleMatch ? styleMatch[1] : null,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function CodeBlock({
|
|
120
|
+
isEmbedded,
|
|
121
|
+
showTypeCopyButton = true,
|
|
122
|
+
...props
|
|
123
|
+
}: React.HTMLProps<HTMLPreElement> & {
|
|
124
|
+
isEmbedded?: boolean
|
|
125
|
+
showTypeCopyButton?: boolean
|
|
126
|
+
dataCodeTitle?: string
|
|
127
|
+
}) {
|
|
128
|
+
// Extract title from data-code-title attribute, handling both camelCase and kebab-case
|
|
129
|
+
const rawTitle = ((props as any)?.dataCodeTitle ||
|
|
130
|
+
(props as any)?.['data-code-title']) as string | undefined
|
|
131
|
+
|
|
132
|
+
// Filter out "undefined" strings, null, and empty strings
|
|
133
|
+
const title =
|
|
134
|
+
rawTitle && rawTitle !== 'undefined' && rawTitle.trim().length > 0
|
|
135
|
+
? rawTitle.trim()
|
|
136
|
+
: undefined
|
|
137
|
+
|
|
138
|
+
const childElement = props.children as
|
|
139
|
+
| undefined
|
|
140
|
+
| { props?: { className?: string; children?: string } }
|
|
141
|
+
const lang = childElement?.props?.className?.replace('language-', '')
|
|
142
|
+
|
|
143
|
+
const children = props.children as
|
|
144
|
+
| undefined
|
|
145
|
+
| {
|
|
146
|
+
props: {
|
|
147
|
+
children: string
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const [copied, setCopied] = React.useState(false)
|
|
152
|
+
const ref = React.useRef<any>(null)
|
|
153
|
+
|
|
154
|
+
const code = children?.props.children
|
|
155
|
+
|
|
156
|
+
const [codeElement, setCodeElement] = React.useState(
|
|
157
|
+
<pre ref={ref} className={`shiki h-full github-light dark:aurora-x`}>
|
|
158
|
+
<code>{lang === 'mermaid' ? <svg /> : code}</code>
|
|
159
|
+
</pre>,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
React[
|
|
163
|
+
typeof document !== 'undefined' ? 'useLayoutEffect' : 'useEffect'
|
|
164
|
+
](() => {
|
|
165
|
+
;(async () => {
|
|
166
|
+
const themes = ['github-light', 'aurora-x']
|
|
167
|
+
const langStr = lang || 'plaintext'
|
|
168
|
+
|
|
169
|
+
const { highlighter, effectiveLang } = await getHighlighter(langStr)
|
|
170
|
+
// Trim trailing newlines to prevent empty lines at end of code block
|
|
171
|
+
const trimmedCode = (code || '').trimEnd()
|
|
172
|
+
|
|
173
|
+
const htmls = await Promise.all(
|
|
174
|
+
themes.map(async (theme) => {
|
|
175
|
+
const output = highlighter.codeToHtml(trimmedCode, {
|
|
176
|
+
lang: effectiveLang,
|
|
177
|
+
theme,
|
|
178
|
+
transformers: [transformerNotationDiff()],
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
if (lang === 'mermaid') {
|
|
182
|
+
const preAttributes = extractPreAttributes(output)
|
|
183
|
+
let svgHtml = genSvgMap.get(trimmedCode)
|
|
184
|
+
if (!svgHtml) {
|
|
185
|
+
const mermaid = await getMermaid()
|
|
186
|
+
const { svg } = await mermaid.render('foo', trimmedCode)
|
|
187
|
+
genSvgMap.set(trimmedCode, svg)
|
|
188
|
+
svgHtml = svg
|
|
189
|
+
}
|
|
190
|
+
return `<div class='${preAttributes.class} py-4 bg-neutral-50'>${svgHtml}</div>`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return output
|
|
194
|
+
}),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
setCodeElement(
|
|
198
|
+
<div
|
|
199
|
+
className={twMerge(
|
|
200
|
+
isEmbedded ? 'h-full [&>pre]:h-full [&>pre]:rounded-none' : '',
|
|
201
|
+
)}
|
|
202
|
+
dangerouslySetInnerHTML={{ __html: htmls.join('') }}
|
|
203
|
+
ref={ref}
|
|
204
|
+
/>,
|
|
205
|
+
)
|
|
206
|
+
})()
|
|
207
|
+
}, [code, lang])
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div
|
|
211
|
+
className={twMerge(
|
|
212
|
+
'codeblock w-full max-w-full relative not-prose border border-gray-500/20 rounded-md [&_pre]:rounded-md',
|
|
213
|
+
props.className,
|
|
214
|
+
)}
|
|
215
|
+
style={props.style}
|
|
216
|
+
>
|
|
217
|
+
{(title || showTypeCopyButton) && (
|
|
218
|
+
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 dark:bg-gray-900">
|
|
219
|
+
<div className="text-xs text-gray-700 dark:text-gray-300">
|
|
220
|
+
{title || (lang?.toLowerCase() === 'bash' ? 'sh' : (lang ?? ''))}
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<Button
|
|
224
|
+
variant="ghost"
|
|
225
|
+
size="xs"
|
|
226
|
+
className={twMerge('border-0 rounded-md transition-opacity')}
|
|
227
|
+
onClick={() => {
|
|
228
|
+
let copyContent =
|
|
229
|
+
typeof ref.current?.innerText === 'string'
|
|
230
|
+
? ref.current.innerText
|
|
231
|
+
: ''
|
|
232
|
+
|
|
233
|
+
if (copyContent.endsWith('\n')) {
|
|
234
|
+
copyContent = copyContent.slice(0, -1)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
navigator.clipboard.writeText(copyContent)
|
|
238
|
+
setCopied(true)
|
|
239
|
+
setTimeout(() => setCopied(false), 2000)
|
|
240
|
+
}}
|
|
241
|
+
aria-label="Copy code to clipboard"
|
|
242
|
+
>
|
|
243
|
+
{copied ? (
|
|
244
|
+
<span className="text-xs">Copied!</span>
|
|
245
|
+
) : (
|
|
246
|
+
<Copy className="w-4 h-4" />
|
|
247
|
+
)}
|
|
248
|
+
</Button>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
{codeElement}
|
|
252
|
+
</div>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
export type FileTabDefinition = {
|
|
4
|
+
slug: string
|
|
5
|
+
name: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type FileTabsProps = {
|
|
9
|
+
tabs: Array<FileTabDefinition>
|
|
10
|
+
children: Array<React.ReactNode> | React.ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function FileTabs({ tabs, children }: FileTabsProps) {
|
|
14
|
+
const id = React.useId()
|
|
15
|
+
const childrenArray = React.Children.toArray(children)
|
|
16
|
+
const [activeSlug, setActiveSlug] = React.useState(tabs[0]?.slug ?? '')
|
|
17
|
+
|
|
18
|
+
if (tabs.length === 0) return null
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="not-prose my-4">
|
|
22
|
+
<div className="flex items-center justify-start gap-0 overflow-x-auto overflow-y-hidden bg-gray-100 dark:bg-gray-900 border border-b-0 border-gray-500/20 rounded-t-md">
|
|
23
|
+
{tabs.map((tab) => (
|
|
24
|
+
<button
|
|
25
|
+
key={`${id}-${tab.slug}`}
|
|
26
|
+
type="button"
|
|
27
|
+
onClick={() => setActiveSlug(tab.slug)}
|
|
28
|
+
aria-label={tab.name}
|
|
29
|
+
title={tab.name}
|
|
30
|
+
className={`px-3 py-1.5 text-sm font-medium transition-colors border-b-2 -mb-[1px] ${
|
|
31
|
+
activeSlug === tab.slug
|
|
32
|
+
? 'border-current text-current bg-white dark:bg-gray-950'
|
|
33
|
+
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-800'
|
|
34
|
+
}`}
|
|
35
|
+
>
|
|
36
|
+
{tab.name}
|
|
37
|
+
</button>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
<div>
|
|
41
|
+
{childrenArray.map((child, index) => {
|
|
42
|
+
const tab = tabs[index]
|
|
43
|
+
if (!tab) return null
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
key={`${id}-${tab.slug}-panel`}
|
|
47
|
+
data-tab={tab.slug}
|
|
48
|
+
hidden={tab.slug !== activeSlug}
|
|
49
|
+
className="file-tabs-panel"
|
|
50
|
+
>
|
|
51
|
+
{child}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
})}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useParams } from '@tanstack/react-router'
|
|
2
|
+
import { Tabs } from './Tabs'
|
|
3
|
+
import { Children, ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
type CodeBlockMeta = {
|
|
6
|
+
title: string
|
|
7
|
+
code: string
|
|
8
|
+
language: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type FrameworkContentProps = {
|
|
12
|
+
codeBlocksByFramework: Record<string, CodeBlockMeta[]>
|
|
13
|
+
availableFrameworks: string[]
|
|
14
|
+
/** Pre-rendered React children for each framework (from domToReact) */
|
|
15
|
+
panelsByFramework: Record<string, ReactNode>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Simple localStorage-based framework preference (replaces useCurrentUserQuery auth dependency)
|
|
19
|
+
function useLocalFrameworkPreference(): string | undefined {
|
|
20
|
+
if (typeof document === 'undefined') return undefined
|
|
21
|
+
return localStorage.getItem('framework') || undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Renders content for the currently selected framework.
|
|
26
|
+
* - If no content for framework: shows nothing
|
|
27
|
+
* - If 1 code block: shows just the code block (minimal style)
|
|
28
|
+
* - If multiple code blocks: shows as file tabs
|
|
29
|
+
* - If no code blocks but has content: shows the content directly
|
|
30
|
+
*/
|
|
31
|
+
export function FrameworkContent({
|
|
32
|
+
codeBlocksByFramework,
|
|
33
|
+
panelsByFramework,
|
|
34
|
+
}: FrameworkContentProps) {
|
|
35
|
+
const { framework: paramsFramework } = useParams({ strict: false })
|
|
36
|
+
const localFramework = useLocalFrameworkPreference()
|
|
37
|
+
|
|
38
|
+
const actualFramework = (paramsFramework ||
|
|
39
|
+
localFramework ||
|
|
40
|
+
'react') as string
|
|
41
|
+
|
|
42
|
+
const normalizedFramework = actualFramework.toLowerCase()
|
|
43
|
+
|
|
44
|
+
// Find the framework's code blocks
|
|
45
|
+
const frameworkBlocks = codeBlocksByFramework[normalizedFramework] || []
|
|
46
|
+
const frameworkPanel = panelsByFramework[normalizedFramework]
|
|
47
|
+
|
|
48
|
+
// If no panel content at all for this framework, show nothing
|
|
49
|
+
if (!frameworkPanel) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If no code blocks, just render the content directly
|
|
54
|
+
if (frameworkBlocks.length === 0) {
|
|
55
|
+
return <div className="framework-content">{frameworkPanel}</div>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// If 1 code block, render minimal style
|
|
59
|
+
if (frameworkBlocks.length === 1) {
|
|
60
|
+
return <div className="framework-content">{frameworkPanel}</div>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Multiple code blocks - show as file tabs
|
|
64
|
+
const tabs = frameworkBlocks.map((block, index) => ({
|
|
65
|
+
slug: `file-${index}`,
|
|
66
|
+
name: block.title || 'Untitled',
|
|
67
|
+
}))
|
|
68
|
+
|
|
69
|
+
const childrenArray = Children.toArray(frameworkPanel)
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="framework-content">
|
|
73
|
+
<Tabs tabs={tabs}>{childrenArray}</Tabs>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { HTMLProps } from 'react'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { MarkdownLink } from './MarkdownLink'
|
|
4
|
+
|
|
5
|
+
import parse, {
|
|
6
|
+
attributesToProps,
|
|
7
|
+
domToReact,
|
|
8
|
+
Element,
|
|
9
|
+
HTMLReactParserOptions,
|
|
10
|
+
} from 'html-react-parser'
|
|
11
|
+
|
|
12
|
+
import { CodeBlock } from './CodeBlock'
|
|
13
|
+
import { handleTabsComponent } from './MarkdownTabsHandler'
|
|
14
|
+
import { handleFrameworkComponent } from './MarkdownFrameworkHandler'
|
|
15
|
+
import { InlineCode } from '../ui/InlineCode'
|
|
16
|
+
import { MarkdownImg } from '../ui/MarkdownImg'
|
|
17
|
+
import type { SiteConfig } from '~/types'
|
|
18
|
+
|
|
19
|
+
type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
|
20
|
+
|
|
21
|
+
const CustomHeading = ({
|
|
22
|
+
Comp,
|
|
23
|
+
id,
|
|
24
|
+
children,
|
|
25
|
+
...props
|
|
26
|
+
}: HTMLProps<HTMLHeadingElement> & {
|
|
27
|
+
Comp: HeadingLevel
|
|
28
|
+
}) => {
|
|
29
|
+
// Convert children to array and strip any inner anchor (native 'a' or MarkdownLink)
|
|
30
|
+
const childrenArray = React.Children.toArray(children)
|
|
31
|
+
const sanitizedChildren = childrenArray.map((child) => {
|
|
32
|
+
if (
|
|
33
|
+
React.isValidElement(child) &&
|
|
34
|
+
(child.type === 'a' || child.type === MarkdownLink)
|
|
35
|
+
) {
|
|
36
|
+
// replace anchor child with its own children so outer anchor remains the only link
|
|
37
|
+
return (child.props as { children?: React.ReactNode }).children ?? null
|
|
38
|
+
}
|
|
39
|
+
return child
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const heading = (
|
|
43
|
+
<Comp id={id} {...props}>
|
|
44
|
+
{sanitizedChildren}
|
|
45
|
+
</Comp>
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if (id) {
|
|
49
|
+
return (
|
|
50
|
+
<a
|
|
51
|
+
href={`#${id}`}
|
|
52
|
+
className={`anchor-heading *:scroll-my-20 *:lg:scroll-my-4`}
|
|
53
|
+
>
|
|
54
|
+
{heading}
|
|
55
|
+
</a>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return heading
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const makeHeading =
|
|
63
|
+
(type: HeadingLevel) => (props: HTMLProps<HTMLHeadingElement>) => (
|
|
64
|
+
<CustomHeading
|
|
65
|
+
Comp={type}
|
|
66
|
+
{...props}
|
|
67
|
+
className={`${props.className ?? ''} block`}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const MarkdownIframe = React.memo(function MarkdownIframe(
|
|
72
|
+
props: HTMLProps<HTMLIFrameElement>,
|
|
73
|
+
) {
|
|
74
|
+
return <iframe {...props} className="w-full" title="Embedded Content" />
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const markdownComponents: Record<string, React.FC> = {
|
|
78
|
+
a: MarkdownLink,
|
|
79
|
+
pre: CodeBlock,
|
|
80
|
+
h1: makeHeading('h1'),
|
|
81
|
+
h2: makeHeading('h2'),
|
|
82
|
+
h3: makeHeading('h3'),
|
|
83
|
+
h4: makeHeading('h4'),
|
|
84
|
+
h5: makeHeading('h5'),
|
|
85
|
+
h6: makeHeading('h6'),
|
|
86
|
+
code: InlineCode,
|
|
87
|
+
iframe: MarkdownIframe,
|
|
88
|
+
img: MarkdownImg,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Cache for lazily-loaded custom components keyed by component name
|
|
92
|
+
const lazyComponentCache = new Map<string, React.LazyExoticComponent<React.ComponentType<any>>>()
|
|
93
|
+
|
|
94
|
+
function getLazyComponent(
|
|
95
|
+
name: string,
|
|
96
|
+
loader: () => Promise<{ default: React.ComponentType<any> }>,
|
|
97
|
+
): React.LazyExoticComponent<React.ComponentType<any>> {
|
|
98
|
+
let cached = lazyComponentCache.get(name)
|
|
99
|
+
if (!cached) {
|
|
100
|
+
cached = React.lazy(loader)
|
|
101
|
+
lazyComponentCache.set(name, cached)
|
|
102
|
+
}
|
|
103
|
+
return cached
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createParserOptions(
|
|
107
|
+
customComponents?: SiteConfig['components'],
|
|
108
|
+
): HTMLReactParserOptions {
|
|
109
|
+
const options: HTMLReactParserOptions = {
|
|
110
|
+
replace: (domNode) => {
|
|
111
|
+
if (domNode instanceof Element && domNode.attribs) {
|
|
112
|
+
if (domNode.name === 'md-comment-component') {
|
|
113
|
+
const componentName = domNode.attribs['data-component']
|
|
114
|
+
const rawAttributes = domNode.attribs['data-attributes']
|
|
115
|
+
const attributes: Record<string, any> = {}
|
|
116
|
+
try {
|
|
117
|
+
Object.assign(attributes, JSON.parse(rawAttributes))
|
|
118
|
+
} catch {
|
|
119
|
+
// ignore JSON parse errors and fall back to empty props
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
switch (componentName?.toLowerCase()) {
|
|
123
|
+
case 'tabs':
|
|
124
|
+
return handleTabsComponent(domNode, attributes, options)
|
|
125
|
+
case 'framework':
|
|
126
|
+
return handleFrameworkComponent(domNode, attributes, options)
|
|
127
|
+
default: {
|
|
128
|
+
// Check custom components for md-comment-component with unknown data-component
|
|
129
|
+
if (componentName && customComponents?.[componentName]) {
|
|
130
|
+
const LazyComp = getLazyComponent(componentName, customComponents[componentName])
|
|
131
|
+
return (
|
|
132
|
+
<React.Suspense fallback={<div />}>
|
|
133
|
+
<LazyComp {...attributes}>
|
|
134
|
+
{domToReact(domNode.children as any, options)}
|
|
135
|
+
</LazyComp>
|
|
136
|
+
</React.Suspense>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
return <div>{domToReact(domNode.children as any, options)}</div>
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check if an unknown HTML element matches a custom component
|
|
145
|
+
if (customComponents?.[domNode.name]) {
|
|
146
|
+
const LazyComp = getLazyComponent(domNode.name, customComponents[domNode.name])
|
|
147
|
+
return (
|
|
148
|
+
<React.Suspense fallback={<div />}>
|
|
149
|
+
<LazyComp {...attributesToProps(domNode.attribs)}>
|
|
150
|
+
{domToReact(domNode.children as any, options)}
|
|
151
|
+
</LazyComp>
|
|
152
|
+
</React.Suspense>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const replacer = markdownComponents[domNode.name]
|
|
157
|
+
if (replacer) {
|
|
158
|
+
return React.createElement(
|
|
159
|
+
replacer,
|
|
160
|
+
attributesToProps(domNode.attribs),
|
|
161
|
+
domToReact(domNode.children as any, options),
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
return options
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Default options (no custom components) for backward compatibility
|
|
173
|
+
const defaultOptions = createParserOptions()
|
|
174
|
+
|
|
175
|
+
type MarkdownProps = {
|
|
176
|
+
/** Raw markdown content -- will be rendered to HTML if provided */
|
|
177
|
+
rawContent?: string
|
|
178
|
+
/** Pre-rendered HTML markup. If not provided, rawContent will be used. */
|
|
179
|
+
htmlMarkup?: string
|
|
180
|
+
/** Optional render function for raw markdown to HTML. Must be provided if rawContent is used. */
|
|
181
|
+
renderMarkdown?: (raw: string) => { markup: string; headings: any[] }
|
|
182
|
+
/** Custom components from SiteConfig for lazy-loading unknown elements */
|
|
183
|
+
customComponents?: SiteConfig['components']
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const Markdown = React.memo(function Markdown({
|
|
187
|
+
rawContent,
|
|
188
|
+
htmlMarkup,
|
|
189
|
+
renderMarkdown,
|
|
190
|
+
customComponents,
|
|
191
|
+
}: MarkdownProps) {
|
|
192
|
+
const rendered = React.useMemo(() => {
|
|
193
|
+
if (rawContent && renderMarkdown) {
|
|
194
|
+
return renderMarkdown(rawContent)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (htmlMarkup) {
|
|
198
|
+
return { markup: htmlMarkup, headings: [] }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { markup: '', headings: [] }
|
|
202
|
+
}, [rawContent, htmlMarkup, renderMarkdown])
|
|
203
|
+
|
|
204
|
+
const options = React.useMemo(
|
|
205
|
+
() => (customComponents ? createParserOptions(customComponents) : defaultOptions),
|
|
206
|
+
[customComponents],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return React.useMemo(() => {
|
|
210
|
+
if (!rendered.markup) {
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return parse(rendered.markup, options)
|
|
215
|
+
}, [rendered.markup, options])
|
|
216
|
+
})
|