feat(anime): show extension and repo icons in extensions panel

- Add extension-icons module (buildIconIndex, isSafeIconUrl, repoFaviconUrl, iconMonogram) with tests
- Render each row's icon with a monogram fallback while loading or missing
- Installed extensions borrow their icon from the catalogue; repo rows use the index host's favicon
- Restrict icon loads to https URLs
- Thread iconUrl through AvailableExtension and the runtime, document the feature
This commit is contained in:
2026-08-06 22:02:38 -07:00
parent 4c6bfaa22e
commit 47d31e9628
7 changed files with 190 additions and 1 deletions
+42
View File
@@ -0,0 +1,42 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { buildIconIndex, iconMonogram, isSafeIconUrl, repoFaviconUrl } from './extension-icons';
test('buildIconIndex maps packages to their icon, skipping empty URLs', () => {
const index = buildIconIndex([
{ pkg: 'a.b.c', iconUrl: 'https://repo.example/icon/a.b.c.png' },
{ pkg: 'd.e.f', iconUrl: '' },
]);
assert.equal(index.get('a.b.c'), 'https://repo.example/icon/a.b.c.png');
assert.equal(index.has('d.e.f'), false);
});
test('isSafeIconUrl accepts https only', () => {
assert.equal(isSafeIconUrl('https://repo.example/icon/x.png'), true);
assert.equal(isSafeIconUrl('http://repo.example/icon/x.png'), false);
assert.equal(isSafeIconUrl('javascript:alert(1)'), false);
assert.equal(isSafeIconUrl(null), false);
assert.equal(isSafeIconUrl(undefined), false);
});
test('repoFaviconUrl points at the index host, https only', () => {
assert.equal(
repoFaviconUrl(' https://raw.githubusercontent.com/u/r/main/index.min.json '),
'https://raw.githubusercontent.com/favicon.ico',
);
assert.equal(repoFaviconUrl('http://repo.example/index.json'), null);
assert.equal(repoFaviconUrl('not a url'), null);
});
test('iconMonogram takes the first letter or digit, uppercased', () => {
assert.equal(iconMonogram('AllAnime'), 'A');
assert.equal(iconMonogram(' gogoanime'), 'G');
assert.equal(iconMonogram('» Foo'), 'F');
assert.equal(iconMonogram('9anime'), '9');
assert.equal(iconMonogram('アニメ'), 'ア');
});
test('iconMonogram falls back to a placeholder for a nameless row', () => {
assert.equal(iconMonogram(''), '?');
assert.equal(iconMonogram('---'), '?');
});