From 75fe97272f0aec008712b89a19733b6e459e8b4d Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 3 Sep 2026 16:58:27 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(frontend)=20codeblock=20throws=20e?= =?UTF-8?q?rrors=20when=20unsupported=20language?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementation of code block throws a errors when a `language` prop is not supported by BlockNote, which crashes the whole editor. In our case, the language is supported but flagged as an alias (e.g. `js` instead of `javascript`), but Blocknote does not resolve aliases and throws an error. This file wraps the code block spec to normalize the `language` prop before BlockNote renders it, so a single legacy code block does not crash the whole editor. --- .../app-impress/assets/test_import.md | 2 +- .../__tests__/app-impress/doc-tree.spec.ts | 2 + .../doc-editor/components/BlockNoteEditor.tsx | 6 +- .../components/custom-blocks/CodeBlock.tsx | 80 +++++++++++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CodeBlock.tsx diff --git a/src/frontend/apps/e2e/__tests__/app-impress/assets/test_import.md b/src/frontend/apps/e2e/__tests__/app-impress/assets/test_import.md index 15abf6dd7..461c52de1 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/assets/test_import.md +++ b/src/frontend/apps/e2e/__tests__/app-impress/assets/test_import.md @@ -28,7 +28,7 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor, nisl ege **Code block:** -```javascript +```js const hello_world = () => { console.log("Hello, world!"); } diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts index cc1d182e0..a10fdda94 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts @@ -164,6 +164,8 @@ test.describe('Doc Tree', () => { await clickOnAddRootSubPage(page); await updateDocTitle(page, 'second move'); + await page.waitForTimeout(500); // Wait for the tree to be stable + const firstSubPageItem = docTree.getByText('first move').first(); const secondSubPageItem = docTree.getByText('second move').first(); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 064ece2fe..0aa5eef0f 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -1,7 +1,6 @@ -import { codeBlockOptions, syntaxHighlighter } from '@blocknote/code-block'; +import { syntaxHighlighter } from '@blocknote/code-block'; import { BlockNoteSchema, - createCodeBlockSpec, defaultBlockSpecs, defaultInlineContentSpecs, withPageBreak, @@ -72,6 +71,7 @@ const AIMenu = BlockNoteAI?.AIMenu; const AIMenuController = BlockNoteAI?.AIMenuController; const useAI = BlockNoteAI?.useAI; const localesBNAI = BlockNoteAI?.localesAI || {}; +import { createSafeCodeBlockSpec } from './custom-blocks/CodeBlock'; import { InterlinkingLinkInlineContent } from './custom-inline-content'; import XLMultiColumn from './xl-multi-column'; @@ -83,7 +83,7 @@ const baseBlockNoteSchema = withPageBreak( blockSpecs: { ...defaultBlockSpecs, callout: CalloutBlock(), - codeBlock: createCodeBlockSpec(codeBlockOptions), + codeBlock: createSafeCodeBlockSpec(), diagram: createReactDiagramBlockSpec(), mathBlock: createReactMathBlockSpec(), pdf: PdfBlock(), diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CodeBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CodeBlock.tsx new file mode 100644 index 000000000..85c655f41 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CodeBlock.tsx @@ -0,0 +1,80 @@ +/** + * Implementation of code block throws a errors when a `language` prop + * is not supported by BlockNote, which crashes the whole editor. + * In our case, the language is supported but flagged as an alias (e.g. `js` instead of `javascript`), + * but Blocknote does not resolve aliases and throws an error. + * + * This file wraps the code block spec to normalize the `language` prop before BlockNote renders it, + * so a single legacy code block does not crash the whole editor. + * + * See: + * https://github.com/TypeCellOS/BlockNote/issues/3005 + * https://github.com/TypeCellOS/BlockNote/blob/main/packages/core/src/blocks/Code/CodeBlockOptions.ts + * https://github.com/TypeCellOS/BlockNote/blob/63c2389e34be417bd4c03d7e047d304f5bfd6555/packages/core/src/blocks/Code/helpers/render/createCodeBlock.ts#L16-L18 + * + * @TODO Remove this wrapper once BlockNote fixes the issue and supports aliases in the code block spec. + */ + +import { codeBlockOptions } from '@blocknote/code-block'; +import { createCodeBlockSpec } from '@blocknote/core'; + +const CODE_BLOCK_FALLBACK_LANGUAGE = 'text'; + +/** + * Find the language id supported by BlockNote for a given language or alias. + */ +const codeBlockLanguageById = new Map( + Object.entries(codeBlockOptions.supportedLanguages).flatMap( + ([id, language]) => + [id, ...(language.aliases ?? [])].map( + (key) => [key.toLowerCase(), id] as const, + ), + ), +); + +export const resolveCodeBlockLanguage = (language: unknown): string => { + if (typeof language !== 'string') { + return CODE_BLOCK_FALLBACK_LANGUAGE; + } + + return ( + codeBlockLanguageById.get(language.trim().toLowerCase()) ?? + CODE_BLOCK_FALLBACK_LANGUAGE + ); +}; + +/** + * Builds the code block spec, wrapping its `render` so an unsupported + * `language` prop is normalized before BlockNote renders the language picker. + * This prevents a single legacy code block from crashing the whole editor. + */ +export const createSafeCodeBlockSpec = (): ReturnType< + typeof createCodeBlockSpec +> => { + const spec = createCodeBlockSpec(codeBlockOptions); + const baseRender = spec.implementation.render; + + return { + ...spec, + implementation: { + ...spec.implementation, + render( + this: ThisParameterType, + ...args: Parameters + ): ReturnType { + const [block, editor] = args; + const language = resolveCodeBlockLanguage(block.props.language); + + if (language === block.props.language) { + return baseRender.apply(this, args); + } + + return baseRender.call( + this, + { ...block, props: { ...block.props, language } }, + editor, + ); + }, + }, + }; +};