Load images in markdown documents

This loads images asynchronously inside markdown documents.

Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
Alexis Sellier
2022-03-21 12:39:39 +01:00
parent ec9e4fd16d
commit 8e77f9f5ae
4 changed files with 72 additions and 5 deletions
+24 -3
View File
@@ -3,14 +3,17 @@
import { marked } from "marked";
import fm from "front-matter";
import type { FrontMatterResult } from "front-matter";
import sanitizeHtml from 'sanitize-html';
import type * as proj from "@app/project";
import { getImageMime } from "@app/utils";
import sanitizeHtml from "sanitize-html";
export let content: string;
export let doc: FrontMatterResult<Record<string, string>> = fm(content);
export let getImage: (path: string) => Promise<proj.Blob>;
let container: HTMLElement;
const sanitize = (content: string): string => {
const render = (content: string): string => {
return sanitizeHtml(marked.parse(content), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat([
"img",
@@ -35,6 +38,24 @@
e.classList.add("no-underline");
}
}
// Iterate over all images, and fetch their data from the API, then
// replace the source with a Data-URL. We do this due to the absence
// of a static file server.
for (let i of container.querySelectorAll("img")) {
const path = i.getAttribute("src");
if (path) {
getImage(path).then(blob => {
if (blob.content) {
const mime = getImageMime(path);
if (mime) {
i.setAttribute("src", `data:${mime};base64,${blob.content}`);
}
}
});
}
}
});
</script>
@@ -213,6 +234,6 @@
{/if}
<div class="markdown" bind:this={container}>
{@html marked(sanitize(doc.body))}
{@html render(doc.body)}
</div>
{/if}
+7 -1
View File
@@ -48,6 +48,12 @@
return state.blob;
};
// Get an image blob based on a relative path.
const getImage = async (imagePath: string): Promise<proj.Blob> => {
const finalPath = utils.canonicalize(imagePath, path);
return project.getBlob(commit, finalPath, { highlight: false });
};
const onSelect = async ({ detail: newPath }: { detail: string }) => {
// Ensure we don't spend any time in a "loading" state. This means
// the loading spinner won't be shown, and instead the blob will be
@@ -159,7 +165,7 @@
<Loading small center />
{:then blob}
{#if utils.isMarkdownPath(blob.path)}
<Readme content={blob.content} />
<Readme content={blob.content} {getImage} />
{:else}
<Blob line={browser.line} {blob} />
{/if}
+4 -1
View File
@@ -1,6 +1,9 @@
<script lang="ts">
import Markdown from '@app/Markdown.svelte';
import type * as proj from '@app/project';
export let content: string;
export let getImage: (path: string) => Promise<proj.Blob>;
</script>
<style>
@@ -12,5 +15,5 @@
</style>
<article>
<Markdown {content} />
<Markdown {content} {getImage} />
</article>
+37
View File
@@ -174,6 +174,43 @@ export function clickOutside(node: HTMLElement, onEventFunction: () => void): an
};
}
// Get the mime type of an image, given a file path.
// Returns `null` if unknown.
export function getImageMime(path: string): string | null {
const mimes: Record<string, string> = {
'apng': 'image/apng',
'png': 'image/png',
'svg': 'image/svg+xml',
'gif': 'image/gif',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'webp': 'image/webp',
};
const ext = path.split(".").pop();
if (ext) {
if (mimes[ext]) {
return mimes[ext];
}
}
return null;
}
// TODO: Needs testing with absolute and relative paths.
export function canonicalize(path: string, base: string): string {
const finalPath = base
.split("/")
.slice(0, -1) // Remove file name.
.concat([path]) // Add image file path.
.join("/");
// URL is used to resolve relative paths, eg. `../../assets/image.png`.
const url = new URL(finalPath, document.location.origin);
const pathname = url.pathname.replace(/^\//, "");
return pathname;
}
// Takes a URL, eg. "https://twitter.com/cloudhead", and return "cloudhead".
// Returns the original string if it was unable to extract the username.
export function parseUsername(input: string): string {