diff --git a/src/output/Console.php b/src/output/Console.php index 2f1cd36..7667cdf 100644 --- a/src/output/Console.php +++ b/src/output/Console.php @@ -21,33 +21,47 @@ class Console implements IMessenger { "m" => self::LEVEL_MAJOR, ]; - const TYPE_PREFIXES = [ + protected static function verifix_level($level, bool $debug): int { + if ($level === null) $level = $debug? self::LEVEL_DEBUG: self::LEVEL_NORMAL; + 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"); + } + return $level; + } + + const GENERIC_PREFIXES = [ self::LEVEL_MAJOR => [ - "section" => [true, "SECTION!", "===", "= ", " =", "==="], - "title" => ["TITLE!", null, "T ", "", "---"], - "desc" => ["DESC!", "> ", ""], - "error" => ["CRITICAL!", "E! ", ""], - "warn" => ["ATTENTION!", "W! ", ""], - "note" => ["IMPORTANT!", "N! ", ""], - "info" => ["IMPORTANT!", "I! ", ""], + "section" => [true, "SECTION!", "===", "=", "=", "==="], + "title" => [false, "TITLE!", null, "T", "", "---"], + "desc" => ["DESC!", ">", ""], + "error" => ["CRITICAL!", "E!", ""], + "warn" => ["ATTENTION!", "W!", ""], + "note" => ["IMPORTANT!", "N!", ""], + "info" => ["IMPORTANT!", "I!", ""], + "print" => [null, null, null], ], self::LEVEL_NORMAL => [ - "section" => [true, "SECTION:", null, ">> ", " <<", "---"], - "title" => ["TITLE:", null, "T ", "", null], - "desc" => ["DESC:", "> ", ""], - "error" => ["ERROR:", "E ", ""], - "warn" => ["WARN:", "W ", ""], - "note" => ["NOTE:", "N ", ""], - "info" => ["INFO:", "I ", ""], + "section" => [true, "SECTION:", "---", "-", "-", "---"], + "title" => [false, "TITLE:", null, "T", "", null], + "desc" => ["DESC:", ">", ""], + "error" => ["ERROR:", "E", ""], + "warn" => ["WARN:", "W", ""], + "note" => ["NOTE:", "N", ""], + "info" => ["INFO:", "I", ""], + "print" => [null, null, null], ], self::LEVEL_DEBUG => [ - "section" => [false, "s", null, ">> ", " <<", null], - "title" => ["t", "t ", ""], - "desc" => [">", "> ", ""], - "error" => ["e", "e ", ""], - "warn" => ["w", "w ", ""], - "note" => ["i", "i ", ""], - "info" => ["D", "D ", ""], + "section" => [false, "section", null, ">>", "<<", null], + "title" => [false, "title", null, "t", "", null], + "desc" => [">", ">", ""], + "error" => ["e", "e", ""], + "warn" => ["w", "w", ""], + "note" => ["i", "i", ""], + "info" => ["D", "D", ""], + "print" => [null, null, null], ], ]; @@ -59,25 +73,24 @@ class Console implements IMessenger { ]; function __construct(?array $params=null) { + $color = cl::get($params, "color"); $debug = boolval(cl::get($params, "debug")); - $minLevel = cl::get($params, "min_level"); - if ($minLevel === null) $minLevel = $debug? self::LEVEL_DEBUG: self::LEVEL_NORMAL; - if (!in_array($minLevel, self::VALID_LEVELS)) { - $minLevel = cl::get(self::LEVEL_MAP, $minLevel, $minLevel); - } - if (!in_array($minLevel, self::VALID_LEVELS)) { - throw new Exception("$minLevel: invalid level"); - } + $minLevel = self::verifix_level(cl::get($params, "min_level"), $debug); + $defaultLevel = self::verifix_level(cl::get($params, "default_level"), false); - $this->out = new StdOutput(STDOUT); - $this->err = new StdOutput(STDERR); + $params = [ + "color" => $color, + "indent" => static::INDENT, + ]; + $this->out = new StdOutput(STDOUT, $params); + $this->err = new StdOutput(STDERR, $params); $this->minLevel = intval($minLevel); - $this->indent = static::INDENT; + $this->defaultLevel = intval($defaultLevel); $this->inSection = false; - $this->titles = null; - $this->currentTitle = null; - $this->actions = null; - $this->currentAction = null; + $this->titles = []; + $this->title = null; + $this->actions = []; + $this->action = null; } /** @var StdOutput la sortie standard */ @@ -89,25 +102,93 @@ class Console implements IMessenger { /** @var int level minimum que doivent avoir les messages pour être affichés */ protected $minLevel; - /** @var string valeur unitaire de l'indentation */ - protected $indent; + /** @var int level par défaut dans lequel les messages sont affichés */ + protected $defaultLevel; + + protected function checkLevel(?int &$level): bool { + if ($level === null) $level = $this->defaultLevel; + return $level >= $this->minLevel; + } + + protected function _printTitle(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 = strlen($line); + if ($len > $maxlen) $maxlen = $len; + $content = [$content, $len]; + }; unset($content); + if ($before !== null) { + $out->iprint($indentLevel, $prefix, substr($before, 1), str_repeat($before[0], $maxlen), $suffix); + } + foreach ($lines as [$content, $len]) { + $padding = $len < $maxlen? str_repeat(" ", $maxlen - $len): null; + $out->iprint($indentLevel, $prefix2, $content, $padding, $suffix2); + } + if ($after !== null) { + $out->iprint($indentLevel, $prefix, substr($after, 1), str_repeat($after[0], $maxlen), $suffix); + } + } else { + $prefix = $prefixes[1]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = str_repeat(" ", strlen($prefix)); + $lines = $out->getLines(false, $content); + foreach ($lines as $content) { + $out->iprint($indentLevel, $prefix, $content); + $prefix = $prefix2; + } + } + } + + protected function _printGeneric(int $level, string $type, $content, int $indentLevel, StdOutput $out): void { + $prefixes = self::GENERIC_PREFIXES[$level][$type]; + if ($out->isColor()) { + $prefix = $prefixes[1]; + $suffix = $prefixes[2]; + } else { + $prefix = $prefixes[0]; + $suffix = null; + } + $line = [$prefix]; + if ($prefix !== null) $line[] = " "; + $line[] = $content; + $line[] = $suffix; + $out->iprint($indentLevel, ...$line); + } /** @var bool est-on dans une section? */ protected $inSection; - /** @var array|string section qui est en attente d'affichage */ + /** @var array section qui est en attente d'affichage */ protected $section; - function section($content, int $level=self::LEVEL_NORMAL): void { + function section($content, ?int $level=null): void { $this->endSection(); $this->inSection = true; - if ($level < $this->minLevel) return; - $this->section = $content; + if (!$this->checkLevel($level)) return; + $this->section = [ + "level" => $level, + "content" => $content, + "print_content" => true, + ]; } protected function printSection() { - if ($this->section !== null) { - $this->section = null; + $section =& $this->section; + if ($section["print_content"]) { + $this->_printTitle($section["level"], "section", $section["content"], 0, $this->err); + $section["print_content"] = false; } } @@ -120,34 +201,57 @@ class Console implements IMessenger { protected $titles; /** @var array */ - protected $currentTitle; + protected $title; - function title($content, int $level=self::LEVEL_NORMAL): void { - if ($level < $this->minLevel) return; + function title($content, ?int $level=null): void { + if (!$this->checkLevel($level)) return; $this->titles[] = [ - "title" => $content, + "level" => $level, + "content" => $content, + "print_content" => true, "descs" => [], - "print" => true, + "print_descs" => false, ]; - $this->currentTitle =& $this->titles[count($this->titles) - 1]; + $this->title =& $this->titles[count($this->titles) - 1]; } - function desc($content, int $level=self::LEVEL_NORMAL): void { - if ($level < $this->minLevel) return; - $this->currentTitle["descs"][] = $content; + function desc($content, ?int $level=null): void { + if (!$this->checkLevel($level)) return; + $title =& $this->title; + $title["descs"][] = [ + "level" => $level, + "content" => $content, + ]; + $title["print_descs"] = true; } protected function printTitles(): void { $this->printSection(); + $out = $this->err; + $indentLevel = 0; + foreach ($this->titles as &$title) { + if ($title["print_content"]) { + $this->_printTitle($title["level"], "title", $title["content"], $indentLevel, $out); + $title["print_content"] = false; + } + if ($title["print_descs"]) { + foreach ($title["descs"] as $desc) { + $this->_printGeneric($desc["level"], "desc", $desc["content"], $indentLevel, $out); + } + $title["descs"] = []; + $title["print_descs"] = false; + } + $indentLevel++; + }; unset($title); } protected function endTitle(): void { array_pop($this->titles); if ($this->titles) { - $this->currentTitle =& $this->titles[count($this->titles) - 1]; + $this->title =& $this->titles[count($this->titles) - 1]; } else { - $this->titles = null; - unset($this->currentTitle); + $this->titles = []; + unset($this->title); } } @@ -155,9 +259,19 @@ class Console implements IMessenger { protected $actions; /** @var array */ - protected $currentAction; + protected $action; - function action($content, int $level=self::LEVEL_NORMAL): void { + protected function getIndentLevel(): int { + $indentLevel = count($this->titles) - 1; + if ($indentLevel < 0) $indentLevel = 0; + foreach ($this->actions as $action) { + if ($action["level"] < $this->minLevel) continue; + $indentLevel++; + } + return $indentLevel; + } + + function action($content, ?int $level=null): void { $this->actions[] = [ "level" => $level, "content" => $content, @@ -166,77 +280,81 @@ class Console implements IMessenger { "result" => null, "print_result" => true, ]; - $this->currentAction =& $this->actions[count($this->actions) - 1]; + $this->action =& $this->actions[count($this->actions) - 1]; } - function printActions(): void { + function printActions(bool $willEnd=false): void { $this->printTitles(); } - function step($content): void { + function step($content, ?int $level=null): void { if (!$this->actions) $this->action(null); $this->printActions(); } function success($content=null): void { if (!$this->actions) $this->action(null); - $this->currentAction["success"] = true; - $this->currentAction["result"] = $content; - $this->printActions(); + $this->action["success"] = true; + $this->action["result"] = $content; + $this->printActions(true); $this->endAction(); } function failure($content=null): void { if (!$this->actions) $this->action(null); - $this->currentAction["success"] = false; - $this->currentAction["result"] = $content; - $this->printActions(); + $this->action["success"] = false; + $this->action["result"] = $content; + $this->printActions(true); $this->endAction(); } function neutral($content=null): void { if (!$this->actions) $this->action(null); - $this->currentAction["success"] = null; - $this->currentAction["result"] = $content; - $this->printActions(); + $this->action["success"] = null; + $this->action["result"] = $content; + $this->printActions(true); $this->endAction(); } protected function endAction(): void { array_pop($this->actions); if ($this->actions) { - $this->currentAction =& $this->actions[count($this->actions) - 1]; + $this->action =& $this->actions[count($this->actions) - 1]; } else { - $this->actions = null; - unset($this->currentAction); + $this->actions = []; + unset($this->action); } } - function print($content, int $level=self::LEVEL_NORMAL): void { - if ($level < $this->minLevel) return; + function print($content, ?int $level=null): void { + if (!$this->checkLevel($level)) return; $this->printActions(); - $this->out->print($content); + $this->_printGeneric($level, "print", $content, $this->getIndentLevel(), $this->out); } - function info($content, int $level=self::LEVEL_NORMAL): void { - if ($level < $this->minLevel) return; + function info($content, ?int $level=null): void { + if (!$this->checkLevel($level)) return; $this->printActions(); + $this->_printGeneric($level, "info", $content, $this->getIndentLevel(), $this->err); } - function note($content, int $level=self::LEVEL_NORMAL): void { - if ($level < $this->minLevel) return; + function note($content, ?int $level=null): void { + if (!$this->checkLevel($level)) return; $this->printActions(); + $this->_printGeneric($level, "note", $content, $this->getIndentLevel(), $this->err); } - function warn($content, int $level=self::LEVEL_NORMAL): void { - if ($level < $this->minLevel) return; + function warn($content, ?int $level=null): void { + if (!$this->checkLevel($level)) return; $this->printActions(); + $this->_printGeneric($level, "warn", $content, $this->getIndentLevel(), $this->err); } - function error($content, int $level=self::LEVEL_NORMAL): void { - if ($level < $this->minLevel) return; + function error($content, ?int $level=null): void { + if (!$this->checkLevel($level)) return; $this->printActions(); + $this->_printGeneric($level, "error", $content, $this->getIndentLevel(), $this->err); } function end(bool $all=false) { diff --git a/src/output/IContent.php b/src/output/IContent.php new file mode 100644 index 0000000..fbddc2a --- /dev/null +++ b/src/output/IContent.php @@ -0,0 +1,10 @@ +outf = $outf; $this->color = boolval($color); $this->filterTags = boolval($filterTags); + $this->indent = $indent; $this->flush = boolval($flush); } @@ -116,6 +118,9 @@ class StdOutput { /** @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; @@ -155,24 +160,82 @@ class StdOutput { } return $text; } - protected function filterColors(string $text): string { + function filterColors(string $text): string { return preg_replace('/\x1B\[.*?m/', "", $text); } - protected function fwrite(array $values): void { + 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); $text = implode("", $values); $text = $this->filterContent($text); if (!$this->color) $text = $this->filterColors($text); - fwrite($this->outf, $text); - if ($this->flush) fflush($this->outf); + $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; + } + + function writeLines($indent, array $lines, bool $addNl=false): void { + $outf = $this->outf; + foreach ($lines as $line) { + if ($indent !== null) fwrite($outf, $indent); + fwrite($outf, $line); + if ($addNl) fwrite($outf, "\n"); + } + if ($this->flush) fflush($outf); } function write(...$values): void { - $this->fwrite($values); + $this->writeLines(null, $this->getLines(true, ...$values)); } function print(...$values): void { $values[] = "\n"; - $this->fwrite($values); + $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/tbin/test-console.php b/tbin/test-console.php new file mode 100755 index 0000000..ca68081 --- /dev/null +++ b/tbin/test-console.php @@ -0,0 +1,84 @@ +#!/usr/bin/php +section("section"); + +$c->title("title"); +$c->desc("desc"); +$c->print("print"); + +$c->action("action"); +$c->step("step"); +$c->success("action success"); + +$c->action("action"); +$c->step("step"); +$c->failure("action failure"); + +$c->action("action"); +$c->success("action success"); + +$c->action("action"); +$c->failure("action failure"); + +$c->action("action0"); +$c->action("action1"); +$c->action("action2"); +$c->success("action2 success"); +$c->success("action1 success"); +$c->success("action0 success"); + +$c->info("info"); +$c->note("note"); +$c->warn("warn"); +$c->error("error"); + +$c->end(); + +$c->title("title0"); +$c->title("title1"); +$c->print("print under title1"); +$c->end(); +$c->print("print under title0"); +$c->end(); + +$c->end(true); + +$c->section("multi-line\nsection"); +$c->title("multi-line\ntitle"); +$c->title("another\ntitle"); +$c->print("multi-line\nprint"); +$c->end(true);