1/**
2 * AIM "Yellow Smiley" set (AIM 4.x/5.x). Typed codes become the original
3 * 19×19 GIFs from /static/smileys (local-only, AOL artwork). When a GIF is
4 * missing the browser shows the img alt text, which is the Unicode emoji.
5 */
6export interface Smiley {
7 file: string;
8 codes: string[];
9 label: string;
10 emoji: string;
11}
12
13export const SMILEYS: Smiley[] = [
14 { file: '01', codes: [':-)', ':)'], label: 'Content', emoji: '🙂' },
15 { file: '02', codes: [';-)', ';)'], label: 'Clin d’œil', emoji: '😉' },
16 { file: '03', codes: [':-(', ':('], label: 'Triste', emoji: '🙁' },
17 { file: '04', codes: [':-P', ':-p', ':P', ':p'], label: 'Langue', emoji: '😛' },
18 { file: '05', codes: ['=-O', '=-o'], label: 'Surpris', emoji: '😮' },
19 { file: '06', codes: [':-*'], label: 'Bisou', emoji: '😘' },
20 { file: '07', codes: ['>:o'], label: 'Crie', emoji: '😠' },
21 { file: '08', codes: [':-D', ':D'], label: 'Rire', emoji: '😁' },
22 { file: '09', codes: [':-$'], label: 'Radin', emoji: '🤑' },
23 { file: '10', codes: [':-!'], label: 'Gaffe', emoji: '😬' },
24 { file: '11', codes: [':-['], label: 'Gêné', emoji: '😳' },
25 { file: '12', codes: ['O:-)'], label: 'Innocent', emoji: '😇' },
26 { file: '13', codes: [':-\\'], label: 'Indécis', emoji: '😕' },
27 { file: '14', codes: [":'("], label: 'Pleure', emoji: '😢' },
28 { file: '15', codes: [':-X', ':-x'], label: 'Bouche cousue', emoji: '🤐' },
29 { file: '16', codes: ['8-)'], label: 'Cool', emoji: '😎' }
30];
31
32const escape = (text: string) =>
33 text
34 .replace(/&/g, '&')
35 .replace(/</g, '<')
36 .replace(/>/g, '>')
37 .replace(/"/g, '"')
38 .replace(/'/g, ''');
39
40// Codes are matched on already-escaped HTML, so match their escaped form.
41const byEscapedCode = new Map<string, Smiley>();
42for (const smiley of SMILEYS)
43 for (const code of smiley.codes) byEscapedCode.set(escape(code), smiley);
44
45const pattern = new RegExp(
46 // Longest first so "O:-)" wins over ":-)"; codes must not touch letters/digits.
47 `(^|[^\\w])(${[...byEscapedCode.keys()]
48 .sort((a, b) => b.length - a.length)
49 .map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
50 .join('|')})(?=$|[^\\w])`,
51 'g'
52);
53
54export function smileyImg(smiley: Smiley) {
55 return `<img class="aim-smiley" src="/smileys/${smiley.file}.gif" alt="${smiley.emoji}" title="${escape(smiley.codes[0])}" width="19" height="19" draggable="false">`;
56}
57
58/**
59 * Replace smiley codes in escaped HTML text. Only text between tags is
60 * touched so attributes (style="color: …") are never rewritten.
61 */
62export function replaceSmileys(html: string): string {
63 return html
64 .split(/(<[^>]*>)/)
65 .map((part) =>
66 part.startsWith('<')
67 ? part
68 : part.replace(pattern, (_, before: string, code: string) => {
69 const smiley = byEscapedCode.get(code);
70 return smiley ? before + smileyImg(smiley) : _;
71 })
72 )
73 .join('');
74}