feat: improve editor code blocks

This commit is contained in:
lucaronin
2026-05-12 18:21:26 +02:00
parent c0b0a02ce6
commit 5215754ffa
8 changed files with 332 additions and 8 deletions

View File

@@ -488,6 +488,10 @@
background-color: var(--editor-code-block-background) !important;
border: 1px solid var(--editor-code-block-border);
color: var(--editor-code-block-text) !important;
column-gap: 12px;
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
overflow-x: hidden;
position: relative;
}
@@ -510,8 +514,34 @@
color: var(--editor-code-block-text);
}
.editor__blocknote-container .bn-block-content[data-content-type="codeBlock"]::before {
border-right: 1px solid color-mix(in srgb, var(--editor-code-block-language) 28%, transparent);
color: var(--editor-code-block-language);
content: attr(data-line-numbers);
font: inherit;
grid-column: 1;
grid-row: 2;
line-height: inherit;
min-width: 2ch;
padding-right: 10px;
pointer-events: none;
text-align: right;
user-select: none;
white-space: pre;
}
.editor__blocknote-container .bn-block-content[data-content-type="codeBlock"] > pre {
color: inherit;
grid-column: 2;
grid-row: 2;
min-width: 0;
overflow-x: hidden;
white-space: pre-wrap;
}
.editor__blocknote-container .bn-block-content[data-content-type="codeBlock"] > div {
grid-column: 1 / -1;
grid-row: 1;
}
.editor__blocknote-container .bn-block-content[data-content-type="codeBlock"] > div > select {
@@ -525,13 +555,21 @@
.editor__blocknote-container .bn-block-content[data-content-type="codeBlock"] pre code {
background-color: transparent;
color: inherit;
padding: 0;
border-radius: 0;
color: inherit;
display: block;
grid-column: 2;
min-width: 0;
overflow-wrap: anywhere;
padding: 0;
white-space: pre-wrap;
}
.editor__blocknote-container .bn-block-content[data-content-type="codeBlock"] pre code .shiki {
background-color: transparent !important;
color: inherit !important;
overflow-wrap: anywhere;
white-space: pre-wrap !important;
}
/* --- Blockquote --- */

View File

@@ -1,6 +1,7 @@
import { codeBlockOptions } from '@blocknote/code-block'
import type { CodeBlockOptions } from '@blocknote/core'
import { createCodeBlockSpec, type CodeBlockOptions } from '@blocknote/core'
import { supportsModernRegexFeatures } from '../utils/regexCapabilities'
import { lineNumbersForText } from '../utils/codeBlockEnhancements'
const LIGHT_CODE_THEME = 'github-light'
const DARK_CODE_THEME = 'github-dark'
@@ -40,3 +41,52 @@ export function createTolariaCodeBlockOptions(): Partial<CodeBlockOptions> {
delete options.createHighlighter
return options
}
interface InlineTextItem {
text?: unknown
}
interface CodeBlockRenderContext {
blockContentDOMAttributes?: Record<string, string>
props?: unknown
renderType?: unknown
}
function textFromInlineContent(content: unknown): string {
if (!Array.isArray(content)) return ''
return content
.map((item: InlineTextItem) => typeof item.text === 'string' ? item.text : '')
.join('')
}
export function createTolariaCodeBlockSpec() {
const spec = createCodeBlockSpec(createTolariaCodeBlockOptions())
if (!spec.implementation?.render) return spec
const baseRender = spec.implementation.render
const render: typeof baseRender = function (this: ThisParameterType<typeof baseRender>, block, editor) {
const context = this as CodeBlockRenderContext
const blockContentDOMAttributes = {
...context.blockContentDOMAttributes,
'data-line-numbers': lineNumbersForText(textFromInlineContent(block.content)),
}
return baseRender.call(
{
...context,
blockContentDOMAttributes,
} as ThisParameterType<typeof baseRender>,
block,
editor,
)
}
return {
...spec,
implementation: {
...spec.implementation,
render,
},
}
}

View File

@@ -1,6 +1,5 @@
/* eslint-disable react-refresh/only-export-components -- module-level schema, not a component file */
import {
createCodeBlockSpec,
BlockNoteSchema,
defaultInlineContentSpecs,
} from '@blocknote/core'
@@ -12,7 +11,7 @@ import { MATH_BLOCK_TYPE, MATH_INLINE_TYPE, renderMathToHtml } from '../utils/ma
import { MERMAID_BLOCK_TYPE, mermaidFenceSource } from '../utils/mermaidMarkdown'
import { TLDRAW_BLOCK_TYPE, TLDRAW_DEFAULT_HEIGHT } from '../utils/tldrawMarkdown'
import type { VaultEntry } from '../types'
import { createTolariaCodeBlockOptions } from './codeBlockOptions'
import { createTolariaCodeBlockSpec } from './codeBlockOptions'
import { NoteTitleIcon } from './NoteTitleIcon'
import { MermaidDiagram } from './MermaidDiagram'
import { SafeHtmlSpan } from './SafeMarkup'
@@ -213,7 +212,7 @@ const TldrawBlock = createReactBlockSpec(
},
)
const codeBlock = createCodeBlockSpec(createTolariaCodeBlockOptions())
const codeBlock = createTolariaCodeBlockSpec()
const mathBlock = MathBlock()
const mermaidBlock = MermaidBlock()
const tldrawBlock = TldrawBlock()

View File

@@ -41,6 +41,7 @@ function installLookbehindMissingRegExp() {
}
afterEach(() => {
document.body.innerHTML = ''
document.documentElement.classList.remove('dark')
delete document.documentElement.dataset.theme
restoreRegExpConstructor()
@@ -48,6 +49,42 @@ afterEach(() => {
})
describe('editor schema code block highlighting', () => {
it('renders one line number per code block source line', async () => {
vi.resetModules()
const { createTolariaCodeBlockSpec } = await import('./codeBlockOptions')
const codeBlockSpec = createTolariaCodeBlockSpec()
type Render = typeof codeBlockSpec.implementation.render
type CodeBlock = Parameters<Render>[0]
type CodeBlockEditor = Parameters<Render>[1]
type RenderContext = ThisParameterType<Render>
const block = {
id: 'code-block-1',
type: 'codeBlock',
props: { language: 'text' },
content: [{ type: 'text', text: 'const a = 1\nconsole.log(a)' }],
children: [],
} as CodeBlock
const editor = {
isEditable: true,
getBlock: () => block,
updateBlock: () => {},
} as CodeBlockEditor
const rendered = codeBlockSpec.implementation.render.call(
{ blockContentDOMAttributes: {}, props: undefined, renderType: 'dom' } as RenderContext,
block,
editor,
)
const host = document.createElement('div')
host.append(rendered.dom)
document.body.append(host)
expect(host.querySelector('[data-content-type="codeBlock"]')?.getAttribute('data-line-numbers')).toBe('1\n2')
rendered.destroy?.()
})
it('uses the light Shiki theme first in light mode', async () => {
vi.resetModules()
document.documentElement.classList.remove('dark')