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";
|
2024-08-04 16:15:09 +02:00
|
|
|
|
2024-07-31 07:23:32 +02:00
|
|
|
export const sortOptions = [
|
|
|
|
|
{ value: "creation-time", text: "Creation time" },
|
|
|
|
|
{ value: "last-modified", text: "Last modified" },
|
|
|
|
|
{ value: "title-a-to-z", text: "Title - A to Z" },
|
|
|
|
|
{ value: "title-z-to-a", text: "Title - Z to A" }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/svg+xml", "image/webp"];
|
2024-08-04 16:15:09 +02:00
|
|
|
|
2024-08-23 18:43:52 +02:00
|
|
|
const createMarkdownParser = () => {
|
2024-08-23 18:43:54 +02:00
|
|
|
const marked = new Marked();
|
|
|
|
|
|
|
|
|
|
marked.use({
|
|
|
|
|
async: true,
|
|
|
|
|
pedantic: false,
|
|
|
|
|
gfm: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
marked.use(
|
2024-08-23 18:43:52 +02:00
|
|
|
markedHighlight({
|
2024-08-23 18:43:54 +02:00
|
|
|
async: true,
|
|
|
|
|
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-18 18:17:59 +02:00
|
|
|
}
|
2024-08-23 18:43:52 +02:00
|
|
|
})
|
|
|
|
|
);
|
2024-08-18 18:17:59 +02:00
|
|
|
|
2024-08-24 19:37:00 +02:00
|
|
|
const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;
|
|
|
|
|
|
|
|
|
|
function unescape(html: string) {
|
|
|
|
|
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 "";
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let slugger = new GithubSlugger();
|
|
|
|
|
let headings: { text: string; raw: string; level: number; id: string }[] = [];
|
|
|
|
|
let sectionStack: { level: number; id: string }[] = [];
|
|
|
|
|
|
|
|
|
|
function gfmHeadingId({ prefix = "" } = {}) {
|
|
|
|
|
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()
|
|
|
|
|
.replace(/<[!\/a-z].*?>/gi, "");
|
|
|
|
|
const level = depth;
|
|
|
|
|
const id = `${prefix}${slugger.slug(raw.toLowerCase())}`;
|
|
|
|
|
const heading = { level, text, id, raw };
|
|
|
|
|
headings.push(heading);
|
|
|
|
|
|
|
|
|
|
// Close any sections that are at a higher level than the current heading
|
|
|
|
|
let closingSections = "";
|
|
|
|
|
while (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].level >= level) {
|
|
|
|
|
sectionStack.pop();
|
|
|
|
|
closingSections += "</section>";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open a new section for this heading
|
|
|
|
|
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) {
|
|
|
|
|
// Close any remaining open sections
|
|
|
|
|
const closingRemainingSection = "</section>".repeat(sectionStack.length);
|
|
|
|
|
|
|
|
|
|
// Generate table of contents
|
|
|
|
|
const tableOfContents =
|
|
|
|
|
headings.length > 0
|
|
|
|
|
? `<details>
|
|
|
|
|
<summary>Table of contents</summary>
|
|
|
|
|
<ul>
|
|
|
|
|
${headings
|
|
|
|
|
.map(
|
|
|
|
|
({ id, text, level }) => `
|
|
|
|
|
<li><a href="#${id}" class="h${level}">${text}</a></li>`
|
|
|
|
|
)
|
|
|
|
|
.join("")}
|
|
|
|
|
</ul>
|
|
|
|
|
</details>`
|
|
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
${tableOfContents}
|
|
|
|
|
${html}
|
|
|
|
|
${closingRemainingSection}
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
marked.use(gfmHeadingId());
|
|
|
|
|
|
2024-08-23 18:43:52 +02:00
|
|
|
return marked;
|
|
|
|
|
};
|
2024-08-18 18:17:59 +02:00
|
|
|
|
2024-08-23 18:43:52 +02:00
|
|
|
const marked = createMarkdownParser();
|
2024-08-18 18:17:59 +02:00
|
|
|
|
2024-08-23 18:43:52 +02:00
|
|
|
export const md = async (markdownContent: string) => {
|
|
|
|
|
const html = await marked.parse(markdownContent);
|
2024-08-18 18:17:59 +02:00
|
|
|
|
2024-08-23 18:43:52 +02:00
|
|
|
return html;
|
|
|
|
|
};
|
2024-08-14 19:33:41 +02:00
|
|
|
|
2024-08-19 20:33:23 +02:00
|
|
|
export const handleImagePaste = async (event: ClipboardEvent, API_BASE_PREFIX: string) => {
|
2024-08-19 19:31:41 +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;
|
2024-08-19 19:31:41 +02:00
|
|
|
|
|
|
|
|
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 response = await request.json();
|
|
|
|
|
const fileId = JSON.parse(response.data)[1];
|
|
|
|
|
const fileUrl = `${API_BASE_PREFIX}/rpc/retrieve_file?id=${fileId}`;
|
|
|
|
|
|
|
|
|
|
const target = event.target as HTMLTextAreaElement;
|
|
|
|
|
const newContent =
|
|
|
|
|
target.value.slice(0, target.selectionStart) +
|
|
|
|
|
`` +
|
|
|
|
|
target.value.slice(target.selectionStart);
|
|
|
|
|
|
|
|
|
|
return newContent;
|
|
|
|
|
};
|