server: store and render markdown on server
This commit is contained in:
parent
30e32e33cf
commit
19988e49ed
23 changed files with 536 additions and 37 deletions
|
@ -1,5 +1,5 @@
|
||||||
import type { Document } from "@lib/types"
|
import type { Document } from "@lib/types"
|
||||||
import DocumentComponent from "@components/document"
|
import DocumentComponent from "@components/edit-document"
|
||||||
import { ChangeEvent, memo, useCallback } from "react"
|
import { ChangeEvent, memo, useCallback } from "react"
|
||||||
|
|
||||||
const DocumentList = ({ docs, removeDoc, updateDocContent, updateDocTitle }: {
|
const DocumentList = ({ docs, removeDoc, updateDocContent, updateDocTitle }: {
|
||||||
|
@ -18,7 +18,6 @@ const DocumentList = ({ docs, removeDoc, updateDocContent, updateDocTitle }: {
|
||||||
<DocumentComponent
|
<DocumentComponent
|
||||||
key={id}
|
key={id}
|
||||||
remove={removeDoc(i)}
|
remove={removeDoc(i)}
|
||||||
editable={true}
|
|
||||||
setContent={updateDocContent(i)}
|
setContent={updateDocContent(i)}
|
||||||
setTitle={updateDocTitle(i)}
|
setTitle={updateDocTitle(i)}
|
||||||
handleOnContentChange={handleOnChange(i)}
|
handleOnContentChange={handleOnChange(i)}
|
|
@ -13,8 +13,6 @@ import Preview from "@components/preview"
|
||||||
|
|
||||||
// import Link from "next/link"
|
// import Link from "next/link"
|
||||||
type Props = {
|
type Props = {
|
||||||
editable?: boolean
|
|
||||||
remove?: () => void
|
|
||||||
title?: string
|
title?: string
|
||||||
content?: string
|
content?: string
|
||||||
setTitle?: (title: string) => void
|
setTitle?: (title: string) => void
|
||||||
|
@ -22,7 +20,7 @@ type Props = {
|
||||||
handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
||||||
initialTab?: "edit" | "preview"
|
initialTab?: "edit" | "preview"
|
||||||
skeleton?: boolean
|
skeleton?: boolean
|
||||||
id?: string
|
remove?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
|
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 codeEditorRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const [tab, setTab] = useState(initialTab)
|
const [tab, setTab] = useState(initialTab)
|
||||||
// const height = editable ? "500px" : '100%'
|
// 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 onTitleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => setTitle ? setTitle(event.target.value) : null, [setTitle])
|
||||||
|
|
||||||
const removeFile = useCallback((remove?: () => void) => {
|
const removeFile = useCallback((remove?: () => void) => () => {
|
||||||
console.log(remove)
|
|
||||||
if (remove) {
|
if (remove) {
|
||||||
if (content && content.trim().length > 0) {
|
if (content && content.trim().length > 0) {
|
||||||
const confirmed = window.confirm("Are you sure you want to remove this file?")
|
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])
|
}, [content])
|
||||||
|
|
||||||
const rawLink = () => {
|
|
||||||
if (id) {
|
|
||||||
return `/file/raw/${id}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skeleton) {
|
if (skeleton) {
|
||||||
return <>
|
return <>
|
||||||
<Spacer height={1} />
|
<Spacer height={1} />
|
||||||
<Card marginBottom={'var(--gap)'} marginTop={'var(--gap)'} style={{ maxWidth: 980, margin: "0 auto" }}>
|
<Card marginBottom={'var(--gap)'} marginTop={'var(--gap)'} style={{ maxWidth: 980, margin: "0 auto" }}>
|
||||||
<div className={styles.fileNameContainer}>
|
<div className={styles.fileNameContainer}>
|
||||||
<Skeleton width={275} height={36} />
|
<Skeleton width={275} height={36} />
|
||||||
{editable && <Skeleton width={36} height={36} />}
|
{remove && <Skeleton width={36} height={36} />}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.descriptionContainer}>
|
<div className={styles.descriptionContainer}>
|
||||||
<div style={{ flexDirection: 'row', display: 'flex' }}><Skeleton width={125} height={36} /></div>
|
<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}
|
size={1.2}
|
||||||
font={1.2}
|
font={1.2}
|
||||||
label="Filename"
|
label="Filename"
|
||||||
disabled={!editable}
|
|
||||||
width={"100%"}
|
width={"100%"}
|
||||||
id={title}
|
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>
|
||||||
<div className={styles.descriptionContainer}>
|
<div className={styles.descriptionContainer}>
|
||||||
{tab === 'edit' && editable && <FormattingIcons setText={setContent} textareaRef={codeEditorRef} />}
|
{tab === 'edit' && <FormattingIcons setText={setContent} textareaRef={codeEditorRef} />}
|
||||||
{rawLink && id && <DownloadButton rawLink={rawLink()} />}
|
|
||||||
<Tabs onChange={handleTabChange} initialValue={initialTab} hideDivider leftSpace={0}>
|
<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> */}
|
{/* <textarea className={styles.lineCounter} wrap='off' readOnly ref={lineNumberRef}>1.</textarea> */}
|
||||||
<div style={{ marginTop: 'var(--gap)', display: 'flex', flexDirection: 'column' }}>
|
<div style={{ marginTop: 'var(--gap)', display: 'flex', flexDirection: 'column' }}>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
@ -137,7 +126,6 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
|
||||||
value={content}
|
value={content}
|
||||||
onChange={handleOnContentChange}
|
onChange={handleOnContentChange}
|
||||||
width="100%"
|
width="100%"
|
||||||
disabled={!editable}
|
|
||||||
// TODO: Textarea should grow to fill parent if height == 100%
|
// TODO: Textarea should grow to fill parent if height == 100%
|
||||||
style={{ flex: 1, minHeight: 350 }}
|
style={{ flex: 1, minHeight: 350 }}
|
||||||
resize="vertical"
|
resize="vertical"
|
||||||
|
@ -146,7 +134,7 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Item>
|
</Tabs.Item>
|
||||||
<Tabs.Item label="Preview" value="preview">
|
<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.Item>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { Button, useToasts, ButtonDropdown } from '@geist-ui/core'
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import generateUUID from '@lib/generate-uuid';
|
import generateUUID from '@lib/generate-uuid';
|
||||||
import DocumentComponent from '../document';
|
|
||||||
import FileDropzone from './drag-and-drop';
|
import FileDropzone from './drag-and-drop';
|
||||||
import styles from './post.module.css'
|
import styles from './post.module.css'
|
||||||
import Title from './title';
|
import Title from './title';
|
||||||
|
@ -10,7 +9,7 @@ import Cookies from 'js-cookie'
|
||||||
import type { PostVisibility, Document as DocumentType } from '@lib/types';
|
import type { PostVisibility, Document as DocumentType } from '@lib/types';
|
||||||
import PasswordModal from './password';
|
import PasswordModal from './password';
|
||||||
import getPostPath from '@lib/get-post-path';
|
import getPostPath from '@lib/get-post-path';
|
||||||
import DocumentList from '@components/document-list';
|
import DocumentList from '@components/edit-document-list';
|
||||||
import { ChangeEvent } from 'react';
|
import { ChangeEvent } from 'react';
|
||||||
|
|
||||||
const Post = () => {
|
const Post = () => {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { Input, Link, Text, Card, Spacer, Grid, Tooltip, Divider } from "@geist-
|
||||||
|
|
||||||
const FilenameInput = ({ title }: { title: string }) => <Input
|
const FilenameInput = ({ title }: { title: string }) => <Input
|
||||||
value={title}
|
value={title}
|
||||||
marginTop="var(--gap-double)"
|
marginTop="var(--gap)"
|
||||||
size={1.2}
|
size={1.2}
|
||||||
font={1.2}
|
font={1.2}
|
||||||
label="Filename"
|
label="Filename"
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import Header from "@components/header/header"
|
import Header from "@components/header/header"
|
||||||
import PageSeo from "@components/page-seo"
|
import PageSeo from "@components/page-seo"
|
||||||
import VisibilityBadge from "@components/visibility-badge"
|
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 styles from './post-page.module.css'
|
||||||
import homeStyles from '@styles/Home.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"
|
import { Page, Button, Text } from "@geist-ui/core"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -51,14 +51,14 @@ const PostPage = ({ post }: Props) => {
|
||||||
Download as ZIP archive
|
Download as ZIP archive
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{post.files.map(({ id, content, title }: { id: any, content: string, title: string }) => (
|
{post.files.map(({ id, content, html, title }: File) => (
|
||||||
<DocumentComponent
|
<DocumentComponent
|
||||||
key={id}
|
key={id}
|
||||||
id={id}
|
|
||||||
content={content}
|
|
||||||
title={title}
|
title={title}
|
||||||
editable={false}
|
|
||||||
initialTab={'preview'}
|
initialTab={'preview'}
|
||||||
|
id={id}
|
||||||
|
html={html}
|
||||||
|
content={content}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Page.Content>
|
</Page.Content>
|
||||||
|
|
20
client/components/preview/html.tsx
Normal file
20
client/components/preview/html.tsx
Normal 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
|
22
client/components/view-document-list/index.tsx
Normal file
22
client/components/view-document-list/index.tsx
Normal 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)
|
41
client/components/view-document/document.module.css
Normal file
41
client/components/view-document/document.module.css
Normal 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;
|
||||||
|
}
|
133
client/components/view-document/index.tsx
Normal file
133
client/components/view-document/index.tsx
Normal 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)
|
|
@ -116,6 +116,7 @@ const Code = ({ code, language, highlight, title, ...props }: {
|
||||||
key={i}
|
key={i}
|
||||||
{...getLineProps({ line, key: i })}
|
{...getLineProps({ line, key: i })}
|
||||||
style={
|
style={
|
||||||
|
//@ts-ignore
|
||||||
highlightedLines.includes((i + 1).toString())
|
highlightedLines.includes((i + 1).toString())
|
||||||
? {
|
? {
|
||||||
background: 'var(--highlight)',
|
background: 'var(--highlight)',
|
||||||
|
|
3
client/lib/types.d.ts
vendored
3
client/lib/types.d.ts
vendored
|
@ -11,10 +11,11 @@ export type Document = {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type File = {
|
export type File = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
content: string
|
content: string
|
||||||
|
html: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Files = File[]
|
type Files = File[]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import styles from '@styles/Home.module.css'
|
import styles from '@styles/Home.module.css'
|
||||||
import Header from '@components/header'
|
import Header from '@components/header'
|
||||||
import Document from '@components/document'
|
import Document from '@components/edit-document'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import ShiftBy from '@components/shift-by'
|
import ShiftBy from '@components/shift-by'
|
||||||
import PageSeo from '@components/page-seo'
|
import PageSeo from '@components/page-seo'
|
||||||
|
|
18
server/config/config.json
Normal file
18
server/config/config.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
27
server/migrations/20220323033259-postAddHtmlColumn.js
Normal file
27
server/migrations/20220323033259-postAddHtmlColumn.js
Normal 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,
|
||||||
|
};
|
|
@ -6,7 +6,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "ts-node index.ts",
|
"start": "ts-node index.ts",
|
||||||
"dev": "nodemon index.ts",
|
"dev": "nodemon index.ts",
|
||||||
"build": "tsc -p ."
|
"build": "tsc -p .",
|
||||||
|
"migrate": "sequelize db:migrate"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
@ -18,7 +19,11 @@
|
||||||
"express": "^4.16.2",
|
"express": "^4.16.2",
|
||||||
"express-jwt": "^6.1.1",
|
"express-jwt": "^6.1.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"marked": "^4.0.12",
|
||||||
"nodemon": "^2.0.15",
|
"nodemon": "^2.0.15",
|
||||||
|
"prism-react-renderer": "^1.3.1",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
"reflect-metadata": "^0.1.10",
|
"reflect-metadata": "^0.1.10",
|
||||||
"sequelize": "^6.17.0",
|
"sequelize": "^6.17.0",
|
||||||
"sequelize-typescript": "^2.1.3",
|
"sequelize-typescript": "^2.1.3",
|
||||||
|
@ -31,7 +36,9 @@
|
||||||
"@types/express": "^4.0.39",
|
"@types/express": "^4.0.39",
|
||||||
"@types/express-jwt": "^6.0.4",
|
"@types/express-jwt": "^6.0.4",
|
||||||
"@types/jsonwebtoken": "^8.5.8",
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
|
"@types/marked": "^4.0.3",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
|
"@types/react-dom": "^17.0.14",
|
||||||
"ts-node": "^10.6.0",
|
"ts-node": "^10.6.0",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"typescript": "^4.6.2"
|
"typescript": "^4.6.2"
|
||||||
|
|
|
@ -35,6 +35,9 @@ export class File extends Model {
|
||||||
@Column
|
@Column
|
||||||
sha!: string;
|
sha!: string;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
html!: string;
|
||||||
|
|
||||||
@ForeignKey(() => User)
|
@ForeignKey(() => User)
|
||||||
@BelongsTo(() => User, 'userId')
|
@BelongsTo(() => User, 'userId')
|
||||||
user!: User;
|
user!: User;
|
||||||
|
|
141
server/src/lib/render-markdown.tsx
Normal file
141
server/src/lib/render-markdown.tsx
Normal 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">​<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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import jwt, { UserJwtRequest } from '../lib/middleware/jwt';
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import { User } from '../lib/models/User';
|
import { User } from '../lib/models/User';
|
||||||
import secretKey from '../lib/middleware/secret-key';
|
import secretKey from '../lib/middleware/secret-key';
|
||||||
|
import markdown from '../lib/render-markdown';
|
||||||
|
|
||||||
export const posts = Router()
|
export const posts = Router()
|
||||||
|
|
||||||
|
@ -45,10 +46,29 @@ posts.post('/create', jwt, async (req, res, next) => {
|
||||||
await newPost.save()
|
await newPost.save()
|
||||||
await newPost.$add('users', req.body.userId);
|
await newPost.$add('users', req.body.userId);
|
||||||
const newFiles = await Promise.all(req.body.files.map(async (file) => {
|
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({
|
const newFile = new File({
|
||||||
title: file.title,
|
title: file.title,
|
||||||
content: file.content,
|
content: file.content,
|
||||||
sha: crypto.createHash('sha256').update(file.content).digest('hex').toString(),
|
sha: crypto.createHash('sha256').update(file.content).digest('hex').toString(),
|
||||||
|
html
|
||||||
})
|
})
|
||||||
|
|
||||||
await newFile.$set("user", req.body.userId);
|
await newFile.$set("user", req.body.userId);
|
||||||
|
@ -118,7 +138,7 @@ posts.get("/:id", async (req, res, next) => {
|
||||||
{
|
{
|
||||||
model: File,
|
model: File,
|
||||||
as: "files",
|
as: "files",
|
||||||
attributes: ["id", "title", "content", "sha", "createdAt", "updatedAt"],
|
attributes: ["id", "title", "content", "sha", "createdAt", "updatedAt", "html"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: User,
|
model: User,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import config from './lib/config';
|
||||||
import { sequelize } from './lib/sequelize';
|
import { sequelize } from './lib/sequelize';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await sequelize.sync();
|
await sequelize.sync({ alter: true });
|
||||||
createServer(app)
|
createServer(app)
|
||||||
.listen(
|
.listen(
|
||||||
config.port,
|
config.port,
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
"jsx": "react-jsxdev",
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
|
|
|
@ -157,6 +157,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@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":
|
"@types/mime@^1":
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644"
|
||||||
integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==
|
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@*":
|
"@types/qs@*":
|
||||||
version "6.9.7"
|
version "6.9.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
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@*":
|
"@types/serve-static@*":
|
||||||
version "1.13.10"
|
version "1.13.10"
|
||||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
|
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"
|
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
|
||||||
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
|
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:
|
dashdash@^1.12.0:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||||
|
@ -1377,7 +1413,7 @@ jake@^10.6.1:
|
||||||
filelist "^1.0.1"
|
filelist "^1.0.1"
|
||||||
minimatch "^3.0.4"
|
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"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
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"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
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:
|
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
|
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:
|
dependencies:
|
||||||
p-defer "^1.0.0"
|
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:
|
md5@^2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
|
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"
|
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||||
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
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:
|
process-nextick-args@~2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
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"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
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:
|
readable-stream@^2.0.6:
|
||||||
version "2.3.7"
|
version "2.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
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"
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
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:
|
semver-diff@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
|
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
|
||||||
|
|
Loading…
Reference in a new issue