feat: support mermaid

This commit is contained in:
YunYouJun 2024-02-10 22:27:42 +08:00
parent 91747dd520
commit 8daff16c88
16 changed files with 1014 additions and 115 deletions

View File

@ -85,6 +85,7 @@ declare module 'vue-router/auto/routes' {
'/posts/long-toc': RouteRecordInfo<'/posts/long-toc', '/posts/long-toc', Record<never, never>, Record<never, never>>,
'/posts/lots-of-images': RouteRecordInfo<'/posts/lots-of-images', '/posts/lots-of-images', Record<never, never>, Record<never, never>>,
'/posts/markdown': RouteRecordInfo<'/posts/markdown', '/posts/markdown', Record<never, never>, Record<never, never>>,
'/posts/mermaid': RouteRecordInfo<'/posts/mermaid', '/posts/mermaid', Record<never, never>, Record<never, never>>,
'/posts/post-updated': RouteRecordInfo<'/posts/post-updated', '/posts/post-updated', Record<never, never>, Record<never, never>>,
'/posts/redirect': RouteRecordInfo<'/posts/redirect', '/posts/redirect', Record<never, never>, Record<never, never>>,
'/posts/test': RouteRecordInfo<'/posts/test', '/posts/test', Record<never, never>, Record<never, never>>,

View File

@ -0,0 +1,118 @@
---
title: Mermaid
---
- [Mermaid](https://mermaid.js.org/)
## Flowchart
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
```txt
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
## Sequence diagram
```mermaid
sequenceDiagram
participant Alice
participant Bob
Alice->>John: Hello John, how are you?
loop Healthcheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts <br/>prevail!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
```
## Gantt diagram
```mermaid
gantt
dateFormat YYYY-MM-DD
title Adding GANTT diagram to mermaid
excludes weekdays 2014-01-10
section A section
Completed task :done, des1, 2014-01-06,2014-01-08
Active task :active, des2, 2014-01-09, 3d
Future task : des3, after des2, 5d
Future task2 : des4, after des3, 5d
```
## Class diagram
```mermaid
classDiagram
Class01 <|-- AveryLongClass : Cool
Class03 *-- Class04
Class05 o-- Class06
Class07 .. Class08
Class09 --> C2 : Where am i?
Class09 --* C3
Class09 --|> Class07
Class07 : equals()
Class07 : Object[] elementData
Class01 : size()
Class01 : int chimp
Class01 : int gorilla
Class08 <--> C2: Cool label
```
## Git graph
```mermaid
gitGraph
commit
commit
branch develop
commit
commit
commit
checkout main
commit
commit
```
## Quadrant Chart
```mermaid
quadrantChart
title Reach and engagement of campaigns
x-axis Low Reach --> High Reach
y-axis Low Engagement --> High Engagement
quadrant-1 We should expand
quadrant-2 Need to promote
quadrant-3 Re-evaluate
quadrant-4 May be improved
Campaign A: [0.3, 0.6]
Campaign B: [0.45, 0.23]
Campaign C: [0.57, 0.69]
Campaign D: [0.78, 0.34]
Campaign E: [0.40, 0.34]
Campaign F: [0.35, 0.78]
```
## XY Chart
```mermaid
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
```

View File

@ -5,7 +5,6 @@ import { addonAlgolia } from 'valaxy-addon-algolia'
import { addonBangumi } from 'valaxy-addon-bangumi'
import { addonComponents } from 'valaxy-addon-components'
import { addonLightGallery } from 'valaxy-addon-lightgallery'
import { addonMeting } from 'valaxy-addon-meting'
import { addonTest } from 'valaxy-addon-test'
import { addonWaline } from 'valaxy-addon-waline'
@ -85,14 +84,14 @@ export default defineValaxyConfig<ThemeConfig>({
comment: true,
}),
addonLightGallery(),
addonMeting({
global: true,
props: {
id: '2049540645',
server: 'netease',
type: 'song',
},
}),
// addonMeting({
// global: true,
// props: {
// id: '2049540645',
// server: 'netease',
// type: 'song',
// },
// }),
// addonTwikoo({
// envId: 'https://twikoo.vercel.app',
// }),

View File

@ -0,0 +1,75 @@
<!--
Mermaid
(auto transformed, you don't need to use this component directly)
Usage:
```mermaid
pie
"Dogs" : 386
"Cats" : 85
"Rats" : 15
```
-->
<script setup lang="ts">
import { getCurrentInstance, ref, watch, watchEffect } from 'vue'
import { renderMermaid } from '../../modules/mermaid'
import ShadowRoot from '../internals/ShadowRoot.vue'
import { isDark } from '../../composables'
const props = defineProps<{
code: string
scale?: number
theme?: string
}>()
const vm = getCurrentInstance()
const el = ref<ShadowRoot>()
const html = ref('')
watchEffect(async (onCleanup) => {
let disposed = false
onCleanup(() => {
disposed = true
})
const svg = await renderMermaid(
props.code || '',
{
theme: props.theme || (isDark.value ? 'dark' : undefined),
...vm!.attrs,
},
)
if (!disposed)
html.value = svg
})
const actualHeight = ref<number>()
watch(html, () => {
actualHeight.value = undefined
})
watchEffect(() => {
const svgEl = el.value?.children?.[0] as SVGElement | undefined
if (svgEl && svgEl.hasAttribute('viewBox') && actualHeight.value == null) {
const v = Number.parseFloat(svgEl.getAttribute('viewBox')?.split(' ')[3] || '')
actualHeight.value = Number.isNaN(v) ? undefined : v
}
}, { flush: 'post' })
watchEffect(() => {
const svgEl = el.value?.children?.[0] as SVGElement | undefined
if (svgEl != null && props.scale != null && actualHeight.value != null) {
svgEl.setAttribute('height', `${actualHeight.value * props.scale}`)
svgEl.removeAttribute('width')
svgEl.removeAttribute('style')
}
}, { flush: 'post' })
</script>
<template>
<ShadowRoot class="mermaid" :inner-html="html" @shadow="el = $event" />
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
const props = defineProps<{
innerHtml: string
}>()
const emit = defineEmits<{
(event: 'shadow', div: ShadowRoot): void
}>()
const el = ref<HTMLDivElement>()
const shadow = computed(() => el.value ? (el.value.shadowRoot || el.value.attachShadow({ mode: 'open' })) : null)
watchEffect(() => {
if (shadow.value && props.innerHtml) {
emit('shadow', shadow.value)
shadow.value.innerHTML = props.innerHtml
}
})
</script>
<template>
<div ref="el" />
</template>

View File

@ -0,0 +1,32 @@
import mermaid from 'mermaid'
import { customAlphabet } from 'nanoid'
import { decode } from 'js-base64'
import { clearUndefined } from '@antfu/utils'
import { isClient } from '@vueuse/core'
import setupMermaid from '../setup/mermaid'
if (isClient) {
mermaid.startOnLoad = false
mermaid.initialize({ startOnLoad: false })
}
const nanoid = customAlphabet('abcedfghicklmn', 10)
const cache = new Map<string, string>()
export async function renderMermaid(encoded: string, options: any) {
const key = encoded + JSON.stringify(options)
const _cache = cache.get(key)
if (_cache)
return _cache
mermaid.initialize({
startOnLoad: false,
...clearUndefined(setupMermaid() || {}),
...clearUndefined(options),
})
const code = decode(encoded)
const id = nanoid()
const { svg } = await mermaid.render(id, code)
cache.set(key, svg)
return svg
}

View File

@ -0,0 +1,15 @@
/* __imports__ */
import { defineMermaidSetup } from 'valaxy'
import type { MermaidOptions } from '../types'
export default defineMermaidSetup(() => {
// eslint-disable-next-line prefer-const
let injection_return: MermaidOptions = {
theme: 'default',
}
/* __injections__ */
return injection_return
})

View File

@ -1,10 +1,20 @@
import type { ViteSSGContext } from 'vite-ssg'
import type { Awaitable } from '@antfu/utils'
import type { MermaidOptions } from './types'
// todo dynamic mermaid import
export type AppContext = ViteSSGContext
export type AppSetup = (ctx: AppContext) => Awaitable<void>
// client
export type MermaidSetup = () => Partial<MermaidOptions> | void
export function defineAppSetup(fn: AppSetup) {
return fn
}
export function defineMermaidSetup(fn: MermaidSetup) {
return fn
}

View File

@ -1,3 +1,6 @@
import type { ViteSSGContext } from 'vite-ssg'
import type mermaid from 'mermaid'
export type UserModule = (ctx: ViteSSGContext) => void
export type MermaidOptions = (typeof mermaid.initialize) extends (a: infer A) => any ? A : never

View File

@ -1,27 +0,0 @@
import type MarkdownIt from 'markdown-it'
import { defaultCodeTheme } from '../../setup'
import { highlight } from '../highlight'
import type { ResolvedValaxyOptions } from '../../../../options'
/**
* Escape `{{}}` in code block to prevent Vue interpret it, #99
*/
export function escapeVueInCode(md: string) {
return md.replace(/{{(.*?)}}/g, '&lbrace;&lbrace;$1&rbrace;&rbrace;')
}
export function setupShiki(mdIt: MarkdownIt, highlight: any) {
mdIt.options.highlight = (code, lang = 'text', attrs) => {
return highlight(code, lang, attrs)
}
}
export async function createMdItShikiPlugin(options?: ResolvedValaxyOptions) {
const mdOptions = options?.config.markdown || {}
const theme = mdOptions.theme ?? defaultCodeTheme
const shikiHighlight = await highlight(theme, mdOptions)
return function (mdIt: MarkdownIt) {
setupShiki(mdIt, shikiHighlight)
}
}

View File

@ -10,8 +10,6 @@ import TaskLists from 'markdown-it-task-lists'
// https://www.npmjs.com/package/markdown-it-image-figures
import imageFigures from 'markdown-it-image-figures'
import { componentPlugin } from '@mdit-vue/plugin-component'
import {
type HeadersPluginOptions,
headersPlugin,
@ -33,7 +31,6 @@ import { preWrapperPlugin } from './plugins/markdown-it/preWrapper'
import { lineNumberPlugin } from './plugins/markdown-it/lineNumbers'
import { snippetPlugin } from './plugins/markdown-it/snippet'
import type { ThemeOptions } from './types'
import { createMdItShikiPlugin } from './plugins/markdown-it/shiki'
export const defaultCodeTheme = { light: 'github-light', dark: 'github-dark' } as const as ThemeOptions
@ -51,12 +48,6 @@ export async function setupMarkdownPlugins(
if (mdOptions.preConfig)
mdOptions.preConfig(md)
// highlight
const shikiPlugin = await createMdItShikiPlugin(options)
md.use(shikiPlugin)
// mdit-vue plugins
md.use(componentPlugin, { ...mdOptions.component })
// custom plugins
md.use(highlightLinePlugin)
.use(preWrapperPlugin, { theme, siteConfig })

View File

@ -3,7 +3,8 @@ import Markdown from 'unplugin-vue-markdown/vite'
import * as base64 from 'js-base64'
import type { ResolvedValaxyOptions } from '../../options'
import { setupMarkdownPlugins } from '.'
import { highlight } from './plugins/highlight'
import { defaultCodeTheme, setupMarkdownPlugins } from '.'
/**
* Transform Mermaid code blocks (render done on client side)
@ -14,7 +15,7 @@ export function transformMermaid(md: string): string {
code = code.trim()
options = options.trim() || '{}'
const encoded = base64.encode(code, true)
return `<Mermaid :code="'${encoded}'" v-bind="${options}" />`
return `<ValaxyMermaid :code="'${encoded}'" v-bind="${options}" />`
})
}
@ -22,24 +23,26 @@ export async function createMarkdownPlugin(
options: ResolvedValaxyOptions,
): Promise<Plugin> {
const mdOptions = options?.config.markdown || {}
const theme = mdOptions.theme ?? defaultCodeTheme
return Markdown({
include: [/\.md$/],
wrapperClasses: '',
// headEnabled: false,
frontmatter: true,
frontmatterOptions: mdOptions.frontmatter || {},
// v-pre
escapeCodeTagInterpolation: true,
// markdownItOptions: {
// quotes: '""\'\'',
// html: true,
// xhtmlOut: true,
// linkify: true,
// ...mdOptions?.markdownItOptions,
// },
markdownItOptions: {
quotes: '""\'\'',
html: true,
xhtmlOut: true,
linkify: true,
highlight: await highlight(theme, mdOptions),
...mdOptions?.markdownItOptions,
},
async markdownItSetup(mdIt) {
// setup mdIt
@ -65,6 +68,9 @@ export async function createMarkdownPlugin(
return code
},
// frontmatterOptions componentOptions in mdOptions
...mdOptions,
},
}) as Plugin
}

View File

@ -12,9 +12,6 @@ import type anchorPlugin from 'markdown-it-anchor'
import type { KatexOptions } from 'katex'
import type {
FrontmatterPluginOptions,
} from '@mdit-vue/plugin-frontmatter'
import type {
HeadersPluginOptions,
} from '@mdit-vue/plugin-headers'
@ -23,9 +20,6 @@ import type { TocPluginOptions } from '@mdit-vue/plugin-toc'
// import type { lazyloadOptions } from './plugins/markdown-it/lazyload'
import type {
ComponentPluginOptions,
} from '@mdit-vue/plugin-component'
import type { Blocks, ContainerOptions } from './plugins/markdown-it/container'
export type ThemeOptions =
@ -105,11 +99,6 @@ export interface MarkdownOptions {
/* ==================== Markdown It Plugins ==================== */
// mdit-vue plugins
/**
* Options for `@mdit-vue/plugin-frontmatter`
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-frontmatter
*/
frontmatter?: FrontmatterPluginOptions
/**
* Options for `@mdit-vue/plugin-headers`
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers
@ -134,11 +123,6 @@ export interface MarkdownOptions {
* Custom block configurations based on `markdown-it-container`
*/
blocks?: Blocks
/**
* Options for `@mdit-vue/plugin-component`
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-component
*/
component?: ComponentPluginOptions
/**
* @see [markdown-it-image-figures](https://www.npmjs.com/package/markdown-it-image-figures)

View File

@ -106,6 +106,7 @@
"markdown-it-table-of-contents": "^0.6.0",
"markdown-it-task-lists": "^2.1.1",
"medium-zoom": "^1.1.0",
"mermaid": "^10.8.0",
"nprogress": "^0.2.0",
"open": "10.0.3",
"ora": "^8.0.1",

View File

@ -68,3 +68,9 @@ declare module '/@valaxyjs/redirects' {
export const useVueRouter: boolean
}
declare module 'mermaid/dist/mermaid.esm.mjs' {
import Mermaid from 'mermaid/dist/mermaid.d.ts'
export default Mermaid
}

File diff suppressed because it is too large Load Diff