diff --git a/nur_src/co.php b/nur_src/co.php index 6e5edaf..8e08a59 100644 --- a/nur_src/co.php +++ b/nur_src/co.php @@ -3,6 +3,7 @@ namespace nur; use nur\b\ui\IContent; use nur\b\ui\IPrintable; +use nur\sery\php\content\c; /** * Class co: affichage générique de données formatées avec {@link c} diff --git a/nur_src/ref/ref_type.php b/nur_src/ref/ref_type.php index 4465c50..da365bd 100644 --- a/nur_src/ref/ref_type.php +++ b/nur_src/ref/ref_type.php @@ -4,6 +4,7 @@ namespace nur\ref; use nur\data\types\Metadata; use nur\md; use nur\sery\os\file\file; +use nur\sery\php\content\c; /** * Class ref_type: référence des types utilisables dans les schémas @@ -102,7 +103,7 @@ class ref_type { /** comme {@link KEY} mais nullable */ const NKEY = "?".self::KEY; - /** comme {@link CONTENT} mais nullable */ + /** comme {@link c} mais nullable */ const NCONTENT = "?".self::CONTENT; /** comme {@link \nur\sery\FILE} mais nullable */ diff --git a/src/php/content/README.md b/src/php/content/README.md new file mode 100644 index 0000000..20e85f3 --- /dev/null +++ b/src/php/content/README.md @@ -0,0 +1,58 @@ +# nulib\php\content + +un contenu est une liste de valeurs, avec une syntaxe pour que certains éléments +soient dynamiquement calculés. + +le contenu final est résolu selon les règles suivantes: +- Si le contenu n'est pas un tableau: + - une chaine est quotée avec `htmlspecialchars()` + - un scalaire ou une instance d'objet sont pris tels quels +- Sinon, le contenu doit être un tableau, séquentiel ou associatif, ça n'a pas + d'incidence + - les éléments scalaires ou instance d'objets sont pris tels quels + - les tableaux représentent un traitement dynamique: appel de fonction, + instanciation, etc. + +Les syntaxes possibles sont: + +`[[], $args...]` +: merge littéral: les valeurs $args... sont insérées dans le + flux du contenu sans modification. c'est la seule façon d'insérer un tableau + dans la liste des valeurs + +`["class_or_function", $args...]` +`[["class_or_function"], $args...]` +: instantiation ou appel de fonction + +`["->method", $args...]` +`[["->method"], $args...]` +`[[null, "method"], $args...]` +: appel de méthode sur l'objet contexte spécifié lors de la résolution du contenu + +`[[$object, "method"], $args...]` +: appel de méthode sur l'objet spécifié + +`[["class", "method"], $args...]` +: appel de méthode statique de la classe spécifiée + +Le fait de transformer un contenu en une liste de valeurs statiques s'appelle +la résolution. la résolution se fait par rapport à un objet contexte, qui est +utilisé lors des appels de méthodes. + +## Affichage d'un contenu + +Deux interfaces sont utilisées pour modéliser un élément de contenu à afficher: +- IContent: objet capable de produire du contenu +- IPrintable: objet capable d'afficher un contenu + +Tous les autres éléments de contenus sont transformés en string avant affichage. +Un système de formatters permet de définir des fonctions ou méthodes à utiliser +pour formatter des objets de certains types. + +Lors de l'affichage du contenu, deux éléments contigûs $a et $b sont affichés +séparés par un espace sauf si: +- $a se termine par `[> ]` OU +- $b commence par `[< ]` +- $a et $b sont dans une section littérale e.g `[[], $a, $b]` + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/src/php/content/c.php b/src/php/content/c.php new file mode 100644 index 0000000..351f915 --- /dev/null +++ b/src/php/content/c.php @@ -0,0 +1,163 @@ + $value) { + if ($key === $index) { + $index++; + $seq = true; + } else { + $seq = false; + } + if (is_array($value)) { + # contenu dynamique + if (count($value) == 0) continue; + $func = cl::first($value); + $args = array_slice($value, 1); + if ($func === []) { + # merge statique + $sindex = 0; + foreach ($args as $skey => $arg) { + if ($skey === $sindex) { + $sindex++; + if ($seq) { + $dest[] = $arg; + } else { + # la première sous-clé séquentielle est ajoutée avec la clé du + # merge statique + $dest[$key] = $arg; + $seq = true; + } + } else { + $dest[$skey] = $arg; + } + } + continue; + } else { + # chaque argument est aussi un contenu + foreach ($args as &$arg) { + $array = is_array($arg); + $arg = self::resolve($arg, $object_or_class, false); + if (!$array) $arg = $arg[0]; + }; unset($arg); + if (func::is_static($func)) { + func::ensure_func($func, $object_or_class, $args); + $value = func::call($func, ...$args); + } elseif (func::is_class($func)) { + func::fix_class_args($func, $args); + $value = func::cons($func, ...$args); + } else { + func::ensure_func($func, $object_or_class, $args); + $value = func::call($func, ...$args); + } + } + } + if ($seq) $dest[] = $value; + else $dest[$key] = $value; + } + return $dest; + } + const resolve = [self::class, "resolve"]; + + private static final function wend(?string $value): bool { + return $value !== null && preg_match('/(\w|\w\.)$/', $value); + } + private static final function startw(?string $value): bool { + return $value !== null && preg_match('/^\w/', $value); + } + + private static final function to_values($content, ?array &$values=null): void { + $pvalue = cl::last($values); + $wend = self::wend($pvalue); + foreach ($content as $value) { + if ($value === null || $value === false) { + continue; + } elseif ($value instanceof IContent) { + self::to_values($value->getContent(), $values); + continue; + } elseif ($value instanceof IPrintable) { + ob_start(null, 0, PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE); + $value->print(); + $value = ob_get_clean(); + } else { + $value = strval($value); + #XXX rendre paramétrable le formatage de $value + } + if ($value !== "") { + $startw = self::startw($value); + if ($wend && $startw) $values[] = " "; + $values[] = $value; + $wend = self::wend($value); + } + } + } + + /** écrire le contenu sur la resource spécifiée, qui vaut STDOUT par défaut */ + static final function write($content, $fd=null, bool $resolve=true): void { + if ($resolve) $content = self::resolve($content); + $wend = false; + foreach ($content as $value) { + if ($value === null || $value === false) { + continue; + } elseif ($value instanceof IPrintable) { + if ($fd === null) { + $value->print(); + } else { + ob_start(null, 0, PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE); + $value->print(); + fwrite($fd, ob_get_clean()); + } + $wend = false; + continue; + } elseif ($value instanceof IContent) { + $values = []; + self::to_values($content, $values); + $value = implode("", $values); + } else { + $value = strval($value); + #XXX rendre paramétrable le formattage de $value + } + $startw = self::startw($value); + if (!$wend && !$startw) $value = " $value"; + if ($fd === null) echo $value; + else fwrite($fd, $value); + $wend = self::wend($value); + } + } + + /** retourner le contenu sous forme de chaine */ + static final function to_string($content, bool $resolve=true): string { + if ($resolve) $content = self::resolve($content); + $values = []; + self::to_values($content, $values); + return implode("", $values); + } +} diff --git a/src/php/func.php b/src/php/func.php index a8bbe78..7a9a305 100644 --- a/src/php/func.php +++ b/src/php/func.php @@ -4,7 +4,6 @@ namespace nur\sery\php; use Closure; use nur\sery\cl; use nur\sery\ref\php\ref_func; -use nur\sery\schema\Schema; use nur\sery\ValueException; use ReflectionClass; use ReflectionFunction; diff --git a/src/wip/web/content/README.md b/src/wip/web/content/README.md new file mode 100644 index 0000000..a58f477 --- /dev/null +++ b/src/wip/web/content/README.md @@ -0,0 +1,71 @@ +# nulib\web\content + +un contenu web est un type spécial de contenu permettant de gérer aussi les +attributs avec une syntaxe particulière. + +les règles diffèrent légèrement: +- Si le contenu n'est pas un tableau: + - une chaine est quotée avec `htmlspecialchars()` + - un scalaire ou une instance d'objet sont pris tels quels +- Sinon, le contenu doit être un tableau, séquentiel ou associatif + - les éléments séquentiels scalaires ou instance d'objets sont pris tels quels + - les éléments associatifs sont des attributs, et sont fusionnés le cas échéant + - les éléments séquentiels de type tableau séqentiel sont du contenu dynamique + - les éléments séquentiels de type tableau associatif sont évalues + récursivement avec les mêmes règles (sauf la partie chaine quotée avec + `htmlspecialchars()`) + +Par exemple, les deux contenus web suivants sont équivalents: +~~~php +# ce contenu: +$content1 = [ + "before", + "class" => "first", + ["class" => "second"], + ["func", "arg"], + "after", +]; +# donne le même résultat que: +$content2 = [ + "class" => "first second", + "before", + func("arg"), + "after", +]; +~~~ + +lors de la définition d'attributs, +- un tableau séquentiel est un appel de fonction +- un tableau associatif permet de faire des définitions conditionnelles + +Par exemple, les deux contenus web suivants sont équivalents: +~~~php +# ce contenu: +$content1 = [ + ["class" => ["func", "arg"]], +]; +# donne le même résultat que: +$content2 = [ + "class" => func("arg"), +]; +~~~ +De même, les deux contenus web suivants sont équivalents: +~~~php +# si $is_primary && !$is_danger +# alors ce contenu: +$content1 = [ + ["class" => [ + "btn", + "btn-primary" => $is_primary, + "btn-danger" => $is_danger, + "btn-default" => !$is_primary && !$is_danger, + ]], +]; +# donne le même résultat que: +$content2 = [ + "class" => "btn btn-primary", +]; +~~~ + + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/src/wip/web/content/v.php b/src/wip/web/content/v.php new file mode 100644 index 0000000..57d4b83 --- /dev/null +++ b/src/wip/web/content/v.php @@ -0,0 +1,8 @@ +", "")); - self::assertSame([""], content::resolve_static([""], [""])); + function testTo_string() { + self::assertSame("", c::to_string(null)); + self::assertSame("", c::to_string(false)); + self::assertSame("pouet<q/>", c::to_string("pouet")); + self::assertSame("pouet", c::to_string(["pouet"])); + self::assertSame("hello world", c::to_string(["hello", "world"])); + self::assertSame("hello1 world", c::to_string(["hello1", "world"])); + self::assertSame("hello", c::to_string(["hello", ""])); + self::assertSame("world", c::to_string(["", "world"])); + self::assertSame("hello,world", c::to_string(["hello,", "world"])); + self::assertSame("hello world", c::to_string(["hello ", "world"])); + self::assertSame("hello. world", c::to_string(["hello.", "world"])); + self::assertSame("hello.", c::to_string(["hello.", ""])); self::assertSame( "

title<q/>

hellobrave<q/>world

", - content::to_string([ + c::to_string([ [html::H1, "title"], [html::P, [ "hello", diff --git a/tests/php/content/impl/AStaticContent.php b/tests/php/content/impl/AStaticContent.php deleted file mode 100644 index cebc4fc..0000000 --- a/tests/php/content/impl/AStaticContent.php +++ /dev/null @@ -1,12 +0,0 @@ -tag = $tag; - $this->contents = $contents; + $this->content = $content; } protected $tag; - protected $contents; + protected $content; function getContent(): iterable { return [ "<$this->tag>", - ...content::quote($this->contents), + ...c::q($this->content), "tag>", ]; } diff --git a/tests/php/content/impl/html.php b/tests/php/content/impl/html.php index 86eeca7..d19388a 100644 --- a/tests/php/content/impl/html.php +++ b/tests/php/content/impl/html.php @@ -7,8 +7,8 @@ class html { const P = [self::class, "p"]; const SPAN = [self::class, "span"]; - static function h1($contents) { return new ATag("h1", $contents); } - static function div($contents) { return new ATag("div", $contents); } - static function p($contents) { return new ATag("p", $contents); } - static function span($contents) { return new ATag("span", $contents); } + static function h1($content) { return new ATag("h1", $content); } + static function div($content) { return new ATag("div", $content); } + static function p($content) { return new ATag("p", $content); } + static function span($content) { return new ATag("span", $content); } }