Skip to content

Commit 631b5c3

Browse files
committed
[Beta] Fully SSR CodeBlock
1 parent e166848 commit 631b5c3

File tree

2 files changed

+260
-68
lines changed

2 files changed

+260
-68
lines changed

beta/src/components/MDX/CodeBlock/CodeBlock.tsx

Lines changed: 256 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
*/
44

55
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';
109
import rangeParser from 'parse-numeric-range';
1110
import {CustomTheme} from '../Sandpack/Themes';
1211

@@ -33,84 +32,275 @@ const CodeBlock = function CodeBlock({
3332
className?: string;
3433
noMargin?: boolean;
3534
}) {
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);
3970
}
71+
decoratorEnds.set(decoratorEnd, decorator.className);
72+
}
4073

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+
);
72168

73-
// e.g. "language-js"
74-
const language = className.substring(9);
75-
const filename = '/index.' + language;
76-
const decorators = getDecoratedLineInfo();
77169
return (
78170
<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-
}
84171
className={cn(
85172
'sandpack sandpack--codeblock',
86173
'rounded-lg h-full w-full overflow-x-auto flex items-center bg-wash dark:bg-gray-95 shadow-lg',
87174
!noMargin && 'my-8'
88175
)}>
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>
108188
</div>
109189
);
110190
};
111191

112192
export default CodeBlock;
113193

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+
114304
/**
115305
*
116306
* @param meta string provided after the language in a markdown block

beta/src/styles/sandpack.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,9 @@ html.dark .sandpack--playground .sp-overlay {
227227
padding: 0;
228228
}
229229

230-
.sandpack--codeblock .cm-content.cm-readonly .cm-line {
231-
padding: 0 var(--sp-space-3);
230+
.sandpack--codeblock .cm-line {
231+
margin-left: -20px;
232+
padding-left: 20px;
232233
}
233234

234235
/**
@@ -239,6 +240,7 @@ html.dark .sandpack--playground .sp-overlay {
239240
font-size: 13.6px;
240241
line-height: 24px;
241242
padding: 18px 0;
243+
-webkit-font-smoothing: auto;
242244
}
243245

244246
.sandpack--codeblock .sp-code-editor .sp-pre-placeholder {

0 commit comments

Comments
 (0)