@movk/nuxt-docs 1.13.0 → 1.14.0
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/app/app.config.ts +1 -1
- package/app/assets/css/main.css +16 -0
- package/app/assets/icons/LICENSE +14 -0
- package/app/assets/icons/ai.svg +1 -0
- package/app/components/OgImage/Nuxt.vue +2 -4
- package/app/components/content/CommitChangelog.vue +110 -28
- package/app/components/content/ComponentEmits.vue +1 -1
- package/app/components/content/ComponentExample.vue +98 -72
- package/app/components/content/ComponentProps.vue +3 -3
- package/app/components/content/ComponentPropsSchema.vue +1 -1
- package/app/components/content/ComponentSlots.vue +1 -1
- package/app/components/content/HighlightInlineType.vue +1 -1
- package/app/components/content/PageLastCommit.vue +6 -5
- package/app/components/header/HeaderLogo.vue +1 -1
- package/app/composables/cachedParseMarkdown.ts +12 -0
- package/app/composables/fetchComponentExample.ts +5 -22
- package/app/composables/fetchComponentMeta.ts +5 -22
- package/app/mdc.config.ts +12 -0
- package/app/pages/docs/[...slug].vue +8 -2
- package/app/templates/releases.vue +3 -1
- package/app/types/index.d.ts +1 -1
- package/app/utils/shiki-transformer-icon-highlight.ts +89 -0
- package/app/workers/prettier.js +26 -17
- package/modules/ai-chat/index.ts +1 -1
- package/modules/component-example.ts +65 -30
- package/modules/config.ts +24 -1
- package/modules/css.ts +1 -1
- package/nuxt.config.ts +40 -2
- package/nuxt.schema.ts +4 -4
- package/package.json +17 -17
- package/server/api/component-example.get.ts +5 -5
- package/server/api/github/{commits.get.ts → commits.json.get.ts} +7 -4
- package/server/api/github/{last-commit.get.ts → last-commit.json.get.ts} +12 -9
- package/server/api/github/releases.json.get.ts +28 -0
- package/server/mcp/resources/documentation-pages.ts +26 -0
- package/server/mcp/resources/examples.ts +17 -0
- package/server/mcp/tools/get-example.ts +1 -1
- package/server/mcp/tools/list-examples.ts +4 -8
- package/server/mcp/tools/list-getting-started-guides.ts +29 -0
- package/server/routes/raw/[...slug].md.get.ts +3 -5
- package/server/utils/stringifyMinimark.ts +345 -0
- package/server/utils/transformMDC.ts +14 -5
- package/utils/meta.ts +1 -1
|
@@ -24,20 +24,23 @@ export default defineCachedEventHandler(async (event) => {
|
|
|
24
24
|
})
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const octokit = new Octokit({
|
|
27
|
+
const octokit = new Octokit({
|
|
28
|
+
auth: process.env.NUXT_GITHUB_TOKEN,
|
|
29
|
+
request: { timeout: 10_000 }
|
|
30
|
+
})
|
|
28
31
|
|
|
29
32
|
const allCommits = await Promise.all(
|
|
30
33
|
paths.map(path =>
|
|
31
|
-
octokit.
|
|
34
|
+
octokit.rest.repos.listCommits({
|
|
32
35
|
sha: github.branch,
|
|
33
36
|
owner: github.owner,
|
|
34
37
|
repo: github.name,
|
|
35
38
|
path,
|
|
36
39
|
since: github.since,
|
|
37
|
-
per_page: github.per_page,
|
|
40
|
+
per_page: github.per_page || 100,
|
|
38
41
|
until: github.until,
|
|
39
42
|
author
|
|
40
|
-
})
|
|
43
|
+
}).then(res => res.data).catch(() => [])
|
|
41
44
|
)
|
|
42
45
|
)
|
|
43
46
|
|
|
@@ -22,16 +22,19 @@ export default defineCachedEventHandler(async (event) => {
|
|
|
22
22
|
})
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const octokit = new Octokit({
|
|
25
|
+
const octokit = new Octokit({
|
|
26
|
+
auth: process.env.NUXT_GITHUB_TOKEN,
|
|
27
|
+
request: { timeout: 10_000 }
|
|
28
|
+
})
|
|
26
29
|
|
|
27
30
|
try {
|
|
28
|
-
const
|
|
31
|
+
const commits = await octokit.rest.repos.listCommits({
|
|
29
32
|
sha: github.branch,
|
|
30
33
|
owner: github.owner,
|
|
31
34
|
repo: github.name,
|
|
32
35
|
path,
|
|
33
36
|
per_page: 1
|
|
34
|
-
})
|
|
37
|
+
}).then(res => res.data).catch(() => [])
|
|
35
38
|
|
|
36
39
|
if (!commits.length) {
|
|
37
40
|
return null
|
|
@@ -54,16 +57,16 @@ export default defineCachedEventHandler(async (event) => {
|
|
|
54
57
|
// 从 squash commit message 中提取 PR 编号 (#166)
|
|
55
58
|
const prMatch = commit.commit.message.match(/#(\d+)/)
|
|
56
59
|
if (prMatch?.[1]) {
|
|
57
|
-
const
|
|
60
|
+
const prData = await octokit.rest.pulls.get({
|
|
58
61
|
owner: github.owner,
|
|
59
62
|
repo: github.name,
|
|
60
63
|
pull_number: Number.parseInt(prMatch[1])
|
|
61
|
-
})
|
|
64
|
+
}).then(res => res.data).catch(() => null)
|
|
62
65
|
|
|
63
|
-
authorLogin = prData
|
|
64
|
-
authorAvatar = prData
|
|
65
|
-
authorName = prData
|
|
66
|
-
commitUrl = prData
|
|
66
|
+
authorLogin = prData?.user?.login ?? authorLogin
|
|
67
|
+
authorAvatar = prData?.user?.avatar_url ?? authorAvatar
|
|
68
|
+
authorName = prData?.user?.name || authorLogin
|
|
69
|
+
commitUrl = prData?.html_url ?? commitUrl
|
|
67
70
|
}
|
|
68
71
|
} catch {
|
|
69
72
|
// 获取 PR 信息失败时忽略,使用原始提交者信息
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Octokit } from '@octokit/rest'
|
|
2
|
+
|
|
3
|
+
export default defineCachedEventHandler(async () => {
|
|
4
|
+
if (!process.env.NUXT_GITHUB_TOKEN) {
|
|
5
|
+
return []
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { github } = useAppConfig()
|
|
9
|
+
|
|
10
|
+
if (!github || typeof github === 'boolean') {
|
|
11
|
+
throw createError({
|
|
12
|
+
status: 500,
|
|
13
|
+
statusText: 'GitHub configuration is not available'
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const octokit = new Octokit({ auth: process.env.NUXT_GITHUB_TOKEN })
|
|
18
|
+
|
|
19
|
+
const releases = await octokit.rest.repos.listReleases({
|
|
20
|
+
owner: github.owner,
|
|
21
|
+
repo: github.name
|
|
22
|
+
}).then(res => res.data).catch(() => [])
|
|
23
|
+
|
|
24
|
+
return releases
|
|
25
|
+
}, {
|
|
26
|
+
maxAge: 60 * 60,
|
|
27
|
+
getKey: () => 'releases'
|
|
28
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { queryCollection } from '@nuxt/content/server'
|
|
2
|
+
|
|
3
|
+
export default defineMcpResource({
|
|
4
|
+
uri: 'resource://docs/documentation-pages',
|
|
5
|
+
description: '所有可用文档页面的完整列表',
|
|
6
|
+
cache: '1h',
|
|
7
|
+
async handler(uri: URL) {
|
|
8
|
+
const event = useEvent()
|
|
9
|
+
|
|
10
|
+
const pages = await queryCollection(event, 'docs').all()
|
|
11
|
+
|
|
12
|
+
const result = pages.map(doc => ({
|
|
13
|
+
title: doc.title,
|
|
14
|
+
description: doc.description,
|
|
15
|
+
path: doc.path
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
contents: [{
|
|
20
|
+
uri: uri.toString(),
|
|
21
|
+
mimeType: 'application/json',
|
|
22
|
+
text: JSON.stringify(result, null, 2)
|
|
23
|
+
}]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @ts-expect-error - no types available
|
|
2
|
+
import { listComponentExamples } from '#component-example/nitro'
|
|
3
|
+
|
|
4
|
+
export default defineMcpResource({
|
|
5
|
+
uri: 'resource://docs/examples',
|
|
6
|
+
description: '所有可用示例代码和演示的完整列表',
|
|
7
|
+
cache: '1h',
|
|
8
|
+
async handler(uri: URL) {
|
|
9
|
+
return {
|
|
10
|
+
contents: [{
|
|
11
|
+
uri: uri.toString(),
|
|
12
|
+
mimeType: 'application/json',
|
|
13
|
+
text: JSON.stringify(await listComponentExamples(), null, 2)
|
|
14
|
+
}]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
})
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
// @ts-expect-error - no types available
|
|
2
|
-
import
|
|
2
|
+
import { listComponentExamples } from '#component-example/nitro'
|
|
3
3
|
|
|
4
4
|
export default defineMcpTool({
|
|
5
|
-
description: '
|
|
5
|
+
description: '列出所有可用的示例和代码演示',
|
|
6
6
|
cache: '1h',
|
|
7
|
-
handler() {
|
|
8
|
-
|
|
9
|
-
return value.pascalName
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
return jsonResult(examples)
|
|
7
|
+
async handler() {
|
|
8
|
+
return jsonResult(await listComponentExamples())
|
|
13
9
|
}
|
|
14
10
|
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { queryCollection } from '@nuxt/content/server'
|
|
2
|
+
import { inferSiteURL } from '../../../utils/meta'
|
|
3
|
+
|
|
4
|
+
export default defineMcpTool({
|
|
5
|
+
description: '列出所有入门指南和安装说明',
|
|
6
|
+
cache: '30m',
|
|
7
|
+
async handler() {
|
|
8
|
+
const event = useEvent()
|
|
9
|
+
const siteUrl = import.meta.dev
|
|
10
|
+
? getRequestURL(event).origin
|
|
11
|
+
: inferSiteURL()
|
|
12
|
+
|
|
13
|
+
const pages = await queryCollection(event, 'docs')
|
|
14
|
+
.where('path', 'LIKE', '/docs/getting-started/%')
|
|
15
|
+
.where('extension', '=', 'md')
|
|
16
|
+
.select('id', 'title', 'description', 'path', 'navigation')
|
|
17
|
+
.all()
|
|
18
|
+
|
|
19
|
+
const result = pages.map(page => ({
|
|
20
|
+
title: page.title,
|
|
21
|
+
description: page.description,
|
|
22
|
+
path: page.path,
|
|
23
|
+
url: `${siteUrl}${page.path}`,
|
|
24
|
+
navigation: page.navigation
|
|
25
|
+
})).sort((a, b) => a.path.localeCompare(b.path))
|
|
26
|
+
|
|
27
|
+
return jsonResult(result)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { withLeadingSlash } from 'ufo'
|
|
2
|
-
import { stringify } from 'minimark/stringify'
|
|
3
2
|
import { queryCollection } from '@nuxt/content/server'
|
|
4
3
|
import type { Collections, PageCollectionItemBase } from '@nuxt/content'
|
|
5
4
|
import { getRouterParams, eventHandler, createError, setHeader } from 'h3'
|
|
6
5
|
import collections from '#content/manifest'
|
|
7
|
-
import { transformMDC } from '../../utils/transformMDC'
|
|
8
6
|
|
|
9
7
|
export default eventHandler(async (event) => {
|
|
10
8
|
const slug = getRouterParams(event)['slug.md']
|
|
11
9
|
if (!slug?.endsWith('.md')) {
|
|
12
|
-
throw createError({
|
|
10
|
+
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
let path = withLeadingSlash(slug.replace('.md', ''))
|
|
@@ -30,7 +28,7 @@ export default eventHandler(async (event) => {
|
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
if (!page) {
|
|
33
|
-
throw createError({
|
|
31
|
+
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
// Transform MDC components to standard elements for LLM consumption
|
|
@@ -43,5 +41,5 @@ export default eventHandler(async (event) => {
|
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
|
|
46
|
-
return
|
|
44
|
+
return stringifyMinimark(page.body)
|
|
47
45
|
})
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
type MinimarkNode = string | [string, Record<string, any>, ...MinimarkNode[]]
|
|
2
|
+
|
|
3
|
+
const BLOCK_SEP = '\n\n'
|
|
4
|
+
|
|
5
|
+
const SELF_CLOSE_TAGS = new Set(['br', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr'])
|
|
6
|
+
const INLINE_TAGS = new Set(['strong', 'em', 'code', 'a', 'br', 'span', 'img', 'b', 'i', 's', 'del', 'sub', 'sup', 'mark', 'abbr', 'kbd'])
|
|
7
|
+
|
|
8
|
+
// --- HTML attribute serialization ---
|
|
9
|
+
|
|
10
|
+
function htmlAttributes(attributes: Record<string, any>): string {
|
|
11
|
+
return Object.entries(attributes).map(([key, value]) => {
|
|
12
|
+
if (typeof value === 'object') return `${key}="${JSON.stringify(value).replace(/"/g, '\\"')}"`
|
|
13
|
+
return `${key}="${value}"`
|
|
14
|
+
}).join(' ')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// --- HTML fallback for unknown tags ---
|
|
18
|
+
|
|
19
|
+
function toHtml(_node: MinimarkNode[], state: State, tag: string, attrs: Record<string, any>, children: MinimarkNode[]): string {
|
|
20
|
+
const attrStr = Object.keys(attrs).length > 0 ? ` ${htmlAttributes(attrs)}` : ''
|
|
21
|
+
|
|
22
|
+
if (SELF_CLOSE_TAGS.has(tag)) {
|
|
23
|
+
return `<${tag}${attrStr} />`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const hasOnlyInline = children.every(c => typeof c === 'string' || INLINE_TAGS.has(String((c as any[])[0])))
|
|
27
|
+
const content = children.map(c => stringify(c, state)).join('')
|
|
28
|
+
|
|
29
|
+
if (hasOnlyInline) {
|
|
30
|
+
return `<${tag}${attrStr}>${content}</${tag}>`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return `<${tag}${attrStr}>\n${content}\n</${tag}>`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Escape helpers ---
|
|
37
|
+
|
|
38
|
+
function escapePipes(text: string): string {
|
|
39
|
+
return text.split('\n').join(' ').split('|').join('\\|')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function escapeLeadingNumberDot(str: string): string {
|
|
43
|
+
const match = /^(\d+)\./.exec(str)
|
|
44
|
+
if (match) return `${match[1]}\\.${str.slice(match[0].length)}`
|
|
45
|
+
return str
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- State ---
|
|
49
|
+
|
|
50
|
+
interface State {
|
|
51
|
+
listDepth: number
|
|
52
|
+
olIndex: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createState(): State {
|
|
56
|
+
return { listDepth: 0, olIndex: 0 }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Inline helpers ---
|
|
60
|
+
|
|
61
|
+
function flow(children: MinimarkNode[], state: State): string {
|
|
62
|
+
return children.map(c => stringify(c, state)).join('')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function indent(text: string, level: number): string {
|
|
66
|
+
const prefix = ' '.repeat(level)
|
|
67
|
+
return text.split('\n').map((line, i) => i === 0 ? line : (line ? prefix + line : line)).join('\n')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- Node handlers ---
|
|
71
|
+
|
|
72
|
+
function stringify(node: MinimarkNode, state: State): string {
|
|
73
|
+
if (typeof node === 'string') return node
|
|
74
|
+
|
|
75
|
+
const [tag, attrs, ...children] = node
|
|
76
|
+
|
|
77
|
+
// Headings
|
|
78
|
+
const headingMatch = /^h([1-6])$/.exec(tag)
|
|
79
|
+
if (headingMatch) {
|
|
80
|
+
const level = Number(headingMatch[1])
|
|
81
|
+
return `${'#'.repeat(level)} ${flow(children, state)}${BLOCK_SEP}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
switch (tag) {
|
|
85
|
+
case 'p':
|
|
86
|
+
return `${flow(children, state)}${BLOCK_SEP}`
|
|
87
|
+
|
|
88
|
+
case 'blockquote':
|
|
89
|
+
return `${flow(children, state).trim().split('\n').map(l => `> ${l}`).join('\n')}${BLOCK_SEP}`
|
|
90
|
+
|
|
91
|
+
case 'pre': {
|
|
92
|
+
const lang = attrs?.language || ''
|
|
93
|
+
const code = attrs?.code ?? flow(children, state)
|
|
94
|
+
const filename = attrs?.filename ? ` [${String(attrs.filename).replace(/\]/g, '\\]')}]` : ''
|
|
95
|
+
const highlights = Array.isArray(attrs?.highlights) ? ` {${formatHighlights(attrs.highlights)}}` : ''
|
|
96
|
+
const meta = attrs?.meta ? ` ${attrs.meta}` : ''
|
|
97
|
+
return `\`\`\`${lang}${filename}${highlights}${meta}\n${String(code).trim()}\n\`\`\`${BLOCK_SEP}`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case 'code': {
|
|
101
|
+
const content = flow(children, state)
|
|
102
|
+
const fence = content.includes('`') ? '``' : '`'
|
|
103
|
+
return `${fence}${content}${fence}`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'strong':
|
|
107
|
+
case 'b':
|
|
108
|
+
return `**${flow(children, state).trim()}**`
|
|
109
|
+
|
|
110
|
+
case 'em':
|
|
111
|
+
case 'i':
|
|
112
|
+
return `*${flow(children, state).trim()}*`
|
|
113
|
+
|
|
114
|
+
case 'del':
|
|
115
|
+
case 's':
|
|
116
|
+
return `~~${flow(children, state).trim()}~~`
|
|
117
|
+
|
|
118
|
+
case 'a': {
|
|
119
|
+
const { href, ...rest } = attrs || {}
|
|
120
|
+
const attrsStr = Object.keys(rest).length > 0 ? `{${Object.entries(rest).map(([k, v]) => `${k}="${v}"`).join(' ')}}` : ''
|
|
121
|
+
return `[${flow(children, state)}](${href || ''})${attrsStr}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case 'img': {
|
|
125
|
+
const { src, alt, title } = attrs || {}
|
|
126
|
+
return title
|
|
127
|
+
? ``
|
|
128
|
+
: ``
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'br':
|
|
132
|
+
return ' \n'
|
|
133
|
+
|
|
134
|
+
case 'hr':
|
|
135
|
+
return `---${BLOCK_SEP}`
|
|
136
|
+
|
|
137
|
+
case 'ul': {
|
|
138
|
+
const prevDepth = state.listDepth
|
|
139
|
+
const childState = { ...state, listDepth: state.listDepth + 1 }
|
|
140
|
+
let result = children.map(c => stringifyLi(c, childState, false, 0)).join('')
|
|
141
|
+
if (prevDepth > 0) {
|
|
142
|
+
result = '\n' + result.split('\n').map(l => l ? ' ' + l : l).join('\n')
|
|
143
|
+
} else {
|
|
144
|
+
result = result + '\n'
|
|
145
|
+
}
|
|
146
|
+
state.listDepth = prevDepth
|
|
147
|
+
return result
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case 'ol': {
|
|
151
|
+
const prevDepth = state.listDepth
|
|
152
|
+
const childState = { ...state, listDepth: state.listDepth + 1 }
|
|
153
|
+
let idx = 1
|
|
154
|
+
let result = children.map((c) => {
|
|
155
|
+
const r = stringifyLi(c, childState, true, idx)
|
|
156
|
+
idx++
|
|
157
|
+
return r
|
|
158
|
+
}).join('')
|
|
159
|
+
if (prevDepth > 0) {
|
|
160
|
+
result = '\n' + result.split('\n').map(l => l ? ' ' + l : l).join('\n')
|
|
161
|
+
} else {
|
|
162
|
+
result = result + '\n'
|
|
163
|
+
}
|
|
164
|
+
state.listDepth = prevDepth
|
|
165
|
+
return result
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
case 'li':
|
|
169
|
+
return stringifyLi(node, state, false, 0)
|
|
170
|
+
|
|
171
|
+
case 'table':
|
|
172
|
+
return stringifyTable(children, state)
|
|
173
|
+
|
|
174
|
+
// Strip style elements (typically last child in document)
|
|
175
|
+
case 'style':
|
|
176
|
+
return ''
|
|
177
|
+
|
|
178
|
+
// Slot templates
|
|
179
|
+
case 'template': {
|
|
180
|
+
const name = attrs?.name || 'default'
|
|
181
|
+
const content = flow(children, state).trim()
|
|
182
|
+
return `#${name}\n${content}${BLOCK_SEP}`
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
default:
|
|
186
|
+
return toHtml(node as any, state, tag, attrs, children)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// --- List item ---
|
|
191
|
+
|
|
192
|
+
function stringifyLi(node: MinimarkNode, state: State, ordered: boolean, index: number): string {
|
|
193
|
+
if (typeof node === 'string') return node
|
|
194
|
+
|
|
195
|
+
const [, attrs, ...children] = node
|
|
196
|
+
const className = Array.isArray(attrs?.className)
|
|
197
|
+
? attrs.className.join(' ')
|
|
198
|
+
: String(attrs?.className || attrs?.class || '')
|
|
199
|
+
|
|
200
|
+
let prefix = ordered ? `${index}. ` : '- '
|
|
201
|
+
|
|
202
|
+
// Task list support
|
|
203
|
+
if (className.includes('task-list-item') && children.length > 0) {
|
|
204
|
+
const first = children[0]
|
|
205
|
+
if (Array.isArray(first) && first[0] === 'input') {
|
|
206
|
+
const checked = first[1]?.checked || first[1]?.[':checked']
|
|
207
|
+
prefix += checked ? '[x] ' : '[ ] '
|
|
208
|
+
children.shift()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let content = flow(children, state).trim()
|
|
213
|
+
if (!ordered) content = escapeLeadingNumberDot(content)
|
|
214
|
+
|
|
215
|
+
return `${prefix}${indent(content, prefix.length / 2)}\n`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- Table ---
|
|
219
|
+
|
|
220
|
+
function getAlignment(attributes: Record<string, any>): 'left' | 'center' | 'right' | null {
|
|
221
|
+
const style = attributes?.style
|
|
222
|
+
if (typeof style !== 'string') return null
|
|
223
|
+
const normalized = style.toLowerCase().replace(/\s/g, '')
|
|
224
|
+
if (normalized.includes('text-align:left')) return 'left'
|
|
225
|
+
if (normalized.includes('text-align:center')) return 'center'
|
|
226
|
+
if (normalized.includes('text-align:right')) return 'right'
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getCellContent(cell: MinimarkNode, state: State): string {
|
|
231
|
+
if (typeof cell === 'string') return escapePipes(cell)
|
|
232
|
+
const [, , ...children] = cell
|
|
233
|
+
return escapePipes(children.map(c => stringify(c, state)).join('').trim())
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getRows(element: MinimarkNode): MinimarkNode[] {
|
|
237
|
+
if (typeof element === 'string') return []
|
|
238
|
+
const [tag, , ...children] = element
|
|
239
|
+
if (tag === 'tr') return [element]
|
|
240
|
+
if (tag === 'thead' || tag === 'tbody') return children.filter(c => typeof c !== 'string' && (c as any[])[0] === 'tr')
|
|
241
|
+
return []
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getCells(row: MinimarkNode): MinimarkNode[] {
|
|
245
|
+
if (typeof row === 'string') return []
|
|
246
|
+
const [, , ...children] = row
|
|
247
|
+
return children.filter(c => typeof c !== 'string' && ((c as any[])[0] === 'th' || (c as any[])[0] === 'td'))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function stringifyTable(children: MinimarkNode[], state: State): string {
|
|
251
|
+
let headerRows: MinimarkNode[] = []
|
|
252
|
+
let bodyRows: MinimarkNode[] = []
|
|
253
|
+
|
|
254
|
+
for (const child of children) {
|
|
255
|
+
if (typeof child === 'string') continue
|
|
256
|
+
const [tag] = child
|
|
257
|
+
if (tag === 'thead') headerRows = getRows(child)
|
|
258
|
+
else if (tag === 'tbody') bodyRows = getRows(child)
|
|
259
|
+
else if (tag === 'tr') {
|
|
260
|
+
const cells = getCells(child)
|
|
261
|
+
if (cells.length > 0 && (cells[0] as any[])[0] === 'th') headerRows.push(child)
|
|
262
|
+
else bodyRows.push(child)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Auto-generate header if missing
|
|
267
|
+
if (headerRows.length === 0 && bodyRows.length > 0) {
|
|
268
|
+
const firstRow = bodyRows[0]!
|
|
269
|
+
const cellCount = getCells(firstRow).length
|
|
270
|
+
headerRows = [['tr', {}, ...Array.from({ length: cellCount }, (_, i) => ['th', {}, `Column ${i + 1}`])] as any]
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (headerRows.length === 0) return ''
|
|
274
|
+
|
|
275
|
+
const headerRow = headerRows[0]!
|
|
276
|
+
const headerCells = getCells(headerRow)
|
|
277
|
+
const headerContent = headerCells.map(c => getCellContent(c, state))
|
|
278
|
+
const alignments = headerCells.map(c => typeof c !== 'string' ? getAlignment((c as any[])[1] || {}) : null)
|
|
279
|
+
|
|
280
|
+
// Calculate column widths
|
|
281
|
+
const colWidths = headerContent.map(c => Math.max(3, c.length))
|
|
282
|
+
for (const row of bodyRows) {
|
|
283
|
+
getCells(row).forEach((cell, i) => {
|
|
284
|
+
if (i < colWidths.length) {
|
|
285
|
+
colWidths[i] = Math.max(colWidths[i]!, getCellContent(cell, state).length)
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Build header
|
|
291
|
+
let result = '| ' + headerContent.map((c, i) => c.padEnd(colWidths[i]!)).join(' | ') + ' |\n'
|
|
292
|
+
|
|
293
|
+
// Build separator with alignment
|
|
294
|
+
result += '| ' + colWidths.map((w, i) => {
|
|
295
|
+
const a = alignments[i]
|
|
296
|
+
if (a === 'left') return ':' + '-'.repeat(w - 1)
|
|
297
|
+
if (a === 'center') return ':' + '-'.repeat(w - 2) + ':'
|
|
298
|
+
if (a === 'right') return '-'.repeat(w - 1) + ':'
|
|
299
|
+
return '-'.repeat(w)
|
|
300
|
+
}).join(' | ') + ' |\n'
|
|
301
|
+
|
|
302
|
+
// Build body rows
|
|
303
|
+
for (const row of bodyRows) {
|
|
304
|
+
const cellContents = getCells(row).map((cell, i) => getCellContent(cell, state).padEnd(colWidths[i] || 0))
|
|
305
|
+
while (cellContents.length < colWidths.length) cellContents.push(''.padEnd(colWidths[cellContents.length]!))
|
|
306
|
+
result += '| ' + cellContents.join(' | ') + ' |\n'
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return result + '\n'
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// --- Highlight ranges ---
|
|
313
|
+
|
|
314
|
+
function formatHighlights(highlights: number[]): string {
|
|
315
|
+
if (highlights.length === 0) return ''
|
|
316
|
+
const sorted = [...highlights].sort((a, b) => a - b)
|
|
317
|
+
const ranges: string[] = []
|
|
318
|
+
let start = sorted[0]!
|
|
319
|
+
let end = sorted[0]!
|
|
320
|
+
for (let i = 1; i <= sorted.length; i++) {
|
|
321
|
+
if (i < sorted.length && sorted[i] === end + 1) {
|
|
322
|
+
end = sorted[i]!
|
|
323
|
+
} else {
|
|
324
|
+
ranges.push(start === end ? String(start) : `${start}-${end}`)
|
|
325
|
+
if (i < sorted.length) {
|
|
326
|
+
start = sorted[i]!
|
|
327
|
+
end = sorted[i]!
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return ranges.join(',')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// --- Entry point ---
|
|
335
|
+
|
|
336
|
+
export function stringifyMinimark(body: { value: MinimarkNode[] }): string {
|
|
337
|
+
const state = createState()
|
|
338
|
+
const lastIndex = body.value.length - 1
|
|
339
|
+
|
|
340
|
+
return body.value.map((child, index) => {
|
|
341
|
+
// Strip trailing style elements
|
|
342
|
+
if (index === lastIndex && Array.isArray(child) && child[0] === 'style') return ''
|
|
343
|
+
return stringify(child, state)
|
|
344
|
+
}).join('').trim() + '\n'
|
|
345
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { H3Event } from 'h3'
|
|
|
2
2
|
import { camelCase, kebabCase, upperFirst } from 'scule'
|
|
3
3
|
import { visit } from '@nuxt/content/runtime'
|
|
4
4
|
// @ts-expect-error - no types available
|
|
5
|
-
import
|
|
5
|
+
import { getComponentExample } from '#component-example/nitro'
|
|
6
6
|
|
|
7
7
|
type Document = {
|
|
8
8
|
title: string
|
|
@@ -179,13 +179,22 @@ export async function transformMDC(event: H3Event, doc: Document): Promise<Docum
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
// Transform component-example to code block
|
|
182
|
+
const exampleNodes: any[][] = []
|
|
182
183
|
visitAndReplace(doc, 'component-example', (node) => {
|
|
183
|
-
|
|
184
|
-
const name = camelName.charAt(0).toUpperCase() + camelName.slice(1)
|
|
185
|
-
const code = components[name].code
|
|
186
|
-
replaceNodeWithPre(node, 'vue', code, `${name}.vue`)
|
|
184
|
+
exampleNodes.push(node)
|
|
187
185
|
})
|
|
188
186
|
|
|
187
|
+
if (exampleNodes.length) {
|
|
188
|
+
await Promise.all(exampleNodes.map(async (node) => {
|
|
189
|
+
const camelName = camelCase(node[1]['name'])
|
|
190
|
+
const name = camelName.charAt(0).toUpperCase() + camelName.slice(1)
|
|
191
|
+
const component = await getComponentExample(name)
|
|
192
|
+
if (component) {
|
|
193
|
+
replaceNodeWithPre(node, 'vue', component.code, `${name}.vue`)
|
|
194
|
+
}
|
|
195
|
+
}))
|
|
196
|
+
}
|
|
197
|
+
|
|
189
198
|
// Transform callout components (tip, note, warning, caution, callout) to blockquotes
|
|
190
199
|
const calloutTypes = ['tip', 'note', 'warning', 'caution', 'callout']
|
|
191
200
|
const calloutLabels: Record<string, string> = {
|
package/utils/meta.ts
CHANGED