refactor: migrate to unplugin-vue-markdown

This commit is contained in:
YunYouJun 2024-02-10 21:31:21 +08:00
parent 5668599091
commit 91747dd520
31 changed files with 490 additions and 371 deletions

View File

@ -932,3 +932,19 @@ Freedom to control your layout!
</div>
```
## Mermaid
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;

View File

@ -0,0 +1,24 @@
import { range, uniq } from '@antfu/utils'
/**
* 1,3-5,8 => [1, 3, 4, 5, 8]
*/
export function parseRangeString(total: number, rangeStr?: string) {
if (!rangeStr || rangeStr === 'all' || rangeStr === '*')
return range(1, total + 1)
const pages: number[] = []
for (const part of rangeStr.split(/[,;]/g)) {
if (!part.includes('-')) {
pages.push(+part)
}
else {
const [start, end] = part.split('-', 2)
pages.push(
...range(+start, !end ? (total + 1) : (+end + 1)),
)
}
}
return uniq(pages).filter(i => i <= total).sort((a, b) => a - b)
}

View File

@ -1,4 +1,5 @@
export * from './cdn'
export * from './code'
export * from './helper'
export * from './time'
export * from './wrap'

View File

@ -35,7 +35,8 @@ export interface MarkdownEnv {
title?: string
path: string
relativePath: string
cleanUrls: CleanUrlsMode
links?: string[]
realPath?: string
cleanUrls?: CleanUrlsMode
}

View File

@ -0,0 +1,34 @@
import MarkdownIt from 'markdown-it'
import type { Header } from 'valaxy/types'
import type { ResolvedValaxyOptions } from '../../options'
import { highlight } from './plugins/highlight'
import { defaultCodeTheme, setupMarkdownPlugins } from './setup'
export * from './env'
export * from './setup'
export * from './transform'
export interface MarkdownParsedData {
hoistedTags?: string[]
links?: string[]
headers?: Header[]
}
export async function createMarkdownRenderer(options?: ResolvedValaxyOptions): Promise<MarkdownIt> {
const mdOptions = options?.config.markdown || {}
const theme = mdOptions.theme ?? defaultCodeTheme
const md = MarkdownIt({
html: true,
linkify: true,
highlight: await highlight(theme, mdOptions),
...mdOptions.options,
}) as MarkdownIt
md.linkify.set({ fuzzyLink: false })
await setupMarkdownPlugins(md, options)
return md
}

View File

@ -4,52 +4,58 @@ import c from 'picocolors'
import { LRUCache } from 'lru-cache'
import _debug from 'debug'
import { resolveTitleFromToken } from '@mdit-vue/shared'
import type { CleanUrlsMode, HeadConfig, PageData } from 'valaxy/types'
import type { HeadConfig, PageData } from 'valaxy/types'
import path from 'pathe'
import { EXTERNAL_URL_RE } from '../constants'
import { getGitTimestamp, slash, transformObject } from '../utils'
import type { ResolvedValaxyOptions } from '../options'
import { encryptContent } from '../utils/encrypt'
import type MarkdownIt from 'markdown-it'
import { EXTERNAL_URL_RE } from '../../constants'
import { getGitTimestamp, slash, transformObject } from '../../utils'
import type { ResolvedValaxyOptions } from '../../options'
import { encryptContent } from '../../utils/encrypt'
import { processIncludes } from './utils/processInclude'
import { createMarkdownRenderer } from '.'
import type { MarkdownEnv, MarkdownRenderer } from '.'
import type { MarkdownEnv } from '.'
const vueTemplateBreaker = '<wbr>'
const debug = _debug('vitepress:md')
const debug = _debug('valaxy:md')
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 })
const includesRE = /<!--\s*@include:\s*(.*?)\s*-->/g
function genReplaceRegexp(
userDefines: Record<string, any> = {},
isBuild: boolean,
): RegExp {
// `process.env` need to be handled in both dev and build
// @see https://github.com/vitejs/vite/blob/cad27ee8c00bbd5aeeb2be9bfb3eb164c1b77885/packages/vite/src/node/plugins/clientInjections.ts#L57-L64
const replacements = ['process.env']
if (isBuild)
replacements.push('import.meta', ...Object.keys(userDefines))
export function injectPageDataCode(
data: PageData,
_replaceRegex: RegExp,
) {
const vueContextImports = [
`import { provide } from 'vue'`,
`import { useRoute } from 'vue-router'`,
`export const data = ${transformObject(data)}`,
`export default {
name:'${data.relativePath}',
data() {
return { data, frontmatter: data.frontmatter, $frontmatter: data.frontmatter }
},
setup() {
const route = useRoute()
route.meta.frontmatter = Object.assign(route.meta.frontmatter || {}, data.frontmatter || {})
provide('pageData', data)
}
}`,
]
return new RegExp(
`\\b(${replacements
.map(key => key.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'))
.join('|')})`,
'g',
)
return vueContextImports
}
/**
* To avoid env variables being replaced by vite:
* - insert `'\u200b'` char into those strings inside js string (page data)
* - insert `<wbr>` tag into those strings inside html string (vue template)
*
* @see https://vitejs.dev/guide/env-and-mode.html#production-replacement
* valaxy main layout
*/
function replaceConstants(str: string, replaceRegex: RegExp, breaker: string) {
// replace a to AppLink
str = str.replace(/<a (.*?)>(.*?)<\/a>/g, '<AppLink $1>$2</AppLink>')
return str.replace(replaceRegex, _ => `${_[0]}${breaker}${_.slice(1)}`)
export function getValaxyMain(mainContentMd: string) {
const pageComponent = 'ValaxyMain'
// :data="data"
return `
<${pageComponent} :frontmatter="frontmatter" >
<template #main-content-md>${mainContentMd}</template>
${generateSlots()}
<slot />
</${pageComponent}>
`
}
export interface MarkdownCompileResult {
@ -59,7 +65,7 @@ export interface MarkdownCompileResult {
includes: string[]
}
function inferTitle(md: MarkdownRenderer, frontmatter: Record<string, any>, title: string) {
function inferTitle(md: MarkdownIt, frontmatter: Record<string, any>, title: string) {
if (typeof frontmatter.title === 'string') {
const titleToken = md.parseInline(frontmatter.title, {})[0]
if (titleToken) {
@ -107,23 +113,37 @@ function handleCodeHeightlimit(mainContentMd: string, options: ResolvedValaxyOpt
return mainContentMd
}
function generateSlots() {
const slots = [
'main-header',
'main-header-after',
'main-nav',
'main-content',
'main-content-after',
'main-nav-before',
'main-nav-after',
'comment',
'footer',
'aside',
'aside-custom',
]
const slotsText = slots
.map(s => `<template #${s}><slot name="${s}" /></template>`)
.join('')
return slotsText
}
export async function createMarkdownToVueRenderFn(
options: ResolvedValaxyOptions,
srcDir: string,
pages: string[],
userDefines: Record<string, any> | undefined,
isBuild = false,
includeLastUpdatedData = false,
// https://vitepress.vuejs.org/config/app-configs#cleanurls-experimental
cleanUrls: CleanUrlsMode = 'with-subfolders',
) {
const md = await createMarkdownRenderer(options)
// for dead link detection
pages = pages.map(p => p.replace(/\.md$/, '').replace(/\/index$/, ''))
const replaceRegex = genReplaceRegexp(userDefines, isBuild)
return async (
src: string,
file: string,
@ -162,7 +182,6 @@ export async function createMarkdownToVueRenderFn(
const env: MarkdownEnv = {
path: file,
relativePath,
cleanUrls,
realPath: fileOrig,
}
@ -171,7 +190,6 @@ export async function createMarkdownToVueRenderFn(
frontmatter = {},
headers = [],
links = [],
sfcBlocks,
title = '',
} = env
@ -241,33 +259,7 @@ export async function createMarkdownToVueRenderFn(
if (includeLastUpdatedData)
pageData.lastUpdated = await getGitTimestamp(file)
const pageComponent = 'ValaxyMain'
function generateSlots() {
const slots = [
'main-header',
'main-header-after',
'main-nav',
'main-content',
'main-content-after',
'main-nav-before',
'main-nav-after',
'comment',
'footer',
'aside',
'aside-custom',
]
const slotsText = slots
.map(s => `<template #${s}><slot name="${s}" /></template>`)
.join('')
return slotsText
}
let mainContentMd = replaceConstants(
html,
replaceRegex,
vueTemplateBreaker,
)
let mainContentMd = html.replace(/<a (.*?)>(.*?)<\/a>/g, '<AppLink $1>$2</AppLink>')
mainContentMd = handleCodeHeightlimit(mainContentMd, options, frontmatter.codeHeightLimit)
@ -321,18 +313,8 @@ export async function createMarkdownToVueRenderFn(
}
const vueSrc = [
...injectPageDataCode(
sfcBlocks?.scripts.map(item => item.content) ?? [],
pageData,
replaceRegex,
),
`<template><${pageComponent} :frontmatter="frontmatter" :data="data">`,
`<template #main-content-md>${mainContentMd}</template>`,
generateSlots(),
'<slot />',
`</${pageComponent}></template>`,
...(sfcBlocks?.styles.map(item => item.content) ?? []),
...(sfcBlocks?.customBlocks.map(item => item.content) ?? []),
// ...injectPageDataCode(),
getValaxyMain(mainContentMd),
].join('\n')
debug(`[render] ${file} in ${Date.now() - start}ms.`)
@ -347,66 +329,3 @@ export async function createMarkdownToVueRenderFn(
return result
}
}
const scriptRE = /<\/script>/
const scriptLangTsRE = /<\s*script[^>]*\blang=['"]ts['"][^>]*/
const scriptSetupRE = /<\s*script[^>]*\bsetup\b[^>]*/
const scriptClientRE = /<\s*script[^>]*\bclient\b[^>]*/
const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/
const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)as(\s*)default/
function injectPageDataCode(
tags: string[],
data: PageData,
_replaceRegex: RegExp,
) {
const existingScriptIndex = tags.findIndex((tag) => {
return (
scriptRE.test(tag)
&& !scriptSetupRE.test(tag)
&& !scriptClientRE.test(tag)
)
})
const isUsingTS = tags.findIndex(tag => scriptLangTsRE.test(tag)) > -1
// merge lastUpdated
const exportScript = `
import { provide } from 'vue'
import { useRoute } from 'vue-router'
export const data = ${transformObject(data)}
export default {
name:'${data.relativePath}',
data() {
return { data, frontmatter: data.frontmatter, $frontmatter: data.frontmatter }
},
setup() {
const route = useRoute()
route.meta.frontmatter = Object.assign(route.meta.frontmatter || {}, data.frontmatter || {})
provide('pageData', data)
}
}`
if (existingScriptIndex > -1) {
const tagSrc = tags[existingScriptIndex]
// user has <script> tag inside markdown
// if it doesn't have export default it will error out on build
const hasDefaultExport
= defaultExportRE.test(tagSrc) || namedDefaultExportRE.test(tagSrc)
tags[existingScriptIndex] = tagSrc.replace(
scriptRE,
`${
(hasDefaultExport
? ''
: `\n${exportScript}`)
}</script>`,
)
}
else {
tags.unshift(
`<script ${isUsingTS ? 'lang="ts"' : ''}>\n${exportScript}</script>`,
)
}
return tags
}

View File

@ -6,7 +6,7 @@
import { URL } from 'node:url'
import type MarkdownIt from 'markdown-it'
import { EXTERNAL_URL_RE, PATHNAME_PROTOCOL_RE } from '../../constants'
import { EXTERNAL_URL_RE, PATHNAME_PROTOCOL_RE } from '../../../constants'
import type { MarkdownEnv } from '../env'
const indexRE = /(^|.*\/)index.md(#?.*)$/i

View File

@ -122,11 +122,6 @@ export function containerPlugin(md: MarkdownIt, options: Options, containerOptio
})
md.use(...createCodeGroup(options))
// explicitly escape Vue syntax
md.use(container, 'v-pre', {
render: (tokens: Token[], idx: number) => tokens[idx].nesting === 1 ? '<div v-pre>\n' : '</div>\n',
})
const languages = ['zh-CN', 'en']
languages.forEach((lang) => {
md.use(container, lang, {

View File

@ -0,0 +1,27 @@
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

@ -1,4 +1,4 @@
import path from 'node:path'
import path from 'pathe'
import fs from 'fs-extra'
import type MarkdownIt from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block'

View File

@ -1,4 +1,4 @@
import MarkdownIt from 'markdown-it'
import type MarkdownIt from 'markdown-it'
import anchorPlugin from 'markdown-it-anchor'
import attrsPlugin from 'markdown-it-attrs'
@ -11,10 +11,7 @@ import TaskLists from 'markdown-it-task-lists'
import imageFigures from 'markdown-it-image-figures'
import { componentPlugin } from '@mdit-vue/plugin-component'
import {
type FrontmatterPluginOptions,
frontmatterPlugin,
} from '@mdit-vue/plugin-frontmatter'
import {
type HeadersPluginOptions,
headersPlugin,
@ -25,11 +22,10 @@ import { type TocPluginOptions, tocPlugin } from '@mdit-vue/plugin-toc'
import { slugify } from '@mdit-vue/shared'
import { cssI18nContainer } from 'css-i18n'
import type { Header } from 'valaxy/types'
import type { ResolvedValaxyOptions } from '../options'
import type { ResolvedValaxyOptions } from '../../options'
import Katex from './plugins/markdown-it/katex'
import { containerPlugin } from './plugins/markdown-it/container'
import { highlight } from './plugins/highlight'
import { highlightLinePlugin } from './plugins/markdown-it/highlightLines'
import { linkPlugin } from './plugins/link'
@ -37,16 +33,7 @@ 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'
export * from './env'
export interface MarkdownParsedData {
hoistedTags?: string[]
links?: string[]
headers?: Header[]
}
export type MarkdownRenderer = MarkdownIt
import { createMdItShikiPlugin } from './plugins/markdown-it/shiki'
export const defaultCodeTheme = { light: 'github-light', dark: 'github-dark' } as const as ThemeOptions
@ -64,6 +51,10 @@ 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
@ -121,9 +112,8 @@ export async function setupMarkdownPlugins(
...mdOptions.anchor,
})
}
md.use(frontmatterPlugin, {
...mdOptions.frontmatter,
} as FrontmatterPluginOptions)
md
.use(headersPlugin, {
slugify,
...(typeof mdOptions.headers === 'boolean' ? undefined : mdOptions.headers),
@ -166,22 +156,5 @@ export async function setupMarkdownPlugins(
if (mdOptions.config)
mdOptions.config(md)
return md as MarkdownRenderer
}
export async function createMarkdownRenderer(options?: ResolvedValaxyOptions): Promise<MarkdownRenderer> {
const mdOptions = options?.config.markdown || {}
const theme = mdOptions.theme ?? defaultCodeTheme
const md = MarkdownIt({
html: true,
linkify: true,
highlight: await highlight(theme, mdOptions),
...mdOptions.options,
}) as MarkdownRenderer
md.linkify.set({ fuzzyLink: false })
await setupMarkdownPlugins(md, options)
return md
return md as MarkdownIt
}

View File

@ -0,0 +1,70 @@
import type { Plugin } from 'vite'
import Markdown from 'unplugin-vue-markdown/vite'
import * as base64 from 'js-base64'
import type { ResolvedValaxyOptions } from '../../options'
import { setupMarkdownPlugins } from '.'
/**
* Transform Mermaid code blocks (render done on client side)
*/
export function transformMermaid(md: string): string {
return md
.replace(/^```mermaid\s*?({.*?})?\n([\s\S]+?)\n```/mg, (full, options = '', code = '') => {
code = code.trim()
options = options.trim() || '{}'
const encoded = base64.encode(code, true)
return `<Mermaid :code="'${encoded}'" v-bind="${options}" />`
})
}
export async function createMarkdownPlugin(
options: ResolvedValaxyOptions,
): Promise<Plugin> {
const mdOptions = options?.config.markdown || {}
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,
// },
async markdownItSetup(mdIt) {
// setup mdIt
await setupMarkdownPlugins(mdIt, options, true)
options?.config.markdown?.markdownItSetup?.(mdIt)
},
transforms: {
before(code, _id) {
// const monaco = (config.monaco === true || config.monaco === mode)
// ? transformMarkdownMonaco
// : truncateMancoMark
// code = transformSlotSugar(code)
// code = transformSnippet(code, options, id)
code = transformMermaid(code)
// code = transformPlantUml(code, config.plantUmlServer)
// code = monaco(code)
// code = transformHighlighter(code)
// code = transformPageCSS(code, id)
// code = transformKaTex(code)
return code
},
},
}) as Plugin
}

View File

@ -0,0 +1,2 @@
export * from './check'
export * from './processInclude'

View File

@ -12,14 +12,14 @@ import UnheadVite from '@unhead/addons/vite'
import { resolve } from 'pathe'
import type { ResolvedValaxyOptions, ValaxyServerOptions } from '../options'
import { setupMarkdownPlugins } from '../markdown'
import { createUnocssPlugin } from './unocss'
import { createConfigPlugin } from './extendConfig'
import { createClientSetupPlugin } from './setupClient'
import { createFixPlugins } from './patchTransform'
import { createRouterPlugin } from './vueRouter'
import { createValaxyPlugin } from './valaxy'
import { createValaxyLoader } from './valaxy'
import { createMarkdownPlugin } from './markdown'
// for render markdown excerpt
export const mdIt = new MarkdownIt({ html: true })
@ -30,9 +30,6 @@ export async function ViteValaxyPlugins(
): Promise<(PluginOption | PluginOption[])[]> {
const { roots, config: valaxyConfig } = options
// setup mdIt
await setupMarkdownPlugins(mdIt, options, true)
const customElements = new Set([
// katex
'annotation',
@ -68,13 +65,15 @@ export async function ViteValaxyPlugins(
'meting-js',
])
const MarkdownPlugin = await createMarkdownPlugin(options)
return [
createValaxyPlugin(options, serverOptions),
MarkdownPlugin,
createConfigPlugin(options),
createClientSetupPlugin(options),
Vue({
include: [/\.vue$/, /\.md$/],
exclude: [],
template: {
compilerOptions: {
isCustomElement: (tag) => {
@ -86,6 +85,8 @@ export async function ViteValaxyPlugins(
...valaxyConfig.vue,
}),
createValaxyLoader(options, serverOptions),
UnheadVite(),
// https://github.com/posva/unplugin-vue-router

View File

@ -2,25 +2,25 @@
* @packageDocumentation valaxy plugin
*/
import { join, relative, resolve } from 'pathe'
import { join, resolve } from 'pathe'
import fs from 'fs-extra'
import type { Plugin, ResolvedConfig } from 'vite'
import type { Plugin } from 'vite'
import { defu } from 'defu'
import pascalCase from 'pascalcase'
import type { DefaultTheme, PageDataPayload, Pkg, SiteConfig } from 'valaxy/types'
import type { DefaultTheme, Pkg, SiteConfig } from 'valaxy/types'
import { dim, yellow } from 'picocolors'
import type { RouteRecordRaw } from 'vue-router'
import { defaultSiteConfig, mergeValaxyConfig, resolveSiteConfig, resolveUserThemeConfig } from '../config'
import type { ResolvedValaxyOptions, ValaxyServerOptions } from '../options'
import { processValaxyOptions, resolveOptions, resolveThemeValaxyConfig } from '../options'
import { resolveImportPath, toAtFS } from '../utils'
import { createMarkdownToVueRenderFn } from '../markdown/markdownToVue'
import type { ValaxyNodeConfig } from '../types'
import { checkMd } from '../markdown/check'
import { vLogger } from '../logger'
import { countPerformanceTime } from '../utils/performance'
import { isProd } from '../utils/env'
import { checkMd } from './markdown/utils'
import { getValaxyMain } from './markdown/markdownToVue'
function generateConfig(options: ResolvedValaxyOptions) {
const routes = options.redirects.map<RouteRecordRaw>((redirect) => {
@ -130,200 +130,230 @@ function generateAppVue(root: string) {
return scripts.join('\n')
}
async function transformMarkdown(code: string, id: string) {
const endCount = countPerformanceTime()
// const path = `/${relative(`${options.userRoot}/pages`, file)}`
const imports = [
``,
]
code = code.replace(/(<script setup.*>)/g, `$1\n${imports.join('\n')}\n`)
const injectA = code.indexOf('<template>') + '<template>'.length
const injectB = code.lastIndexOf('</template>')
let body = code.slice(injectA, injectB).trim()
if (body.startsWith('<div>') && body.endsWith('</div>'))
body = body.slice(5, -6)
code = `${code.slice(0, injectA)}\n${getValaxyMain(body)}\n${code.slice(injectB)}`
vLogger.success(`${yellow('[HMR]')} ${id} ${dim(`updated in ${endCount()}`)}`)
return code
}
/**
* create valaxy plugin (virtual modules)
* create valaxy loader (custom virtual modules)
* multiple plugins
* @internal
* @param options
* @param serverOptions
*/
export function createValaxyPlugin(options: ResolvedValaxyOptions, serverOptions: ValaxyServerOptions = {}): Plugin {
export function createValaxyLoader(options: ResolvedValaxyOptions, serverOptions: ValaxyServerOptions = {}): Plugin[] {
let { config: valaxyConfig } = options
const valaxyPrefix = '/@valaxy'
const roots = options.roots
let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>>
let hasDeadLinks = false
let viteConfig: ResolvedConfig
const hasDeadLinks = false
return {
name: 'valaxy:loader',
enforce: 'pre',
return [
{
name: 'valaxy:loader',
enforce: 'pre',
async configResolved(resolvedConfig) {
viteConfig = resolvedConfig
markdownToVue = await createMarkdownToVueRenderFn(
options,
options.userRoot,
options.pages,
viteConfig.define,
viteConfig.command === 'build',
options.config.siteConfig.lastUpdated,
)
},
// async configResolved(resolvedConfig) {
// // markdownToVue = await createMarkdownToVueRenderFn(
// // options,
// // options.userRoot,
// // options.pages,
// // viteConfig.define,
// // viteConfig.command === 'build',
// // options.config.siteConfig.lastUpdated,
// // )
// },
configureServer(server) {
server.watcher.add([
options.configFile,
options.clientRoot,
options.themeRoot,
options.userRoot,
])
},
configureServer(server) {
server.watcher.add([
options.configFile,
options.clientRoot,
options.themeRoot,
options.userRoot,
])
},
resolveId(id) {
if (id.startsWith(valaxyPrefix))
return id
return null
},
resolveId(id) {
if (id.startsWith(valaxyPrefix))
return id
return null
},
load(id) {
if (id === '/@valaxyjs/config')
// stringify twice for \"
return generateConfig(options)
load(id) {
if (id === '/@valaxyjs/config')
// stringify twice for \"
return generateConfig(options)
if (id === '/@valaxyjs/context') {
return `export default ${JSON.stringify(JSON.stringify({
userRoot: options.userRoot,
// clientRoot: options.clientRoot,
}))}`
}
// generate styles
if (id === '/@valaxyjs/styles')
return generateStyles(roots, options)
if (id === '/@valaxyjs/locales')
return generateLocales(roots)
if (id === '/@valaxyjs/addons')
return generateAddons(options)
if (id === '/@valaxyjs/UserAppVue')
return generateAppVue(options.userRoot)
if (id === '/@valaxyjs/ThemeAppVue')
return generateAppVue(options.themeRoot)
if (id.startsWith(valaxyPrefix)) {
return {
code: '',
map: { mappings: '' },
}
}
},
async transform(code, id) {
if (id.endsWith('.md')) {
checkMd(code, id)
code.replace('{%', '\{\%')
code.replace('%}', '\%\}')
// transform .md files into vueSrc so plugin-vue can handle it
const { vueSrc, deadLinks, includes } = await markdownToVue(
code,
id,
viteConfig.publicDir,
)
if (deadLinks.length)
hasDeadLinks = true
if (includes.length) {
includes.forEach((i) => {
this.addWatchFile(i)
})
if (id === '/@valaxyjs/context') {
return `export default ${JSON.stringify(JSON.stringify({
userRoot: options.userRoot,
// clientRoot: options.clientRoot,
}))}`
}
return vueSrc
}
},
// generate styles
if (id === '/@valaxyjs/styles')
return generateStyles(roots, options)
renderStart() {
if (hasDeadLinks && !valaxyConfig.ignoreDeadLinks)
throw new Error('One or more pages contain dead links.')
},
if (id === '/@valaxyjs/locales')
return generateLocales(roots)
/**
* handle config hmr
* @param ctx
*/
async handleHotUpdate(ctx) {
const endCount = countPerformanceTime()
const { file, server, read } = ctx
if (id === '/@valaxyjs/addons')
return generateAddons(options)
const reloadConfigAndEntries = (config: ValaxyNodeConfig) => {
serverOptions.onConfigReload?.(config, options.config)
Object.assign(options.config, config)
if (id === '/@valaxyjs/UserAppVue')
return generateAppVue(options.userRoot)
valaxyConfig = config
if (id === '/@valaxyjs/ThemeAppVue')
return generateAppVue(options.themeRoot)
const moduleIds = ['/@valaxyjs/config', '/@valaxyjs/context']
const moduleEntries = [
...Array.from(moduleIds).map(id => server.moduleGraph.getModuleById(id)),
].filter(<T>(item: T): item is NonNullable<T> => !!item)
if (id.startsWith(valaxyPrefix)) {
return {
code: '',
map: { mappings: '' },
}
}
},
return moduleEntries
}
async transform(code, id) {
if (id.endsWith('.md')) {
checkMd(code, id)
code.replace('{%', '\{\%')
code.replace('%}', '\%\}')
const configFiles = [options.configFile]
// transform .md files into vueSrc so plugin-vue can handle it
// const { vueSrc, deadLinks, includes } = await markdownToVue(
// code,
// id,
// viteConfig.publicDir,
// )
// if (deadLinks.length)
// hasDeadLinks = true
// handle valaxy.config.ts hmr
if (configFiles.includes(file)) {
const { config } = await resolveOptions({ userRoot: options.userRoot })
return reloadConfigAndEntries(config)
}
// if (includes.length) {
// includes.forEach((i) => {
// this.addWatchFile(i)
// })
// }
// siteConfig
if (file === options.siteConfigFile) {
const { siteConfig } = await resolveSiteConfig(options.userRoot)
valaxyConfig.siteConfig = defu<SiteConfig, [SiteConfig]>(siteConfig, defaultSiteConfig)
return reloadConfigAndEntries(valaxyConfig)
}
return transformMarkdown(code, id)
}
},
// themeConfig
if (file === options.themeConfigFile) {
const { themeConfig } = await resolveUserThemeConfig(options)
const pkg = valaxyConfig.themeConfig.pkg
// @ts-expect-error mount pkg
themeConfig.pkg = pkg
valaxyConfig.themeConfig = themeConfig as (DefaultTheme.Config & { pkg: Pkg })
return reloadConfigAndEntries(valaxyConfig)
}
renderStart() {
if (hasDeadLinks && !valaxyConfig.ignoreDeadLinks)
throw new Error('One or more pages contain dead links.')
},
if (file === resolve(options.themeRoot, 'valaxy.config.ts')) {
const themeValaxyConfig = await resolveThemeValaxyConfig(options)
const valaxyConfig = mergeValaxyConfig(options.config, themeValaxyConfig)
const { config } = await processValaxyOptions(options, valaxyConfig)
return reloadConfigAndEntries(config)
}
/**
* handle config hmr
* @param ctx
*/
async handleHotUpdate(ctx) {
const { file, server } = ctx
// send headers
if (file.endsWith('.md')) {
const content = await read()
const { pageData, vueSrc } = await markdownToVue(
content,
file,
join(options.userRoot, 'public'),
)
const reloadConfigAndEntries = (config: ValaxyNodeConfig) => {
serverOptions.onConfigReload?.(config, options.config)
Object.assign(options.config, config)
const path = `/${relative(`${options.userRoot}/pages`, file)}`
const payload: PageDataPayload = {
path,
pageData,
valaxyConfig = config
const moduleIds = ['/@valaxyjs/config', '/@valaxyjs/context']
const moduleEntries = [
...Array.from(moduleIds).map(id => server.moduleGraph.getModuleById(id)),
].filter(<T>(item: T): item is NonNullable<T> => !!item)
return moduleEntries
}
server.ws.send({
type: 'custom',
event: 'valaxy:pageData',
data: payload,
})
const configFiles = [options.configFile]
// overwrite src so vue plugin can handle the HMR
ctx.read = () => vueSrc
// handle valaxy.config.ts hmr
if (configFiles.includes(file)) {
const { config } = await resolveOptions({ userRoot: options.userRoot })
return reloadConfigAndEntries(config)
}
vLogger.success(`${yellow('[HMR]')} ${path} ${dim(`updated in ${endCount()}`)}`)
}
// siteConfig
if (file === options.siteConfigFile) {
const { siteConfig } = await resolveSiteConfig(options.userRoot)
valaxyConfig.siteConfig = defu<SiteConfig, [SiteConfig]>(siteConfig, defaultSiteConfig)
return reloadConfigAndEntries(valaxyConfig)
}
// themeConfig
if (file === options.themeConfigFile) {
const { themeConfig } = await resolveUserThemeConfig(options)
const pkg = valaxyConfig.themeConfig.pkg
// @ts-expect-error mount pkg
themeConfig.pkg = pkg
valaxyConfig.themeConfig = themeConfig as (DefaultTheme.Config & { pkg: Pkg })
return reloadConfigAndEntries(valaxyConfig)
}
if (file === resolve(options.themeRoot, 'valaxy.config.ts')) {
const themeValaxyConfig = await resolveThemeValaxyConfig(options)
const valaxyConfig = mergeValaxyConfig(options.config, themeValaxyConfig)
const { config } = await processValaxyOptions(options, valaxyConfig)
return reloadConfigAndEntries(config)
}
// send headers
if (file.endsWith('.md')) {
// const { pageData, vueSrc } = await markdownToVue(
// content,
// file,
// join(options.userRoot, 'public'),
// )
// const path = `/${relative(`${options.userRoot}/pages`, file)}`
// const payload: PageDataPayload = {
// path,
// // pageData,
// }
// server.hot.send({
// type: 'custom',
// event: 'valaxy:pageData',
// data: payload,
// })
// overwrite src so vue plugin can handle the HMR
// ctx.read = () => vueSrc
}
},
},
}
{
// ValaxyMain
name: 'valaxy:layout-transform:pre',
enforce: 'pre',
async transform(code, id) {
if (!id.startsWith(valaxyPrefix))
return
if (id.endsWith('.md'))
return transformMarkdown(code, id)
},
},
]
}

View File

@ -2,6 +2,7 @@ import type Vue from '@vitejs/plugin-vue'
import type Components from 'unplugin-vue-components/vite'
import type Layouts from 'vite-plugin-vue-layouts'
import type Markdown from 'unplugin-vue-markdown/vite'
import type Router from 'unplugin-vue-router/vite'
import type { VitePluginConfig as UnoCSSConfig } from 'unocss/vite'
@ -11,7 +12,7 @@ import type { presetAttributify, presetIcons, presetTypography, presetUno } from
import type { Hookable } from 'hookable'
import type { DefaultTheme, PartialDeep, ValaxyAddon, ValaxyConfig } from 'valaxy/types'
import type { ResolvedValaxyOptions } from './options'
import type { MarkdownOptions } from './markdown/types'
import type { MarkdownOptions } from './plugins/markdown/types'
export type ValaxyNodeConfig<ThemeConfig = DefaultTheme.Config> = ValaxyConfig<ThemeConfig> & ValaxyExtendConfig
export type UserValaxyNodeConfig<ThemeConfig = DefaultTheme.Config> = PartialDeep<ValaxyNodeConfig<ThemeConfig>>
@ -97,7 +98,7 @@ export interface ValaxyExtendConfig {
/**
* for markdown
*/
markdown?: MarkdownOptions
markdown?: MarkdownOptions & Parameters<typeof Markdown>[0]
extendMd?: (ctx: {
route: EditableTreeNode
data: Readonly<Record<string, any>>

View File

@ -95,6 +95,7 @@
"html-to-text": "^9.0.5",
"is-installed-globally": "^1.0.0",
"jiti": "^1.21.0",
"js-base64": "^3.7.6",
"katex": "^0.16.9",
"lru-cache": "^10.2.0",
"markdown-it": "^14.0.0",
@ -118,6 +119,7 @@
"unconfig": "^0.3.11",
"unocss": "^0.58.5",
"unplugin-vue-components": "^0.26.0",
"unplugin-vue-markdown": "^0.26.0",
"unplugin-vue-router": "^0.7.0",
"vanilla-lazyload": "^17.8.8",
"vite": "^5.1.1",

View File

@ -3,7 +3,7 @@ import 'vue-router'
// import './client/typed-router'
import type { Post } from './types'
import type { Header } from './node/markdown'
import type { Header } from './node/plugins/markdown'
declare module 'valaxy-addon-*'
declare module '@docsearch/js' {

View File

@ -4,7 +4,6 @@ export type CleanUrlsMode =
| 'disabled'
| 'without-subfolders'
| 'with-subfolders'
export interface Header {
/**
* The level of the header

View File

@ -339,6 +339,9 @@ importers:
jiti:
specifier: ^1.21.0
version: 1.21.0
js-base64:
specifier: ^3.7.6
version: 3.7.6
katex:
specifier: ^0.16.9
version: 0.16.9
@ -408,6 +411,9 @@ importers:
unplugin-vue-components:
specifier: ^0.26.0
version: 0.26.0(rollup@3.29.4)(vue@3.4.18)
unplugin-vue-markdown:
specifier: ^0.26.0
version: 0.26.0(rollup@3.29.4)(vite@5.1.1)
unplugin-vue-router:
specifier: ^0.7.0
version: 0.7.0(rollup@3.29.4)(vue-router@4.2.5)(vue@3.4.18)
@ -2834,7 +2840,6 @@ packages:
dependencies:
'@types/markdown-it': 13.0.7
markdown-it: 14.0.0
dev: true
/@mdit-vue/plugin-frontmatter@2.0.0:
resolution: {integrity: sha512-/LrT6E60QI4XV4mqx3J87hqYXlR7ZyMvndmftR2RGz7cRAwa/xL+kyFLlgrMxkBIKitOShKa3LS/9Ov9b0fU+g==}
@ -2843,7 +2848,6 @@ packages:
'@types/markdown-it': 13.0.7
gray-matter: 4.0.3
markdown-it: 14.0.0
dev: true
/@mdit-vue/plugin-headers@2.0.0:
resolution: {integrity: sha512-ITMMPCnLEYHHgj3XEUL2l75jsNn8guxNqr26YrMSi1f5zcgq4XVy1LIvfwvJ1puqM6Cc5v4BHk3oAyorAi7l1A==}
@ -2890,7 +2894,6 @@ packages:
/@mdit-vue/types@2.0.0:
resolution: {integrity: sha512-1BeEB+DbtmDMUAfvbNUj5Hso8cSl2sBVK2iTyOMAqhfDVLdh+/9+D0JmQHaCeUk/vuJoMhOwbweZvh55wHxm4w==}
dev: true
/@microsoft/api-extractor-model@7.28.9(@types/node@20.11.17):
resolution: {integrity: sha512-lM77dV+VO46MGp5lu4stUBnO3jyr+CrDzU+DtapcOQEZUqJxPYUoK5zjeD+gRZ9ckgGMZC94ch6FBkpmsjwQgw==}
@ -7900,6 +7903,10 @@ packages:
engines: {node: '>=10'}
dev: true
/js-base64@3.7.6:
resolution: {integrity: sha512-NPrWuHFxFUknr1KqJRDgUQPexQF0uIJWjeT+2KjEePhitQxQEx5EJBG1lVn5/hc8aLycTpXrDOgPQ6Zq+EDiTA==}
dev: false
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
requiresBuild: true
@ -11346,6 +11353,23 @@ packages:
- supports-color
dev: false
/unplugin-vue-markdown@0.26.0(rollup@3.29.4)(vite@5.1.1):
resolution: {integrity: sha512-JohVC2KhMklr3OQJB6YfB20E1AZ/K+wV/q+6XtmamyUccr0cmdWRBR5jSS45y4fNtZqN3ULC+02EiD4pmaOqdQ==}
peerDependencies:
vite: ^2.0.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0
dependencies:
'@mdit-vue/plugin-component': 2.0.0
'@mdit-vue/plugin-frontmatter': 2.0.0
'@mdit-vue/types': 2.0.0
'@rollup/pluginutils': 5.1.0(rollup@3.29.4)
'@types/markdown-it': 13.0.7
markdown-it: 14.0.0
unplugin: 1.6.0
vite: 5.1.1(@types/node@20.11.17)(sass@1.70.0)
transitivePeerDependencies:
- rollup
dev: false
/unplugin-vue-router@0.7.0(rollup@3.29.4)(vue-router@4.2.5)(vue@3.4.18):
resolution: {integrity: sha512-ddRreGq0t5vlSB7OMy4e4cfU1w2AwBQCwmvW3oP/0IHQiokzbx4hd3TpwBu3eIAFVuhX2cwNQwp1U32UybTVCw==}
peerDependencies:

View File

@ -1,6 +1,6 @@
// import { resolve } from 'path'
import { describe, expect, it } from 'vitest'
import { createMarkdownRenderer } from '../packages/valaxy/node/markdown'
import { createMarkdownRenderer } from 'valaxy/node/plugins/markdown/index'
// const mdDir = resolve(__dirname, 'fixtures/markdown')