import React from 'react';
import flatMap from 'lodash/flatMap';
import { transparentize } from 'polished';
import { css, cx } from '@leafygreen-ui/emotion';
import { palette } from '@leafygreen-ui/palette';
import { spacing } from '@leafygreen-ui/tokens';
import {
LeafyGreenHighlightResult,
LeafyGreenHLJSPlugin,
TokenObject,
} from '../highlight';
import { useSyntaxContext } from '../Syntax/SyntaxContext';
import { generateKindClassName } from './utils/generateKindClassName/generateKindClassName';
import {
childrenAsKeywords,
isArray,
isNumber,
isObject,
isString,
isTokenObject,
} from './utils/helpers';
import { lineWithKeywords } from './utils/lineWithKeywords/lineWithKeywords';
import {
FlatTokenObject,
LineDefinition,
LineTableRowProps,
TableContentProps,
TokenProps,
TreeItem,
} from './renderingPlugin.types';
function Token({ kind, children }: TokenProps) {
return {children};
}
export function processToken(token: TreeItem, key?: number): React.ReactNode {
if (token == null) {
return null;
}
if (isString(token)) {
return token;
}
if (isArray(token)) {
return token.map(processToken);
}
if (isObject(token)) {
return (
{processToken(token.children)}
);
}
return token;
}
const cellStyle = css`
border-spacing: 0;
vertical-align: top;
padding: 0 ${spacing[300]}px;
`;
function getHighlightedRowStyle(darkMode: boolean) {
let backgroundColor: string, backgroundImage: string, borderColor: string;
if (darkMode) {
backgroundColor = transparentize(0.7, palette.yellow.dark3);
backgroundImage = 'none';
borderColor = palette.gray.dark3;
} else {
backgroundColor = palette.yellow.light3;
backgroundImage = 'none';
borderColor = palette.yellow.light2;
}
return css`
background-color: ${backgroundColor};
background-image: ${backgroundImage};
// Fixes an issue in Safari where the gradient applied to the table row would be applied
// to each cell in the row instead of being continuous across cells.
background-attachment: fixed;
// Selects all children of a highlighted row, and adds a border top
& > td {
border-top: 1px solid ${borderColor};
}
// Selects following rows after a highlighted row, and adds a border top
// We don't add border bottoms here to support consecutive highlighted rows.
& + tr > td {
border-top: 1px solid ${borderColor};
}
// Remove borders between consecutive highlighted rows
& + & > td {
border-top: 0;
}
// If the highlighted row is the last child, then we add a border bottom
&:last-child > td {
border-bottom: 1px solid ${borderColor};
}
`;
}
export function LineTableRow({
lineNumber,
highlighted,
darkMode,
children,
}: LineTableRowProps) {
const numberColor = darkMode ? palette.gray.light1 : palette.gray.dark1;
const highlightedNumberColor = darkMode
? palette.gray.light3
: palette.yellow.dark2;
return (
{lineNumber && (
|
{lineNumber}
|
)}
{children} |
);
}
// Check if object is a TokenObject which has an array with a single string element within it.
function isFlattenedTokenObject(obj: TokenObject): obj is FlatTokenObject {
// default to an empty object in the off-chance "obj" is null or undefined.
const { children } = obj ?? {};
if (isArray(children) && children.length === 1 && isString(children[0])) {
return true;
}
return false;
}
// If an array of tokens contains an object with more than one children, this function will flatten that tree recursively.
export function flattenNestedTree(
children: TokenObject['children'] | TokenObject,
kind?: string,
): Array {
if (typeof children === 'string') {
return children;
}
if (isTokenObject(children)) {
return flattenNestedTree(children.children, kind);
}
// Generate a flat map function with a closure around parent's kind
function flatMapTreeWithKinds(...parentKinds: Array) {
parentKinds = parentKinds.filter(
(str): str is string => isString(str) && str.length > 0,
);
return function (
entity: string | TokenObject,
): string | FlatTokenObject | Array {
if (isString(entity)) {
return parentKinds.length > 0
? {
kind: generateKindClassName([
kind,
...parentKinds,
...childrenAsKeywords(entity),
]),
children: [entity],
}
: entity; // entity is basic text
}
// If this is a nested entity, then flat map it's children
if ((entity?.children?.length ?? 0) >= 1) {
// Generate a new flat map function with this entity's kind
return flatMap(
entity.children,
flatMapTreeWithKinds(kind, entity.kind, ...parentKinds),
);
}
if (isFlattenedTokenObject(entity)) {
return {
kind: generateKindClassName([
kind,
entity.kind,
...parentKinds,
...childrenAsKeywords(...entity.children),
]),
children: entity.children,
};
}
return entity as FlatTokenObject;
};
}
return flatMap(children, flatMapTreeWithKinds(kind));
}
function containsLineBreak(token: TreeItem): boolean {
if (isArray(token)) {
return token.some(containsLineBreak);
}
if (isString(token)) {
return token.includes('\n');
}
if (isObject(token)) {
return (
token.children?.includes('\n') ||
(isString(token.children?.[0]) && token.children[0].includes('\n'))
);
}
return false;
}
export function treeToLines(
children: Array,
): LineDefinition {
const lines: LineDefinition = [];
let currentLineIndex = 0;
// Create a new line, if no lines exist yet
if (lines[currentLineIndex] == null) {
lines[currentLineIndex] = [];
}
const createNewLine = () => {
currentLineIndex++;
lines[currentLineIndex] = [];
};
flattenNestedTree(children).forEach(child => {
// If the current element includes a line break, we need to handle it differently
if (containsLineBreak(child)) {
if (isString(child)) {
child.split('\n').forEach((fragment, i) => {
if (i > 0) {
createNewLine();
}
// Empty new lines should be represented as an empty array
if (fragment) {
lines[currentLineIndex].push(fragment);
}
});
} else {
const tokenString = child.children[0];
tokenString.split('\n').forEach((fragment, i) => {
if (i > 0) {
createNewLine();
}
lines[currentLineIndex].push({
kind: child.kind,
children: [fragment],
});
});
}
} else if (child && (isString(child) || isFlattenedTokenObject(child))) {
lines[currentLineIndex].push(child);
}
});
return lines;
}
export function TableContent({ lines }: TableContentProps) {
const {
highlightLines,
showLineNumbers,
darkMode,
lineNumberStart,
customKeywords = {},
} = useSyntaxContext();
const trimmedLines = [...lines];
// Strip empty lines from the beginning of code blocks
while (trimmedLines[0]?.length === 0) {
trimmedLines.shift();
}
// Strip empty lines from the end of code blocks
while (trimmedLines[trimmedLines.length - 1]?.length === 0) {
trimmedLines.pop();
}
const lineShouldHighlight = (line: number) => {
return highlightLines.some(def => {
if (isNumber(def)) {
return line === def;
}
if (isArray(def)) {
const sortedArr = [...def].sort((a, b) => a - b);
return line >= sortedArr[0] && line <= sortedArr[1];
}
return false;
});
};
return (
<>
{trimmedLines.map((line, index) => {
const currentLineNumber = index + (lineNumberStart ?? 1);
const highlightLine = lineShouldHighlight(currentLineNumber);
let mappedLine = line;
if (Object.keys(customKeywords).length > 0) {
mappedLine = lineWithKeywords(line, customKeywords);
}
const displayLineNumber = showLineNumbers
? currentLineNumber
: undefined;
const processedLine = mappedLine?.length ? (
mappedLine.map(processToken)
) : (
// We create placeholder content when a line break appears to preserve the line break's height
// It needs to be inline-block for the table row to not collapse.
);
return (
{processedLine}
);
})}
>
);
}
const plugin: LeafyGreenHLJSPlugin = {
'after:highlight': function (result: LeafyGreenHighlightResult) {
const { rootNode } = result._emitter;
result.react = ;
},
};
export default plugin;