Make markdown synchronous and add ability to toggle TOC

This commit is contained in:
thiloho
2024-08-24 20:34:06 +02:00
parent 3c49d1e2f3
commit 891cdb46c8
7 changed files with 43 additions and 33 deletions

View File

@@ -1,7 +1,6 @@
button, button,
label, label,
select, select,
summary,
[role="button"], [role="button"],
[role="option"], [role="option"],
label[for="toggle-mobile-preview"] { label[for="toggle-mobile-preview"] {
@@ -14,7 +13,7 @@ textarea,
select, select,
a[role="button"], a[role="button"],
label[for="toggle-mobile-preview"], label[for="toggle-mobile-preview"],
summary { :not(.table-of-contents) > summary {
font: inherit; font: inherit;
color: inherit; color: inherit;
border: var(--border-primary); border: var(--border-primary);
@@ -47,22 +46,27 @@ a[role="button"] {
text-decoration: none; text-decoration: none;
} }
summary { :not(.table-of-contents) > summary {
max-inline-size: fit-content; max-inline-size: fit-content;
} }
button, button,
a[role="button"], a[role="button"],
label[for="toggle-mobile-preview"], label[for="toggle-mobile-preview"],
summary { :not(.table-of-contents) > summary {
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
} }
:is(button, a[role="button"], label[for="toggle-mobile-preview"], summary):hover { :is(
button,
a[role="button"],
label[for="toggle-mobile-preview"],
:not(.table-of-contents) > summary
):hover {
background-color: var(--bg-tertiary); background-color: var(--bg-tertiary);
} }
:is(a, button, input, textarea, select, summary):focus, :is(button, input, textarea, select):focus,
#toggle-mobile-preview:checked + label { #toggle-mobile-preview:checked + label {
outline: 0.125rem solid var(--color-accent); outline: 0.125rem solid var(--color-accent);
outline-offset: 0.25rem; outline-offset: 0.25rem;

View File

@@ -56,11 +56,7 @@
{#if fullPreview} {#if fullPreview}
<iframe src={previewContent} title="Preview"></iframe> <iframe src={previewContent} title="Preview"></iframe>
{:else} {:else}
{#await md(previewContent)} {@html md(previewContent)}
<p>Loading preview...</p>
{:then content}
{@html content}
{/await}
{/if} {/if}
</div> </div>

View File

@@ -34,8 +34,10 @@
<div class="container"> <div class="container">
{@html mainContent} {@html mainContent}
{#if articles.length > 0} {#if articles.length > 0}
<section class="articles"> <section class="articles" id="articles">
<h2>Articles</h2> <h2>
<a href="#articles">Articles</a>
</h2>
<ul class="unpadded"> <ul class="unpadded">
{#each articles as article} {#each articles as article}

View File

@@ -13,18 +13,18 @@ export const sortOptions = [
export const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/svg+xml", "image/webp"]; export const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/svg+xml", "image/webp"];
const createMarkdownParser = () => { const createMarkdownParser = (showToc = true) => {
const marked = new Marked(); const marked = new Marked();
marked.use({ marked.use({
async: true, async: false,
pedantic: false, pedantic: false,
gfm: true gfm: true
}); });
marked.use( marked.use(
markedHighlight({ markedHighlight({
async: true, async: false,
langPrefix: "language-", langPrefix: "language-",
highlight(code, lang) { highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : "plaintext"; const language = hljs.getLanguage(lang) ? lang : "plaintext";
@@ -52,7 +52,7 @@ const createMarkdownParser = () => {
let headings: { text: string; raw: string; level: number; id: string }[] = []; let headings: { text: string; raw: string; level: number; id: string }[] = [];
let sectionStack: { level: number; id: string }[] = []; let sectionStack: { level: number; id: string }[] = [];
function gfmHeadingId({ prefix = "" } = {}) { function gfmHeadingId({ prefix = "", showToc = true } = {}) {
return { return {
renderer: { renderer: {
heading(this: Renderer, { tokens, depth }: { tokens: Token[]; depth: number }) { heading(this: Renderer, { tokens, depth }: { tokens: Token[]; depth: number }) {
@@ -65,14 +65,12 @@ const createMarkdownParser = () => {
const heading = { level, text, id, raw }; const heading = { level, text, id, raw };
headings.push(heading); headings.push(heading);
// Close any sections that are at a higher level than the current heading
let closingSections = ""; let closingSections = "";
while (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].level >= level) { while (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].level >= level) {
sectionStack.pop(); sectionStack.pop();
closingSections += "</section>"; closingSections += "</section>";
} }
// Open a new section for this heading
sectionStack.push({ level, id }); sectionStack.push({ level, id });
const openingSection = `<section id="${id}">`; const openingSection = `<section id="${id}">`;
@@ -94,13 +92,11 @@ const createMarkdownParser = () => {
return src; return src;
}, },
postprocess(html: string) { postprocess(html: string) {
// Close any remaining open sections
const closingRemainingSection = "</section>".repeat(sectionStack.length); const closingRemainingSection = "</section>".repeat(sectionStack.length);
// Generate table of contents
const tableOfContents = const tableOfContents =
headings.length > 0 showToc && headings.length > 0
? `<details> ? `<details class="table-of-contents">
<summary>Table of contents</summary> <summary>Table of contents</summary>
<ul> <ul>
${headings ${headings
@@ -123,17 +119,16 @@ const createMarkdownParser = () => {
}; };
} }
marked.use(gfmHeadingId()); marked.use(gfmHeadingId({ showToc: showToc }));
return marked; return marked;
}; };
const marked = createMarkdownParser(); export const md = (markdownContent: string, showToc = true) => {
const marked = createMarkdownParser(showToc);
const html = marked.parse(markdownContent);
export const md = async (markdownContent: string) => { return html as string;
const html = await marked.parse(markdownContent);
return html;
}; };
export const handleImagePaste = async (event: ClipboardEvent, API_BASE_PREFIX: string) => { export const handleImagePaste = async (event: ClipboardEvent, API_BASE_PREFIX: string) => {

View File

@@ -60,7 +60,7 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true)
title: websiteData.title, title: websiteData.title,
logoType: websiteData.logo_type, logoType: websiteData.logo_type,
logo: websiteData.logo_text, logo: websiteData.logo_text,
mainContent: await md(websiteData.main_content ?? ""), mainContent: md(websiteData.main_content ?? "", false),
articles: websiteData.articles ?? [], articles: websiteData.articles ?? [],
footerAdditionalText: websiteData.additional_text ?? "" footerAdditionalText: websiteData.additional_text ?? ""
} }
@@ -74,7 +74,7 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true)
title: websiteData.title, title: websiteData.title,
logoType: websiteData.logo_type, logoType: websiteData.logo_type,
logo: websiteData.logo_text, logo: websiteData.logo_text,
mainContent: await md(websiteData.main_content ?? ""), mainContent: md(websiteData.main_content ?? "", false),
articles: websiteData.articles ?? [], articles: websiteData.articles ?? [],
footerAdditionalText: websiteData.additional_text ?? "" footerAdditionalText: websiteData.additional_text ?? ""
} }
@@ -117,7 +117,7 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true)
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${article.cover_image}` ? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${article.cover_image}`
: "", : "",
publicationDate: article.publication_date, publicationDate: article.publication_date,
mainContent: await md(article.main_content ?? ""), mainContent: md(article.main_content ?? ""),
footerAdditionalText: websiteData.additional_text ?? "" footerAdditionalText: websiteData.additional_text ?? ""
} }
})); }));
@@ -134,7 +134,7 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true)
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${article.cover_image}` ? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${article.cover_image}`
: "", : "",
publicationDate: article.publication_date, publicationDate: article.publication_date,
mainContent: await md(article.main_content ?? ""), mainContent: md(article.main_content ?? ""),
footerAdditionalText: websiteData.additional_text ?? "" footerAdditionalText: websiteData.additional_text ?? ""
} }
})); }));

View File

@@ -30,3 +30,7 @@ footer {
flex-direction: column; flex-direction: column;
gap: var(--space-xs); gap: var(--space-xs);
} }
.table-of-contents {
margin-block-end: var(--space-s);
}

View File

@@ -210,3 +210,12 @@ td {
padding-block: var(--space-3xs); padding-block: var(--space-3xs);
border: var(--border-primary); border: var(--border-primary);
} }
summary {
cursor: pointer;
}
:is(a, summary):focus {
outline: 0.125rem solid var(--color-accent);
outline-offset: 0.25rem;
}