3
3
*/
4
4
5
5
import cn from 'classnames' ;
6
- import {
7
- SandpackCodeViewer ,
8
- SandpackProvider ,
9
- } from '@codesandbox/sandpack-react' ;
6
+ import { highlightTree } from '@codemirror/highlight' ;
7
+ import { javascript } from '@codemirror/lang-javascript' ;
8
+ import { HighlightStyle , tags } from '@codemirror/highlight' ;
10
9
import rangeParser from 'parse-numeric-range' ;
11
10
import { CustomTheme } from '../Sandpack/Themes' ;
12
11
@@ -33,84 +32,275 @@ const CodeBlock = function CodeBlock({
33
32
className ?: string ;
34
33
noMargin ?: boolean ;
35
34
} ) {
36
- const getDecoratedLineInfo = ( ) => {
37
- if ( ! meta ) {
38
- return [ ] ;
35
+ code = code . trimEnd ( ) ;
36
+ const tree = language . language . parser . parse ( code ) ;
37
+ let tokenStarts = new Map ( ) ;
38
+ let tokenEnds = new Map ( ) ;
39
+ const highlightTheme = getSyntaxHighlight ( CustomTheme ) ;
40
+ highlightTree ( tree , highlightTheme . match , ( from , to , className ) => {
41
+ tokenStarts . set ( from , className ) ;
42
+ tokenEnds . set ( to , className ) ;
43
+ } ) ;
44
+
45
+ const highlightedLines = new Map ( ) ;
46
+ const lines = code . split ( '\n' ) ;
47
+ const lineDecorators = getLineDecorators ( code , meta ) ;
48
+ for ( let decorator of lineDecorators ) {
49
+ highlightedLines . set ( decorator . line - 1 , decorator . className ) ;
50
+ }
51
+
52
+ const inlineDecorators = getInlineDecorators ( code , meta ) ;
53
+ const decoratorStarts = new Map ( ) ;
54
+ const decoratorEnds = new Map ( ) ;
55
+ for ( let decorator of inlineDecorators ) {
56
+ // Find where inline highlight starts and ends.
57
+ let decoratorStart = 0 ;
58
+ for ( let i = 0 ; i < decorator . line - 1 ; i ++ ) {
59
+ decoratorStart += lines [ i ] . length + 1 ;
60
+ }
61
+ decoratorStart += decorator . startColumn ;
62
+ const decoratorEnd =
63
+ decoratorStart + ( decorator . endColumn - decorator . startColumn ) ;
64
+ if ( decoratorStarts . has ( decoratorStart ) ) {
65
+ throw Error ( 'Already opened decorator at ' + decoratorStart ) ;
66
+ }
67
+ decoratorStarts . set ( decoratorStart , decorator . className ) ;
68
+ if ( decoratorEnds . has ( decoratorEnd ) ) {
69
+ throw Error ( 'Already closed decorator at ' + decoratorEnd ) ;
39
70
}
71
+ decoratorEnds . set ( decoratorEnd , decorator . className ) ;
72
+ }
40
73
41
- const linesToHighlight = getHighlightLines ( meta ) ;
42
- const highlightedLineConfig = linesToHighlight . map ( ( line ) => {
43
- return {
44
- className : 'bg-github-highlight dark:bg-opacity-10' ,
45
- line,
46
- } ;
47
- } ) ;
48
-
49
- const inlineHighlightLines = getInlineHighlights ( meta , code ) ;
50
- const inlineHighlightConfig = inlineHighlightLines . map (
51
- ( line : InlineHiglight ) => ( {
52
- ...line ,
53
- elementAttributes : { 'data-step' : `${ line . step } ` } ,
54
- className : cn (
55
- 'code-step bg-opacity-10 dark:bg-opacity-20 relative rounded px-1 py-[1.5px] border-b-[2px] border-opacity-60' ,
56
- {
57
- 'bg-blue-40 border-blue-40 text-blue-60 dark:text-blue-30 font-bold' :
58
- line . step === 1 ,
59
- 'bg-yellow-40 border-yellow-40 text-yellow-60 dark:text-yellow-30 font-bold' :
60
- line . step === 2 ,
61
- 'bg-purple-40 border-purple-40 text-purple-60 dark:text-purple-30 font-bold' :
62
- line . step === 3 ,
63
- 'bg-green-40 border-green-40 text-green-60 dark:text-green-30 font-bold' :
64
- line . step === 4 ,
65
- }
66
- ) ,
67
- } )
68
- ) ;
69
-
70
- return highlightedLineConfig . concat ( inlineHighlightConfig ) ;
71
- } ;
74
+ // Produce output based on tokens and decorators.
75
+ // We assume tokens never overlap other tokens, and
76
+ // decorators never overlap with other decorators.
77
+ // However, tokens and decorators may mutually overlap.
78
+ // In that case, decorators always take precedence.
79
+ let currentDecorator = null ;
80
+ let currentToken = null ;
81
+ let buffer = '' ;
82
+ let lineIndex = 0 ;
83
+ let lineOutput = [ ] ;
84
+ let finalOutput = [ ] ;
85
+ for ( let i = 0 ; i < code . length ; i ++ ) {
86
+ if ( tokenEnds . has ( i ) ) {
87
+ if ( ! currentToken ) {
88
+ throw Error ( 'Cannot close token at ' + i + ' because it was not open.' ) ;
89
+ }
90
+ if ( ! currentDecorator ) {
91
+ lineOutput . push (
92
+ < span key = { i + '/t' } className = { currentToken } >
93
+ { buffer }
94
+ </ span >
95
+ ) ;
96
+ buffer = '' ;
97
+ }
98
+ currentToken = null ;
99
+ }
100
+ if ( decoratorEnds . has ( i ) ) {
101
+ if ( ! currentDecorator ) {
102
+ throw Error (
103
+ 'Cannot close decorator at ' + i + ' because it was not open.'
104
+ ) ;
105
+ }
106
+ lineOutput . push (
107
+ < span key = { i + '/d' } className = { currentDecorator } >
108
+ { buffer }
109
+ </ span >
110
+ ) ;
111
+ buffer = '' ;
112
+ currentDecorator = null ;
113
+ }
114
+ if ( decoratorStarts . has ( i ) ) {
115
+ if ( currentDecorator ) {
116
+ throw Error (
117
+ 'Cannot open decorator at ' + i + ' before closing last one.'
118
+ ) ;
119
+ }
120
+ if ( currentToken ) {
121
+ lineOutput . push (
122
+ < span key = { i + 'd' } className = { currentToken } >
123
+ { buffer }
124
+ </ span >
125
+ ) ;
126
+ buffer = '' ;
127
+ } else {
128
+ lineOutput . push ( buffer ) ;
129
+ buffer = '' ;
130
+ }
131
+ currentDecorator = decoratorStarts . get ( i ) ;
132
+ }
133
+ if ( tokenStarts . has ( i ) ) {
134
+ if ( currentToken ) {
135
+ throw Error ( 'Cannot open token at ' + i + ' before closing last one.' ) ;
136
+ }
137
+ currentToken = tokenStarts . get ( i ) ;
138
+ if ( ! currentDecorator ) {
139
+ lineOutput . push ( buffer ) ;
140
+ buffer = '' ;
141
+ }
142
+ }
143
+ if ( code [ i ] === '\n' ) {
144
+ lineOutput . push ( buffer ) ;
145
+ buffer = '' ;
146
+ finalOutput . push (
147
+ < div
148
+ key = { lineIndex }
149
+ className = { 'cm-line ' + highlightedLines . get ( lineIndex ) } >
150
+ { lineOutput }
151
+ < br />
152
+ </ div >
153
+ ) ;
154
+ lineOutput = [ ] ;
155
+ lineIndex ++ ;
156
+ } else {
157
+ buffer += code [ i ] ;
158
+ }
159
+ }
160
+ lineOutput . push ( buffer ) ;
161
+ finalOutput . push (
162
+ < div
163
+ key = { lineIndex }
164
+ className = { 'cm-line ' + highlightedLines . get ( lineIndex ) } >
165
+ { lineOutput }
166
+ </ div >
167
+ ) ;
72
168
73
- // e.g. "language-js"
74
- const language = className . substring ( 9 ) ;
75
- const filename = '/index.' + language ;
76
- const decorators = getDecoratedLineInfo ( ) ;
77
169
return (
78
170
< div
79
- key = {
80
- // HACK: There seems to be a bug where the rendered result
81
- // "lags behind" the edits to it. For now, force it to reset.
82
- process . env . NODE_ENV === 'development' ? code : ''
83
- }
84
171
className = { cn (
85
172
'sandpack sandpack--codeblock' ,
86
173
'rounded-lg h-full w-full overflow-x-auto flex items-center bg-wash dark:bg-gray-95 shadow-lg' ,
87
174
! noMargin && 'my-8'
88
175
) } >
89
- < SandpackProvider
90
- files = { {
91
- [ filename ] : {
92
- code : code . trimEnd ( ) ,
93
- } ,
94
- } }
95
- customSetup = { {
96
- entry : filename ,
97
- } }
98
- options = { {
99
- initMode : 'immediate' ,
100
- } }
101
- theme = { CustomTheme } >
102
- < SandpackCodeViewer
103
- key = { code . trimEnd ( ) }
104
- showLineNumbers = { false }
105
- decorators = { decorators }
106
- />
107
- </ SandpackProvider >
176
+ { /* These classes are fragile and depend on Sandpack. TODO: some better way. */ }
177
+ < div className = "sp-wrapper sp-121717251 sp-c-fVPbOs sp-c-fVPbOs-LrWkf-variant-dark" >
178
+ < div className = "sp-stack sp-c-kLppIp" >
179
+ < div className = "sp-code-editor sp-c-bNbSGz" >
180
+ < pre className = "sp-cm sp-pristine sp-javascript sp-c-jcgexo sp-c-jkvvao" >
181
+ < code className = "sp-pre-placeholder sp-c-fWymNx" >
182
+ { finalOutput }
183
+ </ code >
184
+ </ pre >
185
+ </ div >
186
+ </ div >
187
+ </ div >
108
188
</ div >
109
189
) ;
110
190
} ;
111
191
112
192
export default CodeBlock ;
113
193
194
+ const language = javascript ( { jsx : true , typescript : false } ) ;
195
+
196
+ function classNameToken ( name : string ) : string {
197
+ return `sp-syntax-${ name } ` ;
198
+ }
199
+
200
+ function getSyntaxHighlight ( theme : any ) : HighlightStyle {
201
+ return HighlightStyle . define ( [
202
+ { tag : tags . link , textdecorator : 'underline' } ,
203
+ { tag : tags . emphasis , fontStyle : 'italic' } ,
204
+ { tag : tags . strong , fontWeight : 'bold' } ,
205
+
206
+ {
207
+ tag : tags . keyword ,
208
+ class : classNameToken ( 'keyword' ) ,
209
+ } ,
210
+ {
211
+ tag : [ tags . atom , tags . number , tags . bool ] ,
212
+ class : classNameToken ( 'static' ) ,
213
+ } ,
214
+ {
215
+ tag : tags . tagName ,
216
+ class : classNameToken ( 'tag' ) ,
217
+ } ,
218
+ { tag : tags . variableName , class : classNameToken ( 'plain' ) } ,
219
+ {
220
+ // Highlight function call
221
+ tag : tags . function ( tags . variableName ) ,
222
+ class : classNameToken ( 'definition' ) ,
223
+ } ,
224
+ {
225
+ // Highlight function definition differently (eg: functional component def in React)
226
+ tag : tags . definition ( tags . function ( tags . variableName ) ) ,
227
+ class : classNameToken ( 'definition' ) ,
228
+ } ,
229
+ {
230
+ tag : tags . propertyName ,
231
+ class : classNameToken ( 'property' ) ,
232
+ } ,
233
+ {
234
+ tag : [ tags . literal , tags . inserted ] ,
235
+ class : classNameToken ( theme . syntax . string ? 'string' : 'static' ) ,
236
+ } ,
237
+ {
238
+ tag : tags . punctuation ,
239
+ class : classNameToken ( 'punctuation' ) ,
240
+ } ,
241
+ {
242
+ tag : [ tags . comment , tags . quote ] ,
243
+ class : classNameToken ( 'comment' ) ,
244
+ } ,
245
+ ] ) ;
246
+ }
247
+
248
+ function getLineDecorators (
249
+ code : string ,
250
+ meta : string
251
+ ) : Array < {
252
+ line : number ;
253
+ className : string ;
254
+ } > {
255
+ if ( ! meta ) {
256
+ return [ ] ;
257
+ }
258
+ const linesToHighlight = getHighlightLines ( meta ) ;
259
+ const highlightedLineConfig = linesToHighlight . map ( ( line ) => {
260
+ return {
261
+ className : 'bg-github-highlight dark:bg-opacity-10' ,
262
+ line,
263
+ } ;
264
+ } ) ;
265
+ return highlightedLineConfig ;
266
+ }
267
+
268
+ function getInlineDecorators (
269
+ code : string ,
270
+ meta : string
271
+ ) : Array < {
272
+ step : number ;
273
+ line : number ;
274
+ startColumn : number ;
275
+ endColumn : number ;
276
+ className : string ;
277
+ } > {
278
+ if ( ! meta ) {
279
+ return [ ] ;
280
+ }
281
+ const inlineHighlightLines = getInlineHighlights ( meta , code ) ;
282
+ const inlineHighlightConfig = inlineHighlightLines . map (
283
+ ( line : InlineHiglight ) => ( {
284
+ ...line ,
285
+ elementAttributes : { 'data-step' : `${ line . step } ` } ,
286
+ className : cn (
287
+ 'code-step bg-opacity-10 dark:bg-opacity-20 relative rounded px-1 py-[1.5px] border-b-[2px] border-opacity-60' ,
288
+ {
289
+ 'bg-blue-40 border-blue-40 text-blue-60 dark:text-blue-30 font-bold' :
290
+ line . step === 1 ,
291
+ 'bg-yellow-40 border-yellow-40 text-yellow-60 dark:text-yellow-30 font-bold' :
292
+ line . step === 2 ,
293
+ 'bg-purple-40 border-purple-40 text-purple-60 dark:text-purple-30 font-bold' :
294
+ line . step === 3 ,
295
+ 'bg-green-40 border-green-40 text-green-60 dark:text-green-30 font-bold' :
296
+ line . step === 4 ,
297
+ }
298
+ ) ,
299
+ } )
300
+ ) ;
301
+ return inlineHighlightConfig ;
302
+ }
303
+
114
304
/**
115
305
*
116
306
* @param meta string provided after the language in a markdown block
0 commit comments