refactor: migrate from shiki to shikiji (#2520)
This commit is contained in:
parent
e63473a5f8
commit
74138a9a58
17 changed files with 124 additions and 223 deletions
|
@ -1,16 +1,15 @@
|
|||
import type { Highlighter, Lang } from 'shiki-es'
|
||||
import { type Highlighter, type BuiltinLanguage as Lang } from 'shikiji'
|
||||
|
||||
const shiki = ref<Highlighter>()
|
||||
const highlighter = ref<Highlighter>()
|
||||
|
||||
const registeredLang = ref(new Map<string, boolean>())
|
||||
let shikiImport: Promise<void> | undefined
|
||||
let shikijiImport: Promise<void> | undefined
|
||||
|
||||
export function useHighlighter(lang: Lang) {
|
||||
if (!shikiImport) {
|
||||
shikiImport = import('shiki-es')
|
||||
.then(async (r) => {
|
||||
r.setCDN('/shiki/')
|
||||
shiki.value = await r.getHighlighter({
|
||||
if (!shikijiImport) {
|
||||
shikijiImport = import('shikiji')
|
||||
.then(async ({ getHighlighter }) => {
|
||||
highlighter.value = await getHighlighter({
|
||||
themes: [
|
||||
'vitesse-dark',
|
||||
'vitesse-light',
|
||||
|
@ -24,27 +23,27 @@ export function useHighlighter(lang: Lang) {
|
|||
})
|
||||
}
|
||||
|
||||
if (!shiki.value)
|
||||
if (!highlighter.value)
|
||||
return undefined
|
||||
|
||||
if (!registeredLang.value.get(lang)) {
|
||||
shiki.value.loadLanguage(lang)
|
||||
highlighter.value.loadLanguage(lang)
|
||||
.then(() => {
|
||||
registeredLang.value.set(lang, true)
|
||||
})
|
||||
.catch(() => {
|
||||
const fallbackLang = 'md'
|
||||
shiki.value?.loadLanguage(fallbackLang).then(() => {
|
||||
highlighter.value?.loadLanguage(fallbackLang).then(() => {
|
||||
registeredLang.value.set(fallbackLang, true)
|
||||
})
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
return shiki.value
|
||||
return highlighter.value
|
||||
}
|
||||
|
||||
export function useShikiTheme() {
|
||||
function useShikijiTheme() {
|
||||
return useColorMode().value === 'dark' ? 'vitesse-dark' : 'vitesse-light'
|
||||
}
|
||||
|
||||
|
@ -61,16 +60,12 @@ function escapeHtml(text: string) {
|
|||
}
|
||||
|
||||
export function highlightCode(code: string, lang: Lang) {
|
||||
const shiki = useHighlighter(lang)
|
||||
if (!shiki)
|
||||
const highlighter = useHighlighter(lang)
|
||||
if (!highlighter)
|
||||
return escapeHtml(code)
|
||||
|
||||
return shiki.codeToHtml(code, {
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang,
|
||||
theme: useShikiTheme(),
|
||||
theme: useShikijiTheme(),
|
||||
})
|
||||
}
|
||||
|
||||
export function useShiki() {
|
||||
return shiki
|
||||
}
|
|
@ -14,7 +14,7 @@ import { Plugin } from 'prosemirror-state'
|
|||
|
||||
import type { Ref } from 'vue'
|
||||
import { TiptapEmojiSuggestion, TiptapHashtagSuggestion, TiptapMentionSuggestion } from './tiptap/suggestion'
|
||||
import { TiptapPluginCodeBlockShiki } from './tiptap/shiki'
|
||||
import { TiptapPluginCodeBlockShikiji } from './tiptap/shikiji'
|
||||
import { TiptapPluginCustomEmoji } from './tiptap/custom-emoji'
|
||||
import { TiptapPluginEmoji } from './tiptap/emoji'
|
||||
|
||||
|
@ -70,7 +70,7 @@ export function useTiptap(options: UseTiptapOptions) {
|
|||
Placeholder.configure({
|
||||
placeholder: () => placeholder.value!,
|
||||
}),
|
||||
TiptapPluginCodeBlockShiki,
|
||||
TiptapPluginCodeBlockShikiji,
|
||||
History.configure({
|
||||
depth: 10,
|
||||
}),
|
||||
|
|
|
@ -1,129 +0,0 @@
|
|||
import type { CodeBlockOptions } from '@tiptap/extension-code-block'
|
||||
import CodeBlock from '@tiptap/extension-code-block'
|
||||
import { VueNodeViewRenderer } from '@tiptap/vue-3'
|
||||
|
||||
import { findChildren } from '@tiptap/core'
|
||||
import type { Node as ProsemirrorNode } from 'prosemirror-model'
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
import TiptapCodeBlock from '~/components/tiptap/TiptapCodeBlock.vue'
|
||||
|
||||
export interface CodeBlockShikiOptions extends CodeBlockOptions {
|
||||
defaultLanguage: string | null | undefined
|
||||
}
|
||||
|
||||
export const TiptapPluginCodeBlockShiki = CodeBlock.extend<CodeBlockShikiOptions>({
|
||||
addOptions() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
defaultLanguage: null,
|
||||
}
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
...this.parent?.() || [],
|
||||
ProseMirrorShikiPlugin({
|
||||
name: this.name,
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return VueNodeViewRenderer(TiptapCodeBlock)
|
||||
},
|
||||
})
|
||||
|
||||
function getDecorations({
|
||||
doc,
|
||||
name,
|
||||
}: { doc: ProsemirrorNode; name: string }) {
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
findChildren(doc, node => node.type.name === name)
|
||||
.forEach((block) => {
|
||||
let from = block.pos + 1
|
||||
const language = block.node.attrs.language
|
||||
|
||||
const shiki = useHighlighter(language)
|
||||
|
||||
if (!shiki)
|
||||
return
|
||||
|
||||
const lines = shiki.codeToThemedTokens(block.node.textContent, language, useShikiTheme())
|
||||
|
||||
lines.forEach((line) => {
|
||||
line.forEach((token) => {
|
||||
const decoration = Decoration.inline(from, from + token.content.length, {
|
||||
style: `color: ${token.color}`,
|
||||
})
|
||||
|
||||
decorations.push(decoration)
|
||||
from += token.content.length
|
||||
})
|
||||
from += 1
|
||||
})
|
||||
})
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
}
|
||||
|
||||
function ProseMirrorShikiPlugin({ name }: { name: string }) {
|
||||
const plugin: Plugin<any> = new Plugin({
|
||||
key: new PluginKey('shiki'),
|
||||
|
||||
state: {
|
||||
init: (_, { doc }) => getDecorations({
|
||||
doc,
|
||||
name,
|
||||
}),
|
||||
apply: (transaction, decorationSet, oldState, newState) => {
|
||||
const oldNodeName = oldState.selection.$head.parent.type.name
|
||||
const newNodeName = newState.selection.$head.parent.type.name
|
||||
const oldNodes = findChildren(oldState.doc, node => node.type.name === name)
|
||||
const newNodes = findChildren(newState.doc, node => node.type.name === name)
|
||||
|
||||
if (
|
||||
transaction.docChanged
|
||||
// Apply decorations if:
|
||||
&& (
|
||||
// selection includes named node,
|
||||
[oldNodeName, newNodeName].includes(name)
|
||||
// OR transaction adds/removes named node,
|
||||
|| newNodes.length !== oldNodes.length
|
||||
// OR transaction has changes that completely encapsulte a node
|
||||
// (for example, a transaction that affects the entire document).
|
||||
// Such transactions can happen during collab syncing via y-prosemirror, for example.
|
||||
|| transaction.steps.some((step) => {
|
||||
// @ts-expect-error cast
|
||||
return step.from !== undefined
|
||||
// @ts-expect-error cast
|
||||
&& step.to !== undefined
|
||||
&& oldNodes.some((node) => {
|
||||
// @ts-expect-error cast
|
||||
return node.pos >= step.from
|
||||
// @ts-expect-error cast
|
||||
&& node.pos + node.node.nodeSize <= step.to
|
||||
})
|
||||
})
|
||||
)
|
||||
) {
|
||||
return getDecorations({
|
||||
doc: transaction.doc,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
return decorationSet.map(transaction.mapping, transaction.doc)
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
decorations(state) {
|
||||
return plugin.getState(state)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return plugin
|
||||
}
|
20
composables/tiptap/shikiji-parser.ts
Normal file
20
composables/tiptap/shikiji-parser.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { type Parser, createParser } from 'prosemirror-highlight/shikiji'
|
||||
import type { BuiltinLanguage } from 'shikiji/langs'
|
||||
|
||||
let parser: Parser | undefined
|
||||
|
||||
export const shikijiParser: Parser = (options) => {
|
||||
const lang = options.language ?? 'text'
|
||||
|
||||
// Register the language if it's not yet registered
|
||||
const highlighter = useHighlighter(lang as BuiltinLanguage)
|
||||
|
||||
// If the language is not loaded, we return an empty set of decorations
|
||||
if (!highlighter)
|
||||
return []
|
||||
|
||||
if (!parser)
|
||||
parser = createParser(highlighter)
|
||||
|
||||
return parser(options)
|
||||
}
|
25
composables/tiptap/shikiji.ts
Normal file
25
composables/tiptap/shikiji.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import CodeBlock from '@tiptap/extension-code-block'
|
||||
import { VueNodeViewRenderer } from '@tiptap/vue-3'
|
||||
|
||||
import { createHighlightPlugin } from 'prosemirror-highlight'
|
||||
import { shikijiParser } from './shikiji-parser'
|
||||
import TiptapCodeBlock from '~/components/tiptap/TiptapCodeBlock.vue'
|
||||
|
||||
export const TiptapPluginCodeBlockShikiji = CodeBlock.extend({
|
||||
addOptions() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
defaultLanguage: null,
|
||||
}
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
createHighlightPlugin({ parser: shikijiParser, nodeTypes: ['codeBlock'] }),
|
||||
]
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return VueNodeViewRenderer(TiptapCodeBlock)
|
||||
},
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue