implémentation contenu web

This commit is contained in:
Jephté Clain 2025-11-10 15:06:15 +04:00
parent d17b826b5a
commit b274372c41
11 changed files with 544 additions and 75 deletions

View File

@ -261,7 +261,7 @@ class v {
return $pieces; 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 = []; $vs = [];
if ($values !== null) { if ($values !== null) {
$functx = $function !== null? func::_prepare($function): 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 { 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 = [ const LIST_SCHEMA = [

View File

@ -0,0 +1,33 @@
<?php
namespace nulib\web\content;
class BlockTag extends Tag {
const START_PREFIX = null;
const START_SUFFIX = null;
const END_PREFIX = null;
const END_SUFFIX = "\n";
protected ?string $startPrefix;
protected ?string $startSuffix;
protected ?string $endPrefix;
protected ?string $endSuffix;
function reset(string $tag, ?array $params=null, $content=null): self {
parent::reset($tag, $params, $content);
$this->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];
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace nulib\web\content;
class EmptyTag extends Tag {
const ALLOW_EMPTY = true;
}

View File

@ -2,35 +2,179 @@
namespace nulib\web\content; namespace nulib\web\content;
use Closure;
use nulib\A; use nulib\A;
use nulib\cl;
use nulib\php\content\c; use nulib\php\content\c;
use nulib\php\content\IContent; use nulib\php\content\IContent;
use nulib\php\content\IPrintable;
use nulib\php\func;
use nur\str;
class Tag implements IContent { class Tag implements IContent, IPrintable {
function __construct(string $tag, $content=null) { const ALLOW_EMPTY = false;
$this->tag = $tag;
$content = c::q($content); const REQUIRE_CONTENT = false;
$this->content = $content;
function __construct(string $tag, ?array $params=null, $content=null) {
$this->reset($tag, $params, $content);
} }
protected bool $allowEmpty;
protected bool $requireContent;
protected string $tag; protected string $tag;
protected iterable $content;
function add($content): self { protected iterable $contents;
if (!is_array($this->content)) {
# si c'est un itérable, l'inclure avec un merge statique, afin de pouvoir protected ?array $attrs = null;
# rajouter des éléments
$this->content = [[[], $this->content]]; protected ?array $children = null;
}
A::merge($this->content, c::q($content)); 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; 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 ["</{$this->tag}>"];
}
function getContent($objectOrClass=null): iterable {
$this->resolve($objectOrClass);
if ($this->requireContent && !$this->children) return [];
return [ return [
"<$this->tag>", ...$this->getStart(),
...c::resolve($this->content, $object_or_class), ...$this->getChildren(),
"</$this->tag>", ...$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());
}
} }

View File

@ -5,6 +5,65 @@ namespace nulib\web\content;
* Class v: classe outil pour gérer du contenu pour le web * Class v: classe outil pour gérer du contenu pour le web
*/ */
class v { class v {
static function h1($content): iterable { return (new Tag("h1", $content))->getContent(); } private const require_content = ["require_content" => true];
const h1 = [Tag::class, null, "h1"]; 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];
} }

78
src/web/layout/README.md Normal file
View File

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

View File

@ -1,9 +1,10 @@
<?php <?php
namespace nulib\php\content; namespace nulib\php\content;
use nulib\php\content\impl\html; use nulib\php\content\impl\AContent;
use nulib\php\content\impl\APrintable;
use nulib\tests\TestCase;
use nulib\web\content\v; use nulib\web\content\v;
use PHPUnit\Framework\TestCase;
class cTest extends TestCase { class cTest extends TestCase {
function testTo_string() { function testTo_string() {
@ -20,21 +21,24 @@ class cTest extends TestCase {
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.<world>", c::to_string(["hello.", "<world>"]));
self::assertSame( self::assertSame(<<<EOT
"<h1>title&lt;q/&gt;</h1><p>hello<nq/><span>brave&lt;q/&gt;</span><span>world<nq/></span></p>", <h1>title&lt;q/&gt;</h1>
c::to_string([ <p>hello<nq/><span>brave&lt;q/&gt;</span><span>world<nq/></span></p>
[html::H1, "title<q/>"], EOT, c::to_string([
[html::P, [ [v::H1, "title<q/>"],
[v::P, [
"hello<nq/>", "hello<nq/>",
[html::SPAN, "brave<q/>"], [v::SPAN, "brave<q/>"],
[html::SPAN, ["world<nq/>"]], [v::SPAN, ["world<nq/>"]],
]], ]],
])); ]));
}
function testXxx() { self::assertSame(<<<EOT
$content = [[v::h1, "hello"]]; <span>content</span><p>printable</p>
self::assertSame("<h1>hello</h1>", c::to_string($content)); EOT, c::to_string([
new AContent(),
new APrintable(),
]));
} }
} }

View File

@ -1,23 +0,0 @@
<?php
namespace nulib\php\content\impl;
use nulib\php\content\c;
use nulib\php\content\IContent;
class ATag implements IContent {
function __construct(string $tag, $content=null) {
$this->tag = $tag;
$this->content = $content;
}
protected $tag;
protected $content;
function getContent(): iterable {
return [
"<$this->tag>",
...c::q($this->content),
"</$this->tag>",
];
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace nulib\php\content\impl;
class html {
const H1 = [self::class, "h1"];
const DIV = [self::class, "div"];
const P = [self::class, "p"];
const SPAN = [self::class, "span"];
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); }
}

View File

@ -0,0 +1,86 @@
<?php
namespace nulib\web\content;
use nulib\php\content\c;
use nulib\tests\TestCase;
class TagTest extends TestCase {
function testTag() {
$tag = new Tag("tag", null, [
"before",
"class" => "first",
["class" => "second"],
function () {
return 42;
},
"attr" => [
"static",
"true" => true,
"false" => false,
],
"after",
]);
self::assertSame([
null,
"<tag",
" ", "class", "=\"", "first second", "\"",
" ", "attr", "=\"", "static true", "\"",
">",
"before",
42,
"after",
"</tag>",
null,
], $tag->getContent());
self::assertSame('<tag class="first second" attr="static true">before 42 after</tag>', 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,
"<tag",
" ", "class", "=\"", "first second third", "\"",
" ", "cond", "=\"", "base ok dynok", "\"",
" ", "plouf", "=\"", "base ok dynok", "\"",
">",
"</tag>",
null,
], $tag->getContent());
self::assertSame('<tag class="first second third" cond="base ok dynok" plouf="base ok dynok"></tag>', c::to_string($tag));
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace nulib\web\content;
use nulib\cl;
use nulib\php\content\c;
use nulib\tests\TestCase;
class vTest extends TestCase {
function testStatic() {
$static = [
[v::H1, "title"],
[v::P, "text"],
];
self::assertSame(<<<EOT
<h1>title</h1>
<p>text</p>
EOT, c::to_string($static));
$static = [
[v::DIV, [
"before",
"class" => "div",
[v::SPAN, "spanned"],
"disabled" => false,
"checked" => true,
"after",
]],
];
self::assertSame(<<<EOT
<div class="div" checked="checked">before<span>spanned</span>after</div>
EOT, c::to_string($static));
}
function testDynamic() {
$dynamic = [
v::h1("title"),
v::p("text"),
];
self::assertSame(<<<EOT
<h1>title</h1>
<p>text</p>
EOT, c::to_string($dynamic));
$dynamic = [
v::div([
"before",
"class" => "div",
v::span("spanned"),
"disabled" => false,
"checked" => true,
"after",
]),
];
self::assertSame(<<<EOT
<div class="div" checked="checked">before<span>spanned</span>after</div>
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(<<<EOT
<table>
<thead>
<tr><th>a</th><th>b</th><th>c</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td><td>3</td></tr>
<tr><td>un</td><td>deux</td><td>trois</td></tr>
<tr><td>one</td><td>two</td><td>three</td></tr>
</tbody>
</table>
EOT, c::to_string($dynamic));
}
}