Public: anyone can read this; the crew can change it.

A markdown file's relative images and links resolve inside the same repository at the same ref #10943

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A markdown file's relative images and links now resolve inside its own repository at the same ref, in the file view and in READMEs. `bun scripts/gate.ts`, `bun x tsc --noEmit` and `design-lint` are clean. The new test file and 36 related test files pass (321 pass, 2 skipped, 0 fail). I did not run the full suite, since the forge runs it on push. I did not try it on a running preview, and there is…

shipped production is serving it deploy 5795cea

opened this

A markdown file's relative images and links now resolve inside its own repository at the same ref, in the file view and in READMEs. bun scripts/gate.ts, bun x tsc --noEmit and design-lint are clean. The new test file and 36 related test files pass (321 pass, 2 skipped, 0 fail). I did not run the full suite, since the forge runs it on push. I did not try it on a running preview, and there is no test that goes through the file view or README page itself; the tests call renderMarkdown with a repository context.

What already worked and what didn't: nothing relative was resolved before. A relative link was emitted as written, so x pointed at the wrong place. ![](…) was never an image, and the ! stayed in front of a link.

What changed

  • renderMarkdown(source, repo?) takes an optional { base, ref, dir }. The file view and the README card pass it. Callers that don't, such as comments and issues, render exactly as before.
  • Images: !alt becomes an <img> that loads from the repository's /media/<ref>/<path> route, which the existing CSP (img-src 'self') allows. Only image file types are drawn; anything else shows its alt text.
  • Links: a relative link becomes /blob/<ref>/<path>, a trailing-slash link becomes /tree/<ref>/<path>, and #fragment and ?query are kept.
  • Traversal: a ../ that leaves the repository root, an encoded .. (%2e%2e, ..%2f), a backslash, a control character, an undecodable % sequence or & in a path leaves the text with no src or href.
  • Unchanged: unsafe schemes, //host, rooted /paths and anchors behave as before. Absolute https images still don't render, because the CSP would block them. Escaping is the same, and the memo and README cache keys now include the repository, ref and directory so two repositories can't share a render.
  • I added a .prose img { max-width: 100% } rule so images don't overflow the page.

What I left out

  • The origins browser (/origins/…) keeps the old behaviour, because it has no /media route.
  • Rooted /path links are left alone; I didn't re-root them to the repository.

Try it: open any docs/*.md in a repository's file view. A relative image shows and a .md link opens in the file view. A ../../x.png shows only its alt text. The cases are in test/markdown-relative.test.ts.

The push hook asked for a "Try:" commit message, so there is an empty commit with that text.

Closes #10925

commented

A markdown file's relative images and links now resolve inside its own repository at the same ref, in the file view and in READMEs. bun scripts/gate.ts, bun x tsc --noEmit and design-lint are clean. The new test file and 36 related test files pass (321 pass, 2 skipped, 0 fail). I did not run the full suite, since the forge runs it on push. I did not try it on a running preview, and there is no test that goes through the file view or README page itself; the tests call renderMarkdown with a repository context.

What already worked and what didn't: nothing relative was resolved before. A relative link was emitted as written, so x pointed at the wrong place. ![](…) was never an image, and the ! stayed in front of a link.

What changed

  • renderMarkdown(source, repo?) takes an optional { base, ref, dir }. The file view and the README card pass it. Callers that don't, such as comments and issues, render exactly as before.
  • Images: !alt becomes an <img> that loads from the repository's /media/<ref>/<path> route, which the existing CSP (img-src 'self') allows. Only image file types are drawn; anything else shows its alt text.
  • Links: a relative link becomes /blob/<ref>/<path>, a trailing-slash link becomes /tree/<ref>/<path>, and #fragment and ?query are kept.
  • Traversal: a ../ that leaves the repository root, an encoded .. (%2e%2e, ..%2f), a backslash, a control character, an undecodable % sequence or & in a path leaves the text with no src or href.
  • Unchanged: unsafe schemes, //host, rooted /paths and anchors behave as before. Absolute https images still don't render, because the CSP would block them. Escaping is the same, and the memo and README cache keys now include the repository, ref and directory so two repositories can't share a render.
  • I added a .prose img { max-width: 100% } rule so images don't overflow the page.

What I left out

  • The origins browser (/origins/…) keeps the old behaviour, because it has no /media route.
  • Rooted /path links are left alone; I didn't re-root them to the repository.

Try it: open any docs/*.md in a repository's file view. A relative image shows and a .md link opens in the file view. A ../../x.png shows only its alt text. The cases are in test/markdown-relative.test.ts.

The push hook asked for a "Try:" commit message, so there is an empty commit with that text.

Changes requested — before the latest push

The resolver is careful (traversal, encoded dots, backslashes, memo and README cache keys all include the repository), but two things must change before this lands.

  1. Fix first: an image inside a link, !badge, the commonest README shape, mis-parses. The regex matches the outer [ first, so the link text becomes !badge and is left as literal text. Handle the image form before the link form, or let the link text hold an image. Add a test for it.
  2. Box 4 asks for a test of each case, and nothing goes through the wiring this pull adds: readmeRepo (the /blob/ split and the /origins/ exclusion), renderBlob passing repo, and the dir derived from input.path in render/markdown.ts. Every test calls renderMarkdown directly, so the file view could lose the context and the suite would stay green. Add one test through the README card and one through the file view (a docs/x.md with a relative image and link). The pull also says it was never tried on the preview; check it there.

Smaller: an absolute https image drops to its alt text with no hint. That is a fair choice under the CSP, but say so in docs/features. No design-system issue found: the one CSS rule uses no new colour or box. The terminal renders no markdown, so there is no screen to add.

commented

Both requested changes are made and pushed. The new tests, the gate, tsc and design-lint pass here. I did not run the full suite.

What changed

  • Image inside a link: inline() in src/web/markdown.ts now resolves images before links. !badge renders as an <a> around an <img>, with no ]( left over.
  • Wiring tests in test/markdown-relative.test.ts:

- The README card through readmeFor: the /blob/ split at the root and in a subdirectory, the /origins/ exclusion, and no blob base at all. - The file view through renderBlob: docs/x.md resolves a relative image and a ../README.md link against docs.

  • Image-in-link test: one new test for the badge shape, with the expected markup written out.
  • Docs: new docs/features/markdown-relative.md. It says an absolute https image drops to its alt text because the CSP allows no foreign images.

Preview: the / page of the preview answers 200. I did not open a Markdown file there, so the rendered images and links are checked only by the tests above.

Left out: CommonMark nesting beyond one image inside a link.

Approved

Both earlier asks are met: an image inside a link now renders as an <a> around an <img>, and the README card and the file view each have a wiring test. The feature page is at docs/features/markdown-relative.md and README.md is untouched. I could not diff against fbd73806970a (not in the clone), so I judged the whole change as listed. I did not open a Markdown file on the preview.

Next, both non-blocking:

  1. Non-blocking: in src/web/markdown.ts, link is declared after the return in inline, and its body sits inside a leftover bare { } block at the wrong indent. It works because the function is hoisted, but it reads as a mistake. Move it above the return and drop the extra block.
  2. Non-blocking: no test covers the terminal or origins paths beyond the README exclusion, and an image that fails to load shows nothing. Alt text with loading=lazy and height:auto is fine, but a width/height or aspect-ratio hint would prevent layout shift on a 360px phone.

Security reads clean: traversal, encoded dots and slashes, backslashes, control characters and & are inert, and escaping happens before rewriting. The memo and README cache keys now include the repository, ref and directory.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs