chore: reorg. content files, add blinking cursor

This commit is contained in:
Danny Avila 2023-03-21 09:46:08 -04:00
parent 04796824d5
commit 0cc4aea204
11 changed files with 7 additions and 159 deletions

View file

@ -0,0 +1,57 @@
import React, { useRef, useState } from 'react';
import Clipboard from '~/components/svg/Clipboard';
import CheckMark from '~/components/svg/CheckMark';
const CodeBlock = ({ lang, codeChildren }) => {
const codeRef = useRef(null);
return (
<div className="rounded-md bg-black">
<CodeBar
lang={lang}
codeRef={codeRef}
/>
<div className="overflow-y-auto p-4">
<code
ref={codeRef}
className={`hljs !whitespace-pre language-${lang}`}
>
{codeChildren}
</code>
</div>
</div>
);
};
const CodeBar = React.memo(({ lang, codeRef }) => {
const [isCopied, setIsCopied] = useState(false);
return (
<div className="relative flex items-center rounded-tl-md rounded-tr-md bg-gray-800 px-4 py-2 font-sans text-xs text-gray-200">
<span className="">{lang}</span>
<button
className="ml-auto flex gap-2"
onClick={async () => {
const codeString = codeRef.current?.textContent;
if (codeString)
navigator.clipboard.writeText(codeString).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 3000);
});
}}
>
{isCopied ? (
<>
<CheckMark />
Copied!
</>
) : (
<>
<Clipboard />
Copy code
</>
)}
</button>
</div>
);
});
export default CodeBlock;

View file

@ -0,0 +1,85 @@
import React from 'react';
import ReactMarkdown from 'react-markdown';
import rehypeKatex from 'rehype-katex';
import rehypeHighlight from 'rehype-highlight';
import remarkMath from 'remark-math';
import remarkGfm from 'remark-gfm';
import CodeBlock from './CodeBlock';
import { langSubset } from '~/utils/languages';
const Content = React.memo(({ content }) => {
return (
<>
<ReactMarkdown
remarkPlugins={[remarkGfm, [remarkMath, { singleDollarTextMath: false }]]}
rehypePlugins={[
[rehypeKatex, { output: 'mathml' }],
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: langSubset
}
]
]}
linkTarget="_new"
components={{
code,
p,
}}
>
{content}
</ReactMarkdown>
</>
);
});
const code = React.memo((props) => {
const { inline, className, children } = props;
const match = /language-(\w+)/.exec(className || '');
const lang = match && match[1];
if (inline) {
return <code className={className}>{children}</code>;
} else {
return (
<CodeBlock
lang={lang || 'text'}
codeChildren={children}
/>
);
}
});
const p = React.memo((props) => {
return <p className="whitespace-pre-wrap ">{props?.children}</p>;
});
const blinker = ({ node }) => {
if (node.type === 'text' && node.value === '█') {
return <span className="result-streaming">{node.value}</span>;
}
return null;
};
const em = React.memo(({ node, ...props }) => {
if (
props.children[0] &&
typeof props.children[0] === 'string' &&
props.children[0].startsWith('^')
) {
return <sup>{props.children[0].substring(1)}</sup>;
}
if (
props.children[0] &&
typeof props.children[0] === 'string' &&
props.children[0].startsWith('~')
) {
return <sub>{props.children[0].substring(1)}</sub>;
}
return <i {...props} />;
});
export default Content;

View file

@ -0,0 +1,37 @@
import React, { useState } from 'react';
import Clipboard from '~/components/svg/Clipboard';
import CheckMark from '~/components/svg/CheckMark';
const Embed = React.memo(({ children, lang = '', code, matched }) => {
const [buttonText, setButtonText] = useState('Copy code');
const isClicked = buttonText === 'Copy code';
const clickHandler = () => {
navigator.clipboard.writeText(code.trim());
setButtonText('Copied!');
setTimeout(() => {
setButtonText('Copy code');
}, 3000);
};
return (
<pre>
<div className="mb-4 rounded-md bg-black">
<div className="relative flex items-center rounded-tl-md rounded-tr-md bg-gray-800 px-4 py-2 font-sans text-xs text-gray-200">
<span className="">{lang === 'javascript' && !matched ? '' : lang}</span>
<button
className="ml-auto flex gap-2"
onClick={clickHandler}
disabled={!isClicked}
>
{isClicked ? <Clipboard /> : <CheckMark />}
{buttonText}
</button>
</div>
<div className="overflow-y-auto p-4">{children}</div>
</div>
</pre>
);
});
export default Embed;

View file

@ -0,0 +1,32 @@
import React, { useState, useEffect } from 'react';
import Highlighter from 'react-highlight';
import hljs from 'highlight.js';
import { languages } from '~/utils/languages';
const Highlight = React.memo(({ language, code }) => {
const [highlightedCode, setHighlightedCode] = useState(code);
const lang = language ? language : 'javascript';
useEffect(() => {
setHighlightedCode(hljs.highlight(code, { language: lang }).value);
}, [code, lang]);
return (
<pre>
{!highlightedCode ? (
// <code className={`hljs !whitespace-pre language-${lang ? lang: 'javascript'}`}>
<Highlighter className={`hljs !whitespace-pre language-${lang ? lang : 'javascript'}`}>
{code}
</Highlighter>
) : (
<code
className={`hljs language-${lang}`}
dangerouslySetInnerHTML={{ __html: highlightedCode }}
/>
)}
</pre>
);
});
export default Highlight;

View file

@ -0,0 +1,15 @@
import React from 'react';
export default function TabLink(a) {
return (
<a
href={a.href}
title={a.title}
className={a.className}
target="_blank"
rel="noopener noreferrer"
>
{a.children}
</a>
);
}

View file

@ -0,0 +1,160 @@
import React from 'react';
import TabLink from './TabLink';
import Markdown from 'markdown-to-jsx';
import Embed from './Embed';
import Highlight from './Highlight';
import regexSplit from '~/utils/regexSplit';
import { wrapperRegex } from '~/utils';
const { codeRegex, inLineRegex, markupRegex, languageMatch, newLineMatch } = wrapperRegex;
const mdOptions = {
wrapper: React.Fragment,
forceWrapper: true,
overrides: {
a: {
component: TabLink,
// props: {
// className: 'foo'
// }
}
}
};
const inLineWrap = (parts) => {
let previousElement = null;
return parts.map((part, i) => {
if (part.match(markupRegex)) {
const codeElement = <code key={i}>{part.slice(1, -1)}</code>;
if (previousElement && typeof previousElement !== 'string') {
// Append code element as a child to previous non-code element
previousElement = (
<Markdown
options={mdOptions}
key={i}
>
{previousElement}
{codeElement}
</Markdown>
);
return previousElement;
} else {
return codeElement;
}
} else {
previousElement = part;
return previousElement;
}
});
};
export default function TextWrapper({ text, generateCursor }) {
let embedTest = false;
let result = null;
// to match unenclosed code blocks
if (text.match(/```/g)?.length === 1) {
embedTest = true;
}
// match enclosed code blocks
if (text.match(codeRegex)) {
const parts = regexSplit(text);
// console.log(parts);
const codeParts = parts.map((part, i) => {
if (part.match(codeRegex)) {
let language = 'javascript';
let matched = false;
if (part.match(languageMatch)) {
language = part.match(languageMatch)[1].toLowerCase();
part = part.replace(languageMatch, '```');
matched = true;
// highlight.js language validation
// const validLanguage = languages.some((lang) => language === lang);
// part = validLanguage ? part.replace(languageMatch, '```') : part;
// language = validLanguage ? language : 'javascript';
}
part = part.replace(newLineMatch, '```');
return (
<Embed
key={i}
language={language}
code={part.slice(3, -3)}
matched={matched}
>
<Highlight
language={language}
code={part.slice(3, -3)}
/>
</Embed>
);
} else if (part.match(inLineRegex)) {
const innerParts = part.split(inLineRegex);
return inLineWrap(innerParts);
} else {
return (
<Markdown
options={mdOptions}
key={i}
>
{part}
</Markdown>
);
}
});
return <>{codeParts}</>; // return the wrapped text
} else if (embedTest) {
const language = text.match(/```(\w+)/)?.[1].toLowerCase() || 'javascript';
const parts = text.split(text.match(/```(\w+)/)?.[0] || '```');
const codeParts = parts.map((part, i) => {
if (i === 1) {
part = part.replace(/^\n+/, '');
return (
<Embed
key={i}
language={language}
>
<Highlight
code={part}
language={language}
/>
</Embed>
);
} else if (part.match(inLineRegex)) {
const innerParts = part.split(inLineRegex);
return inLineWrap(innerParts);
} else {
return (
<Markdown
options={mdOptions}
key={i}
>
{part}
</Markdown>
);
}
});
// return <>{codeParts}</>; // return the wrapped text
result = <>{codeParts}</>;
} else if (text.match(markupRegex)) {
// map over the parts and wrap any text between tildes with <code> tags
const parts = text.split(markupRegex);
const codeParts = inLineWrap(parts);
// return <>{codeParts}</>; // return the wrapped text
result = <>{codeParts}</>;
} else {
// return <Markdown options={mdOptions}>{text}</Markdown>;
result = <Markdown options={mdOptions}>{text}</Markdown>;
}
return (
<>
{result}
{generateCursor()}
</>
);
}

View file

@ -0,0 +1,25 @@
import React from 'react';
import TextWrapper from './TextWrapper';
import Content from './Content';
const Wrapper = React.memo(({ text, generateCursor, isCreatedByUser, searchResult }) => {
if (!isCreatedByUser && searchResult) {
return (
<Content
content={text}
generateCursor={generateCursor}
/>
);
} else if (!isCreatedByUser && !searchResult) {
return (
<TextWrapper
text={text}
generateCursor={generateCursor}
/>
);
} else if (isCreatedByUser) {
return <>{text}</>;
}
});
export default Wrapper;