@movk/nuxt-docs 1.13.1 → 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.
Files changed (43) hide show
  1. package/app/app.config.ts +1 -1
  2. package/app/assets/css/main.css +16 -0
  3. package/app/assets/icons/LICENSE +14 -0
  4. package/app/assets/icons/ai.svg +1 -0
  5. package/app/components/OgImage/Nuxt.vue +2 -4
  6. package/app/components/content/CommitChangelog.vue +8 -3
  7. package/app/components/content/ComponentEmits.vue +1 -1
  8. package/app/components/content/ComponentExample.vue +98 -72
  9. package/app/components/content/ComponentProps.vue +3 -3
  10. package/app/components/content/ComponentPropsSchema.vue +1 -1
  11. package/app/components/content/ComponentSlots.vue +1 -1
  12. package/app/components/content/HighlightInlineType.vue +1 -1
  13. package/app/components/content/PageLastCommit.vue +6 -5
  14. package/app/components/header/HeaderLogo.vue +1 -1
  15. package/app/composables/cachedParseMarkdown.ts +12 -0
  16. package/app/composables/fetchComponentExample.ts +5 -22
  17. package/app/composables/fetchComponentMeta.ts +5 -22
  18. package/app/mdc.config.ts +12 -0
  19. package/app/pages/docs/[...slug].vue +8 -2
  20. package/app/templates/releases.vue +3 -1
  21. package/app/types/index.d.ts +1 -1
  22. package/app/utils/shiki-transformer-icon-highlight.ts +89 -0
  23. package/app/workers/prettier.js +26 -17
  24. package/modules/ai-chat/index.ts +1 -1
  25. package/modules/component-example.ts +65 -30
  26. package/modules/config.ts +24 -1
  27. package/modules/css.ts +1 -1
  28. package/nuxt.config.ts +40 -2
  29. package/nuxt.schema.ts +4 -4
  30. package/package.json +17 -17
  31. package/server/api/component-example.get.ts +5 -5
  32. package/server/api/github/{commits.get.ts → commits.json.get.ts} +7 -4
  33. package/server/api/github/{last-commit.get.ts → last-commit.json.get.ts} +12 -9
  34. package/server/api/github/releases.json.get.ts +2 -2
  35. package/server/mcp/resources/documentation-pages.ts +26 -0
  36. package/server/mcp/resources/examples.ts +17 -0
  37. package/server/mcp/tools/get-example.ts +1 -1
  38. package/server/mcp/tools/list-examples.ts +4 -8
  39. package/server/mcp/tools/list-getting-started-guides.ts +29 -0
  40. package/server/routes/raw/[...slug].md.get.ts +3 -5
  41. package/server/utils/stringifyMinimark.ts +345 -0
  42. package/server/utils/transformMDC.ts +14 -5
  43. package/utils/meta.ts +1 -1
@@ -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,7 +1,7 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  export default defineMcpTool({
4
- description: '检索特定的 UI 示例实现代码和详细信息',
4
+ description: '检索特定的示例实现代码和详细信息',
5
5
  inputSchema: {
6
6
  exampleName: z.string().describe('示例名称(PascalCase)')
7
7
  },
@@ -1,14 +1,10 @@
1
1
  // @ts-expect-error - no types available
2
- import components from '#component-example/nitro'
2
+ import { listComponentExamples } from '#component-example/nitro'
3
3
 
4
4
  export default defineMcpTool({
5
- description: '列出所有可用的 UI 示例和代码演示',
5
+ description: '列出所有可用的示例和代码演示',
6
6
  cache: '1h',
7
- handler() {
8
- const examples = Object.entries<{ pascalName: string }>(components).map(([_key, value]) => {
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({ status: 404, statusText: 'Page not found', fatal: true })
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({ status: 404, statusText: 'Page not found', fatal: true })
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 stringify({ ...page.body, type: 'minimark' }, { format: 'markdown/html' })
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
+ ? `![${alt || ''}](${src || ''} "${title}")`
128
+ : `![${alt || ''}](${src || ''})`
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 components from '#component-example/nitro'
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
- const camelName = camelCase(node[1]['name'])
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
@@ -1,5 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises'
2
- import { resolve } from 'node:path'
2
+ import { resolve } from 'pathe'
3
3
  import { withHttps } from 'ufo'
4
4
 
5
5
  export function inferSiteURL() {