Browse Source

[Beta] Fully SSR CodeBlock (#5110)

main
dan 2 years ago
committed by GitHub
parent
commit
6b4fe35703
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 322
      beta/src/components/MDX/CodeBlock/CodeBlock.tsx
  2. 6
      beta/src/styles/sandpack.css

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

@ -3,10 +3,9 @@
*/
import cn from 'classnames';
import {
SandpackCodeViewer,
SandpackProvider,
} from '@codesandbox/sandpack-react';
import {highlightTree} from '@codemirror/highlight';
import {javascript} from '@codemirror/lang-javascript';
import {HighlightStyle, tags} from '@codemirror/highlight';
import rangeParser from 'parse-numeric-range';
import {CustomTheme} from '../Sandpack/Themes';
@ -33,84 +32,275 @@ const CodeBlock = function CodeBlock({
className?: string;
noMargin?: boolean;
}) {
const getDecoratedLineInfo = () => {
if (!meta) {
return [];
code = code.trimEnd();
const tree = language.language.parser.parse(code);
let tokenStarts = new Map();
let tokenEnds = new Map();
const highlightTheme = getSyntaxHighlight(CustomTheme);
highlightTree(tree, highlightTheme.match, (from, to, className) => {
tokenStarts.set(from, className);
tokenEnds.set(to, className);
});
const highlightedLines = new Map();
const lines = code.split('\n');
const lineDecorators = getLineDecorators(code, meta);
for (let decorator of lineDecorators) {
highlightedLines.set(decorator.line - 1, decorator.className);
}
const inlineDecorators = getInlineDecorators(code, meta);
const decoratorStarts = new Map();
const decoratorEnds = new Map();
for (let decorator of inlineDecorators) {
// Find where inline highlight starts and ends.
let decoratorStart = 0;
for (let i = 0; i < decorator.line - 1; i++) {
decoratorStart += lines[i].length + 1;
}
decoratorStart += decorator.startColumn;
const decoratorEnd =
decoratorStart + (decorator.endColumn - decorator.startColumn);
if (decoratorStarts.has(decoratorStart)) {
throw Error('Already opened decorator at ' + decoratorStart);
}
decoratorStarts.set(decoratorStart, decorator.className);
if (decoratorEnds.has(decoratorEnd)) {
throw Error('Already closed decorator at ' + decoratorEnd);
}
decoratorEnds.set(decoratorEnd, decorator.className);
}
const linesToHighlight = getHighlightLines(meta);
const highlightedLineConfig = linesToHighlight.map((line) => {
return {
className: 'bg-github-highlight dark:bg-opacity-10',
line,
};
});
const inlineHighlightLines = getInlineHighlights(meta, code);
const inlineHighlightConfig = inlineHighlightLines.map(
(line: InlineHiglight) => ({
...line,
elementAttributes: {'data-step': `${line.step}`},
className: cn(
'code-step bg-opacity-10 dark:bg-opacity-20 relative rounded px-1 py-[1.5px] border-b-[2px] border-opacity-60',
{
'bg-blue-40 border-blue-40 text-blue-60 dark:text-blue-30 font-bold':
line.step === 1,
'bg-yellow-40 border-yellow-40 text-yellow-60 dark:text-yellow-30 font-bold':
line.step === 2,
'bg-purple-40 border-purple-40 text-purple-60 dark:text-purple-30 font-bold':
line.step === 3,
'bg-green-40 border-green-40 text-green-60 dark:text-green-30 font-bold':
line.step === 4,
}
),
})
);
return highlightedLineConfig.concat(inlineHighlightConfig);
};
// Produce output based on tokens and decorators.
// We assume tokens never overlap other tokens, and
// decorators never overlap with other decorators.
// However, tokens and decorators may mutually overlap.
// In that case, decorators always take precedence.
let currentDecorator = null;
let currentToken = null;
let buffer = '';
let lineIndex = 0;
let lineOutput = [];
let finalOutput = [];
for (let i = 0; i < code.length; i++) {
if (tokenEnds.has(i)) {
if (!currentToken) {
throw Error('Cannot close token at ' + i + ' because it was not open.');
}
if (!currentDecorator) {
lineOutput.push(
<span key={i + '/t'} className={currentToken}>
{buffer}
</span>
);
buffer = '';
}
currentToken = null;
}
if (decoratorEnds.has(i)) {
if (!currentDecorator) {
throw Error(
'Cannot close decorator at ' + i + ' because it was not open.'
);
}
lineOutput.push(
<span key={i + '/d'} className={currentDecorator}>
{buffer}
</span>
);
buffer = '';
currentDecorator = null;
}
if (decoratorStarts.has(i)) {
if (currentDecorator) {
throw Error(
'Cannot open decorator at ' + i + ' before closing last one.'
);
}
if (currentToken) {
lineOutput.push(
<span key={i + 'd'} className={currentToken}>
{buffer}
</span>
);
buffer = '';
} else {
lineOutput.push(buffer);
buffer = '';
}
currentDecorator = decoratorStarts.get(i);
}
if (tokenStarts.has(i)) {
if (currentToken) {
throw Error('Cannot open token at ' + i + ' before closing last one.');
}
currentToken = tokenStarts.get(i);
if (!currentDecorator) {
lineOutput.push(buffer);
buffer = '';
}
}
if (code[i] === '\n') {
lineOutput.push(buffer);
buffer = '';
finalOutput.push(
<div
key={lineIndex}
className={'cm-line ' + highlightedLines.get(lineIndex)}>
{lineOutput}
<br />
</div>
);
lineOutput = [];
lineIndex++;
} else {
buffer += code[i];
}
}
lineOutput.push(buffer);
finalOutput.push(
<div
key={lineIndex}
className={'cm-line ' + highlightedLines.get(lineIndex)}>
{lineOutput}
</div>
);
// e.g. "language-js"
const language = className.substring(9);
const filename = '/index.' + language;
const decorators = getDecoratedLineInfo();
return (
<div
key={
// HACK: There seems to be a bug where the rendered result
// "lags behind" the edits to it. For now, force it to reset.
process.env.NODE_ENV === 'development' ? code : ''
}
className={cn(
'sandpack sandpack--codeblock',
'rounded-lg h-full w-full overflow-x-auto flex items-center bg-wash dark:bg-gray-95 shadow-lg',
!noMargin && 'my-8'
)}>
<SandpackProvider
files={{
[filename]: {
code: code.trimEnd(),
},
}}
customSetup={{
entry: filename,
}}
options={{
initMode: 'immediate',
}}
theme={CustomTheme}>
<SandpackCodeViewer
key={code.trimEnd()}
showLineNumbers={false}
decorators={decorators}
/>
</SandpackProvider>
{/* These classes are fragile and depend on Sandpack. TODO: some better way. */}
<div className="sp-wrapper sp-121717251 sp-c-fVPbOs sp-c-fVPbOs-LrWkf-variant-dark">
<div className="sp-stack sp-c-kLppIp">
<div className="sp-code-editor sp-c-bNbSGz">
<pre className="sp-cm sp-pristine sp-javascript sp-c-jcgexo sp-c-jkvvao">
<code className="sp-pre-placeholder sp-c-fWymNx">
{finalOutput}
</code>
</pre>
</div>
</div>
</div>
</div>
);
};
export default CodeBlock;
const language = javascript({jsx: true, typescript: false});
function classNameToken(name: string): string {
return `sp-syntax-${name}`;
}
function getSyntaxHighlight(theme: any): HighlightStyle {
return HighlightStyle.define([
{tag: tags.link, textdecorator: 'underline'},
{tag: tags.emphasis, fontStyle: 'italic'},
{tag: tags.strong, fontWeight: 'bold'},
{
tag: tags.keyword,
class: classNameToken('keyword'),
},
{
tag: [tags.atom, tags.number, tags.bool],
class: classNameToken('static'),
},
{
tag: tags.tagName,
class: classNameToken('tag'),
},
{tag: tags.variableName, class: classNameToken('plain')},
{
// Highlight function call
tag: tags.function(tags.variableName),
class: classNameToken('definition'),
},
{
// Highlight function definition differently (eg: functional component def in React)
tag: tags.definition(tags.function(tags.variableName)),
class: classNameToken('definition'),
},
{
tag: tags.propertyName,
class: classNameToken('property'),
},
{
tag: [tags.literal, tags.inserted],
class: classNameToken(theme.syntax.string ? 'string' : 'static'),
},
{
tag: tags.punctuation,
class: classNameToken('punctuation'),
},
{
tag: [tags.comment, tags.quote],
class: classNameToken('comment'),
},
]);
}
function getLineDecorators(
code: string,
meta: string
): Array<{
line: number;
className: string;
}> {
if (!meta) {
return [];
}
const linesToHighlight = getHighlightLines(meta);
const highlightedLineConfig = linesToHighlight.map((line) => {
return {
className: 'bg-github-highlight dark:bg-opacity-10',
line,
};
});
return highlightedLineConfig;
}
function getInlineDecorators(
code: string,
meta: string
): Array<{
step: number;
line: number;
startColumn: number;
endColumn: number;
className: string;
}> {
if (!meta) {
return [];
}
const inlineHighlightLines = getInlineHighlights(meta, code);
const inlineHighlightConfig = inlineHighlightLines.map(
(line: InlineHiglight) => ({
...line,
elementAttributes: {'data-step': `${line.step}`},
className: cn(
'code-step bg-opacity-10 dark:bg-opacity-20 relative rounded px-1 py-[1.5px] border-b-[2px] border-opacity-60',
{
'bg-blue-40 border-blue-40 text-blue-60 dark:text-blue-30 font-bold':
line.step === 1,
'bg-yellow-40 border-yellow-40 text-yellow-60 dark:text-yellow-30 font-bold':
line.step === 2,
'bg-purple-40 border-purple-40 text-purple-60 dark:text-purple-30 font-bold':
line.step === 3,
'bg-green-40 border-green-40 text-green-60 dark:text-green-30 font-bold':
line.step === 4,
}
),
})
);
return inlineHighlightConfig;
}
/**
*
* @param meta string provided after the language in a markdown block

6
beta/src/styles/sandpack.css

@ -227,8 +227,9 @@ html.dark .sandpack--playground .sp-overlay {
padding: 0;
}
.sandpack--codeblock .cm-content.cm-readonly .cm-line {
padding: 0 var(--sp-space-3);
.sandpack--codeblock .cm-line {
margin-left: -20px;
padding-left: 20px;
}
/**
@ -239,6 +240,7 @@ html.dark .sandpack--playground .sp-overlay {
font-size: 13.6px;
line-height: 24px;
padding: 18px 0;
-webkit-font-smoothing: auto;
}
.sandpack--codeblock .sp-code-editor .sp-pre-placeholder {

Loading…
Cancel
Save