Skip to content

Commit

Permalink
[docs] Add copy button to code block (#32390)
Browse files Browse the repository at this point in the history
  • Loading branch information
siriwatknp authored Apr 27, 2022
1 parent 6eabaca commit a3a4bef
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 25 deletions.
47 changes: 47 additions & 0 deletions docs/packages/markdown/parseMarkdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,34 @@ const descriptionRegExp = /<p class="description">(.*?)<\/p>/s;
const headerKeyValueRegExp = /(.*?):[\r\n]?\s+(\[[^\]]+\]|.*)/g;
const emptyRegExp = /^\s*$/;

/**
* Same as https://github.com/markedjs/marked/blob/master/src/helpers.js
* Need to duplicate because `marked` does not export `escape` function
*/
const escapeTest = /[&<>"']/;
const escapeReplace = /[&<>"']/g;
const escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/;
const escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g;
const escapeReplacements = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
};
const getEscapeReplacement = (ch) => escapeReplacements[ch];
function escape(html, encode) {
if (encode) {
if (escapeTest.test(html)) {
return html.replace(escapeReplace, getEscapeReplacement);
}
} else if (escapeTestNoEncode.test(html)) {
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
}

return html;
}

/**
* Extract information from the top of the markdown.
* For instance, the following input:
Expand Down Expand Up @@ -227,6 +255,25 @@ function createRender(context) {

return `<a href="${finalHref}"${more}>${linkText}</a>`;
};
renderer.code = (code, infostring, escaped) => {
// https://github.com/markedjs/marked/blob/30e90e5175700890e6feb1836c57b9404c854466/src/Renderer.js#L15
const lang = (infostring || '').match(/\S*/)[0];
const out = prism(code, lang);
if (out != null && out !== code) {
escaped = true;
code = out;
}

code = `${code.replace(/\n$/, '')}\n`;

if (!lang) {
return `<pre><code>${escaped ? code : escape(code, true)}</code></pre>\n`;
}

return `<div class="MuiCode-root"><pre><code class="language-${escape(lang, true)}">${
escaped ? code : escape(code, true)
}</code></pre><button data-ga-event-category="code" data-ga-event-action="copy-click" aria-label="Copy the code" class="MuiCode-copy">Copy <span class="MuiCode-copyKeypress"><span>or</span> $key + C</span></button></div>\n`;
};

const markedOptions = {
gfm: true,
Expand Down
23 changes: 13 additions & 10 deletions docs/pages/_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import joyPages from 'docs/data/joy/pages';
import systemPages from 'docs/data/system/pages';
import PageContext from 'docs/src/modules/components/PageContext';
import GoogleAnalytics from 'docs/src/modules/components/GoogleAnalytics';
import { CodeCopyProvider } from 'docs/src/modules/utils/CodeCopy';
import { ThemeProvider } from 'docs/src/modules/components/ThemeContext';
import { pathnameToLanguage, getCookie } from 'docs/src/modules/utils/helpers';
import { LANGUAGES } from 'docs/src/modules/constants';
Expand Down Expand Up @@ -218,16 +219,18 @@ function AppWrapper(props) {
</NextHead>
<UserLanguageProvider defaultUserLanguage={pageProps.userLanguage}>
<LanguageNegotiation />
<CodeVariantProvider>
<PageContext.Provider value={{ activePage, pages: productPages }}>
<ThemeProvider>
<DocsStyledEngineProvider cacheLtr={emotionCache}>
{children}
<GoogleAnalytics />
</DocsStyledEngineProvider>
</ThemeProvider>
</PageContext.Provider>
</CodeVariantProvider>
<CodeCopyProvider>
<CodeVariantProvider>
<PageContext.Provider value={{ activePage, pages: productPages }}>
<ThemeProvider>
<DocsStyledEngineProvider cacheLtr={emotionCache}>
{children}
<GoogleAnalytics />
</DocsStyledEngineProvider>
</ThemeProvider>
</PageContext.Provider>
</CodeVariantProvider>
</CodeCopyProvider>
</UserLanguageProvider>
</React.Fragment>
);
Expand Down
17 changes: 10 additions & 7 deletions docs/src/modules/components/Demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,16 @@ export default function Demo(props) {
</NoSsr>
)}
<Collapse in={openDemoSource} unmountOnExit>
<div>
<Code
id={demoSourceId}
code={showPreview && !codeOpen ? demo.jsxPreview : demoData.raw}
language={demoData.sourceLanguage}
/>
</div>
<Code
id={demoSourceId}
code={showPreview && !codeOpen ? demo.jsxPreview : demoData.raw}
language={demoData.sourceLanguage}
copyButtonProps={{
'data-ga-event-category': codeOpen ? 'demo-expand' : 'demo',
'data-ga-event-label': demoOptions.demo,
'data-ga-event-action': 'copy-click',
}}
/>
</Collapse>
{showAd && !disableAd && !demoOptions.disableAd ? <AdCarbonInline /> : null}
</Root>
Expand Down
65 changes: 57 additions & 8 deletions docs/src/modules/components/HighlightedCode.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,79 @@
/* eslint-disable material-ui/no-hardcoded-labels */
import * as React from 'react';
import PropTypes from 'prop-types';
import copy from 'clipboard-copy';
import prism from '@mui/markdown/prism';
import MarkdownElement from 'docs/src/modules/components/MarkdownElement';
import { useCodeCopy } from 'docs/src/modules/utils/CodeCopy';

const HighlightedCode = React.forwardRef(function HighlightedCode(props, ref) {
const { code, language, component: Component = MarkdownElement, ...other } = props;
const {
copyButtonProps,
code,
language,
component: Component = MarkdownElement,
...other
} = props;
const renderedCode = React.useMemo(() => {
return prism(code.trim(), language);
}, [code, language]);
const [copied, setCopied] = React.useState(false);
const [key, setKey] = React.useState('Ctrl');
const handlers = useCodeCopy();
React.useEffect(() => {
if (typeof window !== 'undefined') {
const macOS = window.navigator.platform.toUpperCase().indexOf('MAC') >= 0;
if (macOS) {
setKey('⌘');
}
}
}, []);
React.useEffect(() => {
if (copied) {
const timeout = setTimeout(() => {
setCopied(false);
}, 2000);
return () => {
clearTimeout(timeout);
};
}
return undefined;
}, [copied]);

return (
<Component ref={ref} {...other}>
<pre>
<code
className={`language-${language}`}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: renderedCode }}
/>
</pre>
<div className="MuiCode-root" {...handlers}>
<pre>
<code
className={`language-${language}`}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: renderedCode }}
/>
</pre>
<button
{...copyButtonProps}
aria-label="Copy the code"
type="button"
className="MuiCode-copy"
onClick={async () => {
setCopied(true);
await copy(code);
}}
>
{copied ? 'Copied' : 'Copy'}
<span className="MuiCode-copyKeypress">
<span>or</span> {key} + C
</span>
</button>
</div>
</Component>
);
});

HighlightedCode.propTypes = {
code: PropTypes.string.isRequired,
component: PropTypes.elementType,
copyButtonProps: PropTypes.object,
language: PropTypes.string.isRequired,
};

Expand Down
60 changes: 60 additions & 0 deletions docs/src/modules/components/MarkdownElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,66 @@ const Root = styled('div')(({ theme }) => ({
marginTop: theme.spacing(1),
},
},
'& .MuiCode-root': {
position: 'relative',
'&:hover': {
'& .MuiCode-copy': {
opacity: 1,
},
},
},
'& .MuiCode-copy': {
minWidth: 64,
opacity: 0,
backgroundColor: alpha(blueDark[600], 0.5),
cursor: 'pointer',
position: 'absolute',
top: theme.spacing(1),
right: theme.spacing(1),
fontFamily: 'inherit',
fontSize: '0.813rem',
fontWeight: 500,
padding: theme.spacing(0.5, 1),
borderRadius: 4,
border: `1px solid`,
borderColor: blueDark[500],
color: blueDark[50],
'&:hover, &:focus': {
opacity: 1,
color: '#fff',
backgroundColor: alpha(blueDark[600], 0.7),
borderColor: blueDark[500],
'& .MuiCode-copyKeypress': {
opacity: 1,
},
},
'&[data-copied]': {
// style of the button when it is in copied state.
borderColor: blue[700],
color: '#fff',
backgroundColor: blueDark[600],
},
'&:focus-visible': {
outline: '2px solid',
outlineOffset: 2,
outlineColor: blueDark[500],
},
},
'& .MuiCode-copyKeypress': {
pointerEvents: 'none',
userSelect: 'none',
opacity: 0,
position: 'absolute',
left: '50%',
top: '100%',
minWidth: '100%',
marginTop: theme.spacing(0.5),
whiteSpace: 'nowrap',
transform: 'translateX(-50%)',
'& > span': {
opacity: 0.72,
},
},
'& li': {
'& pre': {
marginTop: theme.spacing(1),
Expand Down
Loading

0 comments on commit a3a4bef

Please sign in to comment.