import React from 'react';
import createElement from './create-element';
import checkForListedLanguage from './checkForListedLanguage';
const newLineRegex = /\n/g;
function getNewLines(str) {
return str.match(newLineRegex);
}
function getAllLineNumbers({ lines, startingLineNumber, style }) {
return lines.map((_, i) => {
const number = i + startingLineNumber;
return (
{`${number}\n`}
);
});
}
function AllLineNumbers({
codeString,
codeStyle,
containerStyle = { float: 'left', paddingRight: '10px' },
numberStyle = {},
startingLineNumber
}) {
return (
{getAllLineNumbers({
lines: codeString.replace(/\n$/, '').split('\n'),
style: numberStyle,
startingLineNumber
})}
);
}
function getEmWidthOfNumber(num) {
return `${num.toString().length}.25em`;
}
function getInlineLineNumber(lineNumber, inlineLineNumberStyle) {
return {
type: 'element',
tagName: 'span',
properties: {
key: `line-number--${lineNumber}`,
className: [
'comment',
'linenumber',
'react-syntax-highlighter-line-number'
],
style: inlineLineNumberStyle
},
children: [
{
type: 'text',
value: lineNumber
}
]
};
}
function assembleLineNumberStyles(
lineNumberStyle,
lineNumber,
largestLineNumber
) {
// minimally necessary styling for line numbers
const defaultLineNumberStyle = {
display: 'inline-block',
minWidth: getEmWidthOfNumber(largestLineNumber),
paddingRight: '1em',
textAlign: 'right',
userSelect: 'none'
};
// prep custom styling
const customLineNumberStyle =
typeof lineNumberStyle === 'function'
? lineNumberStyle(lineNumber)
: lineNumberStyle;
// combine
const assembledStyle = {
...defaultLineNumberStyle,
...customLineNumberStyle
};
return assembledStyle;
}
function createLineElement({
children,
lineNumber,
lineNumberStyle,
largestLineNumber,
showInlineLineNumbers,
lineProps = {},
className = [],
showLineNumbers,
wrapLongLines
}) {
const properties =
typeof lineProps === 'function' ? lineProps(lineNumber) : lineProps;
properties['className'] = className;
if (lineNumber && showInlineLineNumbers) {
const inlineLineNumberStyle = assembleLineNumberStyles(
lineNumberStyle,
lineNumber,
largestLineNumber
);
children.unshift(getInlineLineNumber(lineNumber, inlineLineNumberStyle));
}
if (wrapLongLines & showLineNumbers) {
properties.style = { ...properties.style, display: 'flex' };
}
return {
type: 'element',
tagName: 'span',
properties,
children
};
}
function flattenCodeTree(tree, className = [], newTree = []) {
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.type === 'text') {
newTree.push(
createLineElement({
children: [node],
className: [...new Set(className)]
})
);
} else if (node.children) {
const classNames = className.concat(node.properties.className);
flattenCodeTree(node.children, classNames).forEach(i => newTree.push(i));
}
}
return newTree;
}
function processLines(
codeTree,
wrapLines,
lineProps,
showLineNumbers,
showInlineLineNumbers,
startingLineNumber,
largestLineNumber,
lineNumberStyle,
wrapLongLines
) {
const tree = flattenCodeTree(codeTree.value);
const newTree = [];
let lastLineBreakIndex = -1;
let index = 0;
function createWrappedLine(children, lineNumber, className = []) {
return createLineElement({
children,
lineNumber,
lineNumberStyle,
largestLineNumber,
showInlineLineNumbers,
lineProps,
className,
showLineNumbers,
wrapLongLines
});
}
function createUnwrappedLine(children, lineNumber) {
if (showLineNumbers && lineNumber && showInlineLineNumbers) {
const inlineLineNumberStyle = assembleLineNumberStyles(
lineNumberStyle,
lineNumber,
largestLineNumber
);
children.unshift(getInlineLineNumber(lineNumber, inlineLineNumberStyle));
}
return children;
}
function createLine(children, lineNumber, className = []) {
return wrapLines || className.length > 0
? createWrappedLine(children, lineNumber, className)
: createUnwrappedLine(children, lineNumber);
}
while (index < tree.length) {
const node = tree[index];
const value = node.children[0].value;
const newLines = getNewLines(value);
if (newLines) {
const splitValue = value.split('\n');
splitValue.forEach((text, i) => {
const lineNumber =
showLineNumbers && newTree.length + startingLineNumber;
const newChild = { type: 'text', value: `${text}\n` };
// if it's the first line
if (i === 0) {
const children = tree.slice(lastLineBreakIndex + 1, index).concat(
createLineElement({
children: [newChild],
className: node.properties.className
})
);
const line = createLine(children, lineNumber);
newTree.push(line);
// if it's the last line
} else if (i === splitValue.length - 1) {
const stringChild =
tree[index + 1] &&
tree[index + 1].children &&
tree[index + 1].children[0];
const lastLineInPreviousSpan = { type: 'text', value: `${text}` };
if (stringChild) {
const newElem = createLineElement({
children: [lastLineInPreviousSpan],
className: node.properties.className
});
tree.splice(index + 1, 0, newElem);
} else {
const children = [lastLineInPreviousSpan];
const line = createLine(
children,
lineNumber,
node.properties.className
);
newTree.push(line);
}
// if it's neither the first nor the last line
} else {
const children = [newChild];
const line = createLine(
children,
lineNumber,
node.properties.className
);
newTree.push(line);
}
});
lastLineBreakIndex = index;
}
index++;
}
if (lastLineBreakIndex !== tree.length - 1) {
const children = tree.slice(lastLineBreakIndex + 1, tree.length);
if (children && children.length) {
const lineNumber = showLineNumbers && newTree.length + startingLineNumber;
const line = createLine(children, lineNumber);
newTree.push(line);
}
}
return wrapLines ? newTree : [].concat(...newTree);
}
function defaultRenderer({ rows, stylesheet, useInlineStyles }) {
return rows.map((node, i) =>
createElement({
node,
stylesheet,
useInlineStyles,
key: `code-segement${i}`
})
);
}
// only highlight.js has the highlightAuto method
function isHighlightJs(astGenerator) {
return astGenerator && typeof astGenerator.highlightAuto !== 'undefined';
}
function getCodeTree({ astGenerator, language, code, defaultCodeValue }) {
// figure out whether we're using lowlight/highlight or refractor/prism
// then attempt highlighting accordingly
// lowlight/highlight?
if (isHighlightJs(astGenerator)) {
const hasLanguage = checkForListedLanguage(astGenerator, language);
if (language === 'text') {
return { value: defaultCodeValue, language: 'text' };
} else if (hasLanguage) {
return astGenerator.highlight(language, code);
} else {
return astGenerator.highlightAuto(code);
}
}
// must be refractor/prism, then
try {
return language && language !== 'text'
? { value: astGenerator.highlight(code, language) }
: { value: defaultCodeValue };
} catch (e) {
return { value: defaultCodeValue };
}
}
export default function(defaultAstGenerator, defaultStyle) {
return function SyntaxHighlighter({
language,
children,
style = defaultStyle,
customStyle = {},
codeTagProps = {
className: language ? `language-${language}` : undefined,
style: {
...style['code[class*="language-"]'],
...style[`code[class*="language-${language}"]`]
}
},
useInlineStyles = true,
showLineNumbers = false,
showInlineLineNumbers = true,
startingLineNumber = 1,
lineNumberContainerStyle,
lineNumberStyle = {},
wrapLines,
wrapLongLines = false,
lineProps = {},
renderer,
PreTag = 'pre',
CodeTag = 'code',
code = (Array.isArray(children) ? children[0] : children) || '',
astGenerator,
...rest
}) {
astGenerator = astGenerator || defaultAstGenerator;
const allLineNumbers = showLineNumbers ? (
) : null;
const defaultPreStyle = style.hljs ||
style['pre[class*="language-"]'] || { backgroundColor: '#fff' };
const generatorClassName = isHighlightJs(astGenerator) ? 'hljs' : 'prismjs';
const preProps = useInlineStyles
? Object.assign({}, rest, {
style: Object.assign({}, defaultPreStyle, customStyle)
})
: Object.assign({}, rest, {
className: rest.className
? `${generatorClassName} ${rest.className}`
: generatorClassName,
style: Object.assign({}, customStyle)
});
if (wrapLongLines) {
codeTagProps.style = { ...codeTagProps.style, whiteSpace: 'pre-wrap' };
} else {
codeTagProps.style = { ...codeTagProps.style, whiteSpace: 'pre' };
}
if (!astGenerator) {
return (
{allLineNumbers}
{code}
);
}
/*
* Some custom renderers rely on individual row elements so we need to turn wrapLines on
* if renderer is provided and wrapLines is undefined.
*/
if ((wrapLines === undefined && renderer) || wrapLongLines)
wrapLines = true;
renderer = renderer || defaultRenderer;
const defaultCodeValue = [{ type: 'text', value: code }];
const codeTree = getCodeTree({
astGenerator,
language,
code,
defaultCodeValue
});
if (codeTree.language === null) {
codeTree.value = defaultCodeValue;
}
// determine largest line number so that we can force minWidth on all linenumber elements
const largestLineNumber = codeTree.value.length + startingLineNumber;
const rows = processLines(
codeTree,
wrapLines,
lineProps,
showLineNumbers,
showInlineLineNumbers,
startingLineNumber,
largestLineNumber,
lineNumberStyle,
wrapLongLines
);
return (
{!showInlineLineNumbers && allLineNumbers}
{renderer({ rows, stylesheet: style, useInlineStyles })}
);
};
}