diff --git a/.changeset/thin-files-flow.md b/.changeset/thin-files-flow.md deleted file mode 100644 index aa81cb526..000000000 --- a/.changeset/thin-files-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'gitbook': patch ---- - -Improve performances by highlighting code client-side if the code block is offscreen diff --git a/packages/gitbook/src/components/DocumentView/Annotation/Annotation.tsx b/packages/gitbook/src/components/DocumentView/Annotation/Annotation.tsx index 937f19cc6..883a0eb13 100644 --- a/packages/gitbook/src/components/DocumentView/Annotation/Annotation.tsx +++ b/packages/gitbook/src/components/DocumentView/Annotation/Annotation.tsx @@ -7,7 +7,7 @@ import { Blocks } from '../Blocks'; import { InlineProps } from '../Inline'; import { Inlines } from '../Inlines'; -export function Annotation(props: InlineProps) { +export async function Annotation(props: InlineProps) { const { inline, context, document, children } = props; const fragment = getNodeFragmentByType(inline, 'annotation-body'); diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx deleted file mode 100644 index c90015f59..000000000 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import { DocumentBlockCode } from '@gitbook/api'; -import { useEffect, useState } from 'react'; - -import type { HighlightLine, RenderedInline } from './highlight'; -import type { BlockProps } from '../Block'; -import './theme.css'; -import { ClientCodeBlockRenderer } from './CodeBlockRenderer'; -import { highlightAction } from './highlight-action'; -import { plainHighlight } from './plain-highlight'; - -type ClientBlockProps = Pick, 'block' | 'style'> & { - inlines: RenderedInline[]; -}; - -/** - * Render a code-block client-side by calling a server actions to highlight the code. - * It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting. - */ -export function ClientCodeBlock(props: ClientBlockProps) { - const { block, style, inlines } = props; - const [lines, setLines] = useState(() => plainHighlight(block)); - useEffect(() => { - highlightAction(block, inlines).then(setLines); - }, [block, inlines]); - return ; -} diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlock.tsx index 9fde5c990..e079de1cb 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlock.tsx @@ -1,43 +1,291 @@ -import type { DocumentBlockCode } from '@gitbook/api'; +import { DocumentBlockCode, JSONDocument } from '@gitbook/api'; -import { getNodeFragmentByType } from '@/lib/document'; +import { tcls } from '@/lib/tailwind'; +import { CopyCodeButton } from './CopyCodeButton'; +import { highlight, HighlightLine, HighlightToken, plainHighlighting } from './highlight'; import { BlockProps } from '../Block'; -import { ClientCodeBlock } from './ClientCodeBlock'; -import { getInlines, RenderedInline } from './highlight'; -import { Blocks } from '../Blocks'; -import { ServerCodeBlock } from './ServerCodeBlock'; +import { DocumentContext } from '../DocumentView'; +import { Inline } from '../Inline'; + +import './theme.css'; /** - * Render a code block, can be client-side or server-side. + * Render an entire code-block. The syntax highlighting is done server-side. */ -export function CodeBlock(props: BlockProps) { - const { block, document, style, context, isEstimatedOffscreen } = props; - const inlines = getInlines(block); - const richInlines: RenderedInline[] = inlines.map((inline, index) => { - const body = (() => { - const fragment = getNodeFragmentByType(inline.inline, 'annotation-body'); - if (!fragment) { - return null; - } - return ( - ) { + const { block, document, style, context } = props; + const lines = await highlight(block); + + const id = block.key!; + + const withLineNumbers = !!block.data.lineNumbers && block.nodes.length > 1; + const withWrap = block.data.overflow === 'wrap'; + const title = block.data.title; + const titleRoundingStyle = [ + 'rounded-md', + 'straight-corners:rounded-sm', + title ? 'rounded-ss-none' : null, + ]; + + return ( +
+
+ {title ? ( +
+ {title} +
+ ) : null} +
+ +
+                
+                    {lines.map((line, index) => (
+                        
+                    ))}
+                
+            
+
+ ); +} + +function CodeHighlightLine(props: { + block: DocumentBlockCode; + document: JSONDocument; + line: HighlightLine; + lineIndex: number; + isLast: boolean; + withLineNumbers: boolean; + withWrap: boolean; + context: DocumentContext; +}) { + const { block, document, line, isLast, withLineNumbers, context } = props; + return ( + *]:mt-1', + //last child + '[&.highlighted:last-child]:rounded-b-md', + '[&.highlighted:last-child>*]:mb-1', + //is only child, dont hover effect line + '[&:only-child]:hover:ring-transparent', + //select all highlighted + '[&.highlighted]:rounded-none', + //select first in group + '[&:not(.highlighted)_+_.highlighted]:rounded-t-md', + '[&:not(.highlighted)_+_.highlighted>*]:mt-1', + //select last in group + '[&.highlighted:has(+:not(.highlighted))]:rounded-b-md', + '[&.highlighted:has(+:not(.highlighted))>*]:mb-1', + //select if highlight is singular in group + '[&:not(.highlighted)_+_.highlighted:has(+:not(.highlighted))]:rounded-md', + + line.highlighted ? ['highlighted', 'bg-light-3', 'dark:bg-dark-3'] : null, + )} + > + {withLineNumbers ? ( + + ) : null} + + + + {isLast ? null : !withLineNumbers && line.tokens.length === 0 && 0 ? ( + {'\u200B'} + ) : ( + '\n' + )} + + + ); +} + +function CodeHighlightTokens(props: { + tokens: HighlightToken[]; + document: JSONDocument; + context: DocumentContext; +}) { + const { tokens, document, context } = props; + + return ( + <> + {tokens.map((token, index) => ( + - ); - })(); + ))} + + ); +} - return { inline, body }; - }); +function CodeHighlightToken(props: { + token: HighlightToken; + document: JSONDocument; + context: DocumentContext; +}) { + const { token, document, context } = props; - if (isEstimatedOffscreen) { - return ; + if (token.type === 'inline') { + return ( + + + + ); } - return ; + if (token.type === 'plain') { + return <>{token.content}; + } + + if (!token.token.color) { + return <>{token.token.content}; + } + + return {token.token.content}; } diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css deleted file mode 100644 index cd09db28a..000000000 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css +++ /dev/null @@ -1,36 +0,0 @@ -.highlight-line { - @apply grid [grid-template-columns:subgrid] col-span-2 relative ring-1 ring-transparent hover:ring-dark-4/5 hover:z-[1] dark:hover:ring-light-4/4 rounded; - @apply only:hover:ring-transparent; - - &.highlighted { - @apply bg-light-3 dark:bg-dark-3; - @apply first:rounded-t-md *:first:mt-1; - @apply last:rounded-b-md *:last:mb-1; - @apply rounded-none; - } - - &:not(.highlighted) + .highlighted { - @apply rounded-t-md *:mt-1; - } - - &.highlighted:has(+ :not(.highlighted)) { - @apply rounded-b-md *:mb-1; - } - - &:not(.highlighted) + .highlighted:has(+ :not(.highlighted)) { - @apply rounded-md; - } -} - -.highlight-line-number { - @apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-gradient-to-r from-80% from-light-2 to-transparent dark:from-dark-2 dark:to-transparent; - @apply before:text-dark/5 before:content-[counter(line)] [counter-increment:line] dark:before:text-light/4; - - &.highlighted { - @apply before:text-dark/6 dark:before:text-light/8 bg-gradient-to-r from-80% from-light-3 to-transparent dark:from-dark-3 dark:to-transparent; - } -} - -.highlight-line-content { - @apply ml-3 block text-sm; -} diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx deleted file mode 100644 index 9d0a63c3d..000000000 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { DocumentBlockCode, JSONDocument } from '@gitbook/api'; - -import { tcls } from '@/lib/tailwind'; - -import { CopyCodeButton } from './CopyCodeButton'; -import type { HighlightLine, HighlightToken } from './highlight'; -import { AnnotationPopover } from '../Annotation/AnnotationPopover'; -import { BlockProps } from '../Block'; -import './theme.css'; -import './CodeBlockRenderer.css'; - -type CodeBlockRendererProps = Pick, 'block' | 'style'> & { - lines: HighlightLine[]; -}; - -/** - * The logic of rendering a code block from lines. - */ -export function ClientCodeBlockRenderer(props: CodeBlockRendererProps) { - const { block, style, lines } = props; - - const id = block.key!; - - const withLineNumbers = !!block.data.lineNumbers && block.nodes.length > 1; - const withWrap = block.data.overflow === 'wrap'; - const title = block.data.title; - const titleRoundingStyle = [ - 'rounded-md', - 'straight-corners:rounded-sm', - title ? 'rounded-ss-none' : null, - ]; - - return ( -
-
- {title ? ( -
- {title} -
- ) : null} -
- -
-                
-                    {lines.map((line, index) => (
-                        
-                    ))}
-                
-            
-
- ); -} - -function CodeHighlightLine(props: { - block: DocumentBlockCode; - line: HighlightLine; - lineIndex: number; - isLast: boolean; - withLineNumbers: boolean; - withWrap: boolean; -}) { - const { line, isLast, withLineNumbers } = props; - return ( - - {withLineNumbers ? ( - - ) : null} - - - - {isLast ? null : !withLineNumbers && line.tokens.length === 0 && 0 ? ( - {'\u200B'} - ) : ( - '\n' - )} - - - ); -} - -function CodeHighlightTokens(props: { tokens: HighlightToken[] }) { - const { tokens } = props; - - return ( - <> - {tokens.map((token, index) => ( - - ))} - - ); -} - -function CodeHighlightToken(props: { token: HighlightToken }) { - const { token } = props; - - if (token.type === 'annotation') { - return ( - - - - ); - } - - if (token.type === 'plain') { - return <>{token.content}; - } - - if (!token.token.color) { - return <>{token.token.content}; - } - - return {token.token.content}; -} diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/PlainCodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/PlainCodeBlock.tsx index 48fdb7958..e6b3ff78e 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/PlainCodeBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/PlainCodeBlock.tsx @@ -53,8 +53,7 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) { }} block={block} ancestorBlocks={[]} - // We optimize perf by default - isEstimatedOffscreen + isEstimatedOffscreen={false} /> ); } diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/ServerCodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/ServerCodeBlock.tsx deleted file mode 100644 index e07439fa8..000000000 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/ServerCodeBlock.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { DocumentBlockCode } from '@gitbook/api'; - -import { highlight, RenderedInline } from './highlight'; -import type { BlockProps } from '../Block'; -import './theme.css'; -import { ClientCodeBlockRenderer } from './CodeBlockRenderer'; - -type ClientBlockProps = Pick, 'block' | 'style'> & { - inlines: RenderedInline[]; -}; - -/** - * Render a code-block server-side. - */ -export async function ServerCodeBlock(props: ClientBlockProps) { - const { block, style, inlines } = props; - const lines = await highlight(block, inlines); - return ; -} diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight-action.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight-action.ts deleted file mode 100644 index 475a886a6..000000000 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight-action.ts +++ /dev/null @@ -1,14 +0,0 @@ -'use server'; - -import { DocumentBlockCode } from '@gitbook/api'; - -import { highlight, RenderedInline } from './highlight'; - -/** - * Server action to highlight a code block. - * By using a server action, we can avoid loading the highlighter on the client-side - * and increasing the bundle size. - */ -export async function highlightAction(block: DocumentBlockCode, inlines: RenderedInline[]) { - return highlight(block, inlines); -} diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.test.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.test.ts index dd66ac28e..faed7f48d 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.test.ts +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.test.ts @@ -1,18 +1,9 @@ -import type { DocumentBlockCode } from '@gitbook/api'; import { it, expect } from 'bun:test'; -import { getInlines, highlight, RenderedInline } from './highlight'; - -async function highlightWithInlines(block: DocumentBlockCode) { - const inlines: RenderedInline[] = getInlines(block).map((inline) => ({ - inline, - body: null, - })); - return highlight(block, inlines); -} +import { highlight } from './highlight'; it('should parse plain code', async () => { - const tokens = await highlightWithInlines({ + const tokens = await highlight({ object: 'block', type: 'code', data: {}, @@ -47,35 +38,32 @@ it('should parse plain code', async () => { it('should parse different code in parallel', async () => { await Promise.all( ['shell', 'scss', 'markdown', 'less', 'scss', 'css', 'scss', 'yaml'].map(async (syntax) => - highlight( - { - object: 'block', - type: 'code', - data: { - syntax: syntax, - }, - nodes: [ - { - object: 'block', - type: 'code-line', - data: {}, - nodes: [ - { - object: 'text', - leaves: [{ object: 'leaf', marks: [], text: 'Hello world' }], - }, - ], - }, - ], + highlight({ + object: 'block', + type: 'code', + data: { + syntax: syntax, }, - [], - ), + nodes: [ + { + object: 'block', + type: 'code-line', + data: {}, + nodes: [ + { + object: 'text', + leaves: [{ object: 'leaf', marks: [], text: 'Hello world' }], + }, + ], + }, + ], + }), ), ); }); it('should parse a multilines plain code', async () => { - const tokens = await highlightWithInlines({ + const tokens = await highlight({ object: 'block', type: 'code', data: {}, @@ -150,7 +138,7 @@ it('should parse a multilines plain code', async () => { }); it('should parse code with an inline on a single line', async () => { - const tokens = await highlightWithInlines({ + const tokens = await highlight({ object: 'block', type: 'code', data: { @@ -204,8 +192,10 @@ it('should parse code with an inline on a single line', async () => { }, }, { - type: 'annotation', - body: null, + type: 'inline', + inline: { + type: 'annotation', + }, children: [ { type: 'shiki', @@ -239,7 +229,7 @@ it('should parse code with an inline on a single line', async () => { }); it('should parse code with an inline on a multiple line', async () => { - const tokens = await highlightWithInlines({ + const tokens = await highlight({ object: 'block', type: 'code', data: { @@ -320,8 +310,10 @@ it('should parse code with an inline on a multiple line', async () => { }, }, { - type: 'annotation', - body: null, + type: 'inline', + inline: { + type: 'annotation', + }, children: [ { type: 'shiki', @@ -355,8 +347,10 @@ it('should parse code with an inline on a multiple line', async () => { }, }, { - type: 'annotation', - body: null, + type: 'inline', + inline: { + type: 'annotation', + }, children: [ { type: 'shiki', @@ -390,7 +384,7 @@ it('should parse code with an inline on a multiple line', async () => { }); it('should support code token finishing before the end of the annotation', async () => { - const tokens = await highlightWithInlines({ + const tokens = await highlight({ object: 'block', type: 'code', isVoid: false, @@ -476,8 +470,10 @@ it('should support code token finishing before the end of the annotation', async }, }, { - type: 'annotation', - body: null, + type: 'inline', + inline: { + type: 'annotation', + }, children: [ { type: 'shiki', @@ -511,7 +507,7 @@ it('should support code token finishing before the end of the annotation', async }); it('should support multiple code tokens in an annotation', async () => { - const tokens = await highlightWithInlines({ + const tokens = await highlight({ object: 'block', type: 'code', isVoid: false, @@ -619,8 +615,11 @@ it('should support multiple code tokens in an annotation', async () => { }, }, { - type: 'annotation', - body: null, + type: 'inline', + inline: { + object: 'inline', + type: 'annotation', + }, children: [ { type: 'shiki', @@ -654,7 +653,7 @@ it('should support multiple code tokens in an annotation', async () => { }); it('should handle \\r', async () => { - const tokens = await highlightWithInlines({ + const tokens = await highlight({ object: 'block', type: 'code', data: { diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts index da136e332..c3d6ee054 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts @@ -12,8 +12,6 @@ import { asyncMutexFunction, singleton } from '@/lib/async'; import { getNodeText } from '@/lib/document'; import { trace } from '@/lib/tracing'; -import { plainHighlight } from './plain-highlight'; - export type HighlightLine = { highlighted: boolean; tokens: HighlightToken[]; @@ -22,31 +20,28 @@ export type HighlightLine = { export type HighlightToken = | { type: 'plain'; content: string } | { type: 'shiki'; token: ThemedToken } - | { type: 'annotation'; body: React.ReactNode; children: HighlightToken[] }; + | { type: 'inline'; inline: DocumentInlineAnnotation; children: HighlightToken[] }; -export type InlineIndexed = { inline: any; start: number; end: number }; +type InlineIndexed = { inline: any; start: number; end: number }; type PositionedToken = ThemedToken & { start: number; end: number }; -export type RenderedInline = { - inline: InlineIndexed; - body: React.ReactNode; -}; - /** * Highlight a code block while preserving inline elements. */ -export async function highlight( - block: DocumentBlockCode, - inlines: RenderedInline[], -): Promise { +export async function highlight(block: DocumentBlockCode): Promise { const langName = block.data.syntax ? getLanguageForSyntax(block.data.syntax) : null; if (!langName) { // Language not found, fallback to plain highlighting - return plainHighlight(block, inlines); + return plainHighlighting(block); } - const code = getPlainCodeBlock(block); + const inlines: InlineIndexed[] = []; + const code = getPlainCodeBlock(block, inlines); + + inlines.sort((a, b) => { + return a.start - b.start; + }); const highlighter = await loadHighlighter(); await loadHighlighterLanguage(highlighter, langName); @@ -121,25 +116,10 @@ function getLanguageForSyntax(syntax: string): BundledLanguage | null { return null; } -export function getInlines(block: DocumentBlockCode) { - const inlines: InlineIndexed[] = []; - getPlainCodeBlock(block, inlines); - - inlines.sort((a, b) => { - return a.start - b.start; - }); - - return inlines; -} - /** * Parse a code block without highlighting it. */ -export function plainHighlighting( - block: DocumentBlockCode, - inlines?: RenderedInline[], -): HighlightLine[] { - const inlinesCopy = Array.from(inlines ?? []); +export function plainHighlighting(block: DocumentBlockCode): HighlightLine[] { return block.nodes.map((lineBlock) => { const tokens: HighlightToken[] = []; @@ -150,10 +130,9 @@ export function plainHighlighting( content: getNodeText(node), }); } else { - const inline = inlinesCopy.shift(); tokens.push({ - type: 'annotation', - body: inline?.body ?? null, + type: 'inline', + inline: node, children: [ { type: 'plain', @@ -173,7 +152,7 @@ export function plainHighlighting( function matchTokenAndInlines( eat: () => PositionedToken | null, - allInlines: RenderedInline[], + allInlines: InlineIndexed[], ): HighlightToken[] { const initialToken = eat(); if (!initialToken) { @@ -181,7 +160,7 @@ function matchTokenAndInlines( } const inlines = allInlines.filter( - ({ inline }) => inline.start >= initialToken.start && inline.start < initialToken.end, + (inline) => inline.start >= initialToken.start && inline.start < initialToken.end, ); let token = initialToken; const result: HighlightToken[] = []; @@ -197,7 +176,7 @@ function matchTokenAndInlines( return; } - const [before, afterBefore] = splitPositionedTokenAt(token, inline.inline.start); + const [before, afterBefore] = splitPositionedTokenAt(token, inline.start); if (before) { result.push({ type: 'shiki', @@ -212,7 +191,7 @@ function matchTokenAndInlines( 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) { + while (token.end < inline.end) { children.push({ type: 'shiki', token: token, @@ -225,7 +204,7 @@ function matchTokenAndInlines( token = next; } - const [inside, after] = splitPositionedTokenAt(token, inline.inline.end); + const [inside, after] = splitPositionedTokenAt(token, inline.end); if (!inside) { throw new Error(`expect inside to not be empty`); } @@ -236,8 +215,8 @@ function matchTokenAndInlines( }); result.push({ - type: 'annotation', - body: inline.body, + type: 'inline', + inline: inline.inline, children, }); @@ -251,11 +230,11 @@ function matchTokenAndInlines( return result; } -function getPlainCodeBlock(code: DocumentBlockCode, inlines?: InlineIndexed[]): string { +function getPlainCodeBlock(code: DocumentBlockCode, inlines: InlineIndexed[]): string { let content = ''; code.nodes.forEach((node, index) => { - const lineContent = getPlainCodeBlockLine(node, content.length, inlines); + const lineContent = getPlainCodeBlockLine(node, inlines, content.length); content += lineContent; if (index < code.nodes.length - 1) { @@ -268,8 +247,8 @@ function getPlainCodeBlock(code: DocumentBlockCode, inlines?: InlineIndexed[]): function getPlainCodeBlockLine( parent: DocumentBlockCodeLine | DocumentInlineAnnotation, + inlines: InlineIndexed[], index: number, - inlines?: InlineIndexed[], ): string { let content = ''; @@ -278,16 +257,14 @@ function getPlainCodeBlockLine( content += cleanupLine(node.leaves.map((leaf) => leaf.text).join('')); } else { const start = index + content.length; - content += getPlainCodeBlockLine(node, index + content.length, inlines); + content += getPlainCodeBlockLine(node, inlines, index + content.length); const end = index + content.length; - if (inlines) { - inlines.push({ - inline: node, - start, - end, - }); - } + inlines.push({ + inline: node, + start, + end, + }); } } @@ -364,8 +341,7 @@ const loadHighlighter = singleton(async () => { }); const loadLanguagesMutex = asyncMutexFunction(); - -const loadHighlighterLanguage = async function loadHighlighterLanguage( +async function loadHighlighterLanguage( highlighter: HighlighterGeneric, lang: keyof typeof bundledLanguages, ) { @@ -379,4 +355,4 @@ const loadHighlighterLanguage = async function loadHighlighterLanguage( async () => await highlighter.loadLanguage(lang), ); }); -}; +} diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/plain-highlight.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/plain-highlight.ts deleted file mode 100644 index a1afbecca..000000000 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/plain-highlight.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { DocumentBlockCode } from '@gitbook/api'; - -import { getNodeText } from '@/lib/document'; - -import type { HighlightLine, HighlightToken, RenderedInline } from './highlight'; - -/** - * Parse a code block without highlighting it. - */ -export function plainHighlight( - block: DocumentBlockCode, - inlines?: RenderedInline[], -): HighlightLine[] { - const inlinesCopy = Array.from(inlines ?? []); - return block.nodes.map((lineBlock) => { - const tokens: HighlightToken[] = []; - - for (const node of lineBlock.nodes) { - if (node.object === 'text') { - tokens.push({ - type: 'plain', - content: getNodeText(node), - }); - } else { - const inline = inlinesCopy.shift(); - tokens.push({ - type: 'annotation', - body: inline?.body ?? null, - children: [ - { - type: 'plain', - content: getNodeText(node), - }, - ], - }); - } - } - - return { - highlighted: !!lineBlock.data.highlighted, - tokens, - }; - }); -} diff --git a/packages/gitbook/src/components/PageAside/PageAside.tsx b/packages/gitbook/src/components/PageAside/PageAside.tsx index 8ddcfe8d8..74ec4c72e 100644 --- a/packages/gitbook/src/components/PageAside/PageAside.tsx +++ b/packages/gitbook/src/components/PageAside/PageAside.tsx @@ -13,7 +13,7 @@ import React from 'react'; import urlJoin from 'url-join'; import { t, getSpaceLanguage } from '@/intl/server'; -import { getDocumentSections } from '@/lib/document-sections'; +import { getDocumentSections } from '@/lib/document'; import { getAbsoluteHref } from '@/lib/links'; import { ContentRefContext, resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; diff --git a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx index 3fff267b5..ce3224a3d 100644 --- a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx +++ b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx @@ -3,7 +3,7 @@ import { motion } from 'framer-motion'; import React from 'react'; import { useScrollActiveId } from '@/components/hooks'; -import type { DocumentSection } from '@/lib/document-sections'; +import { DocumentSection } from '@/lib/document'; import { tcls } from '@/lib/tailwind'; import { AsideSectionHighlight } from './AsideSectionHighlight'; diff --git a/packages/gitbook/src/lib/document-sections.ts b/packages/gitbook/src/lib/document-sections.ts deleted file mode 100644 index b7535696b..000000000 --- a/packages/gitbook/src/lib/document-sections.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { JSONDocument, ContentRef } from '@gitbook/api'; - -import { getNodeText } from './document'; -import { fetchOpenAPIBlock } from './openapi'; -import { ResolvedContentRef } from './references'; - -export interface DocumentSection { - id: string; - tag?: string; - title: string; - depth: number; -} - -/** - * Extract a list of sections from a document. - */ -export async function getDocumentSections( - document: JSONDocument, - resolveContentRef: (ref: ContentRef) => Promise, -): Promise { - const sections: DocumentSection[] = []; - let depth = 0; - - for (const block of document.nodes) { - if ((block.type === 'heading-1' || block.type === 'heading-2') && block.meta?.id) { - if (block.type === 'heading-1') { - depth = 1; - } - const title = getNodeText(block); - const id = block.meta.id; - - sections.push({ - id, - title, - depth: block.type === 'heading-1' ? 1 : depth > 0 ? 2 : 1, - }); - } - - if (block.type === 'swagger' && block.meta?.id) { - const { data: operation } = await fetchOpenAPIBlock(block, resolveContentRef); - if (operation) { - sections.push({ - id: block.meta.id, - tag: operation.method.toUpperCase(), - title: operation.operation.summary ?? operation.path, - depth: 1, - }); - } - } - } - - return sections; -} diff --git a/packages/gitbook/src/lib/document.ts b/packages/gitbook/src/lib/document.ts index 1f6666203..8cf5be47b 100644 --- a/packages/gitbook/src/lib/document.ts +++ b/packages/gitbook/src/lib/document.ts @@ -1,12 +1,16 @@ -import type { +import { DocumentText, DocumentInline, DocumentFragment, JSONDocument, DocumentBlock, + ContentRef, } from '@gitbook/api'; import assertNever from 'assert-never'; +import { fetchOpenAPIBlock } from './openapi'; +import { ResolvedContentRef } from './references'; + export interface DocumentSection { id: string; tag?: string; @@ -30,6 +34,47 @@ export function hasFullWidthBlock(document: JSONDocument): boolean { return false; } +/** + * Extract a list of sections from a document. + */ +export async function getDocumentSections( + document: JSONDocument, + resolveContentRef: (ref: ContentRef) => Promise, +): Promise { + const sections: DocumentSection[] = []; + let depth = 0; + + for (const block of document.nodes) { + if ((block.type === 'heading-1' || block.type === 'heading-2') && block.meta?.id) { + if (block.type === 'heading-1') { + depth = 1; + } + const title = getNodeText(block); + const id = block.meta.id; + + sections.push({ + id, + title, + depth: block.type === 'heading-1' ? 1 : depth > 0 ? 2 : 1, + }); + } + + if (block.type === 'swagger' && block.meta?.id) { + const { data: operation } = await fetchOpenAPIBlock(block, resolveContentRef); + if (operation) { + sections.push({ + id: block.meta.id, + tag: operation.method.toUpperCase(), + title: operation.operation.summary ?? operation.path, + depth: 1, + }); + } + } + } + + return sections; +} + /** * Get the text of a block/inline. */