"0", "bold" => "1", "faint" => "2", "underlined" => "4", "reverse" => "7", "normal" => "22", "black" => "30", "red" => "31", "green" => "32", "yellow" => "33", "blue" => "34", "magenta" => "35", "cyan" => "36", "white" => "37", "default" => "39", "black-bg" => "40", "red-bg" => "41", "green-bg" => "42", "yellow-bg" => "43", "blue-bg" => "44", "magenta-bg" => "45", "cyan-bg" => "46", "white-bg" => "47", "default-bg" => "49", ]; const COLOR_MAP = [ "z" => "reset", "o" => "black", "r" => "red", "g" => "green", "y" => "yellow", "b" => "blue", "m" => "magenta", "c" => "cyan", "w" => "white", "O" => "black_bg", "R" => "red_bg", "G" => "green_bg", "Y" => "yellow_bg", "B" => "blue_bg", "M" => "magenta_bg", "C" => "cyan_bg", "W" => "white_bg", "@" => "bold", "-" => "faint", "_" => "underlined", "~" => "reverse", "n" => "normal", ]; /** * @param resource|null $outf * @throws Exception si la destination est un fichier et que son ouverture a * échoué */ function __construct($output=null, ?array $params=null) { if ($output !== null) $params["output"] = $output; elseif (!isset($params["output"])) $params["output"] = STDOUT; if (!isset($params["filter_tags"])) $params["filter_tags"] = true; if (!isset($params["indent"])) $params["indent"] = " "; $this->resetParams($params); } function resetParams(?array $params=null): void { $output = cl::get($params, "output"); $maskErrors = null; $color = cl::get($params, "color"); $filterTags = cl::get($params, "filter_tags"); $indent = cl::get($params, "indent"); $flush = cl::get($params, "flush"); if ($output instanceof Stream) $output = $output->getResource(); if ($output !== null) { if ($output === "php://stdout") { $outf = STDOUT; } elseif ($output === "php://stderr") { $outf = STDERR; } elseif (!is_resource($output)) { # si $outf est un nom de fichier, vérifier que l'ouverture se fait sans # erreur. à partir de là, plus aucune gestion d'erreur n'est faite, à # part afficher les erreurs d'écriture la première fois qu'elles se # produisent $maskErrors = false; $outf = @fopen($output, "ab"); if ($outf === false) { $error = error_get_last(); if ($error !== null) $message = $error["message"]; else $message = "$output: open error"; throw new Exception($message); } if ($flush === null) $flush = true; } else { $outf = $output; } $this->outf = $outf; $this->maskErrors = $maskErrors; if ($color === null) $color = stream_isatty($outf); if ($flush === null) $flush = false; } if ($color !== null) $this->color = boolval($color); if ($filterTags !== null) $this->filterTags = boolval($filterTags); if ($indent !== null) $this->indent = strval($indent); if ($flush !== null) $this->flush = boolval($flush); } /** @var resource */ protected $outf; /** @var bool faut-il masquer les erreurs d'écriture? */ protected $maskErrors; /** @var bool faut-il autoriser la sortie en couleur? */ protected $color; function isColor(): bool { return $this->color; } /** @var bool faut-il enlever les tags dans la sortie? */ protected $filterTags; /** @var string indentation unitaire */ protected $indent; /** @var bool faut-il flush le fichier après l'écriture de chaque ligne */ protected $flush; function isatty(): bool { return stream_isatty($this->outf); } private static function replace_colors(array $ms): string { $colors = []; foreach (preg_split('/\s+/', $ms[1]) as $color) { while ($color && !cl::has(self::COLORS, $color)) { $alias = substr($color, 0, 1); $colors[] = self::COLOR_MAP[$alias]; $color = substr($color, 1); } if ($color) $colors[] = $color; } $text = "\x1B["; $first = true; foreach ($colors as $color) { if (!$color) continue; if ($first) $first = false; else $text .= ";"; $text .= self::COLORS[$color]; } $text .= "m"; return $text; } function filterContent(string $text): string { # couleur au début $text = preg_replace_callback('/]*)>/', [self::class, "replace_colors"], $text); # reset à la fin $text = preg_replace('/<\/color>/', "\x1B[0m", $text); # enlever les tags classiques if ($this->filterTags) { $text = preg_replace('/<[^>]*>/', "", $text); } return $text; } function filterColors(string $text): string { return preg_replace('/\x1B\[.*?m/', "", $text); } function getIndent(int $indentLevel): string { return str_repeat($this->indent, $indentLevel); } function getLines(bool $withNl, ...$values): array { $values = content::flatten($values); if (!$values) return []; $text = implode("", $values); if ($text === "") return [""]; $text = $this->filterContent($text); if (!$this->color) $text = $this->filterColors($text); $lines = explode("\n", $text); $max = count($lines) - 1; if ($withNl) { for ($i = 0; $i < $max; $i++) { $lines[$i] .= "\n"; } } if ($lines[$max] === "") unset($lines[$max]); return $lines; } private function _fwrite($outf, string $data): void { if ($this->maskErrors === null) { # masquer les erreurs d'écriture en permanence @fwrite($outf, $data); return; } # masquer uniquement la première erreur, jusqu'à ce que l'erreur disparaisse if ($this->maskErrors) $r = @fwrite($outf, $data); else $r = fwrite($outf, $data); $this->maskErrors = $r === false; } function writeLines($indent, array $lines, bool $addNl=false): void { $outf = $this->outf; foreach ($lines as $line) { if ($indent !== null) $this->_fwrite($outf, $indent); $this->_fwrite($outf, $line); if ($addNl) $this->_fwrite($outf, "\n"); } if ($this->flush) @fflush($outf); } function write(...$values): void { $this->writeLines(null, $this->getLines(true, ...$values)); } function print(...$values): void { $values[] = "\n"; $this->writeLines(null, $this->getLines(true, ...$values)); } function iwrite(int $indentLevel, ...$values): void { $indent = $this->getIndent($indentLevel); $this->writeLines($indent, $this->getLines(true, ...$values)); } function iprint(int $indentLevel, ...$values): void { $values[] = "\n"; $indent = $this->getIndent($indentLevel); $this->writeLines($indent, $this->getLines(true, ...$values)); } }