diff --git a/.idea/nulib.iml b/.idea/nulib.iml index 61abbb4..265b084 100644 --- a/.idea/nulib.iml +++ b/.idea/nulib.iml @@ -5,6 +5,7 @@ + diff --git a/bash/src/base.tools.sh b/bash/src/base.tools.sh index b786213..4812774 100644 --- a/bash/src/base.tools.sh +++ b/bash/src/base.tools.sh @@ -2,6 +2,11 @@ ##@cooked nocomments module: base.tools "Fonctions de base: outils divers" +function: mkdirof 'Créer le répertoire correspondant au fichier $1' +function mkdirof() { + mkdir -p "$(dirname -- "$1")" +} + function __la_cmd() { [ $# -gt 0 ] || set '*' local arg diff --git a/composer.json b/composer.json index ee7bce3..e820acb 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "autoload": { "psr-4": { "nulib\\": "php/src_base", - "nulib\\output\\": "php/src_output" + "nulib\\output\\": "php/src_output", + "nulib\\web\\": "php/src_web" } }, "autoload-dev": { diff --git a/php/src_output/IContent.php b/php/src_output/IContent.php new file mode 100644 index 0000000..0b92311 --- /dev/null +++ b/php/src_output/IContent.php @@ -0,0 +1,10 @@ +clone($params); + } + + /** @var IMessenger */ + protected static $msg; + + /** @var IMessenger[] */ + protected static $stack; + + /** pousser une nouvelle instance avec un nouveau paramétrage sur la pile */ + static function push(?array $params=null) { + self::$stack[] = static::get(); + self::$msg = self::new($params); + } + + /** dépiler la précédente instance */ + static function pop(): IMessenger { + if (self::$stack) $msg = self::$msg = array_pop(self::$stack); + else $msg = self::$msg; + return $msg; + } + + static final function __callStatic($name, $args) { + $name = str::us2camel($name); + call_user_func_array([static::get(), $name], $args); + } + + ############################################################################# + + const DEBUG = IMessenger::DEBUG; + const MINOR = IMessenger::MINOR; + const NORMAL = IMessenger::NORMAL; + const MAJOR = IMessenger::MAJOR; + const NONE = IMessenger::NONE; + + static function reset_params(?array $params=null): void { static::get()->resetParams($params); } + static function section($content, ?callable $func=null, ?int $level=null): void { static::get()->section($content, $func, $level); } + static function title($content, ?callable $func=null, ?int $level=null): void { static::get()->title($content, $func, $level); } + static function desc($content, ?int $level=null): void { static::get()->desc($content, $level); } + static function action($content, ?callable $func=null, ?int $level=null): void { static::get()->action($content, $func, $level); } + static function step($content, ?int $level=null): void { static::get()->step($content, $level); } + static function asuccess($content=null): void { static::get()->asuccess($content); } + static function afailure($content=null): void { static::get()->afailure($content); } + static function adone($content=null): void { static::get()->adone($content); } + static function print($content, ?int $level=null): void { static::get()->print($content, $level); } + static function info($content, ?int $level=null): void { static::get()->info($content, $level); } + static function note($content, ?int $level=null): void { static::get()->note($content, $level); } + static function warn($content, ?int $level=null): void { static::get()->warn($content, $level); } + static function error($content, ?int $level=null): void { static::get()->error($content, $level); } + static function end(bool $all=false): void { static::get()->end($all); } + + static function debug($content): void { self::info($content, self::DEBUG);} + static function normal($content): void { self::info($content, self::NORMAL);} + static function minor($content): void { self::info($content, self::MINOR);} + static function important($content): void { self::info($content, self::MAJOR);} + static function attention($content): void { self::note($content, self::MAJOR);} + static function critwarn($content): void { self::warn($content, self::MAJOR);} + static function criterror($content): void { self::error($content, self::MAJOR);} +} diff --git a/php/src_output/log.php b/php/src_output/log.php new file mode 100644 index 0000000..0542587 --- /dev/null +++ b/php/src_output/log.php @@ -0,0 +1,30 @@ +msgs = []; + foreach ($msgs as $msg) { + if ($msg !== null) $this->msgs[] = $msg; + } + } + + /** @var IMessenger[] */ + protected $msgs; + + function resetParams(?array $params=null): void { foreach ($this->msgs as $msg) { $msg->resetParams($params); } } + function clone(?array $params=null): self { + $clone = clone $this; + foreach ($clone->msgs as &$msg) { + $msg = $msg->clone($params); + }; unset($msg); + return $clone; + } + function section($content, ?callable $func=null, ?int $level=null): void { + $useFunc = false; + foreach ($this->msgs as $msg) { + $msg->section($content, null, $level); + if ($msg instanceof _IMessenger) $useFunc = true; + } + if ($useFunc && $func !== null) { + try { + $func($this); + } finally { + /** @var _IMessenger $msg */ + foreach ($this->msgs as $msg) { + $msg->_endSection(); + } + } + } + } + function title($content, ?callable $func=null, ?int $level=null): void { + $useFunc = false; + $untils = []; + foreach ($this->msgs as $msg) { + $msg->title($content, null, $level); + if ($msg instanceof _IMessenger) { + $useFunc = true; + $untils[] = $msg->_getTitleMark(); + } + } + if ($useFunc && $func !== null) { + try { + $func($this); + } finally { + /** @var _IMessenger $msg */ + $index = 0; + foreach ($this->msgs as $msg) { + $msg->_endTitle($untils[$index++]); + } + } + } + } + function desc($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->desc($content, $level); } } + function action($content, ?callable $func=null, ?int $level=null): void { + $useFunc = false; + $untils = []; + foreach ($this->msgs as $msg) { + $msg->action($content, null, $level); + if ($msg instanceof _IMessenger) { + $useFunc = true; + $untils[] = $msg->_getTitleMark(); + } + } + if ($useFunc && $func !== null) { + try { + $func($this); + } finally { + /** @var _IMessenger $msg */ + $index = 0; + foreach ($this->msgs as $msg) { + $msg->_endAction($untils[$index++]); + } + } + } + } + function step($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->step($content, $level); } } + function asuccess($content=null): void { foreach ($this->msgs as $msg) { $msg->asuccess($content); } } + function afailure($content=null): void { foreach ($this->msgs as $msg) { $msg->afailure($content); } } + function adone($content=null): void { foreach ($this->msgs as $msg) { $msg->adone($content); } } + function print($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->print($content, $level); } } + function info($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->info($content, $level); } } + function note($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->note($content, $level); } } + function warn($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->warn($content, $level); } } + function error($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->error($content, $level); } } + function end(bool $all=false): void { foreach ($this->msgs as $msg) { $msg->end($all); } } +} diff --git a/php/src_output/std/StdMessenger.php b/php/src_output/std/StdMessenger.php new file mode 100644 index 0000000..b7ba5ca --- /dev/null +++ b/php/src_output/std/StdMessenger.php @@ -0,0 +1,693 @@ + self::DEBUG, + "minor" => self::MINOR, "verbose" => self::MINOR, + "normal" => self::NORMAL, + "major" => self::MAJOR, "quiet" => self::MAJOR, + "none" => self::NONE, "silent" => self::NONE, + ]; + + protected static function verifix_level($level, int $max_level=self::MAX_LEVEL): int { + if (!in_array($level, self::VALID_LEVELS, true)) { + $level = cl::get(self::LEVEL_MAP, $level, $level); + } + if (!in_array($level, self::VALID_LEVELS, true)) { + throw new Exception("$level: invalid level"); + } + if ($level > $max_level) { + throw new Exception("$level: level not allowed here"); + } + return $level; + } + + const GENERIC_PREFIXES = [ + self::MAJOR => [ + "section" => [true, "SECTION!", "===", "=", "=", "==="], + "title" => [false, "TITLE!", null, "T", "", "==="], + "desc" => ["DESC!", ">", ""], + "error" => ["CRIT.ERROR!", "E!", ""], + "warn" => ["CRIT.WARN!", "W!", ""], + "note" => ["ATTENTION!", "N!", ""], + "info" => ["IMPORTANT!", "N!", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + self::NORMAL => [ + "section" => [true, "SECTION:", "---", "-", "-", "---"], + "title" => [false, "TITLE:", null, "T", "", "---"], + "desc" => ["DESC:", ">", ""], + "error" => ["ERROR:", "E", ""], + "warn" => ["WARN:", "W", ""], + "note" => ["NOTE:", "N", ""], + "info" => ["INFO:", "I", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + self::MINOR => [ + "section" => [true, "section", null, ">>", "<<", null], + "title" => [false, "title", null, "t", "", null], + "desc" => ["desc", ">", ""], + "error" => ["error", "E", ""], + "warn" => ["warn", "W", ""], + "note" => ["note", "N", ""], + "info" => ["info", "I", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + self::DEBUG => [ + "section" => [true, "section", null, ">>", "<<", null], + "title" => [false, "title", null, "t", "", null], + "desc" => ["desc", ">", ""], + "error" => ["debugE", "e", ""], + "warn" => ["debugW", "w", ""], + "note" => ["debugN", "i", ""], + "info" => ["debug", "D", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + ]; + + const RESULT_PREFIXES = [ + "failure" => ["(FAILURE)", ""], + "success" => ["(SUCCESS)", ""], + "done" => [null, null], + ]; + + function __construct(?array $params=null) { + $output = cl::get($params, "output"); + $color = cl::get($params, "color"); + $indent = cl::get($params, "indent", static::INDENT); + + $defaultLevel = cl::get($params, "default_level"); + if ($defaultLevel === null) $defaultLevel = self::NORMAL; + $defaultLevel = self::verifix_level($defaultLevel); + + $debug = boolval(cl::get($params, "debug")); + $minLevel = cl::get($params, "min_level"); + if ($minLevel === null && $debug) $minLevel = self::DEBUG; + if ($minLevel === null) $minLevel = cl::get($params, "verbosity"); # alias + if ($minLevel === null) $minLevel = self::NORMAL; + $minLevel = self::verifix_level($minLevel, self::NONE); + + $addDate = boolval(cl::get($params, "add_date")); + $dateFormat = cl::get($params, "date_format", static::DATE_FORMAT); + $id = cl::get($params, "id"); + + $params = [ + "color" => $color, + "indent" => $indent, + ]; + if ($output !== null) { + $this->err = $this->out = new StdOutput($output, $params); + } else { + $this->out = new StdOutput(STDOUT, $params); + $this->err = new StdOutput(STDERR, $params); + } + $this->defaultLevel = $defaultLevel; + $this->minLevel = $minLevel; + $this->addDate = $addDate; + $this->dateFormat = $dateFormat; + $this->id = $id; + $this->inSection = false; + $this->titles = []; + $this->actions = []; + } + + function resetParams(?array $params=null): void { + $output = cl::get($params, "output"); + $color = cl::get($params, "color"); + $indent = cl::get($params, "indent"); + + $defaultLevel = cl::get($params, "default_level"); + if ($defaultLevel !== null) $defaultLevel = self::verifix_level($defaultLevel); + + $debug = cl::get($params, "debug"); + $minLevel = cl::get($params, "min_level"); + if ($minLevel === null && $debug !== null) $minLevel = $debug? self::DEBUG: self::NORMAL; + if ($minLevel === null) $minLevel = cl::get($params, "verbosity"); # alias + if ($minLevel !== null) $minLevel = self::verifix_level($minLevel, self::NONE); + + $addDate = cl::get($params, "add_date"); + $dateFormat = cl::get($params, "date_format"); + $id = cl::get($params, "id"); + + $params = [ + "output" => $output, + "color" => $color, + "indent" => $indent, + ]; + if ($output !== null) { + $this->out->resetParams($params); + } else { + $this->out->resetParams($params); + $this->err->resetParams($params); + } + if ($defaultLevel !== null) $this->defaultLevel = $defaultLevel; + if ($minLevel !== null) $this->minLevel = $minLevel; + if ($addDate !== null) $this->addDate = boolval($addDate); + if ($dateFormat !== null) $this->dateFormat = $dateFormat; + if ($id !== null) $this->id = $id; + } + + function clone(?array $params=null): IMessenger { + $clone = clone $this; + if ($params !== null) $clone->resetParams($params); + #XXX faut-il marquer la section et les titres du clone à "print" => false? + # ou en faire des références au parent? + # dans tous les cas, on considère qu'il n'y a pas d'actions en cours, et on + # ne doit pas dépiler avec end() plus que l'état que l'on a eu lors du clone + return $clone; + } + + /** @var StdOutput la sortie standard */ + protected $out; + + /** @var StdOutput la sortie d'erreur */ + protected $err; + + /** @var int level par défaut dans lequel les messages sont affichés */ + protected $defaultLevel; + + /** @var int level minimum que doivent avoir les messages pour être affichés */ + protected $minLevel; + + /** @var bool faut-il ajouter la date à chaque ligne? */ + protected $addDate; + + /** @var string format de la date */ + protected $dateFormat; + + /** @var ?string identifiant de ce messenger, à ajouter à chaque ligne */ + protected $id; + + protected function getLinePrefix(): ?string { + $linePrefix = null; + if ($this->addDate) { + $date = date_create()->format($this->dateFormat); + $linePrefix .= "$date "; + } + if ($this->id !== null) { + $linePrefix .= "$this->id "; + } + return $linePrefix; + } + + protected function decrLevel(int $level, int $amount=-1): int { + $level += $amount; + if ($level < self::MIN_LEVEL) $level = self::MIN_LEVEL; + return $level; + } + + protected function checkLevel(?int &$level): bool { + if ($level === null) $level = $this->defaultLevel; + elseif ($level < 0) $level = $this->decrLevel($this->defaultLevel, $level); + return $level >= $this->minLevel; + } + + protected function getIndentLevel(bool $withActions=true): int { + $indentLevel = count($this->titles) - 1; + if ($indentLevel < 0) $indentLevel = 0; + if ($withActions) { + foreach ($this->actions as $action) { + if ($action["level"] < $this->minLevel) continue; + $indentLevel++; + } + } + return $indentLevel; + } + + protected function _printTitle(?string $linePrefix, int $level, + string $type, $content, + int $indentLevel, StdOutput $out): void { + $prefixes = self::GENERIC_PREFIXES[$level][$type]; + if ($prefixes[0]) $out->print(); + if ($out->isColor()) { + $before = $prefixes[2]; + $prefix = $prefixes[3]; + $prefix2 = $prefix !== null? "$prefix ": null; + $suffix = $prefixes[4]; + $suffix2 = $suffix !== null? " $suffix": null; + $after = $prefixes[5]; + + $lines = $out->getLines(false, $content); + $maxlen = 0; + foreach ($lines as &$content) { + $line = $out->filterColors($content); + $len = mb_strlen($line); + if ($len > $maxlen) $maxlen = $len; + $content = [$content, $len]; + }; unset($content); + if ($before !== null) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, substr($before, 1), str_repeat($before[0], $maxlen), $suffix); + } + foreach ($lines as [$content, $len]) { + if ($linePrefix !== null) $out->write($linePrefix); + $padding = $len < $maxlen? str_repeat(" ", $maxlen - $len): null; + $out->iprint($indentLevel, $prefix2, $content, $padding, $suffix2); + } + if ($after !== null) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, substr($after, 1), str_repeat($after[0], $maxlen), $suffix); + } + } else { + $prefix = $prefixes[1]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = str_repeat(" ", mb_strlen($prefix)); + $lines = $out->getLines(false, $content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, $content); + $prefix = $prefix2; + } + } + } + + protected function _printAction(?string $linePrefix, int $level, + bool $printContent, $content, + bool $printResult, ?bool $rsuccess, $rcontent, + int $indentLevel, StdOutput $out): void { + $color = $out->isColor(); + if ($rsuccess === true) $type = "success"; + elseif ($rsuccess === false) $type = "failure"; + else $type = "done"; + $rprefixes = self::RESULT_PREFIXES[$type]; + if ($color) { + $rprefix = $rprefixes[1]; + $rprefix2 = null; + if ($rprefix !== null) { + $rprefix .= " "; + $rprefix2 = $out->filterColors($out->filterContent($rprefix)); + $rprefix2 = str_repeat(" ", mb_strlen($rprefix2)); + } + } else { + $rprefix = $rprefixes[0]; + if ($rprefix !== null) $rprefix .= " "; + $rprefix2 = str_repeat(" ", mb_strlen($rprefix)); + } + if ($printContent && $printResult) { + if ($rcontent) { + cl::ensure_array($content); + $content[] = ": "; + $content[] = $rcontent; + } + $lines = $out->getLines(false, $content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $rprefix, $content); + $rprefix = $rprefix2; + } + } elseif ($printContent) { + $prefixes = self::GENERIC_PREFIXES[$level]["step"]; + if ($color) { + $prefix = $prefixes[1]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = $out->filterColors($out->filterContent($prefix)); + $prefix2 = str_repeat(" ", mb_strlen($prefix2)); + $suffix = $prefixes[2]; + } else { + $prefix = $prefixes[0]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = str_repeat(" ", mb_strlen($prefix)); + $suffix = null; + } + $lines = $out->getLines(false, $content, ":"); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, $content, $suffix); + $prefix = $prefix2; + } + } elseif ($printResult) { + if (!$rcontent) { + if ($type === "success") $rcontent = $color? "succès": ""; + elseif ($type === "failure") $rcontent = $color? "échec": ""; + elseif ($type === "done") $rcontent = "fait"; + } + $rprefix = " $rprefix"; + $rprefix2 = " $rprefix2"; + $lines = $out->getLines(false, $rcontent); + foreach ($lines as $rcontent) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $rprefix, $rcontent); + $rprefix = $rprefix2; + } + } + } + + protected function _printGeneric(?string $linePrefix, int $level, + string $type, $content, + int $indentLevel, StdOutput $out): void { + $prefixes = self::GENERIC_PREFIXES[$level][$type]; + if ($out->isColor()) { + $prefix = $prefixes[1]; + $prefix2 = null; + if ($prefix !== null) { + $prefix .= " "; + $prefix2 = $out->filterColors($out->filterContent($prefix)); + $prefix2 = str_repeat(" ", mb_strlen($prefix2)); + } + $suffix = $prefixes[2]; + $lines = $out->getLines(false, $content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, $content, $suffix); + $prefix = $prefix2; + } + } else { + $prefix = $prefixes[0]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = str_repeat(" ", mb_strlen($prefix)); + $lines = $out->getLines(false, $content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, $content); + $prefix = $prefix2; + } + } + } + + protected function _printGenericOrException(?int $level, string $type, $content, int $indentLevel, StdOutput $out): void { + $linePrefix = $this->getLinePrefix(); + # si $content contient des exceptions, les afficher avec un level moindre + $exceptions = null; + if (is_array($content)) { + $valueContent = null; + foreach ($content as $value) { + if ($value instanceof Throwable || $value instanceof ExceptionShadow) { + $exceptions[] = $value; + } else { + $valueContent[] = $value; + } + } + $content = $valueContent; + } elseif ($content instanceof Throwable || $content instanceof ExceptionShadow) { + $exceptions[] = $content; + $content = null; + } + + $printActions = true; + $showContent = $this->checkLevel($level); + if ($content !== null && $showContent) { + $this->printActions(); $printActions = false; + $this->_printGeneric($linePrefix, $level, $type, $content, $indentLevel, $out); + } + if ($exceptions !== null) { + $level1 = $this->decrLevel($level); + $showTraceback = $this->checkLevel($level1); + foreach ($exceptions as $exception) { + # tout d'abord userMessage + $userMessage = UserException::get_user_message($exception); + if ($userMessage !== null && $showContent) { + if ($printActions) { $this->printActions(); $printActions = false; } + $this->_printGeneric($linePrefix, $level, $type, $userMessage, $indentLevel, $out); + } + # puis summary et traceback + if ($showTraceback) { + if ($printActions) { $this->printActions(); $printActions = false; } + $summary = UserException::get_summary($exception); + $traceback = UserException::get_traceback($exception); + $this->_printGeneric($linePrefix, $level1, $type, $summary, $indentLevel, $out); + $this->_printGeneric($linePrefix, $level1, $type, $traceback, $indentLevel, $out); + } + } + } + } + + /** @var bool est-on dans une section? */ + protected $inSection; + + /** @var array section qui est en attente d'affichage */ + protected $section; + + function section($content, ?callable $func=null, ?int $level=null): void { + $this->_endSection(); + $this->inSection = true; + if (!$this->checkLevel($level)) return; + $this->section = [ + "line_prefix" => $this->getLinePrefix(), + "level" => $level, + "content" => $content, + "print_content" => true, + ]; + if ($func !== null) { + try { + $func($this); + } finally { + $this->_endSection(); + } + } + } + + protected function printSection() { + $section =& $this->section; + if ($section !== null && $section["print_content"]) { + $this->_printTitle( + $section["line_prefix"], $section["level"], + "section", $section["content"], + 0, $this->err); + $section["print_content"] = false; + } + } + + function _endSection(): void { + $this->inSection = false; + $this->section = null; + } + + /** @var array */ + protected $titles; + + /** @var array */ + protected $title; + + function _getTitleMark(): int { + return count($this->titles); + } + + function title($content, ?callable $func=null, ?int $level=null): void { + if (!$this->checkLevel($level)) return; + $until = $this->_getTitleMark(); + $this->titles[] = [ + "line_prefix" => $this->getLinePrefix(), + "level" => $level, + "content" => $content, + "print_content" => true, + "descs" => [], + "print_descs" => false, + ]; + $this->title =& $this->titles[$until]; + if ($func !== null) { + try { + $func($this); + } finally { + $this->_endTitle($until); + } + } + } + + function desc($content, ?int $level=null): void { + if (!$this->checkLevel($level)) return; + $title =& $this->title; + $title["descs"][] = [ + "line_prefix" => $this->getLinePrefix(), + "level" => $level, + "content" => $content, + ]; + $title["print_descs"] = true; + } + + protected function printTitles(): void { + $this->printSection(); + $err = $this->err; + $indentLevel = 0; + foreach ($this->titles as &$title) { + if ($title["print_content"]) { + $this->_printTitle( + $title["line_prefix"], $title["level"], + "title", $title["content"], + $indentLevel, $err); + $title["print_content"] = false; + } + if ($title["print_descs"]) { + foreach ($title["descs"] as $desc) { + $this->_printGeneric( + $desc["line_prefix"], $desc["level"], + "desc", $desc["content"], + $indentLevel, $err); + } + $title["descs"] = []; + $title["print_descs"] = false; + } + $indentLevel++; + }; unset($title); + } + + function _endTitle(?int $until=null): void { + if ($until === null) $until = $this->_getTitleMark() - 1; + while (count($this->titles) > $until) { + array_pop($this->titles); + } + if ($this->titles) { + $this->title =& $this->titles[count($this->titles) - 1]; + } else { + $this->titles = []; + unset($this->title); + } + } + + /** @var array */ + protected $actions; + + /** @var array */ + protected $action; + + function _getActionMark(): int { + return count($this->actions); + } + + function action($content, ?callable $func=null, ?int $level=null): void { + $this->checkLevel($level); + $until = $this->_getActionMark(); + $this->actions[] = [ + "line_prefix" => $this->getLinePrefix(), + "level" => $level, + "content" => $content, + "print_content" => true, + "result_success" => null, + "result_content" => null, + ]; + $this->action =& $this->actions[$until]; + if ($func !== null) { + try { + $result = $func($this); + if ($result !== null) { + if ($result === true) $this->asuccess(); + elseif ($result === false) $this->afailure(); + else $this->adone($result); + } + } finally { + $this->_endAction($until); + } + } + } + + function printActions(bool $endAction=false): void { + $this->printTitles(); + $err = $this->err; + $indentLevel = $this->getIndentLevel(false); + $lastIndex = count($this->actions) - 1; + $index = 0; + foreach ($this->actions as &$action) { + $mergeResult = $index++ == $lastIndex && $endAction; + $linePrefix = $action["line_prefix"]; + $level = $action["level"]; + $content = $action["content"]; + $printContent = $action["print_content"]; + $rsuccess = $action["result_success"]; + $rcontent = $action["result_content"]; + if ($level < $this->minLevel) continue; + if ($mergeResult) { + $this->_printAction( + $linePrefix, $level, + $printContent, $content, + true, $rsuccess, $rcontent, + $indentLevel, $err); + } elseif ($printContent) { + $this->_printAction( + $linePrefix, $level, + $printContent, $content, + false, $rsuccess, $rcontent, + $indentLevel, $err); + $action["print_content"] = false; + } + $indentLevel++; + }; unset($action); + if ($endAction) $this->_endAction(); + } + + function step($content, ?int $level=null): void { + $this->_printGenericOrException($level, "step", $content, $this->getIndentLevel(), $this->err); + } + + function asuccess($content=null): void { + if (!$this->actions) $this->action(null); + $this->action["result_success"] = true; + $this->action["result_content"] = $content; + $this->printActions(true); + } + + function afailure($content=null): void { + if (!$this->actions) $this->action(null); + $this->action["result_success"] = false; + $this->action["result_content"] = $content; + $this->printActions(true); + } + + function adone($content=null): void { + if (!$this->actions) $this->action(null); + $this->action["result_success"] = null; + $this->action["result_content"] = $content; + $this->printActions(true); + } + + function _endAction(?int $until=null): void { + if ($until === null) $until = $this->_getActionMark() - 1; + while (count($this->actions) > $until) { + array_pop($this->actions); + } + if ($this->actions) { + $this->action =& $this->actions[count($this->actions) - 1]; + } else { + $this->actions = []; + unset($this->action); + } + } + + function print($content, ?int $level=null): void { + $this->_printGenericOrException($level, "print", $content, $this->getIndentLevel(), $this->out); + } + + function info($content, ?int $level=null): void { + $this->_printGenericOrException($level, "info", $content, $this->getIndentLevel(), $this->err); + } + + function note($content, ?int $level=null): void { + $this->_printGenericOrException($level, "note", $content, $this->getIndentLevel(), $this->err); + } + + function warn($content, ?int $level=null): void { + $this->_printGenericOrException($level, "warn", $content, $this->getIndentLevel(), $this->err); + } + + function error($content, ?int $level=null): void { + $this->_printGenericOrException($level, "error", $content, $this->getIndentLevel(), $this->err); + } + + function end(bool $all=false): void { + if ($all) { + while ($this->actions) $this->adone(); + while ($this->titles) $this->_endTitle(); + $this->_endSection(); + } elseif ($this->actions) { + $this->_endAction(); + } elseif ($this->titles) { + $this->_endTitle(); + } else { + $this->_endSection(); + } + } +} diff --git a/php/src_output/std/StdOutput.php b/php/src_output/std/StdOutput.php new file mode 100644 index 0000000..a95468b --- /dev/null +++ b/php/src_output/std/StdOutput.php @@ -0,0 +1,272 @@ + "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('/]*)>/', [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)); + } +} diff --git a/php/src_output/std/_IMessenger.php b/php/src_output/std/_IMessenger.php new file mode 100644 index 0000000..9b54b59 --- /dev/null +++ b/php/src_output/std/_IMessenger.php @@ -0,0 +1,19 @@ +