feat: render tex (katex) and markdown

This commit is contained in:
diced 2022-12-05 18:16:31 -08:00
parent cfdcf05135
commit 950018673f
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
7 changed files with 1129 additions and 26 deletions

View file

@ -52,22 +52,27 @@
"exiftool-vendored": "^18.6.0",
"fflate": "^0.7.4",
"find-my-way": "^7.3.1",
"katex": "^0.16.3",
"minio": "^7.0.32",
"ms": "canary",
"multer": "^1.4.5-lts.1",
"next": "^13.0.0",
"otplib": "^12.0.1",
"prisma": "^4.5.0",
"prismjs": "^1.29.0",
"qrcode": "^1.5.1",
"react": "^18.2.0",
"react-chartjs-2": "^4.3.1",
"react-dom": "^18.2.0",
"react-feather": "^2.0.10",
"react-markdown": "^8.0.4",
"recoil": "^0.7.6",
"remark-gfm": "^3.0.1",
"sharp": "^0.31.1"
},
"devDependencies": {
"@types/cookie": "^0.5.1",
"@types/katex": "^0.14.0",
"@types/minio": "^7.0.14",
"@types/multer": "^1.4.7",
"@types/node": "^18.11.7",

View file

@ -1,14 +1,15 @@
import { Button, Group, NumberInput, PasswordInput, Select, Tabs, Title, Tooltip } from '@mantine/core';
import { Alert, Button, Card, Container, Group, Select, Tabs, Title } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { useModals } from '@mantine/modals';
import { showNotification, updateNotification } from '@mantine/notifications';
import { Prism } from '@mantine/prism';
import CodeInput from 'components/CodeInput';
import { ClockIcon, ImageIcon, TypeIcon, UploadIcon } from 'components/icons';
import { ImageIcon, TypeIcon, UploadIcon } from 'components/icons';
import KaTeX from 'components/render/KaTeX';
import Markdown from 'components/render/Markdown';
import PrismCode from 'components/render/PrismCode';
import exts from 'lib/exts';
import { userSelector } from 'lib/recoil/user';
import { expireReadToDate } from 'lib/utils/client';
import { Language } from 'prism-react-renderer';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import showFilesModal from './showFilesModal';
@ -24,6 +25,9 @@ export default function Text() {
const [options, setOpened, OptionsModal] = useUploadOptions();
const shouldRenderMarkdown = lang === 'md';
const shouldRenderTex = lang === 'tex';
const handleUpload = async () => {
const file = new File([value], 'text.' + lang);
@ -90,13 +94,26 @@ export default function Text() {
</Tabs.Panel>
<Tabs.Panel mt='sm' value='preview'>
<Prism
sx={(t) => ({ height: '80vh', backgroundColor: t.colors.dark[8] })}
withLineNumbers
language={lang as Language}
>
{value}
</Prism>
{shouldRenderMarkdown || shouldRenderTex ? (
<>
<Alert color='blue' variant='outline' sx={{ width: '100%' }}>
You are viewing a rendered version of your code
</Alert>
<Container>
<Card p='md' my='sm'>
{shouldRenderMarkdown && <Markdown code={value} />}
{shouldRenderTex && <KaTeX code={value} />}
</Card>
</Container>
</>
) : (
<PrismCode
sx={(t) => ({ height: '100vh', backgroundColor: t.colors.dark[8] })}
code={value}
ext={lang}
/>
)}
</Tabs.Panel>
</Tabs>

View file

@ -0,0 +1,51 @@
import { Alert } from '@mantine/core';
import katex, { ParseError } from 'katex';
import { useEffect, useState } from 'react';
import 'katex/dist/katex.min.css';
const sanitize = (str: string) => {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
};
export default function KaTeX({ code, ...props }) {
const [rendered, setRendered] = useState('');
const [error, setError] = useState<JSX.Element>();
const renderError = (error: ParseError | TypeError) => {
return (
<Alert title={error.name} color='red'>
{sanitize(error.message)}
</Alert>
);
};
useEffect(() => {
try {
const html = katex.renderToString(code, {
displayMode: true,
throwOnError: true,
errorColor: '#f44336',
});
setRendered(html);
} catch (e) {
if (e instanceof Error) {
setError(renderError(e));
} else {
throw e;
}
}
}, [rendered, error, code]);
if (error) return error;
return (
<div
dangerouslySetInnerHTML={{
__html: rendered,
}}
{...props}
/>
);
}

View file

@ -0,0 +1,31 @@
import { Code } from '@mantine/core';
import { Prism } from '@mantine/prism';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
export default function Markdown({ code, ...props }) {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
// @ts-ignore
<Prism language={match[1]} {...props}>
{String(children).replace(/\n$/, '')}
</Prism>
) : (
<Code {...props}>{children}</Code>
);
},
img({ node, ...props }) {
return <img {...props} style={{ maxWidth: '100%' }} />;
},
}}
{...props}
>
{code}
</ReactMarkdown>
);
}

View file

@ -0,0 +1,53 @@
import { Prism } from '@mantine/prism';
import { Prism as PrismLib } from 'prism-react-renderer';
import exts from 'lib/exts';
(typeof window === 'undefined' ? global : window).Prism = PrismLib;
require('prismjs/components/prism-markdown');
require('prismjs/components/prism-css');
require('prismjs/components/prism-javascript');
require('prismjs/components/prism-typescript');
require('prismjs/components/prism-java');
require('prismjs/components/prism-python');
require('prismjs/components/prism-ruby');
require('prismjs/components/prism-bash');
require('prismjs/components/prism-php');
require('prismjs/components/prism-perl');
require('prismjs/components/prism-sql');
require('prismjs/components/prism-xml-doc');
require('prismjs/components/prism-yaml');
require('prismjs/components/prism-c');
require('prismjs/components/prism-cpp');
require('prismjs/components/prism-csharp');
require('prismjs/components/prism-go');
require('prismjs/components/prism-docker');
require('prismjs/components/prism-toml');
require('prismjs/components/prism-ini');
require('prismjs/components/prism-batch');
require('prismjs/components/prism-latex');
require('prismjs/components/prism-r');
require('prismjs/components/prism-lua');
require('prismjs/components/prism-powershell');
require('prismjs/components/prism-rust');
require('prismjs/components/prism-swift');
require('prismjs/components/prism-scss');
require('prismjs/components/prism-json');
require('prismjs/components/prism-less');
require('prismjs/components/prism-scala');
require('prismjs/components/prism-kotlin');
require('prismjs/components/prism-visual-basic');
require('prismjs/components/prism-vim');
export default function PrismCode({ code, ext, ...props }) {
return (
<Prism
sx={(t) => ({ height: '100vh', backgroundColor: t.colors.dark[8] })}
withLineNumbers
language={exts[ext]?.toLowerCase()}
{...props}
>
{code}
</Prism>
);
}

View file

@ -1,27 +1,78 @@
import { Prism } from '@mantine/prism';
import { Box, Button, Card, Container } from '@mantine/core';
import KaTeX from 'components/render/KaTeX';
import Markdown from 'components/render/Markdown';
import PrismCode from 'components/render/PrismCode';
import config from 'lib/config';
import exts from 'lib/exts';
import prisma from 'lib/prisma';
import { checkPassword } from 'lib/util';
import { streamToString } from 'lib/utils/streams';
import { GetServerSideProps } from 'next';
import Head from 'next/head';
import { useState } from 'react';
export default function Code({ code, id, title }) {
export default function Code({ code, id, title, render, renderType }) {
const full_title = `${title} - Code (${id})`;
const [overrideRender, setOverrideRender] = useState(false);
return (
<>
<Head>
<title>{full_title}</title>
</Head>
<Prism
sx={(t) => ({ height: '100vh', backgroundColor: t.colors.dark[8] })}
withLineNumbers
language={exts[id.split('.').pop()]?.toLowerCase()}
{render && (
<Box
sx={(t) => ({
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
})}
py={5}
px='md'
>
{code}
</Prism>
{renderType === 'markdown' && (
<span>
You are {overrideRender ? 'not' : 'now'} viewing a rendered version of the markdown file.
</span>
)}
{renderType === 'tex' && (
<span>
You are {overrideRender ? 'not' : 'now'} viewing a KaTeX rendering version of the tex file.
</span>
)}
<Button mx='md' onClick={() => setOverrideRender(!overrideRender)} compact variant='light'>
View {overrideRender ? 'rendered' : 'raw'}
</Button>
</Box>
)}
{renderType === 'markdown' && !overrideRender && (
<Container p='md'>
<Markdown code={code} />
</Container>
)}
{renderType === 'tex' && !overrideRender && (
<Container p='md'>
<Card p={2}>
<KaTeX code={code} />
</Card>
</Container>
)}
{!render && (
<PrismCode
sx={(t) => ({ height: '100vh', backgroundColor: t.colors.dark[8] })}
code={code}
ext={id.split('.').pop()}
/>
)}
{render && overrideRender && (
<PrismCode
sx={(t) => ({ height: '100vh', backgroundColor: t.colors.dark[8] })}
code={code}
ext={id.split('.').pop()}
/>
)}
</>
);
}
@ -56,11 +107,23 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
context.res.setHeader('Cache-Control', 'public, max-age=2628000, stale-while-revalidate=86400');
let renderType;
if (file.file.endsWith('.md')) {
renderType = 'markdown';
} else if (file.file.endsWith('.tex')) {
renderType = 'tex';
} else {
renderType = null;
}
return {
props: {
code: await streamToString(data),
id: context.params.id as string,
title: config.website.title,
render: file.file.endsWith('.md') || file.file.endsWith('.tex'),
renderType,
},
};
};

893
yarn.lock

File diff suppressed because it is too large Load diff