@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
90 lines (83 loc) • 3.53 kB
text/typescript
import type { HighlighterGeneric } from "@shikijs/types";
import {
createExtension,
ExtensionOptions,
} from "../../editor/BlockNoteExtension.js";
import { lazyShikiPlugin } from "./shiki.js";
import {
CustomInlineContentConfig,
InlineContentSpec,
LooseBlockSpec,
} from "../../schema/index.js";
export type SyntaxHighlightingOptions = {
/**
* Creates the Shiki highlighter used for syntax highlighting. Can be
* asynchronous, so the highlighter is only loaded once it's first needed.
*
* When omitted, content renders without syntax highlighting.
*/
createHighlighter: () => Promise<HighlighterGeneric<any, any>>;
};
/**
* Collects the node type names that should be syntax-highlighted from a schema's
* block and inline-content specs.
*
* A spec is a candidate when it has a `meta.highlight` callback (which decides
* the language) AND the node actually holds editable text. Block and
* inline-content specs use different `content` value spaces, so "editable text"
* means `content === "plain"` for both blocks (code/math blocks) and inline
* content (inline math) - both hold plain text - hence the two are filtered
* separately.
*
* Inline content (e.g. inline math) is highlighted too: `prosemirror-highlight`
* collects nodes by `node.inlineContent` since v0.15.3
* (https://github.com/ocavue/prosemirror-highlight/pull/137), so inline nodes
* holding inline content are visited alongside text blocks.
*/
export function collectHighlightNodeTypes(schema: {
blockSpecs: Record<string, unknown>;
inlineContentSpecs: Record<string, unknown>;
}): string[] {
const blockNodeTypes = Object.values(schema.blockSpecs)
.filter(
(blockSpec): blockSpec is LooseBlockSpec =>
typeof (blockSpec as LooseBlockSpec)?.config === "object" &&
(blockSpec as LooseBlockSpec).config.content === "plain" &&
!!(blockSpec as LooseBlockSpec).implementation?.meta?.highlight,
)
.map((blockSpec) => blockSpec.config.type);
const inlineContentNodeTypes = Object.values(schema.inlineContentSpecs)
.filter(
(
inlineContentSpec,
): inlineContentSpec is InlineContentSpec<CustomInlineContentConfig> =>
typeof (
inlineContentSpec as InlineContentSpec<CustomInlineContentConfig>
)?.config === "object" &&
(inlineContentSpec as InlineContentSpec<CustomInlineContentConfig>)
.config.content === "plain" &&
!!(inlineContentSpec as InlineContentSpec<CustomInlineContentConfig>)
.implementation?.meta?.highlight,
)
.map((inlineContentSpec) => inlineContentSpec.config.type);
return [...blockNodeTypes, ...inlineContentNodeTypes];
}
/**
* A single editor-wide extension that syntax-highlights block and inline-content
* content. Which nodes get highlighted (and as which language) is decided by
* each spec's `meta.highlight` callback, so individual specs declare their own
* language rather than the extension configuring them.
*
* Highlighting is opt-in: the user adds this extension to the editor's
* `extensions` (configured with a `createHighlighter`) to enable it. When it's
* absent, content renders as plain text.
*/
export const SyntaxHighlightingExtension = createExtension(
({ editor, options }: ExtensionOptions<SyntaxHighlightingOptions>) => {
const nodeTypes = collectHighlightNodeTypes(editor.schema);
return {
key: "syntaxHighlighting",
prosemirrorPlugins: [lazyShikiPlugin(options, nodeTypes, editor.schema)],
};
},
);