Resolve a giphy token with no metadata to look it up in
`` is a token, not an address, and the only thing that turned it into one was a lookup in the comment's own `media_metadata`. Reddit ships plenty of comments carrying such a token and no `media_metadata` at all, and with nothing to look it up in the token itself was what the comment showed. Giphy is the one of the three token kinds whose id means something off Reddit, so that one can be resolved without the lookup. A variant name after the id is dropped: Giphy does not serve every variant of every gif, but the full one is always there. The other two still resolve only through the metadata. An emote id and an upload id name nothing outside Reddit, so with no entry for them there is still nothing to point them at. Closes #10 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01PLkmgp1fWbA4XbarxKdRKt
This commit is contained in:
@@ -149,11 +149,15 @@ Things worth knowing before editing:
|
|||||||
bodies are Markdown, rendered by `src/render/markdown.ts` — escape first, then put
|
bodies are Markdown, rendered by `src/render/markdown.ts` — escape first, then put
|
||||||
back the constructs we chose to support, never `body_html`. An image in a comment is
|
back the constructs we chose to support, never `body_html`. An image in a comment is
|
||||||
written as a token rather than an address — ``,
|
written as a token rather than an address — ``,
|
||||||
``, `` — and in every case the token is
|
``, `` — and the token is usually a key
|
||||||
a key in that same comment's own `media_metadata`, so `resolveInlineImages` is one
|
in that same comment's own `media_metadata`, so `resolveInlineImages` is one lookup
|
||||||
lookup rather than three special cases. A bare `preview.redd.it` address pasted into a
|
rather than three special cases. Usually: plenty of comments carry a Giphy token and
|
||||||
comment is in there too, keyed by the id inside the URL. Prefer `s.gif` over `s.mp4`
|
no `media_metadata` at all, and Giphy is the one of the three whose id means something
|
||||||
for an animated one: a GIF moves in an `<img>` and an MP4 needs a player. An address
|
off Reddit, so that token alone falls back to `i.giphy.com/media/<id>/giphy.gif`. A
|
||||||
|
variant name after the id is dropped — Giphy does not serve every variant of every
|
||||||
|
gif. A bare `preview.redd.it` address pasted into a comment is in there too, keyed by
|
||||||
|
the id inside the URL. Prefer `s.gif` over `s.mp4` for an animated one: a GIF moves in
|
||||||
|
an `<img>` and an MP4 needs a player. An address
|
||||||
typed without a scheme counts as well, but only when it ends in an image extension —
|
typed without a scheme counts as well, but only when it ends in an image extension —
|
||||||
the rule wants a host, a path *and* that extension, because comments are full of
|
the rule wants a host, a path *and* that extension, because comments are full of
|
||||||
dotted, slashed prose that must not turn into links.
|
dotted, slashed prose that must not turn into links.
|
||||||
|
|||||||
+30
-12
@@ -192,14 +192,30 @@ function bodyOf(link: Link): string | undefined {
|
|||||||
/** The whole of ``, with the target captured. */
|
/** The whole of ``, with the target captured. */
|
||||||
const INLINE_IMAGE = /!\[([^\]\n]*)\]\(([^)\s]+)\)/g;
|
const INLINE_IMAGE = /!\[([^\]\n]*)\]\(([^)\s]+)\)/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Giphy token, which unlike the others says what it points at.
|
||||||
|
*
|
||||||
|
* Reddit writes a variant name after the id on some of them
|
||||||
|
* (`giphy|abc123|downsized`); only the id is kept, because Giphy does not
|
||||||
|
* serve every variant for every gif but always serves the full one.
|
||||||
|
*/
|
||||||
|
const GIPHY_TOKEN = /^giphy\|([A-Za-z0-9]+)(?:\|[a-z_]+)?$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Point a comment's inline images at something fetchable.
|
* Point a comment's inline images at something fetchable.
|
||||||
*
|
*
|
||||||
* Reddit writes them as ``, ``
|
* Reddit writes them as ``, ``
|
||||||
* or `` — a token rather than an address. In every case
|
* or `` — a token rather than an address. Usually the
|
||||||
* the token is a key in that same comment's `media_metadata`, which is where
|
* token is a key in that same comment's `media_metadata`, which is where the
|
||||||
* the real URL is, so one lookup covers all three and none of them needs
|
* real URL is, so one lookup covers all three and none of them needs naming
|
||||||
* naming here.
|
* here.
|
||||||
|
*
|
||||||
|
* Usually, not always: Reddit ships plenty of comments carrying a Giphy token
|
||||||
|
* and no `media_metadata` at all, and with nothing to look the token up in
|
||||||
|
* those showed the token itself where the gif should have been. Giphy is the
|
||||||
|
* one kind that can be resolved without the lookup, the id in it being Giphy's
|
||||||
|
* own, so it falls back to Giphy's address for that id. The other two cannot:
|
||||||
|
* their ids mean nothing off Reddit.
|
||||||
*
|
*
|
||||||
* A target that is already an address is not a key, so it falls through
|
* A target that is already an address is not a key, so it falls through
|
||||||
* untouched.
|
* untouched.
|
||||||
@@ -208,15 +224,17 @@ export function resolveInlineImages(
|
|||||||
body: string,
|
body: string,
|
||||||
meta: Record<string, MediaMeta> | undefined,
|
meta: Record<string, MediaMeta> | undefined,
|
||||||
): string {
|
): string {
|
||||||
if (!meta) return body;
|
|
||||||
|
|
||||||
return body.replace(INLINE_IMAGE, (whole, alt: string, token: string) => {
|
return body.replace(INLINE_IMAGE, (whole, alt: string, token: string) => {
|
||||||
const entry = meta[token];
|
const entry = meta?.[token];
|
||||||
if (!entry || entry.status !== 'valid') return whole;
|
if (entry?.status === 'valid') {
|
||||||
// An animated one has both; the GIF plays in an `<img>` on its own, which
|
// An animated one has both; the GIF plays in an `<img>` on its own,
|
||||||
// an MP4 does not.
|
// which an MP4 does not.
|
||||||
const url = entry.s?.gif ?? entry.s?.u;
|
const url = entry.s?.gif ?? entry.s?.u;
|
||||||
return url ? `` : whole;
|
if (url) return ``;
|
||||||
|
}
|
||||||
|
|
||||||
|
const giphy = GIPHY_TOKEN.exec(token)?.[1];
|
||||||
|
return giphy ? `` : whole;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+51
-1
@@ -224,13 +224,63 @@ test('a target that is already an address is left alone', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('a token with no entry, or a broken one, is not invented', () => {
|
test('a token with no entry, or a broken one, is not invented', () => {
|
||||||
assert.equal(resolveInlineImages('', GIPHY), '');
|
// An upload id and an emote id mean nothing off Reddit, so with no entry to
|
||||||
|
// look them up in there is nothing to point them at.
|
||||||
|
assert.equal(resolveInlineImages('', GIPHY), '');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
resolveInlineImages('', { gone: { status: 'failed', e: 'Image' } }),
|
resolveInlineImages('', { gone: { status: 'failed', e: 'Image' } }),
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a giphy token resolves even when the comment carried no metadata', () => {
|
||||||
|
// Reddit ships plenty of these with no `media_metadata` at all. The id in
|
||||||
|
// the token is Giphy's own, so it does not need Reddit to be readable.
|
||||||
|
assert.equal(
|
||||||
|
resolveInlineImages('', undefined),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
resolveInlineImages('', GIPHY),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the metadata still wins where there is any, being what Reddit will serve', () => {
|
||||||
|
assert.match(resolveInlineImages('', GIPHY), /redd\.it/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a variant name after the id is dropped, not all of them being served', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveInlineImages('', undefined),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('only a giphy token is guessed at, and only a well-formed one', () => {
|
||||||
|
for (const token of ['emote|t5_2th52|4358', 'giphy|', 'giphy|../evil', 'giphy|a|b|c']) {
|
||||||
|
assert.equal(resolveInlineImages(``, undefined), ``);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a real comment carrying a giphy token gets the gif', () => {
|
||||||
|
// Captured from the post itself: the comment has the token and no
|
||||||
|
// `media_metadata`, which is the shape that used to show the token instead.
|
||||||
|
const post = toPost(fixture('reddit/video.json'), URL_);
|
||||||
|
const all: string[] = [];
|
||||||
|
const walk = (list: typeof post.comments) => {
|
||||||
|
for (const comment of list ?? []) {
|
||||||
|
if (comment.text) all.push(comment.text);
|
||||||
|
walk(comment.replies);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(post.comments);
|
||||||
|
|
||||||
|
const gif = all.find((text) => text.includes('giphy'));
|
||||||
|
assert.ok(gif, 'the fixture should still carry a giphy comment');
|
||||||
|
assert.match(gif, /!\[gif\]\(https:\/\/i\.giphy\.com\/media\/QfzMP70zmNQiDf5sGP\/giphy\.gif\)/);
|
||||||
|
});
|
||||||
|
|
||||||
test('inline images survive the walk into the comment tree', () => {
|
test('inline images survive the walk into the comment tree', () => {
|
||||||
const { comments } = commentsFrom({
|
const { comments } = commentsFrom({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
Reference in New Issue
Block a user