Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
radicle-explorer src views repos Source Blob.svelte
<script lang="ts">
  import type { BaseUrl, Blob } from "@http-client";

  import { afterUpdate, onDestroy, onMount } from "svelte";
  import { toHtml } from "hast-util-to-html";

  import * as Syntax from "@app/lib/syntax";
  import { isImagePath, isMarkdownPath, isSvgPath } from "@app/lib/utils";
  import { lineNumbersGutter } from "@app/lib/syntax";

  import Button from "@app/components/Button.svelte";
  import CommitButton from "@app/views/repos/components/CommitButton.svelte";
  import File from "@app/components/File.svelte";
  import FilePath from "@app/components/FilePath.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Markdown from "@app/components/Markdown.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
  import Radio from "@app/components/Radio.svelte";

  export let baseUrl: BaseUrl;
  export let repoId: string;
  export let path: string;
  export let blob: Blob;
  export let highlighted: Syntax.Root | undefined;
  export let rawPath: string;

  $: lastCommit = blob.lastCommit;

  $: content = highlighted ? lineNumbersGutter(highlighted) : undefined;
  $: extension = path.split(".").pop();

  let selectedLineId: string | undefined = undefined;
  $: {
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
    content;
    updateSelectedLineId();
  }

  function updateSelectedLineId() {
    const fragmentId = window.location.hash.substring(1);
    if (fragmentId && fragmentId.match(/^L\d+$/)) {
      selectedLineId = fragmentId;
    } else {
      selectedLineId = undefined;
    }
  }

  $: isMarkdown = isMarkdownPath(blob.path);
  $: isImage = isImagePath(blob.path);
  $: isSvg = isSvgPath(blob.path);
  $: enablePreview = isMarkdown || isSvg;
  $: preview = enablePreview && selectedLineId === undefined;

  afterUpdate(() => {
    for (const item of document.getElementsByClassName("highlight")) {
      item.classList.remove("highlight");
    }
    if (selectedLineId) {
      const target = document.getElementById(selectedLineId);
      if (target) {
        target.classList.add("highlight");
        target.scrollIntoView({ block: "center" });
      }
    }
  });

  onMount(async () => {
    window.addEventListener("hashchange", updateSelectedLineId);
  });

  onDestroy(() => {
    window.removeEventListener("hashchange", updateSelectedLineId);
  });
</script>

<style>
  .code :global(.line-number) {
    font: var(--txt-code-regular);
    color: var(--color-text-disabled);
    text-align: right;
    padding: 0;
    user-select: none;
  }
  .code :global(.line-number a) {
    display: block;
    padding: 0 1rem;
  }
  .code :global(.line-number:hover) {
    cursor: pointer;
    color: var(--color-text-tertiary);
  }

  .code :global(.content) {
    display: inline;
    font: var(--txt-code-regular);
    margin: 0;
  }

  .code :global(.line) {
    line-height: 22px; /* This seems to be the line-height of a pre code block */
  }
  .code :global(.highlight) {
    background-color: var(--color-surface-mid);
    box-shadow: 0 0 0 1px var(--color-border-brand);
  }
  .code :global(.highlight td:first-child) {
    background-color: var(--color-surface-mid);
    border-left: 1px solid var(--color-border-brand);
  }
  .code :global(.highlight td:last-child) {
    background-color: var(--color-surface-mid);
    border-right: 1px solid var(--color-border-brand);
  }

  .code :global(.line-content) {
    padding: 0;
    width: 100%;
  }

  .code {
    width: 100%;
    border-spacing: 0;
    overflow-x: auto;
    font: var(--txt-body-m-regular);
    background-color: var(--color-surface-canvas);
    padding-top: 1rem;
    margin-bottom: 1.5rem;
  }

  .teaser-buttons {
    display: flex;
    gap: 0.5rem;
  }

  .no-scrollbar {
    scrollbar-width: none;
  }

  .no-scrollbar::-webkit-scrollbar {
    display: none;
  }
  .markdown-wrapper {
    padding: 2rem;
    background-color: var(--color-surface-canvas);
    max-width: 75rem;
    margin: 0 auto;
  }
  @media (max-width: 719.98px) {
    .markdown-wrapper {
      padding: 1rem;
    }
  }
</style>

<File sticky={false} containerBackground="var(--color-surface-canvas)">
  <FilePath slot="left-header" filenameWithPath={blob.path} />
  <svelte:fragment slot="right-header">
    <CommitButton {repoId} {baseUrl} commit={lastCommit} />
    <div class="global-hide-on-mobile-down teaser-buttons">
      {#if enablePreview}
        <Radio ariaLabel="Toggle render method">
          <Button
            styleBorderRadius="0"
            variant={!preview ? "selected" : "not-selected"}
            on:click={() => {
              // eslint-disable-next-line
              preview = false;
            }}>
            <Icon name="chevron-left-right" />Code
          </Button>
          <Button
            styleBorderRadius="0"
            variant={preview ? "selected" : "not-selected"}
            on:click={() => {
              window.location.hash = "";
              // eslint-disable-next-line
              preview = true;
            }}>
            <Icon name="eye" />Preview
          </Button>
          <div class="global-spacer"></div>
        </Radio>
      {/if}
      <a href="{rawPath}/{blob.path}" target="_blank" rel="noreferrer">
        <Button variant="gray-white">
          Raw <Icon name="open-external" />
        </Button>
      </a>
    </div>
  </svelte:fragment>

  {#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 class="markdown-wrapper">
        <Markdown 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)}
    </table>
  {:else}
    <div style:margin="4rem 0" style:width="100%">
      <Placeholder iconName="empty-file" caption="Empty file" />
    </div>
  {/if}
</File>