~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
92 lines · 3.2 KB Raw History
import { lfsPointer } from '@pdsjs/git/browser';
import { useEffect, useMemo, useRef } from 'react';
import { bytes } from '#/lib/format.js';
import { account, readerFor } from '#/lib/git.js';
import { renderMarkdown } from '#/lib/markdown.js';

/**
 * The stand-in for an image the repository holds only as a Git LFS pointer.
 * @param {Element} image
 * @param {import('@pdsjs/git/browser').LfsPointer} pointer
 * @returns {Element}
 */
function lfsNotice(image, pointer) {
  const note = document.createElement('span');
  note.className = 'md-lfs';
  const alt = image.getAttribute('alt');
  note.textContent = `${alt || 'Image'}: stored with Git LFS, ${bytes(pointer.size)}`;
  return note;
}

/**
 * A rendered markdown document from the repository.
 *
 * An image the document names by a relative path is a file in the repository,
 * and the repository's objects are already in memory by the time this
 * renders. The renderer leaves those images without a source and puts the
 * path on the tag; this fills them in from the object store, which is why a
 * README's diagrams appear without the server serving anything.
 */
export function Markdown({ text, repo, refName, basePath = '', space }) {
  const container = useRef(null);
  const html = useMemo(
    () => renderMarkdown(text, { repo, refName, basePath }),
    [text, repo, refName, basePath],
  );

  // The effect reads the DOM that html produced, so it has to run again
  // whenever html changes, even though it names no variable from it.
  // biome-ignore lint/correctness/useExhaustiveDependencies: html rebuilds the DOM this reads
  useEffect(() => {
    const node = container.current;
    if (!node) return undefined;
    const images = node.querySelectorAll('img[data-repo-path]');
    if (images.length === 0) return undefined;

    let live = true;
    /** @type {string[]} */
    const urls = [];
    const reader = readerFor(repo, undefined, space);

    (async () => {
      for (const image of images) {
        const path = image.getAttribute('data-repo-path');
        try {
          const file = await reader.readImage(account.did, repo, refName, path);
          if (!live) return;
          if (!file) continue;
          const pointer = lfsPointer(file.bytes);
          if (pointer) {
            image.replaceWith(lfsNotice(image, pointer));
            continue;
          }
          const url = URL.createObjectURL(
            new Blob([file.bytes], { type: file.mediaType }),
          );
          urls.push(url);
          image.setAttribute('src', url);
        } catch {
          // A path the document names but the tree does not hold, or a file
          // that is no image. The alt text stands in, the way a missing image
          // always does. The next image still gets its turn.
        }
      }
    })();

    return () => {
      live = false;
      for (const url of urls) URL.revokeObjectURL(url);
    };
  }, [html, repo, refName]);

  return (
    <div
      ref={container}
      className="markdown"
      // Every tag here was written by renderMarkdown, from the list of tags
      // this page allows.
      // biome-ignore lint/security/noDangerouslySetInnerHtml: the renderer emits only its own tags
      dangerouslySetInnerHTML={{ __html: html }}
    />
  );
}