Add /raw route for getting images

Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Sebastian Martinez
2023-03-14 11:13:15 +01:00
parent 3ea6a07dc4
commit 09ceeed136
10 changed files with 40 additions and 101 deletions
+2 -3
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import type { MaybeBlob } from "@app/lib/project";
import type { Comment, Thread } from "@app/lib/issue";
import Authorship from "@app/components/Authorship.svelte";
@@ -8,7 +7,7 @@
export let comment: Comment | Thread;
export let caption = "commented";
export let getImage: (path: string) => Promise<MaybeBlob>;
export let rawPath: string;
$: source = comment.author.id;
$: title = comment.author.id;
@@ -57,7 +56,7 @@
{#if comment.body.trim() === ""}
<span class="txt-missing">No description.</span>
{:else}
<Markdown content={comment.body} {getImage} />
<Markdown {rawPath} content={comment.body} />
{/if}
</div>
</div>
+12 -17
View File
@@ -1,6 +1,4 @@
<script lang="ts">
import type * as proj from "@app/lib/project";
import dompurify from "dompurify";
import matter from "@radicle/gray-matter";
import { marked } from "marked";
@@ -8,7 +6,7 @@
import { toDom } from "hast-util-to-dom";
import { base } from "@app/lib/router";
import { getImageMime, isUrl, twemoji, scrollIntoView } from "@app/lib/utils";
import { isUrl, twemoji, scrollIntoView, canonicalize } from "@app/lib/utils";
import { highlight } from "@app/lib/syntax";
import {
markdownExtensions as extensions,
@@ -17,8 +15,9 @@
export let content: string;
export let doc = matter(content);
export let getImage: (path: string) => Promise<proj.MaybeBlob>;
export let hash: string | null = null;
export let path: string = "/";
export let rawPath: string;
const frontMatter = Object.entries(doc.data).filter(
([, val]) => typeof val === "string" || typeof val === "number",
@@ -43,22 +42,18 @@
if (hash) scrollIntoView(hash);
// 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.
// Iterate over all images, and replace the source with a canonicalized URL
// pointing at the projects /raw endpoint.
for (const i of container.querySelectorAll("img")) {
const path = i.getAttribute("src");
const imagePath = i.getAttribute("src");
// Make sure the source isn't a URL before trying to fetch it from the repo
if (path && !isUrl(path) && !path.startsWith(`${base}twemoji`)) {
getImage(path).then(blob => {
if (blob?.content) {
const mime = getImageMime(path);
if (mime) {
i.setAttribute("src", `data:${mime};base64,${blob.content}`);
}
}
});
if (
imagePath &&
!isUrl(imagePath) &&
!imagePath.startsWith(`${base}twemoji`)
) {
i.setAttribute("src", `${rawPath}/${canonicalize(imagePath, path)}`);
}
}
+8 -2
View File
@@ -21,7 +21,7 @@ export enum ProjectContent {
}
export interface ProjectInfo {
head: string | null;
head: string;
id: string;
name: string;
description: string;
@@ -125,7 +125,7 @@ export function parseRoute(
export class Project implements ProjectInfo {
id: string;
head: string | null;
head: string;
name: string;
description: string;
defaultBranch: string;
@@ -289,6 +289,12 @@ export class Project implements ProjectInfo {
return result;
}
getRawPath(commit?: string): string {
return `${this.seed.addr.scheme}://${this.seed.addr.host}:${
this.seed.addr.port
}/raw/${this.id}/${commit ?? this.head}`;
}
static async get(
id: string,
seedHost: string,
-22
View File
@@ -119,28 +119,6 @@ export function capitalize(s: string): string {
return s[0].toUpperCase() + s.substring(1);
}
// 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;
}
// Takes a path, eg. "../images/image.png", and a base from where to start resolving, e.g. "static/images/index.html".
// Returns the resolved path.
export function canonicalize(
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import type { MaybeBlob, Blob } from "@app/lib/project";
import type { Blob } from "@app/lib/project";
import type { MaybeHighlighted } from "@app/lib/syntax";
import type { ProjectRoute } from "@app/lib/router/definitions";
@@ -14,7 +14,7 @@
export let activeRoute: ProjectRoute;
export let blob: Blob;
export let getImage: (path: string) => Promise<MaybeBlob>;
export let rawPath: string;
export let line: string | undefined = undefined;
const fileExtension = blob.path.split(".").pop() ?? "";
@@ -247,7 +247,7 @@
<span class="txt-tiny">Binary content</span>
</div>
{:else if showMarkdown && blob.content}
<Readme content={blob.content} {getImage} {activeRoute} />
<Readme content={blob.content} {rawPath} {activeRoute} />
{:else if content}
<table class="code no-scrollbar">
{@html toHtml(content)}
+5 -11
View File
@@ -11,7 +11,6 @@
import type { ProjectRoute } from "@app/lib/router/definitions";
import * as router from "@app/lib/router";
import * as utils from "@app/lib/utils";
import Button from "@app/components/Button.svelte";
import Loading from "@app/components/Loading.svelte";
import Placeholder from "@app/components/Placeholder.svelte";
@@ -59,15 +58,6 @@
browserErrorStore.set(undefined);
});
// Get an image blob based on a relative path.
const getImage = async (imagePath: string) => {
const finalPath = utils.canonicalize(imagePath, path);
return project.getBlob(commit, finalPath).catch(() => {
console.warn("Not able to load image blob:", finalPath);
return undefined;
});
};
const onSelect = async (newPath: string) => {
browserErrorStore.set(undefined);
// Ensure we don't spend any time in a "loading" state. This means
@@ -242,7 +232,11 @@
<Loading small center />
{:then blob}
{#if blob}
<Blob {line} {blob} {getImage} {activeRoute} />
<Blob
{line}
{blob}
{activeRoute}
rawPath={project.getRawPath(commit)} />
{/if}
{/await}
{/if}
+3 -10
View File
@@ -1,23 +1,16 @@
<script lang="ts">
import type { Blob, Project } from "@app/lib/project";
import type { Project } from "@app/lib/project";
import type { Issue } from "@app/lib/issue";
import Authorship from "@app/components/Authorship.svelte";
import Avatar from "@app/components/Comment/Avatar.svelte";
import Chip from "@app/components/Chip.svelte";
import Comment from "@app/components/Comment.svelte";
import { formatNodeId, canonicalize, capitalize } from "@app/lib/utils";
import { formatNodeId, capitalize } from "@app/lib/utils";
import { formatObjectId } from "@app/lib/cobs";
export let issue: Issue;
export let project: Project;
// Get an image blob based on a relative path.
const getImage = async (imagePath: string): Promise<Blob> => {
const finalPath = canonicalize(imagePath, "/"); // We only use the root path in issues.
const commit = project.branches[project.defaultBranch]; // We suppose that all issues are only looked at on HEAD of the default branch.
return project.getBlob(commit, finalPath);
};
</script>
<style>
@@ -140,7 +133,7 @@
<main>
<div class="comments">
{#each issue.discussion as comment}
<Comment {comment} {getImage} />
<Comment {comment} rawPath={project.getRawPath()} />
{/each}
</div>
<div class="metadata layout-desktop">
+4 -11
View File
@@ -1,5 +1,5 @@
<script lang="ts" strictEvents>
import type { MaybeBlob, Project } from "@app/lib/project";
import type { Project } from "@app/lib/project";
import type { Session } from "@app/lib/session";
import * as modal from "@app/lib/modal";
@@ -11,7 +11,7 @@
import TextInput from "@app/components/TextInput.svelte";
import Textarea from "@app/components/Textarea.svelte";
import { Issue } from "@app/lib/issue";
import { canonicalize, formatNodeId, parseNodeId } from "@app/lib/utils";
import { formatNodeId, parseNodeId } from "@app/lib/utils";
import { createEventDispatcher } from "svelte";
export let session: Session;
@@ -19,13 +19,6 @@
const dispatch = createEventDispatcher<{ create: never }>();
// Get an image blob based on a relative path.
const getImage = async (imagePath: string): Promise<MaybeBlob> => {
const finalPath = canonicalize(imagePath, "/"); // We only use the root path in issues.
const commit = project.branches[project.defaultBranch]; // We suppose that all issues are only looked at on HEAD of the default branch.
return project.getBlob(commit, finalPath);
};
let preview: boolean = false;
function handleAddAssignee() {
@@ -181,14 +174,14 @@
</div>
<div class="comments">
<Comment
rawPath={project.getRawPath()}
comment={{
author: { id: session.publicKey },
body: issueText,
reactions: {},
replyTo: null,
timestamp: Date.now(),
}}
{getImage} />
}} />
</div>
{:else}
<Textarea bind:value={issueTitle} placeholder="Title" />
+3 -3
View File
@@ -1,13 +1,13 @@
<script lang="ts">
import type * as proj from "@app/lib/project";
import type { ProjectRoute } from "@app/lib/router/definitions";
import Markdown from "@app/components/Markdown.svelte";
export let content: string;
export let getImage: (path: string) => Promise<proj.MaybeBlob>;
export let rawPath: string;
export let activeRoute: ProjectRoute;
$: path = activeRoute.params.path || "/";
$: hash = activeRoute.params.hash || null;
</script>
@@ -22,5 +22,5 @@
</style>
<article>
<Markdown {content} {getImage} {hash} />
<Markdown {content} {hash} {rawPath} {path} />
</article>
-19
View File
@@ -418,23 +418,4 @@ test.describe("browser error handling", () => {
await expect(page.locator("text=Not able to load file")).toBeVisible();
});
test("error appears when a image with a relative path can't be loaded", async ({
page,
}) => {
await page.route(
`**/v1/projects/${rid}/blob/${aliceMainHead}/src/black-square.png`,
route => route.fulfill({ status: 404 }),
);
await page.goto(projectFixtureUrl);
const sourceTree = page.locator(".source-tree");
await sourceTree.locator("text=markdown/").click();
await sourceTree.locator("text=loading-image.md").click();
// By having a relative path, this gives away that the image has not loaded
// else it would have been converted into a data base64 string
await expect(
page.locator("img[src='../src/black-square.png']"),
).toBeVisible();
});
});