commit 4338bc7124e13db09a36212553359416efde3e8b
parent f39b4850f26efe0989b1d3ad14acbb9924de6718
Author: Jan Dankert <develop@jandankert.de>
Date: Thu, 20 Feb 2020 23:51:34 +0100
Refactoring of the template compiler: The Templates are at first converted into a component tree and then into a element tree which is then rendered.
Diffstat:
12 files changed, 200 insertions(+), 137 deletions(-)
diff --git a/modules/template-engine/Element.class.php b/modules/template-engine/Element.class.php
@@ -14,28 +14,30 @@ class Element
*/
protected $attributes = [];
protected $content = '';
- protected $selfClosing = true;
+
+ protected $selfClosing = false;
/**
* @var array
*/
- private $parents = [];
-
- private $children = [];
+ protected $children = [];
/**
* @param $wrapperElement Element
* @return $this
+ * @deprecated
*/
public function addWrapper($wrapperElement ) {
- $wrapperElement->selfClosing(false);
- $this->parents[] = $wrapperElement;
return $this;
}
public function addChild($child ) {
- $this->selfClosing(false );
- $this->children[] = $child;
+
+ if ( is_array( $child ) )
+ $this->children += $child;
+ else
+ $this->children[] = $child;
+
return $this;
}
@@ -54,39 +56,26 @@ class Element
return $this;
}
- public function getBegin() {
+ public function render() {
- $content = array_reduce( array_reverse($this->parents), function($carry,$item) {
- //$content = '';
- //foreach( $item->children as $child) {
- // $content .= $child->getBegin().$child->getEnd();
- //}
- return $carry.$item->getBegin();
- }, '' );
+ $this->selfClosing = $this->selfClosing && !$this->content && !$this->children;
+
+ $content = '';
if ( $this->name )
$content .= '<'.$this->name.
- array_reduce( array_keys($this->attributes),function($carry,$key){return $carry.' '.$this->getAttributeValue($key);},'').(($this->selfClosing && !$this->content && !$this->children)?' /':'').'>';
+ array_reduce( array_keys($this->attributes),function($carry,$key){return $carry.' '.$this->getAttributeValue($key);},'').(($this->selfClosing ?' /':'').'>');
$content .= $this->getContent();
- return $content;
- }
-
-
- public function getEnd() {
+ $content .= $this->renderChildren();
- $content = '';
- if ( $this->selfClosing && !$this->content && !$this->children)
+ if ( $this->selfClosing )
;
else
if ( $this->name )
$content .= '</'.$this->name.'>';
- $content .= array_reduce( $this->parents, function($carry,$item) {
- return $carry.$item->getEnd();
- }, '' );
-
return $content;
}
@@ -104,6 +93,18 @@ class Element
{
return $this->content;
}
+
+ protected function renderChildren()
+ {
+ $content = '';
+
+ /** @var Element $child */
+ foreach($this->children as $child ) {
+ $content .= $child->render();
+ }
+
+ return $content;
+ }
}
diff --git a/modules/template-engine/EmptyElement.class.php b/modules/template-engine/EmptyElement.class.php
@@ -0,0 +1,23 @@
+<?php
+
+
+namespace modules\template_engine;
+
+
+use cms\template_engine\SimpleAttribute;
+
+
+/**
+ * An empty element.
+ *
+ * 'empty' means, the element has no tag and no text content. But it may (and should) contain child elements.
+ *
+ * @package modules\template_engine
+ */
+class EmptyElement extends Element
+{
+ public function __construct()
+ {
+ parent::__construct(null);
+ }
+}
diff --git a/modules/template-engine/HtmlElement.class.php b/modules/template-engine/HtmlElement.class.php
@@ -33,16 +33,15 @@ class HtmlElement extends Element
return parent::getAttributeValue(htmlspecialchars($name));
}
- public function getBegin()
- {
- $this->addAttribute('class', implode(' ',$this->styleClasses) );
- return parent::getBegin();
- }
-
public function __construct( $name )
{
parent::__construct( $name );
+
+ // Only "void elements" are self-closable, see
+ // https://html.spec.whatwg.org/multipage/syntax.html#void-elements
+ if ( in_array($name,['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']))
+ $this->selfClosing = true;
}
@@ -54,4 +53,11 @@ class HtmlElement extends Element
}
+ public function render()
+ {
+ $this->addAttribute('class', implode(' ',$this->styleClasses) );
+
+ return parent::render();
+ }
+
}
\ No newline at end of file
diff --git a/modules/template-engine/PHPBlockElement.class.php b/modules/template-engine/PHPBlockElement.class.php
@@ -15,15 +15,17 @@ class PHPBlockElement extends HtmlElement
}
- public function getBegin()
+ public function render()
{
- return '<?php '.$this->beforeBlock.' { '.$this->inBlock.' ?>';
- }
+ $content = '';
+ $content .= '<?php '.$this->beforeBlock.' { '.$this->inBlock.' ?>';
- public function getEnd()
- {
- return ' <?php } ?>';
+ $content .= $this->renderChildren();
+
+ $content .= ' <?php } ?>';
+
+ return $content;
}
diff --git a/modules/template-engine/TemplateCompiler.php b/modules/template-engine/TemplateCompiler.php
@@ -40,6 +40,9 @@ foreach(FileUtils::readDir( $dir ) as $action )
$outFile = $dir.'/'.$action.'/'.$method.'.php';
$engine = new TemplateEngine();
+
+ // We are creating a fake request, because the template compiler needs to know
+ // the action and subaction in which it will be executed.
$fakeRequest = new \cms\action\RequestParams();
$fakeRequest->action = $action;
$fakeRequest->method = $method;
diff --git a/modules/template-engine/components/html/Component.class.php b/modules/template-engine/components/html/Component.class.php
@@ -7,9 +7,17 @@ use modules\template_engine\Element;
abstract class Component
{
-
+ private $childComponents = [];
private $depth;
+ /**
+ * @param $component Component
+ */
+ public function addChildComponent($component ) {
+ if ( $component )
+ $this->childComponents[] = $component;
+ }
+
/**
* @var RequestParams
*/
@@ -20,6 +28,11 @@ abstract class Component
*/
private $element;
+ /**
+ * @var Element
+ */
+ protected $adoptiveElement;
+
public function __construct()
{
}
@@ -38,33 +51,31 @@ abstract class Component
return null;
}
-
- /**
- * Gets the beginning of this component.
- * @return string
- */
- public function getBegin()
- {
- return $this->element->getBegin();
- }
-
- public function getEnd()
- {
- return $this->element->getEnd();
- }
-
public function init()
{
$this->element = $this->createElement();
if ( $this->element)
$this->element->selfClosing(false);
+
+ // if there is no special adoptive element, lets use the root element.
+ if ( ! $this->adoptiveElement )
+ $this->adoptiveElement = $this->element;
}
-
-}
-
+ /**
+ * Gets the element with all child elements from all child components.
+ *
+ * @return Element
+ */
+ public function getElement()
+ {
+ /** @var Component $childComponent */
+ foreach ($this->childComponents as $childComponent )
+ $this->adoptiveElement->addChild( $childComponent->getElement() );
+ return $this->element;
+ }
-?>-
\ No newline at end of file
+}
diff --git a/modules/template-engine/components/html/NativeHtmlComponent.class.php b/modules/template-engine/components/html/NativeHtmlComponent.class.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace template_engine\components;
+
+use modules\template_engine\HtmlElement;
+
+class NativeHtmlComponent extends HtmlComponent
+{
+ public $attributes;
+ public $tag;
+
+ public function createElement()
+ {
+ $html = new HtmlElement( $this->tag );
+
+ foreach ($this->attributes as $attribute)
+ $html->addAttribute( $attribute->name,$attribute->value );
+
+ return $html;
+ }
+}
diff --git a/modules/template-engine/components/html/form/Form.class.php b/modules/template-engine/components/html/form/Form.class.php
@@ -99,7 +99,7 @@ class FormComponent extends Component
(new CMSElement('input'))
->addAttribute('type', 'hidden')
->addAttribute('name', REQ_PARAM_TOKEN)
- ->addAttribute('value', '<?php token();') // TODO escaping
+ ->addAttribute('value', '<?php token();?>') // TODO escaping
);
$form->addChild(
diff --git a/modules/template-engine/components/html/input/Input.class.php b/modules/template-engine/components/html/input/Input.class.php
@@ -22,6 +22,7 @@ class InputComponent extends FieldComponent
public $size;
+ public $minlength = 0;
public $maxlength = 256;
public $onchange;
@@ -50,7 +51,8 @@ class InputComponent extends FieldComponent
}
$input->addAttribute('name',$this->name);
- $input->addAttribute('disabled',$this->readonly);
+ if ( $this->readonly )
+ $input->addAttribute('disabled','disabled');
if ( $this->required )
$input->addAttribute( 'required','required');
@@ -65,6 +67,9 @@ class InputComponent extends FieldComponent
$input->addAttribute('type',$this->type);
$input->addAttribute('maxlength',$this->maxlength);
+ if ( $this->minlength )
+ $input->addAttribute('minlength',$this->minlength);
+
if ( $this->class )
$input->addStyleClass($this->class);
diff --git a/modules/template-engine/components/html/table/Table.class.php b/modules/template-engine/components/html/table/Table.class.php
@@ -25,18 +25,20 @@ class TableComponent extends HtmlComponent
$tableWrapper->addChild($filter);
}
- $tableContent = (new HtmlElement('div'))->addStyleClass('or-table-area');
+ $tableContent = (new HtmlElement('div'))->addStyleClass('or-table-area');
$table = new CMSElement('table');
- if ( $this->class)
+ $tableWrapper->addChild( $tableContent->addChild( $table) );
+
+ if ( $this->class)
$table->addStyleClass($this->class);
if ( $this->width)
$table->addAttribute('width',$this->width);
- $table->addWrapper($tableContent)->addWrapper($tableWrapper);
+ $this->adoptiveElement = $table;
- return $table;
+ return $tableWrapper;
}
}
\ No newline at end of file
diff --git a/modules/template-engine/engine/TemplateEngine.class.php b/modules/template-engine/engine/TemplateEngine.class.php
@@ -7,19 +7,24 @@ use DOMDocument;
use DOMElement;
use Exception;
use LogicException;
+use modules\template_engine\EmptyElement;
+use modules\template_engine\HtmlElement;
+use modules\template_engine\PHPBlockElement;
use SimpleXMLElement;
use \template_engine\components\Component;
+use template_engine\components\NativeHtmlComponent;
/**
* Wandelt eine Vorlage in ein PHP-Skript um.
- *
+ *
* Die Vorlage wird gesparst, Elemente werden geladen und in die Zieldatei kopiert.
- *
+ *
* @author Jan Dankert
* @package openrat.services
*/
class TemplateEngine
{
+ // For now we are only supporting HTML rendering.
public $renderType = 'html';
public $config = array();
@@ -52,42 +57,46 @@ class TemplateEngine
// Imports the base class of all component types.
require_once (dirname(__FILE__).'/../components/'.$this->renderType.'/Component.class.' . PHP_EXT);
require_once (dirname(__FILE__).'/../components/'.$this->renderType.'/HtmlComponent.class.' . PHP_EXT);
+ require_once (dirname(__FILE__).'/../components/'.$this->renderType.'/NativeHtmlComponent.class.' . PHP_EXT);
require_once (dirname(__FILE__).'/../components/'.$this->renderType.'/FieldComponent.class.' . PHP_EXT);
+ // We are now building a complete DOM tree and this is the root element.
+ $rootElement = new PHPBlockElement();
+
+ // The generated template should only be executable in our CMS environment (for security reasons).
+ $rootElement->beforeBlock = 'if (!defined(\'OR_TITLE\')) die(\'Forbidden\');';
+
try
{
$confCompiler = $this->config;
-
+
if (is_file($srcXmlFilename))
$srcFilename = $srcXmlFilename;
else
// Wenn Vorlage (noch) nicht existiert
throw new LogicException("Template not found: $srcXmlFilename");
-
+
+ // Vorlage und Zieldatei oeffnen
+ $document = $this->loadDocument($srcFilename);
+
+ // creating a tree of components
+ $rootComponent = $this->processElement( $document->documentElement );
+
+ // converting the component tree to a element tree
+ $rootElement->addChild( $rootComponent->getElement() );
+
$filename = $tplOutName;
-
- // Wenn Vorlage gaendert wurde, dann Umwandlung erneut ausf�hren.
- if (false && is_file($filename) && filemtime($srcFilename) <= filemtime($filename))
- return;
-
+
if (is_file($filename) && ! is_writable($filename))
throw new LogicException("Template output file is read-only: $filename");
-
- // Vorlage und Zieldatei oeffnen
- $document = $this->loadDocument($srcFilename);
-
- $outFile = @fopen($filename, 'w');
- fwrite($outFile, '<?php if (!defined(\'OR_TITLE\')) die(\'Forbidden\'); ?>');
- if (! is_resource($outFile))
- throw new LogicException("Template '$srcXmlFilename': Unable to open file for writing: '$filename'");
-
- $this->processElement( $document->documentElement, $outFile );
+ $writtenBytes = file_put_contents( $filename, $rootElement->render() );
+
+ if ( $writtenBytes === FALSE )
+ throw new LogicException("Unable writing to output file: '$filename'");
- fclose($outFile);
-
// CHMOD ausfuehren.
- if (! empty($confCompiler['chmod']))
+ if ( @$confCompiler['chmod'] )
if (! @chmod($filename, octdec($confCompiler['chmod'])))
throw new \InvalidArgumentException("Template {$srcXmlFilename} failed to compile: CHMOD '{$confCompiler['chmod']}' failed on file {$filename}.");
}
@@ -101,29 +110,28 @@ class TemplateEngine
/**
* @param DOMElement $element
- * @param resource $outFile
* @param int $depth
*/
- private function processElement( $element, $outFile, $depth = 0 ) {
+ private function processElement( $element, $depth = 0 ) {
// Only process DOM Elements (ignoring Text, Comments, ...)
if ( $element->nodeType == XML_ELEMENT_NODE )
;
else
- return;
+ return null;
// The namespace decides what to do with this element:
if ( $element->namespaceURI == 'http://www.openrat.de/template')
- $this->processCMSElement( $element, $outFile, $depth );
+ return $this->processCMSElement( $element, $depth );
elseif ( $element->namespaceURI == 'http://www.w3.org/1999/xhtml')
- $this->processHTMLElement( $element, $outFile, $depth );
+ return $this->processHTMLElement( $element, $depth );
else
throw new LogicException("Unknown Element ".$element->tagName.' in NS '.$element->namespaceURI );
}
- private function processCMSElement(DOMElement $element, $outFile, $depth)
+ private function processCMSElement(DOMElement $element, $depth)
{
$attributes = $element->attributes;
$tag = $element->localName;
@@ -166,52 +174,30 @@ class TemplateEngine
$component->$attributeName = $attributeValue;
}
- $output = $component->init();
- $output = $component->getBegin();
- if($this->debug) $output = '<!-- Begin '.$tag.' -->'.$output;
-
- if ($output) {
- $prepend = ($depth>=0?"\n":'').str_repeat("\t",$depth);
- fwrite($outFile, $prepend.$output);
- }
+ $component->init();
foreach( $element->childNodes as $child ) {
- $this->processElement($child,$outFile,$depth+1);
- }
-
- $output = $component->getEnd();
- if($this->debug) $output = $output.'<!-- End '.$tag.' -->';
- if ( $output ) {
- $prepend = "\n".str_repeat("\t",$depth);
- fwrite($outFile, $prepend.$output);
+ $component->addChildComponent( $this->processElement( $child,$depth ) );
}
-
+ return $component;
}
-
- private function processHTMLElement($element, $outFile, $depth)
+ /**
+ * Creates a new HTML element.
+ * @param $element DOMElement
+ * @param $depth
+ * @return NativeHtmlComponent
+ */
+ private function processHTMLElement($element, $depth)
{
- $attributes = $element->attributes;
- $tag = $element->localName;
-
- $out = '<'.$tag;
- foreach ($attributes as $attribute)
- $out .= ' '.$attribute->name.'="'.$attribute->value.'"';
+ $component = new NativeHtmlComponent();
- $out .= '>';
-
- $prepend = ($depth>=0?"\n":'').str_repeat("\t",$depth);
- fwrite($outFile, $prepend.$out);
-
- foreach( $element->childNodes as $child ) {
- $this->processElement($child,$outFile,$depth+1);
- }
-
- $out = '</'.$tag.'>';
- $prepend = "\n".str_repeat("\t",$depth);
- fwrite($outFile, $prepend.$out);
+ $component->tag = $element->localName;
+ $component->attributes = $element->attributes;
+ $component->init();
+ return $component;
}
/**
@@ -264,20 +250,23 @@ class TemplateEngine
} catch (Exception $e) {
throw new DomainException("Compilation failed for Template '$srcFile'.", 0, $e);
}
- #header("X-CMS-Template-File: " . $templateFile);
+
}
- // Spätestens jetzt muss das Template vorhanden sein.
+ // Spätestens jetzt muss das Template vorhanden sein.
if (!is_file($templateFile))
throw new LogicException("Template file '$templateFile' was not found.");
- // Übertragen der Ausgabe-Variablen in den aktuellen Kontext
+ if ( DEVELOPMENT )
+ // save a few bytes in production mode ;)
+ header("X-CMS-Template-File: " . $templateFile);
+
+ // Übertragen der Ausgabe-Variablen in den aktuellen Kontext
//
extract($outputData);
- // Einbinden des Templates
+ // Include the template
require_once($templateFile);
-
}
}
diff --git a/modules/template-engine/require.php b/modules/template-engine/require.php
@@ -5,6 +5,7 @@ include( dirname(__FILE__) . '/engine/TemplateEngine.class.php');
include( dirname(__FILE__) . '/Element.class.php');
include( dirname(__FILE__) . '/HtmlElement.class.php');
include( dirname(__FILE__) . '/CMSElement.class.php');
+include( dirname(__FILE__) . '/EmptyElement.class.php');
include( dirname(__FILE__) . '/PHPBlockElement.class.php');
include( dirname(__FILE__) . '/SimpleAttribute.php');
include( dirname(__FILE__) . '/ConditionalAttribute.php');