@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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -0
  3. package/dist/demo/formatter.d.ts +17 -0
  4. package/dist/demo/formatter.js +32 -0
  5. package/dist/demo/get-demo-id.d.ts +9 -0
  6. package/dist/demo/get-demo-id.js +19 -0
  7. package/dist/demo/index.d.ts +26 -0
  8. package/dist/demo/index.js +359 -0
  9. package/dist/demo/tsToJs.d.ts +9 -0
  10. package/dist/demo/tsToJs.js +60 -0
  11. package/dist/demo/types.d.ts +23 -0
  12. package/dist/index.d.ts +20 -0
  13. package/dist/index.js +19 -0
  14. package/dist/isolate-styles.d.ts +14 -0
  15. package/dist/isolate-styles.js +30 -0
  16. package/dist/markdown.d.ts +39 -0
  17. package/dist/markdown.js +115 -0
  18. package/dist/md-plugin.d.ts +18 -0
  19. package/dist/md-plugin.js +9 -0
  20. package/dist/md2vue.d.ts +16 -0
  21. package/dist/md2vue.js +106 -0
  22. package/dist/plugins/container.d.ts +16 -0
  23. package/dist/plugins/container.js +53 -0
  24. package/dist/plugins/demo.d.ts +29 -0
  25. package/dist/plugins/demo.js +139 -0
  26. package/dist/plugins/github-alerts.d.ts +6 -0
  27. package/dist/plugins/github-alerts.js +49 -0
  28. package/dist/plugins/image.d.ts +12 -0
  29. package/dist/plugins/image.js +17 -0
  30. package/dist/plugins/link.d.ts +14 -0
  31. package/dist/plugins/link.js +25 -0
  32. package/dist/plugins/pre-wrapper.d.ts +10 -0
  33. package/dist/plugins/pre-wrapper.js +26 -0
  34. package/dist/plugins/stackblitz.d.ts +5 -0
  35. package/dist/plugins/stackblitz.js +21 -0
  36. package/dist/plugins/table.d.ts +5 -0
  37. package/dist/plugins/table.js +25 -0
  38. package/dist/shared.d.ts +7 -0
  39. package/dist/shared.js +7 -0
  40. package/dist/utils/short-hash.d.ts +4 -0
  41. package/dist/utils/short-hash.js +21 -0
  42. package/package.json +94 -0
  43. package/src/components/code-demo/code-editor-bridge.vue +36 -0
  44. package/src/components/code-demo/compile-sfc.ts +207 -0
  45. package/src/components/code-demo/context.ts +86 -0
  46. package/src/components/code-demo/expand-icon.vue +12 -0
  47. package/src/components/code-demo/external-link-icon.vue +5 -0
  48. package/src/components/code-demo/index.vue +682 -0
  49. package/src/components/code-demo/virtual.d.ts +38 -0
  50. package/src/demo/formatter.ts +50 -0
  51. package/src/demo/get-demo-id.ts +33 -0
  52. package/src/demo/index.ts +498 -0
  53. package/src/demo/tsToJs.ts +79 -0
  54. package/src/demo/types.ts +23 -0
  55. package/src/index.ts +19 -0
  56. package/src/isolate-styles.ts +47 -0
  57. package/src/markdown.ts +188 -0
  58. package/src/md-plugin.ts +24 -0
  59. package/src/md2vue.ts +163 -0
  60. package/src/plugins/container.ts +135 -0
  61. package/src/plugins/demo.ts +282 -0
  62. package/src/plugins/github-alerts.ts +69 -0
  63. package/src/plugins/image.ts +29 -0
  64. package/src/plugins/link.ts +32 -0
  65. package/src/plugins/pre-wrapper.ts +49 -0
  66. package/src/plugins/stackblitz.ts +32 -0
  67. package/src/plugins/table.ts +39 -0
  68. package/src/shared.ts +4 -0
  69. 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
+ }
@@ -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: '&ZeroWidthSpace;',
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()
@@ -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, '&#123;')
83
+ .replace(/\}/g, '&#125;')
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
+ }