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 @@
+