diff --git a/src/Template/HtmlBuilder/Block.php b/src/Template/HtmlBuilder/Block.php
new file mode 100644
index 0000000..2015280
--- /dev/null
+++ b/src/Template/HtmlBuilder/Block.php
@@ -0,0 +1,264 @@
+ $css,
+ * @param array $options
+ * @return array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array}
+ * @throws HtmlBuilderExcpetion
+ */
+ public static function cel(
+ string $tag,
+ string $id = '',
+ string $content = '',
+ array $css = [],
+ array $options = []
+ ): array {
+ if (!preg_match("/^[A-Za-z]+$/", $tag)) {
+ Error::setError(
+ '201',
+ 'invalid or empty tag',
+ ['tag' => $tag]
+ );
+ throw new HtmlBuilderExcpetion('Invalid or empty tag');
+ }
+ return [
+ 'tag' => $tag,
+ 'id' => $id,
+ 'name' => $options['name'] ?? '',
+ 'content' => $content,
+ 'css' => $css,
+ 'options' => $options,
+ 'sub' => [],
+ ];
+ }
+
+ /**
+ * Search element tree for id and add
+ * if id is empty add at current
+ *
+ * @param array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array} $base
+ * @param array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array} $attach
+ * @param string $id
+ * @return array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array}
+ */
+ public static function ael(
+ array $base,
+ array $attach,
+ string $id = ''
+ ): array {
+ // no id or matching id
+ if (
+ empty($id) ||
+ $base['id'] == $id
+ ) {
+ self::addSub($base, $attach);
+ return $base;
+ }
+ // find id in 'id' in all 'sub'
+ foreach ($base['sub'] as $el) {
+ $el = self::ael($el, $attach, $id);
+ }
+
+ return $base;
+ }
+
+ /**
+ * Undocumented function
+ *
+ * @param array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array} $base
+ * @param array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array} ...$attach
+ * @return array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array}
+ */
+ public static function aelx(
+ array $base,
+ array ...$attach
+ ): array {
+ $base = self::addSub($base, ...$attach);
+ return $base;
+ }
+
+ /**
+ * Undocumented function
+ *
+ * @param array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array} $element
+ * @param array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array} $sub
+ * @return array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array}
+ */
+ public static function addSub(array $element, array ...$sub): array
+ {
+ if (!isset($element['sub'])) {
+ $element['sub'] = [];
+ }
+ array_push($element['sub'], ...$sub);
+ return $element;
+ }
+
+ /**
+ * Undocumented function
+ *
+ * @param array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array} $element
+ * @return array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array}
+ */
+ public static function resetSub(array $element): array
+ {
+ $element['sub'] = [];
+ return $element;
+ }
+
+ // CSS Elements
+
+ /**
+ * Undocumented function
+ *
+ * @param array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array} $element
+ * @param string ...$css
+ * @return array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array}
+ */
+ public static function acssel(array $element, string ...$css): array
+ {
+ $element['css'] = array_unique(array_merge($element['css'] ?? [], $css));
+ return $element;
+ }
+
+ /**
+ * Undocumented function
+ *
+ * @param array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array} $element
+ * @param string ...$css
+ * @return array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array}
+ */
+ public static function rcssel(array $element, string ...$css): array
+ {
+ $element['css'] = array_diff($element['css'] ?? [], $css);
+ return $element;
+ }
+
+ /**
+ * Undocumented function
+ * scssel (switch) is not supported
+ * use rcssel -> acssel
+ *
+ * @param array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array} $element
+ * @param array $rcss
+ * @param array $acss
+ * @return array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array}
+ */
+ public static function scssel(array $element, array $rcss, array $acss): array
+ {
+ return self::acssel(
+ self::rcssel($element, ...$rcss),
+ ...$acss
+ );
+ }
+
+ /**
+ * Undocumented function
+ * alias phfo
+ *
+ * @param array{tag:string,id:string,name:string,content:string,css:array,options:array,sub:array} $tree
+ * @param bool $add_nl [default=false]
+ * @return string
+ */
+ public static function buildHtml(array $tree, bool $add_nl = false): string
+ {
+ if (empty($tree['tag'])) {
+ return '';
+ }
+ // print "D01: " . microtime(true) . " ";
+ $line = '<' . $tree['tag'];
+
+ if (!empty($tree['id'])) {
+ $line .= ' id="' . $tree['id'] . '"';
+ if (in_array($tree['tag'], Settings::NAME_ELEMENTS)) {
+ $line .= ' name="'
+ . (!empty($tree['name']) ? $tree['name'] : $tree['id'])
+ . '"';
+ }
+ }
+ if (count($tree['css'])) {
+ $line .= ' class="' . join(' ', $tree['css']) . '"';
+ }
+ foreach ($tree['options'] ?? [] as $key => $item) {
+ if (in_array($key, Settings::SKIP_OPTIONS)) {
+ continue;
+ }
+ $line .= ' ' . $key . '="' . $item . '"';
+ }
+ $line .= '>';
+ if (!empty($tree['content'])) {
+ $line .= $tree['content'];
+ }
+ // sub nodes
+ foreach ($tree['sub'] ?? [] as $sub) {
+ if ($add_nl === true) {
+ $line .= "\n";
+ }
+ $line .= self::buildHtml($sub, $add_nl);
+ if ($add_nl === true) {
+ $line .= "\n";
+ }
+ }
+
+ // close line if needed
+ if (!in_array($tree['tag'], Settings::NO_CLOSE)) {
+ $line .= '' . $tree['tag'] . '>';
+ }
+
+ return $line;
+ }
+
+ /**
+ * Undocumented function
+ * alias phfa
+ *
+ * @param array,options:array,sub:array}> $list
+ * @param bool $add_nl [default=false]
+ * @return string
+ */
+ public static function buildHtmlFromList(array $list, bool $add_nl = false): string
+ {
+ $output = '';
+ foreach ($list as $el) {
+ $output .= self::buildHtml($el, $add_nl);
+ }
+ return $output;
+ }
+
+ /**
+ * Undocumented function
+ * wrapper for buildHtmlFromList
+ *
+ * @param array,options:array,sub:array}> $list array of Elements to build string from
+ * @param bool $add_nl [default=false] Optional output string line break
+ * @return string build html as string
+ */
+ public static function printHtmlFromArray(array $list, bool $add_nl = false): string
+ {
+ return self::buildHtmlFromList($list, $add_nl);
+ }
+}
+
+// __END__
diff --git a/src/Template/HtmlBuilder/Element.php b/src/Template/HtmlBuilder/Element.php
new file mode 100644
index 0000000..85d5874
--- /dev/null
+++ b/src/Template/HtmlBuilder/Element.php
@@ -0,0 +1,559 @@
+ */
+ private array $css = [];
+ /** @var array */
+ private array $options = [];
+ /** @var array list of elements */
+ private array $sub = [];
+
+ /**
+ * create new html element
+ *
+ * @param string $tag html tag (eg div, button, etc)
+ * @param string $id html tag id, used also for name if name
+ * not set in $options
+ * @param string $content content text inside, eg
Content
+ * if sub elements exist, they are added after content
+ * @param array $css array of css names, put style in $options
+ * @param array $options Additional element options in
+ * key = value format
+ * eg: onClick => 'something();'
+ * id, css are skipped
+ * name only set on input/button
+ * @throws HtmlBuilderExcpetion
+ */
+ public function __construct(
+ string $tag,
+ string $id = '',
+ string $content = '',
+ array $css = [],
+ array $options = []
+ ) {
+ // exit if not valid tag
+ try {
+ $this->setTag($tag);
+ } catch (HtmlBuilderExcpetion $e) {
+ throw new HtmlBuilderExcpetion('Could not create Element', 0, $e);
+ }
+ $this->setId($id);
+ $this->setName($options['name'] ?? '');
+ $this->setContent($content);
+ $this->addCss(...$css);
+ $this->setOptions($options);
+ }
+
+ /**
+ * set tag
+ *
+ * @param string $tag
+ * @return void
+ * @throws HtmlBuilderExcpetion
+ */
+ public function setTag(string $tag): void
+ {
+ // tag must be letters only
+ if (!preg_match("/^[A-Za-z]+$/", $tag)) {
+ Error::setError(
+ '201',
+ 'invalid or empty tag',
+ ['tag' => $tag]
+ );
+ throw new HtmlBuilderExcpetion('Invalid or empty tag: ' . $tag);
+ }
+ $this->tag = $tag;
+ }
+
+ /**
+ * get the tag name
+ *
+ * @return string HTML element tag
+ */
+ public function getTag(): string
+ {
+ return $this->tag;
+ }
+
+ /**
+ * set the element id
+ *
+ * @param string $id
+ * @return void
+ */
+ public function setId(string $id): void
+ {
+ // invalid id and name check too
+ // be strict: [a-zA-Z0-9], -, _
+ // cannot start with digit, two hyphens or a hyphen with a digit:
+ // 0abc
+ // __abc
+ // _0abc
+ if (
+ !empty($id) &&
+ !preg_match("/^[A-Za-z][\w-]*$/", $id)
+ ) {
+ Error::setWarning(
+ '202',
+ 'possible invalid id',
+ ['id' => $id, 'tag' => $this->getTag()]
+ );
+ // TODO: shoud throw error
+ }
+ $this->id = $id;
+ }
+
+ /**
+ * get the html tag id
+ *
+ * @return string HTML element id
+ */
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ /**
+ * Set name for elements
+ * only for elements that need it (input/button/form)
+ *
+ * @param string $name
+ * @return void
+ */
+ public function setName(string $name): void
+ {
+ if (
+ !empty($name) &&
+ !preg_match("/^[A-Za-z][\w-]*$/", $name)
+ ) {
+ Error::setWarning(
+ '203',
+ 'possible invalid name',
+ ['name' => $name, 'id' => $this->getId(), 'tag' => $this->getTag()]
+ );
+ // TODO: shoud throw error
+ }
+ $this->name = $name;
+ }
+
+ /**
+ * get the name if set
+ *
+ * @return string Optional HTML name (eg for input)
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set new content for element
+ *
+ * @param string $content
+ * @return void
+ */
+ public function setContent(string $content): void
+ {
+ $this->content = $content;
+ }
+
+ /**
+ * get the elment text content (not sub elements)
+ *
+ * @return string HTML content text as is
+ */
+ public function getContent(): string
+ {
+ return $this->content;
+ }
+
+ /**
+ * set or update options
+ *
+ * @param array $options
+ * @return void
+ */
+ public function setOptions(array $options): void
+ {
+ foreach ($options as $key => $value) {
+ if (empty($key)) {
+ Error::setError(
+ '110',
+ 'Cannot set option with empty key',
+ ['id' => $this->getId(), 'tag' => $this->getTag()]
+ );
+ // TODO: shoud throw error
+ continue;
+ }
+ // if data is null
+ if ($value === null) {
+ if (isset($this->options[$key])) {
+ unset($this->options[$key]);
+ } else {
+ Error::setError(
+ '210',
+ 'Cannot set option with null value',
+ ['id' => $this->getId(), 'tag' => $this->getTag()]
+ );
+ }
+ // TODO: shoud throw error
+ continue;
+ }
+ $this->options[$key] = $value;
+ }
+ }
+
+ /**
+ * get the options array
+ * also holds "name" option
+ * anything like: style, javascript, value or any other html tag option
+ * right side can be empty but not null
+ *
+ * @return array get options as list html option name and value
+ */
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+
+ // Sub Elements
+
+ /**
+ * get the sub elements (array of Elements)
+ *
+ * @return array Array of Elements (that can have sub elements)
+ */
+ public function getSub(): array
+ {
+ return $this->sub;
+ }
+
+ /**
+ * add one or many sub elements (add at the end)
+ *
+ * @param Element $sub One or many elements to add
+ * @return void
+ * @throws HtmlBuilderExcpetion
+ */
+ public function addSub(Element ...$sub): void
+ {
+ foreach ($sub as $_sub) {
+ // if one of the elements is the same as this class, ignore it
+ // with this we avoid self reference loop
+ if ($_sub == $this) {
+ Error::setError(
+ '100',
+ 'Cannot assign Element to itself, this would create an infinite loop',
+ ['id' => $this->getId(), 'tag' => $this->getTag()]
+ );
+ throw new HtmlBuilderExcpetion('Cannot assign Element to itself, this would create an infinite loop');
+ }
+ array_push($this->sub, $_sub);
+ }
+ }
+
+ /**
+ * Remove an element from the sub array
+ * By pos in array or id set on first level
+ *
+ * @param int|string $id String id name or int pos number in array
+ * @return void
+ */
+ public function removeSub(int|string $id): void
+ {
+ // find element with id and remove it
+ // or when number find pos in sub and remove it
+ if (is_int($id)) {
+ if (!isset($this->sub[$id])) {
+ return;
+ }
+ unset($this->sub[$id]);
+ return;
+ }
+ // only on first level
+ foreach ($this->sub as $pos => $el) {
+ if (
+ $el->getId() === $id
+ ) {
+ unset($this->sub[$pos]);
+ return;
+ }
+ }
+ }
+
+ /**
+ * remove all sub elements
+ *
+ * @return void
+ */
+ public function resetSub(): void
+ {
+ $this->sub = [];
+ }
+
+ // CSS Elements
+
+ /**
+ * get the current set css elements
+ *
+ * @return array list of css element entries
+ */
+ public function getCss(): array
+ {
+ return $this->css;
+ }
+
+ /**
+ * add one or many new css elements
+ * Note that we can chain: add/remove/reset
+ *
+ * @param string ...$css one or more css strings to add
+ * @return Element Current element for chaining
+ */
+ public function addCss(string ...$css): Element
+ {
+ // should do check for empty/invalid css
+ $_set_css = [];
+ foreach ($css as $_css) {
+ if (empty($_css)) {
+ Error::setError(
+ '204',
+ 'cannot have empty css string',
+ );
+ // TODO: shoud throw error
+ continue;
+ }
+ // -?[_A-Za-z][_A-Za-z0-9-]*
+ if (!preg_match("/^-?[_A-Za-z][_A-Za-z0-9-]*$/", $_css)) {
+ Error::setWarning(
+ '205',
+ 'possible invalid css string',
+ ['css' => $_css, 'id' => $this->id, 'tag' => $this->tag]
+ );
+ // TODO: shoud throw error
+ }
+ $_set_css[] = $_css;
+ }
+ $this->css = array_unique(array_merge($this->css, $_set_css));
+ return $this;
+ }
+
+ /**
+ * remove one or more css elements
+ * Note that we can chain: add/remove/reset
+ *
+ * @param string ...$css one or more css strings to remove
+ * @return Element Current element for chaining
+ */
+ public function removeCss(string ...$css): Element
+ {
+ $this->css = array_diff($this->css, $css);
+ return $this;
+ }
+
+ /**
+ * unset all css elements
+ * Note that we can chain: add/remove/reset
+ *
+ * @return Element
+ */
+ public function resetCss(): Element
+ {
+ $this->css = [];
+ return $this;
+ }
+
+ // build output from tree
+
+ /**
+ * build html string from the current element tree (self)
+ * or from the Element tree given as parameter
+ * if $add_nl is set then new lines are added before each sub element added
+ * no indet is done (tab or other)
+ *
+ * @param Element|null $tree Different Element tree to build
+ * if not set (null), self is used
+ * @param bool $add_nl [default=false] Optional output string line breaks
+ * @return string HTML as string
+ */
+ public function buildHtml(Element $tree = null, bool $add_nl = false): string
+ {
+ // print "D01: " . microtime(true) . " ";
+ if ($tree === null) {
+ $tree = $this;
+ }
+ $line = '<' . $tree->getTag();
+
+ if ($tree->getId()) {
+ $line .= ' id="' . $tree->getId() . '"';
+ if (in_array($tree->getTag(), Settings::NAME_ELEMENTS)) {
+ $line .= ' name="'
+ . (!empty($tree->getName()) ? $tree->getName() : $tree->getId())
+ . '"';
+ }
+ }
+ if (count($tree->getCss())) {
+ $line .= ' class="' . join(' ', $tree->getCss()) . '"';
+ }
+ foreach ($tree->getOptions() as $key => $item) {
+ // skip
+ if (in_array($key, Settings::SKIP_OPTIONS)) {
+ continue;
+ }
+ $line .= ' ' . $key . '="' . $item . '"';
+ }
+ $line .= '>';
+ if (strlen($tree->getContent()) > 0) {
+ $line .= $tree->getContent();
+ }
+ // sub nodes
+ foreach ($tree->getSub() as $sub) {
+ if ($add_nl === true) {
+ $line .= "\n";
+ }
+ $line .= $tree->buildHtml($sub, $add_nl);
+ if ($add_nl === true) {
+ $line .= "\n";
+ }
+ }
+
+ // close line if needed
+ if (!in_array($tree->getTag(), Settings::NO_CLOSE)) {
+ $line .= '' . $tree->getTag() . '>';
+ }
+
+ return $line;
+ }
+
+ // this is static
+
+ /**
+ * Builds a single string from an array of elements
+ * a new line can be added before each new element if $add_nl is set to true
+ *
+ * @param array $list array of Elements, uses buildHtml internal
+ * @param bool $add_nl [default=false] Optional output string line breaks
+ * @return string HTML as string
+ */
+ public static function buildHtmlFromList(array $list, bool $add_nl = false): string
+ {
+ $output = '';
+ foreach ($list as $el) {
+ if (!empty($output) && $add_nl === true) {
+ $output .= "\n";
+ }
+ $output .= $el->buildHtml();
+ }
+ return $output;
+ }
+
+ // so we can call builder statically
+
+ /**
+ * Search element tree for id and add
+ * if id is empty add at element given in parameter $base
+ *
+ * @param Element $base Element to attach to
+ * @param Element $attach Element to attach (single)
+ * @param string $id Optional id, if empty then attached at the end
+ * If set will loop through ALL sub elements until
+ * matching id found. if not found, not added
+ * @return Element Element with attached sub element
+ */
+ public static function addElementWithId(
+ Element $base,
+ Element $attach,
+ string $id = ''
+ ): Element {
+ // no id or matching id
+ if (
+ empty($id) ||
+ $base->getId() == $id
+ ) {
+ $base->addSub($attach);
+ return $base;
+ }
+ // find id in 'id' in all 'sub'
+ foreach ($base->getSub() as $el) {
+ self::addElementWithId($el, $attach, $id);
+ }
+
+ return $base;
+ }
+
+ /**
+ * add one or more elemens to $base
+ *
+ * @param Element $base Element to attach to
+ * @param Element ...$attach Element or Elements to attach
+ * @return Element Element with attached sub elements
+ */
+ public static function addElement(
+ Element $base,
+ Element ...$attach
+ ): Element {
+ // we must make sure we do not self attach
+ $base->addSub(...$attach);
+ return $base;
+ }
+
+ /**
+ * Static call version for building
+ * not recommended to be used, rather use "Element->buildHtml()"
+ * wrapper for buildHtml
+ *
+ * @param ?Element $tree Element tree to build
+ * if not set returns empty string
+ * @param bool $add_nl [default=false] Optional output string line break
+ * @return string build html as string
+ * @deprecated Do not use, use Element->buildHtml() instead
+ */
+ public static function printHtmlFromObject(Element $tree = null, bool $add_nl = false): string
+ {
+ // nothing ->bad
+ if ($tree === null) {
+ return '';
+ }
+ return $tree->buildHtml(add_nl: $add_nl);
+ }
+
+ /**
+ * Undocumented function
+ * wrapper for buildHtmlFromList
+ *
+ * @param array $list array of Elements to build string from
+ * @param bool $add_nl [default=false] Optional output string line break
+ * @return string build html as string
+ */
+ public static function printHtmlFromArray(array $list, bool $add_nl = false): string
+ {
+ return self::buildHtmlFromList($list, $add_nl);
+ }
+}
+
+// __END__
diff --git a/src/Template/HtmlBuilder/General/Error.php b/src/Template/HtmlBuilder/General/Error.php
new file mode 100644
index 0000000..be025f0
--- /dev/null
+++ b/src/Template/HtmlBuilder/General/Error.php
@@ -0,0 +1,127 @@
+}> */
+ private static array $messages = [];
+
+ /**
+ * internal writer for messages
+ *
+ * @param string $level
+ * @param string $id
+ * @param string $message
+ * @param array $context
+ * @return void
+ */
+ private static function writeContent(
+ string $level,
+ string $id,
+ string $message,
+ array $context
+ ): void {
+ self::$messages[] = [
+ 'level' => $level,
+ 'id' => $id,
+ 'message' => $message,
+ 'context' => $context,
+ ];
+ }
+
+ /**
+ * warning collector for all internal string errors
+ * builds an warning with warning id, message text and array with optional content
+ *
+ * @param string $id
+ * @param string $message
+ * @param array $context
+ * @return void
+ */
+ public static function setWarning(string $id, string $message, array $context = []): void
+ {
+ self::writeContent('Warning', $id, $message, $context);
+ }
+
+ /**
+ * error collector for all internal string errors
+ * builds an error with error id, message text and array with optional content
+ *
+ * @param string $id
+ * @param string $message
+ * @param array $context
+ * @return void
+ */
+ public static function setError(string $id, string $message, array $context = []): void
+ {
+ self::writeContent('Error', $id, $message, $context);
+ }
+
+ /**
+ * Return all set errors
+ *
+ * @return array
+ */
+ public static function getMessages(): array
+ {
+ return self::$messages;
+ }
+
+ /**
+ * Reset all errors
+ *
+ * @return void
+ */
+ public static function resetMessages(): void
+ {
+ self::$messages = [];
+ }
+
+ /**
+ * internal level in message array exists check
+ *
+ * @param string $level
+ * @return bool
+ */
+ private static function hasLevel(string $level): bool
+ {
+ return array_filter(
+ self::$messages,
+ function ($var) use ($level) {
+ return $var['level'] == $level ? true : false;
+ }
+ ) === [] ? false : true;
+ }
+
+ /**
+ * Check if any error is set
+ *
+ * @return bool
+ */
+ public static function hasError(): bool
+ {
+ return self::hasLevel('Error');
+ }
+
+ /**
+ * Check if any warning is set
+ *
+ * @return bool
+ */
+ public static function hasWarning(): bool
+ {
+ return self::hasLevel('Warning');
+ }
+}
+
+// __END__
diff --git a/src/Template/HtmlBuilder/General/HtmlBuilderExcpetion.php b/src/Template/HtmlBuilder/General/HtmlBuilderExcpetion.php
new file mode 100644
index 0000000..8fe8a3f
--- /dev/null
+++ b/src/Template/HtmlBuilder/General/HtmlBuilderExcpetion.php
@@ -0,0 +1,21 @@
+ list of html elements that can have the name tag */
+ public const NAME_ELEMENTS = [
+ 'button',
+ 'fieldset',
+ 'form',
+ 'iframe',
+ 'input',
+ 'map',
+ 'meta',
+ 'object',
+ 'output',
+ 'param',
+ 'select',
+ 'textarea',
+ ];
+
+ /** @var array options key entries to be skipped in build */
+ public const SKIP_OPTIONS = [
+ 'id',
+ 'name',
+ 'class',
+ ];
+
+ /** @var array html elements that don't need to be closed */
+ public const NO_CLOSE = [
+ 'input',
+ 'br',
+ 'img',
+ 'hr',
+ 'area',
+ 'col',
+ 'keygen',
+ 'wbr',
+ 'track',
+ 'source',
+ 'param',
+ 'command',
+ // only in header
+ 'base',
+ 'meta',
+ 'link',
+ 'embed',
+ ];
+
+ /** @var array invalid tags, not allowed in body */
+ public const NOT_IN_BODY_ALLOWED = [
+ 'base',
+ 'meta',
+ 'link',
+ 'embed', // not sure
+ ];
+}
+
+// __END__
diff --git a/src/Template/HtmlBuilder/StringReplace.php b/src/Template/HtmlBuilder/StringReplace.php
new file mode 100644
index 0000000..7a259d5
--- /dev/null
+++ b/src/Template/HtmlBuilder/StringReplace.php
@@ -0,0 +1,194 @@
+ */
+ private static array $elements = [];
+ /** @var array */
+ private static array $replace = [];
+
+ /**
+ * load html blocks into array for repeated usage
+ * each array group parameter has 0: index, 1: content
+ * There is no content check done.
+ * index must be non empty (but has no fixed format)
+ * if same index is tried twice it will set an error and skip
+ *
+ * @param array{0:string,1:string} ...$element Elements to load
+ * @return void
+ * @throws HtmlBuilderExcpetion
+ */
+ public static function loadElements(array ...$element): void
+ {
+ foreach ($element as $el) {
+ $index = $el[0] ?? '';
+ if (empty($index)) {
+ Error::setError(
+ '310',
+ 'Index cannot be an empty string',
+ [
+ 'element' => $index
+ ]
+ );
+ throw new HtmlBuilderExcpetion('Index cannot be an empty string');
+ }
+ if (isset(self::$elements[$index])) {
+ Error::setError(
+ '311',
+ 'Index already exists',
+ [
+ 'element' => $index
+ ]
+ );
+ throw new HtmlBuilderExcpetion('Index already exists: ' . $index);
+ }
+ // content check?
+ self::$elements[$index] = $el[1];
+ }
+ }
+
+ /**
+ * update an element at index
+ * can also be used to reset (empty string)
+ *
+ * @param string $index
+ * @param string $element
+ * @return void
+ */
+ public static function updateElement(string $index, string $element): void
+ {
+ if (!isset(self::$elements[$index])) {
+ Error::setError(
+ '312',
+ 'Index does not exists',
+ [
+ 'element' => $index
+ ]
+ );
+ throw new HtmlBuilderExcpetion('Index does not exists: ' . $index);
+ }
+ // allow empty reset
+ self::$elements[$index] = $element;
+ }
+
+ /**
+ * get an element block at index
+ * if not found will return false
+ *
+ * @param string $index
+ * @return string
+ * @throws HtmlBuilderExcpetion
+ */
+ public static function getElement(string $index): string
+ {
+ if (!isset(self::$elements[$index])) {
+ Error::setError('321', 'Index not found in elements', ['element' => $index]);
+ throw new HtmlBuilderExcpetion('Index not found in elements array: ' . $index);
+ }
+ return self::$elements[$index];
+ }
+
+ /**
+ * set a replacement block at index
+ * can be used for setting one block and using it agai
+ *
+ * @param string $index
+ * @param string $content
+ * @return void
+ */
+ public static function setReplaceBlock(string $index, string $content): void
+ {
+ self::$replace[$index] = $content;
+ }
+
+ /**
+ * get replacement block at index, if not found return empty and set error
+ *
+ * @param string $index
+ * @return string
+ * @throws HtmlBuilderExcpetion
+ */
+ public static function getReplaceBlock(string $index): string
+ {
+ if (!isset(self::$replace[$index])) {
+ Error::setError('331', 'Index not found in replace block', ['replace' => $index]);
+ throw new HtmlBuilderExcpetion('Index not found in replace block array: ' . $index);
+ }
+ return self::$replace[$index];
+ }
+
+ /**
+ * build and element on an index and either returns it or also sets it
+ * into the replace block array
+ * if index not found in relement list will return false
+ *
+ * @param string $index index of set element
+ * @param array $replace array of text to search (key) and replace (value) for
+ * @return string
+ * @throws HtmlBuilderExcpetion
+ */
+ public static function buildElement(
+ string $index,
+ array $replace,
+ string $replace_index = ''
+ ): string {
+ try {
+ self::getElement($index);
+ } catch (HtmlBuilderExcpetion $e) {
+ throw new HtmlBuilderExcpetion('Cannot fetch element with index: ' . $index, 0, $e);
+ }
+ if ($replace_index) {
+ self::setReplaceBlock(
+ $replace_index,
+ self::replaceData(self::$elements[$index], $replace)
+ );
+ return self::getReplaceBlock($replace_index);
+ } else {
+ return self::replaceData(self::$elements[$index], $replace);
+ }
+ }
+
+ /**
+ * main replace entries in text string
+ * elements to be replaced are in {} brackets. if they are missing in the
+ * replace array they will be added.
+ * if the replace and content count is not the same then an error will be thrown
+ *
+ * @param string $data
+ * @param array $replace
+ * @return string
+ * @throws HtmlBuilderExcpetion
+ */
+ public static function replaceData(string $data, array $replace): string
+ {
+ $to_replace = array_keys($replace);
+ // all replace data must have {} around
+ array_walk($to_replace, function (&$entry) {
+ if (!str_starts_with($entry, '{')) {
+ $entry = '{' . $entry;
+ }
+ if (!str_ends_with($entry, '}')) {
+ $entry .= '}';
+ }
+ // do some validation?
+ });
+ // replace content
+ return str_replace($to_replace, array_values($replace), $data);
+ }
+}
+
+// __END__
diff --git a/test/phpunit/Security/CoreLibsSecuritySymmetricEncryption.php b/test/phpunit/Security/CoreLibsSecuritySymmetricEncryptionTest.php
similarity index 98%
rename from test/phpunit/Security/CoreLibsSecuritySymmetricEncryption.php
rename to test/phpunit/Security/CoreLibsSecuritySymmetricEncryptionTest.php
index 9e2773d..33a2f45 100644
--- a/test/phpunit/Security/CoreLibsSecuritySymmetricEncryption.php
+++ b/test/phpunit/Security/CoreLibsSecuritySymmetricEncryptionTest.php
@@ -13,7 +13,7 @@ use CoreLibs\Security\SymmetricEncryption;
* @coversDefaultClass \CoreLibs\Security\SymmetricEncryption
* @testdox \CoreLibs\Security\SymmetricEncryption method tests
*/
-final class CoreLibsSecuritySymmetricEncryption extends TestCase
+final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
{
/**
* Undocumented function
diff --git a/test/phpunit/Template/CoreLibsTemplateHtmlBuilderBlockTest.php b/test/phpunit/Template/CoreLibsTemplateHtmlBuilderBlockTest.php
new file mode 100644
index 0000000..0350af9
--- /dev/null
+++ b/test/phpunit/Template/CoreLibsTemplateHtmlBuilderBlockTest.php
@@ -0,0 +1,34 @@
+ 'foo();']);
+ $this->assertEquals(
+ '
',
+ $el->buildHtml(),
+ 'nested build failed'
+ );
+ // this would create a loop, throws error
+ $this->expectException(HtmlBuilderExcpetion::class);
+ $this->expectExceptionMessage("Cannot assign Element to itself, this would create an infinite loop");
+ $el_sub->addSub($el_sub);
+ $this->assertEquals(
+ '',
+ $el_sub->buildHtml(),
+ 'loop detection failed'
+ );
+ $this->assertTrue(
+ Error::hasError(),
+ 'failed to throw error post loop detection'
+ );
+ $this->assertEquals(
+ [[
+ 'level' => 'Error',
+ 'id' => '100',
+ 'message' => 'Cannot assign Element to itself, this would create an infinite loop',
+ 'context' => ['tag' => 'div', 'id' => 'sub-1']
+ ]],
+ Error::getMessages(),
+ 'check error is 100 failed'
+ );
+ // get sub
+ $this->assertEquals(
+ [$el_sub],
+ $el->getSub(),
+ 'get sub failed'
+ );
+ // reset sub
+ $el->resetSub();
+ $this->assertEquals(
+ [],
+ $el->getSub(),
+ 'reset sub failed'
+ );
+ }
+
+ /**
+ * Undocumented function
+ *
+ * @testdox updated nested connection
+ *
+ * @return void
+ */
+ public function testNestedChangeContent(): void
+ {
+ $el = new Element('div', 'build-test');
+ $el_s_1 = new Element('div', 'sub-1');
+ $el_s_2 = new Element('div', 'sub-2');
+ $el_s_3 = new Element('div', 'sub-3');
+ $el_s_4 = new Element('div', 'sub-4');
+
+ $el->addSub($el_s_1, $el_s_2);
+ // only sub -1, -2
+ $this->assertEquals(
+ '
',
+ $el->buildHtml(),
+ 'check basic nested'
+ );
+
+ // now add -3, -4 to both -1 and -2
+ $el_s_1->addSub($el_s_3, $el_s_4);
+ $el_s_2->addSub($el_s_3, $el_s_4);
+ $this->assertEquals(
+ '
'
+ . '
'
+ . '
',
+ $el->buildHtml(),
+ 'check nested added'
+ );
+
+ // now add some css to el_s_3, will update in both sets
+ $el_s_3->addCss('red');
+ $this->assertEquals(
+ '