<?phpnamespace App\Chat;/** * The coded text of the 2002 applet (net.spray.chat.a.g / a.b): a line is segments joined by "\r", each one * "FF" (font) + "CC" (colour) + text. Fonts 00..03 = 10, 12, 14, 16 pt, 04..07 the same in bold; a negative font * ("-1") = its absolute value, without smileys (the nicknames). Colour = an index of PALETTE; "11" is the rainbow * and font "03" the wave: both are spread one segment per character when the line arrives (spread()). */final class Styled{ /** L[] of the applet: the text colours (00..10), then the hover, selection and tooltip colours. */ public const PALETTE = [ '#000000', '#0000ff', '#ff0000', '#ff00c8', '#ff9d00', '#646464', '#1a1a8c', '#7d0000', '#147300', '#6e265f', '#9b9b14', '#eeeeff', '#000080', '#00c0ff', '#ffc0c0', '#ffffc0', '#ff6060', '#74c900', '#a4a4a4', '#ffdd80', ]; /** The colours a member can choose for his sentences (index => colour). 11 = rainbow. */ public const CHOICES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; public const RAINBOW = 11; public const WAVE_FONT = 3; public const SIZES = [10, 12, 14, 16]; private const WAVE = ['00', '01', '02', '01']; private const RAINBOW_CYCLE = ['03', '09', '07', '06', '01', '08', '10', '04', '02']; /** "FFCC" + text: what a sentence is sent as (70 / 72 frames). */ public static function encode(string $text, int $color = 0, int $font = 1, bool $bold = false): string { $font = max(0, min(3, $font)) + ($bold ? 4 : 0); return sprintf('%02d%02d', $font, max(0, min(11, $color))) . self::strip($text); } /** A typed text never carries the separators of the format (tab, line ends). */ public static function strip(string $text): string { return str_replace(["\t", "\r", "\n"], ' ', $text); } /** * The segments of a coded line. * * @return list<array{font: int, size: int, bold: bool, color: int, text: string, smileys: bool}> */ public static function segments(string $coded, bool $boldLine = false): array { $out = []; foreach (explode("\r", $coded) as $part) { if (!preg_match('/^(-\d|\d\d)(\d\d)/', $part)) { if ($part !== '') { $out[] = self::segment(1, 0, $part, true, $boldLine); } continue; } $fontCode = (int)substr($part, 0, 2); $color = (int)substr($part, 2, 2); $out[] = self::segment($fontCode, $color, (string)substr($part, 4), $fontCode >= 0, $boldLine); } return $out; } /** @return array{font: int, size: int, bold: bool, color: int, text: string, smileys: bool} */ private static function segment(int $fontCode, int $color, string $text, bool $smileys, bool $boldLine): array { $font = abs($fontCode) % 8; return [ 'font' => $font % 4, 'size' => self::SIZES[$font % 4], 'bold' => $font >= 4 || $boldLine, 'color' => ($color >= 0 && $color <= 10) ? $color : 0, 'text' => $text, 'smileys' => $smileys, ]; } /** * The wave (font 03) and the rainbow (colour 11): the text after the first four characters becomes one segment * per character (a smiley whole), fonts / colours cycling from a random start, a space getting "00" (as the applet did once, when a * line arrived). Any other line is given back as it is. */ public static function spread(string $coded, ?\Closure $random = null): string { if (strlen($coded) < 4) { return $coded; } $font = substr($coded, 0, 2); $color = substr($coded, 2, 2); $wave = $font === '03'; $rainbow = $color === '11'; if (!$wave && !$rainbow) { return $coded; } $random ??= static fn (int $n): int => random_int(0, $n - 1); $f = $random(4); $c = $random(9); $nonSpace = 0; $parts = []; // a smiley stays whole (":-)", "#13", "@05"): one unit of the cycle, else its characters would be split into // segments of their own and it would show as text $units = []; foreach (Smileys::parse((string)substr($coded, 4)) as $piece) { if (is_array($piece)) { $units[] = $piece['source']; } else { array_push($units, ...mb_str_split($piece)); } } foreach ($units as $char) { if ($char === ' ') { $parts[] = ($wave ? '00' : $font) . ($rainbow ? '00' : $color) . ' '; continue; } $parts[] = ($wave ? self::WAVE[($f + intdiv($nonSpace, 2)) % 4] : $font) . ($rainbow ? self::RAINBOW_CYCLE[($c + $nonSpace) % 9] : $color) . $char; $nonSpace++; } return implode("\r", $parts); } /** The plain text of a coded line (search, tests, logs). */ public static function plain(string $coded): string { return implode('', array_map(static fn (array $s) => $s['text'], self::segments($coded))); }}