feat: add some highlighting to markdown renderer
This commit is contained in:
		| @@ -31,7 +31,7 @@ export function RecipeHero( | ||||
|           <img | ||||
|             src={imageUrl} | ||||
|             alt="Recipe Banner" | ||||
|             style={{ objectPosition: "0% 30%" }} | ||||
|             style={{ objectPosition: "0% 25%" }} | ||||
|             class="absolute object-cover w-full h-full -z-10" | ||||
|           /> | ||||
|         )} | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import { ComponentChildren } from "preact"; | ||||
| import { menu } from "@lib/menus.ts"; | ||||
| import { CSS, KATEX_CSS,render } from "https://deno.land/x/gfm/mod.ts"; | ||||
| import { Head } from "$fresh/runtime.ts"; | ||||
|  | ||||
| export type Props = { | ||||
|   children: ComponentChildren; | ||||
| @@ -15,6 +17,10 @@ export const MainLayout = ({ children, url }: Props) => { | ||||
|       class="md:grid mx-auto" | ||||
|       style={{ gridTemplateColumns: "200px 1fr", maxWidth: "1024px" }} | ||||
|     > | ||||
|       <Head> | ||||
|         <style>{CSS}</style> | ||||
|         <style>{KATEX_CSS}</style> | ||||
|       </Head> | ||||
|       <aside class="p-4 hidden md:block"> | ||||
|         <nav class="min-h-fit rounded-3xl p-3 grid gap-3 fixed t-0"> | ||||
|           {menu.map((m) => { | ||||
|   | ||||
| @@ -1,12 +1,13 @@ | ||||
| import { unified } from "https://esm.sh/unified@10.1.2"; | ||||
| import { render } from "https://deno.land/x/gfm@0.2.5/mod.ts"; | ||||
| import "https://esm.sh/prismjs@1.29.0/components/prism-typescript?no-check"; | ||||
| import "https://esm.sh/prismjs@1.29.0/components/prism-bash?no-check"; | ||||
| import "https://esm.sh/prismjs@1.29.0/components/prism-rust?no-check"; | ||||
| import remarkParse from "https://esm.sh/remark-parse@10.0.2"; | ||||
| import remarkStringify from "https://esm.sh/remark-stringify@10.0.3"; | ||||
| import remarkFrontmatter, { | ||||
|   Root, | ||||
| } from "https://esm.sh/remark-frontmatter@4.0.1"; | ||||
| import remarkRehype from "https://esm.sh/remark-rehype@10.1.0"; | ||||
| import rehypeSanitize from "https://esm.sh/rehype-sanitize@5.0.1"; | ||||
| import rehypeStringify from "https://esm.sh/rehype-stringify@9.0.3"; | ||||
| import * as cache from "@lib/cache/documents.ts"; | ||||
| import { SILVERBULLET_SERVER } from "@lib/env.ts"; | ||||
| import { fixRenderedMarkdown } from "@lib/helpers.ts"; | ||||
| @@ -90,14 +91,9 @@ export function parseDocument(doc: string) { | ||||
| } | ||||
|  | ||||
| export function renderMarkdown(doc: string) { | ||||
|   const out = unified() | ||||
|     .use(remarkParse) | ||||
|     .use(remarkRehype) | ||||
|     .use(rehypeSanitize) | ||||
|     .use(rehypeStringify) | ||||
|     .processSync(doc); | ||||
|  | ||||
|   return String(out); | ||||
|   return render(doc, { | ||||
|     allowMath: true, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export type ParsedDocument = ReturnType<typeof parseDocument>; | ||||
|   | ||||
							
								
								
									
										79
									
								
								lib/highlight.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								lib/highlight.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| import { refractor } from "https://esm.sh/refractor@4.8.1"; | ||||
| import { visit } from "https://esm.sh/unist-util-visit@5.0.0"; | ||||
| import { toString } from "https://esm.sh/hast-util-to-string@2.0.0"; | ||||
|  | ||||
| import jsx from "https://esm.sh/refractor/lang/jsx"; | ||||
| import javascript from "https://esm.sh/refractor/lang/javascript"; | ||||
| import css from "https://esm.sh/refractor/lang/css"; | ||||
| import cssExtras from "https://esm.sh/refractor/lang/css-extras"; | ||||
| import jsExtras from "https://esm.sh/refractor/lang/js-extras"; | ||||
| import sql from "https://esm.sh/refractor/lang/sql"; | ||||
| import typescript from "https://esm.sh/refractor/lang/typescript"; | ||||
| import swift from "https://esm.sh/refractor/lang/swift"; | ||||
| import objectivec from "https://esm.sh/refractor/lang/objectivec"; | ||||
| import markdown from "https://esm.sh/refractor/lang/markdown"; | ||||
| import json from "https://esm.sh/refractor/lang/json"; | ||||
|  | ||||
| refractor.register(jsx); | ||||
| refractor.register(json); | ||||
| refractor.register(typescript); | ||||
| refractor.register(javascript); | ||||
| refractor.register(css); | ||||
| refractor.register(cssExtras); | ||||
| refractor.register(jsExtras); | ||||
| refractor.register(sql); | ||||
| refractor.register(swift); | ||||
| refractor.register(objectivec); | ||||
| refractor.register(markdown); | ||||
|  | ||||
| refractor.alias({ jsx: ["js"] }); | ||||
| refractor.alias({typescript:["ts"]}) | ||||
|  | ||||
| const getLanguage = (node) => { | ||||
|   const className = node.properties.className || []; | ||||
|  | ||||
|   for (const classListItem of className) { | ||||
|     if (classListItem.slice(0, 9) === "language-") { | ||||
|       return classListItem.slice(9).toLowerCase(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return null; | ||||
| }; | ||||
|  | ||||
| const rehypePrism = (options) => { | ||||
|   options = options || {}; | ||||
|  | ||||
|   return (tree) => { | ||||
|     visit(tree, "element", visitor); | ||||
|   }; | ||||
|  | ||||
|   function visitor(node, index, parent) { | ||||
|     if (!parent || parent.tagName !== "pre" || node.tagName !== "code") { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const lang = getLanguage(node); | ||||
|  | ||||
|     if (lang === null) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     let result; | ||||
|     try { | ||||
|       parent.properties.className = (parent.properties.className || []).concat( | ||||
|         "language-" + lang, | ||||
|       ); | ||||
|       result = refractor.highlight(toString(node), lang); | ||||
|     } catch (err) { | ||||
|       if (options.ignoreMissing && /Unknown language/.test(err.message)) { | ||||
|         return; | ||||
|       } | ||||
|       throw err; | ||||
|     } | ||||
|  | ||||
|     node.children = result; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export default rehypePrism; | ||||
| @@ -35,8 +35,7 @@ export const isYoutubeLink = (link: string) => { | ||||
|   try { | ||||
|     const url = new URL(link); | ||||
|     return ["youtu.be", "youtube.com","www.youtube.com" ].includes(url.hostname); | ||||
|   } catch (err) { | ||||
|     console.log(err); | ||||
|   } catch (_err) { | ||||
|     return false; | ||||
|   } | ||||
| }; | ||||
|   | ||||
| @@ -16,6 +16,7 @@ export default function App({ Component }: AppProps) { | ||||
|           rel="stylesheet" | ||||
|         /> | ||||
|         <link href="/global.css" rel="stylesheet" /> | ||||
|         <link href="/prism-material-dark.css" rel="stylesheet" /> | ||||
|       </Head> | ||||
|       <Component /> | ||||
|     </> | ||||
|   | ||||
| @@ -73,6 +73,12 @@ async function processCreateArticle( | ||||
|         return `})`; | ||||
|       } | ||||
|  | ||||
|       if (!src.startsWith("https://") && !src.startsWith("http://")) { | ||||
|         return `}/${ | ||||
|           src.replace(/^\//, "") | ||||
|         })`; | ||||
|       } | ||||
|  | ||||
|       return ``; | ||||
|     }, | ||||
|   }); | ||||
| @@ -87,9 +93,16 @@ async function processCreateArticle( | ||||
|       } | ||||
|  | ||||
|       if (href.startsWith("#")) { | ||||
|         if (content.length < 2) return ""; | ||||
|         return `[${content}](${url.href}#${href})`.replace("##", "#"); | ||||
|       } | ||||
|  | ||||
|       if (!href.startsWith("https://") && !href.startsWith("http://")) { | ||||
|         return `[${content}](${url.origin.replace(/\/$/, "")}/${ | ||||
|           href.replace(/^\//, "") | ||||
|         })`; | ||||
|       } | ||||
|  | ||||
|       return `[${content}](${href})`; | ||||
|     }, | ||||
|   }); | ||||
|   | ||||
| @@ -42,7 +42,9 @@ export default function Greet(props: PageProps<Article>) { | ||||
|           <YoutubePlayer link={article.meta.link} /> | ||||
|         )} | ||||
|         <pre | ||||
|           class="whitespace-break-spaces" | ||||
|           class="whitespace-break-spaces markdown-body" | ||||
|           data-color-mode="dark" | ||||
|           data-dark-theme="dark" | ||||
|           dangerouslySetInnerHTML={{ __html: article.content || "" }} | ||||
|         > | ||||
|           {article.content||""} | ||||
|   | ||||
							
								
								
									
										205
									
								
								static/prism-material-dark.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								static/prism-material-dark.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,205 @@ | ||||
| code[class*="language-"], | ||||
| pre[class*="language-"] { | ||||
| 	text-align: left; | ||||
| 	white-space: pre; | ||||
| 	word-spacing: normal; | ||||
| 	word-break: normal; | ||||
| 	word-wrap: normal; | ||||
| 	color: #eee; | ||||
| 	background: #2f2f2f; | ||||
| 	font-family: Roboto Mono, monospace; | ||||
| 	font-size: 1em; | ||||
| 	line-height: 1.5em; | ||||
|  | ||||
| 	-moz-tab-size: 4; | ||||
| 	-o-tab-size: 4; | ||||
| 	tab-size: 4; | ||||
|  | ||||
| 	-webkit-hyphens: none; | ||||
| 	-moz-hyphens: none; | ||||
| 	-ms-hyphens: none; | ||||
| 	hyphens: none; | ||||
| } | ||||
|  | ||||
| code[class*="language-"]::-moz-selection, | ||||
| pre[class*="language-"]::-moz-selection, | ||||
| code[class*="language-"] ::-moz-selection, | ||||
| pre[class*="language-"] ::-moz-selection { | ||||
| 	background: #363636; | ||||
| } | ||||
|  | ||||
| code[class*="language-"]::selection, | ||||
| pre[class*="language-"]::selection, | ||||
| code[class*="language-"] ::selection, | ||||
| pre[class*="language-"] ::selection { | ||||
| 	background: #363636; | ||||
| } | ||||
|  | ||||
| :not(pre) > code[class*="language-"] { | ||||
| 	white-space: normal; | ||||
| 	border-radius: 0.2em; | ||||
| 	padding: 0.1em; | ||||
| } | ||||
|  | ||||
| pre[class*="language-"] { | ||||
| 	overflow: auto; | ||||
| 	position: relative; | ||||
| 	margin: 0.5em 0; | ||||
| 	padding: 1.25em 1em; | ||||
| } | ||||
|  | ||||
| .language-css > code, | ||||
| .language-sass > code, | ||||
| .language-scss > code { | ||||
| 	color: #fd9170; | ||||
| } | ||||
|  | ||||
| [class*="language-"] .namespace { | ||||
| 	opacity: 0.7; | ||||
| } | ||||
|  | ||||
| .token.atrule { | ||||
| 	color: #c792ea; | ||||
| } | ||||
|  | ||||
| .token.attr-name { | ||||
| 	color: #ffcb6b; | ||||
| } | ||||
|  | ||||
| .token.attr-value { | ||||
| 	color: #a5e844; | ||||
| } | ||||
|  | ||||
| .token.attribute { | ||||
| 	color: #a5e844; | ||||
| } | ||||
|  | ||||
| .token.boolean { | ||||
| 	color: #c792ea; | ||||
| } | ||||
|  | ||||
| .token.builtin { | ||||
| 	color: #ffcb6b; | ||||
| } | ||||
|  | ||||
| .token.cdata { | ||||
| 	color: #80cbc4; | ||||
| } | ||||
|  | ||||
| .token.char { | ||||
| 	color: #80cbc4; | ||||
| } | ||||
|  | ||||
| .token.class { | ||||
| 	color: #ffcb6b; | ||||
| } | ||||
|  | ||||
| .token.class-name { | ||||
| 	color: #f2ff00; | ||||
| } | ||||
|  | ||||
| .token.comment { | ||||
| 	color: #616161; | ||||
| } | ||||
|  | ||||
| .token.constant { | ||||
| 	color: #c792ea; | ||||
| } | ||||
|  | ||||
| .token.deleted { | ||||
| 	color: #ff6666; | ||||
| } | ||||
|  | ||||
| .token.doctype { | ||||
| 	color: #616161; | ||||
| } | ||||
|  | ||||
| .token.entity { | ||||
| 	color: #ff6666; | ||||
| } | ||||
|  | ||||
| .token.function { | ||||
| 	color: #c792ea; | ||||
| } | ||||
|  | ||||
| .token.hexcode { | ||||
| 	color: #f2ff00; | ||||
| } | ||||
|  | ||||
| .token.id { | ||||
| 	color: #c792ea; | ||||
| 	font-weight: bold; | ||||
| } | ||||
|  | ||||
| .token.important { | ||||
| 	color: #c792ea; | ||||
| 	font-weight: bold; | ||||
| } | ||||
|  | ||||
| .token.inserted { | ||||
| 	color: #80cbc4; | ||||
| } | ||||
|  | ||||
| .token.keyword { | ||||
| 	color: #c792ea; | ||||
| } | ||||
|  | ||||
| .token.number { | ||||
| 	color: #fd9170; | ||||
| } | ||||
|  | ||||
| .token.operator { | ||||
| 	color: #89ddff; | ||||
| } | ||||
|  | ||||
| .token.prolog { | ||||
| 	color: #616161; | ||||
| } | ||||
|  | ||||
| .token.property { | ||||
| 	color: #80cbc4; | ||||
| } | ||||
|  | ||||
| .token.pseudo-class { | ||||
| 	color: #a5e844; | ||||
| } | ||||
|  | ||||
| .token.pseudo-element { | ||||
| 	color: #a5e844; | ||||
| } | ||||
|  | ||||
| .token.punctuation { | ||||
| 	color: #89ddff; | ||||
| } | ||||
|  | ||||
| .token.regex { | ||||
| 	color: #f2ff00; | ||||
| } | ||||
|  | ||||
| .token.selector { | ||||
| 	color: #ff6666; | ||||
| } | ||||
|  | ||||
| .token.string { | ||||
| 	color: #a5e844; | ||||
| } | ||||
|  | ||||
| .token.symbol { | ||||
| 	color: #c792ea; | ||||
| } | ||||
|  | ||||
| .token.tag { | ||||
| 	color: #ff6666; | ||||
| } | ||||
|  | ||||
| .token.unit { | ||||
| 	color: #fd9170; | ||||
| } | ||||
|  | ||||
| .token.url { | ||||
| 	color: #ff6666; | ||||
| } | ||||
|  | ||||
| .token.variable { | ||||
| 	color: #ff6666; | ||||
| } | ||||
							
								
								
									
										169
									
								
								static/prism-twilight.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								static/prism-twilight.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| /** | ||||
|  * prism.js Twilight theme | ||||
|  * Based (more or less) on the Twilight theme originally of Textmate fame. | ||||
|  * @author Remy Bach | ||||
|  */ | ||||
| code[class*="language-"], | ||||
| pre[class*="language-"] { | ||||
| 	color: white; | ||||
| 	background: none; | ||||
| 	font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; | ||||
| 	font-size: 1em; | ||||
| 	text-align: left; | ||||
| 	text-shadow: 0 -.1em .2em black; | ||||
| 	white-space: pre; | ||||
| 	word-spacing: normal; | ||||
| 	word-break: normal; | ||||
| 	word-wrap: normal; | ||||
| 	line-height: 1.5; | ||||
|  | ||||
| 	-moz-tab-size: 4; | ||||
| 	-o-tab-size: 4; | ||||
| 	tab-size: 4; | ||||
|  | ||||
| 	-webkit-hyphens: none; | ||||
| 	-moz-hyphens: none; | ||||
| 	-ms-hyphens: none; | ||||
| 	hyphens: none; | ||||
| } | ||||
|  | ||||
| pre[class*="language-"], | ||||
| :not(pre) > code[class*="language-"] { | ||||
| 	background: hsl(0, 0%, 8%); /* #141414 */ | ||||
| } | ||||
|  | ||||
| /* Code blocks */ | ||||
| pre[class*="language-"] { | ||||
| 	border-radius: .5em; | ||||
| 	border: .3em solid hsl(0, 0%, 33%); /* #282A2B */ | ||||
| 	box-shadow: 1px 1px .5em black inset; | ||||
| 	margin: .5em 0; | ||||
| 	overflow: auto; | ||||
| 	padding: 1em; | ||||
| } | ||||
|  | ||||
| pre[class*="language-"]::-moz-selection { | ||||
| 	/* Firefox */ | ||||
| 	background: hsl(200, 4%, 16%); /* #282A2B */ | ||||
| } | ||||
|  | ||||
| pre[class*="language-"]::selection { | ||||
| 	/* Safari */ | ||||
| 	background: hsl(200, 4%, 16%); /* #282A2B */ | ||||
| } | ||||
|  | ||||
| /* Text Selection colour */ | ||||
| pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, | ||||
| code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { | ||||
| 	text-shadow: none; | ||||
| 	background: hsla(0, 0%, 93%, 0.15); /* #EDEDED */ | ||||
| } | ||||
|  | ||||
| pre[class*="language-"]::selection, pre[class*="language-"] ::selection, | ||||
| code[class*="language-"]::selection, code[class*="language-"] ::selection { | ||||
| 	text-shadow: none; | ||||
| 	background: hsla(0, 0%, 93%, 0.15); /* #EDEDED */ | ||||
| } | ||||
|  | ||||
| /* Inline code */ | ||||
| :not(pre) > code[class*="language-"] { | ||||
| 	border-radius: .3em; | ||||
| 	border: .13em solid hsl(0, 0%, 33%); /* #545454 */ | ||||
| 	box-shadow: 1px 1px .3em -.1em black inset; | ||||
| 	padding: .15em .2em .05em; | ||||
| 	white-space: normal; | ||||
| } | ||||
|  | ||||
| .token.comment, | ||||
| .token.prolog, | ||||
| .token.doctype, | ||||
| .token.cdata { | ||||
| 	color: hsl(0, 0%, 47%); /* #777777 */ | ||||
| } | ||||
|  | ||||
| .token.punctuation { | ||||
| 	opacity: .7; | ||||
| } | ||||
|  | ||||
| .token.namespace { | ||||
| 	opacity: .7; | ||||
| } | ||||
|  | ||||
| .token.tag, | ||||
| .token.boolean, | ||||
| .token.number, | ||||
| .token.deleted { | ||||
| 	color: hsl(14, 58%, 55%); /* #CF6A4C */ | ||||
| } | ||||
|  | ||||
| .token.keyword, | ||||
| .token.property, | ||||
| .token.selector, | ||||
| .token.constant, | ||||
| .token.symbol, | ||||
| .token.builtin { | ||||
| 	color: hsl(53, 89%, 79%); /* #F9EE98 */ | ||||
| } | ||||
|  | ||||
| .token.attr-name, | ||||
| .token.attr-value, | ||||
| .token.string, | ||||
| .token.char, | ||||
| .token.operator, | ||||
| .token.entity, | ||||
| .token.url, | ||||
| .language-css .token.string, | ||||
| .style .token.string, | ||||
| .token.variable, | ||||
| .token.inserted { | ||||
| 	color: hsl(76, 21%, 52%); /* #8F9D6A */ | ||||
| } | ||||
|  | ||||
| .token.atrule { | ||||
| 	color: hsl(218, 22%, 55%); /* #7587A6 */ | ||||
| } | ||||
|  | ||||
| .token.regex, | ||||
| .token.important { | ||||
| 	color: hsl(42, 75%, 65%); /* #E9C062 */ | ||||
| } | ||||
|  | ||||
| .token.important, | ||||
| .token.bold { | ||||
| 	font-weight: bold; | ||||
| } | ||||
| .token.italic { | ||||
| 	font-style: italic; | ||||
| } | ||||
|  | ||||
| .token.entity { | ||||
| 	cursor: help; | ||||
| } | ||||
|  | ||||
| /* Markup */ | ||||
| .language-markup .token.tag, | ||||
| .language-markup .token.attr-name, | ||||
| .language-markup .token.punctuation { | ||||
| 	color: hsl(33, 33%, 52%); /* #AC885B */ | ||||
| } | ||||
|  | ||||
| /* Make the tokens sit above the line highlight so the colours don't look faded. */ | ||||
| .token { | ||||
| 	position: relative; | ||||
| 	z-index: 1; | ||||
| } | ||||
|  | ||||
| .line-highlight.line-highlight { | ||||
| 	background: hsla(0, 0%, 33%, 0.25); /* #545454 */ | ||||
| 	background: linear-gradient(to right, hsla(0, 0%, 33%, .1) 70%, hsla(0, 0%, 33%, 0)); /* #545454 */ | ||||
| 	border-bottom: 1px dashed hsl(0, 0%, 33%); /* #545454 */ | ||||
| 	border-top: 1px dashed hsl(0, 0%, 33%); /* #545454 */ | ||||
| 	margin-top: 0.75em; /* Same as .prism’s padding-top */ | ||||
| 	z-index: 0; | ||||
| } | ||||
|  | ||||
| .line-highlight.line-highlight:before, | ||||
| .line-highlight.line-highlight[data-end]:after { | ||||
| 	background-color: hsl(215, 15%, 59%); /* #8794A6 */ | ||||
| 	color: hsl(24, 20%, 95%); /* #F5F2F0 */ | ||||
| } | ||||
		Reference in New Issue
	
	Block a user