feat: codebox and highlighter

This commit is contained in:
Thomas Osmonson
2020-04-13 17:57:01 -05:00
committed by Thomas Osmonson
parent 8518dd85fe
commit 49e99bf0d9
11 changed files with 586 additions and 1 deletions

View File

@@ -16,11 +16,14 @@
"@styled-system/should-forward-prop": "^5.1.5",
"@types/color": "^3.0.1",
"@types/flushable": "^1.0.1",
"@types/prismjs": "^1.16.0",
"@types/styled-components": "^5.1.0",
"@types/styled-system": "^5.1.6",
"@types/styled-system__css": "^5.0.8",
"color": "3.1.2",
"flushable": "^1.0.0",
"prism-react-renderer": "^1.0.2",
"prismjs": "^1.20.0",
"prop-types": "^15.7.2",
"react-spring": "8.0.27",
"styled-system": "5.1.5",
@@ -59,7 +62,7 @@
"fs-extra": "9.0.0",
"glob": "7.1.6",
"path": "0.12.7",
"prettier": "^2.0.4",
"prettier": "^2.0.5",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"rimraf": "3.0.2",

View File

@@ -0,0 +1,31 @@
import React, { useState } from 'react';
import { Highlighter } from '../highlighter';
import { Box, BoxProps } from '../box';
export const CodeBlock = ({
code,
showLineNumbers,
style = {},
...rest
}: { code: string; showLineNumbers?: boolean } & BoxProps) => {
const [editorCode] = useState(code?.toString().trim());
return (
<Box
overflowX="auto"
bg="ink"
borderRadius={[0, 0, '12px']}
py="base"
width="100%"
{...rest}
style={{
...style,
whiteSpace: 'pre',
fontFamily: 'Fira Code, Consolata, monospace',
fontSize: '14px',
}}
>
<Highlighter showLineNumbers={showLineNumbers} code={editorCode} />
</Box>
);
};

View File

@@ -1 +1,10 @@
export type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
interface IntrinsicAttributes {
css?: any;
}
}
}

View File

@@ -0,0 +1,123 @@
import React from 'react';
import Prism from 'prismjs';
import Highlight from 'prism-react-renderer';
import { Box } from '../box';
import { Flex } from '../flex';
import { useTheme } from '../theme-provider';
import { GrammaticalToken, GetGrammaticalTokenProps, RenderProps, Language } from './types';
import { theme } from './prism-theme';
import { startPad } from '../utils';
const lineNumberWidth = 60;
const getLineNumber = (n: number, length: number) => startPad(n, length.toString().length + 1);
const Tokens = ({
tokens,
getTokenProps,
...rest
}: {
tokens: GrammaticalToken[];
getTokenProps: GetGrammaticalTokenProps;
}) => {
const bsTheme = useTheme();
const pl = `calc(${lineNumberWidth}px + ${(bsTheme as any).sizes['base'] || '16px'})`;
return (
<Box pl={pl} pr="base" position="relative" zIndex={2} {...rest}>
{tokens.map((token, key) => (
<Box py="2px" display="inline-block" {...getTokenProps({ token, key })} />
))}
</Box>
);
};
const LineNumber = ({ number, length, ...rest }: { number: number; length: number }) => (
<Flex
textAlign="right"
pr="base"
pl="base"
width={lineNumberWidth}
borderRight="1px solid"
borderRightColor="inherit"
color="ink.400"
flexShrink={0}
style={{ userSelect: 'none' }}
position="absolute"
left={0}
height="100%"
align="baseline"
justify="center"
zIndex={1}
{...rest}
>
{getLineNumber(number, length)}
</Flex>
);
const Line = ({
tokens,
getTokenProps,
index,
length,
showLineNumbers,
...rest
}: {
tokens: GrammaticalToken[];
index: number;
length: number;
getTokenProps: GetGrammaticalTokenProps;
showLineNumbers?: boolean;
}) => {
return (
<Flex
height="loose"
align="baseline"
borderColor="ink.900"
_hover={{ bg: ['unset', 'unset', 'ink.900'], borderColor: ['ink.900', 'ink.900', 'ink.600'] }}
position="relative"
{...rest}
>
{showLineNumbers ? <LineNumber number={index} length={length} /> : null}
<Tokens getTokenProps={getTokenProps} tokens={tokens} />
</Flex>
);
};
const Lines = ({
tokens: lines,
getLineProps,
getTokenProps,
className,
showLineNumbers,
}: { showLineNumbers?: boolean } & RenderProps) => {
return (
<Box display="block" className={className}>
<Box display="block" style={{ fontFamily: 'Fira Code' }}>
{lines.map((tokens, i) => (
<Line
index={i}
tokens={tokens}
getTokenProps={getTokenProps}
length={lines.length + 1}
showLineNumbers={showLineNumbers}
{...getLineProps({ line: tokens, key: i })}
/>
))}
</Box>
</Box>
);
};
interface HighlighterProps {
code: string;
language?: Language;
showLineNumbers?: boolean;
}
export const Highlighter = ({ code, language = 'lisp', showLineNumbers }: HighlighterProps) => {
return (
<Highlight theme={theme} code={code} language={language as any} Prism={Prism as any}>
{props => <Lines showLineNumbers={showLineNumbers} {...props} />}
</Highlight>
);
};

View File

@@ -0,0 +1,197 @@
// @ts-nocheck
// prismjs/components/prism-lisp.js
// this file is here to fix a bug with ssr and prism not loading/refreshing after client hydration
// todo: convert to ts, and define clarity language
import Prism from 'prismjs';
(function (Prism) {
// Functions to construct regular expressions
// simple form
// e.g. (interactive ... or (interactive)
function simple_form(name) {
return RegExp('(\\()' + name + '(?=[\\s\\)])');
}
// booleans and numbers
function primitive(pattern) {
return RegExp('([\\s([])' + pattern + '(?=[\\s)])');
}
// Patterns in regular expressions
// Symbol name. See https://www.gnu.org/software/emacs/manual/html_node/elisp/Symbol-Type.html
// & and : are excluded as they are usually used for special purposes
const symbol = '[-+*/_~!@$%^=<>{}\\w]+';
// symbol starting with & used in function arguments
const marker = '&' + symbol;
// Open parenthesis for look-behind
const par = '(\\()';
const endpar = '(?=\\))';
// End the pattern with look-ahead space
const space = '(?=\\s)';
const language = {
// Three or four semicolons are considered a heading.
// See https://www.gnu.org/software/emacs/manual/html_node/elisp/Comment-Tips.html
heading: {
pattern: /;;;.*/,
alias: ['comment', 'title'],
},
comment: /;.*/,
string: {
pattern: /"(?:[^"\\]|\\.)*"/,
greedy: true,
inside: {
argument: /[-A-Z]+(?=[.,\s])/,
symbol: RegExp('`' + symbol + "'"),
},
},
'quoted-symbol': {
pattern: RegExp("#?'" + symbol),
alias: ['variable', 'symbol'],
},
'lisp-property': {
pattern: RegExp(':' + symbol),
alias: 'property',
},
splice: {
pattern: RegExp(',@?' + symbol),
alias: ['symbol', 'variable'],
},
keyword: [
{
pattern: RegExp(
par +
'(?:(?:lexical-)?let\\*?|(?:cl-)?letf|if|when|while|unless|cons|cl-loop|and|or|not|cond|setq|error|message|null|require|provide|use-package)' +
space
),
lookbehind: true,
},
{
pattern: RegExp(par + '(?:for|do|collect|return|finally|append|concat|in|by)' + space),
lookbehind: true,
},
],
declare: {
pattern: simple_form('declare'),
lookbehind: true,
alias: 'keyword',
},
interactive: {
pattern: simple_form('interactive'),
lookbehind: true,
alias: 'keyword',
},
boolean: {
pattern: primitive('(?:t|nil)'),
lookbehind: true,
},
number: {
pattern: primitive('[-+]?\\d+(?:\\.\\d*)?'),
lookbehind: true,
},
defvar: {
pattern: RegExp(par + 'def(?:var|const|custom|group)\\s+' + symbol),
lookbehind: true,
inside: {
keyword: /^def[a-z]+/,
variable: RegExp(symbol),
},
},
defun: {
pattern: RegExp(par + '(?:cl-)?(?:defun\\*?|defmacro)\\s+' + symbol + '\\s+\\([\\s\\S]*?\\)'),
lookbehind: true,
inside: {
keyword: /^(?:cl-)?def\S+/,
// See below, this property needs to be defined later so that it can
// reference the language object.
arguments: null,
function: {
pattern: RegExp('(^\\s)' + symbol),
lookbehind: true,
},
punctuation: /[()]/,
},
},
lambda: {
pattern: RegExp(par + 'lambda\\s+\\((?:&?' + symbol + '\\s*)*\\)'),
lookbehind: true,
inside: {
keyword: /^lambda/,
// See below, this property needs to be defined later so that it can
// reference the language object.
arguments: null,
punctuation: /[()]/,
},
},
car: {
pattern: RegExp(par + symbol),
lookbehind: true,
},
punctuation: [
// open paren, brackets, and close paren
/(?:['`,]?\(|[)\[\]])/,
// cons
{
pattern: /(\s)\.(?=\s)/,
lookbehind: true,
},
],
};
const arg = {
'lisp-marker': RegExp(marker),
rest: {
argument: {
pattern: RegExp(symbol),
alias: 'variable',
},
varform: {
pattern: RegExp(par + symbol + '\\s+\\S[\\s\\S]*' + endpar),
lookbehind: true,
inside: {
string: language.string,
boolean: language.boolean,
number: language.number,
symbol: language.symbol,
punctuation: /[()]/,
},
},
},
};
const forms = '\\S+(?:\\s+\\S+)*';
const arglist = {
pattern: RegExp(par + '[\\s\\S]*' + endpar),
lookbehind: true,
inside: {
'rest-vars': {
pattern: RegExp('&(?:rest|body)\\s+' + forms),
inside: arg,
},
'other-marker-vars': {
pattern: RegExp('&(?:optional|aux)\\s+' + forms),
inside: arg,
},
keys: {
pattern: RegExp('&key\\s+' + forms + '(?:\\s+&allow-other-keys)?'),
inside: arg,
},
argument: {
pattern: RegExp(symbol),
alias: 'variable',
},
punctuation: /[()]/,
},
};
language['lambda'].inside.arguments = arglist;
language['defun'].inside.arguments = Prism.util.clone(arglist);
language['defun'].inside.arguments.inside.sublist = arglist;
Prism.languages.lisp = language;
Prism.languages.elisp = language;
Prism.languages.emacs = language;
Prism.languages['emacs-lisp'] = language;
})(Prism);

View File

@@ -0,0 +1,76 @@
import { PrismTheme } from 'prism-react-renderer';
export const theme: PrismTheme = {
plain: {
color: '#fff',
backgroundColor: 'transparent',
},
styles: [
{
types: ['prolog'],
style: {
color: 'rgb(0, 0, 128)',
},
},
{
types: ['comment', 'punctuation'],
style: {
color: 'rgb(106, 153, 85)',
},
},
{
types: ['builtin', 'tag', 'changed', 'function', 'keyword'],
style: {
color: 'rgb(86, 156, 214)',
},
},
{
types: ['number', 'variable', 'inserted'],
style: {
color: '#A58FFF',
},
},
{
types: ['operator'],
style: {
color: 'rgb(212, 212, 212)',
},
},
{
types: ['constant'],
style: {
color: 'rgb(100, 102, 149)',
},
},
{
types: ['attr-name'],
style: {
color: 'rgb(156, 220, 254)',
},
},
{
types: ['car'],
style: {
color: 'rgb(156, 220, 254)',
},
},
{
types: ['deleted', 'string'],
style: {
color: '#FF7B48',
},
},
{
types: ['class-name'],
style: {
color: 'rgb(78, 201, 176)',
},
},
{
types: ['char'],
style: {
color: '#FF7B48',
},
},
],
};

View File

@@ -0,0 +1,89 @@
import * as React from 'react';
export interface GrammaticalToken {
types: string[];
content: string;
empty?: boolean;
}
export interface StyleObj {
[key: string]: string | number | null;
}
export interface GrammaticalTokenOutputProps {
key?: React.Key;
style?: StyleObj;
className: string;
children: string;
[otherProp: string]: any;
}
export interface GrammaticalTokenInputProps {
key?: React.Key;
style?: StyleObj;
className?: string;
token: GrammaticalToken;
[otherProp: string]: any;
}
export interface LineInputProps {
key?: React.Key;
style?: StyleObj;
className?: string;
line: GrammaticalToken[];
[otherProp: string]: any;
}
export interface LineOutputProps {
key?: React.Key;
style?: StyleObj;
className: string;
[otherProps: string]: any;
}
export interface RenderProps {
tokens: GrammaticalToken[][];
className: string;
style: StyleObj;
getLineProps: (input: LineInputProps) => LineOutputProps;
getTokenProps: (input: GrammaticalTokenInputProps) => GrammaticalTokenOutputProps;
}
export type GetGrammaticalTokenProps = (
input: GrammaticalTokenInputProps
) => GrammaticalTokenOutputProps;
export type Language =
| 'markup'
| 'bash'
| 'clike'
| 'c'
| 'cpp'
| 'css'
| 'javascript'
| 'jsx'
| 'coffeescript'
| 'actionscript'
| 'css-extr'
| 'diff'
| 'git'
| 'go'
| 'graphql'
| 'handlebars'
| 'json'
| 'less'
| 'lisp'
| 'makefile'
| 'markdown'
| 'objectivec'
| 'ocaml'
| 'python'
| 'reason'
| 'sass'
| 'scss'
| 'sql'
| 'stylus'
| 'tsx'
| 'typescript'
| 'wasm'
| 'yaml';

View File

@@ -24,3 +24,5 @@ export * from './tooltip';
export * from './popper';
export * from './portal';
export * from './hooks';
export * from './highlighter';
export * from './codebox';

View File

@@ -84,6 +84,10 @@ export interface UseTooltipProps {
* @default 10 ( = 10px )
*/
arrowSize?: UsePopperProps['arrowSize'];
/**
* The label, we check if it changes to refresh the position of the tooltip
*/
label?: string;
}
export function useTooltip(props: UseTooltipProps = {}) {

View File

@@ -377,3 +377,6 @@ export function mapResponsive(prop: any, mapper: (val: any) => any) {
return null;
}
export const startPad = (n: number, z = 2, s = '0') =>
(n + '').length <= z ? ['', '-'][+(n < 0)] + (s.repeat(z) + Math.abs(n)).slice(-1 * z) : n + '';

View File

@@ -3744,6 +3744,11 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f"
integrity sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==
"@types/prismjs@^1.16.0":
version "1.16.0"
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.16.0.tgz#4328c9f65698e59f4feade8f4e5d928c748fd643"
integrity sha512-mEyuziLrfDCQ4juQP1k706BUU/c8OGn/ZFl69AXXY6dStHClKX4P+N8+rhqpul1vRDA2VOygzMRSJJZHyDEOfw==
"@types/prop-types@*":
version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
@@ -6389,6 +6394,15 @@ cli-width@^2.0.0:
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
clipboard@^2.0.0:
version "2.0.6"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376"
integrity sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==
dependencies:
good-listener "^1.2.2"
select "^1.1.2"
tiny-emitter "^2.0.0"
cliui@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
@@ -7410,6 +7424,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
delegate@^3.1.2:
version "3.2.0"
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -9528,6 +9547,13 @@ globrex@^0.1.1:
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=
dependencies:
delegate "^3.1.2"
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
@@ -15126,6 +15152,11 @@ prettier@^1.18.2, prettier@^1.19.1:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
prettier@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4"
integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==
pretty-error@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
@@ -15176,6 +15207,13 @@ prism-react-renderer@^1.0.1, prism-react-renderer@^1.0.2:
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.0.2.tgz#3bb9a6a42f76fc049b03266298c7068fdd4b7ea9"
integrity sha512-0++pJyRfu4v2OxI/Us/5RLui9ESDkTiLkVCtKuPZYdpB8UQWJpnJQhPrWab053XtsKW3oM0sD69uJ6N9exm1Ag==
prismjs@^1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03"
integrity sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ==
optionalDependencies:
clipboard "^2.0.0"
private@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@@ -16762,6 +16800,11 @@ select-hose@^2.0.0:
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
select@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
selfsigned@^1.10.7:
version "1.10.7"
resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b"
@@ -17971,6 +18014,11 @@ timers-browserify@^2.0.4:
dependencies:
setimmediate "^1.0.4"
tiny-emitter@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
tiny-glob@^0.2.6:
version "0.2.6"
resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.6.tgz#9e056e169d9788fe8a734dfa1ff02e9b92ed7eda"