mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-03 18:50:30 +08:00
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:
+24
-3
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user