From d1ee9d857fa0bf2c5e1e39c3bcafa12f4337f113 Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Tue, 22 Mar 2022 17:37:21 -0700 Subject: [PATCH] client: beging markdown rendering on server --- client/lib/hooks/use-trace-route.ts | 19 ++ client/lib/render-markdown.tsx | 140 +++++++++++++++ client/pages/api/markdown/[id].tsx | 44 +++++ client/styles/Home.module.css | 31 +--- client/styles/globals.css | 265 +++++++++++++++++++++++++--- client/styles/inter.css | 100 +++++++++++ client/styles/markdown.css | 140 +++++++++++++++ client/styles/nprogress.css | 23 +++ client/styles/syntax.css | 24 +++ 9 files changed, 737 insertions(+), 49 deletions(-) create mode 100644 client/lib/hooks/use-trace-route.ts create mode 100644 client/lib/render-markdown.tsx create mode 100644 client/pages/api/markdown/[id].tsx create mode 100644 client/styles/inter.css create mode 100644 client/styles/markdown.css create mode 100644 client/styles/nprogress.css create mode 100644 client/styles/syntax.css diff --git a/client/lib/hooks/use-trace-route.ts b/client/lib/hooks/use-trace-route.ts new file mode 100644 index 00000000..b0268e32 --- /dev/null +++ b/client/lib/hooks/use-trace-route.ts @@ -0,0 +1,19 @@ +import { useRef, useEffect } from "react"; + +function useTraceUpdate(props: { [key: string]: any }) { + const prev = useRef(props) + useEffect(() => { + const changedProps = Object.entries(props).reduce((ps, [k, v]) => { + if (prev.current[k] !== v) { + ps[k] = [prev.current[k], v] + } + return ps + }, {} as { [key: string]: any }) + if (Object.keys(changedProps).length > 0) { + console.log('Changed props:', changedProps) + } + prev.current = props + }); +} + +export default useTraceUpdate \ No newline at end of file diff --git a/client/lib/render-markdown.tsx b/client/lib/render-markdown.tsx new file mode 100644 index 00000000..5f6f467d --- /dev/null +++ b/client/lib/render-markdown.tsx @@ -0,0 +1,140 @@ +import Link from '@components/Link' +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 + + + {text} + + + ) +} + +renderer.link = (href, _, text) => { + const isHrefLocal = href?.startsWith('/') || href?.startsWith('#') + if (isHrefLocal) { + return renderToStaticMarkup( + + {text} + + ) + } + return `${text}` +} + +renderer.image = function (href, _, text) { + return `${text}` +} + +renderer.checkbox = () => '' +renderer.listitem = (text, task, checked) => { + if (task) { + return `
  • ${text}
  • ` + } + + return `
  • ${text}
  • ` +} + +renderer.code = (code: string, language: string) => { + return renderToStaticMarkup( +
    +            {/* {title && {title} } */}
    +            {/* {language && title &&  {language} } */}
    +            
    +        
    + ) +} + +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 ( + <> + + + ) + + 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 ( + <> + + {({ className, style, tokens, getLineProps, getTokenProps }) => ( + + { + tokens.map((line, i) => ( +
    + { + line.map((token, key) => ( + + )) + } +
    + ))} +
    + )} +
    + + ) +} diff --git a/client/pages/api/markdown/[id].tsx b/client/pages/api/markdown/[id].tsx new file mode 100644 index 00000000..359c33ec --- /dev/null +++ b/client/pages/api/markdown/[id].tsx @@ -0,0 +1,44 @@ +import type { NextApiHandler } from "next"; + +import markdown from "@lib/render-markdown"; + +const renderMarkdown: NextApiHandler = async (req, res) => { + const { id } = req.query + const file = await fetch(`${process.env.API_URL}/files/raw/${id}`, { + headers: { + 'Accept': 'text/plain', + 'x-secret-key': process.env.SECRET_KEY || '', + 'Authorization': `Bearer ${req.cookies['drift-token']}`, + } + }) + + + const json = await file.json() + const { content, title } = json + const renderAsMarkdown = ['m', 'markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', ''] + const fileType = () => { + const pathParts = title.split(".") + const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : "" + return language + } + const type = fileType() + let contentToRender: string = content; + + if (!renderAsMarkdown.includes(type)) { + contentToRender = `~~~${type} +${content} +~~~` + } + + if (typeof contentToRender !== 'string') { + res.status(400).send('content must be a string') + return + } + + res.setHeader('Content-Type', 'text/plain') + res.setHeader('Cache-Control', 'public, max-age=4800') + res.status(200).write(markdown(contentToRender)) + res.end() +} + +export default renderMarkdown diff --git a/client/styles/Home.module.css b/client/styles/Home.module.css index f770e0b2..3c3959ed 100644 --- a/client/styles/Home.module.css +++ b/client/styles/Home.module.css @@ -1,28 +1,11 @@ -.main { - min-height: 100vh; - flex: 1; - display: flex; - flex-direction: column; - margin: 0 auto; - width: var(--main-content-width); -} - -.container { +.wrapper { + height: 100% !important; + padding-bottom: var(--small-gap) !important; width: 100% !important; } -@media screen and (max-width: 768px) { - .container { - width: 100%; - margin: 0 auto !important; - padding: 0; - } - - .container h1 { - font-size: 2rem; - } - - .main { - width: 100%; - } +.main { + max-width: var(--main-content) !important; + margin: 0 auto !important; + padding: 0 var(--gap) !important; } diff --git a/client/styles/globals.css b/client/styles/globals.css index 4bcf1f4a..2e93b838 100644 --- a/client/styles/globals.css +++ b/client/styles/globals.css @@ -1,36 +1,251 @@ +@import "./syntax.css"; +@import "./markdown.css"; +@import "./nprogress.css"; +@import "./inter.css"; + :root { - --main-content-width: 800px; - --page-nav-height: 60px; - --gap: 8px; - --gap-half: calc(var(--gap) / 2); - --gap-double: calc(var(--gap) * 2); - --border-radius: 4px; - --font-size: 16px; + /* Spacing */ + --gap-quarter: 0.25rem; + --gap-half: 0.5rem; + --gap: 1rem; + --gap-double: 2rem; + --small-gap: 4rem; + --big-gap: 4rem; + --main-content: 55rem; + --radius: 8px; + --inline-radius: 5px; + + /* Typography */ + --font-sans: "Inter", -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, + Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + --font-mono: ui-monospace, "SFMono-Regular", "Consolas", "Liberation Mono", + "Menlo", monospace; + + /* Transitions */ + --transition: 0.1s ease-in-out; + --transition-slow: 0.3s ease-in-out; + + /* Dark Mode Colors */ + --bg: #131415; + --fg: #fafbfc; + --gray: #666; + --light-gray: #444; + --lighter-gray: #222; + --lightest-gray: #1a1a1a; + --article-color: #eaeaea; + --header-bg: rgba(19, 20, 21, 0.45); + --gray-alpha: rgba(255, 255, 255, 0.5); + --selection: rgba(255, 255, 255, 0.99); + + /* Forms */ + --input-height: 2.5rem; + --input-border: 1px solid var(--light-gray); + --input-border-focus: 1px solid var(--gray); + --input-border-error: 1px solid var(--red); + --input-bg: var(--bg); + --input-fg: var(--fg); + --input-placeholder-fg: var(--light-gray); + --input-bg-hover: var(--lightest-gray); + + /* Syntax Highlighting */ + --token: #999; + --comment: #999; + --keyword: #fff; + --name: #fff; + --highlight: #2e2e2e; } -@media screen and (max-width: 768px) { - :root { - --main-content-width: 100%; - } -} +[data-theme="light"] { + --bg: #fff; + --fg: #000; + --gray: #888; + --light-gray: #dedede; + --lighter-gray: #f5f5f5; + --lightest-gray: #fafafa; + --article-color: #212121; + --header-bg: rgba(255, 255, 255, 0.8); + --gray-alpha: rgba(19, 20, 21, 0.5); + --selection: rgba(0, 0, 0, 0.99); -html, -body { - padding: 0; - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; -} - -a { - color: inherit; - text-decoration: none; + --token: #666; + --comment: #999; + --keyword: #000; + --name: #333; + --highlight: #eaeaea; } * { box-sizing: border-box; } -li:before { - content: "" !important; +::selection { + text-shadow: none; + background: var(--selection); + color: var(--bg); +} + +html { + line-height: 1.5; +} + +html, +body { + padding: 0; + margin: 0; + font-size: 15px; + background: var(--bg); + color: var(--fg); + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + min-height: 100vh; + font-family: var(--font-sans); + display: flex; + flex-direction: column; +} + +p, +li { + letter-spacing: -0.33px; + font-size: 1.125rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--font-sans); + font-weight: 600; + line-height: 1.75; +} + +h1 { + font-size: 2.5rem; + font-weight: 600; + line-height: 1.25; + letter-spacing: -0.89px; +} + +h2 { + font-size: 2rem; + letter-spacing: -0.69px; +} + +h3 { + font-size: 1.5rem; + letter-spacing: -0.47px; +} + +h4 { + font-size: 1.25rem; + letter-spacing: -0.33px; +} + +hr { + border: none; + border-bottom: 1px solid var(--light-gray); +} + +blockquote { + font-style: italic; + margin: 0; + padding-left: 1rem; + border-left: 3px solid var(--light-gray); +} + +button { + border: none; + padding: 0; + margin: 0; + line-height: inherit; + font-size: inherit; +} + +p a, +a.reset { + outline: none; + color: var(--fg); + text-decoration: none; +} + +p a:hover, +p a:focus, +p a:active, +a.reset:hover, +a.reset:focus { + color: var(--gray); +} + +pre, +code { + font-family: var(--font-mono); +} + +.clamp { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + overflow: hidden; +} + +.clamp-2 { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; +} + +.flex { + display: flex; +} + +kbd { + font-family: var(--font-sans); + font-size: 1rem; + padding: 2px 7px; + font-weight: 600; + background: var(--lighter-gray); + border-radius: 5px; +} + +summary { + cursor: pointer; + outline: none; +} + +details { + border-radius: var(--radius); + background: var(--lightest-gray); + padding: 1rem; + border-radius: var(--radius); +} + +@media print { + :root { + --bg: #fff; + --fg: #000; + --gray: #888; + --light-gray: #dedede; + --lighter-gray: #f5f5f5; + --lightest-gray: #fafafa; + --article-color: #212121; + --header-bg: rgba(255, 255, 255, 0.8); + --gray-alpha: rgba(19, 20, 21, 0.5); + --selection: rgba(0, 0, 0, 0.99); + + --token: #666; + --comment: #999; + --keyword: #000; + --name: #333; + --highlight: #eaeaea; + } + + * { + text-shadow: none !important; + } } diff --git a/client/styles/inter.css b/client/styles/inter.css new file mode 100644 index 00000000..61f0df75 --- /dev/null +++ b/client/styles/inter.css @@ -0,0 +1,100 @@ +/* latin */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 100; + font-display: block; + src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2) + format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 200; + font-display: block; + src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2) + format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 300; + font-display: block; + src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2) + format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + font-display: block; + src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2) + format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 500; + font-display: block; + src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2) + format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 600; + font-display: block; + src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2) + format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 700; + font-display: block; + src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2) + format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 800; + font-display: block; + src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2) + format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 900; + font-display: block; + src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2) + format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} diff --git a/client/styles/markdown.css b/client/styles/markdown.css new file mode 100644 index 00000000..fb3fc107 --- /dev/null +++ b/client/styles/markdown.css @@ -0,0 +1,140 @@ +article { + max-width: var(--main-content); + margin: 0 auto; + line-height: 1.9; +} + +article > * + * { + margin-top: 2em; +} + +article p { + color: var(--article-color); +} + +article img { + max-width: 100%; + /* width: var(--main-content); */ + width: auto; + margin: auto; + display: block; + border-radius: var(--radius); +} + +article [id]::before { + content: ""; + display: block; + height: 70px; + margin-top: -70px; + visibility: hidden; +} + +/* Lists */ + +article ul { + padding: 0; + list-style-position: inside; + list-style-type: circle; +} + +article ol { + padding: 0; + list-style-position: inside; +} + +article ul li.reset { + display: flex; + align-items: flex-start; + + list-style-type: none; + margin-left: -0.5rem; +} + +article ul li.reset .check { + display: flex; + align-items: center; + margin-right: 0.51rem; +} + +/* Checkbox */ + +input[type="checkbox"] { + vertical-align: middle; + appearance: none; + display: inline-block; + background-origin: border-box; + user-select: none; + flex-shrink: 0; + height: 1rem; + width: 1rem; + background-color: var(--bg); + color: var(--fg); + border: 1px solid var(--fg); + border-radius: 3px; +} + +input[type="checkbox"]:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='black' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e"); + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +html[data-theme="light"] input[type="checkbox"]:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e"); +} + +input[type="checkbox"]:focus { + outline: none; + box-shadow: 0 0 0 2px var(--gray); + border-color: var(--fg); +} + +/* Code Snippets */ + +.token-line:not(:last-child) { + min-height: 1.4rem; +} + +article *:not(pre) > code { + font-weight: 600; + font-family: var(--font-sans); + font-size: 1rem; + padding: 0 3px; +} + +article *:not(pre) > code::before, +article *:not(pre) > code::after { + content: "\`"; + color: var(--gray); + user-select: none; +} + +article pre { + overflow-x: auto; + background: var(--lightest-gray); + border-radius: var(--inline-radius); + line-height: 1.8; + padding: 1rem; + font-size: 0.875rem; +} + +/* Linkable Headers */ + +.header-link { + color: inherit; + text-decoration: none; +} + +.header-link::after { + opacity: 0; + content: "#"; + margin-left: var(--gap-half); + color: var(--gray); +} + +.header-link:hover::after { + opacity: 1; +} diff --git a/client/styles/nprogress.css b/client/styles/nprogress.css new file mode 100644 index 00000000..0db6e676 --- /dev/null +++ b/client/styles/nprogress.css @@ -0,0 +1,23 @@ +#nprogress { + pointer-events: none; +} + +#nprogress .bar { + position: fixed; + z-index: 2000; + top: 0; + left: 0; + width: 100%; + height: 5px; + background: var(--fg); +} + +#nprogress::after { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 5px; + background: transparent; +} diff --git a/client/styles/syntax.css b/client/styles/syntax.css new file mode 100644 index 00000000..42a27295 --- /dev/null +++ b/client/styles/syntax.css @@ -0,0 +1,24 @@ +.keyword { + font-weight: bold; + color: var(--keyword); +} + +.token.operator, +.token.punctuation, +.token.string, +.token.number, +.token.builtin, +.token.variable { + color: var(--token); +} + +.token.comment { + color: var(--comment); +} + +.token.class-name, +.token.function, +.token.tag, +.token.attr-name { + color: var(--name); +}