-
-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
472 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,49 @@ | ||
import { CodeBlockLowlight as TiptapCodeBlock } from '@tiptap/extension-code-block-lowlight' | ||
import type { CodeBlockLowlightOptions as TiptapCodeBlockOptions } from '@tiptap/extension-code-block-lowlight' | ||
|
||
import { ActionButton } from '@/components' | ||
import type { BundledLanguage, BundledTheme } from 'shiki' | ||
import type { CodeBlockOptions as CodeBlockExtOptions } from '@tiptap/extension-code-block' | ||
import CodeBlockExt from '@tiptap/extension-code-block' | ||
import type { GeneralOptions } from '@/types' | ||
import { ShikiPlugin } from '@/extensions/CodeBlock/shiki-plugin' | ||
import CodeBlockActiveButton from '@/extensions/CodeBlock/components/CodeBlockActiveButton' | ||
import { DEFAULT_LANGUAGE_CODE_BLOCK } from '@/constants' | ||
|
||
export interface CodeBlockOptions | ||
extends TiptapCodeBlockOptions, | ||
GeneralOptions<CodeBlockOptions> {} | ||
extends GeneralOptions<CodeBlockExtOptions> { | ||
languages?: BundledLanguage[] | ||
defaultTheme: BundledTheme | ||
} | ||
|
||
export const CodeBlock = TiptapCodeBlock.extend<CodeBlockOptions>({ | ||
export const CodeBlock = CodeBlockExt.extend<CodeBlockOptions>({ | ||
addOptions() { | ||
return { | ||
...this.parent?.(), | ||
defaultLanguage: null, | ||
button: ({ editor, t }: any) => ({ | ||
component: ActionButton, | ||
componentProps: { | ||
action: () => editor.commands.toggleCodeBlock(), | ||
isActive: () => editor.isActive('codeBlock') || false, | ||
disabled: !editor.can().toggleCodeBlock(), | ||
icon: 'Code2', | ||
tooltip: t('editor.codeblock.tooltip'), | ||
}, | ||
}), | ||
languages: [], | ||
button: ({ editor, t, extension }: any) => { | ||
const languages = extension?.options?.languages?.length ? extension?.options?.languages : DEFAULT_LANGUAGE_CODE_BLOCK | ||
|
||
return { | ||
component: CodeBlockActiveButton, | ||
componentProps: { | ||
action: (language = 'js') => editor.commands.setCodeBlock({ | ||
language, | ||
}), | ||
isActive: () => editor.isActive('codeBlock') || false, | ||
disabled: !editor.can().toggleCodeBlock(), | ||
icon: 'Code2', | ||
tooltip: t('editor.codeblock.tooltip'), | ||
languages, | ||
}, | ||
} | ||
}, | ||
} | ||
}, | ||
addProseMirrorPlugins() { | ||
return [ | ||
...(this.parent?.() || []), | ||
ShikiPlugin({ | ||
name: this.name, | ||
defaultLanguage: null, | ||
defaultTheme: this.options.defaultTheme, | ||
}), | ||
] | ||
}, | ||
}) |
66 changes: 66 additions & 0 deletions
66
src/extensions/CodeBlock/components/CodeBlockActiveButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import React, { useMemo } from 'react' | ||
|
||
import type { BundledLanguage } from 'shiki' | ||
import { | ||
ActionButton, | ||
DropdownMenu, | ||
DropdownMenuContent, | ||
DropdownMenuItem, | ||
DropdownMenuTrigger, | ||
} from '@/components' | ||
import { MAP_LANGUAGE_CODE_LABELS } from '@/constants' | ||
|
||
interface Props { | ||
editor: any | ||
disabled?: boolean | ||
color?: string | ||
shortcutKeys?: string[] | ||
maxHeight?: string | number | ||
tooltip?: string | ||
languages?: BundledLanguage[] | ||
action: (language: string) => void | ||
icon?: any | ||
} | ||
|
||
function CodeBlockActiveButton({ action, languages, ...props }: Props) { | ||
const onClick = (language: string) => { | ||
action(language) | ||
} | ||
|
||
const langs = useMemo(() => { | ||
return languages?.map((language) => { | ||
const title = MAP_LANGUAGE_CODE_LABELS[language] || language | ||
|
||
return { | ||
title, | ||
// icon: language.icon, | ||
language, | ||
} | ||
}) | ||
}, [languages]) | ||
|
||
return ( | ||
<DropdownMenu> | ||
<DropdownMenuTrigger disabled={props?.disabled} asChild> | ||
<ActionButton | ||
tooltip={props?.tooltip} | ||
disabled={props?.disabled} | ||
icon={props?.icon} | ||
/> | ||
</DropdownMenuTrigger> | ||
<DropdownMenuContent className="w-full"> | ||
{langs?.map((item: any) => { | ||
return ( | ||
<DropdownMenuItem key={`codeblock-${item.title}`} onClick={() => onClick(item.language)}> | ||
<div className="h-full ml-1"> | ||
{item.title} | ||
</div> | ||
</DropdownMenuItem> | ||
) | ||
})} | ||
</DropdownMenuContent> | ||
</DropdownMenu> | ||
) | ||
} | ||
|
||
export default CodeBlockActiveButton |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import type { | ||
BundledLanguage, | ||
BundledTheme, | ||
Highlighter, | ||
} from 'shiki' | ||
import { | ||
bundledLanguages, | ||
bundledThemes, | ||
createHighlighter, | ||
} from 'shiki' | ||
import { findChildren } from '@tiptap/core' | ||
import type { Node as ProsemirrorNode } from '@tiptap/pm/model' | ||
|
||
let highlighter: Highlighter | undefined | ||
let highlighterPromise: Promise<void> | undefined | ||
const loadingLanguages = new Set<BundledLanguage>() | ||
const loadingThemes = new Set<BundledTheme>() | ||
|
||
interface HighlighterOptions { | ||
themes: (BundledTheme | null | undefined)[] | ||
languages: (BundledLanguage | null | undefined)[] | ||
} | ||
|
||
export function resetHighlighter() { | ||
highlighter = undefined | ||
highlighterPromise = undefined | ||
loadingLanguages.clear() | ||
loadingThemes.clear() | ||
} | ||
|
||
export function getShiki() { | ||
return highlighter | ||
} | ||
|
||
/** | ||
* Load the highlighter. Makes sure the highlighter is only loaded once. | ||
*/ | ||
export function loadHighlighter(opts: HighlighterOptions) { | ||
if (!highlighter && !highlighterPromise) { | ||
const themes = opts.themes.filter( | ||
(theme): theme is BundledTheme => !!theme && theme in bundledThemes, | ||
) | ||
const langs = opts.languages.filter( | ||
(lang): lang is BundledLanguage => !!lang && lang in bundledLanguages, | ||
) | ||
highlighterPromise = createHighlighter({ themes, langs }).then((h) => { | ||
highlighter = h | ||
}) | ||
return highlighterPromise | ||
} | ||
|
||
if (highlighterPromise) { | ||
return highlighterPromise | ||
} | ||
} | ||
|
||
/** | ||
* Loads a theme if it's valid and not yet loaded. | ||
* @returns true or false depending on if it got loaded. | ||
*/ | ||
export async function loadTheme(theme: BundledTheme) { | ||
if ( | ||
highlighter | ||
&& !highlighter.getLoadedThemes().includes(theme) | ||
&& !loadingThemes.has(theme) | ||
&& theme in bundledThemes | ||
) { | ||
loadingThemes.add(theme) | ||
await highlighter.loadTheme(theme) | ||
loadingThemes.delete(theme) | ||
return true | ||
} | ||
|
||
return false | ||
} | ||
|
||
/** | ||
* Loads a language if it's valid and not yet loaded | ||
* @returns true or false depending on if it got loaded. | ||
*/ | ||
export async function loadLanguage(language: BundledLanguage) { | ||
if ( | ||
highlighter | ||
&& !highlighter.getLoadedLanguages().includes(language) | ||
&& !loadingLanguages.has(language) | ||
&& language in bundledLanguages | ||
) { | ||
loadingLanguages.add(language) | ||
await highlighter.loadLanguage(language) | ||
loadingLanguages.delete(language) | ||
return true | ||
} | ||
|
||
return false | ||
} | ||
|
||
/** | ||
* Initializes the highlighter based on the prosemirror document, | ||
* with the themes and languages in the document. | ||
*/ | ||
export async function initHighlighter({ | ||
doc, | ||
name, | ||
defaultTheme, | ||
defaultLanguage, | ||
}: { | ||
doc: ProsemirrorNode | ||
name: string | ||
defaultLanguage: BundledLanguage | null | undefined | ||
defaultTheme: BundledTheme | ||
}) { | ||
const codeBlocks = findChildren(doc, node => node.type.name === name) | ||
|
||
const themes = [ | ||
...codeBlocks.map(block => block.node.attrs.theme as BundledTheme), | ||
defaultTheme, | ||
] | ||
const languages = [ | ||
...codeBlocks.map(block => block.node.attrs.language as BundledLanguage), | ||
defaultLanguage, | ||
] | ||
|
||
if (!highlighter) { | ||
const loader = loadHighlighter({ languages, themes }) | ||
await loader | ||
} | ||
else { | ||
await Promise.all([ | ||
...themes.flatMap(theme => loadTheme(theme)), | ||
...languages.flatMap(language => !!language && loadLanguage(language)), | ||
]) | ||
} | ||
} |
Oops, something went wrong.