From b6e4f8c89f6da5bfe98a684e5cefc35798955f68 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Mon, 8 Dec 2025 17:33:18 +0100 Subject: [PATCH 1/6] feat: use highlight options from `@nuxtjs/mdc` to hightligh code snippets --- .../content/ContentEditorTipTap.vue | 4 +- src/app/src/types/index.ts | 3 +- src/app/src/utils/tiptap/tiptapToMdc.ts | 92 +++++++++---------- src/module/src/runtime/composables/useMeta.ts | 18 +++- src/module/src/runtime/host.ts | 1 + src/module/src/runtime/server/routes/meta.ts | 7 +- src/module/src/runtime/utils/document.ts | 4 + 7 files changed, 74 insertions(+), 55 deletions(-) diff --git a/src/app/src/components/content/ContentEditorTipTap.vue b/src/app/src/components/content/ContentEditorTipTap.vue index b7680963..105389a9 100644 --- a/src/app/src/components/content/ContentEditorTipTap.vue +++ b/src/app/src/components/content/ContentEditorTipTap.vue @@ -73,7 +73,9 @@ watch(() => `${document.value?.id}-${props.draftItem.version}-${props.draftItem. watch(tiptapJSON, async (json) => { const cleanedTiptap = removeLastEmptyParagraph(json!) - const { body, data } = await tiptapToMDC(cleanedTiptap) + const { body, data } = await tiptapToMDC(cleanedTiptap, { + syntaxHighlightTheme: host.meta.getSyntaxHighlightTheme(), + }) const compressedBody: MarkdownRoot = compressTree(body) const toc: Toc = generateToc(body, { searchDepth: 2, depth: 2 } as Toc) diff --git a/src/app/src/types/index.ts b/src/app/src/types/index.ts index 75336895..4d041400 100644 --- a/src/app/src/types/index.ts +++ b/src/app/src/types/index.ts @@ -23,7 +23,8 @@ export interface StudioHost { meta: { dev: boolean getComponents: () => ComponentMeta[] - defaultLocale: string + defaultLocale: string, + getSyntaxHighlightTheme: () => { default: string, dark?: string, light?: string } } on: { routeChange: (fn: (to: RouteLocationNormalized, from: RouteLocationNormalized) => void) => void diff --git a/src/app/src/utils/tiptap/tiptapToMdc.ts b/src/app/src/utils/tiptap/tiptapToMdc.ts index 34bcfc5c..9f6562cb 100644 --- a/src/app/src/utils/tiptap/tiptapToMdc.ts +++ b/src/app/src/utils/tiptap/tiptapToMdc.ts @@ -1,15 +1,14 @@ -// import type { MarkdownNode, MarkdownRoot, Toc } from '@nuxt/content' import type { JSONContent } from '@tiptap/vue-3' import Slugger from 'github-slugger' -// import rehypeShiki from '@nuxtjs/mdc/dist/runtime/highlighter/rehype' -// import { createShikiHighlighter } from '@nuxtjs/mdc/runtime/highlighter/shiki' -// import { bundledThemes, bundledLanguages as bundledLangs, createJavaScriptRegexEngine } from 'shiki' -// import { visit } from 'unist-util-visit' -import type { MDCElement, MDCNode, MDCRoot, MDCText } from '@nuxtjs/mdc' -// import type { RehypeMarkdownNode } from '../types' +import type { Highlighter } from '@nuxtjs/mdc'; +import rehypeShiki from '@nuxtjs/mdc/dist/runtime/highlighter/rehype' +import { createShikiHighlighter } from '@nuxtjs/mdc/runtime/highlighter/shiki' +import { bundledThemes, bundledLanguages as bundledLangs, createJavaScriptRegexEngine } from 'shiki' +import { visit } from 'unist-util-visit' +import type { Element, MDCElement, MDCNode, MDCRoot, MDCText } from '@nuxtjs/mdc' let slugs = new Slugger() -// let shikiHighlighter: Highlighter +let shikiHighlighter: Highlighter | undefined type TiptapToMDCMap = Record MDCRoot | MDCNode | MDCNode[]> @@ -50,7 +49,10 @@ const tiptapToMDCMap: TiptapToMDCMap = { } /* Parsing methods */ -export async function tiptapToMDC(node: JSONContent): Promise<{ body: MDCRoot, data: Record }> { +interface TiptapToMDCOptions { + syntaxHighlightTheme?: { default: string, dark?: string, light?: string } +} +export async function tiptapToMDC(node: JSONContent, options: TiptapToMDCOptions = { syntaxHighlightTheme: { default: 'github-light', dark: 'github-dark' } }): Promise<{ body: MDCRoot, data: Record }> { // re-create slugs slugs = new Slugger() @@ -81,7 +83,7 @@ export async function tiptapToMDC(node: JSONContent): Promise<{ body: MDCRoot, d mdc.body = tiptapNodeToMDC(nodeCopy) as MDCRoot - // await applyShikiSyntaxHighlighting(mdc.body) + await applyShikiSyntaxHighlighting(mdc.body, options) return mdc } @@ -115,46 +117,36 @@ export function tiptapNodeToMDC(node: JSONContent): MDCRoot | MDCNode | MDCNode[ } } -// async function applyShikiSyntaxHighlighting(mdc: MarkdownRoot) { -// // convert tag to tagName and props to properties to be compatible with rehype -// // TODO: we may refactor tiptapToMDC to use tagName and properties instead of tag and props to avoid this step -// visit( -// mdc, -// (n: MarkdownNode) => n.tag !== undefined, -// (n: MarkdownNode) => { Object.assign(n, { tagName: n.tag, properties: n.props }) }, -// ) - -// if (typeof useProjects !== 'undefined') { -// const { project } = useProjects() -// if (project.value) { -// const { meta } = useProjectMeta(project.value) -// const theme = meta.value?.content?.highlight?.theme || { default: 'github-light', dark: 'github-dark' } - -// if (!shikiHighlighter) { -// shikiHighlighter = createShikiHighlighter({ bundledThemes, bundledLangs, engine: createJavaScriptRegexEngine({ forgiving: true }) }) -// } -// const shikit = rehypeShiki({ theme, highlighter: shikiHighlighter }) -// // highlight code blocks -// await shikit(mdc as never) -// } -// } - -// // convert back tagName to tag and properties to props to be compatible with MDC -// visit( -// mdc, -// (n: MarkdownNode) => (n as RehypeMarkdownNode).tagName !== undefined, -// (n: MarkdownNode) => { Object.assign(n, { tag: (n as RehypeMarkdownNode).tagName, props: (n as RehypeMarkdownNode).properties, tagName: undefined, properties: undefined }) }, -// ) - -// // remove empty newline text nodes -// visit( -// mdc, -// (n: MarkdownNode) => n.tag === 'pre', -// (n: MarkdownNode) => { -// n.children[0].children = n.children[0].children.filter((child: MarkdownNode) => child.type !== 'text' || child.value.trim()) -// }, -// ) -// } +async function applyShikiSyntaxHighlighting(mdc: MDCRoot, options: TiptapToMDCOptions) { + // convert tag to tagName and props to properties to be compatible with rehype + // TODO: we may refactor tiptapToMDC to use tagName and properties instead of tag and props to avoid this step + // @ts-expect-error MDCNode is not compatible with the type of the visitor + visit(mdc, (n: MDCNode) => n.tag !== undefined, (n: MDCNode) => { Object.assign(n, { tagName: n.tag, properties: n.props }) },) + + if (!shikiHighlighter) { + shikiHighlighter = createShikiHighlighter({ bundledThemes, bundledLangs, engine: createJavaScriptRegexEngine({ forgiving: true }) }) + } + const theme = options.syntaxHighlightTheme || { default: 'github-light', dark: 'github-dark' } + const shikit = rehypeShiki({ theme, highlighter: shikiHighlighter }) + // highlight code blocks + await shikit(mdc as never) + + // convert back tagName to tag and properties to props to be compatible with MDC + visit( + mdc, + (n: unknown) => (n as Element).tagName !== undefined, + (n: unknown) => { Object.assign(n as MDCNode, { tag: (n as Element).tagName, props: (n as Element).properties, tagName: undefined, properties: undefined }) }, + ) + + // remove empty newline text nodes + visit( + mdc, + (n: unknown) => (n as MDCElement).tag === 'pre', + (n: unknown) => { + ((n as MDCElement).children[0] as MDCElement).children = ((n as MDCElement).children[0] as MDCElement).children.filter((child: MDCNode) => child.type !== 'text' || child.value.trim()) + }, + ) +} /* Create element methods */ diff --git a/src/module/src/runtime/composables/useMeta.ts b/src/module/src/runtime/composables/useMeta.ts index 6d90dfc4..b2f1ad79 100644 --- a/src/module/src/runtime/composables/useMeta.ts +++ b/src/module/src/runtime/composables/useMeta.ts @@ -3,14 +3,27 @@ import type { ComponentMeta } from 'nuxt-studio/app' import { shallowRef } from 'vue' import { kebabCase } from 'scule' +interface Meta { + components: ComponentMeta[] + syntaxHighlightTheme: { default: string, dark?: string, light?: string } +} + +const defaultMeta: Meta = { + components: [], + syntaxHighlightTheme: { default: 'github-light', dark: 'github-dark' }, +} + export const useHostMeta = createSharedComposable(() => { const components = shallowRef([]) + const syntaxHighlightTheme = shallowRef() async function fetch() { // TODO: look into this approach and consider possible refactors - const data = await $fetch<{ components: ComponentMeta[] }>('/__nuxt_studio/meta', { + const data = await $fetch('/__nuxt_studio/meta', { headers: { 'content-type': 'application/json' }, - }).catch(() => ({ components: [] })) + }).catch(() => defaultMeta) + + syntaxHighlightTheme.value = data.syntaxHighlightTheme // Markdown elements to exclude (in kebab-case) const markdownElements = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'p', 'li', 'ul', 'ol', 'blockquote', 'code', 'code-block', 'image', 'video', 'link', 'hr', 'img', 'pre', 'em', 'bold', 'italic', 'strike', 'strong', 'tr', 'thead', 'tbody', 'tfoot', 'th', 'td']) @@ -78,5 +91,6 @@ export const useHostMeta = createSharedComposable(() => { return { fetch, components, + syntaxHighlightTheme, } }) diff --git a/src/module/src/runtime/host.ts b/src/module/src/runtime/host.ts index 0525ca4c..7e0dcd09 100644 --- a/src/module/src/runtime/host.ts +++ b/src/module/src/runtime/host.ts @@ -108,6 +108,7 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH dev: false, getComponents: () => meta.components.value, defaultLocale: useRuntimeConfig().public.studio.i18n?.defaultLocale || 'en', + getSyntaxHighlightTheme: () => meta.syntaxHighlightTheme.value || { default: 'github-light', dark: 'github-dark' }, }, on: { routeChange: (fn: (to: RouteLocationNormalized, from: RouteLocationNormalized) => void) => { diff --git a/src/module/src/runtime/server/routes/meta.ts b/src/module/src/runtime/server/routes/meta.ts index d66be578..55eed7f1 100644 --- a/src/module/src/runtime/server/routes/meta.ts +++ b/src/module/src/runtime/server/routes/meta.ts @@ -3,6 +3,8 @@ import { eventHandler, useSession } from 'h3' import { useRuntimeConfig, createError } from '#imports' // @ts-expect-error import does exist import components from '#nuxt-component-meta/nitro' +// @ts-expect-error import does exist +import { highlight } from '@mdc-imports' interface NuxtComponentMeta { pascalName: string @@ -38,8 +40,11 @@ export default eventHandler(async (event) => { }, } }) - + const runtimeConfig = useRuntimeConfig() + const { content } = runtimeConfig return { + content, + syntaxHighlightTheme: highlight?.theme || { default: 'github-light', dark: 'github-dark' }, components: mappedComponents, } }) diff --git a/src/module/src/runtime/utils/document.ts b/src/module/src/runtime/utils/document.ts index b98ef9f2..53595507 100644 --- a/src/module/src/runtime/utils/document.ts +++ b/src/module/src/runtime/utils/document.ts @@ -16,6 +16,7 @@ import type { MinimarkNode, MinimarkTree } from 'minimark' // import type { ParsedContentFile } from '@nuxt/content' import { stringifyMarkdown } from '@nuxtjs/mdc/runtime' import type { Node } from 'unist' +import { useHostMeta } from '../composables/useMeta' const reservedKeys = ['id', 'fsPath', 'stem', 'extension', '__hash__', 'path', 'body', 'meta', 'rawbody'] @@ -323,6 +324,9 @@ export async function generateDocumentFromJSONContent(id: string, content: strin export async function generateDocumentFromMarkdownContent(id: string, content: string, options: MarkdownParsingOptions = { compress: true }): Promise { const document = await parseMarkdown(content, { + highlight: { + theme: useHostMeta().syntaxHighlightTheme.value, + }, remark: { plugins: { 'remark-mdc': { From 614fcd48bb467e58bac146b3bb745398f28dd313 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Mon, 8 Dec 2025 17:35:22 +0100 Subject: [PATCH 2/6] lint: fix --- src/app/src/types/index.ts | 2 +- src/app/src/utils/tiptap/tiptapToMdc.ts | 5 ++--- src/module/src/runtime/server/routes/meta.ts | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app/src/types/index.ts b/src/app/src/types/index.ts index 4d041400..7fc50791 100644 --- a/src/app/src/types/index.ts +++ b/src/app/src/types/index.ts @@ -23,7 +23,7 @@ export interface StudioHost { meta: { dev: boolean getComponents: () => ComponentMeta[] - defaultLocale: string, + defaultLocale: string getSyntaxHighlightTheme: () => { default: string, dark?: string, light?: string } } on: { diff --git a/src/app/src/utils/tiptap/tiptapToMdc.ts b/src/app/src/utils/tiptap/tiptapToMdc.ts index 9f6562cb..72a8267c 100644 --- a/src/app/src/utils/tiptap/tiptapToMdc.ts +++ b/src/app/src/utils/tiptap/tiptapToMdc.ts @@ -1,11 +1,10 @@ import type { JSONContent } from '@tiptap/vue-3' import Slugger from 'github-slugger' -import type { Highlighter } from '@nuxtjs/mdc'; +import type { Highlighter, Element, MDCElement, MDCNode, MDCRoot, MDCText } from '@nuxtjs/mdc' import rehypeShiki from '@nuxtjs/mdc/dist/runtime/highlighter/rehype' import { createShikiHighlighter } from '@nuxtjs/mdc/runtime/highlighter/shiki' import { bundledThemes, bundledLanguages as bundledLangs, createJavaScriptRegexEngine } from 'shiki' import { visit } from 'unist-util-visit' -import type { Element, MDCElement, MDCNode, MDCRoot, MDCText } from '@nuxtjs/mdc' let slugs = new Slugger() let shikiHighlighter: Highlighter | undefined @@ -121,7 +120,7 @@ async function applyShikiSyntaxHighlighting(mdc: MDCRoot, options: TiptapToMDCOp // convert tag to tagName and props to properties to be compatible with rehype // TODO: we may refactor tiptapToMDC to use tagName and properties instead of tag and props to avoid this step // @ts-expect-error MDCNode is not compatible with the type of the visitor - visit(mdc, (n: MDCNode) => n.tag !== undefined, (n: MDCNode) => { Object.assign(n, { tagName: n.tag, properties: n.props }) },) + visit(mdc, (n: MDCNode) => n.tag !== undefined, (n: MDCNode) => Object.assign(n, { tagName: n.tag, properties: n.props })) if (!shikiHighlighter) { shikiHighlighter = createShikiHighlighter({ bundledThemes, bundledLangs, engine: createJavaScriptRegexEngine({ forgiving: true }) }) diff --git a/src/module/src/runtime/server/routes/meta.ts b/src/module/src/runtime/server/routes/meta.ts index 55eed7f1..5d40d8bb 100644 --- a/src/module/src/runtime/server/routes/meta.ts +++ b/src/module/src/runtime/server/routes/meta.ts @@ -4,7 +4,7 @@ import { useRuntimeConfig, createError } from '#imports' // @ts-expect-error import does exist import components from '#nuxt-component-meta/nitro' // @ts-expect-error import does exist -import { highlight } from '@mdc-imports' +import { highlight } from '@mdc-imports' interface NuxtComponentMeta { pascalName: string From 7e5b958e6ea99b7177a2fa9fcdd5949c860a46d4 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Mon, 8 Dec 2025 17:36:08 +0100 Subject: [PATCH 3/6] chore: add shiki to dependencies --- package.json | 1 + pnpm-lock.yaml | 314 ++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 246 insertions(+), 69 deletions(-) diff --git a/package.json b/package.json index 0d17e8fa..fd1c9c3b 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "destr": "^2.0.5", "js-yaml": "^4.1.1", "nuxt-component-meta": "^0.15.0", + "shiki": "^3.19.0", "unstorage": "1.17.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 246e38b1..f9f16b95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,7 +17,7 @@ importers: version: 1.2.73 '@nuxtjs/mdc': specifier: ^0.18.3 - version: 0.18.3(magicast@0.5.1) + version: 0.18.3 '@vueuse/core': specifier: ^13.9.0 version: 13.9.0(vue@3.5.24(typescript@5.9.3)) @@ -33,6 +33,9 @@ importers: nuxt-component-meta: specifier: ^0.15.0 version: 0.15.0(magicast@0.5.1) + shiki: + specifier: ^3.19.0 + version: 3.19.0 unstorage: specifier: 1.17.1 version: 1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2) @@ -54,7 +57,7 @@ importers: version: 4.2.1(magicast@0.5.1) '@nuxt/module-builder': specifier: ^1.0.2 - version: 1.0.2(@nuxt/cli@3.30.0)(@vue/compiler-core@3.5.24)(esbuild@0.25.12)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3)) + version: 1.0.2(@nuxt/cli@3.30.0)(@vue/compiler-core@3.5.25)(esbuild@0.25.12)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3)) '@nuxt/ui': specifier: https://pkg.pr.new/@nuxt/ui@049b182 version: https://pkg.pr.new/@nuxt/ui@049b182(5e8db7122f91659ef416cbbf35e4b85d) @@ -132,16 +135,16 @@ importers: dependencies: '@nuxt/content': specifier: latest - version: 3.8.2(better-sqlite3@12.4.1)(magicast@0.5.1) + version: 3.9.0(better-sqlite3@12.4.1)(magicast@0.5.1) '@nuxt/ui': specifier: https://pkg.pr.new/@nuxt/ui@049b182 - version: https://pkg.pr.new/@nuxt/ui@049b182(ef9e2f34e26c9c927cac06bd226309c5) + version: https://pkg.pr.new/@nuxt/ui@049b182(7dc40839a48769e5440cac311ba7e0b2) better-sqlite3: specifier: ^12.4.1 version: 12.4.1 docus: specifier: ^5.2.1 - version: 5.2.1(61f5cd6ace809d8b701fd5614108e082) + version: 5.2.1(11f0878163c5ad20c88674cad1ad542a) nuxt: specifier: ^4.2.1 version: 4.2.1(@parcel/watcher@2.5.1)(@types/node@24.10.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(meow@13.2.0)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) @@ -153,7 +156,7 @@ importers: dependencies: '@nuxt/content': specifier: latest - version: 3.8.2(better-sqlite3@12.4.1)(magicast@0.5.1) + version: 3.9.0(better-sqlite3@12.4.1)(magicast@0.5.1) better-sqlite3: specifier: ^12.4.1 version: 12.4.1 @@ -945,15 +948,16 @@ packages: valibot: optional: true - '@nuxt/content@3.8.2': - resolution: {integrity: sha512-bqqS2bTpkmLJDCCU3GuApBQBa6BlHuT7OW9GGoTEZ14evilwjlKSLlFNRYKIyS3Ua4L+GXz7Its7pLbXe+nW5w==} + '@nuxt/content@3.9.0': + resolution: {integrity: sha512-ayCDADViRdx/mksxCsWn6CsNgAiSY96UOQI8M9uJ/AfTFrd6E/7pfWMd5yamEziqmRzFLNXO3Q6hE90ZjnW5nQ==} + engines: {node: '>= 20.19.0'} peerDependencies: '@electric-sql/pglite': '*' '@libsql/client': '*' '@valibot/to-json-schema': ^1.3.0 - better-sqlite3: ^12.4.1 + better-sqlite3: ^12.5.0 sqlite3: '*' - valibot: ^1.1.0 + valibot: ^1.2.0 peerDependenciesMeta: '@electric-sql/pglite': optional: true @@ -1106,6 +1110,9 @@ packages: '@nuxtjs/mdc@0.18.3': resolution: {integrity: sha512-Fl64a9OZBH3J7ZpqzSWkrS64oFmLLvledZMcnYH3UzVtgFPo/GaICdJN3Ml83NSs/J9At6HHaP0k3+nrxu2qJw==} + '@nuxtjs/mdc@0.19.1': + resolution: {integrity: sha512-XqhisvgaqTGo2vnF69pn2sQzRwAxgU6pmYa0+Llfz+TrOUrASr9hSxWDt25YhOYocsD40HFpeZyo7pU1TTL+jA==} + '@nuxtjs/robots@5.5.6': resolution: {integrity: sha512-PFp0sSaQs2ceEubvkiUPrWQ0GYTTu5bDH0lGVmJlm0h/Dqmt/e9TziXNKahL8HUV3VG22YzRyuyjd7p8+BaNgw==} @@ -2048,24 +2055,39 @@ packages: '@shikijs/core@3.15.0': resolution: {integrity: sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==} - '@shikijs/engine-javascript@3.15.0': - resolution: {integrity: sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==} + '@shikijs/core@3.19.0': + resolution: {integrity: sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA==} - '@shikijs/engine-oniguruma@3.15.0': - resolution: {integrity: sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==} + '@shikijs/engine-javascript@3.19.0': + resolution: {integrity: sha512-ZfWJNm2VMhKkQIKT9qXbs76RRcT0SF/CAvEz0+RkpUDAoDaCx0uFdCGzSRiD9gSlhm6AHkjdieOBJMaO2eC1rQ==} + + '@shikijs/engine-oniguruma@3.19.0': + resolution: {integrity: sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg==} '@shikijs/langs@3.15.0': resolution: {integrity: sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==} + '@shikijs/langs@3.19.0': + resolution: {integrity: sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg==} + '@shikijs/themes@3.15.0': resolution: {integrity: sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==} + '@shikijs/themes@3.19.0': + resolution: {integrity: sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A==} + '@shikijs/transformers@3.15.0': resolution: {integrity: sha512-Hmwip5ovvSkg+Kc41JTvSHHVfCYF+C8Cp1omb5AJj4Xvd+y9IXz2rKJwmFRGsuN0vpHxywcXJ1+Y4B9S7EG1/A==} + '@shikijs/transformers@3.19.0': + resolution: {integrity: sha512-e6vwrsyw+wx4OkcrDbL+FVCxwx8jgKiCoXzakVur++mIWVcgpzIi8vxf4/b4dVTYrV/nUx5RjinMf4tq8YV8Fw==} + '@shikijs/types@3.15.0': resolution: {integrity: sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==} + '@shikijs/types@3.19.0': + resolution: {integrity: sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ==} + '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -2794,6 +2816,9 @@ packages: '@vue/compiler-core@3.5.24': resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==} + '@vue/compiler-core@3.5.25': + resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==} + '@vue/compiler-dom@3.5.24': resolution: {integrity: sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==} @@ -2861,6 +2886,9 @@ packages: '@vue/shared@3.5.24': resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==} + '@vue/shared@3.5.25': + resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==} + '@vueuse/core@10.11.1': resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} @@ -3303,6 +3331,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -5044,6 +5076,9 @@ packages: mdast-util-to-hast@13.2.0: resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} @@ -5491,8 +5526,8 @@ packages: oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} - oniguruma-to-es@4.3.3: - resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} + oniguruma-to-es@4.3.4: + resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} @@ -6038,6 +6073,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -6112,6 +6151,9 @@ packages: remark-mdc@3.8.1: resolution: {integrity: sha512-TGFY61OhgziAITAomenbw4THQvEHC7MxZI1kO1YL/VuWQTHZ0RG20G6GGATIFeGnq65IUe7dngiQVcVIeFdB/g==} + remark-mdc@3.9.0: + resolution: {integrity: sha512-hRbVWknG8V6HCfWz+YHUQaNey6AchYIi0jheYTUk9Y2XcMrc7ON5uVQOIhnBVQg2zKFm6bIlx4JoETUMM0Pq3g==} + remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} @@ -6291,8 +6333,8 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} - shiki@3.15.0: - resolution: {integrity: sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==} + shiki@3.19.0: + resolution: {integrity: sha512-77VJr3OR/VUZzPiStyRhADmO2jApMM0V2b1qf0RpfWya8Zr1PeZev5AEpPGAAKWdiYUtcZGBE4F5QvJml1PvWA==} side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} @@ -7450,6 +7492,11 @@ packages: peerDependencies: zod: ^3.24.1 + zod-to-json-schema@3.25.0: + resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} + peerDependencies: + zod: ^3.25 || ^4 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -8109,7 +8156,7 @@ snapshots: fast-glob: 3.3.3 pathe: 2.0.3 picocolors: 1.1.1 - unplugin: 2.3.10 + unplugin: 2.3.11 vue: 3.5.24(typescript@5.9.3) optionalDependencies: vue-i18n: 11.1.12(vue@3.5.24(typescript@5.9.3)) @@ -8337,7 +8384,7 @@ snapshots: pkg-types: 2.3.0 remark-mdc: 3.8.1 scule: 1.3.0 - shiki: 3.15.0 + shiki: 3.19.0 slugify: 1.6.6 socket.io-client: 4.8.1 std-env: 3.10.0 @@ -8360,16 +8407,16 @@ snapshots: - supports-color - utf-8-validate - '@nuxt/content@3.8.2(better-sqlite3@12.4.1)(magicast@0.5.1)': + '@nuxt/content@3.9.0(better-sqlite3@12.4.1)(magicast@0.5.1)': dependencies: '@nuxt/kit': 4.2.1(magicast@0.5.1) - '@nuxtjs/mdc': 0.18.3(magicast@0.5.1) - '@shikijs/langs': 3.15.0 + '@nuxtjs/mdc': 0.19.1(magicast@0.5.1) + '@shikijs/langs': 3.19.0 '@sqlite.org/sqlite-wasm': 3.50.4-build1 '@standard-schema/spec': 1.0.0 '@webcontainer/env': 1.1.1 c12: 3.3.2(magicast@0.5.1) - chokidar: 4.0.3 + chokidar: 5.0.0 consola: 3.4.2 db0: 0.3.4(better-sqlite3@12.4.1) defu: 6.1.4 @@ -8378,8 +8425,8 @@ snapshots: hookable: 5.5.3 jiti: 2.6.1 json-schema-to-typescript: 15.0.4 - knitwork: 1.2.0 - mdast-util-to-hast: 13.2.0 + knitwork: 1.3.0 + mdast-util-to-hast: 13.2.1 mdast-util-to-string: 4.0.0 micromark: 4.0.2 micromark-util-character: 2.1.1 @@ -8395,9 +8442,9 @@ snapshots: ohash: 2.0.11 pathe: 2.0.3 pkg-types: 2.3.0 - remark-mdc: 3.8.1 + remark-mdc: 3.9.0 scule: 1.3.0 - shiki: 3.15.0 + shiki: 3.19.0 slugify: 1.6.6 socket.io-client: 4.8.1 std-env: 3.10.0 @@ -8407,9 +8454,9 @@ snapshots: unified: 11.0.5 unist-util-stringify-position: 4.0.0 unist-util-visit: 5.0.0 - unplugin: 2.3.10 + unplugin: 2.3.11 zod: 3.25.76 - zod-to-json-schema: 3.24.6(zod@3.25.76) + zod-to-json-schema: 3.25.0(zod@3.25.76) optionalDependencies: better-sqlite3: 12.4.1 transitivePeerDependencies: @@ -8648,7 +8695,7 @@ snapshots: ignore: 7.0.5 jiti: 2.6.1 klona: 2.0.6 - knitwork: 1.2.0 + knitwork: 1.3.0 mlly: 1.8.0 ohash: 2.0.11 pathe: 2.0.3 @@ -8688,7 +8735,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/module-builder@1.0.2(@nuxt/cli@3.30.0)(@vue/compiler-core@3.5.24)(esbuild@0.25.12)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3))': + '@nuxt/module-builder@1.0.2(@nuxt/cli@3.30.0)(@vue/compiler-core@3.5.25)(esbuild@0.25.12)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3))': dependencies: '@nuxt/cli': 3.30.0(magicast@0.5.1) citty: 0.1.6 @@ -8696,14 +8743,14 @@ snapshots: defu: 6.1.4 jiti: 2.6.1 magic-regexp: 0.10.0 - mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3)) + mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.25)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3)) mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 tsconfck: 3.1.6(typescript@5.9.3) typescript: 5.9.3 - unbuild: 3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3)) - vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)) + unbuild: 3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.25)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3)) + vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.25)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)) transitivePeerDependencies: - '@vue/compiler-core' - esbuild @@ -8906,7 +8953,7 @@ snapshots: - vite - vue - '@nuxt/ui@https://pkg.pr.new/@nuxt/ui@049b182(ef9e2f34e26c9c927cac06bd226309c5)': + '@nuxt/ui@https://pkg.pr.new/@nuxt/ui@049b182(7dc40839a48769e5440cac311ba7e0b2)': dependencies: '@iconify/vue': 5.0.0(vue@3.5.24(typescript@5.9.3)) '@internationalized/date': 3.10.0 @@ -8963,7 +9010,7 @@ snapshots: vaul-vue: 0.4.1(reka-ui@2.6.1(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3)) vue-component-type-helpers: 3.1.5 optionalDependencies: - '@nuxt/content': 3.8.2(better-sqlite3@12.4.1)(magicast@0.5.1) + '@nuxt/content': 3.9.0(better-sqlite3@12.4.1)(magicast@0.5.1) vue-router: 4.6.3(vue@3.5.24(typescript@5.9.3)) zod: 3.25.76 transitivePeerDependencies: @@ -9176,7 +9223,7 @@ snapshots: remark-rehype: 11.1.2 remark-stringify: 11.0.0 scule: 1.3.0 - shiki: 3.15.0 + shiki: 3.19.0 ufo: 1.6.1 unified: 11.0.5 unist-builder: 4.0.0 @@ -9187,7 +9234,7 @@ snapshots: - magicast - supports-color - '@nuxtjs/mdc@0.18.3(magicast@0.5.1)': + '@nuxtjs/mdc@0.18.3': dependencies: '@nuxt/kit': 4.2.1(magicast@0.5.1) '@shikijs/core': 3.15.0 @@ -9225,7 +9272,56 @@ snapshots: remark-rehype: 11.1.2 remark-stringify: 11.0.0 scule: 1.3.0 - shiki: 3.15.0 + shiki: 3.19.0 + ufo: 1.6.1 + unified: 11.0.5 + unist-builder: 4.0.0 + unist-util-visit: 5.0.0 + unwasm: 0.5.0 + vfile: 6.0.3 + transitivePeerDependencies: + - magicast + - supports-color + + '@nuxtjs/mdc@0.19.1(magicast@0.5.1)': + dependencies: + '@nuxt/kit': 4.2.1(magicast@0.5.1) + '@shikijs/core': 3.19.0 + '@shikijs/langs': 3.19.0 + '@shikijs/themes': 3.19.0 + '@shikijs/transformers': 3.19.0 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@vue/compiler-core': 3.5.25 + consola: 3.4.2 + debug: 4.4.3 + defu: 6.1.4 + destr: 2.0.5 + detab: 3.0.2 + github-slugger: 2.0.0 + hast-util-format: 1.1.0 + hast-util-to-mdast: 10.1.2 + hast-util-to-string: 3.0.1 + mdast-util-to-hast: 13.2.1 + micromark-util-sanitize-uri: 2.0.1 + parse5: 8.0.0 + pathe: 2.0.3 + property-information: 7.1.0 + rehype-external-links: 3.0.0 + rehype-minify-whitespace: 6.0.2 + rehype-raw: 7.0.0 + rehype-remark: 10.0.1 + rehype-slug: 6.0.0 + rehype-sort-attribute-values: 5.0.1 + rehype-sort-attributes: 5.0.1 + remark-emoji: 5.0.2 + remark-gfm: 4.0.1 + remark-mdc: 3.9.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-stringify: 11.0.0 + scule: 1.3.0 + shiki: 3.19.0 ufo: 1.6.1 unified: 11.0.5 unist-builder: 4.0.0 @@ -9900,35 +9996,60 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@3.15.0': + '@shikijs/core@3.19.0': dependencies: - '@shikijs/types': 3.15.0 + '@shikijs/types': 3.19.0 '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.3 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 - '@shikijs/engine-oniguruma@3.15.0': + '@shikijs/engine-javascript@3.19.0': dependencies: - '@shikijs/types': 3.15.0 + '@shikijs/types': 3.19.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.4 + + '@shikijs/engine-oniguruma@3.19.0': + dependencies: + '@shikijs/types': 3.19.0 '@shikijs/vscode-textmate': 10.0.2 '@shikijs/langs@3.15.0': dependencies: '@shikijs/types': 3.15.0 + '@shikijs/langs@3.19.0': + dependencies: + '@shikijs/types': 3.19.0 + '@shikijs/themes@3.15.0': dependencies: '@shikijs/types': 3.15.0 + '@shikijs/themes@3.19.0': + dependencies: + '@shikijs/types': 3.19.0 + '@shikijs/transformers@3.15.0': dependencies: '@shikijs/core': 3.15.0 '@shikijs/types': 3.15.0 + '@shikijs/transformers@3.19.0': + dependencies: + '@shikijs/core': 3.19.0 + '@shikijs/types': 3.19.0 + '@shikijs/types@3.15.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + '@shikijs/types@3.19.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/vscode-textmate@10.0.2': {} '@shuding/opentype.js@1.4.0-beta.0': @@ -10697,6 +10818,14 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-core@3.5.25': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.25 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-dom@3.5.24': dependencies: '@vue/compiler-core': 3.5.24 @@ -10813,6 +10942,8 @@ snapshots: '@vue/shared@3.5.24': {} + '@vue/shared@3.5.25': {} + '@vueuse/core@10.11.1(vue@3.5.24(typescript@5.9.3))': dependencies: '@types/web-bluetooth': 0.0.20 @@ -11239,6 +11370,10 @@ snapshots: dependencies: readdirp: 4.1.2 + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + chownr@1.1.4: {} chownr@3.0.0: {} @@ -11646,15 +11781,15 @@ snapshots: diff@8.0.2: {} - docus@5.2.1(61f5cd6ace809d8b701fd5614108e082): + docus@5.2.1(11f0878163c5ad20c88674cad1ad542a): dependencies: '@iconify-json/lucide': 1.2.73 '@iconify-json/simple-icons': 1.2.58 '@iconify-json/vscode-icons': 1.2.33 - '@nuxt/content': 3.8.2(better-sqlite3@12.4.1)(magicast@0.5.1) + '@nuxt/content': 3.9.0(better-sqlite3@12.4.1)(magicast@0.5.1) '@nuxt/image': 1.11.0(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.1) '@nuxt/kit': 4.2.1(magicast@0.5.1) - '@nuxt/ui': https://pkg.pr.new/@nuxt/ui@049b182(ef9e2f34e26c9c927cac06bd226309c5) + '@nuxt/ui': https://pkg.pr.new/@nuxt/ui@049b182(7dc40839a48769e5440cac311ba7e0b2) '@nuxtjs/i18n': 10.2.0(@vue/compiler-dom@3.5.24)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.1)(rollup@4.53.2)(vue@3.5.24(typescript@5.9.3)) '@nuxtjs/mdc': 0.18.2(magicast@0.5.1) '@nuxtjs/robots': 5.5.6(h3@1.15.4)(magicast@0.5.1)(vue@3.5.24(typescript@5.9.3)) @@ -11667,7 +11802,7 @@ snapshots: motion-v: 1.7.4(@vueuse/core@13.9.0(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3)) nuxt: 4.2.1(@parcel/watcher@2.5.1)(@types/node@24.10.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(meow@13.2.0)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) nuxt-llms: 0.1.3(magicast@0.5.1) - nuxt-og-image: 5.1.12(@unhead/vue@2.0.19(vue@3.5.24(typescript@5.9.3)))(h3@1.15.4)(magicast@0.5.1)(unstorage@1.17.2(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3)) + nuxt-og-image: 5.1.12(@unhead/vue@2.0.19(vue@3.5.24(typescript@5.9.3)))(h3@1.15.4)(magicast@0.5.1)(unstorage@1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3)) pkg-types: 2.3.0 scule: 1.3.0 tailwindcss: 4.1.17 @@ -13285,6 +13420,18 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.3 + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + mdast-util-to-markdown@2.1.2: dependencies: '@types/mdast': 4.0.4 @@ -13562,7 +13709,7 @@ snapshots: mkdirp-classic@0.5.3: {} - mkdist@2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3)): + mkdist@2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.25)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3)): dependencies: autoprefixer: 10.4.22(postcss@8.5.6) citty: 0.1.6 @@ -13580,7 +13727,7 @@ snapshots: optionalDependencies: typescript: 5.9.3 vue: 3.5.24(typescript@5.9.3) - vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)) + vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.25)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)) vue-tsc: 3.1.3(typescript@5.9.3) mlly@1.8.0: @@ -13697,7 +13844,7 @@ snapshots: ioredis: 5.8.2 jiti: 2.6.1 klona: 2.0.6 - knitwork: 1.2.0 + knitwork: 1.3.0 listhen: 1.9.0 magic-string: 0.30.21 magicast: 0.5.1 @@ -13841,7 +13988,7 @@ snapshots: transitivePeerDependencies: - magicast - nuxt-og-image@5.1.12(@unhead/vue@2.0.19(vue@3.5.24(typescript@5.9.3)))(h3@1.15.4)(magicast@0.5.1)(unstorage@1.17.2(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3)): + nuxt-og-image@5.1.12(@unhead/vue@2.0.19(vue@3.5.24(typescript@5.9.3)))(h3@1.15.4)(magicast@0.5.1)(unstorage@1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3)): dependencies: '@nuxt/devtools-kit': 2.7.0(magicast@0.5.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1)) '@nuxt/kit': 4.2.1(magicast@0.5.1) @@ -13872,7 +14019,7 @@ snapshots: strip-literal: 3.1.0 ufo: 1.6.1 unplugin: 2.3.10 - unstorage: 1.17.2(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2) + unstorage: 1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2) unwasm: 0.3.11 yoga-wasm-web: 0.3.3 transitivePeerDependencies: @@ -14068,7 +14215,7 @@ snapshots: oniguruma-parser@0.12.1: {} - oniguruma-to-es@4.3.3: + oniguruma-to-es@4.3.4: dependencies: oniguruma-parser: 0.12.1 regex: 6.0.1 @@ -14770,6 +14917,8 @@ snapshots: readdirp@4.1.2: {} + readdirp@5.0.0: {} + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -14937,6 +15086,29 @@ snapshots: transitivePeerDependencies: - supports-color + remark-mdc@3.9.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + flat: 6.0.1 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark: 4.0.2 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + parse-entities: 4.0.2 + scule: 1.3.0 + stringify-entities: 4.0.4 + unified: 11.0.5 + unist-util-visit: 5.0.0 + unist-util-visit-parents: 6.0.2 + yaml: 2.8.1 + transitivePeerDependencies: + - supports-color + remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -15155,14 +15327,14 @@ snapshots: shell-quote@1.8.3: {} - shiki@3.15.0: + shiki@3.19.0: dependencies: - '@shikijs/core': 3.15.0 - '@shikijs/engine-javascript': 3.15.0 - '@shikijs/engine-oniguruma': 3.15.0 - '@shikijs/langs': 3.15.0 - '@shikijs/themes': 3.15.0 - '@shikijs/types': 3.15.0 + '@shikijs/core': 3.19.0 + '@shikijs/engine-javascript': 3.19.0 + '@shikijs/engine-oniguruma': 3.19.0 + '@shikijs/langs': 3.19.0 + '@shikijs/themes': 3.19.0 + '@shikijs/types': 3.19.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -15598,7 +15770,7 @@ snapshots: ultrahtml@1.6.0: {} - unbuild@3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3)): + unbuild@3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.25)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3)): dependencies: '@rollup/plugin-alias': 5.1.1(rollup@4.53.2) '@rollup/plugin-commonjs': 28.0.9(rollup@4.53.2) @@ -15614,7 +15786,7 @@ snapshots: hookable: 5.5.3 jiti: 2.6.1 magic-string: 0.30.21 - mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3)) + mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.25)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3)) mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 @@ -15889,17 +16061,17 @@ snapshots: unwasm@0.3.11: dependencies: - knitwork: 1.2.0 + knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 - unplugin: 2.3.10 + unplugin: 2.3.11 unwasm@0.4.2: dependencies: exsolve: 1.0.8 - knitwork: 1.2.0 + knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.0 pathe: 2.0.3 @@ -16183,10 +16355,10 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.24(typescript@5.9.3) - vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)): + vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.25)(esbuild@0.25.12)(vue@3.5.24(typescript@5.9.3)): dependencies: '@babel/parser': 7.28.5 - '@vue/compiler-core': 3.5.24 + '@vue/compiler-core': 3.5.25 esbuild: 0.25.12 vue: 3.5.24(typescript@5.9.3) @@ -16351,6 +16523,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.0(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.25.76: {} zod@4.1.12: {} From b1d6f73bd09a610438d9c8e8b17db88e6ab26774 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 9 Dec 2025 12:00:51 +0100 Subject: [PATCH 4/6] test: add test for shiki validation --- src/app/test/unit/utils/tiptap.test.ts | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/app/test/unit/utils/tiptap.test.ts b/src/app/test/unit/utils/tiptap.test.ts index 53e11063..a3805f18 100644 --- a/src/app/test/unit/utils/tiptap.test.ts +++ b/src/app/test/unit/utils/tiptap.test.ts @@ -809,3 +809,54 @@ Hello expect(outputContent).toBe(`${inputContent}\n`) }) }) + +describe('code block', () => { + test('simple code block highlighting', async () => { + const mdcInput: MDCRoot = { + type: 'root', + children: [ + { + type: 'element', + tag: 'pre', + props: { + language: 'javascript', + }, + children: [ + { + type: 'element', + tag: 'code', + props: {}, + children: [{ type: 'text', value: 'console.log("Hello, world!");' }], + }, + ], + }, + ], + } + + const tiptapJSON = await mdcToTiptap(mdcInput, {}) + + const generatedMdcJSON = await tiptapToMDC(tiptapJSON, { }) + const pre = generatedMdcJSON.body.children[0] as JSONContent + + // Tags: pre -> code -> line -> span -> text + expect(pre.tag).toBe('pre') + expect(pre.children.length).toBe(1) + expect(pre.children[0].tag).toBe('code') + expect(pre.props.language).toBe('javascript') + expect(pre.props.code).toBe('console.log("Hello, world!");') + expect(pre.props.className).toBe('shiki shiki-themes github-light github-dark') + + const code = pre.children[0] as JSONContent + const line0 = code.children[0] as JSONContent + + // Make sure the line is parsed correctly + expect(line0.children.length).toBe(5) // console. -- log -- ( -- "Hello, world!" -- ); + for (const child of line0.children) { + expect(child.tag).toBe('span') + expect(child.children.length).toBe(1) + expect(child.children[0].type).toBe('text') + expect(child.props.style.includes('--shiki-default:')).toBeTruthy() + } + // Note we don't check the styles and colors because they are generated by Shiki and we don't want to test Shiki hereq + }) +}) From f2c949452648bae02d8543534824ee77378d6dc2 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 9 Dec 2025 12:42:29 +0100 Subject: [PATCH 5/6] fix: build --- src/app/vite.config.ts | 6 ++++++ src/module/src/runtime/server/routes/meta.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/vite.config.ts b/src/app/vite.config.ts index 6c184601..ac46f953 100644 --- a/src/app/vite.config.ts +++ b/src/app/vite.config.ts @@ -77,5 +77,11 @@ export default defineConfig({ formats: ['es'], }, sourcemap: false, + minify: 'terser', + terserOptions: { + mangle: { + reserved: ['h'], // Reserve 'h' to avoid conflicts + }, + }, }, }) diff --git a/src/module/src/runtime/server/routes/meta.ts b/src/module/src/runtime/server/routes/meta.ts index 5d40d8bb..22f369dc 100644 --- a/src/module/src/runtime/server/routes/meta.ts +++ b/src/module/src/runtime/server/routes/meta.ts @@ -4,7 +4,7 @@ import { useRuntimeConfig, createError } from '#imports' // @ts-expect-error import does exist import components from '#nuxt-component-meta/nitro' // @ts-expect-error import does exist -import { highlight } from '@mdc-imports' +import { highlight } from '#mdc-imports' interface NuxtComponentMeta { pascalName: string From a15dfcacb09c3b21aee9e232e8adddaec0e4039c Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Tue, 9 Dec 2025 16:31:22 +0100 Subject: [PATCH 6/6] types and mini refactor --- .../content/ContentEditorTipTap.vue | 2 +- src/app/src/types/content.ts | 6 + src/app/src/types/index.ts | 4 +- src/app/src/utils/tiptap/tiptapToMdc.ts | 166 ++++++++++-------- src/app/test/unit/utils/tiptap.test.ts | 5 +- src/module/src/runtime/composables/useMeta.ts | 10 +- src/module/src/runtime/host.ts | 2 +- src/module/src/runtime/server/routes/meta.ts | 6 +- src/module/src/runtime/utils/document.ts | 2 +- 9 files changed, 111 insertions(+), 92 deletions(-) diff --git a/src/app/src/components/content/ContentEditorTipTap.vue b/src/app/src/components/content/ContentEditorTipTap.vue index 105389a9..769979e1 100644 --- a/src/app/src/components/content/ContentEditorTipTap.vue +++ b/src/app/src/components/content/ContentEditorTipTap.vue @@ -74,7 +74,7 @@ watch(tiptapJSON, async (json) => { const cleanedTiptap = removeLastEmptyParagraph(json!) const { body, data } = await tiptapToMDC(cleanedTiptap, { - syntaxHighlightTheme: host.meta.getSyntaxHighlightTheme(), + highlightTheme: host.meta.getHighlightTheme(), }) const compressedBody: MarkdownRoot = compressTree(body) diff --git a/src/app/src/types/content.ts b/src/app/src/types/content.ts index 6c78866a..bc43f982 100644 --- a/src/app/src/types/content.ts +++ b/src/app/src/types/content.ts @@ -1,3 +1,9 @@ export interface MarkdownParsingOptions { compress?: boolean } + +export interface SyntaxHighlightTheme { + default: string + dark?: string + light?: string +} diff --git a/src/app/src/types/index.ts b/src/app/src/types/index.ts index 7fc50791..12df1ff0 100644 --- a/src/app/src/types/index.ts +++ b/src/app/src/types/index.ts @@ -4,7 +4,7 @@ import type { RouteLocationNormalized } from 'vue-router' import type { MediaItem } from './media' import type { Repository } from './git' import type { ComponentMeta } from './component' -import type { MarkdownParsingOptions } from './content' +import type { MarkdownParsingOptions, SyntaxHighlightTheme } from './content' export * from './file' export * from './item' @@ -24,7 +24,7 @@ export interface StudioHost { dev: boolean getComponents: () => ComponentMeta[] defaultLocale: string - getSyntaxHighlightTheme: () => { default: string, dark?: string, light?: string } + getHighlightTheme: () => SyntaxHighlightTheme } on: { routeChange: (fn: (to: RouteLocationNormalized, from: RouteLocationNormalized) => void) => void diff --git a/src/app/src/utils/tiptap/tiptapToMdc.ts b/src/app/src/utils/tiptap/tiptapToMdc.ts index 72a8267c..aa4ee533 100644 --- a/src/app/src/utils/tiptap/tiptapToMdc.ts +++ b/src/app/src/utils/tiptap/tiptapToMdc.ts @@ -5,12 +5,14 @@ import rehypeShiki from '@nuxtjs/mdc/dist/runtime/highlighter/rehype' import { createShikiHighlighter } from '@nuxtjs/mdc/runtime/highlighter/shiki' import { bundledThemes, bundledLanguages as bundledLangs, createJavaScriptRegexEngine } from 'shiki' import { visit } from 'unist-util-visit' - -let slugs = new Slugger() -let shikiHighlighter: Highlighter | undefined +import type { SyntaxHighlightTheme } from '../../types/content' type TiptapToMDCMap = Record MDCRoot | MDCNode | MDCNode[]> +interface TiptapToMDCOptions { + highlightTheme?: SyntaxHighlightTheme +} + const markToTag: Record = { bold: 'strong', italic: 'em', @@ -47,11 +49,16 @@ const tiptapToMDCMap: TiptapToMDCMap = { 'br': (node: JSONContent) => createElement(node, 'br'), } -/* Parsing methods */ -interface TiptapToMDCOptions { - syntaxHighlightTheme?: { default: string, dark?: string, light?: string } -} -export async function tiptapToMDC(node: JSONContent, options: TiptapToMDCOptions = { syntaxHighlightTheme: { default: 'github-light', dark: 'github-dark' } }): Promise<{ body: MDCRoot, data: Record }> { +let slugs = new Slugger() +let shikiHighlighter: Highlighter | undefined + +/* + *************************************************************** + ******************** Parsing methods ************************** + *************************************************************** + */ + +export async function tiptapToMDC(node: JSONContent, options?: TiptapToMDCOptions): Promise<{ body: MDCRoot, data: Record }> { // re-create slugs slugs = new Slugger() @@ -82,7 +89,7 @@ export async function tiptapToMDC(node: JSONContent, options: TiptapToMDCOptions mdc.body = tiptapNodeToMDC(nodeCopy) as MDCRoot - await applyShikiSyntaxHighlighting(mdc.body, options) + await applyShikiSyntaxHighlighting(mdc.body, options?.highlightTheme) return mdc } @@ -116,72 +123,9 @@ export function tiptapNodeToMDC(node: JSONContent): MDCRoot | MDCNode | MDCNode[ } } -async function applyShikiSyntaxHighlighting(mdc: MDCRoot, options: TiptapToMDCOptions) { - // convert tag to tagName and props to properties to be compatible with rehype - // TODO: we may refactor tiptapToMDC to use tagName and properties instead of tag and props to avoid this step - // @ts-expect-error MDCNode is not compatible with the type of the visitor - visit(mdc, (n: MDCNode) => n.tag !== undefined, (n: MDCNode) => Object.assign(n, { tagName: n.tag, properties: n.props })) - - if (!shikiHighlighter) { - shikiHighlighter = createShikiHighlighter({ bundledThemes, bundledLangs, engine: createJavaScriptRegexEngine({ forgiving: true }) }) - } - const theme = options.syntaxHighlightTheme || { default: 'github-light', dark: 'github-dark' } - const shikit = rehypeShiki({ theme, highlighter: shikiHighlighter }) - // highlight code blocks - await shikit(mdc as never) - - // convert back tagName to tag and properties to props to be compatible with MDC - visit( - mdc, - (n: unknown) => (n as Element).tagName !== undefined, - (n: unknown) => { Object.assign(n as MDCNode, { tag: (n as Element).tagName, props: (n as Element).properties, tagName: undefined, properties: undefined }) }, - ) - - // remove empty newline text nodes - visit( - mdc, - (n: unknown) => (n as MDCElement).tag === 'pre', - (n: unknown) => { - ((n as MDCElement).children[0] as MDCElement).children = ((n as MDCElement).children[0] as MDCElement).children.filter((child: MDCNode) => child.type !== 'text' || child.value.trim()) - }, - ) -} - -/* Create element methods */ - -/** - * Unwrap a single child if it matches the specified type - */ -function unwrapParagraph(content: JSONContent[]): JSONContent[] { - if (content.length === 1 && content[0]?.type === 'paragraph') { - return content[0].content || [] - } - return content -} - -/** - * Unwrap a default slot's content directly to parent level - */ -function unwrapDefaultSlot(content: JSONContent[]): JSONContent[] { - if (content.length === 1 && content[0]?.type === 'slot' && content[0].attrs?.name === 'default') { - return content[0].content || [] - } - return content -} - -/** - * Process and normalize element props, converting className to class - */ -function normalizeProps(nodeProps: Record, extraProps: object): Array<[string, string]> { - return Object.entries({ ...nodeProps, ...extraProps }) - .map(([key, value]) => { - if (key === 'className') { - return ['class', typeof value === 'string' ? value : (value as Array).join(' ')] as [string, string] - } - return [key.trim(), String(value).trim()] as [string, string] - }) - .filter(([key]) => Boolean(String(key).trim())) -} +/*************************************************************** + *********************** Create element methods **************** + ***************************************************************/ function createElement(node: JSONContent, tag?: string, extra: unknown = {}): MDCElement { const { props = {}, ...rest } = extra as { props: object } @@ -381,7 +325,77 @@ function createListItemElement(node: JSONContent) { return createElement(node, 'li') } -// Merge adjacent children with the same tag if separated by a single space text node +/*************************************************************** + ******************** Utility methods ************************** + ***************************************************************/ + +async function applyShikiSyntaxHighlighting(mdc: MDCRoot, theme: SyntaxHighlightTheme = { default: 'github-light', dark: 'github-dark' }) { + // @ts-expect-error MDCNode is not compatible with the type of the visitor + // Convert tag to tagName and props to properties to be compatible with rehype + visit(mdc, (n: MDCNode) => n.tag !== undefined, (n: MDCNode) => Object.assign(n, { tagName: n.tag, properties: n.props })) + + if (!shikiHighlighter) { + shikiHighlighter = createShikiHighlighter({ bundledThemes, bundledLangs, engine: createJavaScriptRegexEngine({ forgiving: true }) }) + } + + // Highlight code blocks + const shikit = rehypeShiki({ theme: theme as never, highlighter: shikiHighlighter }) + await shikit(mdc as never) + + // Convert back tagName to tag and properties to props to be compatible with MDC + visit( + mdc, + (n: unknown) => (n as Element).tagName !== undefined, + (n: unknown) => { Object.assign(n as MDCNode, { tag: (n as Element).tagName, props: (n as Element).properties, tagName: undefined, properties: undefined }) }, + ) + + // Remove empty newline text nodes + visit( + mdc, + (n: unknown) => (n as MDCElement).tag === 'pre', + (n: unknown) => { + ((n as MDCElement).children[0] as MDCElement).children = ((n as MDCElement).children[0] as MDCElement).children.filter((child: MDCNode) => child.type !== 'text' || child.value.trim()) + }, + ) +} + +/** + * Unwrap a single child if it matches the specified type + */ +function unwrapParagraph(content: JSONContent[]): JSONContent[] { + if (content.length === 1 && content[0]?.type === 'paragraph') { + return content[0].content || [] + } + return content +} + +/** + * Unwrap a default slot's content directly to parent level + */ +function unwrapDefaultSlot(content: JSONContent[]): JSONContent[] { + if (content.length === 1 && content[0]?.type === 'slot' && content[0].attrs?.name === 'default') { + return content[0].content || [] + } + return content +} + +/** + * Process and normalize element props, converting className to class + */ +function normalizeProps(nodeProps: Record, extraProps: object): Array<[string, string]> { + return Object.entries({ ...nodeProps, ...extraProps }) + .map(([key, value]) => { + if (key === 'className') { + return ['class', typeof value === 'string' ? value : (value as Array).join(' ')] as [string, string] + } + return [key.trim(), String(value).trim()] as [string, string] + }) + .filter(([key]) => Boolean(String(key).trim())) +} + +/** + * Merge adjacent children with the same tag if separated by a single space text node + */ function mergeSiblingsWithSameTag(children: MDCNode[], allowedTags: string[]): MDCNode[] { if (!Array.isArray(children)) return children const merged: MDCNode[] = [] diff --git a/src/app/test/unit/utils/tiptap.test.ts b/src/app/test/unit/utils/tiptap.test.ts index a3805f18..ec9cac32 100644 --- a/src/app/test/unit/utils/tiptap.test.ts +++ b/src/app/test/unit/utils/tiptap.test.ts @@ -835,7 +835,7 @@ describe('code block', () => { const tiptapJSON = await mdcToTiptap(mdcInput, {}) - const generatedMdcJSON = await tiptapToMDC(tiptapJSON, { }) + const generatedMdcJSON = await tiptapToMDC(tiptapJSON, { highlightTheme: { default: 'github-light', dark: 'github-dark' } }) const pre = generatedMdcJSON.body.children[0] as JSONContent // Tags: pre -> code -> line -> span -> text @@ -857,6 +857,7 @@ describe('code block', () => { expect(child.children[0].type).toBe('text') expect(child.props.style.includes('--shiki-default:')).toBeTruthy() } - // Note we don't check the styles and colors because they are generated by Shiki and we don't want to test Shiki hereq + + // Note we don't check the styles and colors because they are generated by Shiki and we don't want to test Shiki here }) }) diff --git a/src/module/src/runtime/composables/useMeta.ts b/src/module/src/runtime/composables/useMeta.ts index b2f1ad79..cc447208 100644 --- a/src/module/src/runtime/composables/useMeta.ts +++ b/src/module/src/runtime/composables/useMeta.ts @@ -5,17 +5,17 @@ import { kebabCase } from 'scule' interface Meta { components: ComponentMeta[] - syntaxHighlightTheme: { default: string, dark?: string, light?: string } + highlightTheme: { default: string, dark?: string, light?: string } } const defaultMeta: Meta = { components: [], - syntaxHighlightTheme: { default: 'github-light', dark: 'github-dark' }, + highlightTheme: { default: 'github-light', dark: 'github-dark' }, } export const useHostMeta = createSharedComposable(() => { const components = shallowRef([]) - const syntaxHighlightTheme = shallowRef() + const highlightTheme = shallowRef() async function fetch() { // TODO: look into this approach and consider possible refactors @@ -23,7 +23,7 @@ export const useHostMeta = createSharedComposable(() => { headers: { 'content-type': 'application/json' }, }).catch(() => defaultMeta) - syntaxHighlightTheme.value = data.syntaxHighlightTheme + highlightTheme.value = data.highlightTheme // Markdown elements to exclude (in kebab-case) const markdownElements = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'p', 'li', 'ul', 'ol', 'blockquote', 'code', 'code-block', 'image', 'video', 'link', 'hr', 'img', 'pre', 'em', 'bold', 'italic', 'strike', 'strong', 'tr', 'thead', 'tbody', 'tfoot', 'th', 'td']) @@ -91,6 +91,6 @@ export const useHostMeta = createSharedComposable(() => { return { fetch, components, - syntaxHighlightTheme, + highlightTheme, } }) diff --git a/src/module/src/runtime/host.ts b/src/module/src/runtime/host.ts index 7e0dcd09..7543e298 100644 --- a/src/module/src/runtime/host.ts +++ b/src/module/src/runtime/host.ts @@ -108,7 +108,7 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH dev: false, getComponents: () => meta.components.value, defaultLocale: useRuntimeConfig().public.studio.i18n?.defaultLocale || 'en', - getSyntaxHighlightTheme: () => meta.syntaxHighlightTheme.value || { default: 'github-light', dark: 'github-dark' }, + getHighlightTheme: () => meta.highlightTheme.value!, }, on: { routeChange: (fn: (to: RouteLocationNormalized, from: RouteLocationNormalized) => void) => { diff --git a/src/module/src/runtime/server/routes/meta.ts b/src/module/src/runtime/server/routes/meta.ts index 22f369dc..4110aaec 100644 --- a/src/module/src/runtime/server/routes/meta.ts +++ b/src/module/src/runtime/server/routes/meta.ts @@ -40,11 +40,9 @@ export default eventHandler(async (event) => { }, } }) - const runtimeConfig = useRuntimeConfig() - const { content } = runtimeConfig + return { - content, - syntaxHighlightTheme: highlight?.theme || { default: 'github-light', dark: 'github-dark' }, + highlightTheme: highlight?.theme || { default: 'github-light', dark: 'github-dark', light: 'github-light' }, components: mappedComponents, } }) diff --git a/src/module/src/runtime/utils/document.ts b/src/module/src/runtime/utils/document.ts index d3148fce..b6f925df 100644 --- a/src/module/src/runtime/utils/document.ts +++ b/src/module/src/runtime/utils/document.ts @@ -330,7 +330,7 @@ export async function generateDocumentFromJSONContent(id: string, content: strin export async function generateDocumentFromMarkdownContent(id: string, content: string, options: MarkdownParsingOptions = { compress: true }): Promise { const document = await parseMarkdown(content, { highlight: { - theme: useHostMeta().syntaxHighlightTheme.value, + theme: useHostMeta().highlightTheme.value, }, remark: { plugins: {