From dbf245d5261cd17694ce3f2b94f48f4ec96f3f38 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 25 Sep 2022 17:33:34 +0100 Subject: [PATCH 1/4] [Beta] Highlight code blocks at build --- beta/src/components/MDX/CodeBlock/index.tsx | 52 ++--- beta/src/pages/[[...markdownPath]].js | 6 +- beta/src/utils/prepareMDX.js | 99 --------- .../CodeBlock.tsx => utils/prepareMDX.tsx} | 198 ++++++++++++++---- beta/tailwind.config.js | 1 + 5 files changed, 188 insertions(+), 168 deletions(-) delete mode 100644 beta/src/utils/prepareMDX.js rename beta/src/{components/MDX/CodeBlock/CodeBlock.tsx => utils/prepareMDX.tsx} (67%) diff --git a/beta/src/components/MDX/CodeBlock/index.tsx b/beta/src/components/MDX/CodeBlock/index.tsx index e449e6b9ef4..30029aff61b 100644 --- a/beta/src/components/MDX/CodeBlock/index.tsx +++ b/beta/src/components/MDX/CodeBlock/index.tsx @@ -2,38 +2,40 @@ * Copyright (c) Facebook, Inc. and its affiliates. */ -import * as React from 'react'; import cn from 'classnames'; -import {lazy, memo, Suspense} from 'react'; -const CodeBlock = lazy(() => import('./CodeBlock')); -export default memo(function CodeBlockWrapper(props: { +const CodeBlock = function CodeBlock({ + children: { + props: {highlightedCode}, + }, + noMargin, +}: { children: React.ReactNode & { props: { - className: string; - children: string; - meta?: string; + highlightedCode: string; }; }; - isFromPackageImport: boolean; + className?: string; noMargin?: boolean; - noMarkers?: boolean; -}): any { - const {children, isFromPackageImport} = props; +}) { return ( - -
-

{children}

+
+
+
+
+
+              {highlightedCode}
+            
- - }> - - +
+
+
); -}); +}; + +export default CodeBlock; diff --git a/beta/src/pages/[[...markdownPath]].js b/beta/src/pages/[[...markdownPath]].js index 839a61c7804..49cc57ea33b 100644 --- a/beta/src/pages/[[...markdownPath]].js +++ b/beta/src/pages/[[...markdownPath]].js @@ -58,13 +58,11 @@ function reviveNodeOnClient(key, val) { const DISK_CACHE_BREAKER = 4; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +import {prepareMDX, PREPARE_MDX_CACHE_BREAKER} from '../utils/prepareMDX'; + // Put MDX output into JSON for client. export async function getStaticProps(context) { const fs = require('fs'); - const { - prepareMDX, - PREPARE_MDX_CACHE_BREAKER, - } = require('../utils/prepareMDX'); const rootDir = process.cwd() + '/src/content/'; const mdxComponentNames = Object.keys(MDXComponents); diff --git a/beta/src/utils/prepareMDX.js b/beta/src/utils/prepareMDX.js deleted file mode 100644 index 5dd15d62284..00000000000 --- a/beta/src/utils/prepareMDX.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) Facebook, Inc. and its affiliates. - */ - -import {Children} from 'react'; - -// TODO: This logic could be in MDX plugins instead. - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -export const PREPARE_MDX_CACHE_BREAKER = 2; -// !!! IMPORTANT !!! Bump this if you change any logic. -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -export function prepareMDX(rawChildren) { - const toc = getTableOfContents(rawChildren); - const children = wrapChildrenInMaxWidthContainers(rawChildren); - return {toc, children}; -} - -function wrapChildrenInMaxWidthContainers(children) { - // Auto-wrap everything except a few types into - // wrappers. Keep reusing the same - // wrapper as long as we can until we meet - // a full-width section which interrupts it. - let fullWidthTypes = [ - 'Sandpack', - 'FullWidth', - 'Illustration', - 'IllustrationBlock', - 'Challenges', - 'Recipes', - ]; - let wrapQueue = []; - let finalChildren = []; - function flushWrapper(key) { - if (wrapQueue.length > 0) { - const Wrapper = 'MaxWidth'; - finalChildren.push({wrapQueue}); - wrapQueue = []; - } - } - function handleChild(child, key) { - if (child == null) { - return; - } - if (typeof child !== 'object') { - wrapQueue.push(child); - return; - } - if (fullWidthTypes.includes(child.type)) { - flushWrapper(key); - finalChildren.push(child); - } else { - wrapQueue.push(child); - } - } - Children.forEach(children, handleChild); - flushWrapper('last'); - return finalChildren; -} - -function getTableOfContents(children) { - const anchors = Children.toArray(children) - .filter((child) => { - if (child.type) { - return ['h1', 'h2', 'h3', 'Challenges', 'Recap'].includes(child.type); - } - return false; - }) - .map((child) => { - if (child.type === 'Challenges') { - return { - url: '#challenges', - depth: 2, - text: 'Challenges', - }; - } - if (child.type === 'Recap') { - return { - url: '#recap', - depth: 2, - text: 'Recap', - }; - } - return { - url: '#' + child.props.id, - depth: (child.type && parseInt(child.type.replace('h', ''), 0)) ?? 0, - text: child.props.children, - }; - }); - if (anchors.length > 0) { - anchors.unshift({ - url: '#', - text: 'Overview', - depth: 2, - }); - } - return anchors; -} diff --git a/beta/src/components/MDX/CodeBlock/CodeBlock.tsx b/beta/src/utils/prepareMDX.tsx similarity index 67% rename from beta/src/components/MDX/CodeBlock/CodeBlock.tsx rename to beta/src/utils/prepareMDX.tsx index ec2058b47ba..02cc9ed1b45 100644 --- a/beta/src/components/MDX/CodeBlock/CodeBlock.tsx +++ b/beta/src/utils/prepareMDX.tsx @@ -2,12 +2,162 @@ * Copyright (c) Facebook, Inc. and its affiliates. */ +import * as React from 'react'; +import {Children, isValidElement, cloneElement} from 'react'; import cn from 'classnames'; 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'; +import {CustomTheme} from 'components/MDX/Sandpack/Themes'; + +if (typeof window !== 'undefined') { + document.body.innerHTML = ''; + throw Error('This should never run on the client.'); +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +export const PREPARE_MDX_CACHE_BREAKER = 3; +// !!! IMPORTANT !!! Bump this if you change any logic. +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +export function prepareMDX(rawChildren: any): { + toc: Array<{ + url: string; + text: string; + depth: number; + }>; + children: any; +} { + const toc = getTableOfContents(rawChildren); + const children = processAndWrapInMaxWidthContainers(rawChildren); + return {toc, children}; +} + +function processAndWrapInMaxWidthContainers(children: any): any { + // Auto-wrap everything except a few types into + // wrappers. Keep reusing the same + // wrapper as long as we can until we meet + // a full-width section which interrupts it. + let fullWidthTypes = [ + 'Sandpack', + 'FullWidth', + 'Illustration', + 'IllustrationBlock', + 'Challenges', + 'Recipes', + ]; + let wrapQueue: any[] = []; + let finalChildren: any[] = []; + function flushWrapper(key: number | string) { + if (wrapQueue.length > 0) { + const Wrapper = 'MaxWidth'; + // @ts-ignore + finalChildren.push({wrapQueue}); + wrapQueue = []; + } + } + function handleChild(child: any, key: number | string) { + if (child == null) { + return; + } + if (typeof child !== 'object') { + wrapQueue.push(child); + return; + } + if (fullWidthTypes.includes((child as any).type)) { + flushWrapper(key); + finalChildren.push(highlightCodeBlocksRecursively(child, [])); + } else { + wrapQueue.push(highlightCodeBlocksRecursively(child, [])); + } + } + Children.forEach(children, handleChild); + flushWrapper('last'); + return finalChildren; +} + +function highlightCodeBlocksRecursively( + child: any, + parentPath: Array +): any { + if (!isValidElement(child)) { + return child; + } + const props: any = child.props; + const newParentPath = [...parentPath, child.type as string]; + const overrideProps: any = { + children: Array.isArray(props.children) + ? Children.map(props.children, (grandchild: any) => + highlightCodeBlocksRecursively(grandchild, newParentPath) + ) + : highlightCodeBlocksRecursively(props.children, newParentPath), + }; + if ( + child.type === 'code' && + // @ts-ignore + parentPath.at(-1) === 'pre' && // Don't highlight inline text + // @ts-ignore + parentPath.at(-2) !== 'Sandpack' // Interactive snippets highlight on the client + ) { + overrideProps.highlightedCode = prepareCodeBlockChildren( + props.children, + props.meta + ); + } + return cloneElement(child, overrideProps); +} + +// -------------------------------------------------------- +// TOC +// -------------------------------------------------------- + +function getTableOfContents(children: any): Array<{ + url: string; + text: string; + depth: number; +}> { + const anchors = Children.toArray(children) + .filter((child: any) => { + if (child.type) { + return ['h1', 'h2', 'h3', 'Challenges', 'Recap'].includes(child.type); + } + return false; + }) + .map((child: any) => { + if (child.type === 'Challenges') { + return { + url: '#challenges', + depth: 2, + text: 'Challenges', + }; + } + if (child.type === 'Recap') { + return { + url: '#recap', + depth: 2, + text: 'Recap', + }; + } + return { + url: '#' + child.props.id, + depth: (child.type && parseInt(child.type.replace('h', ''), 0)) ?? 0, + text: child.props.children, + }; + }); + if (anchors.length > 0) { + anchors.unshift({ + url: '#', + text: 'Overview', + depth: 2, + }); + } + return anchors; +} + +// -------------------------------------------------------- +// CodeBlock +// -------------------------------------------------------- interface InlineHiglight { step: number; @@ -16,23 +166,11 @@ interface InlineHiglight { endColumn: number; } -const CodeBlock = function CodeBlock({ - children: { - props: {className = 'language-js', children: code = '', meta}, - }, - noMargin, -}: { - children: React.ReactNode & { - props: { - className: string; - children?: string; - meta?: string; - }; - }; - className?: string; - noMargin?: boolean; -}) { - code = code.trimEnd(); +function prepareCodeBlockChildren( + rawChildren: string = '', + meta: string +): React.ReactNode { + const code = rawChildren.trimEnd(); const tree = language.language.parser.parse(code); let tokenStarts = new Map(); let tokenEnds = new Map(); @@ -165,28 +303,8 @@ const CodeBlock = function CodeBlock({ {lineOutput}
); - - return ( -
-
-
-
-
-              {finalOutput}
-            
-
-
-
-
- ); -}; - -export default CodeBlock; + return finalOutput; +} const language = javascript({jsx: true, typescript: false}); diff --git a/beta/tailwind.config.js b/beta/tailwind.config.js index bcedbe06497..d77635a45b8 100644 --- a/beta/tailwind.config.js +++ b/beta/tailwind.config.js @@ -10,6 +10,7 @@ module.exports = { './src/components/**/*.{js,ts,jsx,tsx}', './src/pages/**/*.{js,ts,jsx,tsx}', './src/styles/**/*.{js,ts,jsx,tsx}', + './src/utils/**/*.{js,ts,jsx,tsx}', ], darkMode: 'class', theme: { From df30449e32800dcb1875369c9778f01919c747f8 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 25 Sep 2022 17:54:33 +0100 Subject: [PATCH 2/4] Fix disappearing CSS --- beta/src/components/MDX/CodeBlock/index.tsx | 4 +++- beta/src/pages/[[...markdownPath]].js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/beta/src/components/MDX/CodeBlock/index.tsx b/beta/src/components/MDX/CodeBlock/index.tsx index 30029aff61b..8621a5c36ec 100644 --- a/beta/src/components/MDX/CodeBlock/index.tsx +++ b/beta/src/components/MDX/CodeBlock/index.tsx @@ -29,7 +29,9 @@ const CodeBlock = function CodeBlock({
-              {highlightedCode}
+              
+                {highlightedCode}
+              
             
diff --git a/beta/src/pages/[[...markdownPath]].js b/beta/src/pages/[[...markdownPath]].js index 49cc57ea33b..7e408847863 100644 --- a/beta/src/pages/[[...markdownPath]].js +++ b/beta/src/pages/[[...markdownPath]].js @@ -55,7 +55,7 @@ function reviveNodeOnClient(key, val) { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~ IMPORTANT: BUMP THIS IF YOU CHANGE ANY CODE BELOW ~~~ -const DISK_CACHE_BREAKER = 4; +const DISK_CACHE_BREAKER = 5; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import {prepareMDX, PREPARE_MDX_CACHE_BREAKER} from '../utils/prepareMDX'; From 0277ffe5fb3fa1b3621742a382522cc42309fc6b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 25 Sep 2022 17:56:34 +0100 Subject: [PATCH 3/4] Fix build --- beta/src/utils/prepareMDX.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/beta/src/utils/prepareMDX.tsx b/beta/src/utils/prepareMDX.tsx index 02cc9ed1b45..6e235c55c83 100644 --- a/beta/src/utils/prepareMDX.tsx +++ b/beta/src/utils/prepareMDX.tsx @@ -17,7 +17,7 @@ if (typeof window !== 'undefined') { } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -export const PREPARE_MDX_CACHE_BREAKER = 3; +export const PREPARE_MDX_CACHE_BREAKER = 4; // !!! IMPORTANT !!! Bump this if you change any logic. // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -95,10 +95,8 @@ function highlightCodeBlocksRecursively( }; if ( child.type === 'code' && - // @ts-ignore - parentPath.at(-1) === 'pre' && // Don't highlight inline text - // @ts-ignore - parentPath.at(-2) !== 'Sandpack' // Interactive snippets highlight on the client + parentPath[parentPath.length - 1] === 'pre' && // Don't highlight inline text + parentPath[parentPath.length - 2] !== 'Sandpack' // Interactive snippets highlight on the client ) { overrideProps.highlightedCode = prepareCodeBlockChildren( props.children, From b7dd85663d191094373691a559d30339ebd50c14 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 25 Sep 2022 18:07:45 +0100 Subject: [PATCH 4/4] Optimize output --- beta/src/styles/sandpack.css | 4 +++- beta/src/utils/prepareMDX.tsx | 44 ++++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/beta/src/styles/sandpack.css b/beta/src/styles/sandpack.css index b4e1ea02e8d..454774b4f4a 100644 --- a/beta/src/styles/sandpack.css +++ b/beta/src/styles/sandpack.css @@ -233,10 +233,12 @@ html.dark .sandpack--playground .sp-overlay { padding: 0; } -.sandpack--codeblock .cm-line { +.sandpack--codeblock .hl-line { margin-left: -20px; padding-left: 20px; padding-right: 20px; + @apply bg-github-highlight; + @apply dark:bg-opacity-10; } /** diff --git a/beta/src/utils/prepareMDX.tsx b/beta/src/utils/prepareMDX.tsx index 6e235c55c83..b6329a58f63 100644 --- a/beta/src/utils/prepareMDX.tsx +++ b/beta/src/utils/prepareMDX.tsx @@ -17,7 +17,7 @@ if (typeof window !== 'undefined') { } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -export const PREPARE_MDX_CACHE_BREAKER = 4; +export const PREPARE_MDX_CACHE_BREAKER = 5; // !!! IMPORTANT !!! Bump this if you change any logic. // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -98,6 +98,7 @@ function highlightCodeBlocksRecursively( parentPath[parentPath.length - 1] === 'pre' && // Don't highlight inline text parentPath[parentPath.length - 2] !== 'Sandpack' // Interactive snippets highlight on the client ) { + overrideProps.children = undefined; overrideProps.highlightedCode = prepareCodeBlockChildren( props.children, props.meta @@ -217,7 +218,7 @@ function prepareCodeBlockChildren( let buffer = ''; let lineIndex = 0; let lineOutput = []; - let finalOutput = []; + let finalOutput: Array = []; for (let i = 0; i < code.length; i++) { if (tokenEnds.has(i)) { if (!currentToken) { @@ -277,16 +278,19 @@ function prepareCodeBlockChildren( } } if (code[i] === '\n') { - lineOutput.push(buffer); + lineOutput.push(buffer,
); buffer = ''; - finalOutput.push( -
- {lineOutput} -
-
- ); + if (highlightedLines.has(lineIndex)) { + finalOutput.push( +
+ {lineOutput} +
+ ); + } else { + finalOutput = [...finalOutput, ...lineOutput]; + } lineOutput = []; lineIndex++; } else { @@ -294,13 +298,15 @@ function prepareCodeBlockChildren( } } lineOutput.push(buffer); - finalOutput.push( -
- {lineOutput} -
- ); + if (highlightedLines.has(lineIndex)) { + finalOutput.push( +
+ {lineOutput} +
+ ); + } else { + finalOutput = [...finalOutput, ...lineOutput]; + } return finalOutput; } @@ -371,7 +377,7 @@ function getLineDecorators( const linesToHighlight = getHighlightLines(meta); const highlightedLineConfig = linesToHighlight.map((line) => { return { - className: 'bg-github-highlight dark:bg-opacity-10', + className: 'hl-line', line, }; });