Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/src/lib/shared.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/** Fixed social-card canvas dimensions and cache version. */
/** Standard social-card canvas dimensions and cache version. */
export const card = {
height: 420,
padding: 80,
scale: 1.5,
version: 3,
version: 4,
width: 800,
} as const

Expand Down
20 changes: 10 additions & 10 deletions app/worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const app = new Hono<{ Bindings: Cloudflare.Env }>()
if (!kept) return c.redirect('/og.jpg', 302)
// Named rather than `caches.default`, which shares its keyspace with the
// asset cache in front of this Worker.
const cache = await caches.open('og')
const cache = await caches.open(`og:${Links.card.version}`)
const hit = await cache.match(c.req.raw)
if (hit) return hit

Expand Down Expand Up @@ -273,7 +273,7 @@ function varied(response: Response): Response {
/**
* Renders a shared-link card from encoded editor state.
*
* Uses the image route to share validation and rendering behavior. Returns
* Uses the document route to share validation and rendering behavior. Returns
* `undefined` when rendering fails so callers can serve the default card.
*/
async function card(
Expand All @@ -289,27 +289,27 @@ async function card(
Wallpapers.at(settings.background) ??
(settings.background === 'default' ? Wallpapers.byId(settings.theme) : undefined)
const picture = named ? await inlined(env, origin, named.id) : undefined
// Limit source to a fixed viewport so dense snippets remain readable.
const shown = Links.excerpt(Links.withoutTypes(settings.code))
// Truncate source to the largest canvas, then size the canvas to what remains.
const shape = Links.layout(Links.withoutTypes(settings.code))
const drawn = await api.request(
'/document',
{
body: JSON.stringify({
// Pass wallpaper content through `picture`, not the application identifier.
background: settings.background.startsWith('wallpaper:') ? 'default' : settings.background,
code: shown.code,
height: Links.card.height,
code: shape.code,
height: shape.height,
// Resolve automatic language detection before invoking the renderer.
lang: settings.lang === 'auto' ? (detect(settings.code) ?? 'typescript') : settings.lang,
padding: Links.card.padding,
padding: shape.padding,
...(picture ? { picture } : {}),
// Apply the selected theme's frame radius override.
radius: Themes.frame(settings.theme).radius ?? settings.radius,
theme: settings.theme,
titleBar: false,
// Preview crawlers cannot wait for package acquisition during Twoslash.
twoslash: false,
width: Links.card.width,
width: shape.width,
}),
headers: { 'content-type': 'application/json' },
method: 'POST',
Expand All @@ -320,8 +320,8 @@ async function card(
if (!drawn.ok) return undefined
try {
const png = await Browser.screenshot(env.BROWSER, {
html: Links.fade(await drawn.text(), shown.overflow),
scale: Links.card.scale,
html: Links.fade(await drawn.text(), shape.overflow),
scale: shape.scale,
})
return png.buffer.slice(png.byteOffset, png.byteOffset + png.byteLength) as ArrayBuffer
} catch {
Expand Down
106 changes: 84 additions & 22 deletions app/worker/links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('card', () => {
"height": 420,
"padding": 80,
"scale": 1.5,
"version": 3,
"version": 4,
"width": 800,
}
`)
Expand All @@ -45,27 +45,6 @@ describe('card', () => {
})
})

describe('excerpt', () => {
test('limits long lines to 72 display columns', () => {
const shown = Links.excerpt('x'.repeat(100))
expect(shown.code).toHaveLength(72)
expect(shown.overflow).toEqual({ horizontal: true, vertical: false })
})

test('limits tall snippets to ten lines', () => {
const shown = Links.excerpt(Array.from({ length: 12 }, (_, at) => `line ${at}`).join('\n'))
expect(shown.code.split('\n')).toHaveLength(10)
expect(shown.overflow).toEqual({ horizontal: false, vertical: true })
})

test('preserves code that fits the viewport', () => {
expect(Links.excerpt('const a = 1')).toEqual({
code: 'const a = 1',
overflow: { horizontal: false, vertical: false },
})
})
})

describe('withoutTypes', () => {
test('removes query rows without changing surrounding source', () => {
expect(Links.withoutTypes('const value = run()\n// ^?\nvalue\n // ^? ')).toBe(
Expand Down Expand Up @@ -95,6 +74,89 @@ describe('fade', () => {
})
})

describe('layout', () => {
test('uses standard dimensions for a short snippet', () => {
const { code, overflow, ...shape } = Links.layout('const a = 1\n')
expect(code).toBe('const a = 1\n')
expect(overflow).toEqual({ horizontal: false, vertical: false })
expect(shape).toMatchInlineSnapshot(`
{
"height": 420,
"padding": 80,
"scale": 1.5,
"width": 800,
}
`)
})

test('preserves output dimensions as the canvas grows', () => {
const fifteen = Array.from({ length: 15 }, (_, at) => `const v${at} = ${at}`).join('\n')
const { code, overflow, ...grown } = Links.layout(fifteen)
expect(code).toBe(fifteen)
expect(overflow).toEqual({ horizontal: false, vertical: false })
expect(grown).toMatchInlineSnapshot(`
{
"height": 546,
"padding": 80,
"scale": 1.1538461538461537,
"width": 1040,
}
`)
// Verify that scaling produces the fixed social-card dimensions.
expect(Math.round(grown.width * grown.scale)).toBe(1200)
expect(Math.round(grown.height * grown.scale)).toBe(630)
})

test('draws the tallest snippet the card holds without dropping a line', () => {
const most = Array.from({ length: 29 }, () => 'const a = 1').join('\n')
const grown = Links.layout(most)
expect(grown.width).toBe(1600)
expect(grown.code).toBe(most)
expect(grown.overflow.vertical).toBe(false)
})

test('drops the line past the last one the card holds', () => {
const over = Array.from({ length: 30 }, (_, at) => `const v${at} = ${at}`).join('\n')
const grown = Links.layout(over)
expect(grown.width).toBe(1600)
expect(grown.code).toBe(`${over.split('\n').slice(0, 29).join('\n')}\n`)
expect(grown.overflow.vertical).toBe(true)
})

test('grows the canvas when a long line wraps', () => {
expect(Links.layout('x'.repeat(721)).width).toBeGreaterThan(800)
})

test('cuts wrapped code to the rows available at the widest canvas', () => {
const grown = Links.layout('x'.repeat(10_000))
expect(grown.code.length).toBeLessThan(10_000)
expect(grown.width).toBe(1600)
expect(grown.overflow.vertical).toBe(true)
})

test('wraps a wide-character line at half the columns of a Latin one', () => {
// East Asian wide characters occupy two display columns, so the same count wraps sooner.
const wide = Links.layout('あ'.repeat(400))
const latin = Links.layout('a'.repeat(400))
expect(latin.width).toBe(800)
expect(wide.width).toBeGreaterThan(latin.width)
})

test('keeps single-column Unicode without truncating it as wide text', () => {
const code = Array.from({ length: 29 }, () => 'éλЖ'.repeat(30)).join('\n')
const grown = Links.layout(code)
expect(grown.width).toBe(1600)
expect(grown.code).toBe(code)
})

test('includes annotation-row margins when choosing the canvas', () => {
const plain = Array.from({ length: 10 }, () => 'const a = 1').join('\n')
const tagged = Array.from({ length: 10 }, () => '// @log: looked at').join('\n')
expect(Links.layout(plain).width).toBe(800)
expect(Links.layout(tagged).width).toBeGreaterThan(800)
})
})

describe('describe', () => {
/** Creates a deterministic implementation of the model interface. */
function answering(answer: unknown): Links.describe.Model {
Expand Down
Loading
Loading