From badbfb907fb36a58ce3e2217e5ee887a88a08da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 15 Jul 2020 14:16:17 +0200 Subject: [PATCH] Refactor Template and improve performance and type safety (#1349) * Refactor Template and improve performance and type safety * DEBUG: parse 100% same incl. tags/index, but del/set rely on ref * test - also not working * DEBUG: use old/working Template * Never delete, empty only * WIP parallel templating to verify new Template * FIX DIFF: it works, but "Content" should not be set set instead * Fix parallel rendering for Grid/direct template usage * fix CS * do not set unneeded empty strings * Revert "FIX DIFF: it works, but "Content" should not be set set instead" This reverts commit 1caaa4d03f73ec2db737ae1ca2dd65a4cff24b88. * Better fix: it works, but "Content" should not be set set instead * Revert debug * fix original typo * fix tryDel with array * Fix checkbox render properly --- docs/render.rst | 2 +- docs/template.rst | 4 +- src/Form/Control/Checkbox.php | 4 +- src/Form/Layout.php | 7 +- src/GridLayout.php | 8 +- src/Header.php | 1 + src/Modal.php | 4 +- src/ProgressBar.php | 5 +- src/Template.php | 581 +++++++----------- .../semantic-ui/form/control/checkbox.html | 4 +- .../semantic-ui/form/control/checkbox.pug | 5 +- tests/TemplateTest.php | 178 ++---- 12 files changed, 276 insertions(+), 527 deletions(-) diff --git a/docs/render.rst b/docs/render.rst index 008188252e..153f43c511 100644 --- a/docs/render.rst +++ b/docs/render.rst @@ -93,7 +93,7 @@ Agile UI sometimes uses the following approach to render element on the outside: 1. Create new instance of $sub_view. 2. Set $sub_view->id = false; 3. Calls $view->_add($sub_view); -4. executes $sub_view->renderHTML() +4. executes $sub_view->renderHtml() This returns a HTML that's stripped of any ID values, still linked to the main application but will not become part of the render tree. This approach is useful when it's necessary to manipulate HTML and inject it directly into the template for diff --git a/docs/template.rst b/docs/template.rst index 87b11227d9..2842fa652d 100644 --- a/docs/template.rst +++ b/docs/template.rst @@ -708,7 +708,7 @@ under ``$template->template`:: // template property: array ( 0 => 'Hello ', - 'subject#1' => array ( + 'subject#0' => array ( 0 => 'world', ), 1 => '!!', @@ -718,7 +718,7 @@ Property tags would contain:: array ( 'subject'=> array( &array ), - 'subject#1'=> array( &array ) + 'subject#0'=> array( &array ) ) As a result each tag will be stored under it's actual name and the name with diff --git a/src/Form/Control/Checkbox.php b/src/Form/Control/Checkbox.php index c4d090325b..dc8fd4a127 100644 --- a/src/Form/Control/Checkbox.php +++ b/src/Form/Control/Checkbox.php @@ -76,7 +76,9 @@ public function set($value = null, $junk = null) */ protected function renderView(): void { - $this->template['label'] = $this->label ?: $this->caption; + if ($this->label) { + $this->template->set('Content', $this->label); + } if ($this->field ? $this->field->get() : $this->content) { $this->template->set('checked', 'checked'); diff --git a/src/Form/Layout.php b/src/Form/Layout.php index 220b1caf66..81ae801f62 100644 --- a/src/Form/Layout.php +++ b/src/Form/Layout.php @@ -188,11 +188,6 @@ protected function recursiveRender(): void if ($element instanceof \atk4\ui\Form\Control\Checkbox) { $template = $noLabelControl; $element->template->set('Content', $label); - /* - $element->addClass('field'); - $this->template->appendHtml('Fields', '
'.$element->getHtml().'
'); - continue; - */ } if ($this->label && $this->inline) { @@ -204,7 +199,7 @@ protected function recursiveRender(): void $element->placeholder = $label; } - // Fields get extra pampering + // Controls get extra pampering $template->setHtml('Input', $element->getHtml()); $template->trySet('label', $label); $template->trySet('label_for', $element->id . '_input'); diff --git a/src/GridLayout.php b/src/GridLayout.php index 5781e7078f..803b97421a 100644 --- a/src/GridLayout.php +++ b/src/GridLayout.php @@ -91,9 +91,13 @@ protected function buildTemplate() $this->t_wrap->appendHtml('rows', '{/rows}'); $tmp = new Template($this->t_wrap->render()); - $this->template->template['rows#1'] = $tmp->template['rows#1']; + // TODO replace later, the only use of direct template property access + $t = $this; + \Closure::bind(function () use ($t, $tmp) { + $t->template->template['rows#0'] = $tmp->template['rows#0']; + $t->template->rebuildTagsIndex(); + }, null, Template::class)(); - $this->template->rebuildTags(); $this->addClass($this->words[$this->columns] . ' column'); } } diff --git a/src/Header.php b/src/Header.php index ca45ae307e..b39af64b5f 100644 --- a/src/Header.php +++ b/src/Header.php @@ -90,6 +90,7 @@ protected function renderView(): void if (!$this->icon && !$this->elements) { $this->template->del('has_content'); $this->template->set('title', $this->content); + $this->content = false; } parent::renderView(); diff --git a/src/Modal.php b/src/Modal.php index ec9e3610e9..3db99b4606 100644 --- a/src/Modal.php +++ b/src/Modal.php @@ -54,9 +54,9 @@ class Modal extends View */ public $contentCSS = ['img', 'content', 'atk-dialog-content']; - /* + /** * if true, the
at the bottom of the modal is - * shown. Automatically set to true if any actions are added + * shown. Automatically set to true if any actions are added. * * @var bool */ diff --git a/src/ProgressBar.php b/src/ProgressBar.php index 2bce4186a9..067a11ab03 100644 --- a/src/ProgressBar.php +++ b/src/ProgressBar.php @@ -7,10 +7,7 @@ /** * Class implements ProgressBar. * - * $bar = ProgressBar::addTo($app, [ - * 10, - * 'label' => 'Processing files', - * ]); + * $bar = ProgressBar::addTo($app, [10, 'label' => 'Processing files']); */ class ProgressBar extends View { diff --git a/src/Template.php b/src/Template.php index 78a2f6974e..4f062f37c1 100644 --- a/src/Template.php +++ b/src/Template.php @@ -18,36 +18,37 @@ class Template implements \ArrayAccess use \atk4\core\DiContainerTrait; // needed for StaticAddToTrait, removed once php7.2 support is dropped use \atk4\core\StaticAddToTrait; + /** @const string */ + public const TOP_TAG = '_top'; + /** @var array */ private static $_filesCache = []; + /** @var array */ + private static $_parseTemplateCache = []; // {{{ Properties of a template /** - * This array contains list of all tags found inside template implementing - * faster access when manipulating the template. + * This is a parsed contents of the template organized inside an array. This + * structure makes it very simple to modify any part of the array. * * @var array */ - public $tags = []; + private $template; /** - * This is a parsed contents of the template organized inside an array. This - * structure makes it very simple to modify any part of the array. + * List of all tags found inside template implementing faster access when manipulating the template. * * @var array */ - public $template = []; + private $tagsIndex; /** * Contains information about where the template was loaded from. * * @var string */ - public $source; - - /** @var string */ - public $default_exception = 'Exception_Template'; + protected $source; // }}} @@ -55,63 +56,27 @@ class Template implements \ArrayAccess // Template creation, interface functions - /** - * Construct template. - * - * @param string $template - */ - public function __construct($template = null) + public function __construct(string $template = '') { - if ($template !== null) { - $this->loadTemplateFromString($template); - } + $this->loadTemplateFromString($template); } - /** - * Clone template. - */ - public function __clone() + private function exceptionAddMoreInfo(Exception $e): Exception { - $this->template = unserialize(serialize($this->template)); + $e->addMoreInfo('tags', implode(', ', array_keys($this->tagsIndex))); + $e->addMoreInfo('template', $this->template); + $e->addMoreInfo('source', $this->source); - $this->tags = null; - $this->rebuildTags(); - } - - /** - * Returns relevant exception class. Use this method with "throw". - */ - public function exception($message = 'Undefined Exception', $code = 0): Exception - { - $ex = new Exception($message, $code); - $ex->addMoreInfo('tags', implode(', ', array_keys($this->tags))); - $ex->addMoreInfo('template', $this->template); - if ($this->source) { - $ex->addMoreInfo('source', $this->source); - } - - return $ex; + return $e; } // }}} // {{{ Tag manipulation - /** - * Returns true if specified tag is a top-tag of the template. - * - * Since Agile Toolkit 4.3 this tag is always called _top. - * - * @param string $tag - */ - public function isTopTag($tag): bool - { - return $tag === '_top'; - } - /** * This is a helper method which returns reference to element of template - * array referenced by a said tag. + * array referenced by a tag. * * Because there might be multiple tags and getTagRef is * returning only one template, it will return the first @@ -119,90 +84,65 @@ public function isTopTag($tag): bool * * {greeting}hello{/}, {greeting}world{/} * - * calling &getTagRef('greeting') will return reference to &array('hello'); - * - * @param string $tag + * calling &getTagRef('greeting') will return reference to &'hello'; * - * @return &array + * @param int|string|null $ref Null to return the first tag */ - public function &getTagRef($tag) + protected function &getTagRef(string $tag, $ref = null): array { - if ($this->isTopTag($tag)) { - return $this->template; - } + if ($ref !== null) { + if (!isset($this->tagsIndex[$tag][$ref])) { + throw $this->exceptionAddMoreInfo( + (new Exception('Tag not found in template')) + ->addMoreInfo('tag', $tag . '#' . $ref) + ); + } - $a = explode('#', $tag); - $tag = array_shift($a); - //$ref = array_shift($a); // unused - if (!isset($this->tags[$tag])) { - throw $this->exception('Tag not found in Template') - ->addMoreInfo('tag', $tag) - ->addMoreInfo('tags', implode(', ', array_keys($this->tags))); + $path = $this->tagsIndex[$tag][$ref]; + } else { + if ($tag === self::TOP_TAG) { + return $this->template; + } + + $tag = explode('#', $tag, 2)[0]; + if (!isset($this->tagsIndex[$tag])) { + throw $this->exceptionAddMoreInfo( + (new Exception('Tag not found in template')) + ->addMoreInfo('tag', $tag) + ); + } + + $path = reset($this->tagsIndex[$tag]); } - // return first array element only - reset($this->tags[$tag]); - $key = key($this->tags[$tag]) !== null ? key($this->tags[$tag]) : null; + $vRef = &$this->template; + foreach ($path as $k) { + $vRef = &$vRef[$k]; + } - return $this->tags[$tag][$key]; + return $vRef; } - /** - * For methods which execute action on several tags, this method - * will return array of templates. You can then iterate - * through the array and update all the template values. - * - * {greeting}hello{/}, {greeting}world{/} - * - * calling $template =& getTagRefList('greeting') will point - * $template towards array(&array('hello'),&array('world')); - * - * If $tag is specified as an array, then $template will - * contain all occurrences of all tags from the array. - * - * @param string|array $tag - * - * @return array of references to template tags - */ - public function getTagRefList($tag) + protected function getTagRefs(string $tag): array { - if (is_array($tag)) { - $res = []; - foreach ($tag as $t) { - $list = $this->getTagRefList($t); - foreach ($list as &$tpl) { - $res[] = &$tpl; - } - } - - return $res; - } - - if ($this->isTopTag($tag)) { + if ($tag === self::TOP_TAG) { return [&$this->template]; } - $a = explode('#', $tag); - $tag = array_shift($a); - $ref = array_shift($a); - if (!$ref) { - if (!isset($this->tags[$tag])) { - throw $this->exception('Tag not found in Template') + $tag = explode('#', $tag, 2)[0]; + if (!isset($this->tagsIndex[$tag])) { + throw $this->exceptionAddMoreInfo( + (new Exception('Tag not found in template')) ->addMoreInfo('tag', $tag) - ->addMoreInfo('tags', implode(', ', array_keys($this->tags))); - } - - return $this->tags[$tag]; + ); } - if (!isset($this->tags[$tag][$ref - 1])) { - throw $this->exception('Tag not found in Template') - ->addMoreInfo('tag', $tag) - ->addMoreInfo('tags', implode(', ', array_keys($this->tags))); + $vsRef = []; + foreach ($this->tagsIndex[$tag] as $ref => $ignore) { + $vsRef[$ref] = &$this->getTagRef($tag, $ref); } - //return [&$this->tags[$tag][$ref - 1]]; - return $this->tags[$tag][$ref - 1]; + return $vsRef; } /** @@ -224,43 +164,36 @@ public function hasTag($tag): bool return true; } - // check if tag exist - $a = explode('#', $tag); - $tag = array_shift($a); - //$ref = array_shift($a); // unused + $tag = explode('#', $tag, 2)[0]; - return isset($this->tags[$tag]) || $this->isTopTag($tag); + return isset($this->tagsIndex[$tag]) || $tag === self::TOP_TAG; } /** - * Re-create tag indexes from scratch for the whole template. + * Re-create tags index from scratch for the whole template. */ - public function rebuildTags() + protected function rebuildTagsIndex(): void { - $this->tags = []; - - $this->rebuildTagsRegion($this->template); + $this->tagsIndex = []; + $this->rebuildTagsIndexRegion([], $this->template); } - /** - * Add tags from a specified region. - * - * @param array $template - */ - protected function rebuildTagsRegion(&$template) + private function rebuildTagsIndexRegion(array $path, array $template): void { - foreach ($template as $tag => &$val) { + $path[] = null; + + foreach ($template as $tag => $val) { if (is_numeric($tag)) { continue; } - $a = explode('#', $tag); - $key = array_shift($a); - $ref = array_shift($a); + $path[array_key_last($path)] = $tag; + + [$tag, $ref] = explode('#', $tag, 2); - $this->tags[$key][$ref] = &$val; + $this->tagsIndex[$tag][$ref] = $path; if (is_array($val)) { - $this->rebuildTagsRegion($val); + $this->rebuildTagsIndexRegion($path, $val); } } } @@ -269,18 +202,27 @@ protected function rebuildTagsRegion(&$template) // {{{ Manipulating contents of tags + protected function _emptyRef(array &$ref): void + { + foreach ($ref as $k => $v) { + if (is_array($v)) { + $this->_emptyRef($ref[$k]); + } else { + unset($ref[$k]); + } + } + } + /** * Internal method for setting or appending content in $tag. * + * If tag contains another tags, these tags are set to empty values. + * * @param string|array|Model $tag * @param string $value * @param bool $encode Should we HTML encode content - * @param bool $append Should we append value instead of changing it? - * @param bool $strict Should we throw exception if tag not found? - * - * @return $this */ - protected function _setOrAppend($tag, $value = null, $encode = true, $append = false, $strict = true) + protected function _setOrAppend($tag, $value = null, $encode = true, $append = false, $throwIfNotFound = true): void { // check tag if ($tag instanceof Model) { @@ -294,18 +236,18 @@ protected function _setOrAppend($tag, $value = null, $encode = true, $append = f $this->_setOrAppend($t, $v, $encode, $append, false); } - return $this; + return; } if (!$tag) { - throw (new Exception('Tag is not set')) + throw (new Exception('Tag must not be empty')) ->addMoreInfo('tag', $tag) ->addMoreInfo('value', $value); } // check value if (!is_scalar($value) && $value !== null) { - throw (new Exception('Value should be scalar')) + throw (new Exception('Value must be scalar')) ->addMoreInfo('tag', $tag) ->addMoreInfo('value', $value); } @@ -317,27 +259,19 @@ protected function _setOrAppend($tag, $value = null, $encode = true, $append = f $value = htmlspecialchars($value, ENT_NOQUOTES, 'UTF-8'); } - // if no value, then set respective conditional regions to empty string - if (substr($tag, -1) !== '?' && ($value === false || !strlen((string) $value))) { - $this->trySet($tag . '?', ''); - } - // ignore not existent tags - if (!$strict && !$this->hasTag($tag)) { - return $this; + if (!$throwIfNotFound && !$this->hasTag($tag)) { + return; } // set or append value - $template = $this->getTagRefList($tag); + $template = $this->getTagRefs($tag); foreach ($template as &$ref) { - if ($append) { - $ref[] = $value; - } else { - $ref = [$value]; + if (!$append) { + $this->_emptyRef($ref); } + $ref[] = $value; } - - return $this; } /** @@ -362,7 +296,9 @@ protected function _setOrAppend($tag, $value = null, $encode = true, $append = f */ public function set($tag, $value = null, $encode = true) { - return $this->_setOrAppend($tag, $value, $encode, false, true); + $this->_setOrAppend($tag, $value, $encode, false, true); + + return $this; } /** @@ -376,7 +312,9 @@ public function set($tag, $value = null, $encode = true) */ public function trySet($tag, $value = null, bool $encode = true) { - return $this->_setOrAppend($tag, $value, $encode, false, false); + $this->_setOrAppend($tag, $value, $encode, false, false); + + return $this; } /** @@ -390,7 +328,9 @@ public function trySet($tag, $value = null, bool $encode = true) */ public function setHtml($tag, $value = null) { - return $this->_setOrAppend($tag, $value, false, false, true); + $this->_setOrAppend($tag, $value, false, false, true); + + return $this; } /** @@ -404,7 +344,9 @@ public function setHtml($tag, $value = null) */ public function trySetHtml($tag, $value = null) { - return $this->_setOrAppend($tag, $value, false, false, false); + $this->_setOrAppend($tag, $value, false, false, false); + + return $this; } /** @@ -417,7 +359,9 @@ public function trySetHtml($tag, $value = null) */ public function append($tag, $value, bool $encode = true) { - return $this->_setOrAppend($tag, $value, $encode, true, true); + $this->_setOrAppend($tag, $value, $encode, true, true); + + return $this; } /** @@ -431,7 +375,9 @@ public function append($tag, $value, bool $encode = true) */ public function tryAppend($tag, $value, bool $encode = true) { - return $this->_setOrAppend($tag, $value, $encode, true, false); + $this->_setOrAppend($tag, $value, $encode, true, false); + + return $this; } /** @@ -445,7 +391,9 @@ public function tryAppend($tag, $value, bool $encode = true) */ public function appendHtml($tag, $value) { - return $this->_setOrAppend($tag, $value, false, true, true); + $this->_setOrAppend($tag, $value, false, true, true); + + return $this; } /** @@ -459,20 +407,18 @@ public function appendHtml($tag, $value) */ public function tryAppendHtml($tag, $value) { - return $this->_setOrAppend($tag, $value, false, true, false); + $this->_setOrAppend($tag, $value, false, true, false); + + return $this; } /** * Get value of the tag. Note that this may contain an array * if tag contains a structure. - * - * @param string $tag - * - * @return array */ - public function get($tag) + public function get(string $tag): array { - return /*&*/$this->getTagRef($tag); // return array not referenced to it + return $this->getTagRef($tag); } /** @@ -491,21 +437,20 @@ public function del($tag) { if (is_array($tag)) { foreach ($tag as $t) { - $this->tryDel($t); + $this->del($t); } return $this; } - if ($this->isTopTag($tag)) { + if ($tag === self::TOP_TAG) { $this->loadTemplateFromString(''); - - return $this; - } - - $template = $this->getTagRefList($tag); - foreach ($template as &$ref) { - $ref = []; + } else { + $template = $this->getTagRefs($tag); + foreach ($template as &$ref) { + $ref = []; + } + $this->rebuildTagsIndex(); } return $this; @@ -521,7 +466,11 @@ public function del($tag) public function tryDel($tag) { if (is_array($tag)) { - return $this->del($tag); + foreach ($tag as $t) { + $this->tryDel($t); + } + + return $this; } return $this->hasTag($tag) ? $this->del($tag) : $this; @@ -558,16 +507,11 @@ public function offsetUnset($name) * Executes call-back for each matching tag in the template. * * @param string|array $tag - * @param callable $callable * * @return $this */ - public function eachTag($tag, $callable) + public function eachTag($tag, \Closure $callable) { - if (!$this->hasTag($tag)) { - return $this; - } - // array support if (is_array($tag)) { foreach ($tag as $t) { @@ -577,15 +521,8 @@ public function eachTag($tag, $callable) return $this; } - // $tag should be string here - $template = $this->getTagRefList($tag); - if ($template !== $this->template) { - foreach ($template as $key => $templ) { - $ref = $tag . '#' . ($key + 1); - $this->tags[$tag][$key] = [call_user_func($callable, $this->recursiveRender($templ), $ref)]; - } - } else { - $this->tags[$tag][0] = [call_user_func($callable, $this->recursiveRender($template), $tag)]; + foreach ($this->getTagRefs($tag) as $ref => &$vRef) { + $vRef = [(string) call_user_func($callable, $this->renderRegion($vRef), $tag . '#' . $ref)]; } return $this; @@ -594,24 +531,22 @@ public function eachTag($tag, $callable) /** * Creates a new template using portion of existing template. * - * @param string $tag - * - * @return self + * @return static */ - public function cloneRegion($tag) + public function cloneRegion(string $tag) { - if ($this->isTopTag($tag)) { - return clone $this; + $template = new static(); + $template->app = $this->app; + if ($tag === self::TOP_TAG) { + $template->template = $this->template; + $template->source = $this->source; + } else { + $template->template = [self::TOP_TAG . '#0' => $this->get($tag)]; + $template->source = 'clone of tag "' . $tag . '" from template "' . $this->source . '"'; } + $template->rebuildTagsIndex(); - $cl = static::class; - $n = new $cl(); - $n->app = $this->app; - $n->template = unserialize(serialize(['_top#1' => $this->get($tag)])); - $n->rebuildTags(); - $n->source = 'clone (' . $tag . ') of template ' . $this->source; - - return $n; + return $template; } // }}} @@ -621,14 +556,12 @@ public function cloneRegion($tag) /** * Loads template from a specified file. * - * @param string $filename Template file name - * * @return $this */ - public function load($filename) + public function load(string $filename) { - if ($t = $this->tryLoad($filename)) { - return $t; + if ($this->tryLoad($filename) !== false) { + return $this; } throw (new Exception('Unable to read template from file')) @@ -638,14 +571,13 @@ public function load($filename) /** * Same as load(), but will not throw exception. * - * @param string $filename Template file name - * * @return $this|false */ public function tryLoad(string $filename) { + $filename = realpath($filename); if (!isset(self::$_filesCache[$filename])) { - self::$_filesCache[$filename] = is_file($filename) ? file_get_contents($filename) : false; + self::$_filesCache[$filename] = $filename !== false ? file_get_contents($filename) : false; } if (self::$_filesCache[$filename] === false) { @@ -662,21 +594,14 @@ public function tryLoad(string $filename) /** * Initialize current template from the supplied string. * - * @param string $str - * * @return $this */ - public function loadTemplateFromString($str) + public function loadTemplateFromString(string $str) { $this->source = 'string: ' . $str; - $this->template = $this->tags = []; - if (!$str) { - return; - } - $this->tag_cnt = []; - - // First expand self-closing tags {$tag} -> {tag}{/tag} - $str = preg_replace('/{\$([-_:\w]+)}/', '{\1}{/\1}', $str); + $this->template = []; + $this->tagsIndex = []; + $this->tagCnt = []; $this->parseTemplate($str); @@ -689,112 +614,87 @@ public function loadTemplateFromString($str) /** * Used for adding unique tag alternatives. E.g. if your template has - * {$name}{$name}, then first would become 'name#1' and second 'name#2', but + * {$name}{$name}, then first would become 'name#0' and second 'name#1', but * both would still respond to 'name' tag. * * @var array */ - private $tag_cnt = []; + private $tagCnt = []; /** - * Register tags and return unique tag name. - * - * @param string $tag tag name - * - * @return string unique tag name + * Register tag and return unique tag name. */ - protected function regTag($tag) + protected function regTag(string $tag): string { - if (!isset($this->tag_cnt[$tag])) { - $this->tag_cnt[$tag] = 0; + if (!isset($this->tagCnt[$tag])) { + $this->tagCnt[$tag] = -1; } + $nextIndex = ++$this->tagCnt[$tag]; - return $tag . '#' . (++$this->tag_cnt[$tag]); + return $tag . '#' . $nextIndex; } - /** - * Recursively find nested tags inside a string, converting them to array. - * - * @param array &$input - * @param array &$template - */ - protected function parseTemplateRecursive(&$input, &$template) + protected function parseTemplateTree(array &$inputReversed, string $openedTag = null): array { - if (!is_array($input) || empty($input)) { - return; - } - - while (true) { - $tag = current($input); - next($input); - - if ($tag === false) { - break; - } + $prefix = array_pop($inputReversed); + $template = $prefix !== '' ? [$prefix] : []; + while (($tag = array_pop($inputReversed)) !== null) { $firstChar = substr($tag, 0, 1); + if ($firstChar === '/') { // is closing tag + $tag = substr($tag, 1); + if ($openedTag === null + || ($tag !== '' && $tag !== $openedTag)) { + throw (new Exception('Template parse error: tag was not opened')) + ->addMoreInfo('opened_tag', $openedTag) + ->addMoreInfo('tag', $tag); + } - switch ($firstChar) { - // is closing TAG - case '/': - return substr($tag, 1); - - break; - // is TAG - case '$': - - $tag = substr($tag, 1); - - $full_tag = $this->regTag($tag); - $template[$full_tag] = ''; // empty value - $this->tags[$tag][] = &$template[$full_tag]; - - // eat next chunk - $chunk = current($input); next($input); - if ($chunk !== false && $chunk !== null) { - $template[] = $chunk; - } + $openedTag = null; break; - // recurse - default: - - $full_tag = $this->regTag($tag); - - // next would be prefix - $prefix = current($input); next($input); - $template[$full_tag] = ($prefix === false || $prefix === null) ? [] : [$prefix]; - - $this->tags[$tag][] = &$template[$full_tag]; - - $this->parseTemplateRecursive($input, $template[$full_tag]); + } - $chunk = current($input); next($input); - if ($chunk !== false && !empty($chunk)) { - $template[] = $chunk; - } + // is new/opening tag + $fullTag = $this->regTag($tag); + $template[$fullTag] = $this->parseTemplateTree($inputReversed, $tag); - break; + $chunk = array_pop($inputReversed); + if ($chunk !== null && $chunk !== '') { + $template[] = $chunk; } } + + if ($openedTag !== null) { + throw (new Exception('Template parse error: tag is not closed')) + ->addMoreInfo('tag', $openedTag); + } + + return $template; } /** * Deploys parse recursion. - * - * @param string $str */ - protected function parseTemplate($str) + protected function parseTemplate(string $str): void { - $tag = '/{([\/$]?[-_:\w]*[\?]?)}/'; + $cKey = $str; + if (!isset(self::$_parseTemplateCache[$cKey])) { + // expand self-closing tags {$tag} -> {tag}{/tag} + $str = preg_replace('~\{\$([-_:\w]+)\}~', '{\1}{/\1}', $str); + + $input = preg_split('~\{(/?[-_:\w]*)\}~', $str, -1, PREG_SPLIT_DELIM_CAPTURE); + $inputReversed = array_reverse($input); // reverse to allow to use fast array_pop() - $input = preg_split($tag, $str, -1, PREG_SPLIT_DELIM_CAPTURE); + $this->template = $this->parseTemplateTree($inputReversed); + $this->rebuildTagsIndex(); - $prefix = current($input); - next($input); - $this->template = [$prefix]; + self::$_parseTemplateCache[$cKey] = [$this->template, $this->tagsIndex]; + $this->template = null; + $this->tagsIndex = null; + } - $this->parseTemplateRecursive($input, $this->template); + [$this->template, $this->tagsIndex] = self::$_parseTemplateCache[$cKey]; } // }}} @@ -804,72 +704,25 @@ protected function parseTemplate($str) /** * Render either a whole template or a specified region. Returns * current contents of a template. - * - * @param string $region - * - * @return string */ - public function render($region = null) + public function render(string $region = null): string { - if ($region) { - return $this->recursiveRender($this->get($region)); - } - - return $this->recursiveRender($this->template); + return $this->renderRegion($region !== null ? $this->get($region) : $this->template); } /** * Walk through the template array collecting the values * and returning them as a string. - * - * @param array $template - * - * @return string */ - protected function recursiveRender($template) + protected function renderRegion(array $template): string { - $output = ''; + $res = []; foreach ($template as $val) { - if (is_array($val)) { - $output .= $this->recursiveRender($val); - } else { - $output .= $val; - } - } - - return $output; - } - - // }}} - - // {{{ Debugging functions - - /* - * Returns HTML-formatted code with all tags - * - public function _getDumpTags($template) - { - $s = ''; - foreach ($template as $key => $val) { - if (is_array($val)) { - $s .= '{'.$key.'}'. - $this->_getDumpTags($val).'{/'.$key.'}'; - } else { - $s .= htmlspecialchars($val); - } + $res[] = is_array($val) ? $this->renderRegion($val) : $val; } - return $s; + return implode('', $res); } - /*** TO BE REFACTORED */ - /* - * Output all tags - * - public function dumpTags() - { - echo '"'.$this->_getDumpTags($this->template).'"'; - } - /*** TO BE REFACTORED */ // }}} } diff --git a/template/semantic-ui/form/control/checkbox.html b/template/semantic-ui/form/control/checkbox.html index f231f0ffe4..16d5c2d80b 100644 --- a/template/semantic-ui/form/control/checkbox.html +++ b/template/semantic-ui/form/control/checkbox.html @@ -1,5 +1,5 @@
- - + +
diff --git a/template/semantic-ui/form/control/checkbox.pug b/template/semantic-ui/form/control/checkbox.pug index 7be055aa4e..1a683d0a9c 100644 --- a/template/semantic-ui/form/control/checkbox.pug +++ b/template/semantic-ui/form/control/checkbox.pug @@ -1,5 +1,4 @@ |
-| -label {Content}{$label}{/} -| +| +| |
diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 0b755663ea..63b7e219dc 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -20,16 +20,6 @@ public function testBasicInit() $this->assertSame('hello, bar', $t->render()); } - /** - * Test isTopTag(). - */ - public function testIsTopTag() - { - $t = new \atk4\ui\Template('a{$foo}b'); - $this->assertTrue($t->isTopTag('_top')); - $this->assertFalse($t->isTopTag('foo')); - } - /** * Test getTagRef(). */ @@ -37,105 +27,19 @@ public function testGetTagRef() { // top tag $t = new \atk4\ui\Template('{foo}hello{/}, cruel {bar}world{/}. {foo}hello{/}'); - $t1 = &$t->getTagRef('_top'); - $this->assertSame(['', 'foo#1' => ['hello'], ', cruel ', 'bar#1' => ['world'], '. ', 'foo#2' => ['hello']], $t1); + $t1 = &$this->callProtected($t, 'getTagRef', '_top'); + $this->assertSame(['foo#0' => ['hello'], ', cruel ', 'bar#0' => ['world'], '. ', 'foo#1' => ['hello']], $t1); $t1 = ['good bye']; // will change $t->template because it's by reference - $this->assertSame(['good bye'], $t->template); + $this->assertSame(['good bye'], $this->getProtected($t, 'template')); // any tag $t = new \atk4\ui\Template('{foo}hello{/}, cruel {bar}world{/}. {foo}hello{/}'); - $t2 = &$t->getTagRef('foo'); + $t2 = &$this->callProtected($t, 'getTagRef', 'foo'); $this->assertSame(['hello'], $t2); $t2 = ['good bye']; // will change $t->template because it's by reference - $this->assertSame(['', 'foo#1' => ['good bye'], ', cruel ', 'bar#1' => ['world'], '. ', 'foo#2' => ['hello']], $t->template); - } - - /** - * Test conditional tag. - */ - public function testConditionalTags() - { - $s = 'My {email?}e-mail {$email}{/email?} {phone?}phone {$phone}{/?}. Contact me!'; - $t = new \atk4\ui\Template($s); - - $t1 = &$t->getTagRef('_top'); - $this->assertSame([ - 0 => 'My ', - 'email?#1' => [ - 0 => 'e-mail ', - 'email#1' => [''], - ], - 1 => ' ', - 'phone?#1' => [ - 0 => 'phone ', - 'phone#1' => [''], - ], - 2 => '. Contact me!', - ], $t1); - - // test filled values - $t = new \atk4\ui\Template($s); - $t->set('email', 'test@example.com'); - $t->set('phone', 123); - $this->assertSame('My e-mail test@example.com phone 123. Contact me!', $t->render()); - - $t = new \atk4\ui\Template($s); - $t->set('email', null); - $t->set('phone', 123); - $this->assertSame('My phone 123. Contact me!', $t->render()); - - $t = new \atk4\ui\Template($s); - $t->set('email', ''); - $t->set('phone', 123); - $this->assertSame('My phone 123. Contact me!', $t->render()); - - $t = new \atk4\ui\Template($s); - $t->set('email', false); - $t->set('phone', 0); - $this->assertSame('My phone 0. Contact me!', $t->render()); - - // nested conditional tags (renders comma only when both values are provided) - $s = 'My {email?}e-mail {$email}{/email?}{email?}{phone?}, {/?}{/?}{phone?}phone {$phone}{/?}. Contact me!'; - - $t = new \atk4\ui\Template($s); - $t->set('email', 'test@example.com'); - $t->set('phone', 123); - $this->assertSame('My e-mail test@example.com, phone 123. Contact me!', $t->render()); - - $t = new \atk4\ui\Template($s); - $t->set('email', null); - $t->set('phone', 123); - $this->assertSame('My phone 123. Contact me!', $t->render()); - - $t = new \atk4\ui\Template($s); - $t->set('email', false); - $t->set('phone', 0); - $this->assertSame('My phone 0. Contact me!', $t->render()); - } - - /** - * Conditional tag usage example - VAT usage. - */ - public function testConditionalTagsVat() - { - $s = '{vat_applied?}VAT is {$vat}%{/?}' . - '{vat_zero?}VAT is zero{/?}' . - '{vat_not_applied?}VAT is not applied{/?}'; - - $f = function ($vat) use ($s) { - return (new \atk4\ui\Template($s))->set([ - 'vat_applied' => !empty($vat), - 'vat_zero' => ($vat === 0), - 'vat_not_applied' => ($vat === null), - 'vat' => $vat, - ])->render(); - }; - - $this->assertSame('VAT is 21%', $f(21)); - $this->assertSame('VAT is zero', $f(0)); - $this->assertSame('VAT is not applied', $f(null)); + $this->assertSame(['foo#0' => ['good bye'], ', cruel ', 'bar#0' => ['world'], '. ', 'foo#1' => ['hello']], $this->getProtected($t, 'template')); } /** @@ -143,54 +47,50 @@ public function testConditionalTagsVat() */ public function testGetTagRefException() { - $this->expectException(Exception::class); $t = new \atk4\ui\Template('{foo}hello{/}'); - $t->getTagRef('bar'); // not existent tag + $this->expectException(Exception::class); + $this->callProtected($t, 'getTagRef', 'bar'); // not existent tag } /** - * Test getTagRefList(). + * Test getTagRefs(). */ - public function testGetTagRefList() + public function testGetTagRefs() { // top tag - $t = new \atk4\ui\Template('{foo}hello{/}, cruel {bar}world{/}. {foo}hello{/}'); - $t1 = $t->getTagRefList('_top'); - $this->assertSame([['', 'foo#1' => ['hello'], ', cruel ', 'bar#1' => ['world'], '. ', 'foo#2' => ['hello']]], $t1); + $t = new \atk4\ui\Template('{foo}hello{/}, cruel {bar}world{/}. {foo}hello2{/}'); + $t1 = $this->callProtected($t, 'getTagRefs', '_top'); + $this->assertSame([['foo#0' => ['hello'], ', cruel ', 'bar#0' => ['world'], '. ', 'foo#1' => ['hello2']]], $t1); $t1[0] = ['good bye']; // will change $t->template because it's by reference - $this->assertSame(['good bye'], $t->template); + $this->assertSame(['good bye'], $this->getProtected($t, 'template')); // any tag - $t = new \atk4\ui\Template('{foo}hello{/}, cruel {bar}world{/}. {foo}hello{/}'); - $t2 = $t->getTagRefList('foo'); - $this->assertSame([['hello'], ['hello']], $t2); - + $t = new \atk4\ui\Template('{foo}hello{/}, cruel {bar}world{/}. {foo}hello2{/}'); + $t2 = $this->callProtected($t, 'getTagRefs', 'foo'); + $this->assertSame([['hello'], ['hello2']], $t2); $t2[1] = ['good bye']; // will change $t->template last "foo" tag because it's by reference - $this->assertSame(['', 'foo#1' => ['hello'], ', cruel ', 'bar#1' => ['world'], '. ', 'foo#2' => ['good bye']], $t->template); - - // array of tags - $t = new \atk4\ui\Template('{foo}hello{/}, cruel {bar}world{/}. {foo}hello{/}'); - $t2 = $t->getTagRefList(['foo', 'bar']); - $this->assertSame([['hello'], ['hello'], ['world']], $t2); + $this->assertSame(['foo#0' => ['hello'], ', cruel ', 'bar#0' => ['world'], '. ', 'foo#1' => ['good bye']], $this->getProtected($t, 'template')); - $t2[1] = ['good bye']; // will change $t->template last "foo" tag because it's by reference - $t2[2] = ['planet']; // will change $t->template "bar" tag because it's by reference too - $this->assertSame(['', 'foo#1' => ['hello'], ', cruel ', 'bar#1' => ['planet'], '. ', 'foo#2' => ['good bye']], $t->template); + $t = new \atk4\ui\Template('{foo}hello{/}, cruel {bar}world{/}. {foo}hello2{/}'); + $t2 = $this->callProtected($t, 'getTagRefs', 'bar'); + $this->assertSame([['world']], $t2); + $t2[0] = ['planet']; // will change $t->template last "foo" tag because it's by reference + $this->assertSame(['foo#0' => ['hello'], ', cruel ', 'bar#0' => ['planet'], '. ', 'foo#1' => ['hello2']], $this->getProtected($t, 'template')); } /** - * Non existant template - throw exception. + * Non existent template - throw exception. */ public function testBadTemplate1() { - $this->expectException(Exception::class); $t = new \atk4\ui\Template(); + $this->expectException(Exception::class); $t->load('bad_template_file'); } /** - * Non existant template - no exception. + * Non existent template - no exception. */ public function testBadTemplate2() { @@ -199,13 +99,13 @@ public function testBadTemplate2() } /** - * Exception in getTagRefList(). + * Exception in getTagRefs(). */ - public function testGetTagRefListException() + public function testGetTagRefsException() { - $this->expectException(Exception::class); $t = new \atk4\ui\Template('{foo}hello{/}'); - $t->getTagRefList('bar'); // not existent tag + $this->expectException(Exception::class); + $this->callProtected($t, 'getTagRefs', 'bar'); // not existent tag } /** @@ -223,8 +123,8 @@ public function testHasTag() */ public function testSetException1() { - $this->expectException(Exception::class); $t = new \atk4\ui\Template('{foo}hello{/} guys'); + $this->expectException(Exception::class); $t->set('qwe', 'Hello'); // not existent tag } @@ -233,8 +133,8 @@ public function testSetException1() */ public function testSetException2() { - $this->expectException(Exception::class); $t = new \atk4\ui\Template('{foo}hello{/} guys'); + $this->expectException(Exception::class); $t->set('foo', new \StdClass()); // bad value } @@ -280,7 +180,7 @@ public function testArrayAccess() $this->assertTrue(isset($t['foo'])); $t['foo'] = 'Hi'; - $this->assertSame(['Hi'], $t['foo']); + $this->assertSame([1 => 'Hi'], $t['foo']); // 1 index instead of 0 because of https://bugs.php.net/bug.php?id=79844 unset($t['foo']); $this->assertSame([], $t['foo']); @@ -295,14 +195,12 @@ public function testEachTag() { $t = new \atk4\ui\Template('{foo}hello{/}, {how}cruel{/how} {bar}world{/}. {foo}welcome{/}'); - // don't throw exception if tag does not exist - $t->eachTag('ignore', function () { - }); - // replace values in these tags - $t->eachTag(['foo', 'bar'], function ($value, $tag) { - return strtoupper($value); - }); + foreach (['foo', 'bar'] as $tag) { + $t->eachTag($tag, function ($value, $fullTag) { + return strtoupper($value); + }); + } $this->assertSame('HELLO, cruel WORLD. WELCOME', $t->render()); // tag contains all template (for example in Lister) @@ -334,8 +232,8 @@ public function testClone() */ public function testLoadException() { - $this->expectException(Exception::class); $t = new \atk4\ui\Template(); + $this->expectException(Exception::class); $t->load('such-file-does-not-exist.txt'); }