From 34bd3aed834d9e9ec8f97231ceb97eb0028fc212 Mon Sep 17 00:00:00 2001 From: tommy0103 Date: Mon, 3 Aug 2026 01:28:45 +0800 Subject: [PATCH] fix(renderer): accept both marked image renderer signatures marked <= 14 calls renderer.image(href, title, text); marked >= 15 passes the token instead. With only the positional form handled, an upgrade of the pinned CDN build would turn every session image into fallback text without any error, so normalise both shapes and cover them with a unit test. The tag name moves into session-image-contract.js so the Markdown renderer no longer reaches it through the module that imports the .vue component, which is what kept it out of Node's test runner. Co-Authored-By: Claude Opus 5 (1M context) --- .../renderer/src/markdown-image-renderer.js | 25 ++++++++++--- .../renderer/src/session-image-contract.js | 10 ++++++ app/src/renderer/src/session-image-element.js | 3 +- tests/markdown-image-renderer.test.mjs | 36 +++++++++++++++++++ 4 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 app/src/renderer/src/session-image-contract.js create mode 100644 tests/markdown-image-renderer.test.mjs 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: '' }, + ); +});