|
1 |
| -import type { DocumentBlockCode } from '@gitbook/api'; |
| 1 | +import { DocumentBlockCode, JSONDocument } from '@gitbook/api'; |
2 | 2 |
|
3 |
| -import { getNodeFragmentByType } from '@/lib/document'; |
| 3 | +import { tcls } from '@/lib/tailwind'; |
4 | 4 |
|
| 5 | +import { CopyCodeButton } from './CopyCodeButton'; |
| 6 | +import { highlight, HighlightLine, HighlightToken, plainHighlighting } from './highlight'; |
5 | 7 | import { BlockProps } from '../Block';
|
6 |
| -import { ClientCodeBlock } from './ClientCodeBlock'; |
7 |
| -import { getInlines, RenderedInline } from './highlight'; |
8 |
| -import { Blocks } from '../Blocks'; |
9 |
| -import { ServerCodeBlock } from './ServerCodeBlock'; |
| 8 | +import { DocumentContext } from '../DocumentView'; |
| 9 | +import { Inline } from '../Inline'; |
| 10 | + |
| 11 | +import './theme.css'; |
10 | 12 |
|
11 | 13 | /**
|
12 |
| - * Render a code block, can be client-side or server-side. |
| 14 | + * Render an entire code-block. The syntax highlighting is done server-side. |
13 | 15 | */
|
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 |
| 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 |
25 | 248 | 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} |
26 | 275 | document={document}
|
27 |
| - ancestorBlocks={[]} |
28 | 276 | context={context}
|
29 |
| - nodes={fragment.nodes} |
30 |
| - style={['space-y-4']} |
31 | 277 | />
|
32 |
| - ); |
33 |
| - })(); |
| 278 | + </Inline> |
| 279 | + ); |
| 280 | + } |
34 | 281 |
|
35 |
| - return { inline, body }; |
36 |
| - }); |
| 282 | + if (token.type === 'plain') { |
| 283 | + return <>{token.content}</>; |
| 284 | + } |
37 | 285 |
|
38 |
| - if (isEstimatedOffscreen) { |
39 |
| - return <ClientCodeBlock block={block} style={style} inlines={richInlines} />; |
| 286 | + if (!token.token.color) { |
| 287 | + return <>{token.token.content}</>; |
40 | 288 | }
|
41 | 289 |
|
42 |
| - return <ServerCodeBlock block={block} style={style} inlines={richInlines} />; |
| 290 | + return <span style={{ color: token.token.color }}>{token.token.content}</span>; |
43 | 291 | }
|
0 commit comments