-
Notifications
You must be signed in to change notification settings - Fork 7
render-helper: fast thumbnail lookup, thumbnails first, memoized nulls #1611
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
d03b384
render-helper: fast thumbnail lookup, thumbnails first, memoized nulls
feruzm 88a26ba
review: match the renderer's bare-URL rules, keep the entry preload o…
feruzm faa6aa9
review: strip HTML comments with an index scan, rebuild dist
feruzm 9c97b7e
review: mark tag interiors, walk every YouTube link, compare anchors …
feruzm c43173b
review: pin the script-text case as a documented divergence, rebuild …
feruzm da8a37b
review: strip hidden regions with index scans throughout, rebuild dist
feruzm b73356c
review: a markdown autolink is not a tag, rebuild dist
feruzm 85f469f
review: case-insensitive autolinks, tag boundaries as the renderer re…
feruzm a55f841
review: read the opening tag the renderer's way, rebuild dist
feruzm d63ded4
review: image anchors by first text child, typed fixtures, memo spec …
feruzm 4f07981
review: hide <pre> only where the parser keeps it raw, rebuild dist
feruzm f56ebd2
review: decide block context on the original lines, rebuild dist
feruzm e23e74b
review: blank spans in one pass, rebuild dist
feruzm eff25a3
review: container prefixes in the block model, linear image-href chec…
feruzm 08ab1ae
review: container prefixes in any alternation, rebuild dist
feruzm e9c3d78
review: nested list markers, indented code in context, rebuild dist
feruzm d31cd01
review: an open HTML block is raw until its blank line, rebuild dist
feruzm 5927907
review: a bare ! is prose, anchors matched quote-aware with bare href…
feruzm 1925087
review: join anchor blanking once, rebuild dist
feruzm f097a15
review: data-href is not the href, rebuild dist
feruzm 31515ba
chore: apply changeset versioning for PR #1611
github-actions[bot] ac40c7b
review: classify URL tokens in code, rebuild dist
feruzm e208f7d
review: a tag with a glued attribute is text, rebuild dist
feruzm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
212 changes: 212 additions & 0 deletions
212
packages/render-helper/src/catch-post-image-fast.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| import { catchPostImage, getEntryImageRawUrl } from './catch-post-image' | ||
| import { markdown2Html } from './markdown-2-html' | ||
| import { buildPictureSources, proxifyImageSrc } from './proxify-image-src' | ||
|
|
||
| // Distinct author/permlink per fixture: catchPostImage memoizes per post and | ||
| // size, process-wide, so two fixtures sharing a key would share an answer. | ||
| let n = 0 | ||
| const entry = (body: string, json_metadata: unknown = {}) => ({ | ||
| author: 'fast', | ||
| permlink: `p-${n++}`, | ||
| last_update: '2019-05-10T09:15:21', | ||
| body, | ||
| json_metadata | ||
| }) as any | ||
|
|
||
| const FAST = { fast: true } | ||
|
|
||
| describe('catchPostImage thumbnails tier', () => { | ||
| it('prefers json_metadata.thumbnails[0] over image[0]', () => { | ||
| const e = entry('text', { | ||
| thumbnails: ['https://images.hive.blog/poster.png'], | ||
| image: ['https://images.hive.blog/cover.png'] | ||
| }) | ||
| expect(catchPostImage(e, 320, 180, 'match')).toBe( | ||
| proxifyImageSrc('https://images.hive.blog/poster.png', 320, 180, 'match') | ||
| ) | ||
| }) | ||
|
|
||
| it('accepts thumbnails published as a bare string', () => { | ||
| const e = entry('text', { thumbnails: 'https://images.hive.blog/single.png' }) | ||
| expect(catchPostImage(e, 320, 180, 'match')).toBe( | ||
| proxifyImageSrc('https://images.hive.blog/single.png', 320, 180, 'match') | ||
| ) | ||
| }) | ||
|
|
||
| it('skips non-string members and falls through to image when thumbnails holds nothing usable', () => { | ||
| const junk = entry('text', { thumbnails: [null, 42, ''], image: ['https://images.hive.blog/cover.png'] }) | ||
| expect(catchPostImage(junk, 320, 180, 'match')).toBe( | ||
| proxifyImageSrc('https://images.hive.blog/cover.png', 320, 180, 'match') | ||
| ) | ||
| const shape = entry('text', { thumbnails: { 0: 'https://images.hive.blog/object.png' }, image: ['https://images.hive.blog/cover.png'] }) | ||
| expect(catchPostImage(shape, 320, 180, 'match')).toBe( | ||
| proxifyImageSrc('https://images.hive.blog/cover.png', 320, 180, 'match') | ||
| ) | ||
| }) | ||
|
|
||
| it('proxies a gif thumbnail unsized, like a gif cover', () => { | ||
| const e = entry('text', { thumbnails: ['https://images.hive.blog/anim.gif'] }) | ||
| expect(catchPostImage(e, 320, 180, 'match')).toBe( | ||
| proxifyImageSrc('https://images.hive.blog/anim.gif', 0, 0, 'match') | ||
| ) | ||
| }) | ||
|
|
||
| it('still reads image when there is no thumbnails field at all', () => { | ||
| const e = entry('text', { image: ['https://images.hive.blog/cover.png'] }) | ||
| expect(catchPostImage(e, 320, 180, 'match')).toBe( | ||
| proxifyImageSrc('https://images.hive.blog/cover.png', 320, 180, 'match') | ||
| ) | ||
| }) | ||
| }) | ||
|
|
||
| describe('catchPostImage fast mode', () => { | ||
| // Two fixtures with identical content, one per mode, so the memo cannot hand | ||
| // the second call the first one's answer. | ||
| const both = (body: string, meta: unknown = {}) => ({ | ||
| full: catchPostImage(entry(body, meta), 600, 500, 'match'), | ||
| fast: catchPostImage(entry(body, meta), 600, 500, 'match', FAST) | ||
| }) | ||
|
|
||
| it('agrees with the full lookup on a metadata image', () => { | ||
| const r = both('text', { image: ['https://images.hive.blog/cover.png'] }) | ||
| expect(r.fast).toBe(r.full) | ||
| expect(r.fast).toBeTruthy() | ||
| }) | ||
|
|
||
| it('agrees with the full lookup on a markdown image', () => { | ||
| const r = both('intro\n\n\n\nrest') | ||
| expect(r.fast).toBe(r.full) | ||
| expect(r.fast).toBe(proxifyImageSrc('https://images.hive.blog/in-body.png', 600, 500, 'match')) | ||
| }) | ||
|
|
||
| it('agrees with the full lookup on an HTML img', () => { | ||
| const r = both('<p>hi</p><img src="https://images.hive.blog/tag.png" alt="">') | ||
| expect(r.fast).toBe(r.full) | ||
| expect(r.fast).toBeTruthy() | ||
| }) | ||
|
|
||
| it('finds a <center>-wrapped bare image URL without rendering', () => { | ||
| const r = both('<center>https://images.hive.blog/DQmb59qYM1czWSDDw2dRmUHJ7s97L6S6Rk3uZLyA5vCxAEr/pic.jpg</center>') | ||
| expect(r.fast).toBe(r.full) | ||
| expect(r.fast).toBeTruthy() | ||
| }) | ||
|
|
||
| it('derives the same YouTube poster the full render produces for a bare URL', () => { | ||
| const r = both('Check this out\n\nhttps://www.youtube.com/watch?v=dQw4w9WgXcQ\n\nthanks') | ||
| expect(r.full).toBeTruthy() | ||
| expect(r.fast).toBe(r.full) | ||
| }) | ||
|
|
||
| it('derives the poster for youtu.be, shorts and a [url](url) link too', () => { | ||
| for (const body of [ | ||
| 'see https://youtu.be/dQw4w9WgXcQ now', | ||
| 'see https://www.youtube.com/shorts/dQw4w9WgXcQ now', | ||
| 'see [https://www.youtube.com/watch?v=dQw4w9WgXcQ](https://www.youtube.com/watch?v=dQw4w9WgXcQ) now' | ||
| ]) { | ||
| const r = both(body) | ||
| expect(r.full, body).toBeTruthy() | ||
| expect(r.fast, body).toBe(r.full) | ||
| } | ||
| }) | ||
|
|
||
| it('keeps the full lookup precedence: a markdown image wins over an earlier video', () => { | ||
| // The full lookup returns the regex-found image before it would ever render | ||
| // the markdown and see the poster. Fast mode mirrors that, not source order. | ||
| const r = both('https://www.youtube.com/watch?v=dQw4w9WgXcQ\n\n') | ||
| expect(r.fast).toBe(r.full) | ||
| expect(r.fast).toBe(proxifyImageSrc('https://images.hive.blog/later.png', 600, 500, 'match')) | ||
| }) | ||
|
|
||
| it('keeps the full lookup precedence: a markdown image before the video', () => { | ||
| const r = both('\n\nhttps://www.youtube.com/watch?v=dQw4w9WgXcQ') | ||
| expect(r.fast).toBe(r.full) | ||
| expect(r.fast).toBe(proxifyImageSrc('https://images.hive.blog/first.png', 600, 500, 'match')) | ||
| }) | ||
|
|
||
| it('orders a bare image URL and a video poster by source position, as the render does', () => { | ||
| const posterFirst = both('https://www.youtube.com/watch?v=dQw4w9WgXcQ\n\nhttps://files.peakd.com/x/bare.png') | ||
| expect(posterFirst.full).toBeTruthy() | ||
| expect(posterFirst.fast).toBe(posterFirst.full) | ||
| expect(posterFirst.fast).not.toBe(proxifyImageSrc('https://files.peakd.com/x/bare.png', 600, 500, 'match')) | ||
|
|
||
| const bareFirst = both('https://files.peakd.com/x/bare.png\n\nhttps://www.youtube.com/watch?v=dQw4w9WgXcQ') | ||
| expect(bareFirst.fast).toBe(bareFirst.full) | ||
| expect(bareFirst.fast).toBe(proxifyImageSrc('https://files.peakd.com/x/bare.png', 600, 500, 'match')) | ||
| }) | ||
|
|
||
| it('does not read a YouTube link whose label differs from its href as a poster', () => { | ||
| const r = both('watch [this](https://www.youtube.com/watch?v=dQw4w9WgXcQ) later') | ||
| expect(r.fast).toBe(r.full) | ||
| expect(r.fast).toBeNull() | ||
| }) | ||
|
|
||
| it('ignores a YouTube URL inside a code block', () => { | ||
| const r = both('```\nhttps://www.youtube.com/watch?v=dQw4w9WgXcQ\n```') | ||
| expect(r.fast).toBe(r.full) | ||
| expect(r.fast).toBeNull() | ||
| }) | ||
|
|
||
| it('returns null for a body with no image at all, same as the full lookup', () => { | ||
| const r = both('<p>lorem ipsum dolor</p> sit amet') | ||
| expect(r.fast).toBeNull() | ||
| expect(r.full).toBeNull() | ||
| }) | ||
|
|
||
| it('gives up where only the markdown tier could decide (ambiguous markdown URL)', () => { | ||
| // The regex bails on a markdown image URL containing `(`; the full render | ||
| // resolves it. That is the one class fast mode knowingly hands back null for. | ||
| const r = both('_full.jpg)') | ||
| expect(r.full).toBeTruthy() | ||
| expect(r.fast).toBeNull() | ||
| }) | ||
|
|
||
| it('applies to a raw markdown string as well', () => { | ||
| expect(catchPostImage('<center>https://images.hive.blog/x/pic.jpg</center>', 0, 0, 'match', FAST)).toBeTruthy() | ||
| expect(catchPostImage('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 0, 0, 'match', FAST)).toBe( | ||
| catchPostImage('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 0, 0, 'match') | ||
| ) | ||
| expect(catchPostImage('_full.jpg)', 0, 0, 'match', FAST)).toBeNull() | ||
| }) | ||
|
|
||
| it('memoizes the two modes separately', () => { | ||
| const e = entry('_full.jpg)') | ||
| expect(catchPostImage(e, 0, 0, 'match', FAST)).toBeNull() | ||
| expect(catchPostImage(e, 0, 0, 'match')).toBeTruthy() | ||
| expect(catchPostImage(e, 0, 0, 'match', FAST)).toBeNull() | ||
| }) | ||
| }) | ||
|
|
||
| describe('getEntryImageRawUrl and the LCP preload for a <center>-wrapped bare URL', () => { | ||
| it('finds it, and the preload avif still byte-matches the in-body <picture>', () => { | ||
| const firstAvif = (ss: string) => ss.split(',')[0].trim().split(/\s+/)[0].replace(/&/g, '&') | ||
| const e = entry('<center>https://files.peakd.com/x/center-cover.png</center>') | ||
| const raw = getEntryImageRawUrl(e) | ||
| expect(raw).toBe('https://files.peakd.com/x/center-cover.png') | ||
| const m = markdown2Html(e, false).match(/<source type="image\/avif" srcset="([^"]+)"/) | ||
| expect(m).not.toBeNull() | ||
| expect(firstAvif(buildPictureSources(raw as string).avif)).toBe(firstAvif(m![1])) | ||
| }) | ||
|
|
||
| it('does not read the text of an anchor pointing elsewhere as a bare URL', () => { | ||
| // `>` before a URL is allowed for wrapping tags, which would also admit an | ||
| // anchor's text. The renderer leaves such a link alone unless text equals | ||
| // href, so the scan blanks those anchors first. Applies to images and to | ||
| // YouTube URLs alike, and to the LCP preload (getEntryImageRawUrl). | ||
| const img = '<a href="https://example.com/page">https://files.peakd.com/x/linked.png</a>' | ||
| expect(markdown2Html(entry(img), false)).not.toContain('<img') | ||
| expect(getEntryImageRawUrl(entry(img))).toBeNull() | ||
| expect(catchPostImage(entry(img), 0, 0, 'match', FAST)).toBeNull() | ||
| expect(catchPostImage(entry(img), 0, 0, 'match')).toBeNull() | ||
|
|
||
| const yt = '<a href="https://example.com/page">https://www.youtube.com/watch?v=dQw4w9WgXcQ</a>' | ||
| expect(catchPostImage(entry(yt), 0, 0, 'match', FAST)).toBe(catchPostImage(entry(yt), 0, 0, 'match')) | ||
| }) | ||
|
|
||
| it('still reads an anchor whose text equals its image href, which the renderer promotes', () => { | ||
| const u = 'https://files.peakd.com/x/self.png' | ||
| const body = `<a href="${u}">${u}</a>` | ||
| const full = catchPostImage(entry(body), 0, 0, 'match') | ||
| expect(catchPostImage(entry(body), 0, 0, 'match', FAST)).toBe(full) | ||
| expect(getEntryImageRawUrl(entry(body))).toBe(markdown2Html(entry(body), false).includes('<img') ? u : null) | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { vi } from 'vitest' | ||
|
|
||
| // Counted stand-in for the markdown tier. Returning an image-free document | ||
| // makes every lookup land on the null branch, which is the case the memo used | ||
| // to miss: `if (item)` treated a cached null as a miss and re-rendered. | ||
| const render = vi.fn(() => '<p>nothing to see</p>') | ||
| vi.mock('./markdown-2-html', () => ({ markdown2Html: (...args: unknown[]) => render(...args) })) | ||
|
|
||
| import { catchPostImage } from './catch-post-image' | ||
|
qodo-code-review[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| let n = 0 | ||
| const entry = (body: string) => ({ | ||
| author: 'memo', | ||
| permlink: `p-${n++}`, | ||
| last_update: '2019-05-10T09:15:21', | ||
| body, | ||
| json_metadata: {} | ||
| }) as any | ||
|
|
||
| describe('catchPostImage memoizes null results', () => { | ||
| beforeEach(() => render.mockClear()) | ||
|
|
||
| it('renders markdown once for a body with no image, not once per call', () => { | ||
| const e = entry('a long body with no image in it') | ||
| expect(catchPostImage(e, 0, 0, 'match')).toBeNull() | ||
| expect(catchPostImage(e, 0, 0, 'match')).toBeNull() | ||
| expect(catchPostImage(e, 0, 0, 'match')).toBeNull() | ||
| expect(render).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| it('keys the memo on size and format, as before', () => { | ||
| const e = entry('another body with no image') | ||
| catchPostImage(e, 0, 0, 'match') | ||
| catchPostImage(e, 600, 500, 'match') | ||
| catchPostImage(e, 600, 500, 'match') | ||
| expect(render).toHaveBeenCalledTimes(2) | ||
| }) | ||
|
|
||
| it('never renders markdown in fast mode', () => { | ||
| const e = entry('a body with no image, looked up in fast mode') | ||
| expect(catchPostImage(e, 0, 0, 'match', { fast: true })).toBeNull() | ||
| expect(catchPostImage(e, 600, 500, 'match', { fast: true })).toBeNull() | ||
| expect(catchPostImage(e.body, 0, 0, 'match', { fast: true })).toBeNull() | ||
| expect(render).not.toHaveBeenCalled() | ||
| }) | ||
| }) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.