@mapbox/mr-ui
Version:
UI components for Mapbox projects
305 lines (287 loc) • 12 kB
JavaScript
;
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
};