diff --git a/nur_src/v/v.php b/nur_src/v/v.php index a657974..db94d17 100644 --- a/nur_src/v/v.php +++ b/nur_src/v/v.php @@ -261,7 +261,7 @@ class v { return $pieces; } - private static function _list(?iterable $values, ?string $sep=", ", ?string $prefix=null, ?string $suffix=null, ?callable $function): array { + private static function _list(?iterable $values, ?string $sep=", ", ?string $prefix=null, ?string $suffix=null, ?callable $function=null): array { $vs = []; if ($values !== null) { $functx = $function !== null? func::_prepare($function): null; @@ -285,7 +285,7 @@ class v { } static final function simple_list(?iterable $values, ?string $sep=", ", ?string $prefix=null, ?string $suffix=null): array { - return self::_list($values, $sep, $prefix, $suffix, null); + return self::_list($values, $sep, $prefix, $suffix); } const LIST_SCHEMA = [ diff --git a/src/web/content/BlockTag.php b/src/web/content/BlockTag.php new file mode 100644 index 0000000..ad5c04a --- /dev/null +++ b/src/web/content/BlockTag.php @@ -0,0 +1,33 @@ +startPrefix = $params["start_prefix"] ?? self::START_PREFIX; + $this->startSuffix = $params["start_suffix"] ?? self::START_SUFFIX; + $this->endPrefix = $params["end_prefix"] ?? self::END_PREFIX; + $this->endSuffix = $params["end_suffix"] ?? self::END_SUFFIX; + return $this; + } + + function getStart(): array { + return [$this->startPrefix, ...parent::getStart(), $this->startSuffix]; + } + + function getEnd(): array { + return [$this->endPrefix, ...parent::getEnd(), $this->endSuffix]; + } +} diff --git a/src/web/content/EmptyTag.php b/src/web/content/EmptyTag.php new file mode 100644 index 0000000..2e60179 --- /dev/null +++ b/src/web/content/EmptyTag.php @@ -0,0 +1,6 @@ +tag = $tag; - $content = c::q($content); - $this->content = $content; +class Tag implements IContent, IPrintable { + const ALLOW_EMPTY = false; + + const REQUIRE_CONTENT = false; + + function __construct(string $tag, ?array $params=null, $content=null) { + $this->reset($tag, $params, $content); } - protected string $tag; - protected iterable $content; + protected bool $allowEmpty; - function add($content): self { - if (!is_array($this->content)) { - # si c'est un itérable, l'inclure avec un merge statique, afin de pouvoir - # rajouter des éléments - $this->content = [[[], $this->content]]; - } - A::merge($this->content, c::q($content)); + protected bool $requireContent; + + protected string $tag; + + protected iterable $contents; + + protected ?array $attrs = null; + + protected ?array $children = null; + + protected bool $empty = false; + + function reset(string $tag, ?array $params=null, $content=null): self { + $this->allowEmpty = $params["allow_empty"] ?? self::ALLOW_EMPTY; + $this->requireContent = $params["require_content"] ?? self::REQUIRE_CONTENT; + $this->tag = $tag; + $this->contents = c::q($content); + $this->attrs = null; + $this->children = null; + $this->empty = false; return $this; } - function getContent($object_or_class=null): iterable { + function add($content): self { + if (!is_array($this->contents)) { + # si c'est un itérable, l'inclure avec un merge statique, afin de pouvoir + # rajouter des éléments + $contents = $this->contents; + $this->contents = [static function() use ($contents) { + return $contents; + }]; + } + A::merge($this->contents, c::q($content)); + $this->attrs = null; + $this->children = null; + $this->empty = false; + return $this; + } + + private function add_contents(iterable $source, array &$contents, array &$attrs): void { + $index = 0; + foreach ($source as $key => $content) { + if ($content instanceof Closure) { + # les closure sont appelés dès la résolution + $content = func::with($content)->invoke([$this]); + if ($key === $index) { + $index++; + # le résultat est inséré tels quels dans le flux + if (is_iterable($content)) { + $contents[] = $content; + } else { + A::merge($contents, cl::with($content)); + } + } else { + # le résultat est la valeur de l'attribut + A::merge($attrs[$key], [$content]); + } + } elseif (is_array($content)) { + if ($key === $index) { + $index++; + $this->add_contents($content, $contents, $attrs); + } else { + foreach ($content as &$value) { + if ($value instanceof Closure) { + $value = func::with($value)->invoke([$this]); + } + }; unset($value); + A::merge($attrs[$key], $content); + } + } elseif ($key === $index) { + $index++; + A::merge($contents, cl::with($content)); + } else { + A::merge($attrs[$key], [$content]); + } + } + } + + function resolve($objectOrClass=null): self { + if ($this->attrs === null) { + $attrs = []; + $contents = []; + $this->add_contents($this->contents, $contents, $attrs); + $this->attrs = $attrs; + $this->children = c::resolve($contents, $objectOrClass); + $this->empty = $this->allowEmpty && !$this->children; + } + return $this; + } + + function getStart(): array { + $parts = ["<{$this->tag}"]; + foreach ($this->attrs as $key => $value) { + if ($value === null || $value === false) continue; + if ($value === true || $value === [true]) { + $value = $key; + } elseif (is_array($value)) { + $value = str::join3($value); + if (!$value) continue; + } + $parts[] = " "; + $parts[] = $key; + $parts[] = "=\""; + $parts[] = htmlspecialchars($value); + $parts[] = "\""; + } + $parts[] = $this->empty? "/>": ">"; + return $parts; + } + + function getChildren(): array { + return $this->children; + } + + function getEnd(): array { + if ($this->empty) return []; + else return ["tag}>"]; + } + + function getContent($objectOrClass=null): iterable { + $this->resolve($objectOrClass); + if ($this->requireContent && !$this->children) return []; return [ - "<$this->tag>", - ...c::resolve($this->content, $object_or_class), - "tag>", + ...$this->getStart(), + ...$this->getChildren(), + ...$this->getEnd(), ]; } + + /** afficher le tag ouvrant. */ + function printStart(): void { + $this->resolve(); + c::write($this->getStart()); + } + + /** afficher le contenu enfant */ + function printChildren(): void { + $this->resolve(); + c::write($this->getChildren()); + } + + /** afficher le tag fermant */ + function printEnd(): void { + $this->resolve(); + c::write($this->getEnd()); + } + + /** afficher le tag et le contenu enfant */ + function print(): void { + $this->resolve(); + if ($this->requireContent && !$this->children) return; + c::write($this->getStart()); + c::write($this->getChildren()); + c::write($this->getEnd()); + } } diff --git a/src/web/content/v.php b/src/web/content/v.php index e6d7fbb..1d57970 100644 --- a/src/web/content/v.php +++ b/src/web/content/v.php @@ -5,6 +5,65 @@ namespace nulib\web\content; * Class v: classe outil pour gérer du contenu pour le web */ class v { - static function h1($content): iterable { return (new Tag("h1", $content))->getContent(); } - const h1 = [Tag::class, null, "h1"]; + private const require_content = ["require_content" => true]; + private const start_nl = ["start_suffix" => "\n"]; + + private static function h(string $tag, $content): BlockTag { + return new BlockTag($tag, self::require_content, $content); + } + static function h1($content): BlockTag { return self::h("h1", $content); } + const H1 = [BlockTag::class, false, "h1", self::require_content]; + static function h2($content): BlockTag { return self::h("h2", $content); } + const H2 = [BlockTag::class, false, "h2", self::require_content]; + static function h3($content): BlockTag { return self::h("h3", $content); } + const H3 = [BlockTag::class, false, "h3", self::require_content]; + static function h4($content): BlockTag { return self::h("h4", $content); } + const H4 = [BlockTag::class, false, "h4", self::require_content]; + static function h5($content): BlockTag { return self::h("h5", $content); } + const H5 = [BlockTag::class, false, "h5", self::require_content]; + static function h6($content): BlockTag { return self::h("h6", $content); } + const H6 = [BlockTag::class, false, "h6", self::require_content]; + + static function hr($content): EmptyTag { return (new EmptyTag("hr", null, $content)); } + const HR = [EmptyTag::class, false, "hr", null]; + static function br($content): EmptyTag { return (new EmptyTag("br", null, $content)); } + const BR = [EmptyTag::class, false, "br", null]; + + static function div($content): Tag { return (new Tag("div", null, $content)); } + const DIV = [Tag::class, false, "div", null]; + static function p($content): Tag { return (new Tag("p", self::require_content, $content)); } + const P = [Tag::class, false, "p", self::require_content]; + static function pre($content): Tag { return (new Tag("pre", self::require_content, $content)); } + const PRE = [Tag::class, false, "pre", self::require_content]; + + static function span($content): Tag { return (new Tag("span", null, $content)); } + const SPAN = [Tag::class, false, "span", null]; + static function b($content): Tag { return (new Tag("b", null, $content)); } + const B = [Tag::class, false, "b", null]; + static function i($content): Tag { return (new Tag("i", null, $content)); } + const I = [Tag::class, false, "i", null]; + static function em($content): Tag { return (new Tag("em", null, $content)); } + const EM = [Tag::class, false, "em", null]; + static function strong($content): Tag { return (new Tag("strong", null, $content)); } + const STRONG = [Tag::class, false, "strong", null]; + + static function ul($content): BlockTag { return (new BlockTag("ul", self::start_nl, $content)); } + const UL = [BlockTag::class, false, "ul", self::start_nl]; + static function ol($content): BlockTag { return (new BlockTag("ol", self::start_nl, $content)); } + const OL = [BlockTag::class, false, "ol", self::start_nl]; + static function li($content): BlockTag { return (new BlockTag("li", null, $content)); } + const LI = [BlockTag::class, false, "li", null]; + + static function table($content): BlockTag { return (new BlockTag("table", self::start_nl, $content)); } + const TABLE = [BlockTag::class, false, "table", self::start_nl]; + static function thead($content): BlockTag { return (new BlockTag("thead", self::start_nl, $content)); } + const THEAD = [BlockTag::class, false, "thead", self::start_nl]; + static function tbody($content): BlockTag { return (new BlockTag("tbody", self::start_nl, $content)); } + const TBODY = [BlockTag::class, false, "tbody", self::start_nl]; + static function tr($content): BlockTag { return (new BlockTag("tr", null, $content)); } + const TR = [BlockTag::class, false, "tr", null]; + static function th($content): Tag { return (new Tag("th", null, $content)); } + const TH = [Tag::class, false, "th", null]; + static function td($content): Tag { return (new Tag("td", null, $content)); } + const TD = [Tag::class, false, "td", null]; } diff --git a/src/web/layout/README.md b/src/web/layout/README.md new file mode 100644 index 0000000..515c0a0 --- /dev/null +++ b/src/web/layout/README.md @@ -0,0 +1,78 @@ +# nulib\web\layout + +faire le layout d'avance, e.g +~~~php +ly::prepare([ + ["row", "class" => "row-gap", + ["col", 2, "content" => "a"], + ["col", 10, "content" => "b"], + ], + ["row", + ["col", 6, "content" => "c"], + ["col", 6, "content" => "d"], + ], +]); +~~~ +dans cet exemple, il y a 4 sections de contenu appelées "a", "b", "c" et "d" + +désactiver le contenu dans un colonne ou un row avec `"content" => false` + +faut-il prévoir d'autres types que "row" et "col", par exemple "panel"? + +une fois que le layout est fait, on sélectionne les sections avant de les remplir +~~~php +ly::start("a"); +//... contenu de la section "a" +ly::start("b"); +//... contenu de la section "b" +ly::end(); +~~~ +tant que les sections sont mentionnées dans l'ordre, l'affichage se fait au fur +et à mesure + +*éventuellement*, supporter un mode où les sections sont remplies dans un ordre +quelconque. dans ce cas, le contenu est enregistré dans un fichier temporaire +mémoire avec `ob_start()` et `ob_end()` puis il est affiché à la fin lors de +ly::end() +exemple: +~~~php +ly::prepare([["row", + ["col", 3, "content" => "menu"], + ["col", 9, "content" => "details"], +]]); +foreach ($items as $item) { + ly::start("menu"); + write(link); + ly::start("details"); + write(details); +} +~~~ + +## alternatives + +les ids de contenu sont des clés +~~~php +ly::prepare([["row", + "menu" => ["col", 3], + "details" => ["col", 9], +]]); +~~~ +conflit possible avec `"class" => xxx` et autres attributs? + +`ly::start($id, $func)` permet de basculer temporairement dans une section +~~~php +ly::start("details"); +foreach ($items as $item) { + ly::start("menu", function() { + write(link); + }); + write(details); +} +~~~ + +le comportement d'enregistrer le contenu devrait être demandé explicitement +* true +* false: exception si une section n'est pas remplie avant de passer à la suivante +* auto: activé si les sections sont accédées dans un ordre différent du naturel + +-*- 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/tests/php/content/cTest.php b/tests/php/content/cTest.php index 64875e4..f6d0cf3 100644 --- a/tests/php/content/cTest.php +++ b/tests/php/content/cTest.php @@ -1,9 +1,10 @@ ", c::to_string(["hello.", ""])); - self::assertSame( - "

title<q/>

hellobrave<q/>world

", - c::to_string([ - [html::H1, "title"], - [html::P, [ - "hello", - [html::SPAN, "brave"], - [html::SPAN, ["world"]], - ]], - ])); - } + self::assertSame(<<title<q/> +

hellobrave<q/>world

+EOT, c::to_string([ + [v::H1, "title"], + [v::P, [ + "hello", + [v::SPAN, "brave"], + [v::SPAN, ["world"]], + ]], + ])); - function testXxx() { - $content = [[v::h1, "hello"]]; - self::assertSame("

hello

", c::to_string($content)); + self::assertSame(<<content

printable

+EOT, c::to_string([ + new AContent(), + new APrintable(), + ])); } } diff --git a/tests/php/content/impl/ATag.php b/tests/php/content/impl/ATag.php deleted file mode 100644 index b4cc2ed..0000000 --- a/tests/php/content/impl/ATag.php +++ /dev/null @@ -1,23 +0,0 @@ -tag = $tag; - $this->content = $content; - } - - protected $tag; - protected $content; - - function getContent(): iterable { - return [ - "<$this->tag>", - ...c::q($this->content), - "tag>", - ]; - } -} diff --git a/tests/php/content/impl/html.php b/tests/php/content/impl/html.php deleted file mode 100644 index 3567b2e..0000000 --- a/tests/php/content/impl/html.php +++ /dev/null @@ -1,14 +0,0 @@ - "first", + ["class" => "second"], + function () { + return 42; + }, + "attr" => [ + "static", + "true" => true, + "false" => false, + ], + "after", + ]); + + self::assertSame([ + null, + "", + "before", + 42, + "after", + "", + null, + ], $tag->getContent()); + + self::assertSame('before 42 after', c::to_string($tag)); + } + + function testMerge() { + $tag = new Tag("tag", null, [ + "class" => "first", + ["class" => "second"], + ["class" => function () { + return "third"; + }], + "cond" => [ + "base", + "ok" => true, + "ko" => false, + "dynok" => function () { + return true; + }, + "dynko" => function () { + return false; + }, + ], + ["plouf" => "base"], + ["plouf" => [ + "ok" => true, + "ko" => false, + ]], + ["plouf" => [ + "dynok" => function () { + return true; + }, + "dynko" => function () { + return false; + }, + ]], + ]); + + self::assertSame([ + null, + "", + "", + null, + ], $tag->getContent()); + + self::assertSame('', c::to_string($tag)); + } +} diff --git a/tests/web/content/vTest.php b/tests/web/content/vTest.php new file mode 100644 index 0000000..5d972ae --- /dev/null +++ b/tests/web/content/vTest.php @@ -0,0 +1,96 @@ +title +

text

+EOT, c::to_string($static)); + + $static = [ + [v::DIV, [ + "before", + "class" => "div", + [v::SPAN, "spanned"], + "disabled" => false, + "checked" => true, + "after", + ]], + ]; + self::assertSame(<<beforespannedafter +EOT, c::to_string($static)); + } + + function testDynamic() { + $dynamic = [ + v::h1("title"), + v::p("text"), + ]; + self::assertSame(<<title +

text

+EOT, c::to_string($dynamic)); + + $dynamic = [ + v::div([ + "before", + "class" => "div", + v::span("spanned"), + "disabled" => false, + "checked" => true, + "after", + ]), + ]; + self::assertSame(<<beforespannedafter +EOT, c::to_string($dynamic)); + } + + function testDynamic2() { + $rows = [ + ["a" => 1, "b" => 2, "c" => 3], + ["a" => "un", "b" => "deux", "c" => "trois"], + ["a" => "one", "b" => "two", "c" => "three"], + ]; + $dynamic = v::table([ + v::thead(v::tr(function() use ($rows) { + $headers = array_keys(cl::first($rows)); + foreach ($headers as $header) { + yield v::th($header); + } + })), + v::tbody(function() use ($rows) { + foreach ($rows as $row) { + yield v::tr(function () use ($row) { + foreach ($row as $col) { + yield v::td($col); + } + }); + } + }), + ]); + self::assertSame(<< + +abc + + +123 +undeuxtrois +onetwothree + + + +EOT, c::to_string($dynamic)); + } +}