mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-08 13:10:44 +08:00
Add /raw route for getting images
Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user