[] {} so shapes still read. */ function ansi_color(array $p, ?string $cur): ?string { static $basic = ['000000', 'aa0000', '00aa00', 'aa5500', '0000aa', 'aa00aa', '00aaaa', 'aaaaaa', '555555', 'ff5555', '55ff55', 'ffff55', '5555ff', 'ff55ff', '55ffff', 'ffffff']; for ($i = 0; $i < count($p); $i++) { $n = (int) $p[$i]; if ($n === 0 || $n === 39) { $cur = null; } elseif ($n >= 30 && $n <= 37) { $cur = $basic[$n - 30]; } elseif ($n >= 90 && $n <= 97) { $cur = $basic[$n - 82]; } elseif ($n === 38 && ($p[$i + 1] ?? '') === '2') { $cur = sprintf('%02x%02x%02x', $p[$i + 2] ?? 0, $p[$i + 3] ?? 0, $p[$i + 4] ?? 0); $i += 4; } elseif ($n === 38 && ($p[$i + 1] ?? '') === '5') { $c = (int) ($p[$i + 2] ?? 0); $i += 2; if ($c < 16) { $cur = $basic[$c]; } elseif ($c < 232) { $c -= 16; $lvl = static fn(int $v): int => $v ? 55 + 40 * $v : 0; $cur = sprintf('%02x%02x%02x', $lvl(intdiv($c, 36)), $lvl(intdiv($c, 6) % 6), $lvl($c % 6)); } else { $cur = str_repeat(sprintf('%02x', 8 + 10 * ($c - 232)), 3); } } } return $cur; } /** ansi_html(), cached on disk until the .ans file changes (parsing costs ~30 ms a side). */ function ansi_html_cached(string $file, bool $mirror = false): string { $dir = (defined('DATA_DIR') ? DATA_DIR : sys_get_temp_dir()) . '/art-cache'; $base = $dir . '/' . md5($file . '|' . (int) $mirror); $key = $base . '-' . filemtime($file) . '-layers.html'; // bump the suffix when the markup changes if (is_file($key)) { return (string) file_get_contents($key); } $html = ansi_html($file, $mirror); @mkdir($dir, 0755, true); array_map('unlink', glob($base . '-*.html') ?: []); // every deploy bumps the mtime $tmp = $key . '.' . getmypid(); if (@file_put_contents($tmp, $html) !== false) { rename($tmp, $key); } return $html; } function ansi_html(string $file, bool $mirror = false): string { static $flip = ['/' => '\\', '\\' => '/', '(' => ')', ')' => '(', '<' => '>', '>' => '<', '[' => ']', ']' => '[', '{' => '}', '}' => '{']; $lines = []; foreach (explode("\n", rtrim((string) file_get_contents($file), "\n")) as $line) { // split into [char, color] cells $cells = []; $cur = null; foreach (preg_split('/(\e\[[0-9;]*m)/', $line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY) as $part) { if ($part[0] === "\e") { $cur = ansi_color(explode(';', substr($part, 2, -1)), $cur); continue; } foreach (mb_str_split($part) as $ch) { $cells[] = [$ch, $cur]; } } $lines[] = $cells; } $width = max(array_map('count', $lines) ?: [0]); foreach ($lines as &$cells) { $cells = array_pad($cells, $width, [' ', null]); if ($mirror) { $cells = array_map(static fn($c) => [$flip[$c[0]] ?? $c[0], $c[1]], array_reverse($cells)); } } unset($cells); // One full-size text layer per color, holding only that color's // characters; CSS stacks them. A few dozen elements instead of a span per // color run (thousands), which is what made the art slow to lay out. $colors = []; foreach ($lines as $cells) { foreach ($cells as [$ch, $color]) { if ($ch !== ' ') { $colors[$color ?? ''] = true; } } } $html = ''; foreach (array_keys($colors) as $color) { $rows = []; foreach ($lines as $cells) { $row = ''; foreach ($cells as [$ch, $c]) { $row .= ($ch !== ' ' && ($c ?? '') === $color) ? $ch : ' '; } $rows[] = $row; } $style = $color !== '' ? " style=\"color:#$color\"" : ''; $html .= "" . htmlspecialchars(implode("\n", $rows)) . ''; } return $html; }