diff --git a/beta/src/components/MDX/CodeBlock/index.tsx b/beta/src/components/MDX/CodeBlock/index.tsx index e449e6b9ef4..8621a5c36ec 100644 --- a/beta/src/components/MDX/CodeBlock/index.tsx +++ b/beta/src/components/MDX/CodeBlock/index.tsx @@ -2,38 +2,42 @@ * 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..7e408847863 100644 --- a/beta/src/pages/[[...markdownPath]].js +++ b/beta/src/pages/[[...markdownPath]].js @@ -55,16 +55,14 @@ 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'; + // 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/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.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 63% rename from beta/src/components/MDX/CodeBlock/CodeBlock.tsx rename to beta/src/utils/prepareMDX.tsx index ec2058b47ba..b6329a58f63 100644 --- a/beta/src/components/MDX/CodeBlock/CodeBlock.tsx +++ b/beta/src/utils/prepareMDX.tsx @@ -2,12 +2,161 @@ * 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 = 5; +// !!! 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' && + 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 + ); + } + 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 +165,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(); @@ -81,7 +218,7 @@ const CodeBlock = function CodeBlock({ 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) { @@ -141,16 +278,19 @@ const CodeBlock = function CodeBlock({ } } 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 { @@ -158,35 +298,17 @@ const CodeBlock = function CodeBlock({ } } lineOutput.push(buffer); - finalOutput.push( -
- {lineOutput} -
- ); - - return ( -
-
-
-
-
-              {finalOutput}
-            
-
-
+ if (highlightedLines.has(lineIndex)) { + finalOutput.push( +
+ {lineOutput}
-
- ); -}; - -export default CodeBlock; + ); + } else { + finalOutput = [...finalOutput, ...lineOutput]; + } + return finalOutput; +} const language = javascript({jsx: true, typescript: false}); @@ -255,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, }; }); 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: {