Example: code-block-themes
Install this example with
shadcn:npx shadcn@latest add @prosekit/react-example-code-block-themesnpx shadcn@latest add @prosekit/preact-example-code-block-themesnpx shadcn@latest add @prosekit/solid-example-code-block-themesnpx shadcn@latest add @prosekit/svelte-example-code-block-themesnpx shadcn@latest add @prosekit/vue-example-code-block-themesimport 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { useMemo } from 'preact/hooks'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/preact'
import { defaultContent } from '../../sample/sample-doc-code-block'
import { defineExtension } from './extension'
import Toolbar from './toolbar'
export default function Editor() {
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension, defaultContent })
}, [])
return (
<ProseKit editor={editor}>
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<Toolbar />
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineCodeBlockView } from '../../ui/code-block-view'
export function defineExtension() {
return union(defineBasicExtension(), defineCodeBlockView())
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor'import type { JSX } from 'preact'
import {
useMemo,
useState,
} from 'preact/hooks'
import {
defineCodeBlockShiki,
shikiBundledThemesInfo,
type ShikiBundledTheme,
} from 'prosekit/extensions/code-block'
import { useExtension } from 'prosekit/preact'
export function ThemeSelector() {
const [theme, setTheme] = useState('github-dark')
const extension = useMemo(() => {
return defineCodeBlockShiki({ themes: [theme as ShikiBundledTheme] })
}, [theme])
useExtension(extension)
const handleChange = (
event: JSX.TargetedEvent<HTMLSelectElement, Event>,
) => {
setTheme(event.currentTarget.value)
}
return (
<>
<label htmlFor="code-block-theme-selector">Theme</label>
<select
id="code-block-theme-selector"
value={theme}
onChange={handleChange}
className="outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700"
>
{shikiBundledThemesInfo.map((info) => (
<option key={info.id} value={info.id}>
{info.id}
</option>
))}
</select>
</>
)
}import { ThemeSelector } from './theme-selector'
export default function Toolbar() {
return (
<div className="z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center">
<ThemeSelector />
</div>
)
}import type { NodeJSON } from 'prosekit/core'
const js = `
async function start() {
while (true) {
await sleep()
await eat()
await code('JavaScript!')
}
}
`.trim()
const py = `
async def start():
while True:
await sleep()
await eat()
await code('Python!')
`.trim()
const go = `
func start() {
for {
sleep()
eat()
code('Go!')
}
}
`.trim()
export const defaultContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'codeBlock',
attrs: { language: 'javascript' },
content: [{ type: 'text', text: js }],
},
{
type: 'codeBlock',
attrs: { language: 'python' },
content: [{ type: 'text', text: py }],
},
{
type: 'codeBlock',
attrs: { language: 'go' },
content: [{ type: 'text', text: go }],
},
],
}import type { JSX } from 'preact'
import type { CodeBlockAttrs } from 'prosekit/extensions/code-block'
import { shikiBundledLanguagesInfo } from 'prosekit/extensions/code-block'
import type { PreactNodeViewProps } from 'prosekit/preact'
export default function CodeBlockView(props: PreactNodeViewProps) {
const attrs = props.node.attrs as CodeBlockAttrs
const language = attrs.language || ''
const setLanguage = (language: string) => {
const attrs: CodeBlockAttrs = { language }
props.setAttrs(attrs)
}
const handleChange = (
event: JSX.TargetedEvent<HTMLSelectElement, Event>,
) => {
setLanguage(event.currentTarget.value)
}
return (
<>
<div className="relative mx-2 top-3 h-0 select-none overflow-visible text-xs" contentEditable={false}>
<select
aria-label="Code block language"
className="outline-unset focus:outline-unset relative box-border w-auto cursor-pointer select-none appearance-none rounded-sm border-none bg-transparent px-2 py-1 text-xs transition text-(--prosemirror-highlight) opacity-0 hover:opacity-80 [div[data-node-view-root]:hover_&]:opacity-50 hover:[div[data-node-view-root]:hover_&]:opacity-80"
onChange={handleChange}
value={language || ''}
>
<option value="">Plain Text</option>
{shikiBundledLanguagesInfo.map((info) => (
<option key={info.id} value={info.id}>
{info.name}
</option>
))}
</select>
</div>
<pre ref={props.contentRef} data-language={language}></pre>
</>
)
}import type { Extension } from 'prosekit/core'
import {
definePreactNodeView,
type PreactNodeViewComponent,
} from 'prosekit/preact'
import CodeBlockView from './code-block-view'
export function defineCodeBlockView(): Extension {
return definePreactNodeView({
name: 'codeBlock',
contentAs: 'code',
component: CodeBlockView satisfies PreactNodeViewComponent,
})
}import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo } from 'react'
import { defaultContent } from '../../sample/sample-doc-code-block'
import { defineExtension } from './extension'
import Toolbar from './toolbar'
export default function Editor() {
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension, defaultContent })
}, [])
return (
<ProseKit editor={editor}>
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<Toolbar />
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineCodeBlockView } from '../../ui/code-block-view'
export function defineExtension() {
return union(defineBasicExtension(), defineCodeBlockView())
}
export type EditorExtension = ReturnType<typeof defineExtension>'use client'
export { default as ExampleEditor } from './editor'import {
defineCodeBlockShiki,
shikiBundledThemesInfo,
type ShikiBundledTheme,
} from 'prosekit/extensions/code-block'
import { useExtension } from 'prosekit/react'
import {
useMemo,
useState,
} from 'react'
export function ThemeSelector() {
const [theme, setTheme] = useState('github-dark')
const extension = useMemo(() => {
return defineCodeBlockShiki({ themes: [theme as ShikiBundledTheme] })
}, [theme])
useExtension(extension)
return (
<>
<label htmlFor="code-block-theme-selector">Theme</label>
<select
id="code-block-theme-selector"
value={theme}
onChange={(event) => setTheme(event.target.value)}
className="outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700"
>
{shikiBundledThemesInfo.map((info) => (
<option key={info.id} value={info.id}>
{info.id}
</option>
))}
</select>
</>
)
}import { ThemeSelector } from './theme-selector'
export default function Toolbar() {
return (
<div className="z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center">
<ThemeSelector />
</div>
)
}import type { NodeJSON } from 'prosekit/core'
const js = `
async function start() {
while (true) {
await sleep()
await eat()
await code('JavaScript!')
}
}
`.trim()
const py = `
async def start():
while True:
await sleep()
await eat()
await code('Python!')
`.trim()
const go = `
func start() {
for {
sleep()
eat()
code('Go!')
}
}
`.trim()
export const defaultContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'codeBlock',
attrs: { language: 'javascript' },
content: [{ type: 'text', text: js }],
},
{
type: 'codeBlock',
attrs: { language: 'python' },
content: [{ type: 'text', text: py }],
},
{
type: 'codeBlock',
attrs: { language: 'go' },
content: [{ type: 'text', text: go }],
},
],
}import type { CodeBlockAttrs } from 'prosekit/extensions/code-block'
import { shikiBundledLanguagesInfo } from 'prosekit/extensions/code-block'
import type { ReactNodeViewProps } from 'prosekit/react'
export default function CodeBlockView(props: ReactNodeViewProps) {
const attrs = props.node.attrs as CodeBlockAttrs
const language = attrs.language
const setLanguage = (language: string) => {
const attrs: CodeBlockAttrs = { language }
props.setAttrs(attrs)
}
return (
<>
<div className="relative mx-2 top-3 h-0 select-none overflow-visible text-xs" contentEditable={false}>
<select
aria-label="Code block language"
className="outline-unset focus:outline-unset relative box-border w-auto cursor-pointer select-none appearance-none rounded-sm border-none bg-transparent px-2 py-1 text-xs transition text-(--prosemirror-highlight) opacity-0 hover:opacity-80 [div[data-node-view-root]:hover_&]:opacity-50 hover:[div[data-node-view-root]:hover_&]:opacity-80"
onChange={(event) => setLanguage(event.target.value)}
value={language || ''}
>
<option value="">Plain Text</option>
{shikiBundledLanguagesInfo.map((info) => (
<option key={info.id} value={info.id}>
{info.name}
</option>
))}
</select>
</div>
<pre ref={props.contentRef} data-language={language}></pre>
</>
)
}import type { Extension } from 'prosekit/core'
import {
defineReactNodeView,
type ReactNodeViewComponent,
} from 'prosekit/react'
import CodeBlockView from './code-block-view'
export function defineCodeBlockView(): Extension {
return defineReactNodeView({
name: 'codeBlock',
contentAs: 'code',
component: CodeBlockView satisfies ReactNodeViewComponent,
})
}import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/solid'
import type { JSX } from 'solid-js'
import { defaultContent } from '../../sample/sample-doc-code-block'
import { defineExtension } from './extension'
import Toolbar from './toolbar'
export default function Editor(): JSX.Element {
const extension = defineExtension()
const editor = createEditor({ extension, defaultContent })
return (
<ProseKit editor={editor}>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<Toolbar />
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineCodeBlockView } from '../../ui/code-block-view'
export function defineExtension() {
return union(defineBasicExtension(), defineCodeBlockView())
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor'import {
defineCodeBlockShiki,
shikiBundledThemesInfo,
type ShikiBundledTheme,
} from 'prosekit/extensions/code-block'
import { useExtension } from 'prosekit/solid'
import {
createMemo,
createSignal,
For,
type JSX,
} from 'solid-js'
export function ThemeSelector(): JSX.Element {
const [theme, setTheme] = createSignal('github-dark')
const extension = createMemo(() => {
return defineCodeBlockShiki({ themes: [theme() as ShikiBundledTheme] })
})
useExtension(extension)
return (
<>
<label for="code-block-theme-selector">Theme</label>
<select
id="code-block-theme-selector"
value={theme()}
onChange={(event) => setTheme(event.target.value)}
class="outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700"
>
<For each={shikiBundledThemesInfo}>
{(info) => (
<option value={info.id}>
{info.id}
</option>
)}
</For>
</select>
</>
)
}import type { JSX } from 'solid-js'
import { ThemeSelector } from './theme-selector'
export default function Toolbar(): JSX.Element {
return (
<div class="z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center">
<ThemeSelector />
</div>
)
}import type { NodeJSON } from 'prosekit/core'
const js = `
async function start() {
while (true) {
await sleep()
await eat()
await code('JavaScript!')
}
}
`.trim()
const py = `
async def start():
while True:
await sleep()
await eat()
await code('Python!')
`.trim()
const go = `
func start() {
for {
sleep()
eat()
code('Go!')
}
}
`.trim()
export const defaultContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'codeBlock',
attrs: { language: 'javascript' },
content: [{ type: 'text', text: js }],
},
{
type: 'codeBlock',
attrs: { language: 'python' },
content: [{ type: 'text', text: py }],
},
{
type: 'codeBlock',
attrs: { language: 'go' },
content: [{ type: 'text', text: go }],
},
],
}import type { CodeBlockAttrs } from 'prosekit/extensions/code-block'
import { shikiBundledLanguagesInfo } from 'prosekit/extensions/code-block'
import type { SolidNodeViewProps } from 'prosekit/solid'
import {
For,
type JSX,
} from 'solid-js'
export default function CodeBlockView(props: SolidNodeViewProps): JSX.Element {
const attrs = () => props.node.attrs as CodeBlockAttrs
const language = () => attrs().language
const setLanguage = (lang: string) => {
const newAttrs: CodeBlockAttrs = { language: lang }
props.setAttrs(newAttrs)
}
return (
<>
<div class="relative mx-2 top-3 h-0 select-none overflow-visible text-xs" contentEditable={false}>
<select
aria-label="Code block language"
class="outline-unset focus:outline-unset relative box-border w-auto cursor-pointer select-none appearance-none rounded-sm border-none bg-transparent px-2 py-1 text-xs transition text-(--prosemirror-highlight) opacity-0 hover:opacity-80 [div[data-node-view-root]:hover_&]:opacity-50 hover:[div[data-node-view-root]:hover_&]:opacity-80"
onChange={(event) => setLanguage(event.target.value)}
value={language() || ''}
>
<option value="">Plain Text</option>
<For each={shikiBundledLanguagesInfo}>
{(info) => (
<option value={info.id}>
{info.name}
</option>
)}
</For>
</select>
</div>
<pre ref={props.contentRef} data-language={language()}></pre>
</>
)
}import type { Extension } from 'prosekit/core'
import {
defineSolidNodeView,
type SolidNodeViewComponent,
} from 'prosekit/solid'
import CodeBlockView from './code-block-view'
export function defineCodeBlockView(): Extension {
return defineSolidNodeView({
name: 'codeBlock',
contentAs: 'code',
component: CodeBlockView satisfies SolidNodeViewComponent,
})
}- examples/code-block-themes/editor.svelte
- examples/code-block-themes/extension.ts
- examples/code-block-themes/index.ts
- examples/code-block-themes/theme-selector.svelte
- examples/code-block-themes/toolbar.svelte
- sample/sample-doc-code-block.ts
- ui/code-block-view/code-block-view.svelte
- ui/code-block-view/index.ts
<script lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/svelte'
import { defaultContent } from '../../sample/sample-doc-code-block'
import { defineExtension } from './extension'
import Toolbar from './toolbar.svelte'
const extension = defineExtension()
const editor = createEditor({ extension, defaultContent })
const mount = (element: HTMLElement) => {
editor.mount(element)
return { destroy: () => editor.unmount() }
}
</script>
<ProseKit {editor}>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<Toolbar />
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div use:mount class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</div>
</ProseKit>import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineCodeBlockView } from '../../ui/code-block-view'
export function defineExtension() {
return union(defineBasicExtension(), defineCodeBlockView())
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor.svelte'<script lang="ts">
import {
defineCodeBlockShiki,
shikiBundledThemesInfo,
type ShikiBundledTheme,
} from 'prosekit/extensions/code-block'
import { useExtension } from 'prosekit/svelte'
import {
derived,
writable,
} from 'svelte/store'
const theme = writable<ShikiBundledTheme>('github-dark')
const extension = derived(theme, ($theme) => {
return defineCodeBlockShiki({ themes: [$theme] })
})
useExtension(extension)
function handleChange(event: Event) {
const target = event.target as HTMLSelectElement
theme.set(target.value as ShikiBundledTheme)
}
</script>
<label for="code-block-theme-selector">Theme</label>
<select
id="code-block-theme-selector"
value={$theme}
onchange={handleChange}
class="outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700"
>
{#each shikiBundledThemesInfo as info (info.id)}
<option value={info.id}>
{info.id}
</option>
{/each}
</select><script lang="ts">
import ThemeSelector from './theme-selector.svelte'
</script>
<div class="z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center">
<ThemeSelector />
</div>import type { NodeJSON } from 'prosekit/core'
const js = `
async function start() {
while (true) {
await sleep()
await eat()
await code('JavaScript!')
}
}
`.trim()
const py = `
async def start():
while True:
await sleep()
await eat()
await code('Python!')
`.trim()
const go = `
func start() {
for {
sleep()
eat()
code('Go!')
}
}
`.trim()
export const defaultContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'codeBlock',
attrs: { language: 'javascript' },
content: [{ type: 'text', text: js }],
},
{
type: 'codeBlock',
attrs: { language: 'python' },
content: [{ type: 'text', text: py }],
},
{
type: 'codeBlock',
attrs: { language: 'go' },
content: [{ type: 'text', text: go }],
},
],
}<script lang="ts">
import type { CodeBlockAttrs } from 'prosekit/extensions/code-block'
import { shikiBundledLanguagesInfo } from 'prosekit/extensions/code-block'
import type { SvelteNodeViewProps } from 'prosekit/svelte'
interface Props extends SvelteNodeViewProps {}
const props: Props = $props()
const node = props.node
const attrs = $derived($node.attrs as CodeBlockAttrs)
function setLanguage(lang: string) {
const newAttrs: CodeBlockAttrs = { language: lang }
props.setAttrs(newAttrs)
}
function bindContentRef(element: HTMLPreElement) {
props.contentRef(element)
}
</script>
<div class="relative mx-2 top-3 h-0 select-none overflow-visible text-xs" contentEditable="false">
<select
aria-label="Code block language"
class="outline-unset focus:outline-unset relative box-border w-auto cursor-pointer select-none appearance-none rounded-sm border-none bg-transparent px-2 py-1 text-xs transition text-(--prosemirror-highlight) opacity-0 hover:opacity-80 [div[data-node-view-root]:hover_&]:opacity-50 hover:[div[data-node-view-root]:hover_&]:opacity-80"
value={attrs.language || ''}
onchange={(event) => setLanguage((event.target as HTMLSelectElement).value)}
>
<option value="">Plain Text</option>
{#each shikiBundledLanguagesInfo as info (info.id)}
<option value={info.id}>
{info.name}
</option>
{/each}
</select>
</div>
<pre use:bindContentRef data-language={attrs.language}></pre>import type { Extension } from 'prosekit/core'
import {
defineSvelteNodeView,
type SvelteNodeViewComponent,
} from 'prosekit/svelte'
import CodeBlockView from './code-block-view.svelte'
export function defineCodeBlockView(): Extension {
return defineSvelteNodeView({
name: 'codeBlock',
contentAs: 'code',
component: CodeBlockView as SvelteNodeViewComponent,
})
}<script setup lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/vue'
import {
ref,
watchPostEffect,
} from 'vue'
import { defaultContent } from '../../sample/sample-doc-code-block'
import { defineExtension } from './extension'
import Toolbar from './toolbar.vue'
const extension = defineExtension()
const editor = createEditor({
extension,
defaultContent,
})
const editorRef = ref<HTMLDivElement | null>(null)
watchPostEffect((onCleanup) => {
editor.mount(editorRef.value)
onCleanup(() => editor.unmount())
})
</script>
<template>
<ProseKit :editor="editor">
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<Toolbar />
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div ref="editorRef" class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500" />
</div>
</div>
</ProseKit>
</template>import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineCodeBlockView } from '../../ui/code-block-view'
export function defineExtension() {
return union(defineBasicExtension(), defineCodeBlockView())
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor.vue'<script setup lang="ts">
import {
defineCodeBlockShiki,
shikiBundledThemesInfo,
type ShikiBundledTheme,
} from 'prosekit/extensions/code-block'
import { useExtension } from 'prosekit/vue'
import {
computed,
ref,
} from 'vue'
const theme = ref<ShikiBundledTheme>('github-dark')
const extension = computed(() => {
return defineCodeBlockShiki({ themes: [theme.value] })
})
useExtension(extension)
</script>
<template>
<label for="code-block-theme-selector">Theme</label>
<select
id="code-block-theme-selector"
v-model="theme"
class="outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700"
>
<option
v-for="info in shikiBundledThemesInfo"
:key="info.id"
:value="info.id"
>
{{ info.id }}
</option>
</select>
</template><script setup lang="ts">
import ThemeSelector from './theme-selector.vue'
</script>
<template>
<div class="z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center">
<ThemeSelector />
</div>
</template>import type { NodeJSON } from 'prosekit/core'
const js = `
async function start() {
while (true) {
await sleep()
await eat()
await code('JavaScript!')
}
}
`.trim()
const py = `
async def start():
while True:
await sleep()
await eat()
await code('Python!')
`.trim()
const go = `
func start() {
for {
sleep()
eat()
code('Go!')
}
}
`.trim()
export const defaultContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'codeBlock',
attrs: { language: 'javascript' },
content: [{ type: 'text', text: js }],
},
{
type: 'codeBlock',
attrs: { language: 'python' },
content: [{ type: 'text', text: py }],
},
{
type: 'codeBlock',
attrs: { language: 'go' },
content: [{ type: 'text', text: go }],
},
],
}<script setup lang="ts">
import type { CodeBlockAttrs } from 'prosekit/extensions/code-block'
import { shikiBundledLanguagesInfo } from 'prosekit/extensions/code-block'
import type { VueNodeViewProps } from 'prosekit/vue'
const props = defineProps<VueNodeViewProps>()
const attrs = () => props.node.value.attrs as CodeBlockAttrs
const language = () => attrs().language
function setLanguage(lang: string) {
const newAttrs: CodeBlockAttrs = { language: lang }
props.setAttrs(newAttrs)
}
</script>
<template>
<div class="relative mx-2 top-3 h-0 select-none overflow-visible text-xs" contentEditable="false">
<select
aria-label="Code block language"
class="outline-unset focus:outline-unset relative box-border w-auto cursor-pointer select-none appearance-none rounded-sm border-none bg-transparent px-2 py-1 text-xs transition text-(--prosemirror-highlight) opacity-0 hover:opacity-80 [div[data-node-view-root]:hover_&]:opacity-50 hover:[div[data-node-view-root]:hover_&]:opacity-80"
:value="language() || ''"
@change="(event) => setLanguage((event.target as HTMLSelectElement).value)"
>
<option value="">Plain Text</option>
<option
v-for="info in shikiBundledLanguagesInfo"
:key="info.id"
:value="info.id"
>
{{ info.name }}
</option>
</select>
</div>
<pre :ref="contentRef" :data-language="language()"></pre>
</template>import type { Extension } from 'prosekit/core'
import {
defineVueNodeView,
type VueNodeViewComponent,
} from 'prosekit/vue'
import CodeBlockView from './code-block-view.vue'
export function defineCodeBlockView(): Extension {
return defineVueNodeView({
name: 'codeBlock',
contentAs: 'code',
component: CodeBlockView as VueNodeViewComponent,
})
}