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 (
); }