Skip to content

Commit 0d615e3

Browse files
authored
Highlight code client-side (#2786)
1 parent 68287d3 commit 0d615e3

16 files changed

+527
-405
lines changed

.changeset/thin-files-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'gitbook': patch
3+
---
4+
5+
Improve performances by highlighting code client-side if the code block is offscreen

packages/gitbook/src/components/DocumentView/Annotation/Annotation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Blocks } from '../Blocks';
77
import { InlineProps } from '../Inline';
88
import { Inlines } from '../Inlines';
99

10-
export async function Annotation(props: InlineProps<DocumentInlineAnnotation>) {
10+
export function Annotation(props: InlineProps<DocumentInlineAnnotation>) {
1111
const { inline, context, document, children } = props;
1212

1313
const fragment = getNodeFragmentByType(inline, 'annotation-body');
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use client';
2+
3+
import { DocumentBlockCode } from '@gitbook/api';
4+
import { useEffect, useState } from 'react';
5+
6+
import type { HighlightLine, RenderedInline } from './highlight';
7+
import type { BlockProps } from '../Block';
8+
import './theme.css';
9+
import { ClientCodeBlockRenderer } from './CodeBlockRenderer';
10+
import { highlightAction } from './highlight-action';
11+
import { plainHighlight } from './plain-highlight';
12+
13+
type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
14+
inlines: RenderedInline[];
15+
};
16+
17+
/**
18+
* Render a code-block client-side by calling a server actions to highlight the code.
19+
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
20+
*/
21+
export function ClientCodeBlock(props: ClientBlockProps) {
22+
const { block, style, inlines } = props;
23+
const [lines, setLines] = useState<HighlightLine[]>(() => plainHighlight(block));
24+
useEffect(() => {
25+
highlightAction(block, inlines).then(setLines);
26+
}, [block, inlines]);
27+
return <ClientCodeBlockRenderer block={block} style={style} lines={lines} />;
28+
}
Lines changed: 28 additions & 276 deletions
Original file line numberDiff line numberDiff line change
@@ -1,291 +1,43 @@
1-
import { DocumentBlockCode, JSONDocument } from '@gitbook/api';
1+
import type { DocumentBlockCode } from '@gitbook/api';
22

3-
import { tcls } from '@/lib/tailwind';
3+
import { getNodeFragmentByType } from '@/lib/document';
44

5-
import { CopyCodeButton } from './CopyCodeButton';
6-
import { highlight, HighlightLine, HighlightToken, plainHighlighting } from './highlight';
75
import { BlockProps } from '../Block';
8-
import { DocumentContext } from '../DocumentView';
9-
import { Inline } from '../Inline';
10-
11-
import './theme.css';
6+
import { ClientCodeBlock } from './ClientCodeBlock';
7+
import { getInlines, RenderedInline } from './highlight';
8+
import { Blocks } from '../Blocks';
9+
import { ServerCodeBlock } from './ServerCodeBlock';
1210

1311
/**
14-
* Render an entire code-block. The syntax highlighting is done server-side.
12+
* Render a code block, can be client-side or server-side.
1513
*/
16-
export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
17-
const { block, document, style, context } = props;
18-
const lines = await highlight(block);
19-
20-
const id = block.key!;
21-
22-
const withLineNumbers = !!block.data.lineNumbers && block.nodes.length > 1;
23-
const withWrap = block.data.overflow === 'wrap';
24-
const title = block.data.title;
25-
const titleRoundingStyle = [
26-
'rounded-md',
27-
'straight-corners:rounded-sm',
28-
title ? 'rounded-ss-none' : null,
29-
];
30-
31-
return (
32-
<div className={tcls('group/codeblock', 'grid', 'grid-flow-col', style)}>
33-
<div
34-
className={tcls(
35-
'flex',
36-
'items-center',
37-
'justify-start',
38-
'[grid-area:1/1]',
39-
'text-sm',
40-
'gap-2',
41-
)}
42-
>
43-
{title ? (
44-
<div
45-
className={tcls(
46-
'text-xs',
47-
'tracking-wide',
48-
'text-dark/7',
49-
'leading-none',
50-
'inline-flex',
51-
'items-center',
52-
'justify-center',
53-
'bg-light-2',
54-
'rounded-t',
55-
'straight-corners:rounded-t-s',
56-
'px-3',
57-
'py-2',
58-
'dark:bg-dark-2',
59-
'dark:text-light/7',
60-
)}
61-
>
62-
{title}
63-
</div>
64-
) : null}
65-
</div>
66-
<CopyCodeButton
67-
codeId={id}
68-
style={[
69-
'group-hover/codeblock:opacity-[1]',
70-
'transition-opacity',
71-
'duration-75',
72-
'opacity-0',
73-
'text-xs',
74-
'[grid-area:2/1]',
75-
'z-[2]',
76-
'justify-self-end',
77-
'backdrop-blur-md',
78-
'leading-none',
79-
'self-start',
80-
'ring-1',
81-
'ring-dark/2',
82-
'text-dark/7',
83-
'bg-transparent',
84-
'rounded-md',
85-
'mr-2',
86-
'mt-2',
87-
'p-1',
88-
'hover:ring-dark/3',
89-
'dark:ring-light/2',
90-
'dark:text-light/7',
91-
'dark:hover:ring-light/3',
92-
]}
93-
/>
94-
<pre
95-
className={tcls(
96-
'[grid-area:2/1]',
97-
'relative',
98-
'overflow-auto',
99-
'bg-light-2',
100-
'dark:bg-dark-2',
101-
'border-light-4',
102-
'dark:border-dark-4',
103-
'hide-scroll',
104-
titleRoundingStyle,
105-
)}
106-
>
107-
<code
108-
id={id}
109-
className={tcls(
110-
'min-w-full',
111-
'inline-grid',
112-
'[grid-template-columns:auto_1fr]',
113-
'py-2',
114-
'px-2',
115-
'[counter-reset:line]',
116-
withWrap ? 'whitespace-pre-wrap' : '',
117-
)}
118-
>
119-
{lines.map((line, index) => (
120-
<CodeHighlightLine
121-
block={block}
122-
document={document}
123-
key={index}
124-
line={line}
125-
lineIndex={index + 1}
126-
isLast={index === lines.length - 1}
127-
withLineNumbers={withLineNumbers}
128-
withWrap={withWrap}
129-
context={context}
130-
/>
131-
))}
132-
</code>
133-
</pre>
134-
</div>
135-
);
136-
}
137-
138-
function CodeHighlightLine(props: {
139-
block: DocumentBlockCode;
140-
document: JSONDocument;
141-
line: HighlightLine;
142-
lineIndex: number;
143-
isLast: boolean;
144-
withLineNumbers: boolean;
145-
withWrap: boolean;
146-
context: DocumentContext;
147-
}) {
148-
const { block, document, line, isLast, withLineNumbers, context } = props;
149-
return (
150-
<span
151-
className={tcls(
152-
'grid',
153-
'[grid-template-columns:subgrid]',
154-
'col-span-2',
155-
'relative',
156-
'ring-1',
157-
'ring-transparent',
158-
'hover:ring-dark-4/5',
159-
'hover:z-[1]',
160-
'dark:hover:ring-light-4/4',
161-
'rounded',
162-
//first child
163-
'[&.highlighted:first-child]:rounded-t-md',
164-
'[&.highlighted:first-child>*]:mt-1',
165-
//last child
166-
'[&.highlighted:last-child]:rounded-b-md',
167-
'[&.highlighted:last-child>*]:mb-1',
168-
//is only child, dont hover effect line
169-
'[&:only-child]:hover:ring-transparent',
170-
//select all highlighted
171-
'[&.highlighted]:rounded-none',
172-
//select first in group
173-
'[&:not(.highlighted)_+_.highlighted]:rounded-t-md',
174-
'[&:not(.highlighted)_+_.highlighted>*]:mt-1',
175-
//select last in group
176-
'[&.highlighted:has(+:not(.highlighted))]:rounded-b-md',
177-
'[&.highlighted:has(+:not(.highlighted))>*]:mb-1',
178-
//select if highlight is singular in group
179-
'[&:not(.highlighted)_+_.highlighted:has(+:not(.highlighted))]:rounded-md',
180-
181-
line.highlighted ? ['highlighted', 'bg-light-3', 'dark:bg-dark-3'] : null,
182-
)}
183-
>
184-
{withLineNumbers ? (
185-
<span
186-
className={tcls(
187-
'text-sm',
188-
'text-right',
189-
'pr-3.5',
190-
'rounded-l',
191-
'pl-2',
192-
'sticky',
193-
'left-[-3px]',
194-
'bg-gradient-to-r',
195-
'from-80%',
196-
'from-light-2',
197-
'to-transparent',
198-
'dark:from-dark-2',
199-
'dark:to-transparent',
200-
withLineNumbers
201-
? [
202-
'before:text-dark/5',
203-
'before:content-[counter(line)]',
204-
'[counter-increment:line]',
205-
'dark:before:text-light/4',
206-
207-
line.highlighted
208-
? [
209-
'before:text-dark/6',
210-
'dark:before:text-light/8',
211-
'bg-gradient-to-r',
212-
'from-80%',
213-
'from-light-3',
214-
'to-transparent',
215-
'dark:from-dark-3',
216-
'dark:to-transparent',
217-
]
218-
: null,
219-
]
220-
: [],
221-
)}
222-
></span>
223-
) : null}
224-
225-
<span className={tcls('ml-3', 'block', 'text-sm')}>
226-
<CodeHighlightTokens tokens={line.tokens} document={document} context={context} />
227-
{isLast ? null : !withLineNumbers && line.tokens.length === 0 && 0 ? (
228-
<span className="ew">{'\u200B'}</span>
229-
) : (
230-
'\n'
231-
)}
232-
</span>
233-
</span>
234-
);
235-
}
236-
237-
function CodeHighlightTokens(props: {
238-
tokens: HighlightToken[];
239-
document: JSONDocument;
240-
context: DocumentContext;
241-
}) {
242-
const { tokens, document, context } = props;
243-
244-
return (
245-
<>
246-
{tokens.map((token, index) => (
247-
<CodeHighlightToken
14+
export function CodeBlock(props: BlockProps<DocumentBlockCode>) {
15+
const { block, document, style, context, isEstimatedOffscreen } = props;
16+
const inlines = getInlines(block);
17+
const richInlines: RenderedInline[] = inlines.map((inline, index) => {
18+
const body = (() => {
19+
const fragment = getNodeFragmentByType(inline.inline, 'annotation-body');
20+
if (!fragment) {
21+
return null;
22+
}
23+
return (
24+
<Blocks
24825
key={index}
249-
token={token}
250-
document={document}
251-
context={context}
252-
/>
253-
))}
254-
</>
255-
);
256-
}
257-
258-
function CodeHighlightToken(props: {
259-
token: HighlightToken;
260-
document: JSONDocument;
261-
context: DocumentContext;
262-
}) {
263-
const { token, document, context } = props;
264-
265-
if (token.type === 'inline') {
266-
return (
267-
<Inline
268-
inline={token.inline}
269-
document={document}
270-
context={context}
271-
ancestorInlines={[]}
272-
>
273-
<CodeHighlightTokens
274-
tokens={token.children}
27526
document={document}
27+
ancestorBlocks={[]}
27628
context={context}
29+
nodes={fragment.nodes}
30+
style={['space-y-4']}
27731
/>
278-
</Inline>
279-
);
280-
}
32+
);
33+
})();
28134

282-
if (token.type === 'plain') {
283-
return <>{token.content}</>;
284-
}
35+
return { inline, body };
36+
});
28537

286-
if (!token.token.color) {
287-
return <>{token.token.content}</>;
38+
if (isEstimatedOffscreen) {
39+
return <ClientCodeBlock block={block} style={style} inlines={richInlines} />;
28840
}
28941

290-
return <span style={{ color: token.token.color }}>{token.token.content}</span>;
42+
return <ServerCodeBlock block={block} style={style} inlines={richInlines} />;
29143
}

0 commit comments

Comments
 (0)