client: beging markdown rendering on server

This commit is contained in:
Max Leiter 2022-03-22 17:37:21 -07:00
parent da46422764
commit d1ee9d857f
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
9 changed files with 737 additions and 49 deletions

View file

@ -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

View file

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

View file

@ -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

View file

@ -1,28 +1,11 @@
.main { .wrapper {
min-height: 100vh; height: 100% !important;
flex: 1; padding-bottom: var(--small-gap) !important;
display: flex;
flex-direction: column;
margin: 0 auto;
width: var(--main-content-width);
}
.container {
width: 100% !important; width: 100% !important;
} }
@media screen and (max-width: 768px) { .main {
.container { max-width: var(--main-content) !important;
width: 100%; margin: 0 auto !important;
margin: 0 auto !important; padding: 0 var(--gap) !important;
padding: 0;
}
.container h1 {
font-size: 2rem;
}
.main {
width: 100%;
}
} }

View file

@ -1,36 +1,251 @@
@import "./syntax.css";
@import "./markdown.css";
@import "./nprogress.css";
@import "./inter.css";
:root { :root {
--main-content-width: 800px; /* Spacing */
--page-nav-height: 60px; --gap-quarter: 0.25rem;
--gap: 8px; --gap-half: 0.5rem;
--gap-half: calc(var(--gap) / 2); --gap: 1rem;
--gap-double: calc(var(--gap) * 2); --gap-double: 2rem;
--border-radius: 4px; --small-gap: 4rem;
--font-size: 16px; --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) { [data-theme="light"] {
:root { --bg: #fff;
--main-content-width: 100%; --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, --token: #666;
body { --comment: #999;
padding: 0; --keyword: #000;
margin: 0; --name: #333;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, --highlight: #eaeaea;
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
li:before { ::selection {
content: "" !important; 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;
}
} }

100
client/styles/inter.css Normal file
View file

@ -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;
}

140
client/styles/markdown.css Normal file
View file

@ -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;
}

View file

@ -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;
}

24
client/styles/syntax.css Normal file
View file

@ -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);
}