feat(desktop): embed BoosAPI Desktop client as a subproject

Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
This commit is contained in:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,660 @@
import { readdirSync } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { transformerTwoslash } from '@shikijs/vitepress-twoslash'
import { defineConfig } from 'vitepress'
const docsSiteUrl = 'https://markstream-vue-docs.simonhe.me'
const docsOgImageUrl = `${docsSiteUrl}/og-image.svg`
const docsDefaultDescription = 'Streaming-friendly Markdown renderer for Vue 3, Vue 2, React, and Angular'
const githubRepoUrl = 'https://github.com/Simon-He95/markstream-vue'
const docsRootDir = fileURLToPath(new URL('..', import.meta.url))
const workspaceRootDir = fileURLToPath(new URL('../..', import.meta.url))
// TypeScript sometimes rejects VitePress site locales on the `Config` type.
// Cast to `any` to avoid strict type errors in the docs config while keeping intellisense.
const markdownParserSrc = fileURLToPath(new URL('../../packages/markdown-parser/src/index.ts', import.meta.url))
const markdownParserSrcDir = path.dirname(markdownParserSrc)
const playgroundItems = [
{ text: 'Vue 3', link: 'https://markstream-vue.simonhe.me/' },
{ text: 'React', link: 'https://markstream-react.pages.dev/' },
{ text: 'Nuxt', link: 'https://markstream-nuxt.pages.dev/' },
{ text: 'Vue 2', link: 'https://markstream-vue2.pages.dev/' },
{ text: 'Angular', link: 'https://markstream-angular.pages.dev/' },
]
const rootNav = [
{ text: 'Get started', link: '/guide/installation' },
{ text: 'Customization', link: '/guide/component-overrides' },
{ text: 'Migration / AI', link: '/guide/ai-workflows' },
{ text: 'API Reference', link: '/guide/api' },
{ text: 'Playground', items: playgroundItems },
{ text: 'GitHub', link: 'https://github.com/Simon-He95/markstream-vue' },
]
const zhNav = [
{ text: '快速开始', link: '/zh/guide/installation' },
{ text: '自定义', link: '/zh/guide/component-overrides' },
{ text: '接入与迁移', link: '/zh/guide/ai-workflows' },
{ text: 'API 参考', link: '/zh/guide/api' },
{ text: '演示', items: playgroundItems },
{ text: '搜索', link: '/zh/guide/search' },
{ text: 'GitHub', link: 'https://github.com/Simon-He95/markstream-vue' },
]
const englishGuideSidebar = [
{
text: 'Start Here',
items: [
{ text: 'Guide Home', link: '/guide/' },
{ text: 'Installation', link: '/guide/installation' },
{ text: 'Quick Start', link: '/guide/quick-start' },
{ text: 'Usage & Streaming', link: '/guide/usage' },
{ text: 'Docs Site & VitePress', link: '/guide/vitepress-docs-integration' },
{ text: 'AI Chat & Streaming', link: '/guide/ai-chat-streaming' },
{ text: 'Troubleshooting by Symptom', link: '/guide/troubleshooting-path' },
{ text: 'Props & Options', link: '/guide/props' },
{ text: 'Features Overview', link: '/guide/features' },
{ text: 'Examples', link: '/guide/examples' },
{ text: 'Playground', link: '/guide/playground' },
],
},
{
text: 'Customization',
items: [
{ text: 'Renderer & Node Components', link: '/guide/components' },
{ text: 'Override Built-in Components', link: '/guide/component-overrides' },
{ text: 'Custom Tags & Advanced Components', link: '/guide/custom-components' },
{ text: 'Advanced Parser Hooks', link: '/guide/advanced' },
{ text: 'Parser Overview', link: '/guide/parser' },
{ text: 'Parser API', link: '/guide/parser-api' },
{ text: 'Tailwind & Styling', link: '/guide/tailwind' },
{ text: 'Troubleshooting', link: '/guide/troubleshooting' },
],
},
{
text: 'Feature Guides',
collapsed: true,
items: [
{ text: 'Code Blocks', link: '/guide/code-blocks' },
{ text: 'Code Block Header', link: '/guide/codeblock-header' },
{ text: 'CodeBlockNode', link: '/guide/code-block-node' },
{ text: 'ImageNode', link: '/guide/image-node' },
{ text: 'Math', link: '/guide/math' },
{ text: 'Mermaid', link: '/guide/mermaid' },
{ text: 'MermaidBlockNode', link: '/guide/mermaid-block-node' },
{ text: 'MermaidBlockNode (override)', link: '/guide/mermaid-block-node-override' },
{ text: 'Mermaid export demo', link: '/guide/mermaid-export-demo' },
{ text: 'D2', link: '/guide/d2' },
{ text: 'AntV Infographic', link: '/guide/infographic' },
{ text: 'ECharts', link: '/guide/echarts' },
{ text: 'Monaco', link: '/guide/monaco' },
],
},
{
text: 'Frameworks & Migration',
collapsed: true,
items: [
{ text: 'Nuxt SSR', link: '/nuxt-ssr' },
{ text: 'Vue 2 Quick Start', link: '/guide/vue2-quick-start' },
{ text: 'Vue 2 Installation', link: '/guide/vue2-installation' },
{ text: 'Vue 2 Components', link: '/guide/vue2-components' },
{ text: 'React Quick Start', link: '/guide/react-quick-start' },
{ text: 'React Installation', link: '/guide/react-installation' },
{ text: 'React Components', link: '/guide/react-components' },
{ text: 'Migrate from react-markdown', link: '/guide/react-markdown-migration' },
{ text: 'Migration Cookbook', link: '/guide/react-markdown-migration-cookbook' },
{ text: 'Angular Quick Start', link: '/guide/angular-quick-start' },
{ text: 'Angular Installation', link: '/guide/angular-installation' },
{ text: 'AI / Skills workflows', link: '/guide/ai-workflows' },
],
},
{
text: 'Reference',
collapsed: true,
items: [
{ text: 'API Overview', link: '/guide/api' },
{ text: 'Performance', link: '/guide/performance' },
{ text: 'Why use it?', link: '/guide/why' },
{ text: 'Compared', link: '/guide/compared' },
{ text: 'Monaco Internals', link: '/guide/monaco-internals' },
{ text: 'Legacy builds & iOS regex compatibility', link: '/guide/legacy-builds' },
{ text: 'Contributing', link: '/guide/contributing' },
],
},
]
const chineseGuideSidebar = [
{
text: '开始使用',
items: [
{ text: '指南首页', link: '/zh/guide/' },
{ text: '安装', link: '/zh/guide/installation' },
{ text: '快速开始', link: '/zh/guide/quick-start' },
{ text: '使用与流式渲染', link: '/zh/guide/usage' },
{ text: '文档站与 VitePress 集成', link: '/zh/guide/vitepress-docs-integration' },
{ text: 'AI 聊天与流式输出', link: '/zh/guide/ai-chat-streaming' },
{ text: '按症状排查', link: '/zh/guide/troubleshooting-path' },
{ text: 'Props 与选项', link: '/zh/guide/props' },
{ text: '功能特性', link: '/zh/guide/features' },
{ text: '示例', link: '/zh/guide/examples' },
{ text: 'Playground', link: '/zh/guide/playground' },
],
},
{
text: '自定义',
items: [
{ text: '渲染器与节点组件', link: '/zh/guide/components' },
{ text: '覆盖内置组件', link: '/zh/guide/component-overrides' },
{ text: '自定义标签与高级组件', link: '/zh/guide/custom-components' },
{ text: '高级解析', link: '/zh/guide/advanced' },
{ text: '解析器概览', link: '/zh/guide/parser' },
{ text: '解析器 API', link: '/zh/guide/parser-api' },
{ text: 'Tailwind 与样式', link: '/zh/guide/tailwind' },
{ text: '故障排除', link: '/zh/guide/troubleshooting' },
],
},
{
text: '功能专题',
collapsed: true,
items: [
{ text: '代码块', link: '/zh/guide/code-blocks' },
{ text: '代码块头部', link: '/zh/guide/codeblock-header' },
{ text: 'CodeBlockNode', link: '/zh/guide/code-block-node' },
{ text: 'ImageNode', link: '/zh/guide/image-node' },
{ text: 'Math', link: '/zh/guide/math' },
{ text: 'Mermaid', link: '/zh/guide/mermaid' },
{ text: 'MermaidBlockNode', link: '/zh/guide/mermaid-block-node' },
{ text: '覆盖 MermaidBlockNode', link: '/zh/guide/mermaid-block-node-override' },
{ text: 'Mermaid 导出示例', link: '/zh/guide/mermaid-export-demo' },
{ text: 'D2 图表', link: '/zh/guide/d2' },
{ text: 'AntV Infographic', link: '/zh/guide/infographic' },
{ text: 'ECharts', link: '/zh/guide/echarts' },
{ text: 'Monaco', link: '/zh/guide/monaco' },
],
},
{
text: '框架与迁移',
collapsed: true,
items: [
{ text: 'Nuxt SSR', link: '/zh/nuxt-ssr' },
{ text: 'Vue 2 快速开始', link: '/zh/guide/vue2-quick-start' },
{ text: 'Vue 2 安装', link: '/zh/guide/vue2-installation' },
{ text: 'Vue 2 组件', link: '/zh/guide/vue2-components' },
{ text: 'React 快速开始', link: '/zh/guide/react-quick-start' },
{ text: 'React 安装', link: '/zh/guide/react-installation' },
{ text: 'React 组件', link: '/zh/guide/react-components' },
{ text: '从 react-markdown 迁移', link: '/zh/guide/react-markdown-migration' },
{ text: '迁移 Cookbook', link: '/zh/guide/react-markdown-migration-cookbook' },
{ text: 'Angular 快速开始', link: '/zh/guide/angular-quick-start' },
{ text: 'Angular 安装', link: '/zh/guide/angular-installation' },
{ text: 'AI / Skills 工作流', link: '/zh/guide/ai-workflows' },
],
},
{
text: '参考',
collapsed: true,
items: [
{ text: 'API 总览', link: '/zh/guide/api' },
{ text: '搜索', link: '/zh/guide/search' },
{ text: '性能', link: '/zh/guide/performance' },
{ text: '为什么使用?', link: '/zh/guide/why' },
{ text: '对比', link: '/zh/guide/compared' },
{ text: 'Monaco 内部', link: '/zh/guide/monaco-internals' },
{ text: 'Legacy 构建与 iOS 正则兼容', link: '/zh/guide/legacy-builds' },
{ text: '贡献指南', link: '/zh/guide/contributing' },
],
},
]
const siteHead = [
['link', { rel: 'icon', href: '/logo.svg', type: 'image/svg+xml' }],
['meta', { name: 'theme-color', content: '#111827' }],
['meta', { name: 'robots', content: 'index,follow' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:site_name', content: 'markstream-vue' }],
['meta', { property: 'og:image', content: docsOgImageUrl }],
['meta', { property: 'og:image:alt', content: 'markstream-vue documentation and playground previews' }],
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
['meta', { name: 'twitter:image', content: docsOgImageUrl }],
]
const seoExcludedDocsPaths = new Set([
'/404',
'/LEGACY-BUILDS',
'/e2e-testing-report',
'/zh/e2e-testing-report',
'/guide/docs-style',
'/zh/guide/docs-style',
'/guide/e2e-testing-report',
'/zh/guide/e2e-testing-report',
'/guide/katex-worker-performance-analysis',
'/zh/guide/katex-worker-performance-analysis',
'/guide/monorepo-migration',
'/zh/guide/monorepo-migration',
'/guide/thanks',
'/zh/guide/thanks',
'/guide/translation',
'/zh/guide/translation',
'/katex-cache-analysis',
'/zh/katex-cache-analysis',
'/katex-worker-performance-analysis',
'/zh/katex-worker-performance-analysis',
'/llms',
'/llms.zh-CN',
'/monorepo-migration',
'/zh/monorepo-migration',
'/nuxt-ssr.zh-CN',
])
function normalizeDocsSeoPath(page: string) {
if (!page || page === '.')
return '/'
let normalized = page
if (!normalized.startsWith('/'))
normalized = `/${normalized}`
normalized = normalized.replace(/\/index\.md$/, '/')
normalized = normalized.replace(/\/index\.html$/, '/')
normalized = normalized.replace(/\.md$/, '')
normalized = normalized.replace(/\.html$/, '')
normalized = normalized.replace(/\/index$/, '/')
if (normalized !== '/' && normalized.endsWith('/'))
normalized = normalized.slice(0, -1)
return normalized || '/'
}
function createDocsSeoTitle(pageTitle?: string) {
if (!pageTitle || pageTitle === 'markstream-vue')
return 'markstream-vue'
return `${pageTitle} | markstream-vue`
}
function isDocsSeoExcluded(path: string) {
return seoExcludedDocsPaths.has(path) || path.endsWith('.zh-CN')
}
function getDocsPriorityKey(path: string) {
if (path === '/zh')
return '/'
if (path.startsWith('/zh/'))
return path.slice(3) || '/'
return path
}
function collectDocsRoutePaths(rootDir: string) {
const routePaths = new Set<string>()
function walk(currentDir: string) {
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
if (entry.name === '.vitepress' || entry.name === 'node_modules')
continue
const absolutePath = path.join(currentDir, entry.name)
if (entry.isDirectory()) {
walk(absolutePath)
continue
}
if (!entry.isFile() || !entry.name.endsWith('.md'))
continue
const relativePath = path.relative(rootDir, absolutePath).replace(/\\/g, '/')
routePaths.add(normalizeDocsSeoPath(`/${relativePath}`))
}
}
walk(rootDir)
return routePaths
}
const availableDocsRoutePaths = collectDocsRoutePaths(docsRootDir)
const docsPrimaryLandingPaths = new Set([
'/guide',
'/guide/installation',
'/guide/quick-start',
'/guide/usage',
'/guide/vitepress-docs-integration',
'/guide/ai-chat-streaming',
'/guide/troubleshooting-path',
'/guide/component-overrides',
'/guide/custom-components',
'/guide/api',
'/guide/components',
'/guide/props',
'/guide/performance',
'/guide/tailwind',
'/guide/react-markdown-migration',
'/guide/playground',
'/nuxt-ssr',
])
const docsSecondaryLandingPaths = new Set([
'/guide/parser',
'/guide/parser-api',
'/guide/features',
'/guide/examples',
'/guide/react-markdown-migration-cookbook',
'/guide/vue2-quick-start',
'/guide/react-quick-start',
'/guide/angular-quick-start',
'/guide/troubleshooting',
])
function getDocsSitemapHints(path: string) {
const priorityKey = getDocsPriorityKey(path)
if (priorityKey === '/')
return { priority: 1, changefreq: 'weekly' as const }
if (priorityKey === '/guide')
return { priority: 0.95, changefreq: 'weekly' as const }
if (docsPrimaryLandingPaths.has(priorityKey))
return { priority: 0.88, changefreq: 'weekly' as const }
if (docsSecondaryLandingPaths.has(priorityKey))
return { priority: 0.78, changefreq: 'monthly' as const }
if (priorityKey.startsWith('/guide/'))
return { priority: 0.68, changefreq: 'monthly' as const }
return { priority: 0.6, changefreq: 'monthly' as const }
}
function getDocsAlternatePaths(path: string) {
const englishPath = path === '/zh' ? '/' : path.startsWith('/zh/') ? path.slice(3) || '/' : path
const chinesePath = path === '/' ? '/zh' : path.startsWith('/zh/') ? path : `/zh${path}`
const alternates = {
english: availableDocsRoutePaths.has(englishPath) ? englishPath : null,
chinese: availableDocsRoutePaths.has(chinesePath) ? chinesePath : null,
}
return alternates
}
function createDocsBreadcrumbItems(path: string, title: string, isChinese: boolean) {
const items = []
const homeName = isChinese ? '首页' : 'Home'
const guideName = isChinese ? '指南' : 'Guide'
const homePath = isChinese ? '/zh' : '/'
items.push({
'@type': 'ListItem',
'position': 1,
'name': homeName,
'item': `${docsSiteUrl}${homePath === '/' ? '/' : homePath}`,
})
if (path === homePath || (isChinese && path === '/zh') || (!isChinese && path === '/'))
return items
const guideRoot = isChinese ? '/zh/guide' : '/guide'
if (path === guideRoot || path.startsWith(`${guideRoot}/`)) {
items.push({
'@type': 'ListItem',
'position': 2,
'name': guideName,
'item': `${docsSiteUrl}${guideRoot}`,
})
if (path !== guideRoot) {
items.push({
'@type': 'ListItem',
'position': 3,
'name': title,
'item': `${docsSiteUrl}${path}`,
})
}
return items
}
items.push({
'@type': 'ListItem',
'position': 2,
'name': title,
'item': `${docsSiteUrl}${path}`,
})
return items
}
function createDocsStructuredData(path: string, title: string, description: string, isChinese: boolean) {
const graph: Record<string, any>[] = []
const homePath = isChinese ? '/zh' : '/'
if (path === homePath) {
graph.push({
'@type': 'WebSite',
'@id': `${docsSiteUrl}${homePath === '/' ? '/' : homePath}#website`,
'name': 'markstream-vue Docs',
'url': `${docsSiteUrl}${homePath === '/' ? '/' : homePath}`,
'inLanguage': isChinese ? 'zh-CN' : 'en-US',
description,
'publisher': {
'@type': 'Organization',
'name': 'markstream-vue',
'url': githubRepoUrl,
},
})
graph.push({
'@type': 'SoftwareApplication',
'name': 'markstream-vue',
'applicationCategory': 'DeveloperApplication',
'operatingSystem': 'Web',
'url': `${docsSiteUrl}${homePath === '/' ? '/' : homePath}`,
description,
'offers': {
'@type': 'Offer',
'price': '0',
'priceCurrency': 'USD',
},
'sameAs': [
githubRepoUrl,
docsSiteUrl,
'https://markstream-vue.simonhe.me/',
],
})
}
const breadcrumbItems = createDocsBreadcrumbItems(path, title, isChinese)
if (breadcrumbItems.length > 0) {
graph.push({
'@type': 'BreadcrumbList',
'itemListElement': breadcrumbItems,
})
}
if (graph.length === 0)
return []
return [[
'script',
{ type: 'application/ld+json' },
JSON.stringify({
'@context': 'https://schema.org',
'@graph': graph,
}),
]] as [string, Record<string, string>, string][]
}
export default defineConfig({
title: 'markstream-vue',
description: docsDefaultDescription,
base: process.env.VITEPRESS_BASE || '/',
head: siteHead,
markdown: {
languages: ['ts', 'tsx', 'js', 'jsx', 'vue'],
codeTransformers: [
transformerTwoslash({
explicitTrigger: true,
twoslashOptions: {
compilerOptions: {
baseUrl: workspaceRootDir,
paths: {
'markstream-vue': ['docs/.vitepress/twoslash/markstream-vue.d.ts'],
'stream-markdown-parser': ['packages/markdown-parser/src/index.ts'],
},
},
},
}),
],
},
sitemap: {
hostname: docsSiteUrl,
transformItems(items) {
return items
.map((item) => {
const url = new URL(item.url, docsSiteUrl)
const path = normalizeDocsSeoPath(url.pathname)
const { priority, changefreq } = getDocsSitemapHints(path)
url.pathname = path === '/' ? '/' : path
const links = item.links?.map((link) => {
const alternateUrl = new URL(link.url, docsSiteUrl)
const alternatePath = normalizeDocsSeoPath(alternateUrl.pathname)
alternateUrl.pathname = alternatePath === '/' ? '/' : alternatePath
return {
...link,
url: alternateUrl.toString(),
}
})
return {
...item,
url: url.toString(),
links,
priority,
changefreq,
fullPrecisionPriority: true,
}
})
.filter(item => !isDocsSeoExcluded(normalizeDocsSeoPath(new URL(item.url, docsSiteUrl).pathname)))
},
},
vite: {
resolve: {
// Docs import the built `markstream-vue` package, which keeps
// `stream-markdown-parser` external. Point VitePress at the workspace
// source so clean CI checkouts don't need a prebuilt parser dist.
alias: [
{
find: /^stream-markdown-parser$/,
replacement: markdownParserSrc,
},
{
find: /^stream-markdown-parser\//,
replacement: `${markdownParserSrcDir}/`,
},
],
},
},
locales: {
root: {
label: 'English',
lang: 'en-US',
title: 'markstream-vue',
description: docsDefaultDescription,
link: '/',
},
zh: {
label: '简体中文',
lang: 'zh-CN',
title: 'markstream-vue',
description: '适用于 Vue 3 的流式 Markdown 渲染器',
link: '/zh/',
},
},
themeConfig: {
logo: '/logo.svg',
search: {
provider: 'local',
},
nav: rootNav,
sidebar: {
'/guide/': englishGuideSidebar,
'/zh/guide/': chineseGuideSidebar,
'/zh/': chineseGuideSidebar,
'/': englishGuideSidebar,
},
locales: {
root: {
selectText: 'Languages',
label: 'English',
ariaLabel: 'Select language',
nav: rootNav,
},
zh: {
selectText: '选择语言',
label: '简体中文',
ariaLabel: '选择语言',
nav: zhNav,
},
},
},
transformHead(ctx) {
const normalizedPath = normalizeDocsSeoPath(ctx.page)
const canonicalUrl = `${docsSiteUrl}${normalizedPath}`
const frontmatter = ctx.pageData.frontmatter ?? {}
const isChinese = normalizedPath === '/zh' || normalizedPath.startsWith('/zh/')
const description = frontmatter.description || ctx.description || docsDefaultDescription
const title = createDocsSeoTitle(frontmatter.title || ctx.pageData.title || ctx.title)
const shouldIndex = !ctx.pageData.isNotFound && !frontmatter.noindex && !isDocsSeoExcluded(normalizedPath)
const alternates = getDocsAlternatePaths(normalizedPath)
const alternateHead = []
const alternateLocales = []
if (alternates.english) {
alternateHead.push(['link', { rel: 'alternate', hreflang: 'en-US', href: `${docsSiteUrl}${alternates.english}` }])
alternateLocales.push('en_US')
}
if (alternates.chinese) {
alternateHead.push(['link', { rel: 'alternate', hreflang: 'zh-CN', href: `${docsSiteUrl}${alternates.chinese}` }])
alternateLocales.push('zh_CN')
}
const defaultAlternate = alternates.english || alternates.chinese
if (defaultAlternate)
alternateHead.push(['link', { rel: 'alternate', hreflang: 'x-default', href: `${docsSiteUrl}${defaultAlternate}` }])
const structuredData = shouldIndex
? createDocsStructuredData(
normalizedPath,
frontmatter.title || ctx.pageData.title || ctx.title || 'markstream-vue',
description,
isChinese,
)
: []
return [
['link', { rel: 'canonical', href: canonicalUrl }],
...alternateHead,
['meta', { name: 'description', content: description }],
['meta', { name: 'robots', content: shouldIndex ? 'index,follow' : 'noindex,nofollow' }],
['meta', { property: 'og:locale', content: isChinese ? 'zh_CN' : 'en_US' }],
...alternateLocales
.filter(locale => locale !== (isChinese ? 'zh_CN' : 'en_US'))
.map(locale => ['meta', { property: 'og:locale:alternate', content: locale }]),
['meta', { property: 'og:title', content: title }],
['meta', { property: 'og:description', content: description }],
['meta', { property: 'og:url', content: canonicalUrl }],
['meta', { name: 'twitter:title', content: title }],
['meta', { name: 'twitter:description', content: description }],
...structuredData,
]
},
} as any)
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" fill="none">
<rect width="120" height="120" rx="24" fill="#0ea5e9"/>
<text x="50%" y="58%" dominant-baseline="middle" text-anchor="middle" font-size="48" font-family="Arial" fill="#fff">MD</text>
</svg>

After

Width:  |  Height:  |  Size: 269 B

@@ -0,0 +1,35 @@
<svg width="1200" height="630" viewBox="0 0 1200 630" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="630" fill="#081122"/>
<rect x="38" y="38" width="1124" height="554" rx="32" fill="url(#panel)"/>
<rect x="86" y="96" width="492" height="340" rx="28" fill="#0F172A" stroke="#334155" stroke-width="2"/>
<rect x="118" y="132" width="184" height="18" rx="9" fill="#22C55E" fill-opacity="0.9"/>
<rect x="118" y="172" width="402" height="20" rx="10" fill="#E2E8F0" fill-opacity="0.92"/>
<rect x="118" y="208" width="348" height="20" rx="10" fill="#94A3B8" fill-opacity="0.8"/>
<rect x="118" y="258" width="116" height="116" rx="24" fill="#111827"/>
<rect x="254" y="258" width="116" height="116" rx="24" fill="#0B1220"/>
<rect x="390" y="258" width="156" height="116" rx="24" fill="#111827"/>
<path d="M174 319H224" stroke="#38BDF8" stroke-width="18" stroke-linecap="round"/>
<path d="M312 289L312 349" stroke="#22C55E" stroke-width="18" stroke-linecap="round"/>
<path d="M446 290H486C506.987 290 524 307.013 524 328C524 348.987 506.987 366 486 366H446V290Z" fill="#F59E0B"/>
<rect x="608" y="118" width="502" height="394" rx="32" fill="#020617" stroke="#1E293B" stroke-width="2"/>
<rect x="652" y="152" width="92" height="34" rx="17" fill="#22C55E" fill-opacity="0.14" stroke="#22C55E" stroke-opacity="0.7"/>
<text x="698" y="169" fill="#DCFCE7" font-family="Arial, Helvetica, sans-serif" font-size="16" font-weight="700" text-anchor="middle" dominant-baseline="middle">DOCS</text>
<text x="652" y="232" fill="#F8FAFC" font-family="Arial, Helvetica, sans-serif" font-size="46" font-weight="700">markstream-vue</text>
<text x="652" y="288" fill="#93C5FD" font-family="Arial, Helvetica, sans-serif" font-size="28" font-weight="700">Guides and API</text>
<text x="652" y="342" fill="#CBD5E1" font-family="Arial, Helvetica, sans-serif" font-size="20">Streaming Markdown for Vue</text>
<text x="652" y="378" fill="#CBD5E1" font-family="Arial, Helvetica, sans-serif" font-size="20">VitePress, overrides, custom tags</text>
<rect x="652" y="416" width="100" height="36" rx="18" fill="#0F1D33" stroke="#334155"/>
<text x="702" y="434" fill="#E2E8F0" font-family="Arial, Helvetica, sans-serif" font-size="16" font-weight="700" text-anchor="middle" dominant-baseline="middle">Guides</text>
<rect x="764" y="416" width="72" height="36" rx="18" fill="#0F1D33" stroke="#334155"/>
<text x="800" y="434" fill="#E2E8F0" font-family="Arial, Helvetica, sans-serif" font-size="16" font-weight="700" text-anchor="middle" dominant-baseline="middle">API</text>
<rect x="848" y="416" width="118" height="36" rx="18" fill="#0F1D33" stroke="#334155"/>
<text x="907" y="434" fill="#E2E8F0" font-family="Arial, Helvetica, sans-serif" font-size="16" font-weight="700" text-anchor="middle" dominant-baseline="middle">Migration</text>
<rect x="652" y="462" width="164" height="42" rx="21" fill="#22C55E" fill-opacity="0.18" stroke="#22C55E"/>
<text x="734" y="483" fill="#DCFCE7" font-family="Arial, Helvetica, sans-serif" font-size="22" font-weight="700" text-anchor="middle" dominant-baseline="middle">Read docs</text>
<defs>
<linearGradient id="panel" x1="78" y1="48" x2="1082" y2="582" gradientUnits="userSpaceOnUse">
<stop stop-color="#111827"/>
<stop offset="1" stop-color="#0F766E"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://markstream-vue-docs.simonhe.me/sitemap.xml
@@ -0,0 +1,5 @@
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 360' width='600' height='360'>
<rect width='100%' height='100%' fill='#111827'/>
<rect x='56' y='40' width='488' height='280' rx='8' fill='#0f172a'/>
<text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' font-size='22' fill='#9CA3AF' font-family='Courier New, monospace'>Monaco / CodeBlock demo</text>
</svg>

After

Width:  |  Height:  |  Size: 388 B

@@ -0,0 +1,6 @@
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 360' width='600' height='360'>
<rect width='100%' height='100%' fill='#0ea5e9'/>
<rect x='24' y='24' width='552' height='312' rx='12' fill='#fff'/>
<text x='50%' y='40%' dominant-baseline='middle' text-anchor='middle' font-size='18' fill='#0ea5e9' font-family='Arial'>Mermaid Diagram Example</text>
<text x='50%' y='56%' dominant-baseline='middle' text-anchor='middle' font-size='14' fill='#0ea5e9' font-family='Arial'>Progressive diagram rendering</text>
</svg>

After

Width:  |  Height:  |  Size: 528 B

@@ -0,0 +1,6 @@
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 360' width='600' height='360'>
<rect width='100%' height='100%' fill='#0ea5e9'/>
<rect x='24' y='24' width='552' height='312' rx='12' fill='#fff'/>
<text x='50%' y='40%' dominant-baseline='middle' text-anchor='middle' font-size='28' fill='#0ea5e9' font-family='Arial'>Playground Demo</text>
<text x='50%' y='54%' dominant-baseline='middle' text-anchor='middle' font-size='16' fill='#0ea5e9' font-family='Arial'>Live editor + preview</text>
</svg>

After

Width:  |  Height:  |  Size: 512 B

@@ -0,0 +1,136 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const props = defineProps<{
repo: string
}>()
const stars = ref<number | null>(null)
const isLoading = ref(true)
onMounted(async () => {
try {
const response = await fetch(`https://api.github.com/repos/${props.repo}`)
if (response.ok) {
const data = await response.json()
stars.value = data.stargazers_count
}
}
catch {
// Silently fail if API request fails
}
finally {
isLoading.value = false
}
})
function formatNumber(num: number): string {
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}k`
}
return num.toString()
}
</script>
<template>
<a
:href="`https://github.com/${repo}`"
target="_blank"
rel="noopener noreferrer"
class="github-star-badge"
aria-label="Star on GitHub"
>
<svg
class="star-icon"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
>
<path
d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"
/>
</svg>
<span v-if="isLoading" class="stars-count">...</span>
<span v-else-if="stars !== null" class="stars-count">{{ formatNumber(stars) }}</span>
<span v-else class="stars-count">Star</span>
</a>
</template>
<style scoped>
.github-star-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.4375rem 0.875rem;
background: linear-gradient(135deg, #f6f8fa 0%, #f1f3f5 100%);
border: 1px solid #d0d7de;
border-radius: 24px;
color: #24292f;
text-decoration: none;
font-size: 0.875rem;
font-weight: 600;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
}
.github-star-badge:hover {
background: linear-gradient(135deg, #ffffff 0%, #f6f8fa 100%);
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.08);
}
.github-star-badge:active {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
}
@media (prefers-color-scheme: dark) {
.github-star-badge {
background: linear-gradient(135deg, #161b22 0%, #0d1117 100%);
border-color: #30363d;
color: #c9d1d9;
}
.github-star-badge:hover {
background: linear-gradient(135deg, #21262d 0%, #161b22 100%);
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
}
.star-icon {
flex-shrink: 0;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
transition: transform 0.25s ease;
}
.github-star-badge:hover .star-icon {
transform: scale(1.15) rotate(8deg);
}
.stars-count {
font-variant-numeric: tabular-nums;
letter-spacing: 0.025em;
min-width: 2ch;
text-align: center;
}
.stars-count:empty::before {
content: '...';
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
</style>
@@ -0,0 +1,139 @@
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import { onMounted, ref } from 'vue'
const stars = ref<number | null>(null)
const isLoading = ref(true)
onMounted(async () => {
try {
const response = await fetch('https://api.github.com/repos/Simon-He95/markstream-vue')
if (response.ok) {
const data = await response.json()
stars.value = data.stargazers_count
}
}
catch {
// Silently fail if API request fails
}
finally {
isLoading.value = false
}
})
function formatNumber(num: number): string {
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}k`
}
return num.toString()
}
</script>
<template>
<DefaultTheme.Layout>
<template #nav-bar-content-after>
<a
href="https://github.com/Simon-He95/markstream-vue"
target="_blank"
rel="noopener noreferrer"
class="github-star-badge-nav"
aria-label="Star on GitHub"
>
<svg
class="star-icon"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
>
<path
d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"
/>
</svg>
<span v-if="isLoading" class="stars-count">...</span>
<span v-else-if="stars !== null" class="stars-count">{{ formatNumber(stars) }}</span>
<span v-else class="stars-count">Star</span>
</a>
</template>
</DefaultTheme.Layout>
</template>
<style>
.github-star-badge-nav {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
margin-left: 0.75rem;
background: linear-gradient(135deg, #f6f8fa 0%, #f1f3f5 100%);
border: 1px solid #d0d7de;
border-radius: 20px;
color: #24292f;
text-decoration: none;
font-size: 0.8125rem;
font-weight: 600;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.github-star-badge-nav:hover {
background: linear-gradient(135deg, #ffffff 0%, #f6f8fa 100%);
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.github-star-badge-nav:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.dark .github-star-badge-nav {
background: linear-gradient(135deg, #161b22 0%, #0d1117 100%);
border-color: #30363d;
color: #c9d1d9;
}
.dark .github-star-badge-nav:hover {
background: linear-gradient(135deg, #21262d 0%, #161b22 100%);
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.github-star-badge-nav .star-icon {
flex-shrink: 0;
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
transition: transform 0.25s ease;
}
.github-star-badge-nav:hover .star-icon {
transform: scale(1.1) rotate(5deg);
}
.github-star-badge-nav .stars-count {
font-variant-numeric: tabular-nums;
letter-spacing: 0.025em;
}
/* Animation for loading state */
.github-star-badge-nav .stars-count {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Remove animation when loaded */
.github-star-badge-nav .stars-count:not(:empty) {
animation: none;
}
</style>
@@ -0,0 +1,189 @@
<script setup lang="ts">
import { withBase } from 'vitepress'
interface Props {
title: string
description: string
note?: string
alipayLabel?: string
wechatLabel?: string
}
const props = withDefaults(defineProps<Props>(), {
alipayLabel: 'Alipay',
wechatLabel: 'WeChat Pay',
})
const alipaySrc = withBase('/sponsor/zhifubao.jpg')
const wechatSrc = withBase('/sponsor/weixin.jpg')
</script>
<template>
<section class="support-qrs">
<div class="support-qrs__copy">
<h2 class="support-qrs__title">
{{ props.title }}
</h2>
<p class="support-qrs__description">
{{ props.description }}
</p>
</div>
<div class="support-qrs__grid">
<figure class="support-qrs__card">
<div class="support-qrs__image-frame">
<img
class="support-qrs__image"
:src="alipaySrc"
:alt="`${props.alipayLabel} QR code`"
loading="lazy"
>
</div>
<figcaption class="support-qrs__label">
{{ props.alipayLabel }}
</figcaption>
</figure>
<figure class="support-qrs__card">
<div class="support-qrs__image-frame">
<img
class="support-qrs__image"
:src="wechatSrc"
:alt="`${props.wechatLabel} QR code`"
loading="lazy"
>
</div>
<figcaption class="support-qrs__label">
{{ props.wechatLabel }}
</figcaption>
</figure>
</div>
<p v-if="props.note" class="support-qrs__note">
{{ props.note }}
</p>
</section>
</template>
<style scoped>
.support-qrs {
margin: 1.5rem 0 2.25rem;
padding: 1.25rem;
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
background:
radial-gradient(circle at top left, rgba(61, 120, 255, 0.12), transparent 34%),
linear-gradient(180deg, rgba(248, 250, 252, 0.92), rgba(255, 255, 255, 0.98));
}
.support-qrs__copy {
text-align: center;
}
.support-qrs__title {
margin: 0;
font-size: 1.25rem;
line-height: 1.25;
}
.support-qrs__description {
max-width: 44rem;
margin: 0.6rem auto 0;
color: var(--vp-c-text-2);
line-height: 1.7;
}
.support-qrs__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
margin-top: 1.25rem;
}
.support-qrs__card {
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 16px;
background: rgba(255, 255, 255, 0.84);
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
}
.support-qrs__image-frame {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 320px;
padding: 0.875rem;
border-radius: 14px;
background: linear-gradient(180deg, rgba(248, 250, 252, 0.94), rgba(241, 245, 249, 0.9));
}
.support-qrs__image {
width: auto;
max-width: min(100%, 240px);
max-height: 320px;
height: auto;
object-fit: contain;
border-radius: 12px;
background: #fff;
box-shadow: 0 16px 32px rgba(15, 23, 42, 0.12);
}
.support-qrs__label {
margin-top: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.support-qrs__note {
margin: 1rem auto 0;
text-align: center;
color: var(--vp-c-text-2);
font-size: 0.95rem;
line-height: 1.7;
}
.dark .support-qrs {
background:
radial-gradient(circle at top left, rgba(122, 162, 255, 0.16), transparent 36%),
linear-gradient(180deg, rgba(16, 24, 40, 0.92), rgba(10, 15, 26, 0.98));
}
.dark .support-qrs__card {
background: rgba(15, 23, 42, 0.78);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.24);
}
.dark .support-qrs__image-frame {
background: linear-gradient(180deg, rgba(30, 41, 59, 0.82), rgba(15, 23, 42, 0.92));
}
@media (max-width: 640px) {
.support-qrs {
padding: 1rem;
border-radius: 16px;
}
.support-qrs__grid {
grid-template-columns: 1fr;
}
.support-qrs__card {
padding: 0.875rem;
}
.support-qrs__image-frame {
min-height: 260px;
padding: 0.75rem;
}
.support-qrs__image {
max-height: 260px;
}
}
</style>
@@ -0,0 +1,21 @@
import { useData } from 'vitepress'
/**
* VitePress theme composable for dark mode
* Uses VitePress's built-in isDark from useData()
*
* @example
* ```vue
* <script setup>
* const isDark = useDark()
* </script>
*
* <template>
* <MarkdownRender :is-dark="isDark" />
* </template>
* ```
*/
export function useDark() {
const { isDark } = useData()
return isDark
}
@@ -0,0 +1,24 @@
import type { EnhanceAppContext } from 'vitepress'
import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client'
import MarkdownRender from 'markstream-vue'
import Theme from 'vitepress/theme'
import GitHubStarBadge from './GitHubStarBadge.vue'
import Layout from './Layout.vue'
import SupportQRCodes from './SupportQRCodes.vue'
import '@shikijs/vitepress-twoslash/style.css'
import 'markstream-vue/index.css'
import './style.css'
export default {
extends: Theme,
Layout,
enhanceApp({ app }: EnhanceAppContext) {
app.use(TwoslashFloatingVue)
app.component('GitHubStarBadge', GitHubStarBadge)
app.component('MarkdownRender', MarkdownRender)
app.component('SupportQRCodes', SupportQRCodes)
},
}
// Export useDark for use in VitePress markdown files and components
export { useDark } from './composables/useDark'
@@ -0,0 +1 @@
/* Custom styles for markstream-vue theme */
@@ -0,0 +1,61 @@
import type { DefineComponent } from 'vue'
import type { BaseNode } from 'stream-markdown-parser'
import type {
CodeBlockDiffHunkActionContext,
CodeBlockMonacoOptions,
CodeBlockMonacoTheme,
CodeBlockNodeProps,
CustomComponents,
D2BlockNodeProps,
ImageNodeProps,
MermaidBlockEvent,
MermaidBlockNodeProps,
PreCodeNodeProps,
} from '../../../src/exports'
import type { NodeRendererProps } from '../../../src/types/node-renderer-props'
export type {
BaseNode,
CodeBlockDiffHunkActionContext,
CodeBlockMonacoOptions,
CodeBlockMonacoTheme,
CodeBlockNodeProps,
CustomComponents,
D2BlockNodeProps,
ImageNodeProps,
MermaidBlockEvent,
MermaidBlockNodeProps,
NodeRendererProps,
PreCodeNodeProps,
}
export {
CodeBlockNode,
D2BlockNode,
ImageNode,
MarkdownCodeBlockNode,
MermaidBlockNode,
PreCodeNode,
createKaTeXWorkerFromCDN,
createMermaidWorkerFromCDN,
enableD2,
enableKatex,
enableMermaid,
getMarkdown,
getUseMonaco,
parseMarkdownToStructure,
preloadExtendedLanguageIcons,
removeCustomComponents,
setCustomComponents,
setD2Loader,
setDefaultI18nMap,
setDefaultMathOptions,
setKaTeXWorker,
setKatexLoader,
setMermaidLoader,
setMermaidWorker,
} from '../../../src/exports'
export declare const MarkdownRender: DefineComponent<NodeRendererProps>
export default MarkdownRender