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 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)}
|
|
@ -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>
|
||||
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
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}
|
||||
{...getLineProps({ line, key: i })}
|
||||
style={
|
||||
//@ts-ignore
|
||||
highlightedLines.includes((i + 1).toString())
|
||||
? {
|
||||
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
|
||||
}
|
||||
|
||||
type File = {
|
||||
export type File = {
|
||||
id: string
|
||||
title: string
|
||||
content: string
|
||||
html: string
|
||||
}
|
||||
|
||||
type Files = 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
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": {
|
||||
"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"
|
||||
|
|
|
@ -35,6 +35,9 @@ export class File extends Model {
|
|||
@Column
|
||||
sha!: string;
|
||||
|
||||
@Column
|
||||
html!: string;
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@BelongsTo(() => User, 'userId')
|
||||
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 { 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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react-jsxdev",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"noUnusedLocals": true,
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue