diff --git a/app/src/renderer/src/markdown-image-renderer.js b/app/src/renderer/src/markdown-image-renderer.js index 84c11e0..f48abdd 100644 --- a/app/src/renderer/src/markdown-image-renderer.js +++ b/app/src/renderer/src/markdown-image-renderer.js @@ -1,4 +1,4 @@ -import { SESSION_IMAGE_TAG } from './session-image-element.js'; +import { SESSION_IMAGE_TAG } from './session-image-contract.js'; const SAFE_IMAGE_PROTOCOLS = new Set(['blob:', 'file:', 'http:', 'https:']); let configuredMarked = null; @@ -26,10 +26,25 @@ function imageFallback(alt) { return fallback.outerHTML; } -export function renderSessionMarkdownImage(href, title, text) { - const source = decodeMarkedAttribute(href).trim(); - const alt = decodeMarkedAttribute(text); - const accessibleTitle = decodeMarkedAttribute(title); +// marked <= 14 calls renderer.image(href, title, text); marked >= 15 passes the +// image token instead. Accepting both keeps an upgrade from silently turning +// every session image into fallback text. +export function normalizeMarkdownImageToken(hrefOrToken, title, text) { + if (hrefOrToken && typeof hrefOrToken === 'object') { + return { + href: hrefOrToken.href ?? '', + title: hrefOrToken.title ?? '', + text: hrefOrToken.text ?? '', + }; + } + return { href: hrefOrToken ?? '', title: title ?? '', text: text ?? '' }; +} + +export function renderSessionMarkdownImage(hrefOrToken, title, text) { + const token = normalizeMarkdownImageToken(hrefOrToken, title, text); + const source = decodeMarkedAttribute(token.href).trim(); + const alt = decodeMarkedAttribute(token.text); + const accessibleTitle = decodeMarkedAttribute(token.title); if (!source || !isSafeImageSource(source)) return imageFallback(alt); const image = document.createElement(SESSION_IMAGE_TAG); diff --git a/app/src/renderer/src/session-image-contract.js b/app/src/renderer/src/session-image-contract.js new file mode 100644 index 0000000..3e6da82 --- /dev/null +++ b/app/src/renderer/src/session-image-contract.js @@ -0,0 +1,10 @@ +// Shared constants for the session image element. Kept free of the component +// import so the Markdown renderer (and its tests) do not have to pull a .vue +// module into scope just to know the tag name. + +export const SESSION_IMAGE_TAG = 'obelisk-session-image'; + +// Fired from inside the session image element (composed, so it crosses the +// shadow boundary) once the image has either decoded or failed. The virtualized +// timeline uses it to tell real media growth apart from an estimate correction. +export const SESSION_IMAGE_SETTLED_EVENT = 'obelisk-session-image-settled'; diff --git a/app/src/renderer/src/session-image-element.js b/app/src/renderer/src/session-image-element.js index f365952..abaea24 100644 --- a/app/src/renderer/src/session-image-element.js +++ b/app/src/renderer/src/session-image-element.js @@ -1,7 +1,6 @@ import { defineCustomElement } from 'vue'; import SessionImage from './components/SessionImage.ce.vue'; - -export const SESSION_IMAGE_TAG = 'obelisk-session-image'; +import { SESSION_IMAGE_TAG } from './session-image-contract.js'; export function registerSessionImageElement() { if (customElements.get(SESSION_IMAGE_TAG)) return; diff --git a/tests/markdown-image-renderer.test.mjs b/tests/markdown-image-renderer.test.mjs new file mode 100644 index 0000000..cd65fe0 --- /dev/null +++ b/tests/markdown-image-renderer.test.mjs @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { normalizeMarkdownImageToken } from '../app/src/renderer/src/markdown-image-renderer.js'; + +// marked <= 14 calls renderer.image(href, title, text); marked >= 15 passes the +// token. Getting this wrong degrades silently: every session image turns into +// fallback text because the href is no longer a string. +test('accepts the positional renderer signature', () => { + assert.deepEqual( + normalizeMarkdownImageToken('http://example.test/a.png', 'A title', 'Alt text'), + { href: 'http://example.test/a.png', title: 'A title', text: 'Alt text' }, + ); +}); + +test('accepts the token renderer signature', () => { + assert.deepEqual( + normalizeMarkdownImageToken({ + type: 'image', + href: 'http://example.test/a.png', + title: 'A title', + text: 'Alt text', + }), + { href: 'http://example.test/a.png', title: 'A title', text: 'Alt text' }, + ); +}); + +test('fills in the fields marked leaves null', () => { + assert.deepEqual( + normalizeMarkdownImageToken({ href: 'http://example.test/a.png', title: null, text: '' }), + { href: 'http://example.test/a.png', title: '', text: '' }, + ); + assert.deepEqual( + normalizeMarkdownImageToken('http://example.test/a.png', null, null), + { href: 'http://example.test/a.png', title: '', text: '' }, + ); +});