Display images in source browser and diffs, enable preview for svgs

This commit is contained in:
Sebastian Martinez
2024-02-27 16:59:07 +00:00
committed by Rūdolfs Ošiņš
parent 481e25dca1
commit add8a9ad94
5 changed files with 117 additions and 43 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { tick } from "svelte";
import ExpandButton from "./ExpandButton.svelte";
export let collapsable: boolean = false;
@@ -74,7 +75,7 @@
</div>
<div class="right">
<slot name="right-header" />
<slot name="right-header" {expanded} />
</div>
</div>
+10
View File
@@ -192,6 +192,16 @@ export function isCommit(input: string): boolean {
return /^[a-f0-9]{40}$/.test(input);
}
// Check whether the given path has a valid image file extension.
export function isImagePath(input: string): boolean {
return /\.(jpg|jpeg|png|gif|bmp)$/i.test(input);
}
// Check whether the given path has a valid svg file extension.
export function isSvgPath(input: string): boolean {
return /\.svg$/i.test(input);
}
export function isFulfilled<T>(
input: PromiseSettledResult<T>,
): input is PromiseFulfilledResult<T> {
+54 -14
View File
@@ -5,6 +5,7 @@
import { toHtml } from "hast-util-to-html";
import * as Syntax from "@app/lib/syntax";
import { isImagePath, isSvgPath } from "@app/lib/utils";
import Badge from "@app/components/Badge.svelte";
import File from "@app/components/File.svelte";
@@ -14,6 +15,8 @@
import Link from "@app/components/Link.svelte";
import Loading from "@app/components/Loading.svelte";
import Placeholder from "@app/components/Placeholder.svelte";
import Radio from "@app/components/Radio.svelte";
import Button from "@app/components/Button.svelte";
export let filePath: string;
export let oldContent: string | undefined = undefined;
@@ -35,6 +38,8 @@
let selection: Selection | undefined = undefined;
let highlighting: { new?: string[]; old?: string[] } | undefined = undefined;
let syntaxHighlightingLoading: boolean = false;
let preview = false;
$: extension = filePath.split(".").pop();
onMount(() => {
window.addEventListener("click", deselectHandler);
@@ -400,29 +405,52 @@
{/if}
</svelte:fragment>
<svelte:fragment slot="right-header">
<svelte:fragment slot="right-header" let:expanded>
{#if revision}
{#if syntaxHighlightingLoading}
<Loading small />
{/if}
<Link
route={{
resource: "project.source",
project: projectId,
node: baseUrl,
path: filePath,
revision,
}}>
<IconButton title="View file">
<IconSmall name="chevron-left-right" />
</IconButton>
</Link>
<div style:display="flex" style:align-items="center" style:gap="0.5rem">
{#if isSvgPath(filePath) && expanded}
<Radio ariaLabel="Toggle render method">
<Button
styleBorderRadius="0"
variant={!preview ? "selected" : "not-selected"}
on:click={() => {
preview = false;
}}>
<IconSmall name="chevron-left-right" />Code
</Button>
<Button
styleBorderRadius="0"
variant={preview ? "selected" : "not-selected"}
on:click={() => {
window.location.hash = "";
preview = true;
}}>
<IconSmall name="eye-open" />Preview
</Button>
</Radio>
{/if}
<Link
route={{
resource: "project.source",
project: projectId,
node: baseUrl,
path: filePath,
revision,
}}>
<IconButton title="View file">
<IconSmall name="chevron-left-right" />
</IconButton>
</Link>
</div>
{/if}
</svelte:fragment>
<div class="container">
{#if fileDiff.type === "plain"}
{#if fileDiff.hunks.length > 0}
{#if fileDiff.hunks.length > 0 && !preview}
<table class="diff" data-file-diff-select>
{#each fileDiff.hunks as hunk, hunkIdx}
<tr
@@ -484,6 +512,18 @@
{/each}
{/each}
</table>
{:else if isImagePath(filePath) && extension && content}
<div style:margin="1rem 0" style:text-align="center">
<img
src={`data:image/${extension};base64,${content}`}
alt={filePath} />
</div>
{:else if preview && content}
<div style:margin="1rem 0" style:text-align="center">
<img
src={`data:image/svg+xml;base64,${btoa(content)}`}
alt={filePath} />
</div>
{:else}
<div style:margin="1rem 0">
<Placeholder iconName="empty-file" caption="Empty file" inline />
+43 -20
View File
@@ -5,13 +5,14 @@
import { toHtml } from "hast-util-to-html";
import * as Syntax from "@app/lib/syntax";
import { isMarkdownPath } from "@app/lib/utils";
import { isImagePath, isMarkdownPath, isSvgPath } from "@app/lib/utils";
import { lineNumbersGutter } from "@app/lib/syntax";
import { routeToPath } from "@app/lib/router";
import Button from "@app/components/Button.svelte";
import File from "@app/components/File.svelte";
import FilePath from "@app/components/FilePath.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
import Link from "@app/components/Link.svelte";
import Markdown from "@app/components/Markdown.svelte";
@@ -30,6 +31,7 @@
$: lastCommit = blob.lastCommit;
$: content = highlighted ? lineNumbersGutter(highlighted) : undefined;
$: extension = path.split(".").pop();
let selectedLineId: string | undefined = undefined;
$: {
@@ -47,7 +49,10 @@
}
$: isMarkdown = isMarkdownPath(blob.path);
$: showMarkdown = isMarkdown && selectedLineId === undefined;
$: isImage = isImagePath(blob.path);
$: isSvg = isSvgPath(blob.path);
$: enablePreview = isMarkdown || isSvg;
$: preview = enablePreview && selectedLineId === undefined;
let linkBaseUrl: string | undefined;
@@ -197,41 +202,59 @@
</div>
</div>
<div class="global-hide-on-mobile teaser-buttons">
{#if isMarkdown}
{#if enablePreview}
<Radio ariaLabel="Toggle render method">
<Button
styleBorderRadius="0"
variant={showMarkdown ? "selected" : "not-selected"}
variant={!preview ? "selected" : "not-selected"}
on:click={() => {
window.location.hash = "";
showMarkdown = true;
preview = false;
}}>
Markdown
<IconSmall name="chevron-left-right" />Code
</Button>
<Button
styleBorderRadius="0"
variant={!showMarkdown ? "selected" : "not-selected"}
variant={preview ? "selected" : "not-selected"}
on:click={() => {
showMarkdown = false;
window.location.hash = "";
preview = true;
}}>
Plain
<IconSmall name="eye-open" />Preview
</Button>
</Radio>
{/if}
<a href="{rawPath}/{blob.path}">
<Button variant="gray-white">Raw</Button>
<a href="{rawPath}/{blob.path}" target="_blank" rel="noreferrer">
<Button variant="gray-white">
Raw <IconSmall name="arrow-box-up-right" />
</Button>
</a>
</div>
</svelte:fragment>
{#if blob.binary}
<div style:margin="4rem 0" style:width="100%">
<Placeholder iconName="binary-file" caption="Binary file" />
</div>
{:else if showMarkdown && blob.content}
<div style:padding="2rem">
<Markdown {linkBaseUrl} content={blob.content} {rawPath} {path} />
</div>
{#if blob.binary && blob.content}
{#if isImage && extension}
<div style:margin="1rem 0" style:text-align="center">
<img
src={`data:image/${extension};base64,${blob.content}`}
alt={path} />
</div>
{:else}
<div style:margin="4rem 0" style:width="100%">
<Placeholder iconName="binary-file" caption="Binary file" />
</div>
{/if}
{:else if preview && blob.content}
{#if isMarkdown}
<div style:padding="2rem">
<Markdown {linkBaseUrl} content={blob.content} {rawPath} {path} />
</div>
{:else if isSvg}
<div style:margin="1rem 0" style:text-align="center">
<img
src={`data:image/svg+xml;base64,${btoa(blob.content)}`}
alt={path} />
</div>
{/if}
{:else if content}
<table class="code no-scrollbar">
{@html toHtml(content)}
+8 -8
View File
@@ -86,7 +86,7 @@ test("source file highlighting", async ({ page }) => {
test("navigate line numbers", async ({ page }) => {
await page.goto(`${markdownUrl}/tree/main/cheatsheet.md`);
await page.getByRole("button", { name: "Plain" }).click();
await page.getByRole("button", { name: "Code" }).click();
await page.getByRole("link", { name: "5", exact: true }).click();
await expect(page.locator("#L5")).toHaveClass("line highlight");
@@ -103,7 +103,7 @@ test("navigate line numbers", async ({ page }) => {
// Check that we go back to the Markdown view when navigating to a different
// file.
await page.getByRole("link", { name: "footnotes.md" }).click();
await expect(page.getByRole("button", { name: "Markdown" })).toHaveClass(
await expect(page.getByRole("button", { name: "Preview" })).toHaveClass(
/selected/,
);
});
@@ -229,21 +229,21 @@ test("markdown files", async ({ page }) => {
// Switch between raw and rendered modes.
{
await expect(page.getByRole("button", { name: "Markdown" })).toHaveClass(
await expect(page.getByRole("button", { name: "Preview" })).toHaveClass(
/selected/,
);
await expect(page.getByRole("button", { name: "Plain" })).toHaveClass(
await expect(page.getByRole("button", { name: "Code" })).toHaveClass(
/not-selected/,
);
await page.getByRole("button", { name: "Plain" }).click();
await expect(page.getByRole("button", { name: "Markdown" })).toHaveClass(
await page.getByRole("button", { name: "Code" }).click();
await expect(page.getByRole("button", { name: "Preview" })).toHaveClass(
/not-selected/,
);
await expect(page.getByRole("button", { name: "Plain" })).toHaveClass(
await expect(page.getByRole("button", { name: "Code" })).toHaveClass(
/selected/,
);
await expect(page.getByText("##### Table of Contents")).toBeVisible();
await page.getByRole("button", { name: "Markdown" }).click();
await page.getByRole("button", { name: "Preview" }).click();
}
// Internal links go to anchor.