diff --git a/public/images/group-symbols/☂️.png b/public/images/group-symbols/☂️.png new file mode 100644 index 0000000..5a5766d Binary files /dev/null and b/public/images/group-symbols/☂️.png differ diff --git a/public/images/group-symbols/☕.png b/public/images/group-symbols/☕.png new file mode 100644 index 0000000..2aae1fd Binary files /dev/null and b/public/images/group-symbols/☕.png differ diff --git a/public/images/group-symbols/⚽.png b/public/images/group-symbols/⚽.png new file mode 100644 index 0000000..1d9c5b1 Binary files /dev/null and b/public/images/group-symbols/⚽.png differ diff --git a/public/images/group-symbols/🌻.png b/public/images/group-symbols/🌻.png new file mode 100644 index 0000000..c06523a Binary files /dev/null and b/public/images/group-symbols/🌻.png differ diff --git a/public/images/group-symbols/🍎.png b/public/images/group-symbols/🍎.png new file mode 100644 index 0000000..90fd146 Binary files /dev/null and b/public/images/group-symbols/🍎.png differ diff --git a/public/images/group-symbols/📘.png b/public/images/group-symbols/📘.png new file mode 100644 index 0000000..f67a15b Binary files /dev/null and b/public/images/group-symbols/📘.png differ diff --git a/public/images/group-symbols/🔑.png b/public/images/group-symbols/🔑.png new file mode 100644 index 0000000..5062d24 Binary files /dev/null and b/public/images/group-symbols/🔑.png differ diff --git a/public/images/group-symbols/🔔.png b/public/images/group-symbols/🔔.png new file mode 100644 index 0000000..050ff5f Binary files /dev/null and b/public/images/group-symbols/🔔.png differ diff --git a/public/js/group-symbols.js b/public/js/group-symbols.js new file mode 100644 index 0000000..12a6499 --- /dev/null +++ b/public/js/group-symbols.js @@ -0,0 +1,51 @@ +(function (ns) { + 'use strict'; + + const entries = Object.freeze([ + ['🍎', 'Apfel'], + ['🔑', 'Schlüssel'], + ['🔔', 'Glocke'], + ['📘', 'Buch'], + ['🌻', 'Sonnenblume'], + ['☂️', 'Regenschirm'], + ['⚽', 'Fußball'], + ['☕', 'Tasse'] + ].map(([unicode, label]) => Object.freeze({ + unicode, + label, + imageUrl: `/images/group-symbols/${unicode}.png` + }))); + + // Presentation selectors must not create different symbols for the same object. + function normalizeUnicode(value) { + if (typeof value !== 'string') return null; + return value.normalize('NFC').replaceAll(/[\uFE0E\uFE0F]/g, ''); + } + + const byUnicode = new Map(entries.map(entry => [normalizeUnicode(entry.unicode), entry])); + + /** + * Read-only catalogue of the eight approved group-code illustrations. + * Loading this file only registers the class; it does not create UI or sessions. + */ + class GroupSymbols { + static list() { + return entries; + } + + /** Returns the immutable motif descriptor, or null for an unknown symbol. */ + static get(unicode) { + return byUnicode.get(normalizeUnicode(unicode)) ?? null; + } + + static has(unicode) { + return this.get(unicode) !== null; + } + + static imageUrl(unicode) { + return this.get(unicode)?.imageUrl ?? null; + } + } + + ns.GroupSymbols = GroupSymbols; +}(globalThis.Wegwichtel ??= {})); diff --git a/test/group-symbols.test.js b/test/group-symbols.test.js new file mode 100644 index 0000000..b440353 --- /dev/null +++ b/test/group-symbols.test.js @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import test from 'node:test'; +import vm from 'node:vm'; + +const root = path.resolve(import.meta.dirname, '..'); + +async function catalogue(namespace) { + const filename = path.join(root, 'public/js/group-symbols.js'); + const context = { Wegwichtel: namespace }; + context.globalThis = context; + vm.runInNewContext(await fs.readFile(filename, 'utf8'), context, { filename }); + return context.Wegwichtel.GroupSymbols; +} + +test('group motifs resolve all eight approved Unicode symbols to existing PNG assets', async () => { + const existing = { Config: { apiBase: '/api' } }; + const symbols = await catalogue(existing); + assert.equal(existing.Config.apiBase, '/api'); + const expected = ['🍎', '🔑', '🔔', '📘', '🌻', '☂️', '⚽', '☕']; + assert.deepEqual(Array.from(symbols.list(), entry => entry.unicode), expected); + assert.equal(new Set(symbols.list().map(entry => entry.unicode)).size, 8); + for (const entry of symbols.list()) { + assert.equal(symbols.get(entry.unicode), entry); + assert.equal(symbols.has(entry.unicode), true); + assert.equal(symbols.imageUrl(entry.unicode), entry.imageUrl); + assert.equal(entry.imageUrl, `/images/group-symbols/${entry.unicode}.png`); + assert.ok(entry.label.length > 0); + const png = await fs.readFile(path.join(root, 'public', entry.imageUrl)); + assert.deepEqual(png.subarray(0, 8), Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])); + assert.equal(png[25], 6, 'each motif must be an RGBA PNG'); + assert.equal(Object.isFrozen(entry), true); + } + assert.equal(Object.isFrozen(symbols.list()), true); + assert.throws(() => { symbols.list()[0].label = 'changed'; }, TypeError); + assert.throws(() => symbols.list().push({}), { name: 'TypeError' }); +}); + +test('Unicode presentation selectors resolve to one motif and invalid inputs stay unknown', async () => { + const symbols = await catalogue(); + for (const base of ['☂', '⚽', '☕']) { + assert.equal(symbols.get(base), symbols.get(`${base}\uFE0F`)); + assert.equal(symbols.get(base), symbols.get(`${base}\uFE0E`)); + } + for (const value of ['', '🍎🔑', 'unknown', ' 🍎 ', null, undefined, 42, {}]) { + assert.equal(symbols.get(value), null); + assert.equal(symbols.imageUrl(value), null); + assert.equal(symbols.has(value), false); + } +});