ajout SimpleTag

This commit is contained in:
Jephté Clain 2025-11-10 15:57:14 +04:00
parent b274372c41
commit b09d01c37a
7 changed files with 216 additions and 150 deletions

View File

@ -14,8 +14,8 @@ class BlockTag extends Tag {
protected ?string $endPrefix;
protected ?string $endSuffix;
function reset(string $tag, ?array $params=null, $content=null): self {
parent::reset($tag, $params, $content);
function reset(string $tag, $content=null, ?array $params=null): self {
parent::reset($tag, $content, $params);
$this->startPrefix = $params["start_prefix"] ?? self::START_PREFIX;
$this->startSuffix = $params["start_suffix"] ?? self::START_SUFFIX;
$this->endPrefix = $params["end_prefix"] ?? self::END_PREFIX;

View File

@ -1,6 +1,23 @@
<?php
namespace nulib\web\content;
class EmptyTag extends Tag {
const ALLOW_EMPTY = true;
use nulib\A;
use nulib\php\content\c;
class EmptyTag extends SimpleTag {
function getContent($objectOrClass=null): iterable {
$attrs = [];
$children = [];
$this->resolveContents($attrs, $children, $this->contents);
$parts = ["<{$this->tag}"];
$this->addAttrs($parts, $attrs);
if ($children) {
$parts[] = ">";
A::merge($parts, $children);
$parts[] = "</{$this->tag}>";
} else {
$parts[] = "/>";
}
return [c::to_string($parts)];
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace nulib\web\content;
use Closure;
use nulib\A;
use nulib\cl;
use nulib\php\content\c;
use nulib\php\content\IContent;
use nulib\php\content\IPrintable;
use nulib\php\func;
use nur\str;
class SimpleTag implements IContent {
function __construct(string $tag, $content=null) {
$this->reset($tag, $content);
}
protected string $tag;
protected iterable $contents;
function reset(string $tag, $content=null, ?array $params=null): self {
$this->tag = $tag;
$this->contents = c::q($content);
return $this;
}
function add($content): self {
if (!is_array($this->contents)) {
# si c'est un itérable, le mettre en tant qu'élément dans le tableau
$contents = $this->contents;
$this->contents = [static function() use ($contents) {
return $contents;
}];
}
A::merge($this->contents, c::q($content));
return $this;
}
protected function resolveContents(array &$attrs, array &$children, iterable $contents): void {
$index = 0;
foreach ($contents 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)) {
$children[] = $content;
} else {
A::merge($children, 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->resolveContents($attrs, $children, $content);
} 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($children, cl::with($content));
} else {
A::merge($attrs[$key], [$content]);
}
}
}
protected function addAttrs(array &$parts, array $attrs): void {
foreach ($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[] = "\"";
}
}
function getContent($objectOrClass=null): iterable {
$attrs = [];
$children = [];
$this->resolveContents($attrs, $children, $this->contents);
$parts = ["<{$this->tag}"];
$this->addAttrs($parts, $attrs);
$parts[] = ">";
A::merge($parts, $children);
$parts[] = "</{$this->tag}>";
return [c::to_string($parts)];
}
}

View File

@ -2,133 +2,52 @@
namespace nulib\web\content;
use Closure;
use nulib\A;
use nulib\cl;
use nulib\php\content\c;
use nulib\php\content\IContent;
use nulib\php\content\IPrintable;
use nulib\php\func;
use nur\str;
class Tag implements IContent, IPrintable {
const ALLOW_EMPTY = false;
class Tag extends SimpleTag implements IPrintable {
const REQUIRE_CONTENT = false;
function __construct(string $tag, ?array $params=null, $content=null) {
$this->reset($tag, $params, $content);
$this->reset($tag, $content, $params);
}
protected bool $allowEmpty;
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;
function reset(string $tag, $content=null, ?array $params=null): self {
parent::reset($tag, $content, $params);
$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 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));
parent::add($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);
$children = [];
$this->resolveContents($attrs, $children, $this->contents);
$this->attrs = $attrs;
$this->children = c::resolve($contents, $objectOrClass);
$this->empty = $this->allowEmpty && !$this->children;
$this->children = c::resolve($children, $objectOrClass);
}
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? "/>": ">";
$this->addAttrs($parts, $this->attrs);
$parts[] = ">";
return $parts;
}
@ -137,8 +56,7 @@ class Tag implements IContent, IPrintable {
}
function getEnd(): array {
if ($this->empty) return [];
else return ["</{$this->tag}>"];
return ["</{$this->tag}>"];
}
function getContent($objectOrClass=null): iterable {

View File

@ -24,10 +24,10 @@ class v {
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 hr($content=null): EmptyTag { return (new EmptyTag("hr", $content)); }
const HR = [EmptyTag::class, false, "hr"];
static function br($content=null): EmptyTag { return (new EmptyTag("br", $content)); }
const BR = [EmptyTag::class, false, "br"];
static function div($content): Tag { return (new Tag("div", null, $content)); }
const DIV = [Tag::class, false, "div", null];
@ -36,16 +36,16 @@ class v {
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 span($content): SimpleTag { return (new SimpleTag("span", $content)); }
const SPAN = [SimpleTag::class, false, "span"];
static function b($content): SimpleTag { return (new SimpleTag("b", $content)); }
const B = [SimpleTag::class, false, "b"];
static function i($content): SimpleTag { return (new SimpleTag("i", $content)); }
const I = [SimpleTag::class, false, "i"];
static function em($content): SimpleTag { return (new SimpleTag("em", $content)); }
const EM = [SimpleTag::class, false, "em"];
static function strong($content): SimpleTag { return (new SimpleTag("strong", $content)); }
const STRONG = [SimpleTag::class, false, "strong"];
static function ul($content): BlockTag { return (new BlockTag("ul", self::start_nl, $content)); }
const UL = [BlockTag::class, false, "ul", self::start_nl];

View File

@ -22,7 +22,6 @@ class TagTest extends TestCase {
]);
self::assertSame([
null,
"<tag",
" ", "class", "=\"", "first second", "\"",
" ", "attr", "=\"", "static true", "\"",
@ -31,7 +30,6 @@ class TagTest extends TestCase {
42,
"after",
"</tag>",
null,
], $tag->getContent());
self::assertSame('<tag class="first second" attr="static true">before 42 after</tag>', c::to_string($tag));
@ -71,14 +69,12 @@ class TagTest extends TestCase {
]);
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

@ -7,53 +7,81 @@ 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));
EOT, c::to_string([
[v::H1, "title"],
[v::P, "text"],
]));
$static = [
self::assertSame(<<<EOT
<h1>title</h1>
<p>the<b>bold</b>text<br/>linefeed</p><hr/>
EOT, c::to_string([
[v::H1, "title"],
[v::P, [
"the",
[v::B, "bold"],
"text",
[v::BR],
"linefeed",
]],
[v::HR],
]));
self::assertSame(<<<EOT
<div class="div" checked="checked">before<span>spanned</span><span class="black">blacked</span>after</div>
EOT, c::to_string([
[v::DIV, [
"before",
"class" => "div",
[v::SPAN, "spanned"],
[v::SPAN, ["class" => "black", "blacked"]],
"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));
EOT, c::to_string([
v::h1("title"),
v::p("text"),
]));
$dynamic = [
self::assertSame(<<<EOT
<h1>title</h1>
<p>the<b>bold</b>text<br/>linefeed</p><hr/>
EOT, c::to_string([
v::h1("title"),
v::p([
"the",
v::b("bold"),
"text",
v::br(),
"linefeed",
]),
v::hr(),
]));
self::assertSame(<<<EOT
<div class="div" checked="checked">before<span>spanned</span><span class="black">blacked</span>after</div>
EOT, c::to_string([
v::div([
"before",
"class" => "div",
v::span("spanned"),
v::span(["class" => "black", "blacked"]),
"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() {
@ -62,7 +90,19 @@ EOT, c::to_string($dynamic));
["a" => "un", "b" => "deux", "c" => "trois"],
["a" => "one", "b" => "two", "c" => "three"],
];
$dynamic = v::table([
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(v::table([
v::thead(v::tr(function() use ($rows) {
$headers = array_keys(cl::first($rows));
foreach ($headers as $header) {
@ -78,19 +118,6 @@ EOT, c::to_string($dynamic));
});
}
}),
]);
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));
])));
}
}