Files
thatguygriffandClaude Opus 5 031101c382
CI / Typecheck, test, build (pull_request) Successful in 26s
Show a picture whose address was typed without a scheme
`preview.redd.it/lz4drsqh0clh1.jpeg?width=1290&...` was rendering as plain
text -- not a picture, and not even a link. The bare-address rule has always
insisted on `https://`, so anything copied out of an address bar, where the
browser hides the scheme, fell through to nothing at all. That predates the
inline images from the last commit; it just did not matter until pictures
started being worth placing.

The rule is deliberately narrow: a host with a dot, a path, and an image
extension. Widening it to every schemeless address would be the obvious move
and is wrong, because a comment thread is full of dotted, slashed prose --
`src/render/post.ts` would become a link to a website in Tonga, and
`node_modules/foo/bar.js` a website in Jersey. Requiring the extension costs
nothing here, since the only thing worth guessing a scheme for is a picture.

https is assumed. Every host that serves these redirects to it anyway.

Checked both ways: the four address shapes that should become pictures do,
and nine pieces of ordinary prose that must not -- file paths, a Windows
drive letter, a relative path, an email address followed by a filename, a
version number -- still do not.

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

156 lines
6.3 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/);
});
test('an image address typed without a scheme is still the picture', () => {
// Which is how people type them: no https, straight from the address bar.
const out = mdi('preview.redd.it/lz4drsqh0clh1.jpeg?width=1290&s=b27e');
assert.ok(out.includes('<img src="https://preview.redd.it/lz4drsqh0clh1.jpeg?width=1290&amp;s=b27e"'), out);
});
test('a schemeless image address mid-sentence keeps the sentence', () => {
const out = mdi('look at i.redd.it/x.png nice one');
assert.ok(out.startsWith('<p>look at <img'), out);
assert.ok(out.endsWith(' nice one</p>'), out);
});
test('prose full of dots and slashes is not mistaken for an address', () => {
// The reason this rule insists on a host, a path and an image extension.
for (const text of [
'the file is at src/render/post.ts',
'see node_modules/foo/bar.js',
'a path like ./images/cat.jpg',
'C:/Users/x/cat.png',
'email [email protected]/nope.jpg',
'version 1.2.3/4.png',
]) {
assert.ok(!mdi(text).includes('<img'), `treated as an image: ${text}`);
}
});
test('a schemeless address that is not an image is left alone', () => {
// Guessing a scheme is worth it for a picture and not for prose.
assert.equal(mdi('example.com/article'), '<p>example.com/article</p>');
});