Render giphy tokens, and give a video its shape before it plays #11
@@ -149,11 +149,15 @@ Things worth knowing before editing:
|
||||
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
|
||||
written as a token rather than an address — ``,
|
||||
``, `` — and in every case the token is
|
||||
a key in that same comment's own `media_metadata`, so `resolveInlineImages` is one
|
||||
lookup rather than three special cases. 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
|
||||
``, `` — and the token is usually a key
|
||||
in that same comment's own `media_metadata`, so `resolveInlineImages` is one lookup
|
||||
rather than three special cases. Usually: plenty of comments carry a Giphy token and
|
||||
no `media_metadata` at all, and Giphy is the one of the three whose id means something
|
||||
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 —
|
||||
the rule wants a host, a path *and* that extension, because comments are full of
|
||||
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. */
|
||||
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.
|
||||
*
|
||||
* Reddit writes them as ``, ``
|
||||
* or `` — a token rather than an address. In every case
|
||||
* the token is a key in that same comment's `media_metadata`, which is where
|
||||
* the real URL is, so one lookup covers all three and none of them needs
|
||||
* naming here.
|
||||
* or `` — a token rather than an address. Usually the
|
||||
* token is a key in that same comment's `media_metadata`, which is where the
|
||||
* real URL is, so one lookup covers all three and none of them needs naming
|
||||
* 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
|
||||
* untouched.
|
||||
@@ -208,15 +224,17 @@ export function resolveInlineImages(
|
||||
body: string,
|
||||
meta: Record<string, MediaMeta> | undefined,
|
||||
): string {
|
||||
if (!meta) return body;
|
||||
|
||||
return body.replace(INLINE_IMAGE, (whole, alt: string, token: string) => {
|
||||
const entry = meta[token];
|
||||
if (!entry || entry.status !== 'valid') return whole;
|
||||
// An animated one has both; the GIF plays in an `<img>` on its own, which
|
||||
// an MP4 does not.
|
||||
const url = entry.s?.gif ?? entry.s?.u;
|
||||
return url ? `` : whole;
|
||||
const entry = meta?.[token];
|
||||
if (entry?.status === 'valid') {
|
||||
// An animated one has both; the GIF plays in an `<img>` on its own,
|
||||
// which an MP4 does not.
|
||||
const url = entry.s?.gif ?? entry.s?.u;
|
||||
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', () => {
|
||||
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(
|
||||
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', () => {
|
||||
const { comments } = commentsFrom({
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user