UNPKG

@mapbox/mr-ui

Version:

UI components for Mapbox projects

305 lines (287 loc) 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = CodeSnippet; var _react = _interopRequireWildcard(require("react")); var _propTypes = _interopRequireDefault(require("prop-types")); var _debounce = _interopRequireDefault(require("debounce")); var _copyButton = _interopRequireDefault(require("../copy-button")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } const defaultTheme = ` .hljs-comment, .hljs-quote { color: #7285b7; } .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-deletion { color: #ff9da4; } .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-meta, .hljs-link { color: #ffc58f; } .hljs-attribute { color: #ffeead; } .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #d1f1a9; } .hljs-title, .hljs-section { color: #bbdaff; } .hljs-keyword, .hljs-selector-tag { color: #ebbbff; } .hljs { display: block; font-family: 'Menlo', 'Bitstream Vera Sans Mono', 'Monaco', 'Consolas', monospace; font-size: 12px; line-height: 1.5em; overflow-x: auto; background: #273d56; color: #fff; padding: 12px; border-radius: 3px; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; }`; // Theme cache, used to prevent the creation of multiple <style> elements with the same content. const injectedThemes = []; function CodeSnippet(_ref) { let { code, highlightedCode, copyRanges, maxHeight, onCopy, highlightThemeCss = defaultTheme, characterWidth = 7.225 // Will need to change this if we change font size } = _ref; const showCopyButton = (0, _react.useRef)(_copyButton.default.isCopySupported()); const containerElement = (0, _react.useRef)(null); const adjustPositions = (0, _debounce.default)(() => { if (!containerElement.current) return; const chunkOverlays = containerElement.current.querySelectorAll('[data-chunk-overlay]'); for (let i = 0, l = chunkOverlays.length; i < l; i++) { const overlayElement = chunkOverlays[i]; const chunkId = overlayElement.getAttribute('data-chunk-overlay'); const codeElement = containerElement.current.querySelector(`[data-chunk-code="${chunkId}"]`); if (!codeElement) throw new Error(`No code element found with [data-chunk-code="${chunkId}"]`); const copyElement = containerElement.current.querySelector(`[data-chunk-copy="${chunkId}"]`); if (!copyElement) throw new Error(`No copy element found with [data-chunk-copy="${chunkId}"]`); overlayElement.style.top = `${codeElement.offsetTop}px`; copyElement.style.top = `${codeElement.offsetTop + 2}px`; overlayElement.style.height = `${codeElement.clientHeight}px`; // Since these elements move into position a split-second after the component // mounts and renders, we'll fade them in after they're positioned overlayElement.style.opacity = '1'; copyElement.style.opacity = '1'; } }, 300); (0, _react.useEffect)(() => { window.addEventListener('resize', adjustPositions); // Do not load themes that have already been injected. if (injectedThemes.indexOf(highlightThemeCss) === -1) { injectedThemes.push(highlightThemeCss); const styleTag = document.createElement('style'); styleTag.innerHTML = highlightThemeCss; document.head.appendChild(styleTag); } ; return () => { window.removeEventListener('resize', adjustPositions); }; }, [adjustPositions, highlightThemeCss]); // Adjust position on every render (0, _react.useEffect)(adjustPositions); const rawCodeLines = code.trim().split('\n'); // If highlightedCode is not provided, show raw code. const displayCode = highlightedCode || code; const splitDisplayCode = displayCode.trim().split('\n'); // Use copyRanges to split the highlighted code into chunks, // some of which are "live", i.e. copyable, some which are not. // If there are no copyRanges, the whole snippet is copyable and there // is no fancy live-chunk styling. const mutableCopyRanges = copyRanges !== undefined && copyRanges.slice(); let currentLiveRange = mutableCopyRanges && mutableCopyRanges.shift(); let currentChunk = []; const allChunks = []; const endCurrentChunk = _ref2 => { let { live } = _ref2; allChunks.push({ live, highlightedLines: currentChunk.map(line => line.highlighted), raw: currentChunk.reduce((result, line) => result += line.raw + '\n', ''), element: undefined }); currentChunk = []; }; for (let i = 0, l = splitDisplayCode.length; i < l; i++) { const chunk = splitDisplayCode[i]; const lineNumber = i + 1; if (currentLiveRange && lineNumber === currentLiveRange[0]) { endCurrentChunk({ live: false }); } else if (currentLiveRange && lineNumber > currentLiveRange[1]) { endCurrentChunk({ live: true }); currentLiveRange = mutableCopyRanges && mutableCopyRanges.shift(); } currentChunk.push({ highlighted: chunk, raw: rawCodeLines[i] }); } if (currentChunk.length) { endCurrentChunk({ live: false }); } const codeElements = []; const highlightElements = []; const copyElements = []; let liveChunkCount = -1; // Incremented to give CopyButtons an identifier allChunks.forEach((codeChunk, i) => { const chunkId = `chunk-${i}`; const lineEls = codeChunk.highlightedLines.map((line, i) => { // Left padding is determined below let lineClasses = 'pr12'; if (codeChunk.live) lineClasses += ' py3'; if (!codeChunk.live && copyRanges !== undefined) lineClasses += ' opacity75'; // Remove leading spaces, which are replaced with padding to avoid // weird behaviors that occur when there are long unbroken strings: // a line break might be introduced between the leading spaces and the // long word, creating an empty line that nobody wanted. const indentingSpacesMatch = line.match(/^[ ]*/); const indentingSpaces = indentingSpacesMatch ? indentingSpacesMatch[0] : ''; const indentingSpacesCount = indentingSpaces.length; const paddingLeft = indentingSpacesCount * characterWidth + 12; const displayLine = line.replace(/^[ ]*/, ''); /* eslint-disable react/no-danger */ return /*#__PURE__*/_react.default.createElement("div", { key: i, className: lineClasses, style: { paddingLeft } }, /*#__PURE__*/_react.default.createElement("div", { // We must use dangerouslySetInnerHTML because we've already // highlighted the code with lowlight, so we have an HTML string dangerouslySetInnerHTML: { __html: displayLine || ' ' } // Super fancy hanging indent , style: { textIndent: -2 * characterWidth, marginLeft: 2 * characterWidth } })); /* eslint-enable react/no-danger */ }); codeElements.push( /*#__PURE__*/_react.default.createElement("div", { key: i // z-index this line above the highlighted background element for // live chunks , className: "relative z2", "data-chunk-code": chunkId }, lineEls)); if (codeChunk.live) { highlightElements.push( /*#__PURE__*/_react.default.createElement("div", { key: i, "data-chunk-overlay": chunkId, className: "bg-darken75 absolute left right", style: { opacity: 0, transition: 'opacity 300ms linear' } }, /*#__PURE__*/_react.default.createElement("div", { className: "bg-blue h-full w6" }))); const chunkIndex = ++liveChunkCount; if (onCopy) { copyElements.push( /*#__PURE__*/_react.default.createElement("div", { key: i, "data-chunk-copy": chunkId, className: "absolute z3 right mr3 color-white", style: { opacity: 0, transition: 'opacity 300ms linear' } }, /*#__PURE__*/_react.default.createElement(_copyButton.default, { text: codeChunk.raw, onCopy: text => onCopy(text, chunkIndex) }))); } } }); // Prevent the default x-axis padding because each line pads itself let codeClasses = 'px0 hljs'; let copyAllButton = null; if (copyRanges === undefined && onCopy) { copyAllButton = /*#__PURE__*/_react.default.createElement("div", { className: "absolute z2 top right mr6 mt6 color-white" }, /*#__PURE__*/_react.default.createElement(_copyButton.default, { text: code, onCopy: onCopy })); } let containerClasses = 'relative round z0 scroll-styled'; if (maxHeight !== undefined) containerClasses += ' overflow-auto'; const containerStyles = {}; if (maxHeight !== undefined) { containerStyles.maxHeight = maxHeight; } return /*#__PURE__*/_react.default.createElement("div", { className: containerClasses, ref: containerElement, style: containerStyles }, /*#__PURE__*/_react.default.createElement("pre", null, /*#__PURE__*/_react.default.createElement("code", { className: codeClasses }, codeElements)), showCopyButton.current && copyAllButton, highlightElements, showCopyButton.current && copyElements); } CodeSnippet.propTypes = { /** Raw (unhighlighted) code. When the user clicks a copy button, this is what they'll get. If no `highlightedCode` is provided, `code` is displayed. */ code: _propTypes.default.string.isRequired, /** The HTML output of running code through a syntax highlighter. If this is not provided, `code` is displayed, instead. The default theme CSS assumes the highlighter is [`highlight.js`](https://github.com/isagalaev/highlight.js). If you are using another highlighter, provide your own theme. */ highlightedCode: _propTypes.default.string, /** Specific line ranges that should be independently copiable. Each range is a two-value array, consisting of the starting and ending line. If this is not provided, the entire snippet is copiable. */ copyRanges: _propTypes.default.arrayOf(_propTypes.default.arrayOf(_propTypes.default.number)), /** A maximum height for the snippet. If the code exceeds this height, the snippet will scroll internally. */ maxHeight: _propTypes.default.number, /** A callback that is invoked when the snippet (or a chunk of the snippet) is copied. If `copyRanges` are provided, the callback is passed the index (0-based) of the chunk that was copied. */ onCopy: _propTypes.default.func, /** CSS that styles the highlighted code. The default theme is a [`highlight.js` theme](https://highlightjs.readthedocs.io/en/latest/style-guide.html#defining-a-theme) theme. It is the dark theme used on mapbox.com's installation flow. */ highlightThemeCss: _propTypes.default.string, /** The width of a character in the theme's monospace font, used for indentation. If you use a font or font-size different than the default theme, you may need to change this value. */ characterWidth: _propTypes.default.number };