server: store and render markdown on server

This commit is contained in:
Max Leiter 2022-03-22 21:18:26 -07:00
parent 30e32e33cf
commit 19988e49ed
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
23 changed files with 536 additions and 37 deletions

View file

@ -1,5 +1,5 @@
import type { Document } from "@lib/types"
import DocumentComponent from "@components/document"
import DocumentComponent from "@components/edit-document"
import { ChangeEvent, memo, useCallback } from "react"
const DocumentList = ({ docs, removeDoc, updateDocContent, updateDocTitle }: {
@ -18,7 +18,6 @@ const DocumentList = ({ docs, removeDoc, updateDocContent, updateDocTitle }: {
<DocumentComponent
key={id}
remove={removeDoc(i)}
editable={true}
setContent={updateDocContent(i)}
setTitle={updateDocTitle(i)}
handleOnContentChange={handleOnChange(i)}

View file

@ -13,8 +13,6 @@ import Preview from "@components/preview"
// import Link from "next/link"
type Props = {
editable?: boolean
remove?: () => void
title?: string
content?: string
setTitle?: (title: string) => void
@ -22,7 +20,7 @@ type Props = {
handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
initialTab?: "edit" | "preview"
skeleton?: boolean
id?: string
remove?: () => void
}
const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
@ -53,7 +51,7 @@ const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
}
const Document = ({ remove, editable, title, content, setTitle, setContent, initialTab = 'edit', skeleton, id, handleOnContentChange }: Props) => {
const Document = ({ remove, title, content, setTitle, setContent, initialTab = 'edit', skeleton, handleOnContentChange }: Props) => {
const codeEditorRef = useRef<HTMLTextAreaElement>(null)
const [tab, setTab] = useState(initialTab)
// const height = editable ? "500px" : '100%'
@ -68,8 +66,7 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
const onTitleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => setTitle ? setTitle(event.target.value) : null, [setTitle])
const removeFile = useCallback((remove?: () => void) => {
console.log(remove)
const removeFile = useCallback((remove?: () => void) => () => {
if (remove) {
if (content && content.trim().length > 0) {
const confirmed = window.confirm("Are you sure you want to remove this file?")
@ -82,19 +79,13 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
}
}, [content])
const rawLink = () => {
if (id) {
return `/file/raw/${id}`
}
}
if (skeleton) {
return <>
<Spacer height={1} />
<Card marginBottom={'var(--gap)'} marginTop={'var(--gap)'} style={{ maxWidth: 980, margin: "0 auto" }}>
<div className={styles.fileNameContainer}>
<Skeleton width={275} height={36} />
{editable && <Skeleton width={36} height={36} />}
{remove && <Skeleton width={36} height={36} />}
</div>
<div className={styles.descriptionContainer}>
<div style={{ flexDirection: 'row', display: 'flex' }}><Skeleton width={125} height={36} /></div>
@ -118,17 +109,15 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
size={1.2}
font={1.2}
label="Filename"
disabled={!editable}
width={"100%"}
id={title}
/>
{remove && editable && <Button type="abort" ghost icon={<Trash />} auto height={'36px'} width={'36px'} onClick={() => removeFile(remove)} />}
{remove && <Button type="abort" ghost icon={<Trash />} auto height={'36px'} width={'36px'} onClick={() => removeFile(remove)} />}
</div>
<div className={styles.descriptionContainer}>
{tab === 'edit' && editable && <FormattingIcons setText={setContent} textareaRef={codeEditorRef} />}
{rawLink && id && <DownloadButton rawLink={rawLink()} />}
{tab === 'edit' && <FormattingIcons setText={setContent} textareaRef={codeEditorRef} />}
<Tabs onChange={handleTabChange} initialValue={initialTab} hideDivider leftSpace={0}>
<Tabs.Item label={editable ? "Edit" : "Raw"} value="edit">
<Tabs.Item label={"Edit"} value="edit">
{/* <textarea className={styles.lineCounter} wrap='off' readOnly ref={lineNumberRef}>1.</textarea> */}
<div style={{ marginTop: 'var(--gap)', display: 'flex', flexDirection: 'column' }}>
<Textarea
@ -137,7 +126,6 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
value={content}
onChange={handleOnContentChange}
width="100%"
disabled={!editable}
// TODO: Textarea should grow to fill parent if height == 100%
style={{ flex: 1, minHeight: 350 }}
resize="vertical"
@ -146,7 +134,7 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
</div>
</Tabs.Item>
<Tabs.Item label="Preview" value="preview">
<Preview height={height} fileId={id} title={title} content={content} />
<Preview height={height} title={title} content={content} />
</Tabs.Item>
</Tabs>

View file

@ -2,7 +2,6 @@ import { Button, useToasts, ButtonDropdown } from '@geist-ui/core'
import { useRouter } from 'next/router';
import { useCallback, useState } from 'react'
import generateUUID from '@lib/generate-uuid';
import DocumentComponent from '../document';
import FileDropzone from './drag-and-drop';
import styles from './post.module.css'
import Title from './title';
@ -10,7 +9,7 @@ import Cookies from 'js-cookie'
import type { PostVisibility, Document as DocumentType } from '@lib/types';
import PasswordModal from './password';
import getPostPath from '@lib/get-post-path';
import DocumentList from '@components/document-list';
import DocumentList from '@components/edit-document-list';
import { ChangeEvent } from 'react';
const Post = () => {

View file

@ -9,7 +9,7 @@ import { Input, Link, Text, Card, Spacer, Grid, Tooltip, Divider } from "@geist-
const FilenameInput = ({ title }: { title: string }) => <Input
value={title}
marginTop="var(--gap-double)"
marginTop="var(--gap)"
size={1.2}
font={1.2}
label="Filename"

View file

@ -1,11 +1,11 @@
import Header from "@components/header/header"
import PageSeo from "@components/page-seo"
import VisibilityBadge from "@components/visibility-badge"
import DocumentComponent from '@components/document'
import DocumentComponent from '@components/view-document'
import styles from './post-page.module.css'
import homeStyles from '@styles/Home.module.css'
import type { Post } from "@lib/types"
import type { File, Post } from "@lib/types"
import { Page, Button, Text } from "@geist-ui/core"
type Props = {
@ -51,14 +51,14 @@ const PostPage = ({ post }: Props) => {
Download as ZIP archive
</Button>
</div>
{post.files.map(({ id, content, title }: { id: any, content: string, title: string }) => (
{post.files.map(({ id, content, html, title }: File) => (
<DocumentComponent
key={id}
id={id}
content={content}
title={title}
editable={false}
initialTab={'preview'}
id={id}
html={html}
content={content}
/>
))}
</Page.Content>

View file

@ -0,0 +1,20 @@
import useTheme from "@lib/hooks/use-theme"
import { memo, useEffect, useState } from "react"
import styles from './preview.module.css'
type Props = {
height?: number | string
html: string
// file extensions we can highlight
}
const HtmlPreview = ({ height = 500, html }: Props) => {
const { theme } = useTheme()
return (<article
data-theme={theme}
className={styles.markdownPreview}
dangerouslySetInnerHTML={{ __html: html }}
style={{ height }} />)
}
export default HtmlPreview

View file

@ -0,0 +1,22 @@
import type { Document } from "@lib/types"
import DocumentComponent from "@components/edit-document"
import { ChangeEvent, memo, useCallback } from "react"
const DocumentList = ({ docs }: {
docs: Document[],
}) => {
return (<>{
docs.map(({ content, id, title }) => {
return (
<DocumentComponent
key={id}
content={content}
title={title}
/>
)
})
}
</>)
}
export default memo(DocumentList)

View file

@ -0,0 +1,41 @@
.input {
background: #efefef;
}
.descriptionContainer {
display: flex;
flex-direction: column;
min-height: 400px;
overflow: auto;
}
.fileNameContainer {
display: flex;
justify-content: space-between;
align-items: center;
height: 36px;
}
.fileNameContainer {
display: flex;
align-content: center;
}
.fileNameContainer > div {
/* Override geist-ui styling */
margin: 0 !important;
}
.textarea {
height: 100%;
}
.actionWrapper {
position: relative;
z-index: 1;
}
.actionWrapper .actions {
position: absolute;
right: 0;
}

View file

@ -0,0 +1,133 @@
import { memo, useRef, useState } from "react"
import styles from './document.module.css'
import Download from '@geist-ui/icons/download'
import ExternalLink from '@geist-ui/icons/externalLink'
import Skeleton from "react-loading-skeleton"
import { Button, ButtonGroup, Card, Input, Spacer, Tabs, Textarea, Tooltip } from "@geist-ui/core"
import Preview from "@components/preview"
import HtmlPreview from "@components/preview/html"
// import Link from "next/link"
type Props = {
title: string
html: string
initialTab?: "edit" | "preview"
skeleton?: boolean
id: string
content: string
}
const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
return (<div className={styles.actionWrapper}>
<ButtonGroup className={styles.actions}>
<Tooltip text="Download">
<a href={`${rawLink}?download=true`} target="_blank" rel="noopener noreferrer">
<Button
scale={2 / 3} px={0.6}
icon={<Download />}
auto
aria-label="Download"
/>
</a>
</Tooltip>
<Tooltip text="Open raw in new tab">
<a href={rawLink} target="_blank" rel="noopener noreferrer">
<Button
scale={2 / 3} px={0.6}
icon={<ExternalLink />}
auto
aria-label="Open raw file in new tab"
/>
</a>
</Tooltip>
</ButtonGroup>
</div>)
}
const Document = ({ content, title, html, initialTab = 'edit', skeleton, id }: Props) => {
const codeEditorRef = useRef<HTMLTextAreaElement>(null)
const [tab, setTab] = useState(initialTab)
// const height = editable ? "500px" : '100%'
const height = "100%";
const handleTabChange = (newTab: string) => {
if (newTab === 'edit') {
codeEditorRef.current?.focus()
}
setTab(newTab as 'edit' | 'preview')
}
const rawLink = () => {
if (id) {
return `/file/raw/${id}`
}
}
if (skeleton) {
return <>
<Spacer height={1} />
<Card marginBottom={'var(--gap)'} marginTop={'var(--gap)'} style={{ maxWidth: 980, margin: "0 auto" }}>
<div className={styles.fileNameContainer}>
<Skeleton width={275} height={36} />
</div>
<div className={styles.descriptionContainer}>
<div style={{ flexDirection: 'row', display: 'flex' }}><Skeleton width={125} height={36} /></div>
<Skeleton width={'100%'} height={350} />
</div >
</Card>
</>
}
return (
<>
<Spacer height={1} />
<Card marginBottom={'var(--gap)'} marginTop={'var(--gap)'} style={{ maxWidth: 980, margin: "0 auto" }}>
<div className={styles.fileNameContainer}>
<Input
value={title}
readOnly
marginTop="var(--gap-double)"
size={1.2}
font={1.2}
label="Filename"
width={"100%"}
id={title}
/>
</div>
<div className={styles.descriptionContainer}>
<DownloadButton rawLink={rawLink()} />
<Tabs onChange={handleTabChange} initialValue={initialTab} hideDivider leftSpace={0}>
<Tabs.Item label={"Raw"} value="edit">
{/* <textarea className={styles.lineCounter} wrap='off' readOnly ref={lineNumberRef}>1.</textarea> */}
<div style={{ marginTop: 'var(--gap)', display: 'flex', flexDirection: 'column' }}>
<Textarea
readOnly
ref={codeEditorRef}
value={content}
width="100%"
// TODO: Textarea should grow to fill parent if height == 100%
style={{ flex: 1, minHeight: 350 }}
resize="vertical"
className={styles.textarea}
/>
</div>
</Tabs.Item>
<Tabs.Item label="Preview" value="preview">
<HtmlPreview height={height} html={html} />
</Tabs.Item>
</Tabs>
</div >
</Card >
<Spacer height={1} />
</>
)
}
export default memo(Document)

View file

@ -116,6 +116,7 @@ const Code = ({ code, language, highlight, title, ...props }: {
key={i}
{...getLineProps({ line, key: i })}
style={
//@ts-ignore
highlightedLines.includes((i + 1).toString())
? {
background: 'var(--highlight)',

View file

@ -11,10 +11,11 @@ export type Document = {
id: string
}
type File = {
export type File = {
id: string
title: string
content: string
html: string
}
type Files = File[]

View file

@ -1,6 +1,6 @@
import styles from '@styles/Home.module.css'
import Header from '@components/header'
import Document from '@components/document'
import Document from '@components/edit-document'
import Image from 'next/image'
import ShiftBy from '@components/shift-by'
import PageSeo from '@components/page-seo'

18
server/config/config.json Normal file
View file

@ -0,0 +1,18 @@
{
"production": {
"database": "../drift.sqlite",
"host": "127.0.0.1",
"port": "3306",
"user": "root",
"password": "root",
"dialect": "sqlite"
},
"development": {
"database": "../drift.sqlite",
"host": "127.0.0.1",
"port": "3306",
"user": "root",
"password": "root",
"dialect": "sqlite"
}
}

View file

@ -0,0 +1,27 @@
const { DataTypes } = require("sequelize");
async function up(qi) {
try {
await qi.addColumn("Posts", "html", {
allowNull: true,
type: DataTypes.STRING,
});
} catch (e) {
console.error(e);
throw e;
}
}
async function down(qi) {
try {
await qi.removeColumn("Posts", "html");
} catch (e) {
console.error(e);
throw e;
}
}
module.exports = {
up,
down,
};

View file

@ -6,7 +6,8 @@
"scripts": {
"start": "ts-node index.ts",
"dev": "nodemon index.ts",
"build": "tsc -p ."
"build": "tsc -p .",
"migrate": "sequelize db:migrate"
},
"author": "",
"license": "ISC",
@ -18,7 +19,11 @@
"express": "^4.16.2",
"express-jwt": "^6.1.1",
"jsonwebtoken": "^8.5.1",
"marked": "^4.0.12",
"nodemon": "^2.0.15",
"prism-react-renderer": "^1.3.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"reflect-metadata": "^0.1.10",
"sequelize": "^6.17.0",
"sequelize-typescript": "^2.1.3",
@ -31,7 +36,9 @@
"@types/express": "^4.0.39",
"@types/express-jwt": "^6.0.4",
"@types/jsonwebtoken": "^8.5.8",
"@types/marked": "^4.0.3",
"@types/node": "^17.0.21",
"@types/react-dom": "^17.0.14",
"ts-node": "^10.6.0",
"tslint": "^6.1.3",
"typescript": "^4.6.2"

View file

@ -35,6 +35,9 @@ export class File extends Model {
@Column
sha!: string;
@Column
html!: string;
@ForeignKey(() => User)
@BelongsTo(() => User, 'userId')
user!: User;

View file

@ -0,0 +1,141 @@
import { marked } from 'marked'
import Highlight, { defaultProps, Language, } from 'prism-react-renderer'
import { renderToStaticMarkup } from 'react-dom/server'
//@ts-ignore
delete defaultProps.theme
// import linkStyles from '../components/link/link.module.css'
const renderer = new marked.Renderer()
renderer.heading = (text, level, _, slugger) => {
const id = slugger.slug(text)
const Component = `h${level}`
return renderToStaticMarkup(
//@ts-ignore
<Component>
<a href={`#${id}`} id={id} style={{ color: "inherit" }} >
{text}
</a>
</Component>
)
}
renderer.link = (href, _, text) => {
const isHrefLocal = href?.startsWith('/') || href?.startsWith('#')
if (isHrefLocal) {
return renderToStaticMarkup(
<a href={href || ''}>
{text}
</a>
)
}
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`
}
renderer.image = function (href, _, text) {
return `<Image loading="lazy" src="${href}" alt="${text}" layout="fill" />`
}
renderer.checkbox = () => ''
renderer.listitem = (text, task, checked) => {
if (task) {
return `<li class="reset"><span class="check">&#8203;<input type="checkbox" disabled ${checked ? 'checked' : ''
} /></span><span>${text}</span></li>`
}
return `<li>${text}</li>`
}
renderer.code = (code: string, language: string) => {
return renderToStaticMarkup(
<pre>
{/* {title && <code>{title} </code>} */}
{/* {language && title && <code style={{}}> {language} </code>} */}
<Code
language={language}
// title={title}
code={code}
// highlight={highlight}
/>
</pre>
)
}
marked.setOptions({
gfm: true,
breaks: true,
headerIds: true,
renderer,
})
const markdown = (markdown: string) => marked(markdown)
export default markdown
const Code = ({ code, language, highlight, title, ...props }: {
code: string,
language: string,
highlight?: string,
title?: string,
}) => {
if (!language)
return (
<>
<code {...props} dangerouslySetInnerHTML={{ __html: code }
} />
</>
)
const highlightedLines = highlight
//@ts-ignore
? highlight.split(',').reduce((lines, h) => {
if (h.includes('-')) {
// Expand ranges like 3-5 into [3,4,5]
const [start, end] = h.split('-').map(Number)
const x = Array(end - start + 1)
.fill(undefined)
.map((_, i) => i + start)
return [...lines, ...x]
}
return [...lines, Number(h)]
}, [])
: ''
// https://mdxjs.com/guides/syntax-harkedighlighting#all-together
return (
<>
<Highlight {...defaultProps} code={code.trim()} language={language as Language} >
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<code className={className} style={{ ...style }}>
{
tokens.map((line, i) => (
<div
key={i}
{...getLineProps({ line, key: i })}
style={
//@ts-ignore
highlightedLines.includes((i + 1).toString())
? {
background: 'var(--highlight)',
margin: '0 -1rem',
padding: '0 1rem',
}
: undefined
}
>
{
line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
))
}
</div>
))}
</code>
)}
</Highlight>
</>
)
}

View file

@ -6,6 +6,7 @@ import jwt, { UserJwtRequest } from '../lib/middleware/jwt';
import * as crypto from "crypto";
import { User } from '../lib/models/User';
import secretKey from '../lib/middleware/secret-key';
import markdown from '../lib/render-markdown';
export const posts = Router()
@ -45,10 +46,29 @@ posts.post('/create', jwt, async (req, res, next) => {
await newPost.save()
await newPost.$add('users', req.body.userId);
const newFiles = await Promise.all(req.body.files.map(async (file) => {
const renderAsMarkdown = ['markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', '']
const fileType = () => {
const pathParts = file.title.split(".")
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
return language
}
const type = fileType()
let contentToRender: string = (file.content || '');
if (!renderAsMarkdown.includes(type)) {
contentToRender =
`~~~${type}
${file.content}
~~~`
} else {
contentToRender = '\n' + file.content;
}
const html = markdown(contentToRender)
const newFile = new File({
title: file.title,
content: file.content,
sha: crypto.createHash('sha256').update(file.content).digest('hex').toString(),
html
})
await newFile.$set("user", req.body.userId);
@ -118,7 +138,7 @@ posts.get("/:id", async (req, res, next) => {
{
model: File,
as: "files",
attributes: ["id", "title", "content", "sha", "createdAt", "updatedAt"],
attributes: ["id", "title", "content", "sha", "createdAt", "updatedAt", "html"],
},
{
model: User,

View file

@ -4,7 +4,7 @@ import config from './lib/config';
import { sequelize } from './lib/sequelize';
(async () => {
await sequelize.sync();
await sequelize.sync({ alter: true });
createServer(app)
.listen(
config.port,

View file

@ -4,6 +4,7 @@
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"jsx": "react-jsxdev",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"noUnusedLocals": true,

View file

@ -157,6 +157,11 @@
dependencies:
"@types/node" "*"
"@types/marked@^4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.3.tgz#2098f4a77adaba9ce881c9e0b6baf29116e5acc4"
integrity sha512-HnMWQkLJEf/PnxZIfbm0yGJRRZYYMhb++O9M36UCTA9z53uPvVoSlAwJr3XOpDEryb7Hwl1qAx/MV6YIW1RXxg==
"@types/mime@^1":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
@ -172,6 +177,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644"
integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==
"@types/prop-types@*":
version "15.7.4"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
"@types/qs@*":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
@ -182,6 +192,27 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-dom@^17.0.14":
version "17.0.14"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.14.tgz#c8f917156b652ddf807711f5becbd2ab018dea9f"
integrity sha512-H03xwEP1oXmSfl3iobtmQ/2dHF5aBHr8aUMwyGZya6OW45G+xtdzmq6HkncefiBt5JU8DVyaWl/nWZbjZCnzAQ==
dependencies:
"@types/react" "*"
"@types/react@*":
version "17.0.41"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.41.tgz#6e179590d276394de1e357b3f89d05d7d3da8b85"
integrity sha512-chYZ9ogWUodyC7VUTRBfblysKLjnohhFY9bGLwvnUFFy48+vB9DikmB3lW0qTFmBcKSzmdglcvkHK71IioOlDA==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/scheduler@*":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/serve-static@*":
version "1.13.10"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
@ -671,6 +702,11 @@ crypto-random-string@^2.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
csstype@^3.0.2:
version "3.0.11"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@ -1377,7 +1413,7 @@ jake@^10.6.1:
filelist "^1.0.1"
minimatch "^3.0.4"
js-tokens@^4.0.0:
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
@ -1526,6 +1562,13 @@ lodash@^4.17.20, lodash@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loose-envify@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
@ -1562,6 +1605,11 @@ map-age-cleaner@^0.1.3:
dependencies:
p-defer "^1.0.0"
marked@^4.0.12:
version "4.0.12"
resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.12.tgz#2262a4e6fd1afd2f13557726238b69a48b982f7d"
integrity sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==
md5@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
@ -1913,6 +1961,11 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
prism-react-renderer@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d"
integrity sha512-xUeDMEz074d0zc5y6rxiMp/dlC7C+5IDDlaEUlcBOFE2wddz7hz5PNupb087mPwTt7T9BrFmewObfCBuf/LKwQ==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@ -1991,6 +2044,23 @@ rc@^1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-dom@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler "^0.20.2"
react@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
readable-stream@^2.0.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
@ -2108,6 +2178,14 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
semver-diff@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"