@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,282 @@
|
|
|
1
|
+
import type { MarkdownItEnv, MarkdownItHeader } from '@mdit-vue/types'
|
|
2
|
+
import type MarkdownIt from 'markdown-it'
|
|
3
|
+
import pathe from 'pathe'
|
|
4
|
+
import { getDemoId } from '../demo/get-demo-id'
|
|
5
|
+
|
|
6
|
+
const HEADING_LEVEL_RE = /^h([2-6])$/
|
|
7
|
+
const SRC_ATTR_RE = /(\s|^)src=(['"])(.*?)\2/gi
|
|
8
|
+
|
|
9
|
+
export interface DemoMarkdownPluginOptions {
|
|
10
|
+
/** 包裹 demo 的标签名,默认 `demo` */
|
|
11
|
+
wrapper?: string
|
|
12
|
+
/**
|
|
13
|
+
* demo 锚点归属模式:
|
|
14
|
+
* - `examples`:demo 统一挂到 slug 为 `examples` 的标题下(antdv-next 约定,默认)
|
|
15
|
+
* - `section`:demo 挂到当前所在章节标题下(跟随标题层级)
|
|
16
|
+
*/
|
|
17
|
+
headerMode?: 'examples' | 'section'
|
|
18
|
+
/**
|
|
19
|
+
* 生产构建时跳过 `debug` demo 的目录收集
|
|
20
|
+
* @default true
|
|
21
|
+
*/
|
|
22
|
+
debugDemo?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare module '@mdit-vue/types' {
|
|
26
|
+
interface MarkdownItEnv {
|
|
27
|
+
id?: string
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checkWrapper(content: string, wrapper = 'demo') {
|
|
32
|
+
return new RegExp(`<${wrapper}(\\s|>|/)`, 'i').test(content)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isProdDebugDemo(tag: string) {
|
|
36
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const debugAttr = tag.match(/(?:^|\s)debug(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s/>]+)))?/i)
|
|
41
|
+
if (!debugAttr) {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const value = debugAttr[1] ?? debugAttr[2] ?? debugAttr[3]
|
|
46
|
+
return value === undefined || value === '' || value === 'true'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function flattenHeaders(headers: MarkdownItHeader[] = []) {
|
|
50
|
+
const headerMap = new Map<string, MarkdownItHeader>()
|
|
51
|
+
|
|
52
|
+
const visit = (items: MarkdownItHeader[]) => {
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
headerMap.set(item.slug, item)
|
|
55
|
+
if (item.children?.length)
|
|
56
|
+
visit(item.children)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
visit(headers)
|
|
61
|
+
return headerMap
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function flattenHeadersInOrder(headers: MarkdownItHeader[] = []) {
|
|
65
|
+
const flat: MarkdownItHeader[] = []
|
|
66
|
+
|
|
67
|
+
const visit = (items: MarkdownItHeader[]) => {
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
flat.push(item)
|
|
70
|
+
if (item.children?.length)
|
|
71
|
+
visit(item.children)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
visit(headers)
|
|
76
|
+
return flat
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getHeadingSlug(token: { attrs?: [string, string][] | null }) {
|
|
80
|
+
return token.attrs?.find(attr => attr[0] === 'id')?.[1]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getHeadingTitle(token?: { type?: string, content?: string }) {
|
|
84
|
+
if (token?.type !== 'inline')
|
|
85
|
+
return ''
|
|
86
|
+
return token.content?.trim() ?? ''
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getHeadingLevel(token: { tag?: string }) {
|
|
90
|
+
const match = token.tag?.match(HEADING_LEVEL_RE)
|
|
91
|
+
if (!match)
|
|
92
|
+
return null
|
|
93
|
+
return Number(match[1])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function replaceSrcPath(
|
|
97
|
+
content: string,
|
|
98
|
+
id: string,
|
|
99
|
+
root: string,
|
|
100
|
+
wrapper = 'demo',
|
|
101
|
+
parentHeader?: MarkdownItHeader,
|
|
102
|
+
skipDebugDemo = false,
|
|
103
|
+
) {
|
|
104
|
+
function replaceSrcInTag(tagMatch: string, titleContent?: string) {
|
|
105
|
+
return tagMatch.replace(SRC_ATTR_RE, (srcMatch, prefix, quote, srcValue) => {
|
|
106
|
+
if (!srcValue || srcValue.startsWith('/'))
|
|
107
|
+
return srcMatch
|
|
108
|
+
|
|
109
|
+
const dir = pathe.dirname(id)
|
|
110
|
+
const filePath = pathe.resolve(dir, srcValue)
|
|
111
|
+
const relative = pathe.relative(root, filePath)
|
|
112
|
+
|
|
113
|
+
if (parentHeader && titleContent && !(skipDebugDemo && isProdDebugDemo(tagMatch))) {
|
|
114
|
+
const slug = getDemoId(filePath)
|
|
115
|
+
const item = {
|
|
116
|
+
level: parentHeader.level + 1,
|
|
117
|
+
title: titleContent,
|
|
118
|
+
slug,
|
|
119
|
+
link: `#${slug}`,
|
|
120
|
+
children: [],
|
|
121
|
+
}
|
|
122
|
+
if (parentHeader.children)
|
|
123
|
+
parentHeader.children.push(item)
|
|
124
|
+
else
|
|
125
|
+
parentHeader.children = [item]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return `${prefix}src=${quote}${relative.startsWith('/') ? relative : `/${relative}`}${quote}`
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 1. First, match closed tags <demo>Title</demo> to extract title content
|
|
133
|
+
const closedTag = new RegExp(`(<${wrapper}(?!-)\\b[^>]*>)([\\s\\S]*?)<\\/${wrapper}>`, 'gi')
|
|
134
|
+
let result = content.replace(closedTag, (tagMatch, openTag, titleContent) => {
|
|
135
|
+
return tagMatch.replace(
|
|
136
|
+
openTag,
|
|
137
|
+
replaceSrcInTag(openTag, titleContent?.trim()),
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// 2. Then, match self-closing tags <demo ... />
|
|
142
|
+
const selfClosing = new RegExp(`<${wrapper}(?!-)\\b[^>]*/\\s*>`, 'gi')
|
|
143
|
+
result = result.replace(selfClosing, tagMatch => replaceSrcInTag(tagMatch))
|
|
144
|
+
|
|
145
|
+
// 3. Finally, match standalone open tags <demo ...> (when parsed separately from closing tag)
|
|
146
|
+
const openTag = new RegExp(`<${wrapper}(?!-)\\b[^>]*>`, 'gi')
|
|
147
|
+
return result.replace(openTag, tagMatch => replaceSrcInTag(tagMatch))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function demoMarkdownPlugin(
|
|
151
|
+
md: MarkdownIt,
|
|
152
|
+
config: { root?: string } & DemoMarkdownPluginOptions = {},
|
|
153
|
+
) {
|
|
154
|
+
const wrapper = config.wrapper ?? 'demo'
|
|
155
|
+
const headerMode = config.headerMode ?? 'examples'
|
|
156
|
+
const skipDebugDemo = config.debugDemo ?? true
|
|
157
|
+
// 保存原始 render 函数
|
|
158
|
+
const originalRender = md.renderer.render.bind(md.renderer)
|
|
159
|
+
|
|
160
|
+
md.renderer.render = function render(tokens, options, env: MarkdownItEnv) {
|
|
161
|
+
const root = config.root ?? process.cwd()
|
|
162
|
+
const currentId = env.id || ''
|
|
163
|
+
const headers = env.headers
|
|
164
|
+
|
|
165
|
+
// 'examples' 模式:所有 demo 统一挂到 slug 为 examples 的标题下
|
|
166
|
+
const fixedHeader = headerMode === 'examples'
|
|
167
|
+
? headers?.find(item => item.slug === 'examples')
|
|
168
|
+
: undefined
|
|
169
|
+
|
|
170
|
+
const headerMap = flattenHeaders(headers)
|
|
171
|
+
const orderedHeaders = flattenHeadersInOrder(headers)
|
|
172
|
+
const headerIndexBySlug = new Map(
|
|
173
|
+
orderedHeaders.map((item, index) => [item.slug, index]),
|
|
174
|
+
)
|
|
175
|
+
const activeHeaders = new Map<number, MarkdownItHeader>()
|
|
176
|
+
let headerCursor = 0
|
|
177
|
+
|
|
178
|
+
function getCurrentHeader() {
|
|
179
|
+
const levels = Array.from(activeHeaders.keys()).sort(
|
|
180
|
+
(left, right) => right - left,
|
|
181
|
+
)
|
|
182
|
+
const nearestLevel = levels[0]
|
|
183
|
+
return nearestLevel ? activeHeaders.get(nearestLevel) : undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function resolveHeading(
|
|
187
|
+
headingLevel: number,
|
|
188
|
+
headingSlug?: string,
|
|
189
|
+
headingTitle?: string,
|
|
190
|
+
) {
|
|
191
|
+
if (headingSlug) {
|
|
192
|
+
const bySlug = headerMap.get(headingSlug)
|
|
193
|
+
if (bySlug) {
|
|
194
|
+
const index = headerIndexBySlug.get(headingSlug)
|
|
195
|
+
if (typeof index === 'number')
|
|
196
|
+
headerCursor = Math.max(headerCursor, index + 1)
|
|
197
|
+
return bySlug
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (let index = headerCursor; index < orderedHeaders.length; index += 1) {
|
|
202
|
+
const header = orderedHeaders[index]!
|
|
203
|
+
if (header.level !== headingLevel)
|
|
204
|
+
continue
|
|
205
|
+
if (headingTitle && header.title !== headingTitle)
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
headerCursor = index + 1
|
|
209
|
+
return header
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return undefined
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function processToken(token: {
|
|
216
|
+
type?: string
|
|
217
|
+
tag?: string
|
|
218
|
+
attrs?: [string, string][] | null
|
|
219
|
+
content?: string
|
|
220
|
+
children?: unknown[] | null
|
|
221
|
+
}) {
|
|
222
|
+
const tokenType = token.type ?? ''
|
|
223
|
+
const tokenContent = token.content ?? ''
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
(tokenType === 'html_block'
|
|
227
|
+
|| tokenType === 'html_inline'
|
|
228
|
+
|| tokenType === 'inline')
|
|
229
|
+
&& checkWrapper(tokenContent, wrapper)
|
|
230
|
+
) {
|
|
231
|
+
token.content = replaceSrcPath(
|
|
232
|
+
tokenContent,
|
|
233
|
+
currentId,
|
|
234
|
+
root,
|
|
235
|
+
wrapper,
|
|
236
|
+
headerMode === 'examples' ? fixedHeader : getCurrentHeader(),
|
|
237
|
+
skipDebugDemo,
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (token.children) {
|
|
242
|
+
for (const child of token.children as (typeof token)[]) {
|
|
243
|
+
processToken(child)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const typedTokens = tokens as typeof tokens & Array<{
|
|
249
|
+
type: string
|
|
250
|
+
tag?: string
|
|
251
|
+
attrs?: [string, string][] | null
|
|
252
|
+
content?: string
|
|
253
|
+
children?: unknown[] | null
|
|
254
|
+
}>
|
|
255
|
+
|
|
256
|
+
typedTokens.forEach((token, index) => {
|
|
257
|
+
if (token.type === 'heading_open') {
|
|
258
|
+
const headingLevel = getHeadingLevel(token)
|
|
259
|
+
if (headingLevel) {
|
|
260
|
+
for (const level of activeHeaders.keys()) {
|
|
261
|
+
if (level >= headingLevel)
|
|
262
|
+
activeHeaders.delete(level)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const headingSlug = getHeadingSlug(token)
|
|
266
|
+
const headingTitle = getHeadingTitle(typedTokens[index + 1])
|
|
267
|
+
const header = resolveHeading(
|
|
268
|
+
headingLevel,
|
|
269
|
+
headingSlug,
|
|
270
|
+
headingTitle,
|
|
271
|
+
)
|
|
272
|
+
if (header)
|
|
273
|
+
activeHeaders.set(headingLevel, header)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
processToken(token)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
return originalRender(tokens, options, env)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type MarkdownIt from 'markdown-it'
|
|
2
|
+
import type { ContainerOptions } from './container'
|
|
3
|
+
|
|
4
|
+
const markerRE
|
|
5
|
+
= /^\[!(TIP|NOTE|INFO|IMPORTANT|WARNING|CAUTION|DANGER)\]([^\n\r]*)/i
|
|
6
|
+
|
|
7
|
+
export function gitHubAlertsPlugin(md: MarkdownIt, options?: ContainerOptions) {
|
|
8
|
+
const titleMark = {
|
|
9
|
+
tip: options?.tipLabel || 'TIP',
|
|
10
|
+
note: options?.noteLabel || 'NOTE',
|
|
11
|
+
info: options?.infoLabel || 'INFO',
|
|
12
|
+
important: options?.importantLabel || 'IMPORTANT',
|
|
13
|
+
warning: options?.warningLabel || 'WARNING',
|
|
14
|
+
caution: options?.cautionLabel || 'CAUTION',
|
|
15
|
+
danger: options?.dangerLabel || 'DANGER',
|
|
16
|
+
} as Record<string, string>
|
|
17
|
+
|
|
18
|
+
md.core.ruler.after('block', 'github-alerts', (state) => {
|
|
19
|
+
const tokens = state.tokens
|
|
20
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
21
|
+
if (tokens[i]!.type !== 'blockquote_open')
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
const startIndex = i
|
|
25
|
+
const open = tokens[startIndex]!
|
|
26
|
+
let endIndex = i + 1
|
|
27
|
+
while (
|
|
28
|
+
endIndex < tokens.length
|
|
29
|
+
&& (tokens[endIndex]!.type !== 'blockquote_close'
|
|
30
|
+
|| tokens[endIndex]!.level !== open.level)
|
|
31
|
+
) {
|
|
32
|
+
endIndex++
|
|
33
|
+
}
|
|
34
|
+
if (endIndex === tokens.length)
|
|
35
|
+
continue
|
|
36
|
+
const close = tokens[endIndex]!
|
|
37
|
+
const firstContent = tokens
|
|
38
|
+
.slice(startIndex, endIndex + 1)
|
|
39
|
+
.find(token => token.type === 'inline')
|
|
40
|
+
if (!firstContent)
|
|
41
|
+
continue
|
|
42
|
+
const match = firstContent.content.match(markerRE)
|
|
43
|
+
if (!match)
|
|
44
|
+
continue
|
|
45
|
+
const type = match[1]!.toLowerCase()
|
|
46
|
+
const title = match[2]?.trim() || titleMark[type] || capitalize(type)
|
|
47
|
+
firstContent.content = firstContent.content
|
|
48
|
+
.slice(match[0].length)
|
|
49
|
+
.trimStart()
|
|
50
|
+
open.type = 'github_alert_open'
|
|
51
|
+
open.tag = 'div'
|
|
52
|
+
open.meta = {
|
|
53
|
+
title,
|
|
54
|
+
type,
|
|
55
|
+
}
|
|
56
|
+
close.type = 'github_alert_close'
|
|
57
|
+
close.tag = 'div'
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
md.renderer.rules.github_alert_open = function (tokens, idx) {
|
|
61
|
+
const { title, type } = tokens[idx]!.meta
|
|
62
|
+
const attrs = ''
|
|
63
|
+
return `<div class="${type} custom-block github-alert"${attrs}><p class="custom-block-title">${title}</p>\n`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function capitalize(str: string) {
|
|
68
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
69
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// markdown-it plugin for normalizing image source
|
|
2
|
+
|
|
3
|
+
import type MarkdownIt from 'markdown-it'
|
|
4
|
+
import { EXTERNAL_URL_RE } from '../shared'
|
|
5
|
+
|
|
6
|
+
export interface ImagePluginOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Support native lazy loading for the `<img>` tag.
|
|
9
|
+
* @default false
|
|
10
|
+
*/
|
|
11
|
+
lazyLoading?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function imagePlugin(md: MarkdownIt, { lazyLoading }: ImagePluginOptions = {}) {
|
|
15
|
+
const imageRule = md.renderer.rules.image!
|
|
16
|
+
md.renderer.rules.image = (tokens, idx, options, env, self) => {
|
|
17
|
+
const token = tokens[idx]!
|
|
18
|
+
let url = token.attrGet('src')
|
|
19
|
+
if (url && !EXTERNAL_URL_RE.test(url)) {
|
|
20
|
+
if (!/^\.?\//.test(url))
|
|
21
|
+
url = `./${url}`
|
|
22
|
+
token.attrSet('src', decodeURIComponent(url))
|
|
23
|
+
}
|
|
24
|
+
if (lazyLoading)
|
|
25
|
+
token.attrSet('loading', 'lazy')
|
|
26
|
+
|
|
27
|
+
return imageRule(tokens, idx, options, env, self)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type MarkdownIt from 'markdown-it'
|
|
2
|
+
import { EXTERNAL_URL_RE } from '../shared'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 让 markdown 中的外部链接在新标签页打开。
|
|
6
|
+
*
|
|
7
|
+
* 仅对带协议(http(s)://、mailto: 等)的外部链接生效,
|
|
8
|
+
* 站内相对链接保持默认行为,避免影响路由内跳转与锚点导航。
|
|
9
|
+
*
|
|
10
|
+
* 已通过 `markdown-it-attrs` 等方式显式声明的 `target` / `rel`
|
|
11
|
+
* 不会被覆盖,保留作者对单个链接的控制权。
|
|
12
|
+
*/
|
|
13
|
+
export function linkPlugin(md: MarkdownIt) {
|
|
14
|
+
const defaultLinkRender
|
|
15
|
+
= md.renderer.rules.link_open
|
|
16
|
+
|| ((tokens, idx, options, _env, self) =>
|
|
17
|
+
self.renderToken(tokens, idx, options))
|
|
18
|
+
|
|
19
|
+
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
|
20
|
+
const token = tokens[idx]!
|
|
21
|
+
const href = token.attrGet('href')
|
|
22
|
+
|
|
23
|
+
if (href && EXTERNAL_URL_RE.test(href)) {
|
|
24
|
+
if (!token.attrGet('target'))
|
|
25
|
+
token.attrSet('target', '_blank')
|
|
26
|
+
if (!token.attrGet('rel'))
|
|
27
|
+
token.attrSet('rel', 'noopener noreferrer')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return defaultLinkRender(tokens, idx, options, env, self)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type MarkdownIt from 'markdown-it'
|
|
2
|
+
|
|
3
|
+
export interface Options {
|
|
4
|
+
hasSingleTheme: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function preWrapperPlugin(md: MarkdownIt, options: Options) {
|
|
8
|
+
const fence = md.renderer.rules.fence!
|
|
9
|
+
md.renderer.rules.fence = (...args) => {
|
|
10
|
+
const [tokens, idx] = args
|
|
11
|
+
const token = tokens[idx]!
|
|
12
|
+
|
|
13
|
+
// remove title from info
|
|
14
|
+
token.info = token.info.replace(/\[.*\]/, '')
|
|
15
|
+
|
|
16
|
+
const active = / active(?: |$)/.test(token.info) ? ' active' : ''
|
|
17
|
+
token.info = token.info.replace(/ active$/, '').replace(/ active /, ' ')
|
|
18
|
+
|
|
19
|
+
const lang = extractLang(token.info)
|
|
20
|
+
const rawCode = fence(...args)
|
|
21
|
+
return `<div class="language-${lang}${getAdaptiveThemeMarker(
|
|
22
|
+
options,
|
|
23
|
+
)}${active}"><button title="Copy Code" class="copy"></button><span class="lang">${lang}</span>${rawCode}</div>`
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getAdaptiveThemeMarker(options: Options) {
|
|
28
|
+
return options.hasSingleTheme ? '' : ' ant-code-theme'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function extractTitle(info: string, html = false) {
|
|
32
|
+
if (html) {
|
|
33
|
+
return (
|
|
34
|
+
info.replace(/<!--[\s\S]*?-->/g, '').match(/data-title="(.*?)"/)?.[1] || ''
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
return info.match(/\[(.*)\]/)?.[1] || extractLang(info) || 'txt'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractLang(info: string) {
|
|
41
|
+
return info
|
|
42
|
+
.trim()
|
|
43
|
+
.replace(/=(\d*)/, '')
|
|
44
|
+
// eslint-disable-next-line regexp/optimal-quantifier-concatenation
|
|
45
|
+
.replace(/:(no-)?line-numbers(\{| |$|=\d*).*/, '')
|
|
46
|
+
.replace(/(-vue|\{| ).*$/, '')
|
|
47
|
+
.replace(/^vue-html$/, 'template')
|
|
48
|
+
.replace(/^ansi$/, '')
|
|
49
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type MarkdownIt from 'markdown-it'
|
|
2
|
+
|
|
3
|
+
export function stackblitzPlugin(md: MarkdownIt) {
|
|
4
|
+
const fence = md.renderer.rules.fence!
|
|
5
|
+
md.renderer.rules.fence = (...args) => {
|
|
6
|
+
const [tokens, idx] = args
|
|
7
|
+
const token = tokens[idx]!
|
|
8
|
+
const info = token.info.trim()
|
|
9
|
+
|
|
10
|
+
if (info.startsWith('stackblitz')) {
|
|
11
|
+
const code = token.content
|
|
12
|
+
// Extract title from {title="..."}
|
|
13
|
+
const titleMatch = info.match(/\{[^}]*title\s*=\s*"([^"]*)"[^}]*\}/)
|
|
14
|
+
const title = titleMatch ? titleMatch[1]! : ''
|
|
15
|
+
|
|
16
|
+
// Encode the code for safe HTML attribute usage
|
|
17
|
+
const encodedCode = encodeURIComponent(code)
|
|
18
|
+
|
|
19
|
+
return `<stackblitz code="${encodedCode}" title="${escapeHtml(title)}"></stackblitz>`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return fence(...args)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function escapeHtml(str: string) {
|
|
27
|
+
return str
|
|
28
|
+
.replace(/&/g, '&')
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/</g, '<')
|
|
31
|
+
.replace(/>/g, '>')
|
|
32
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type MarkdownIt from 'markdown-it'
|
|
2
|
+
import type StateCore from 'markdown-it/lib/rules_core/state_core.mjs'
|
|
3
|
+
|
|
4
|
+
export function tablePlugin(md: MarkdownIt) {
|
|
5
|
+
md.core.ruler.push('table_api_attribute', (state: StateCore) => {
|
|
6
|
+
const tokens = state.tokens
|
|
7
|
+
let inApiSection = false
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
10
|
+
const token = tokens[i]!
|
|
11
|
+
|
|
12
|
+
// 检测 ## 标题
|
|
13
|
+
if (token.type === 'heading_open' && token.tag === 'h2') {
|
|
14
|
+
// 获取标题内容(下一个 token 是 inline)
|
|
15
|
+
const inlineToken = tokens[i + 1]
|
|
16
|
+
if (inlineToken && inlineToken.type === 'inline') {
|
|
17
|
+
const headingText = inlineToken.content.trim().toLowerCase()
|
|
18
|
+
// 检查是否是 Api 标题(支持 Api、API、api 等)
|
|
19
|
+
if (headingText === 'api') {
|
|
20
|
+
inApiSection = true
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
// 遇到其他 ## 标题,结束 Api 区域
|
|
24
|
+
inApiSection = false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 如果在 Api 区域内,给 table 添加类名
|
|
30
|
+
if (inApiSection && token.type === 'table_open') {
|
|
31
|
+
const existingClass = token.attrGet('class') || ''
|
|
32
|
+
const newClass = existingClass ? `${existingClass} component-table-api` : 'component-table-api'
|
|
33
|
+
token.attrSet('class', newClass)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return true
|
|
38
|
+
})
|
|
39
|
+
}
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const BASE62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
2
|
+
|
|
3
|
+
function toBase62(num: number): string {
|
|
4
|
+
let str = ''
|
|
5
|
+
do {
|
|
6
|
+
str = BASE62[num % 62] + str
|
|
7
|
+
num = Math.floor(num / 62)
|
|
8
|
+
} while (num > 0)
|
|
9
|
+
return str
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function shortHash(str: string): string {
|
|
13
|
+
const encoder = new TextEncoder()
|
|
14
|
+
const bytes = encoder.encode(str)
|
|
15
|
+
|
|
16
|
+
let hash = 2166136261
|
|
17
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
18
|
+
hash ^= bytes[i]!
|
|
19
|
+
hash += (hash << 1)
|
|
20
|
+
+ (hash << 4)
|
|
21
|
+
+ (hash << 7)
|
|
22
|
+
+ (hash << 8)
|
|
23
|
+
+ (hash << 24)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return toBase62(hash >>> 0) // 无符号整数 → Base62
|
|
27
|
+
}
|