openrat-cms

# OpenRat Content Management System
git clone http://git.code.weiherhei.de/openrat-cms.git
Log | Files | Refs

commit dc737712bb1fad0a7b850a012a37d26bcc41dd37
parent e76f4277ad1eaefcdf6420a23f3f3c37331ccc1a
Author: Jan Dankert <develop@jandankert.de>
Date:   Sat,  4 May 2019 03:01:17 +0200

Neuer Parser für Mustache-Templates. Benötigt keine temporären Dateien und kein require(). Dieser soll den bisherigen einfachen Template-Mechanismus ablösen.

Diffstat:
modules/util/Mustache.class.php | 555+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
modules/util/require.php | 1+
2 files changed, 556 insertions(+), 0 deletions(-)

diff --git a/modules/util/Mustache.class.php b/modules/util/Mustache.class.php @@ -0,0 +1,555 @@ +<?php + + +namespace cms\mustache; +use Twig\Extension\StringLoaderExtension; + + +/** + * Class Mustache. + * + * This is a simple Mustache template implementation, + * See https://mustache.github.io/ for the specification. + * + * This implementation has the following advantages: + * - no temporary files + * - no require calls or includes + * - no eval calls + * - no weird ob_start calls + * + * But this implementation is probably slower than "Bog the cow's" implementation: + * https://github.com/bobthecow/mustache.php + * + * But for some situations (i.e. for statifying CMS) this is not a problem ;) + * + * Features: + * - Simple values + * - Comments + * - Blocks (normal and negating blocks) + * - Lists with arrays and objects + * - Wrapper functions + * + * This implementation lacks a few features: + * - no Partials (No Partials ootb, you may define a partial loader) + * - no dot notation on property names + * - no Delimiter change (but the delimiters are public class members) + * + * Have a look for an example at the end of this file. + * + * Author: Jan Dankert + * License: GPL v3. + * + * @package cms\mustache + */ +class Mustache +{ + /** + * Open Tag. + * @var string + */ + public $openTag = '{{'; + + /** + * Closing tag. + * @var string + */ + public $closeTag = '}}'; + + /** + * Escape function, feel free to set it to a function of your own. + * @var \Closure + */ + public $escape; + + /** + * Partial loader. If you want to use partials, you have to define a partial loader in this member. + * @var \Closure + */ + public $partialLoader; + + /** + * Root block. + * @var MustacheBlock + */ + private $root; + + + /** + * Creates a new Mustache parser with a given source. + * + * @param $source + */ + public function __construct( $source=null ) { + + $escape = static function( $text) { + return htmlentities($text); + }; + $this->escape = $escape; + + if ( $source ) + $this->parse($source); + } + + + /** + * Render the template. + * + * @param $data data + * @return string + */ + public function render( $data ) { + + return $this->root->render( $data ); + } + + /** + * Parsing the source. + * + * @param $source + */ + public function parse($source) + { + $tagList = array(); + $pos = 0; + + while (true) { + + // searching for: {{#name}} + // +----->+ + $begin = strpos($source, $this->openTag, $pos); + if ( $begin === FALSE ) + break; + + $end = strpos($source, $this->closeTag, $begin + strlen($this->openTag)); + if ( $end === FALSE ) + break; + + // Example {{#name}} + // Looking for +---+ + $tagText = substr($source, $begin + strlen($this->openTag), $end - $begin - strlen($this->openTag)); + + $line = substr_count($source, "\n", 0, $begin) + 1; + $column = $begin - strrpos(substr($source, 0, $begin), "\n") + ($line==1?1:0); + + $tag = new MustacheTag($tagText, $begin, $end+strlen($this->closeTag) , $line, $column); + + if ( $tag->type == MustacheTag::PARTIAL ) { + if ( !is_callable($this->partialLoader) ) + throw new \RuntimeException('No loader is defined, unable to inject a partial at '.$tag->__toString() ); + + $partialSource = $this->partialLoader( $tag->propertyName ); + $source = substr_replace($source,$partialSource,$end+strlen($this->closeTag),0); + // Partial-Tag is not added to the taglist, we don't need it. + } + else { + // All other tags are added to our list. + $tagList[] = $tag; + } + + $pos = $end + strlen($this->closeTag); + + //$source = substr($source,0,$begin-1).substr($source,$end+1); + } + + //echo '<pre>'; echo var_dump($this); echo '</pre'; + + $this->parseStripTags( $source, $tagList ); + } + + + /** + * @param $source + * @param $tagList + */ + private function parseStripTags($source, $tagList) + { + $newTagList = array(); + $removedBytes = 0; + + /** @var MustacheTag $tag */ + foreach($tagList as $tag ) { + + $tagLength = $tag ->end - $tag->position; + + $tag->position -= $removedBytes; + $tag->end = $tag->position; + + $source = substr_replace($source,'',$tag->position,$tagLength); + $newTagList[] = $tag; + + $removedBytes += $tagLength; + } + + $this->root = $this->parseBlock($newTagList,$source); + } + + /** + * @param $tagList + * @param $source + * @return MustacheBlock + */ + public function parseBlock($tagList, $source ) { + + $block = new MustacheBlock(null); + $indent = 0; + $subtagList = array(); + $removedChars = 0; + $openTag = null; + + /** @var MustacheTag $tag */ + foreach($tagList as $tag ) { + + if ( $tag->kind == MustacheTag::OPEN ) { + if ( $indent == 0 ) + $openTag = $tag; + $indent++; + } + elseif ( $tag->kind == MustacheTag::CLOSE ) { + $indent--; + if ( $indent < 0) { + throw new \RuntimeException('Superfluous closing tag: '.$tag->__toString() ); + } + if ( $indent == 0 ) { + if ( !empty($tag->propertyName) && $tag->propertyName != $openTag->propertyName ) + throw new \RuntimeException('Start tag: '.$openTag->__toString().' does not match the closing tag: '.$tag->__toString() ); + + $subSourceLength = $tag->position-$openTag->position; + $subsource = substr($source,$openTag->position-$removedChars,$subSourceLength); + + $source = substr_replace($source,'',$openTag->position-$removedChars,$subSourceLength); + $openTag->position -= $removedChars; + $removedChars += $subSourceLength; + + // Append new subblock. + $subBlock = $this->parseBlock($subtagList,$subsource); + $subtagList = array(); // resetting... + $subBlock->tag = $openTag; + + $block->nodes[] = $subBlock; + } + } else { + if ( $indent == 0) { + // This tag belongs to this block. + $tag->position -= $removedChars; + + switch( $tag->type ) { + case MustacheTag::COMMENT; + $node = new MustacheComment($tag); + break; + case MustacheTag::UNESCAPED; + $node = new MustacheValue($tag); + $node->escape = null; + break; + case MustacheTag::VARIABLE; + $node = new MustacheValue($tag); + $node->escape = $this->escape; + break; + default: + throw new \RuntimeException('Unsupported tag: '.$tag->__toString() ); + } + $block->nodes[] = $node; + + } else { + // This is a tag of a subblock + $tag->position -= $openTag->position; + $subtagList[] = $tag; + } + } + } + + if ( $indent > 0) { + throw new \RuntimeException('Missing closing tag for: '.$openTag->__toString() ); + } + + $block->source = $source; + return $block; + } + +} + +class MustacheTag +{ + public $type; + public $kind; + public $propertyName; + + public $position; + public $end; + + private $sourceLine; + private $sourceColumn; + + const SIMPLE = 0; + const OPEN = 1; + const CLOSE = 2; + + const CLOSING = '/'; + const NEGATION = '^'; + const SECTION = '#'; + const COMMENT = '!'; + const PARTIAL = '>'; + const PARENT = '<'; + const DELIM_CHANGE = '='; + const UNESCAPED_2 = '{'; + const UNESCAPED = '&'; + const PRAGMA = '%'; + const BLOCK_VAR = '$'; + + const VARIABLE = ''; + const VALID_TYPES = array( + self::CLOSING, self::NEGATION, self::SECTION, self::COMMENT, self::PARTIAL, self::PARENT, self::DELIM_CHANGE, self::UNESCAPED_2, self::UNESCAPED, self::PRAGMA, self::BLOCK_VAR + ); + + public function __construct($tagText, $position, $end, $line, $column) + { + $this->sourceLine = $line; + $this->sourceColumn = $column; + $this->position = $position; + $this->end = $end; + + $this->parseTag($tagText); + } + + /** + * Textual representation of a Mustache tag, suitable for error reporting. + * @return string + */ + public function __toString() + { + return 'tag "'.$this->type . $this->propertyName.'" (@ pos ' . $this->sourceLine . ':' . $this->sourceColumn . ') '; + } + + private function parseTag($tagText) + { + $t = substr($tagText, 0, 1); + if (in_array($t, self::VALID_TYPES)) { + $this->type = $t; + $property = substr($tagText, 1); + $this->propertyName = trim($property); + if ( $t == self::SECTION || $t == self::NEGATION ) + $this->kind = self::OPEN; + elseif ( $t == self::CLOSING ) + $this->kind = self::CLOSE; + else + $this->kind = self::SIMPLE; + } else { + $this->type = self::VARIABLE; + $this->propertyName = trim($tagText); + $this->kind = self::SIMPLE; + } + + } +} + +class MustacheNode { + + public $type; + + /** + * @var MustacheTag + */ + public $tag; + + public function __construct( $tag ) + { + $this->tag = $tag; + } + + public function render( $data ) { + return ''; + } + + public function getValue( $data ) { + + if ( !is_object($this->tag)) + return false; // on root-block, there is no tag. + + if ( !isset($data[$this->tag->propertyName]) ) + return false; + $value = $data[$this->tag->propertyName]; + if ( is_object($value)) + { + if ($value instanceof \Closure) + ; // anonymous functions + else + $value = get_object_vars($value); + } + + return $value; + } +} + + +class MustacheBlock extends MustacheNode { + + /** + * @var String + */ + public $source; + + /** + * @var MustacheNode + */ + public $nodes; + + /** + * Should this block be rendered? + * + * @param $data data + * @return bool + */ + public function isRendered( $data ) { + + if ( !is_object($this->tag)) + return true; // on root-block, there is no tag. + + $propIsTrue = boolval( $this->getValue( $data ) ); + + if ( $this->tag->type == MustacheTag::NEGATION ) + $propIsTrue = ! $propIsTrue; + + return $propIsTrue; + } + + public function render($data) + { + if ( $this->isRendered($data ) ) { + + $values = $this->getValue($data); + if ( !is_array($values) || !isset($values[0]) ) + $values = array( $values); + + $sumOutput = ''; + foreach( $values as $value) { + + $data = array_merge($data,(array) $value ); + $output = $this->source; + $insertedBytes = 0; + + + /** @var MustacheNode $node */ + foreach($this->nodes as $node) { + + if ( $node instanceof MustacheBlock ) + $o = $node->render( $data); + else + { + $o = $node->render($data); + if ( is_callable($value) ) + $o = $value( $o ); + } + + $value = $this->getValue($data); + if ( is_callable($value) ) + $o = $value($o); + + $output = substr_replace($output, $o, $node->tag->position+$insertedBytes, 0); + $insertedBytes += strlen($o); + } + $sumOutput .= $output; + } + + return $sumOutput; + } + else { + return ''; + } + + } +} + +class MustacheValue extends MustacheNode { + + /** + * escaping function. + */ + public $escape; + + public function render($data) + { + $value = $this->getValue($data); + + if ( is_callable( $this->escape)) { + + $escape = $this->escape; + $value = $escape($value); + } + + return $value; + } +} + +class MustacheComment extends MustacheNode { + + public function render($data) + { + return ''; + } +} + + +/* + * Example. + * + * Uncomment the following for a working example. + */ + +/* +error_reporting(E_ALL); +ini_set('display_errors', 1); + +$source = <<<SRC +Hello {{planet}}, {{& planet }}.{{! Simple example with a simple property }} + +{{#test}} +Yes, this is a {{test}}. {{! yes, it is}} +{{/test}} +{{^test}} +No, this is not a {{test}}. {{ ! will not be displayed, because test is not false }} +{{/test}} + +{{#car}} +My Car is {{color}}. {{! this is a property of the array car }} +It drives on {{& planet }}.{{! this property is inherited from the upper context }} +{{/}} + +{{#house}} +My House is {{size}}. {{! this property is read from an object }} +{{/}} {{! short closing tags are allowed }} + +Some names: +{{#names}} +my name is {{ name }}.{{! yes, spaces are allowed}} +{{/names}} + +{{#empty}} +this is not displayed {{! because the list is empty }} +{{/empty}} + +{{#upper}} +Hello again, {{planet}}. {{!displayed in uppercase}} +{{/}} + + + +SRC; + +$m = new Mustache( $source ); + +echo 'Object: <pre><code>'; print_r($m); echo '</code></pre>'; + +$data = array( + 'planet' => '<b>world</b>', + 'test' => 'Test', + 'car' => array('color'=>'red'), + 'house' => (object) array('size'=>'big' ), + 'names' => array( + array('name'=>'Alice'), + array('name'=>'Bob') + ), + 'empty' => array(), + 'upper' => static function($text) { return strtoupper($text); } + +); + +echo '<pre>'.$m->render( $data ).'</pre>'; diff --git a/modules/util/require.php b/modules/util/require.php @@ -33,4 +33,5 @@ require_once( __DIR__.'/'.'Upload.class.php' ); require_once( __DIR__.'/'.'ArchiveTar.class.php' ); require_once( __DIR__.'/'.'ArchiveUnzip.class.php' ); require_once( __DIR__.'/'.'ArchiveZip.class.php' ); +require_once( __DIR__.'/'.'Mustache.class.php' );