Files
archtika/web-app/src/lib/utils.ts

192 lines
5.4 KiB
TypeScript
Raw Normal View History

2024-08-23 18:43:52 +02:00
import { Marked } from "marked";
2024-08-24 19:37:00 +02:00
import type { Renderer, Token } from "marked";
2024-08-23 18:43:52 +02:00
import { markedHighlight } from "marked-highlight";
2024-08-23 18:43:54 +02:00
import hljs from "highlight.js";
2024-08-24 19:37:00 +02:00
import GithubSlugger from "github-slugger";
import DOMPurify from "isomorphic-dompurify";
import { applyAction, deserialize } from "$app/forms";
2024-07-31 07:23:32 +02:00
export const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/svg+xml", "image/webp"];
const createMarkdownParser = (showToc = true) => {
2024-08-23 18:43:54 +02:00
const marked = new Marked();
marked.use({
async: false,
2024-08-23 18:43:54 +02:00
pedantic: false,
gfm: true
});
marked.use(
2024-08-23 18:43:52 +02:00
markedHighlight({
async: false,
2024-08-23 18:43:54 +02:00
langPrefix: "language-",
2024-08-23 18:43:52 +02:00
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : "plaintext";
return hljs.highlight(code, { language }).value;
}
2024-08-23 18:43:52 +02:00
})
);
2024-08-24 19:37:00 +02:00
const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;
2024-08-27 16:39:29 +02:00
const unescape = (html: string) => {
2024-08-24 19:37:00 +02:00
return html.replace(unescapeTest, (_, n) => {
n = n.toLowerCase();
if (n === "colon") return ":";
if (n.charAt(0) === "#") {
return n.charAt(1) === "x"
? String.fromCharCode(parseInt(n.substring(2), 16))
: String.fromCharCode(+n.substring(1));
}
return "";
});
2024-08-27 16:39:29 +02:00
};
2024-08-24 19:37:00 +02:00
let slugger = new GithubSlugger();
let headings: { text: string; raw: string; level: number; id: string }[] = [];
let sectionStack: { level: number; id: string }[] = [];
2024-08-27 16:39:29 +02:00
const gfmHeadingId = ({ prefix = "", showToc = true } = {}) => {
2024-08-24 19:37:00 +02:00
return {
renderer: {
heading(this: Renderer, { tokens, depth }: { tokens: Token[]; depth: number }) {
const text = this.parser.parseInline(tokens);
const raw = unescape(this.parser.parseInline(tokens, this.parser.textRenderer))
.trim()
2024-09-07 13:04:09 +02:00
.replace(/<[!a-z].*?>/gi, "");
2024-08-24 19:37:00 +02:00
const level = depth;
const id = `${prefix}${slugger.slug(raw.toLowerCase())}`;
const heading = { level, text, id, raw };
headings.push(heading);
let closingSections = "";
while (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].level >= level) {
sectionStack.pop();
closingSections += "</section>";
}
sectionStack.push({ level, id });
const openingSection = `<section id="${id}">`;
return `
${closingSections}
${openingSection}
<h${level}>
<a href="#${id}">${text}</a>
</h${level}>
`;
}
},
hooks: {
preprocess(src: string) {
headings = [];
sectionStack = [];
slugger = new GithubSlugger();
return src;
},
postprocess(html: string) {
const closingRemainingSection = "</section>".repeat(sectionStack.length);
2024-08-25 14:35:57 +02:00
let tableOfContents = "";
if (showToc && headings.length > 0) {
const tocItems = [];
let currentLevel = 0;
for (const { id, text, level } of headings) {
while (currentLevel < level - 1) {
tocItems.push("<ul>");
currentLevel++;
}
while (currentLevel > level - 1) {
tocItems.push("</ul>");
currentLevel--;
}
tocItems.push(`<li><a href="#${id}">${text}</a>`);
if (level > currentLevel) {
tocItems.push("<ul>");
currentLevel = level;
} else {
tocItems.push("</li>");
}
}
while (currentLevel > 0) {
tocItems.push("</ul></li>");
currentLevel--;
}
tableOfContents = `
<section id="table-of-contents">
<h2>
<a href="#table-of-contents">Table of contents</a>
</h2>
2024-08-25 14:35:57 +02:00
${tocItems.join("")}
</section>
2024-08-25 14:35:57 +02:00
`;
}
2024-08-24 19:37:00 +02:00
return `
${tableOfContents}
${html}
${closingRemainingSection}
`;
}
}
};
2024-08-27 16:39:29 +02:00
};
2024-08-24 19:37:00 +02:00
marked.use(gfmHeadingId({ showToc: showToc }));
2024-08-24 19:37:00 +02:00
2024-08-23 18:43:52 +02:00
return marked;
};
export const md = (markdownContent: string, showToc = true) => {
const marked = createMarkdownParser(showToc);
const html = DOMPurify.sanitize(marked.parse(markdownContent) as string);
return html;
2024-08-23 18:43:52 +02:00
};
export const handleImagePaste = async (event: ClipboardEvent, API_BASE_PREFIX: string) => {
2024-09-08 16:42:32 +02:00
const clipboardItems = Array.from(event.clipboardData?.items ?? []);
const file = clipboardItems.find((item) => item.type.startsWith("image/"));
2024-08-20 19:17:05 +02:00
if (!file) return null;
event.preventDefault();
const fileObject = file.getAsFile();
if (!fileObject) return;
const formData = new FormData();
formData.append("file", fileObject);
const request = await fetch("?/pasteImage", {
method: "POST",
body: formData
});
const result = deserialize(await request.clone().text());
applyAction(result);
const response = await request.json();
if (JSON.parse(response.data)[1]) {
const fileId = JSON.parse(response.data)[3];
const fileUrl = `${API_BASE_PREFIX}/rpc/retrieve_file?id=${fileId}`;
const target = event.target as HTMLTextAreaElement;
const newContent =
target.value.slice(0, target.selectionStart) +
`![](${fileUrl})` +
target.value.slice(target.selectionStart);
return newContent;
} else {
return "";
}
};