Markdown Library for the Browser; Marked !
Here is how I use the npm package marked
to convert my markdown files to HTML.
This snippet is from my new project, I’m using Vue 3 and Tauri.
Code
markdown.ts
import { marked } from "marked";
import { markedHighlight } from "marked-highlight";
import hljs from "highlight.js";
import { convertFileSrc } from "@tauri-apps/api/tauri";
import { documentDir, sep } from "@tauri-apps/api/path";
import { convertImages } from "./image";
export async function markdown(filepath: string, content: string) {
const localContent = (await convertImages(
filepath.split(sep).slice(0, -1).join(sep),
content
)) as string;
const renderer = {
list: (body: string, ordered: boolean, start: number) => {
return `<ul class="list-disc list-inside leading-4">${body}</ul>`;
},
link: (href: string, title: string, text: string) => {
return `<a class="link" href="${href}" alt=${title} target="_blank">${text}</a>`;
},
html(html: string, block: boolean) {
return html;
},
image(href: string, title: string, text: string) {
return `<img src="${convertFileSrc(documentDirPath)}${encodeURIComponent(
sep
)}${href}" alt="${title}">`;
},
table(header: string, body: string) {
return `<table class="table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
},
};
const documentDirPath = await documentDir();
marked.use(
markedHighlight({
async: true,
langPrefix: "hljs language-",
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : "plaintext";
return hljs.highlight(code, { language }).value;
},
})
);
marked.use({ renderer });
return marked.parse(localContent, {
mangle: false,
headerIds: false,
});
}
image.ts
import { documentDir, join, sep } from "@tauri-apps/api/path";
import { convertFileSrc } from "@tauri-apps/api/tauri";
import {
imageDataOnlyRe,
imageLocalRe,
imageMarkdownRe,
imageSrcRe,
} from "../Regexes/Images";
import { base64ToArrayBuffer } from "./utils";
export async function convertImages(
filepath: string,
content: string
): Promise<string> {
const newContent: string[] = [];
const documentDirPath = await documentDir();
for await (let line of content.split("\n")) {
const rule1 = imageMarkdownRe;
const rule2 = imageLocalRe;
const match1 = rule1.exec(line);
const match2 = rule2.exec(line);
const alt = rule2.exec(line)?.[2];
const width = rule2.exec(line)?.[3];
let data: any;
try {
if (line !== "" && (match1 || match2)) {
if (match1)
data = await fetch(`${convertFileSrc(documentDirPath)}${match1[2]}`);
if (match2) {
if (match2[1].startsWith("."))
data = await fetch(convertFileSrc(await join(filepath, match2[1])));
else
data = await fetch(
`${convertFileSrc(documentDirPath)}${match2[1]}`
);
}
const blob = await data.blob();
const buffer = await blob.arrayBuffer();
const base64 = btoa(
new Uint8Array(buffer).reduce(
(data, byte) => data + String.fromCharCode(byte),
""
)
);
let extension = blob.type;
newContent.push(
`<img src="data:${extension};base64,${base64}" alt="${alt}" width="${width}" class="mx-auto">`
);
} else {
newContent.push(line);
}
} catch {
newContent.push(line);
}
}
return newContent.join("\n");
}
export async function extractURLOrData(
url: string
): Promise<{ type: string | undefined; buffer: ArrayBuffer }> {
const u = url.replace(imageSrcRe, "$1");
if (!u) throw new Error("Invalid image url, nothing to do");
if (u.startsWith("data:")) {
const type: string | undefined = u?.match(imageDataOnlyRe)?.[1];
const base64: string | undefined = u?.match(imageDataOnlyRe)?.[2];
if (!base64)
throw new Error("unable to extract the base64 content from the image");
return { type, buffer: base64ToArrayBuffer(base64) };
}
if (u.startsWith("blob:")) {
const data = await fetch(u);
if (!data.body)
throw new Error("Something went wrong while fetching the image content");
const blob = await data.blob();
const buffer = await blob.arrayBuffer();
const type = blob.type;
return { type: type.split("/")[1], buffer };
}
if (u.startsWith("http:") || u.startsWith("https:")) {
console.log("http or https", u);
const data = await fetch(u);
if (!data.body)
throw new Error("Something went wrong while fetching the image content");
const blob = await data.blob();
const buffer = await blob.arrayBuffer();
const type = blob.type;
console.log(type, buffer);
return { type: type.split("/")[1], buffer };
}
throw new Error("Url format not implemented.");
}
The ./utils
code is available: https://webuxlab.com/en/web-projects/typescript-helper-functions
The ../Regexes/Images
code is available: https://webuxlab.com/en/web-projects/javascript-regexes
Usage (With Vue 3)
I do not sanitize the input because in this specific project I control the content.
You must implement some kind of sanitization when using with public data.
Markdown.vue
<script setup lang="ts">
import { onMounted, Ref, ref } from "vue";
import {
NotificationType,
useNotificationStore,
} from "../../stores/notification";
import { markdown } from "../../libs/markdown";
const notificationStore = useNotificationStore();
const props = defineProps({
content: { type: String, required: true },
filepath: { type: String, required: true },
});
const html: Ref<string> = ref("");
onMounted(async () => {
try {
html.value = await markdown(props.filepath, props.content);
} catch (e: any) {
notificationStore.showNotification(NotificationType.ERROR, e.message);
}
});
</script>
<template>
<article class="prose lg:prose-xl" v-html="html"></article>
</template>