diff --git a/beta/src/components/MDX/CodeBlock/CodeBlock.tsx b/beta/src/components/MDX/CodeBlock/CodeBlock.tsx index e9ba2452..0b44734a 100644 --- a/beta/src/components/MDX/CodeBlock/CodeBlock.tsx +++ b/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( + + {buffer} + + ); + buffer = ''; + } + currentToken = null; + } + if (decoratorEnds.has(i)) { + if (!currentDecorator) { + throw Error( + 'Cannot close decorator at ' + i + ' because it was not open.' + ); + } + lineOutput.push( + + {buffer} + + ); + buffer = ''; + currentDecorator = null; + } + if (decoratorStarts.has(i)) { + if (currentDecorator) { + throw Error( + 'Cannot open decorator at ' + i + ' before closing last one.' + ); + } + if (currentToken) { + lineOutput.push( + + {buffer} + + ); + 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( +
+ {lineOutput} +
+
+ ); + lineOutput = []; + lineIndex++; + } else { + buffer += code[i]; + } + } + lineOutput.push(buffer); + finalOutput.push( +
+ {lineOutput} +
+ ); - // e.g. "language-js" - const language = className.substring(9); - const filename = '/index.' + language; - const decorators = getDecoratedLineInfo(); return (
- - - + {/* These classes are fragile and depend on Sandpack. TODO: some better way. */} +
+
+
+
+              
+                {finalOutput}
+              
+            
+
+
+
); }; 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 diff --git a/beta/src/styles/sandpack.css b/beta/src/styles/sandpack.css index 32013601..e1ca99e3 100644 --- a/beta/src/styles/sandpack.css +++ b/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 {