feat: add some highlighting to markdown renderer

This commit is contained in:
max_richter 2023-08-02 17:21:03 +02:00
parent 21871a72e6
commit 70d16913f3
10 changed files with 485 additions and 15 deletions

View File

@ -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"
/>
)}

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

@ -73,6 +73,12 @@ async function processCreateArticle(
return `![${alt}](${url.origin}${src.replace(/$\//, "")})`;
}
if (!src.startsWith("https://") && !src.startsWith("http://")) {
return `![${alt}](${url.origin.replace(/\/$/, "")}/${
src.replace(/^\//, "")
})`;
}
return `![${alt}](${src})`;
},
});
@ -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})`;
},
});

View File

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

View 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
View 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 .prisms 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 */
}