@movk/nuxt-docs 1.12.1 → 1.12.3

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 CHANGED
@@ -108,7 +108,8 @@ export default defineAppConfig({
108
108
  streaming: 'i-lucide-chevron-down',
109
109
  providers: {
110
110
  deepseek: 'i-ri-deepseek-fill',
111
- alibaba: 'i-simple-icons-alibabacloud'
111
+ alibaba: 'i-simple-icons-alibabacloud',
112
+ zai: 'i-simple-icons:zig'
112
113
  }
113
114
  }
114
115
  }
package/app/app.vue CHANGED
@@ -64,8 +64,8 @@ provide('navigation', rootNavigation)
64
64
  <ClientOnly>
65
65
  <LazyUContentSearch :files="files" :navigation="rootNavigation" :fuse="{ resultLimit: 1000 }" />
66
66
  <template v-if="isAiChatEnabled">
67
- <LazyAiChatFloatingInput />
68
67
  <LazyAiChatPanel />
68
+ <LazyAiChatFloatingInput />
69
69
  </template>
70
70
  </ClientOnly>
71
71
  </template>
@@ -22,19 +22,3 @@
22
22
  :root {
23
23
  --ui-container: var(--container-8xl);
24
24
  }
25
-
26
- /* Shiki icon highlight transformer styles */
27
- .shiki-icon-highlight {
28
- display: inline-block;
29
- width: 1.25em;
30
- height: 1.25em;
31
- vertical-align: -0.25em;
32
- margin-right: 0.125em;
33
- background-color: var(--ui-text-highlighted);
34
- -webkit-mask-repeat: no-repeat;
35
- mask-repeat: no-repeat;
36
- -webkit-mask-size: 100% 100%;
37
- mask-size: 100% 100%;
38
- -webkit-mask-image: var(--shiki-icon-url);
39
- mask-image: var(--shiki-icon-url);
40
- }
@@ -32,7 +32,7 @@ export interface AiChatModuleOptions {
32
32
  models?: string[]
33
33
  }
34
34
 
35
- const log = logger.withTag('movk-nuxt-docs:ai-assistant')
35
+ const log = logger.withTag('movk-nuxt-docs')
36
36
 
37
37
  export default defineNuxtModule<AiChatModuleOptions>({
38
38
  meta: {
@@ -77,7 +77,7 @@ export default defineNuxtModule<AiChatModuleOptions>({
77
77
  }
78
78
 
79
79
  if (!hasApiKey) {
80
- log.warn('[ai-chat] Module disabled: no API key found in environment variables.')
80
+ log.warn('[movk-nuxt-docs] Ai Chat Module disabled: no API key found in environment variables.')
81
81
  return
82
82
  }
83
83
 
@@ -119,7 +119,7 @@ export default defineNuxtModule<AiChatModuleOptions>({
119
119
  handler: resolve('./runtime/server/api/ai-chat')
120
120
  })
121
121
  } else {
122
- log.info(`[ai-chat] Using custom handler, skipping default handler registration`)
122
+ log.info(`[movk-nuxt-docs] Using custom handler, skipping default handler registration`)
123
123
  }
124
124
  }
125
125
  })
@@ -61,39 +61,48 @@ defineShortcuts(shortcuts)
61
61
  :animate="{ y: 0, opacity: 1 }"
62
62
  :exit="{ y: 100, opacity: 0 }"
63
63
  :transition="{ duration: 0.2, ease: 'easeOut' }"
64
- class="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 px-4"
64
+ class="fixed inset-x-0 z-10 px-4 sm:px-80 bottom-[max(1.5rem,env(safe-area-inset-bottom))]"
65
65
  style="will-change: transform"
66
66
  >
67
- <form @submit.prevent="handleSubmit">
68
- <UInput
69
- ref="inputRef"
70
- v-model="input"
71
- :placeholder="aiChat.texts.placeholder"
72
- size="lg"
73
- :ui="{
74
- root: 'w-72 py-0.5 focus-within:w-96 transition-all duration-300 ease-out',
75
- base: 'bg-default/80 backdrop-blur-xl shadow-lg',
76
- trailing: 'pe-2'
77
- }"
78
- @keydown.enter.exact.prevent="handleSubmit"
79
- >
80
- <template #trailing>
81
- <div class="flex items-center gap-2">
82
- <div class="hidden sm:!flex items-center gap-1">
83
- <UKbd v-for="key in shortcutDisplayKeys" :key="key" :value="key" />
84
- </div>
67
+ <form
68
+ class="flex w-full justify-center"
69
+ @submit.prevent="handleSubmit"
70
+ >
71
+ <div class="w-full max-w-96">
72
+ <UInput
73
+ ref="inputRef"
74
+ v-model="input"
75
+ :placeholder="aiChat.texts.placeholder"
76
+ size="lg"
77
+ maxlength="1000"
78
+ :ui="{
79
+ root: 'group w-full! min-w-0 sm:max-w-96 transition-all duration-300 ease-out [@media(hover:hover)]:hover:scale-105 [@media(hover:hover)]:focus-within:scale-105',
80
+ base: 'bg-default shadow-lg text-base',
81
+ trailing: 'pe-2'
82
+ }"
83
+ @keydown.enter.exact.prevent="handleSubmit"
84
+ >
85
+ <template #trailing>
86
+ <div class="flex items-center gap-2">
87
+ <div class="hidden sm:flex group-focus-within:hidden items-center gap-1">
88
+ <UKbd
89
+ v-for="key in shortcutDisplayKeys"
90
+ :key="key"
91
+ :value="key"
92
+ />
93
+ </div>
85
94
 
86
- <UButton
87
- aria-label="Send Message"
88
- type="submit"
89
- icon="i-lucide-arrow-up"
90
- color="primary"
91
- size="xs"
92
- :disabled="!input.trim()"
93
- />
94
- </div>
95
- </template>
96
- </UInput>
95
+ <UButton
96
+ type="submit"
97
+ icon="i-lucide-arrow-up"
98
+ color="primary"
99
+ size="xs"
100
+ :disabled="!input.trim()"
101
+ />
102
+ </div>
103
+ </template>
104
+ </UInput>
105
+ </div>
97
106
  </form>
98
107
  </motion.div>
99
108
  </AnimatePresence>
@@ -121,13 +121,10 @@ onMounted(() => {
121
121
  <template>
122
122
  <DefineChatContent v-slot="{ showExpandButton = true }">
123
123
  <div class="flex h-full flex-col">
124
- <div class="flex h-16 shrink-0 items-center justify-between border-b border-muted/50 px-4">
124
+ <div class="flex h-16 shrink-0 items-center justify-between border-b border-default px-4">
125
125
  <span class="font-medium text-highlighted">{{ aiChat.texts.title }}</span>
126
126
  <div class="flex items-center gap-1">
127
- <UTooltip
128
- v-if="showExpandButton"
129
- :text="isExpanded ? aiChat.texts.collapse : aiChat.texts.expand"
130
- >
127
+ <UTooltip v-if="showExpandButton" :text="isExpanded ? aiChat.texts.collapse : aiChat.texts.expand">
131
128
  <UButton
132
129
  :icon="isExpanded ? 'i-lucide-minimize-2' : 'i-lucide-maximize-2'"
133
130
  color="neutral"
@@ -138,10 +135,7 @@ onMounted(() => {
138
135
  @click="toggleExpanded"
139
136
  />
140
137
  </UTooltip>
141
- <UTooltip
142
- v-if="chat.messages.length > 0"
143
- :text="aiChat.texts.clearChat"
144
- >
138
+ <UTooltip v-if="chat.messages.length > 0" :text="aiChat.texts.clearChat">
145
139
  <UButton
146
140
  :icon="aiChat.icons.clearChat"
147
141
  color="neutral"
@@ -183,7 +177,11 @@ onMounted(() => {
183
177
  v-for="(part, index) in message.parts"
184
178
  :key="`${message.id}-${part.type}-${index}${'state' in part ? `-${part.state}` : ''}`"
185
179
  >
186
- <AiChatReasoning v-if="part.type === 'reasoning'" :text="part.text" :is-streaming="part.state !== 'done'" />
180
+ <AiChatReasoning
181
+ v-if="part.type === 'reasoning'"
182
+ :text="part.text"
183
+ :is-streaming="part.state !== 'done'"
184
+ />
187
185
 
188
186
  <MDCCached
189
187
  v-else-if="part.type === 'text'"
@@ -217,19 +215,10 @@ onMounted(() => {
217
215
  </template>
218
216
  </UChatMessages>
219
217
 
220
- <div
221
- v-else
222
- class="p-4"
223
- >
224
- <div
225
- v-if="!faqQuestions?.length"
226
- class="flex h-full flex-col items-center justify-center py-12 text-center"
227
- >
218
+ <div v-else class="p-4">
219
+ <div v-if="!faqQuestions?.length" class="flex h-full flex-col items-center justify-center py-12 text-center">
228
220
  <div class="mb-4 flex size-12 items-center justify-center rounded-full bg-primary/10">
229
- <UIcon
230
- name="i-lucide-message-circle-question"
231
- class="size-6 text-primary"
232
- />
221
+ <UIcon name="i-lucide-message-circle-question" class="size-6 text-primary" />
233
222
  </div>
234
223
  <h3 class="mb-2 text-base font-medium text-highlighted">
235
224
  {{ aiChat.texts.askAnything }}
@@ -253,17 +242,16 @@ onMounted(() => {
253
242
  <UChatPrompt
254
243
  v-model="input"
255
244
  :rows="2"
256
- class="text-sm"
257
- variant="subtle"
258
245
  :placeholder="aiChat.texts.placeholder"
246
+ maxlength="1000"
259
247
  :ui="{
260
248
  root: 'shadow-none!',
261
- body: '*:p-0! *:rounded-none!'
249
+ body: '*:p-0! *:rounded-none! *:text-sm!'
262
250
  }"
263
251
  @submit="handleSubmit"
264
252
  >
265
253
  <template #footer>
266
- <div class="hidden items-center divide-x divide-muted/50 sm:flex">
254
+ <div class="flex items-center gap-1 text-xs text-muted">
267
255
  <AiChatModelSelect v-model="model" />
268
256
  <div class="flex gap-1 justify-between items-center px-1 text-xs text-muted">
269
257
  <span>{{ aiChat.texts.lineBreak }}</span>
@@ -281,22 +269,25 @@ onMounted(() => {
281
269
  />
282
270
  </template>
283
271
  </UChatPrompt>
272
+ <div class="mt-1 flex text-xs text-dimmed items-center justify-between">
273
+ <span>刷新时聊天会被清除</span>
274
+ <span>
275
+ {{ input.length }}/1000
276
+ </span>
277
+ </div>
284
278
  </div>
285
279
  </div>
286
280
  </DefineChatContent>
287
281
 
288
282
  <aside
289
283
  v-if="!isMobile"
290
- class="fixed top-0 z-50 h-dvh overflow-hidden border-l border-muted/50 bg-default/95 backdrop-blur-xl transition-[right,width] duration-200 ease-linear will-change-[right,width]"
284
+ class="left-auto! fixed top-0 z-50 h-dvh overflow-hidden border-l border-default bg-default/95 backdrop-blur-xl transition-[right,width] duration-200 ease-linear will-change-[right,width]"
291
285
  :style="{
292
286
  width: `${panelWidth}px`,
293
287
  right: isOpen ? '0' : `-${panelWidth}px`
294
288
  }"
295
289
  >
296
- <div
297
- class="h-full transition-[width] duration-200 ease-linear"
298
- :style="{ width: `${panelWidth}px` }"
299
- >
290
+ <div class="h-full transition-[width] duration-200 ease-linear" :style="{ width: `${panelWidth}px` }">
300
291
  <ReuseChatContent :show-expand-button="true" />
301
292
  </div>
302
293
  </aside>
@@ -304,8 +295,6 @@ onMounted(() => {
304
295
  <USlideover
305
296
  v-else
306
297
  v-model:open="isOpen"
307
- title=" "
308
- description=" "
309
298
  side="right"
310
299
  :ui="{
311
300
  content: 'ring-0 bg-default'
@@ -1,16 +1,28 @@
1
- import { createHighlighter } from 'shiki'
2
- import type { HighlighterGeneric } from 'shiki'
3
- import { createJavaScriptRegexEngine } from 'shiki/engine-javascript.mjs'
1
+ import { createHighlighterCore } from '@shikijs/core'
2
+ import type { HighlighterCore } from '@shikijs/core'
3
+ import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript'
4
4
 
5
- let highlighter: HighlighterGeneric<any, any> | null = null
6
-
7
- let promise: Promise<HighlighterGeneric<any, any>> | null = null
5
+ let highlighter: HighlighterCore | null = null
6
+ let promise: Promise<HighlighterCore> | null = null
8
7
 
9
8
  export const useHighlighter = async () => {
10
9
  if (!promise) {
11
- promise = createHighlighter({
12
- langs: ['vue', 'js', 'ts', 'css', 'html', 'json', 'yaml', 'markdown', 'bash'],
13
- themes: ['material-theme-palenight', 'material-theme-lighter'],
10
+ promise = createHighlighterCore({
11
+ langs: [
12
+ import('@shikijs/langs/vue'),
13
+ import('@shikijs/langs/javascript'),
14
+ import('@shikijs/langs/typescript'),
15
+ import('@shikijs/langs/css'),
16
+ import('@shikijs/langs/html'),
17
+ import('@shikijs/langs/json'),
18
+ import('@shikijs/langs/yaml'),
19
+ import('@shikijs/langs/markdown'),
20
+ import('@shikijs/langs/bash')
21
+ ],
22
+ themes: [
23
+ import('@shikijs/themes/material-theme-palenight'),
24
+ import('@shikijs/themes/material-theme-lighter')
25
+ ],
14
26
  engine: createJavaScriptRegexEngine()
15
27
  })
16
28
  }
package/modules/config.ts CHANGED
@@ -18,7 +18,8 @@ export default defineNuxtModule({
18
18
  const meta = await getPackageJsonMetadata(dir)
19
19
  const gitInfo = await getLocalGitInfo(dir) || getGitEnv()
20
20
 
21
- const siteName = nuxt.options?.site?.name || meta.name || gitInfo?.name || ''
21
+ const site = nuxt.options.site
22
+ const siteName = (site && site.name) || meta.name || gitInfo?.name || ''
22
23
 
23
24
  nuxt.options.site = defu(nuxt.options.site, {
24
25
  url,
package/nuxt.config.ts CHANGED
@@ -1,21 +1,21 @@
1
1
  import { defineNuxtConfig } from 'nuxt/config'
2
2
  import pkg from './package.json'
3
+ import { createResolver } from '@nuxt/kit'
3
4
 
4
- // WASM runtime imports that Rollup should not attempt to resolve
5
- const WASM_EXTERNALS = ['env', 'wasi_snapshot_preview1']
6
-
7
- function mergeExternals(existing: unknown, additions: string[]): string[] {
8
- if (Array.isArray(existing)) return [...existing, ...additions]
9
- if (typeof existing === 'string') return [existing, ...additions]
10
- return additions
11
- }
5
+ const { resolve } = createResolver(import.meta.url)
12
6
 
13
7
  export default defineNuxtConfig({
14
8
  modules: [
9
+ resolve('./modules/config'),
10
+ resolve('./modules/routing'),
11
+ resolve('./modules/md-rewrite'),
12
+ resolve('./modules/component-example'),
13
+ resolve('./modules/css'),
15
14
  '@nuxt/ui',
16
15
  '@nuxt/content',
17
16
  '@nuxt/image',
18
17
  '@nuxt/a11y',
18
+ '@nuxtjs/robots',
19
19
  '@nuxtjs/mcp-toolkit',
20
20
  '@vueuse/nuxt',
21
21
  'nuxt-component-meta',
@@ -50,7 +50,8 @@ export default defineNuxtConfig({
50
50
 
51
51
  mdc: {
52
52
  highlight: {
53
- noApiRoute: false
53
+ noApiRoute: false,
54
+ shikiEngine: 'javascript'
54
55
  }
55
56
  },
56
57
 
@@ -76,9 +77,22 @@ export default defineNuxtConfig({
76
77
  crawlLinks: true,
77
78
  failOnError: false,
78
79
  autoSubfolderIndex: false
80
+ },
81
+ compatibilityDate: {
82
+ // Don't generate observability routes for now
83
+ vercel: '2025-07-14'
84
+ }
85
+ },
86
+
87
+ vite: {
88
+ build: {
89
+ sourcemap: false,
90
+ chunkSizeWarningLimit: 1024
79
91
  }
80
92
  },
81
93
 
94
+ telemetry: false,
95
+
82
96
  hooks: {
83
97
  'vite:extendConfig': async (config) => {
84
98
  // Ensure optimizeDeps.include exists
@@ -103,28 +117,6 @@ export default defineNuxtConfig({
103
117
  '@movk/nuxt-docs > mermaid > d3',
104
118
  '@movk/nuxt-docs > mermaid > dompurify'
105
119
  )
106
-
107
- // WASM plugin support for Shiki
108
- const [wasm, topLevelAwait] = await Promise.all([
109
- import('vite-plugin-wasm'),
110
- import('vite-plugin-top-level-await')
111
- ])
112
- config.plugins!.push(wasm.default(), topLevelAwait.default() as any)
113
-
114
- const build = config.build || ((config as any).build = {})
115
- build.rollupOptions ??= {}
116
- build.rollupOptions.external = mergeExternals(
117
- build.rollupOptions.external,
118
- WASM_EXTERNALS
119
- )
120
- },
121
-
122
- 'nitro:config': (nitroConfig) => {
123
- nitroConfig.rollupConfig ??= {}
124
- nitroConfig.rollupConfig.external = mergeExternals(
125
- nitroConfig.rollupConfig.external,
126
- WASM_EXTERNALS
127
- )
128
120
  }
129
121
  },
130
122
 
@@ -156,12 +148,12 @@ export default defineNuxtConfig({
156
148
  fonts: {
157
149
  families: [
158
150
  { name: 'Public Sans', provider: 'google', global: true },
159
- { name: 'DM Sans', provider: 'google', global: true },
160
- { name: 'Geist', provider: 'google', global: true },
161
- { name: 'Inter', provider: 'google', global: true },
162
- { name: 'Poppins', provider: 'google', global: true },
163
- { name: 'Outfit', provider: 'google', global: true },
164
- { name: 'Raleway', provider: 'google', global: true }
151
+ { name: 'DM Sans', provider: 'google' },
152
+ { name: 'Geist', provider: 'google' },
153
+ { name: 'Inter', provider: 'google' },
154
+ { name: 'Poppins', provider: 'google' },
155
+ { name: 'Outfit', provider: 'google' },
156
+ { name: 'Raleway', provider: 'google' }
165
157
  ]
166
158
  },
167
159
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@movk/nuxt-docs",
3
3
  "type": "module",
4
- "version": "1.12.1",
4
+ "version": "1.12.3",
5
5
  "private": false,
6
6
  "description": "Modern Nuxt 4 documentation theme with auto-generated component docs, AI chat assistant, MCP server, and complete developer experience optimization.",
7
7
  "author": "YiXuan <mhaibaraai@gmail.com>",
@@ -31,7 +31,7 @@
31
31
  "dependencies": {
32
32
  "@ai-sdk/gateway": "^3.0.39",
33
33
  "@ai-sdk/mcp": "^1.0.19",
34
- "@ai-sdk/vue": "^3.0.77",
34
+ "@ai-sdk/vue": "^3.0.78",
35
35
  "@iconify-json/lucide": "^1.2.89",
36
36
  "@iconify-json/simple-icons": "^1.2.70",
37
37
  "@iconify-json/vscode-icons": "^1.2.40",
@@ -43,13 +43,18 @@
43
43
  "@nuxt/ui": "^4.4.0",
44
44
  "@nuxtjs/mcp-toolkit": "^0.6.3",
45
45
  "@nuxtjs/mdc": "^0.20.1",
46
+ "@nuxtjs/robots": "^5.7.0",
46
47
  "@octokit/rest": "^22.0.1",
47
- "@openrouter/ai-sdk-provider": "^2.1.1",
48
+ "@openrouter/ai-sdk-provider": "^2.2.1",
49
+ "@shikijs/core": "^3.22.0",
50
+ "@shikijs/engine-javascript": "^3.22.0",
51
+ "@shikijs/langs": "^3.22.0",
52
+ "@shikijs/themes": "^3.22.0",
48
53
  "@vercel/analytics": "^1.6.1",
49
54
  "@vercel/speed-insights": "^1.3.1",
50
55
  "@vueuse/core": "^14.2.0",
51
56
  "@vueuse/nuxt": "^14.2.0",
52
- "ai": "^6.0.77",
57
+ "ai": "^6.0.78",
53
58
  "defu": "^6.1.4",
54
59
  "dompurify": "^3.3.1",
55
60
  "exsolve": "^1.0.8",
@@ -58,7 +63,7 @@
58
63
  "minimark": "^0.2.0",
59
64
  "motion-v": "^1.10.2",
60
65
  "nuxt": "^4.3.1",
61
- "nuxt-component-meta": "^0.17.1",
66
+ "nuxt-component-meta": "^0.17.2",
62
67
  "nuxt-llms": "^0.2.0",
63
68
  "nuxt-og-image": "^5.1.13",
64
69
  "ohash": "^2.0.11",
@@ -66,14 +71,13 @@
66
71
  "pkg-types": "^2.3.0",
67
72
  "prettier": "^3.8.1",
68
73
  "scule": "^1.3.0",
69
- "shiki": "^3.22.0",
70
74
  "shiki-stream": "^0.1.4",
71
75
  "tailwind-merge": "^3.4.0",
72
76
  "tailwindcss": "^4.1.18",
73
77
  "ufo": "^1.6.3",
78
+ "unist-util-visit": "^5.1.0",
74
79
  "vue-component-meta": "^3.2.4",
75
- "vite-plugin-top-level-await": "^1.6.0",
76
- "vite-plugin-wasm": "^3.5.0",
77
- "zod": "^4.3.6"
80
+ "zod": "^4.3.6",
81
+ "zod-to-json-schema": "^3.25.1"
78
82
  }
79
83
  }
@@ -6,8 +6,17 @@ export default defineNitroPlugin((nitroApp) => {
6
6
  await transformMDC(event, doc as any)
7
7
  })
8
8
 
9
- // @ts-expect-error - no types available
10
- nitroApp.hooks.hook('llms:generate', (_, { sections }) => {
9
+ nitroApp.hooks.hook('llms:generate', (_, { sections, domain }) => {
10
+ // Transform links except for "Documentation Sets"
11
+ sections.forEach((section) => {
12
+ if (section.title !== 'Documentation Sets') {
13
+ section.links = section.links?.map(link => ({
14
+ ...link,
15
+ href: `${link.href.replace(new RegExp(`^${domain}`), `${domain}/raw`)}.md`
16
+ }))
17
+ }
18
+ })
19
+
11
20
  // Move "Documentation Sets" to the end
12
21
  const docSetIdx = sections.findIndex((s: any) => s.title === 'Documentation Sets')
13
22
  if (docSetIdx !== -1) {
package/utils/meta.ts CHANGED
@@ -1,16 +1,21 @@
1
- // copy from https://github.com/nuxt-content/docus/tree/main/layer/utils
2
1
  import { readFile } from 'node:fs/promises'
3
2
  import { resolve } from 'node:path'
3
+ import { withHttps } from 'ufo'
4
4
 
5
5
  export function inferSiteURL() {
6
6
  // https://github.com/unjs/std-env/issues/59
7
- return (
8
- process.env.NUXT_SITE_URL
9
- || (process.env.NEXT_PUBLIC_VERCEL_URL && `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`) // Vercel
7
+ const url = (
8
+ process.env.NUXT_PUBLIC_SITE_URL // Nuxt public runtime config
9
+ || process.env.NUXT_SITE_URL // Nuxt site config
10
+ || process.env.VERCEL_PROJECT_PRODUCTION_URL // Vercel production URL
11
+ || process.env.VERCEL_BRANCH_URL // Vercel branch URL
12
+ || process.env.VERCEL_URL // Vercel deployment URL
10
13
  || process.env.URL // Netlify
11
14
  || process.env.CI_PAGES_URL // Gitlab Pages
12
15
  || process.env.CF_PAGES_URL // Cloudflare Pages
13
16
  )
17
+
18
+ return url ? withHttps(url) : undefined
14
19
  }
15
20
 
16
21
  export async function getPackageJsonMetadata(dir: string) {
package/app/mdc.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig } from '@nuxtjs/mdc/config'
2
- import { transformerIconHighlight } from './utils/shiki-transformer-icon-highlight'
3
-
4
- export default defineConfig({
5
- shiki: {
6
- transformers: [
7
- transformerIconHighlight()
8
- ]
9
- }
10
- })
@@ -1,89 +0,0 @@
1
- import type { ShikiTransformer } from 'shiki'
2
-
3
- export interface TransformerIconHighlightOptions {
4
- /**
5
- * Custom function to render the icon HTML
6
- * @default Uses Iconify API with mask mode
7
- */
8
- htmlIcon?: (icon: string) => string
9
- }
10
-
11
- // Common icon collections to validate against (sorted by length descending for proper matching)
12
- const ICON_COLLECTIONS = [
13
- 'simple-icons',
14
- 'vscode-icons',
15
- 'tabler',
16
- 'lucide',
17
- 'logos',
18
- 'ph'
19
- ]
20
-
21
- function parseIconName(text: string): { collection: string, name: string, format: 'i' | 'colon' } | null {
22
- // Strip quotes if present (single, double, or backticks)
23
- let cleanText = text
24
- if (/^['"`].*['"`]$/.test(text)) {
25
- cleanText = text.slice(1, -1)
26
- }
27
-
28
- // Try i-{collection}-{name} format
29
- if (cleanText.startsWith('i-')) {
30
- const rest = cleanText.slice(2)
31
- for (const collection of ICON_COLLECTIONS) {
32
- if (rest.startsWith(`${collection}-`)) {
33
- const name = rest.slice(collection.length + 1)
34
- if (name && /^[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(name)) {
35
- return { collection, name, format: 'i' }
36
- }
37
- }
38
- }
39
- }
40
-
41
- // Try {collection}:{name} format
42
- const colonIndex = cleanText.indexOf(':')
43
- if (colonIndex > 0) {
44
- const collection = cleanText.slice(0, colonIndex)
45
- const name = cleanText.slice(colonIndex + 1)
46
- if (ICON_COLLECTIONS.includes(collection) && name && /^[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(name)) {
47
- return { collection, name, format: 'colon' }
48
- }
49
- }
50
-
51
- return null
52
- }
53
-
54
- export function transformerIconHighlight(options: TransformerIconHighlightOptions = {}): ShikiTransformer {
55
- const { htmlIcon } = options
56
-
57
- return {
58
- name: 'shiki-transformer-icon-highlight',
59
- span(hast, _line, _col, _lineElement, token) {
60
- const text = token.content
61
-
62
- // Try to parse as an icon
63
- const parsed = parseIconName(text)
64
- if (!parsed) return
65
-
66
- const iconIdentifier = `${parsed.collection}:${parsed.name}`
67
- // Add color=black for mask-image to work properly (mask uses luminance)
68
- const iconUrl = `https://api.iconify.design/${iconIdentifier}.svg?color=%23000`
69
-
70
- // Create the icon element as a proper HAST element
71
- const iconElement = htmlIcon
72
- ? { type: 'raw' as const, value: htmlIcon(iconIdentifier) }
73
- : {
74
- type: 'element' as const,
75
- tagName: 'i',
76
- properties: {
77
- class: 'shiki-icon-highlight',
78
- style: `--shiki-icon-url: url(${iconUrl})`
79
- },
80
- children: []
81
- }
82
-
83
- // Prepend the icon to the span content
84
- if (hast.children) {
85
- hast.children.unshift(iconElement)
86
- }
87
- }
88
- }
89
- }