nulib/php/src_output/std/StdOutput.php

273 lines
7.7 KiB
PHP

<?php
namespace nulib\output\std;
use Exception;
use nulib\cl;
use nulib\output\IContent;
use nulib\output\IPrintable;
/**
* Class StdOutput: affichage sur STDOUT, STDERR ou dans un fichier quelconque
*
* si la destination est un fichier, aucune gestion d'erreur n'est faite, sauf
* lors de l'ouverture.
*/
class StdOutput {
const COLORS = [
"reset" => "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 !== 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('/<color([^>]*)>/', [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);
}
static function flatten($values, ?array &$dest=null): array {
if ($dest === null) $dest = [];
if ($values === null) return $dest;
if (is_string($values)) {
$dest[] = $values;
return $dest;
} elseif (!is_array($values)) {
if ($values instanceof IContent) {
$values = $values->getContent();
} elseif ($values instanceof IPrintable) {
ob_start(null, 0, PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE);
$values->print();
$dest[] = ob_get_clean();
return $dest;
} elseif (!is_iterable($values)) {
$dest[] = strval($values);
return $dest;
}
}
foreach ($values as $value) {
self::flatten($value, $dest);
}
return $dest;
}
function getIndent(int $indentLevel): string {
return str_repeat($this->indent, $indentLevel);
}
function getLines(bool $withNl, ...$values): array {
$values = self::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));
}
}