@antdv-next/docs-plugins 0.0.1
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/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/demo/formatter.d.ts +17 -0
- package/dist/demo/formatter.js +32 -0
- package/dist/demo/get-demo-id.d.ts +9 -0
- package/dist/demo/get-demo-id.js +19 -0
- package/dist/demo/index.d.ts +26 -0
- package/dist/demo/index.js +359 -0
- package/dist/demo/tsToJs.d.ts +9 -0
- package/dist/demo/tsToJs.js +60 -0
- package/dist/demo/types.d.ts +23 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +19 -0
- package/dist/isolate-styles.d.ts +14 -0
- package/dist/isolate-styles.js +30 -0
- package/dist/markdown.d.ts +39 -0
- package/dist/markdown.js +115 -0
- package/dist/md-plugin.d.ts +18 -0
- package/dist/md-plugin.js +9 -0
- package/dist/md2vue.d.ts +16 -0
- package/dist/md2vue.js +106 -0
- package/dist/plugins/container.d.ts +16 -0
- package/dist/plugins/container.js +53 -0
- package/dist/plugins/demo.d.ts +29 -0
- package/dist/plugins/demo.js +139 -0
- package/dist/plugins/github-alerts.d.ts +6 -0
- package/dist/plugins/github-alerts.js +49 -0
- package/dist/plugins/image.d.ts +12 -0
- package/dist/plugins/image.js +17 -0
- package/dist/plugins/link.d.ts +14 -0
- package/dist/plugins/link.js +25 -0
- package/dist/plugins/pre-wrapper.d.ts +10 -0
- package/dist/plugins/pre-wrapper.js +26 -0
- package/dist/plugins/stackblitz.d.ts +5 -0
- package/dist/plugins/stackblitz.js +21 -0
- package/dist/plugins/table.d.ts +5 -0
- package/dist/plugins/table.js +25 -0
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/dist/utils/short-hash.d.ts +4 -0
- package/dist/utils/short-hash.js +21 -0
- package/package.json +94 -0
- package/src/components/code-demo/code-editor-bridge.vue +36 -0
- package/src/components/code-demo/compile-sfc.ts +207 -0
- package/src/components/code-demo/context.ts +86 -0
- package/src/components/code-demo/expand-icon.vue +12 -0
- package/src/components/code-demo/external-link-icon.vue +5 -0
- package/src/components/code-demo/index.vue +682 -0
- package/src/components/code-demo/virtual.d.ts +38 -0
- package/src/demo/formatter.ts +50 -0
- package/src/demo/get-demo-id.ts +33 -0
- package/src/demo/index.ts +498 -0
- package/src/demo/tsToJs.ts +79 -0
- package/src/demo/types.ts +23 -0
- package/src/index.ts +19 -0
- package/src/isolate-styles.ts +47 -0
- package/src/markdown.ts +188 -0
- package/src/md-plugin.ts +24 -0
- package/src/md2vue.ts +163 -0
- package/src/plugins/container.ts +135 -0
- package/src/plugins/demo.ts +282 -0
- package/src/plugins/github-alerts.ts +69 -0
- package/src/plugins/image.ts +29 -0
- package/src/plugins/link.ts +32 -0
- package/src/plugins/pre-wrapper.ts +49 -0
- package/src/plugins/stackblitz.ts +32 -0
- package/src/plugins/table.ts +39 -0
- package/src/shared.ts +4 -0
- package/src/utils/short-hash.ts +27 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface DemoLocale {
|
|
2
|
+
html?: string
|
|
3
|
+
title?: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface DemoExtraFile {
|
|
7
|
+
name: string
|
|
8
|
+
lang: string
|
|
9
|
+
code: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DemoSourceData {
|
|
13
|
+
source: string
|
|
14
|
+
jsSource: string
|
|
15
|
+
extraFiles: DemoExtraFile[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DemoModule {
|
|
19
|
+
component?: () => Promise<unknown>
|
|
20
|
+
locales?: Record<string, DemoLocale>
|
|
21
|
+
sourceVersion: number
|
|
22
|
+
loadSource: (signal?: AbortSignal) => Promise<DemoSourceData>
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from './demo/formatter'
|
|
2
|
+
export * from './demo/get-demo-id'
|
|
3
|
+
export * from './demo/index'
|
|
4
|
+
export * from './demo/tsToJs'
|
|
5
|
+
export * from './demo/types'
|
|
6
|
+
export * from './isolate-styles'
|
|
7
|
+
export * from './markdown'
|
|
8
|
+
export * from './md2vue'
|
|
9
|
+
export * from './md-plugin'
|
|
10
|
+
export * from './plugins/container'
|
|
11
|
+
export * from './plugins/demo'
|
|
12
|
+
export * from './plugins/github-alerts'
|
|
13
|
+
export * from './plugins/image'
|
|
14
|
+
export * from './plugins/link'
|
|
15
|
+
export * from './plugins/pre-wrapper'
|
|
16
|
+
export * from './plugins/stackblitz'
|
|
17
|
+
export * from './plugins/table'
|
|
18
|
+
export * from './shared'
|
|
19
|
+
export * from './utils/short-hash'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Plugin } from 'postcss'
|
|
2
|
+
import selectorParser from 'postcss-selector-parser'
|
|
3
|
+
|
|
4
|
+
interface Options {
|
|
5
|
+
includeFiles?: RegExp[]
|
|
6
|
+
ignoreFiles?: RegExp[]
|
|
7
|
+
prefix?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* PostCSS 插件:为 markdown 样式添加隔离前缀,
|
|
12
|
+
* 避免文档站点样式污染组件演示区(`.vp-raw` 内不做隔离)。
|
|
13
|
+
*/
|
|
14
|
+
export function postcssIsolateStyles({
|
|
15
|
+
includeFiles = [/styles\/markdown\/index\.css/],
|
|
16
|
+
ignoreFiles,
|
|
17
|
+
prefix = ':not(:where(.vp-raw, .vp-raw *))',
|
|
18
|
+
}: Options = {}): Plugin {
|
|
19
|
+
const prefixNodes = selectorParser().astSync(prefix).first.nodes
|
|
20
|
+
|
|
21
|
+
return /* prettier-ignore */ {
|
|
22
|
+
postcssPlugin: 'postcss-isolate-styles',
|
|
23
|
+
Once(root) {
|
|
24
|
+
const file = root.source?.input.file
|
|
25
|
+
if (file && includeFiles?.length && !includeFiles.some(re => re.test(file)))
|
|
26
|
+
return
|
|
27
|
+
if (file && ignoreFiles?.length && ignoreFiles.some(re => re.test(file)))
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
root.walkRules((rule) => {
|
|
31
|
+
if (!rule.selector || rule.selector.includes(prefix))
|
|
32
|
+
return
|
|
33
|
+
if (rule.parent?.type === 'atrule' && /\bkeyframes$/i.test(rule.parent.name))
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
rule.selector = selectorParser((selectors) => {
|
|
37
|
+
selectors.each((sel) => {
|
|
38
|
+
if (!sel.nodes.length)
|
|
39
|
+
return
|
|
40
|
+
const insertionIndex = (sel as any).nodes.findLastIndex((n: any) => n.type !== 'pseudo') + 1
|
|
41
|
+
sel.nodes.splice(insertionIndex, 0, ...prefixNodes.map(n => n.clone() as any))
|
|
42
|
+
})
|
|
43
|
+
}).processSync(rule.selector)
|
|
44
|
+
})
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/markdown.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { MarkdownItAsync } from 'markdown-it-async'
|
|
2
|
+
import type { DemoMarkdownPluginOptions } from './plugins/demo'
|
|
3
|
+
import { frontmatterPlugin } from '@mdit-vue/plugin-frontmatter'
|
|
4
|
+
import { headersPlugin } from '@mdit-vue/plugin-headers'
|
|
5
|
+
import { titlePlugin } from '@mdit-vue/plugin-title'
|
|
6
|
+
import { tocPlugin } from '@mdit-vue/plugin-toc'
|
|
7
|
+
import { slugify } from '@mdit-vue/shared'
|
|
8
|
+
import { fromAsyncCodeToHtml } from '@shikijs/markdown-it/async'
|
|
9
|
+
import {
|
|
10
|
+
transformerMetaHighlight,
|
|
11
|
+
transformerMetaWordHighlight,
|
|
12
|
+
transformerNotationDiff,
|
|
13
|
+
transformerNotationErrorLevel,
|
|
14
|
+
transformerNotationFocus,
|
|
15
|
+
transformerNotationHighlight,
|
|
16
|
+
transformerNotationWordHighlight,
|
|
17
|
+
} from '@shikijs/transformers'
|
|
18
|
+
import anchorPlugin from 'markdown-it-anchor'
|
|
19
|
+
import MarkdownIt from 'markdown-it-async'
|
|
20
|
+
import attrsPlugin from 'markdown-it-attrs'
|
|
21
|
+
// @ts-expect-error markdown-it-emoji has no proper types for this entry
|
|
22
|
+
import { full as emoji } from 'markdown-it-emoji'
|
|
23
|
+
import { codeToHtml } from 'shiki'
|
|
24
|
+
import { containerPlugin } from './plugins/container'
|
|
25
|
+
import { demoMarkdownPlugin } from './plugins/demo'
|
|
26
|
+
import { gitHubAlertsPlugin } from './plugins/github-alerts'
|
|
27
|
+
import { imagePlugin } from './plugins/image'
|
|
28
|
+
import { linkPlugin } from './plugins/link'
|
|
29
|
+
import { preWrapperPlugin } from './plugins/pre-wrapper'
|
|
30
|
+
import { stackblitzPlugin } from './plugins/stackblitz'
|
|
31
|
+
import { tablePlugin } from './plugins/table'
|
|
32
|
+
|
|
33
|
+
export interface CreateMarkdownOptions {
|
|
34
|
+
/**
|
|
35
|
+
* 是否加载插件
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
withPlugin?: boolean
|
|
39
|
+
/**
|
|
40
|
+
* 自定义插件前的钩子
|
|
41
|
+
*/
|
|
42
|
+
preConfig?: (md: MarkdownItAsync) => void
|
|
43
|
+
/**
|
|
44
|
+
* 自定义插件后的钩子
|
|
45
|
+
*/
|
|
46
|
+
config?: (md: MarkdownItAsync) => void
|
|
47
|
+
/**
|
|
48
|
+
* 资源路径配置
|
|
49
|
+
*/
|
|
50
|
+
root?: string
|
|
51
|
+
/**
|
|
52
|
+
* markdown-it 层 demo 插件选项
|
|
53
|
+
*/
|
|
54
|
+
demo?: DemoMarkdownPluginOptions
|
|
55
|
+
/**
|
|
56
|
+
* 外部链接在新标签页打开
|
|
57
|
+
* @default true
|
|
58
|
+
*/
|
|
59
|
+
externalLink?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function loadShiki(md: MarkdownItAsync, cls: string = 'ant-doc-code') {
|
|
63
|
+
md.use(fromAsyncCodeToHtml(codeToHtml, {
|
|
64
|
+
themes: {
|
|
65
|
+
light: 'vitesse-light',
|
|
66
|
+
dark: 'vitesse-dark',
|
|
67
|
+
},
|
|
68
|
+
defaultColor: false,
|
|
69
|
+
cssVariablePrefix: '--ant-doc-',
|
|
70
|
+
transformers: [
|
|
71
|
+
transformerMetaHighlight(),
|
|
72
|
+
transformerMetaWordHighlight(),
|
|
73
|
+
transformerNotationDiff(),
|
|
74
|
+
transformerNotationErrorLevel(),
|
|
75
|
+
transformerNotationFocus(),
|
|
76
|
+
transformerNotationHighlight(),
|
|
77
|
+
transformerNotationWordHighlight(),
|
|
78
|
+
{
|
|
79
|
+
name: 'remove:clean-up',
|
|
80
|
+
code(element) {
|
|
81
|
+
if (element.tagName === 'code' && element.properties.class)
|
|
82
|
+
delete element.properties.class
|
|
83
|
+
},
|
|
84
|
+
pre(element) {
|
|
85
|
+
delete element.properties.tabindex
|
|
86
|
+
delete element.properties.style
|
|
87
|
+
this.addClassToHast(element, cls)
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
}))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function loadBaseMd(md: MarkdownItAsync, options: { externalLink?: boolean } = {}) {
|
|
95
|
+
// 允许在 markdown 中使用 frontmatter
|
|
96
|
+
md.use(frontmatterPlugin)
|
|
97
|
+
// 头部插件
|
|
98
|
+
md.use(headersPlugin, {
|
|
99
|
+
level: [2, 3, 4, 5, 6],
|
|
100
|
+
slugify,
|
|
101
|
+
})
|
|
102
|
+
// 标题插件
|
|
103
|
+
md.use(titlePlugin)
|
|
104
|
+
// 加载emoji表情包
|
|
105
|
+
md.use(emoji)
|
|
106
|
+
// 加载处理器
|
|
107
|
+
md.use(attrsPlugin)
|
|
108
|
+
// 外部链接新标签页打开
|
|
109
|
+
if (options.externalLink !== false)
|
|
110
|
+
md.use(linkPlugin)
|
|
111
|
+
// 加载容器插件
|
|
112
|
+
md.use(containerPlugin)
|
|
113
|
+
// 加载github警告插件
|
|
114
|
+
md.use(gitHubAlertsPlugin)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function withPlugins(md: MarkdownItAsync, options: CreateMarkdownOptions) {
|
|
118
|
+
// 处理demo
|
|
119
|
+
md.use(demoMarkdownPlugin, {
|
|
120
|
+
root: options.root,
|
|
121
|
+
...options.demo,
|
|
122
|
+
})
|
|
123
|
+
// 加载基础markdown配置
|
|
124
|
+
loadBaseMd(md, options)
|
|
125
|
+
// 目录插件
|
|
126
|
+
md.use(tocPlugin)
|
|
127
|
+
// 加载anchor插件
|
|
128
|
+
md.use(anchorPlugin, {
|
|
129
|
+
slugify,
|
|
130
|
+
permalink: anchorPlugin.permalink.linkInsideHeader({
|
|
131
|
+
symbol: '​',
|
|
132
|
+
renderAttrs: (slug, state) => {
|
|
133
|
+
// Find `heading_open` with the id identical to slug
|
|
134
|
+
const idx = state.tokens.findIndex((token) => {
|
|
135
|
+
const attrs = token.attrs
|
|
136
|
+
const id = attrs?.find(attr => attr[0] === 'id')
|
|
137
|
+
return id && slug === id[1]
|
|
138
|
+
})
|
|
139
|
+
// Get the actual heading content
|
|
140
|
+
const title = state.tokens[idx + 1]?.content || ''
|
|
141
|
+
return {
|
|
142
|
+
'aria-label': `Permalink to "${title}"`,
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
})
|
|
147
|
+
// 配置加载高亮配置
|
|
148
|
+
loadShiki(md)
|
|
149
|
+
// 加载图片插件
|
|
150
|
+
md.use(imagePlugin)
|
|
151
|
+
// 加载pre标签包裹插件
|
|
152
|
+
md.use(preWrapperPlugin, {
|
|
153
|
+
hasSingleTheme: false, // 是否只有一个主题
|
|
154
|
+
})
|
|
155
|
+
// 加载表格插件,为 Api 区域的表格添加属性
|
|
156
|
+
md.use(tablePlugin)
|
|
157
|
+
// 加载 StackBlitz 代码块插件
|
|
158
|
+
md.use(stackblitzPlugin)
|
|
159
|
+
|
|
160
|
+
// 禁用markdown自动识别链接
|
|
161
|
+
md.linkify.set({ fuzzyLink: false })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function createMarkdown() {
|
|
165
|
+
let md: MarkdownItAsync | undefined
|
|
166
|
+
return (options: CreateMarkdownOptions = {}) => {
|
|
167
|
+
if (md) {
|
|
168
|
+
return md
|
|
169
|
+
}
|
|
170
|
+
md = MarkdownIt({
|
|
171
|
+
html: true,
|
|
172
|
+
linkify: true,
|
|
173
|
+
typographer: true,
|
|
174
|
+
})
|
|
175
|
+
if (options.preConfig) {
|
|
176
|
+
options.preConfig(md)
|
|
177
|
+
}
|
|
178
|
+
if (options.withPlugin !== false) {
|
|
179
|
+
withPlugins(md, options)
|
|
180
|
+
}
|
|
181
|
+
if (options.config) {
|
|
182
|
+
options.config(md)
|
|
183
|
+
}
|
|
184
|
+
return md
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const useMarkdown = createMarkdown()
|
package/src/md-plugin.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PluginOption } from 'vite'
|
|
2
|
+
import type { DemoPluginOptions } from './demo/index'
|
|
3
|
+
import type { CreateMarkdownOptions } from './markdown'
|
|
4
|
+
import { demoPlugin } from './demo/index'
|
|
5
|
+
import { md2VuePlugin } from './md2vue'
|
|
6
|
+
|
|
7
|
+
export interface DocsPluginsOptions {
|
|
8
|
+
/**
|
|
9
|
+
* markdown-it 渲染链路选项(传给 md2vue / createMarkdown)
|
|
10
|
+
*/
|
|
11
|
+
markdown?: CreateMarkdownOptions
|
|
12
|
+
/**
|
|
13
|
+
* vite demo 插件选项
|
|
14
|
+
*/
|
|
15
|
+
demo?: DemoPluginOptions
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** docs 站点默认的 markdown 处理插件组合 */
|
|
19
|
+
export function mdPlugin(options: DocsPluginsOptions = {}): PluginOption[] {
|
|
20
|
+
return [
|
|
21
|
+
md2VuePlugin(options.markdown),
|
|
22
|
+
demoPlugin(options.demo),
|
|
23
|
+
]
|
|
24
|
+
}
|
package/src/md2vue.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { MarkdownItEnv } from '@mdit-vue/types'
|
|
2
|
+
import type { PluginOption } from 'vite'
|
|
3
|
+
import type { CreateMarkdownOptions } from './markdown'
|
|
4
|
+
import { LRUCache } from 'lru-cache'
|
|
5
|
+
import { findStaticImports } from 'mlly'
|
|
6
|
+
import pathe from 'pathe'
|
|
7
|
+
import { useMarkdown } from './markdown'
|
|
8
|
+
import { SCRIPT_REGEX, STYLE_REGEX } from './shared'
|
|
9
|
+
import { shortHash } from './utils/short-hash'
|
|
10
|
+
|
|
11
|
+
export function formatPageData(env: MarkdownItEnv) {
|
|
12
|
+
const pageData = {
|
|
13
|
+
title: env.title ?? '',
|
|
14
|
+
frontmatter: env.frontmatter ?? {},
|
|
15
|
+
headers: env.headers ?? [],
|
|
16
|
+
description: '',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof env.frontmatter?.description === 'string')
|
|
20
|
+
pageData.description = env.frontmatter.description
|
|
21
|
+
|
|
22
|
+
if (typeof env.frontmatter?.title === 'string' && !env.title)
|
|
23
|
+
pageData.title = env.frontmatter.title
|
|
24
|
+
|
|
25
|
+
return pageData
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function checkPkgImport(findCode: string, pkg: string, importName: string) {
|
|
29
|
+
if (!findCode)
|
|
30
|
+
return false
|
|
31
|
+
const imports = findStaticImports(findCode)
|
|
32
|
+
return imports.some(
|
|
33
|
+
item => item.specifier === pkg && item.imports.includes(importName),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function addScriptSetup(scriptTags: RegExpMatchArray | null, env: MarkdownItEnv) {
|
|
38
|
+
const pageData = formatPageData(env)
|
|
39
|
+
const baseCode = `const __pageData = ${JSON.stringify(pageData)};\nconst frontmatter = ${JSON.stringify(env.frontmatter ?? {})};\n`
|
|
40
|
+
const importsCode = `import { inject, provide, ref } from 'vue';\n`
|
|
41
|
+
const injectedCode = `const __parentPageData = ref({});provide('__pageData__',(data)=>{__parentPageData.value=data});const __pageDataFunc__ = inject('__pageData__',null);if(__pageDataFunc__)__pageDataFunc__(__pageData);provide('__pageInfo__',__pageData);defineExpose({frontmatter,pageData:__pageData})`
|
|
42
|
+
|
|
43
|
+
if (!scriptTags || !scriptTags.length) {
|
|
44
|
+
return `<script setup lang="ts">\n${importsCode}${baseCode}${injectedCode}\n</script>\n`
|
|
45
|
+
}
|
|
46
|
+
const scriptSetupCode = scriptTags.find((v) => {
|
|
47
|
+
// 只要是单行的script标签,中包含<script setup>的标签
|
|
48
|
+
return /<script\b[^>]+\bsetup\b[^>]*>/i.test(v)
|
|
49
|
+
})
|
|
50
|
+
if (!scriptSetupCode) {
|
|
51
|
+
// 如果没有找到script setup标签,则直接返回
|
|
52
|
+
return `<script setup lang="ts">\n${importsCode}${baseCode}${injectedCode}\n</script>\n${scriptTags.join('\n')}\n`
|
|
53
|
+
}
|
|
54
|
+
// 剩余的script代码
|
|
55
|
+
const restScriptTags = scriptTags.filter((v) => {
|
|
56
|
+
return v !== scriptSetupCode
|
|
57
|
+
})
|
|
58
|
+
// 获取内部的代码
|
|
59
|
+
const scriptContent = scriptSetupCode
|
|
60
|
+
? scriptSetupCode
|
|
61
|
+
.replace(/<script\b[^>]+\bsetup\b[^>]*>/i, '')
|
|
62
|
+
.replace(/<\/script>/i, '')
|
|
63
|
+
: ''
|
|
64
|
+
const importArr: string[] = []
|
|
65
|
+
if (!checkPkgImport(scriptContent, 'vue', 'provide'))
|
|
66
|
+
importArr.push('provide')
|
|
67
|
+
if (!checkPkgImport(scriptContent, 'vue', 'inject'))
|
|
68
|
+
importArr.push('inject')
|
|
69
|
+
if (!checkPkgImport(scriptContent, 'vue', 'ref'))
|
|
70
|
+
importArr.push('ref')
|
|
71
|
+
const _importsCode = importArr.length
|
|
72
|
+
? `import { ${importArr.join(', ')} } from 'vue';\n`
|
|
73
|
+
: ''
|
|
74
|
+
return `<script setup lang="ts">\n${_importsCode}${scriptContent}${baseCode}${injectedCode}\n</script>\n${restScriptTags.join('\n')}\n`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function md2Vue(code: string, env: MarkdownItEnv) {
|
|
78
|
+
// 先处理代码块,转义其中可能影响 Vue 模板解析的特殊字符
|
|
79
|
+
code = code.replace(/<pre\b[^>]*>[\s\S]*?<\/pre>/gi, (match) => {
|
|
80
|
+
// 转义 { } 避免被 Vue 模板引擎误解析
|
|
81
|
+
return match
|
|
82
|
+
.replace(/\{/g, '{')
|
|
83
|
+
.replace(/\}/g, '}')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const scriptTags = code.match(SCRIPT_REGEX)
|
|
87
|
+
const styleTags = code.match(STYLE_REGEX)
|
|
88
|
+
// Remove script and style tags from the original code
|
|
89
|
+
const templateCode = code.replace(SCRIPT_REGEX, '').replace(STYLE_REGEX, '')
|
|
90
|
+
// Create a Vue component string
|
|
91
|
+
const vueComponent = `<template>\n<div class="ant-doc vp-doc">${templateCode}</div>\n</template>\n`
|
|
92
|
+
const scriptContent = addScriptSetup(scriptTags, env)
|
|
93
|
+
const styleContent = styleTags ? styleTags.join('\n') : ''
|
|
94
|
+
return `${scriptContent}\n${vueComponent}\n${styleContent}`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function md2VuePlugin(options: CreateMarkdownOptions = {}): PluginOption {
|
|
98
|
+
let md: ReturnType<typeof useMarkdown>
|
|
99
|
+
const cache = new LRUCache<string, { hash: string, code: string }>({
|
|
100
|
+
max: 500,
|
|
101
|
+
ttl: 1000 * 60 * 10,
|
|
102
|
+
allowStale: true,
|
|
103
|
+
updateAgeOnGet: true,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
async function transform(code: string, id: string) {
|
|
107
|
+
const hash = shortHash(code)
|
|
108
|
+
const cached = cache.get(id)
|
|
109
|
+
if (cached && cached.hash === hash) {
|
|
110
|
+
return cached.code
|
|
111
|
+
}
|
|
112
|
+
const env: MarkdownItEnv = {
|
|
113
|
+
id,
|
|
114
|
+
}
|
|
115
|
+
const html = await md.renderAsync(code, env)
|
|
116
|
+
const vueCode = md2Vue(html, env)
|
|
117
|
+
cache.set(id, { hash, code: vueCode })
|
|
118
|
+
return vueCode
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function shouldSkipTransform(id: string) {
|
|
122
|
+
if (id.includes('?vue'))
|
|
123
|
+
return true
|
|
124
|
+
|
|
125
|
+
const query = id.split('?')[1]
|
|
126
|
+
if (!query)
|
|
127
|
+
return false
|
|
128
|
+
const params = new URLSearchParams(query)
|
|
129
|
+
return params.has('raw') || params.has('url')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
enforce: 'pre',
|
|
134
|
+
name: 'vite:md2vue',
|
|
135
|
+
configResolved(config) {
|
|
136
|
+
md = useMarkdown({
|
|
137
|
+
root: pathe.resolve(config.root ?? process.cwd(), '.'),
|
|
138
|
+
...options,
|
|
139
|
+
})
|
|
140
|
+
},
|
|
141
|
+
transform: {
|
|
142
|
+
filter: {
|
|
143
|
+
id: /\.md($|\?)/,
|
|
144
|
+
},
|
|
145
|
+
async handler(code, id) {
|
|
146
|
+
if (shouldSkipTransform(id))
|
|
147
|
+
return null
|
|
148
|
+
return transform(code, id)
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
handleHotUpdate(ctx) {
|
|
152
|
+
const { file, read } = ctx
|
|
153
|
+
if (!file.endsWith('.md'))
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
const defaultRead = read
|
|
157
|
+
ctx.read = async () => {
|
|
158
|
+
const code = await defaultRead()
|
|
159
|
+
return transform(code, file)
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type MarkdownIt from 'markdown-it'
|
|
2
|
+
import type { RenderRule } from 'markdown-it/lib/renderer.mjs'
|
|
3
|
+
import type Token from 'markdown-it/lib/token.mjs'
|
|
4
|
+
import type { Options } from './pre-wrapper'
|
|
5
|
+
import container from 'markdown-it-container'
|
|
6
|
+
import { nanoid } from 'nanoid'
|
|
7
|
+
import { extractTitle, getAdaptiveThemeMarker } from './pre-wrapper'
|
|
8
|
+
|
|
9
|
+
export function containerPlugin(md: MarkdownIt, options: Options, containerOptions?: ContainerOptions) {
|
|
10
|
+
md.use(...createContainer('tip', containerOptions?.tipLabel || 'TIP', md))
|
|
11
|
+
.use(...createContainer('info', containerOptions?.infoLabel || 'INFO', md))
|
|
12
|
+
.use(
|
|
13
|
+
...createContainer(
|
|
14
|
+
'warning',
|
|
15
|
+
containerOptions?.warningLabel || 'WARNING',
|
|
16
|
+
md,
|
|
17
|
+
),
|
|
18
|
+
)
|
|
19
|
+
.use(
|
|
20
|
+
...createContainer(
|
|
21
|
+
'danger',
|
|
22
|
+
containerOptions?.dangerLabel || 'DANGER',
|
|
23
|
+
md,
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
.use(
|
|
27
|
+
...createContainer(
|
|
28
|
+
'details',
|
|
29
|
+
containerOptions?.detailsLabel || 'Details',
|
|
30
|
+
md,
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
// explicitly escape Vue syntax
|
|
34
|
+
.use(container as any, 'v-pre', {
|
|
35
|
+
render: (tokens: Token[], idx: number) =>
|
|
36
|
+
tokens[idx]!.nesting === 1 ? `<div v-pre>\n` : `</div>\n`,
|
|
37
|
+
})
|
|
38
|
+
.use(container as any, 'raw', {
|
|
39
|
+
render: (tokens: Token[], idx: number) =>
|
|
40
|
+
tokens[idx]!.nesting === 1 ? `<div class="vp-raw">\n` : `</div>\n`,
|
|
41
|
+
})
|
|
42
|
+
.use(...createCodeGroup(options))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type ContainerArgs = [any, string, { render: RenderRule }]
|
|
46
|
+
|
|
47
|
+
function createContainer(
|
|
48
|
+
klass: string,
|
|
49
|
+
defaultTitle: string,
|
|
50
|
+
md: MarkdownIt,
|
|
51
|
+
): ContainerArgs {
|
|
52
|
+
return [
|
|
53
|
+
container,
|
|
54
|
+
klass,
|
|
55
|
+
{
|
|
56
|
+
render(tokens, idx, _options, env) {
|
|
57
|
+
const token = tokens[idx]!
|
|
58
|
+
const info = token.info.trim().slice(klass.length).trim()
|
|
59
|
+
const attrs = md.renderer.renderAttrs(token)
|
|
60
|
+
if (token.nesting === 1) {
|
|
61
|
+
const title = md.renderInline(info || defaultTitle, {
|
|
62
|
+
references: env.references,
|
|
63
|
+
})
|
|
64
|
+
if (klass === 'details')
|
|
65
|
+
return `<details class="${klass} custom-block"${attrs}><summary>${title}</summary>\n`
|
|
66
|
+
return `<div class="${klass} custom-block"${attrs}><p class="custom-block-title">${title}</p>\n`
|
|
67
|
+
}
|
|
68
|
+
else { return klass === 'details' ? `</details>\n` : `</div>\n` }
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createCodeGroup(options: Options): ContainerArgs {
|
|
75
|
+
return [
|
|
76
|
+
container,
|
|
77
|
+
'code-group',
|
|
78
|
+
{
|
|
79
|
+
render(tokens, idx) {
|
|
80
|
+
if (tokens[idx]!.nesting === 1) {
|
|
81
|
+
const name = nanoid(5)
|
|
82
|
+
let tabs = ''
|
|
83
|
+
let checked = 'checked="checked"'
|
|
84
|
+
|
|
85
|
+
for (
|
|
86
|
+
let i = idx + 1;
|
|
87
|
+
!(
|
|
88
|
+
tokens[i]!.nesting === -1
|
|
89
|
+
&& tokens[i]!.type === 'container_code-group_close'
|
|
90
|
+
);
|
|
91
|
+
++i
|
|
92
|
+
) {
|
|
93
|
+
const token = tokens[i]!
|
|
94
|
+
const isHtml = token.type === 'html_block'
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
(token.type === 'fence' && token.tag === 'code')
|
|
98
|
+
|| isHtml
|
|
99
|
+
) {
|
|
100
|
+
const title = extractTitle(
|
|
101
|
+
isHtml ? token.content : token.info,
|
|
102
|
+
isHtml,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if (title) {
|
|
106
|
+
const id = nanoid(7)
|
|
107
|
+
tabs += `<input type="radio" name="group-${name}" id="tab-${id}" ${checked}><label for="tab-${id}">${title}</label>`
|
|
108
|
+
|
|
109
|
+
if (checked && !isHtml)
|
|
110
|
+
token.info += ' active'
|
|
111
|
+
checked = ''
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return `<div class="vp-code-group${getAdaptiveThemeMarker(
|
|
117
|
+
options,
|
|
118
|
+
)}"><div class="tabs">${tabs}</div><div class="blocks">\n`
|
|
119
|
+
}
|
|
120
|
+
return `</div></div>\n`
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ContainerOptions {
|
|
127
|
+
infoLabel?: string
|
|
128
|
+
noteLabel?: string
|
|
129
|
+
tipLabel?: string
|
|
130
|
+
warningLabel?: string
|
|
131
|
+
dangerLabel?: string
|
|
132
|
+
detailsLabel?: string
|
|
133
|
+
importantLabel?: string
|
|
134
|
+
cautionLabel?: string
|
|
135
|
+
}
|