atagen

malice

public
Star0
Watch0
d1818305ffd6912e4b9ac7b706c43eccd01bdd21
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/emotes.ts

05a48e9e7942atagen2026-10-24 09:32 UTC1
import type { EmojiEntry } from './emoji';
05a48e9e7942atagen2026-10-24 09:32 UTC2
import type { ImagePack } from './types';
05a48e9e7942atagen2026-10-24 09:32 UTC3
05a48e9e7942atagen2026-10-24 09:32 UTC4
export interface Emote {
05a48e9e7942atagen2026-10-24 09:32 UTC5
  shortcode: string;
05a48e9e7942atagen2026-10-24 09:32 UTC6
  mxc: string;
05a48e9e7942atagen2026-10-24 09:32 UTC7
}
05a48e9e7942atagen2026-10-24 09:32 UTC8
05a48e9e7942atagen2026-10-24 09:32 UTC9
export type EmoteCandidate =
05a48e9e7942atagen2026-10-24 09:32 UTC10
  | { kind: 'emoji'; emoji: string; shortcode: string }
05a48e9e7942atagen2026-10-24 09:32 UTC11
  | { kind: 'custom'; shortcode: string; mxc: string };
05a48e9e7942atagen2026-10-24 09:32 UTC12
05a48e9e7942atagen2026-10-24 09:32 UTC13
export interface EmoteQuery {
05a48e9e7942atagen2026-10-24 09:32 UTC14
  start: number;
05a48e9e7942atagen2026-10-24 09:32 UTC15
  query: string;
05a48e9e7942atagen2026-10-24 09:32 UTC16
}
05a48e9e7942atagen2026-10-24 09:32 UTC17
05a48e9e7942atagen2026-10-24 09:32 UTC18
const SHORTCODE = /^[\w+-]+$/;
05a48e9e7942atagen2026-10-24 09:32 UTC19
05a48e9e7942atagen2026-10-24 09:32 UTC20
export function emoteQueryAt(text: string, caret: number): EmoteQuery | null {
05a48e9e7942atagen2026-10-24 09:32 UTC21
  const before = text.slice(0, caret);
05a48e9e7942atagen2026-10-24 09:32 UTC22
  const colon = before.lastIndexOf(':');
05a48e9e7942atagen2026-10-24 09:32 UTC23
  if (colon < 0 || (colon > 0 && !/\s/.test(before[colon - 1]))) return null;
05a48e9e7942atagen2026-10-24 09:32 UTC24
  const query = before.slice(colon + 1);
05a48e9e7942atagen2026-10-24 09:32 UTC25
  if (query.length < 2 || query.length > 40 || !SHORTCODE.test(query)) return null;
05a48e9e7942atagen2026-10-24 09:32 UTC26
  return { start: colon, query };
05a48e9e7942atagen2026-10-24 09:32 UTC27
}
05a48e9e7942atagen2026-10-24 09:32 UTC28
05a48e9e7942atagen2026-10-24 09:32 UTC29
export function customEmotes(packs: ImagePack[]): Emote[] {
05a48e9e7942atagen2026-10-24 09:32 UTC30
  const seen = new Map<string, Emote>();
05a48e9e7942atagen2026-10-24 09:32 UTC31
  for (const pack of packs) {
05a48e9e7942atagen2026-10-24 09:32 UTC32
    for (const image of pack.images) {
05a48e9e7942atagen2026-10-24 09:32 UTC33
      if (image.usage.length && !image.usage.includes('Emoticon')) continue;
05a48e9e7942atagen2026-10-24 09:32 UTC34
      if (!seen.has(image.shortcode)) seen.set(image.shortcode, { shortcode: image.shortcode, mxc: image.media_id });
05a48e9e7942atagen2026-10-24 09:32 UTC35
    }
05a48e9e7942atagen2026-10-24 09:32 UTC36
  }
05a48e9e7942atagen2026-10-24 09:32 UTC37
  return [...seen.values()];
05a48e9e7942atagen2026-10-24 09:32 UTC38
}
05a48e9e7942atagen2026-10-24 09:32 UTC39
05a48e9e7942atagen2026-10-24 09:32 UTC40
function score(code: string, q: string): number {
05a48e9e7942atagen2026-10-24 09:32 UTC41
  const c = code.toLowerCase();
05a48e9e7942atagen2026-10-24 09:32 UTC42
  if (c === q) return 0;
05a48e9e7942atagen2026-10-24 09:32 UTC43
  if (c.startsWith(q)) return 1;
05a48e9e7942atagen2026-10-24 09:32 UTC44
  if (c.split(/[_-]/).some((part) => part.startsWith(q))) return 2;
05a48e9e7942atagen2026-10-24 09:32 UTC45
  return c.includes(q) ? 3 : -1;
05a48e9e7942atagen2026-10-24 09:32 UTC46
}
05a48e9e7942atagen2026-10-24 09:32 UTC47
05a48e9e7942atagen2026-10-24 09:32 UTC48
export function rankEmotes(query: string, customs: Emote[], emoji: EmojiEntry[], limit = 8): EmoteCandidate[] {
05a48e9e7942atagen2026-10-24 09:32 UTC49
  const q = query.toLowerCase();
05a48e9e7942atagen2026-10-24 09:32 UTC50
  const scored: { candidate: EmoteCandidate; score: number; length: number }[] = [];
05a48e9e7942atagen2026-10-24 09:32 UTC51
  for (const emote of customs) {
05a48e9e7942atagen2026-10-24 09:32 UTC52
    const s = score(emote.shortcode, q);
05a48e9e7942atagen2026-10-24 09:32 UTC53
    if (s >= 0) scored.push({ candidate: { kind: 'custom', ...emote }, score: s, length: emote.shortcode.length });
05a48e9e7942atagen2026-10-24 09:32 UTC54
  }
05a48e9e7942atagen2026-10-24 09:32 UTC55
  for (const entry of emoji) {
05a48e9e7942atagen2026-10-24 09:32 UTC56
    let best: { code: string; s: number } | null = null;
05a48e9e7942atagen2026-10-24 09:32 UTC57
    for (const code of entry.shortcodes) {
05a48e9e7942atagen2026-10-24 09:32 UTC58
      const s = score(code, q);
05a48e9e7942atagen2026-10-24 09:32 UTC59
      if (s >= 0 && (!best || s < best.s || (s === best.s && code.length < best.code.length))) best = { code, s };
05a48e9e7942atagen2026-10-24 09:32 UTC60
    }
05a48e9e7942atagen2026-10-24 09:32 UTC61
    if (best) scored.push({ candidate: { kind: 'emoji', emoji: entry.emoji, shortcode: best.code }, score: best.s, length: best.code.length });
05a48e9e7942atagen2026-10-24 09:32 UTC62
  }
05a48e9e7942atagen2026-10-24 09:32 UTC63
  return scored
05a48e9e7942atagen2026-10-24 09:32 UTC64
    .sort((a, b) => a.score - b.score || a.length - b.length)
05a48e9e7942atagen2026-10-24 09:32 UTC65
    .slice(0, limit)
05a48e9e7942atagen2026-10-24 09:32 UTC66
    .map((s) => s.candidate);
05a48e9e7942atagen2026-10-24 09:32 UTC67
}
05a48e9e7942atagen2026-10-24 09:32 UTC68
05a48e9e7942atagen2026-10-24 09:32 UTC69
/** When the caret sits just after a closing colon of a known emoji shortcode, swap the shortcode for the emoji. */
05a48e9e7942atagen2026-10-24 09:32 UTC70
export function completeShortcode(text: string, caret: number, emoji: EmojiEntry[]): { text: string; caret: number } | null {
05a48e9e7942atagen2026-10-24 09:32 UTC71
  const match = /(^|\s):([\w+-]+):$/.exec(text.slice(0, caret));
05a48e9e7942atagen2026-10-24 09:32 UTC72
  if (!match) return null;
05a48e9e7942atagen2026-10-24 09:32 UTC73
  const code = match[2].toLowerCase();
05a48e9e7942atagen2026-10-24 09:32 UTC74
  const entry = emoji.find((e) => e.shortcodes.includes(code));
05a48e9e7942atagen2026-10-24 09:32 UTC75
  if (!entry) return null;
05a48e9e7942atagen2026-10-24 09:32 UTC76
  const start = caret - code.length - 2;
05a48e9e7942atagen2026-10-24 09:32 UTC77
  return { text: text.slice(0, start) + entry.emoji + text.slice(caret), caret: start + entry.emoji.length };
05a48e9e7942atagen2026-10-24 09:32 UTC78
}
05a48e9e7942atagen2026-10-24 09:32 UTC79
05a48e9e7942atagen2026-10-24 09:32 UTC80
export function resolveEmotes(body: string, customs: Emote[]): Emote[] {
05a48e9e7942atagen2026-10-24 09:32 UTC81
  const used = new Set([...body.matchAll(/:([\w+-]+):/g)].map((m) => m[1]));
05a48e9e7942atagen2026-10-24 09:32 UTC82
  return customs.filter((e) => used.has(e.shortcode));
05a48e9e7942atagen2026-10-24 09:32 UTC83
}