Files
antisocial/test/markdown.test.ts
T
thatguygriffandClaude Opus 5 0db18547c8
CI / Typecheck, test, build (pull_request) Successful in 11s
Show the pictures inside Reddit comments
A comment that was a picture rendered as either a link or, for a Giphy, the
literal text `![gif](giphy|Ve7wX45gaOFmw8eeEM)`. On r/aww that is most of the
thread.

Reddit writes an inline image as a token rather than an address, in three
shapes: `![gif](giphy|ID)` for a Giphy, `![img](emote|t5_2th52|4358)` for a
subreddit emote, and `![img](jo8gf0ca92zd1)` for an image uploaded straight to
the comment. The useful part is that all three tokens are keys in that same
comment's own `media_metadata`, so this is one lookup and not three special
cases. Nothing in the adapter has to know what Giphy is.

The fourth shape is someone pasting the address of a picture, which on Reddit
is how most images in comments actually arrive -- 117 of them against 21
Giphys in the sample I scanned. Those are shown as pictures too, decided by
the file extension. A link that is not to an image stays a link.

Animated ones take `s.gif` over `s.mp4` even though the MP4 is several times
smaller: a GIF moves on its own in an `<img>`, and an MP4 would need a player
element with autoplay, loop and muted set, for something the size of a
postage stamp.

Everything goes through the `/m/` proxy, like all other media. Without that a
comment thread would have the reader's browser fetch dozens of files straight
from Reddit, which is the one thing this whole app exists to avoid.

Placing the image is the renderer's job, not the Markdown parser's, because
the proxy is a render-time concern and markdown.ts knows nothing about it --
so it takes an optional `ImageRenderer` and, without one, an image stays a
link exactly as before. The parser also learned `![...]` proper: the link rule
was matching from the `[` and stranding the `!` as text.

Verified on r/aww/comments/171dxph, which carries one Giphy and 77 pasted
images: 35 render on the first page, all 35 load, all 35 through the proxy,
no upstream address reaches the page, no token is left unresolved, and
nothing overflows the column or scrolls the page sideways.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017nMQ2eDKnqALYhAibpTKTu
2026-08-27 16:14:06 -03:00

125 lines
5.1 KiB
TypeScript

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { escapeHtml } from '../src/render/html.ts';
import { renderMarkdown } from '../src/render/markdown.ts';
const md = (text: string): string => String(renderMarkdown(text));
/** Stands in for the real one, which proxies. Escapes the way that one does:
* placing the image is the renderer's job, and so is making it safe. */
const img = (url: string, alt: string): string =>
`<img src="${escapeHtml(url)}" alt="${escapeHtml(alt)}">`;
const mdi = (text: string): string => String(renderMarkdown(text, img));
test('markup a commenter typed is text, not markup', () => {
const out = md('<script>alert(1)</script> & "quoted"');
assert.ok(!out.includes('<script>'));
assert.match(out, /&lt;script&gt;/);
assert.match(out, /&amp;/);
});
test('only http and https become links', () => {
assert.match(md('[go](https://example.com/a)'), /<a href="https:\/\/example\.com\/a"/);
// The label is still shown; it is only the link that is refused.
const dangerous = md('[go](javascript:alert(1))');
assert.ok(!dangerous.includes('<a '));
assert.match(dangerous, /\[go\]/);
});
test('a bare URL is linked without swallowing the sentence it ends', () => {
const out = md('see https://example.com/x.');
assert.match(out, /href="https:\/\/example\.com\/x"/);
assert.ok(out.endsWith('.</p>'), `trailing full stop should stay outside the link: ${out}`);
});
test('an ampersand in a link target survives as one', () => {
const out = md('[x](https://example.com/?a=1&b=2)');
assert.match(out, /href="https:\/\/example\.com\/\?a=1&amp;b=2"/);
});
test('a quote is a block of its own, so the reply is not read as part of it', () => {
const out = md('> they said this\n\nand I disagree');
assert.match(out, /<blockquote><p>they said this<\/p><\/blockquote><p>and I disagree<\/p>/);
});
test('quoting a quote keeps both levels', () => {
assert.match(md('> > deep\n> shallow'), /<blockquote><blockquote>/);
});
test('emphasis does not fire inside a word', () => {
// `snake_case_names` are ordinary in the subreddits this will be pointed at.
assert.equal(md('some_variable_name'), '<p>some_variable_name</p>');
assert.match(md('_yes_'), /<em>yes<\/em>/);
});
test('code is left exactly as typed', () => {
assert.match(md('`a < b && c`'), /<code>a &lt; b &amp;&amp; c<\/code>/);
assert.match(md('```\n<b>not bold</b>\n```'), /<pre><code>&lt;b&gt;not bold&lt;\/b&gt;<\/code><\/pre>/);
});
test('subreddit and user references link back to reddit', () => {
assert.match(md('over in r/aww'), /href="https:\/\/www\.reddit\.com\/r\/aww"/);
assert.match(md('ask u/someone'), /href="https:\/\/www\.reddit\.com\/u\/someone"/);
});
test('lists survive, both kinds', () => {
assert.match(md('- one\n- two'), /<ul><li>one<\/li><li>two<\/li><\/ul>/);
assert.match(md('1. one\n2. two'), /<ol><li>one<\/li><li>two<\/li><\/ol>/);
});
test('a single newline inside a paragraph is a line break, a blank line is a new one', () => {
assert.equal(md('one\ntwo'), '<p>one<br>two</p>');
assert.equal(md('one\n\ntwo'), '<p>one</p><p>two</p>');
});
test('an image is a picture when there is something to place it with', () => {
assert.equal(mdi('![a cat](https://i.redd.it/x.jpg)'),
'<p><img src="https://i.redd.it/x.jpg" alt="a cat"></p>');
});
test('an image degrades to a link when there is not', () => {
const out = md('![a cat](https://i.redd.it/x.jpg)');
assert.ok(out.includes('<a href="https://i.redd.it/x.jpg"'));
assert.ok(!out.includes('<img'));
});
test("an image's `!` is not left behind as text", () => {
// The link rule would otherwise match from the `[` and strand the bang.
assert.ok(!mdi('![](https://i.redd.it/x.png)').includes('!'));
});
test('a pasted image address becomes the picture, not a link to it', () => {
// Which is how most images in a Reddit comment arrive.
const out = mdi('look\n\nhttps://preview.redd.it/abc.jpeg?width=1274&s=deadbeef');
assert.ok(out.includes('<img src="https://preview.redd.it/abc.jpeg?width=1274&amp;s=deadbeef"'));
assert.ok(!out.includes('<a href'));
});
test('a link that is not an image is still a link', () => {
const out = mdi('see https://example.com/article');
assert.ok(out.includes('<a href="https://example.com/article"'));
assert.ok(!out.includes('<img'));
});
test('a sentence after a pasted image keeps its punctuation out of the address', () => {
const out = mdi('here https://i.redd.it/x.jpg.');
assert.ok(out.includes('src="https://i.redd.it/x.jpg"'), out);
assert.ok(out.endsWith('.</p>'), out);
});
test('only http and https become pictures', () => {
const out = mdi('![x](javascript:alert(1))');
assert.ok(!out.includes('<img'));
assert.ok(out.includes('![x]'));
});
test("an image's alt text is escaped like anything else a stranger wrote", () => {
const out = mdi('![" onerror=alert(1)](https://i.redd.it/x.jpg)');
assert.ok(!out.includes('onerror=alert(1)>'), out);
assert.ok(out.includes('&quot;'));
});
test('images inside a quote are still placed', () => {
assert.match(mdi('> ![](https://i.redd.it/x.gif)'), /<blockquote><p><img/);
});