/** * Default hint alphabet — qwerty home row first, then top, then bottom. * Picked for typing speed; all 26 letters are included so single-char labels * cover articles with up to 26 links. */ export const DEFAULT_HINT_ALPHABET = 'asdfghjklqwertyuiopzxcvbnm'; /** * Generate `count` unique hint labels from `alphabet` such that no label is a * prefix of any other (so a single keypress can resolve unambiguously * whenever possible). * * Uses a trie-expansion strategy that works for any count, even beyond the * 26-or 676-label single/two-char ranges: we keep a pool of candidate labels * (starting with every single letter) and repeatedly expand the front-most * candidate into its `A` children until the pool is large enough. Expanding * the front keeps the shortest labels for the earliest links. Because every * label in the pool is always a leaf of the trie, the result is prefix-free * by construction at any length. */ export function generateHintLabels(count: number, alphabet: string = DEFAULT_HINT_ALPHABET): string[] { if (count <= 0) return []; const A = alphabet.length; if (A === 0) return []; const letters = alphabet.split(''); if (count <= A) { return letters.slice(0, count); } // Pool of prefix-free candidate labels, shortest-first. Each expansion turns // one leaf into A leaves, growing the pool by A - 1. const pool = [...letters]; let head = 0; while (pool.length - head < count) { const prefix = pool[head++] as string; for (const letter of letters) { pool.push(prefix + letter); } } // The labels consumed as prefixes (indices < head) are no longer leaves, so // drop them; the remaining pool is prefix-free. Take the first `count`, // which are the shortest available. return pool.slice(head, head + count); }