From cd506e3f7958e36203d8ef04d0c367d9f86ed4f6 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:59:02 +0200 Subject: [PATCH] Split the Shiki highlighter from the plain token helpers (#4467) --- .changeset/shiki-out-of-initial-bundle.md | 5 + .../CodeBlock/ClientCodeBlock.tsx | 2 +- .../CodeBlock/CodeBlockRenderer.tsx | 2 +- .../CodeBlock/MermaidCodeBlock.tsx | 2 +- .../CodeBlock/highlight-tokens.ts | 367 +++++++++++++++++ .../DocumentView/CodeBlock/highlight.ts | 381 +----------------- .../DocumentView/CodeBlock/plain-highlight.ts | 2 +- .../components/DocumentView/Prompt/Prompt.tsx | 2 +- 8 files changed, 391 insertions(+), 372 deletions(-) create mode 100644 .changeset/shiki-out-of-initial-bundle.md create mode 100644 packages/gitbook/src/components/DocumentView/CodeBlock/highlight-tokens.ts diff --git a/.changeset/shiki-out-of-initial-bundle.md b/.changeset/shiki-out-of-initial-bundle.md new file mode 100644 index 000000000..f9cdceff4 --- /dev/null +++ b/.changeset/shiki-out-of-initial-bundle.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Keep Shiki out of the initial page bundle by splitting the highlighter from the plain-text token helpers, so client code no longer pulls the engine and language bundles in through a shared import. diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx index 254ee057b..a52d7de85 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx @@ -13,7 +13,7 @@ import { useDebounceCallback } from 'usehooks-ts'; import type { BlockProps } from '../Block'; import { type InlineExpressionVariables, useEvaluateInlineExpression } from '../InlineExpression'; import { CodeBlockRenderer } from './CodeBlockRenderer'; -import type { HighlightTheme, RenderedInline } from './highlight'; +import type { HighlightTheme, RenderedInline } from './highlight-tokens'; import { plainHighlight } from './plain-highlight'; export type ClientBlockProps = Pick, 'block' | 'style'> & { diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx index 6024e2df8..84b3dc439 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx @@ -8,7 +8,7 @@ import { AnnotationPopover } from '../Annotation/AnnotationPopover'; import type { BlockProps } from '../Block'; import { AskAICodeButton } from './AskAICodeButton'; import { CopyCodeButton } from './CopyCodeButton'; -import type { HighlightLine, HighlightTheme, HighlightToken } from './highlight'; +import type { HighlightLine, HighlightTheme, HighlightToken } from './highlight-tokens'; type CodeBlockRendererProps = Pick, 'block' | 'style'> & { theme: HighlightTheme; diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/MermaidCodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/MermaidCodeBlock.tsx index 9d0210fbc..d64ca56b5 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/MermaidCodeBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/MermaidCodeBlock.tsx @@ -12,7 +12,7 @@ import type { RenderResult } from 'mermaid'; import { FocusScope, usePreventScroll } from 'react-aria'; import { type ClientBlockProps, ClientCodeBlock } from './ClientCodeBlock'; import { MermaidPanZoomControls } from './MermaidPanZoomControls'; -import { getPlainCodeBlock } from './highlight'; +import { getPlainCodeBlock } from './highlight-tokens'; /** Duration of the fullscreen dialog enter/exit animation, must match `animate-blur-in/out`. */ const DIALOG_ANIMATION_MS = 200; diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight-tokens.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight-tokens.ts new file mode 100644 index 000000000..536edd33b --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight-tokens.ts @@ -0,0 +1,367 @@ +import { + CustomizationCodeTheme, + type CustomizationThemedCodeTheme, + type DocumentBlockCode, + type DocumentBlockCodeLine, + type DocumentInlineAnnotation, +} from '@gitbook/api'; +import type { ThemeRegistrationAny, ThemedToken } from 'shiki/core'; + +import { nullIfNever } from '@/lib/typescript'; + +// Split from `./highlight` so client code can use these helpers without dragging Shiki's engine +// and language bundles into the initial chunk. Imports from `shiki/core` here must stay type-only. + +export const DEFAULT_THEMES: CustomizationThemedCodeTheme = { + light: CustomizationCodeTheme.DefaultLight, + dark: CustomizationCodeTheme.DefaultDark, +}; + +export type HighlightTheme = { + bg?: string; + fg?: string; + themes: { + light: ThemeRegistrationAny; + dark: ThemeRegistrationAny; + }; + lines: HighlightLine[]; +}; + +export type LineDiffNotation = 'added' | 'deleted'; + +export type HighlightLine = { + highlighted: boolean; + diff: LineDiffNotation | null; + tokens: HighlightToken[]; +}; + +export type HighlightToken = + | { type: 'plain'; content: string } + | { type: 'shiki'; token: ThemedToken } + | { type: 'annotation'; body: React.ReactNode; children: HighlightToken[] }; + +export type InlineIndexed = { inline: any; start: number; end: number }; + +export type PositionedToken = ThemedToken & { start: number; end: number }; + +export type RenderedInline = { + inline: InlineIndexed; + body: React.ReactNode; +}; + +/** + * Detects an in-source diff notation marker at the end of a line, e.g. + * `// [!code ++]`, `# [!code --]`, ``, `/* [!code --] *​/`. + * Mirror of the gitbook-x parser; keep regex byte-for-byte identical. + */ +const NOTATION_PATTERN = + /[ \t]*(?:(?:\/\/|#|--|;)\s*\[!code\s+(\+\+|--)\]||\/\*\s*\[!code\s+(\+\+|--)\]\s*\*\/)\s*$/; + +export function parseDiffNotation( + line: string +): { diff: LineDiffNotation; markerStart: number } | null { + const match = NOTATION_PATTERN.exec(line); + if (!match) { + return null; + } + const variant = match[1] ?? match[2] ?? match[3]; + return { + diff: variant === '++' ? 'added' : 'deleted', + markerStart: match.index, + }; +} + +/** + * Truncate a sequence of HighlightTokens so only the first `maxLen` characters + * (counted across all tokens, recursing into annotations) remain. Used to strip + * trailing diff-notation markers from rendered output. + */ +export function truncateHighlightTokens( + tokens: HighlightToken[], + maxLen: number +): HighlightToken[] { + const out: HighlightToken[] = []; + let remaining = maxLen; + for (const token of tokens) { + if (remaining <= 0) { + break; + } + const len = highlightTokenLength(token); + if (len <= remaining) { + out.push(token); + remaining -= len; + continue; + } + out.push(sliceHighlightToken(token, remaining)); + remaining = 0; + } + return out; +} + +function highlightTokenLength(token: HighlightToken): number { + switch (token.type) { + case 'plain': + return token.content.length; + case 'shiki': + return token.token.content.length; + case 'annotation': + return token.children.reduce((acc, child) => acc + highlightTokenLength(child), 0); + } +} + +/** + * Concatenate the text content of a sequence of HighlightTokens, recursing + * into annotation children. Mirror of {@link highlightTokenLength}. + */ +export function getHighlightTokensText(tokens: HighlightToken[]): string { + return tokens + .map((token) => { + switch (token.type) { + case 'plain': + return token.content; + case 'shiki': + return token.token.content; + case 'annotation': + return getHighlightTokensText(token.children); + } + }) + .join(''); +} + +function sliceHighlightToken(token: HighlightToken, maxLen: number): HighlightToken { + switch (token.type) { + case 'plain': + return { type: 'plain', content: token.content.slice(0, maxLen) }; + case 'shiki': { + const inner = token.token as ThemedToken & { start?: number; end?: number }; + const newContent = inner.content.slice(0, maxLen); + const newToken: ThemedToken & { start?: number; end?: number } = { + ...inner, + content: newContent, + }; + if (typeof inner.start === 'number') { + newToken.end = inner.start + newContent.length; + } + return { type: 'shiki', token: newToken }; + } + case 'annotation': + return { ...token, children: truncateHighlightTokens(token.children, maxLen) }; + } +} + +export function getInlines(block: DocumentBlockCode) { + const inlines: InlineIndexed[] = []; + getPlainCodeBlock(block, inlines); + + inlines.sort((a, b) => a.start - b.start); + + return inlines; +} + +export function matchTokenAndInlines( + eat: () => PositionedToken | null, + allInlines: RenderedInline[] +): HighlightToken[] { + const initialToken = eat(); + if (!initialToken) { + return []; + } + + const inlines = allInlines.filter( + ({ inline }) => inline.start >= initialToken.start && inline.start < initialToken.end + ); + let token = initialToken; + const result: HighlightToken[] = []; + + const matchAgainstInline = () => { + const inline = inlines.shift(); + if (!inline) { + result.push({ + type: 'shiki', + token, + }); + + return; + } + + const [before, afterBefore] = splitPositionedTokenAt(token, inline.inline.start); + if (before) { + result.push({ + type: 'shiki', + token: before, + }); + } + if (!afterBefore) { + throw new Error('expect afterBefore to not be empty'); + } + + token = afterBefore; + const children: HighlightToken[] = []; + + // If shiki token finished before the end of the annotation or the annotation contains multiple tokens + while (token.end < inline.inline.end) { + children.push({ + type: 'shiki', + token: token, + }); + + const next = eat(); + if (!next) { + throw new Error('expect token to not be empty'); + } + token = next; + } + + const [inside, after] = splitPositionedTokenAt(token, inline.inline.end); + if (!inside) { + throw new Error('expect inside to not be empty'); + } + + children.push({ + type: 'shiki', + token: inside, + }); + + result.push({ + type: 'annotation', + body: inline.body, + children, + }); + + if (after) { + token = after; + matchAgainstInline(); + } + }; + + matchAgainstInline(); + return result; +} + +export function getPlainCodeBlock( + code: DocumentBlockCode, + inlines?: InlineIndexed[], + options?: { + evaluateInlineExpression?: (expr: string) => string; + } +): string { + let content = ''; + + code.nodes.forEach((node, index) => { + const lineContent = getPlainCodeBlockLine(node, content.length, inlines, options); + content += lineContent; + + if (index < code.nodes.length - 1) { + content += '\n'; + } + }); + + return content; +} + +function getPlainCodeBlockLine( + parent: DocumentBlockCodeLine | DocumentInlineAnnotation, + index: number, + inlines?: InlineIndexed[], + options?: { + evaluateInlineExpression?: (expr: string) => string; + } +): string { + let content = ''; + + for (const node of parent.nodes) { + if (node.object === 'text') { + content += cleanupLine(node.leaves.map((leaf) => leaf.text).join('')); + } else { + switch (node.type) { + case 'annotation': { + const start = index + content.length; + content += getPlainCodeBlockLine( + node, + index + content.length, + inlines, + options + ); + const end = index + content.length; + + if (inlines) { + inlines.push({ + inline: node, + start, + end, + }); + } + break; + } + case 'expression': { + const start = index + content.length; + const exprValue = + options?.evaluateInlineExpression?.(node.data.expression) ?? ''; + content += exprValue; + const end = start + exprValue.length; + + if (inlines) { + inlines.push({ + inline: node, + start, + end, + }); + } + break; + } + default: { + nullIfNever(node); + break; + } + } + } + } + + return content; +} + +function slicePositionedToken( + token: PositionedToken, + relativeStart: number, + relativeLength: number +): PositionedToken { + return { + ...token, + start: token.start + relativeStart, + end: token.start + relativeStart + relativeLength, + content: token.content.slice(relativeStart, relativeStart + relativeLength), + }; +} + +function splitPositionedTokenAt( + token: PositionedToken, + absoluteIndex: number +): [PositionedToken | null, PositionedToken | null] { + if (absoluteIndex < token.start || absoluteIndex > token.end) { + throw new Error(`index (${absoluteIndex}) out of bound (${token.start}:${token.end})`); + } + + const before = slicePositionedToken(token, 0, absoluteIndex - token.start); + const after = slicePositionedToken( + token, + absoluteIndex - token.start, + token.end - absoluteIndex + ); + + return [ + isEmptyPositionedToken(before) ? null : before, + isEmptyPositionedToken(after) ? null : after, + ]; +} + +function isEmptyPositionedToken(token: PositionedToken): boolean { + return token.start === token.end; +} + +/** + * Currently it's possible for some lines to contain \r characters, we need to remove them + * as they are considered as new lines by shikijs. + */ +function cleanupLine(line: string): string { + return line.replace(/\r/g, ''); +} diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts index 965704bc7..ab11a48ed 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts @@ -1,160 +1,24 @@ -import { - CustomizationCodeTheme, - type CustomizationThemedCodeTheme, - type DocumentBlockCode, - type DocumentBlockCodeLine, - type DocumentInlineAnnotation, -} from '@gitbook/api'; -import { - type ThemeRegistrationAny, - type ThemedToken, - createSingletonShorthands, - createdBundledHighlighter, -} from 'shiki/core'; +import type { CustomizationThemedCodeTheme, DocumentBlockCode } from '@gitbook/api'; +import { createSingletonShorthands, createdBundledHighlighter } from 'shiki/core'; import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'; import { type BundledLanguage, bundledLanguages } from 'shiki/langs'; import { bundledThemes } from 'shiki/themes'; -import { nullIfNever } from '@/lib/typescript'; import { customThemes } from './customThemes'; +import { + DEFAULT_THEMES, + type HighlightTheme, + type HighlightToken, + type PositionedToken, + type RenderedInline, + getPlainCodeBlock, + matchTokenAndInlines, + parseDiffNotation, + truncateHighlightTokens, +} from './highlight-tokens'; import { plainHighlight } from './plain-highlight'; -export const DEFAULT_THEMES: CustomizationThemedCodeTheme = { - light: CustomizationCodeTheme.DefaultLight, - dark: CustomizationCodeTheme.DefaultDark, -}; - -export type HighlightTheme = { - bg?: string; - fg?: string; - themes: { - light: ThemeRegistrationAny; - dark: ThemeRegistrationAny; - }; - lines: HighlightLine[]; -}; - -export type LineDiffNotation = 'added' | 'deleted'; - -export type HighlightLine = { - highlighted: boolean; - diff: LineDiffNotation | null; - tokens: HighlightToken[]; -}; - -/** - * Detects an in-source diff notation marker at the end of a line, e.g. - * `// [!code ++]`, `# [!code --]`, ``, `/* [!code --] *​/`. - * Mirror of the gitbook-x parser; keep regex byte-for-byte identical. - */ -const NOTATION_PATTERN = - /[ \t]*(?:(?:\/\/|#|--|;)\s*\[!code\s+(\+\+|--)\]||\/\*\s*\[!code\s+(\+\+|--)\]\s*\*\/)\s*$/; - -export function parseDiffNotation( - line: string -): { diff: LineDiffNotation; markerStart: number } | null { - const match = NOTATION_PATTERN.exec(line); - if (!match) { - return null; - } - const variant = match[1] ?? match[2] ?? match[3]; - return { - diff: variant === '++' ? 'added' : 'deleted', - markerStart: match.index, - }; -} - -/** - * Truncate a sequence of HighlightTokens so only the first `maxLen` characters - * (counted across all tokens, recursing into annotations) remain. Used to strip - * trailing diff-notation markers from rendered output. - */ -export function truncateHighlightTokens( - tokens: HighlightToken[], - maxLen: number -): HighlightToken[] { - const out: HighlightToken[] = []; - let remaining = maxLen; - for (const token of tokens) { - if (remaining <= 0) { - break; - } - const len = highlightTokenLength(token); - if (len <= remaining) { - out.push(token); - remaining -= len; - continue; - } - out.push(sliceHighlightToken(token, remaining)); - remaining = 0; - } - return out; -} - -function highlightTokenLength(token: HighlightToken): number { - switch (token.type) { - case 'plain': - return token.content.length; - case 'shiki': - return token.token.content.length; - case 'annotation': - return token.children.reduce((acc, child) => acc + highlightTokenLength(child), 0); - } -} - -/** - * Concatenate the text content of a sequence of HighlightTokens, recursing - * into annotation children. Mirror of {@link highlightTokenLength}. - */ -export function getHighlightTokensText(tokens: HighlightToken[]): string { - return tokens - .map((token) => { - switch (token.type) { - case 'plain': - return token.content; - case 'shiki': - return token.token.content; - case 'annotation': - return getHighlightTokensText(token.children); - } - }) - .join(''); -} - -function sliceHighlightToken(token: HighlightToken, maxLen: number): HighlightToken { - switch (token.type) { - case 'plain': - return { type: 'plain', content: token.content.slice(0, maxLen) }; - case 'shiki': { - const inner = token.token as ThemedToken & { start?: number; end?: number }; - const newContent = inner.content.slice(0, maxLen); - const newToken: ThemedToken & { start?: number; end?: number } = { - ...inner, - content: newContent, - }; - if (typeof inner.start === 'number') { - newToken.end = inner.start + newContent.length; - } - return { type: 'shiki', token: newToken }; - } - case 'annotation': - return { ...token, children: truncateHighlightTokens(token.children, maxLen) }; - } -} - -export type HighlightToken = - | { type: 'plain'; content: string } - | { type: 'shiki'; token: ThemedToken } - | { type: 'annotation'; body: React.ReactNode; children: HighlightToken[] }; - -export type InlineIndexed = { inline: any; start: number; end: number }; - -type PositionedToken = ThemedToken & { start: number; end: number }; - -export type RenderedInline = { - inline: InlineIndexed; - body: React.ReactNode; -}; +export * from './highlight-tokens'; // Merge bundled Shiki themes with our custom themes so both are available to the highlighter const { getSingletonHighlighter } = createSingletonShorthands( @@ -311,220 +175,3 @@ function getLanguageForSyntax(syntax: string): BundledLanguage | null { return null; } - -export function getInlines(block: DocumentBlockCode) { - const inlines: InlineIndexed[] = []; - getPlainCodeBlock(block, inlines); - - inlines.sort((a, b) => a.start - b.start); - - return inlines; -} - -function matchTokenAndInlines( - eat: () => PositionedToken | null, - allInlines: RenderedInline[] -): HighlightToken[] { - const initialToken = eat(); - if (!initialToken) { - return []; - } - - const inlines = allInlines.filter( - ({ inline }) => inline.start >= initialToken.start && inline.start < initialToken.end - ); - let token = initialToken; - const result: HighlightToken[] = []; - - const matchAgainstInline = () => { - const inline = inlines.shift(); - if (!inline) { - result.push({ - type: 'shiki', - token, - }); - - return; - } - - const [before, afterBefore] = splitPositionedTokenAt(token, inline.inline.start); - if (before) { - result.push({ - type: 'shiki', - token: before, - }); - } - if (!afterBefore) { - throw new Error('expect afterBefore to not be empty'); - } - - token = afterBefore; - const children: HighlightToken[] = []; - - // If shiki token finished before the end of the annotation or the annotation contains multiple tokens - while (token.end < inline.inline.end) { - children.push({ - type: 'shiki', - token: token, - }); - - const next = eat(); - if (!next) { - throw new Error('expect token to not be empty'); - } - token = next; - } - - const [inside, after] = splitPositionedTokenAt(token, inline.inline.end); - if (!inside) { - throw new Error('expect inside to not be empty'); - } - - children.push({ - type: 'shiki', - token: inside, - }); - - result.push({ - type: 'annotation', - body: inline.body, - children, - }); - - if (after) { - token = after; - matchAgainstInline(); - } - }; - - matchAgainstInline(); - return result; -} - -export function getPlainCodeBlock( - code: DocumentBlockCode, - inlines?: InlineIndexed[], - options?: { - evaluateInlineExpression?: (expr: string) => string; - } -): string { - let content = ''; - - code.nodes.forEach((node, index) => { - const lineContent = getPlainCodeBlockLine(node, content.length, inlines, options); - content += lineContent; - - if (index < code.nodes.length - 1) { - content += '\n'; - } - }); - - return content; -} - -function getPlainCodeBlockLine( - parent: DocumentBlockCodeLine | DocumentInlineAnnotation, - index: number, - inlines?: InlineIndexed[], - options?: { - evaluateInlineExpression?: (expr: string) => string; - } -): string { - let content = ''; - - for (const node of parent.nodes) { - if (node.object === 'text') { - content += cleanupLine(node.leaves.map((leaf) => leaf.text).join('')); - } else { - switch (node.type) { - case 'annotation': { - const start = index + content.length; - content += getPlainCodeBlockLine( - node, - index + content.length, - inlines, - options - ); - const end = index + content.length; - - if (inlines) { - inlines.push({ - inline: node, - start, - end, - }); - } - break; - } - case 'expression': { - const start = index + content.length; - const exprValue = - options?.evaluateInlineExpression?.(node.data.expression) ?? ''; - content += exprValue; - const end = start + exprValue.length; - - if (inlines) { - inlines.push({ - inline: node, - start, - end, - }); - } - break; - } - default: { - nullIfNever(node); - break; - } - } - } - } - - return content; -} - -function slicePositionedToken( - token: PositionedToken, - relativeStart: number, - relativeLength: number -): PositionedToken { - return { - ...token, - start: token.start + relativeStart, - end: token.start + relativeStart + relativeLength, - content: token.content.slice(relativeStart, relativeStart + relativeLength), - }; -} - -function splitPositionedTokenAt( - token: PositionedToken, - absoluteIndex: number -): [PositionedToken | null, PositionedToken | null] { - if (absoluteIndex < token.start || absoluteIndex > token.end) { - throw new Error(`index (${absoluteIndex}) out of bound (${token.start}:${token.end})`); - } - - const before = slicePositionedToken(token, 0, absoluteIndex - token.start); - const after = slicePositionedToken( - token, - absoluteIndex - token.start, - token.end - absoluteIndex - ); - - return [ - isEmptyPositionedToken(before) ? null : before, - isEmptyPositionedToken(after) ? null : after, - ]; -} - -function isEmptyPositionedToken(token: PositionedToken): boolean { - return token.start === token.end; -} - -/** - * Currently it's possible for some lines to contain \r characters, we need to remove them - * as they are considered as new lines by shikijs. - */ -function cleanupLine(line: string): string { - return line.replace(/\r/g, ''); -} diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/plain-highlight.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/plain-highlight.ts index 00212c8e4..5eb0b9e92 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/plain-highlight.ts +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/plain-highlight.ts @@ -10,7 +10,7 @@ import { getHighlightTokensText, parseDiffNotation, truncateHighlightTokens, -} from './highlight'; +} from './highlight-tokens'; /** * Parse a code block without highlighting it. diff --git a/packages/gitbook/src/components/DocumentView/Prompt/Prompt.tsx b/packages/gitbook/src/components/DocumentView/Prompt/Prompt.tsx index facdfadb2..568539d47 100644 --- a/packages/gitbook/src/components/DocumentView/Prompt/Prompt.tsx +++ b/packages/gitbook/src/components/DocumentView/Prompt/Prompt.tsx @@ -7,7 +7,7 @@ import { import { validateIconName } from '@gitbook/icons/icons'; import type { BlockProps } from '../Block'; import { CodeBlock } from '../CodeBlock'; -import { getPlainCodeBlock } from '../CodeBlock/highlight'; +import { getPlainCodeBlock } from '../CodeBlock/highlight-tokens'; import { PromptClient } from './PromptClient'; const PROMPT_COLLAPSED_LINE_COUNT = 10;