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 .= ''; + } + + 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 .= '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( + '
content
', + Block::buildHtml($el) + ); + } + + // ael + // aelx|addSub + // resetSub + // acssel/rcssel/scssel + // buildHtml + // buildHtmlFromList|printHtmlFromArray +} + +// __END__ diff --git a/test/phpunit/Template/CoreLibsTemplateHtmlBuilderElementTest.php b/test/phpunit/Template/CoreLibsTemplateHtmlBuilderElementTest.php new file mode 100644 index 0000000..7c96e32 --- /dev/null +++ b/test/phpunit/Template/CoreLibsTemplateHtmlBuilderElementTest.php @@ -0,0 +1,546 @@ + [ + 'tag' => 'div', + 'id' => 'id', + 'content' => 'content', + 'css' => ['css'], + 'options' => ['onclick' => 'foo();'], + 'expected' => '
content
' + ], + 'simple input' => [ + 'tag' => 'input', + 'id' => 'id', + 'content' => null, + 'css' => ['css'], + 'options' => ['name' => 'name', 'onclick' => 'foo();'], + 'expected' => '' + ] + ]; + } + + + /** + * Undocumented function + * + * @covers ::Element + * @covers ::buildHtml + * @covers ::getTag + * @covers ::getId + * @covers ::getContent + * @covers ::getOptions + * @covers ::getCss + * @dataProvider providerCreateElements + * @testdox create single new Element test [$_dataName] + * + * @param string $tag + * @param string|null $id + * @param string|null $content + * @param array|null $css + * @param array|null $options + * @param string $expected + * @return void + */ + public function testCreateElement( + string $tag, + ?string $id, + ?string $content, + ?array $css, + ?array $options, + string $expected + ): void { + $el = new Element($tag, $id ?? '', $content ?? '', $css ?? [], $options ?? []); + $this->assertEquals( + $expected, + $el->buildHtml(), + 'element creation failed' + ); + + $this->assertEquals( + $tag, + $el->getTag(), + 'get tag failed' + ); + + if ($id !== null) { + $this->assertEquals( + $id, + $el->getId(), + 'get id failed' + ); + } + if ($content !== null) { + $this->assertEquals( + $content, + $el->getContent(), + 'get content failed' + ); + } + if ($css !== null) { + $this->assertEquals( + $css, + $el->getCss(), + 'get css failed' + ); + } + if ($options !== null) { + $this->assertEquals( + $options, + $el->getOptions(), + 'get options failed' + ); + } + if (!empty($options['name'])) { + $this->assertEquals( + $options['name'], + $el->getName(), + 'get name failed' + ); + } + } + + /** + * css add/remove + * + * @cover ::getCss + * @cover ::addCss + * @cover ::removeCss + * @testdox test handling of adding and removing css classes + * + * @return void + */ + public function testCssHandling(): void + { + $el = new Element('div', 'css-test', 'CSS content'); + $this->assertEqualsCanonicalizing( + [], + $el->getCss(), + 'check empty css' + ); + $el->addCss('foo'); + $this->assertEqualsCanonicalizing( + ['foo'], + $el->getCss(), + 'check added one css' + ); + $el->removeCss('foo'); + $this->assertEqualsCanonicalizing( + [], + $el->getCss(), + 'check remove added css' + ); + // add serveral + // remove some of them + $el->addCss('a', 'b', 'c'); + $this->assertEqualsCanonicalizing( + ['a', 'b', 'c'], + $el->getCss(), + 'check added some css' + ); + $el->removeCss('a', 'c'); + // $this->assertArray + $this->assertEqualsCanonicalizing( + ['b'], + $el->getCss(), + 'check remove some css' + ); + // chained add and remove + $el->addCss('a', 'b', 'c', 'd')->removeCss('b', 'd'); + $this->assertEqualsCanonicalizing( + ['a', 'c'], + $el->getCss(), + 'check chain add remove some css' + ); + $el->resetCss(); + $this->assertEqualsCanonicalizing( + [], + $el->getCss(), + 'check reset css' + ); + // remove something that does not eixst + $el->addCss('exists'); + $el->removeCss('not'); + $this->assertEqualsCanonicalizing( + ['exists'], + $el->getCss(), + 'check remove not exitsing' + ); + } + + /** + * nested test + * + * @testdox nested test and loop assign detection + * + * @return void + */ + public function testBuildNested(): void + { + Error::resetMessages(); + $el = new Element('div', 'build-test'); + $el_sub = new Element('div', 'sub-1'); + $el->addSub($el_sub); + $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( + '
' + . '
' + . '
', + $el->buildHtml(), + 'check nested u@dated' + ); + } + + /** + * Undocumented function + * + * @testdox test change tag/id/content + * + * @return void + */ + public function testChangeElementData(): void + { + $el = new Element('div', 'id', 'Content'); + // content change + $this->assertEquals( + 'Content', + $el->getContent(), + 'set content' + ); + $el->setContent('New Content'); + $this->assertEquals( + 'New Content', + $el->getContent(), + 'changed content' + ); + + $this->assertEquals( + 'div', + $el->getTag(), + 'set tag' + ); + $el->setTag('span'); + $this->assertEquals( + 'span', + $el->getTag(), + 'changed tag' + ); + + $this->assertEquals( + 'id', + $el->getId(), + 'set id' + ); + $el->setId('id-2'); + $this->assertEquals( + 'id-2', + $el->getId(), + 'changed id' + ); + } + + /** + * Undocumented function + * + * @testdox test change options + * + * @return void + */ + public function testChangeOptions(): void + { + $el = new Element('button', 'id', 'Action', ['css'], ['value' => '3']); + $this->assertEquals( + ['value' => '3'], + $el->getOptions(), + 'set option' + ); + $el->setOptions([ + 'value' => '2' + ]); + $this->assertEquals( + ['value' => '2'], + $el->getOptions(), + 'changed option' + ); + // add a new one + $el->setOptions([ + 'Foo' => 'bar', + 'Moo' => 'cow' + ]); + $this->assertEquals( + [ + 'value' => '2', + 'Foo' => 'bar', + 'Moo' => 'cow' + ], + $el->getOptions(), + 'changed option' + ); + } + + // build output + // build from array list + + /** + * Undocumented function + * + * @testdox build element tree from object + * + * @return void + */ + public function testBuildHtmlObject(): void + { + // build a simple block + // div -> div -> button + // -> div -> span + // -> div -> input + $el = new Element('div', 'master', '', ['master']); + $el->addSub( + Element::addElement( + new Element('div', 'div-button', '', ['dv-bt']), + new Element('button', 'button-id', 'Click me', ['bt-red'], [ + 'OnClick' => 'action();', + 'value' => 'click', + 'type' => 'button' + ]), + ), + Element::addElement( + new Element('div', 'div-span', '', ['dv-sp']), + new Element('span', 'span-id', 'Big important message
other', ['red']), + ), + Element::addElement( + new Element('div', 'div-input', '', ['dv-in']), + new Element('input', 'input-id', '', ['in-blue'], [ + 'OnClick' => 'otherAction();', + 'value' => 'Touch', + 'type' => 'button' + ]), + ), + ); + $this->assertEquals( + '
' + . '
' + . '' + . '
' + . '
' + . 'Big important message
other
' + . '
' + . '
' + . '' + . '
' + . '
', + $el->buildHtml() + ); + } + + /** + * Undocumented function + * + * @testdox build elements from array list + * + * @return void + */ + public function testbuildHtmlArray(): void + { + $this->assertEquals( + '
A
' + . '
B
' + . '
C
', + Element::buildHtmlFromList([ + new Element('div', 'id-1', 'A'), + new Element('div', 'id-2', 'B'), + new Element('div', 'id-3', 'C'), + ]) + ); + } + + /** + * Undocumented function + * + * @testdox check for invalid tag detection, possible invalid id, possible invalid css + * + * @return void + */ + public function testInvalidElement(): void + { + Error::resetMessages(); + $this->expectException(HtmlBuilderExcpetion::class); + $this->expectExceptionMessage("Could not create Element"); + $el = new Element(''); + $this->assertTrue( + Error::hasError(), + 'failed to set error invalid tag detection' + ); + $this->assertEquals( + [[ + 'level' => 'Error', + 'id' => '201', + 'message' => 'invalid or empty tag', + 'context' => ['tag' => ''] + ]], + Error::getMessages(), + 'check error message failed' + ); + + // if we set invalid tag + $el = new Element('div'); + $this->expectException(HtmlBuilderExcpetion::class); + $this->expectExceptionMessageMatches("/^Invalid or empty tag: /"); + $this->expectExceptionMessage("Invalid or empty tag: 123123"); + $el->setTag('123123'); + $this->assertTrue( + Error::hasError(), + 'failed to set error invalid tag detection' + ); + $this->assertEquals( + [[ + 'level' => 'Error', + 'id' => '201', + 'message' => 'invalid or empty tag', + 'context' => ['tag' => ''] + ]], + Error::getMessages(), + 'check error message failed' + ); + + + // invalid id (warning) + Error::resetMessages(); + $el = new Element('div', '-$a15'); + $this->assertTrue( + Error::hasWarning(), + 'failed to set warning invalid id detection' + ); + $this->assertEquals( + [[ + 'level' => 'Warning', + 'id' => '202', + 'message' => 'possible invalid id', + 'context' => ['id' => '-$a15', 'tag' => 'div'] + ]], + Error::getMessages(), + 'check error message failed' + ); + + // invalid name + Error::resetMessages(); + $el = new Element('div', 'valid', '', [], ['name' => '-$asdf&']); + $this->assertTrue( + Error::hasWarning(), + 'failed to set warning invalid name detection' + ); + $this->assertEquals( + [[ + 'level' => 'Warning', + 'id' => '203', + 'message' => 'possible invalid name', + 'context' => ['name' => '-$asdf&', 'id' => 'valid', 'tag' => 'div'] + ]], + Error::getMessages(), + 'check error message failed' + ); + } + + // static add element + // print object/array +} + +// __END__ diff --git a/test/phpunit/Template/CoreLibsTemplateHtmlBuilderStringReplaceTest.php b/test/phpunit/Template/CoreLibsTemplateHtmlBuilderStringReplaceTest.php new file mode 100644 index 0000000..491d889 --- /dev/null +++ b/test/phpunit/Template/CoreLibsTemplateHtmlBuilderStringReplaceTest.php @@ -0,0 +1,65 @@ + + {CONTENT} + +HTML; + + $this->assertEquals( + << + Some content here
with bla bla inside + +HTML, + StringReplace::replaceData( + $html_block, + [ + 'ID' => 'block-id', + 'CSS' => join(',', ['blue', 'red']), + '{CONTENT}' => 'Some content here
with bla bla inside', + ] + ) + ); + } + + /** + * Undocumented function + * + * @testdox replaceData error + * + * @return void + */ + /* public function testReplaceDataErrors(): void + { + $this->expectException(HtmlBuilderExcpetion::class); + $this->expectExceptionMessage("Replace and content array count differ"); + StringReplace::replaceData('{FOO}', ['{FOO}', '{BAR}'], ['foo']); + } */ +} + +// __END__