commit 9a908b4b6a56f246633f26bcadec8301c558755f
Author: Jan Dankert <develop@jandankert.de>
Date: Mon, 27 Jan 2020 22:43:32 +0100
Exporting the mustache parser from OpenRat-CMS to this new workspace.
Diffstat:
Mustache.class.php | | | 608 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
Mustache.test.php | | | 89 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 697 insertions(+), 0 deletions(-)
diff --git a/Mustache.class.php b/Mustache.class.php
@@ -0,0 +1,607 @@
+<?php
+
+
+namespace cms\mustache;
+
+
+/**
+ * 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
+ * - Partials (you need to define a partial loader)
+ * - Delimiter change
+ * - no dot notation on property names
+ *
+ * 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) {
+
+ if ( is_scalar($text))
+ return htmlentities( $text);
+ else
+ return false;
+ };
+ $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;
+
+ $nextOpenTag = $this->openTag;
+ $nextClosTag = $this->closeTag;
+
+ while (true) {
+
+ $openTag = $nextOpenTag;
+ $closTag = $nextClosTag;
+
+ // searching for: {{#name}}
+ // +----->+
+ $begin = strpos($source, $openTag, $pos);
+ if ( $begin === FALSE )
+ break;
+
+ $end = strpos($source, $closTag, $begin + strlen($openTag));
+ if ( $end === FALSE )
+ break;
+
+ // Example {{#name}}
+ // Looking for +---+
+ $tagText = substr($source, $begin + strlen($openTag), $end - $begin - strlen($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($closTag) , $line, $column);
+
+ if ( $tag->type == MustacheTag::DELIM_CHANGE ) {
+ $parts = explode(' ',$tag->propertyName);
+ if ( sizeof($parts ) >= 2 ) {
+ $nextOpenTag = $parts[0];
+ $nextClosTag = substr($parts[1],0,-1);
+ }
+ $source = substr_replace($source,'',$begin,$end-$begin+strlen($closTag));
+ // Delimiter-Tag is not added to the taglist, we don't need it.
+ $pos = $begin;
+ }
+ elseif ( $tag->type == MustacheTag::PARTIAL ) {
+ if ( !is_callable($this->partialLoader) )
+ throw new \RuntimeException('No loader is defined, unable to inject a partial at '.$tag->__toString() );
+
+ $loader = $this->partialLoader;
+ $partialSource = $loader( $tag->propertyName );
+ $source = substr_replace($source,$partialSource,$begin,$end-$begin+strlen($closTag));
+ // Partial-Tag is not added to the taglist, we don't need it.
+ $pos = $begin;
+ }
+ else {
+ // All other tags are added to our list.
+ $tagList[] = $tag;
+ $pos = $end + strlen($closTag);
+ }
+
+
+ //$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 = '';
+
+ private $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, $this->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.
+
+ $value = $data;
+
+ // Evaluate "dot notation"
+ foreach( explode('.',$this->tag->propertyName ) as $key )
+ {
+ if ( is_array($value) && isset($value[$key]) ) {
+ $value = $value[$key];
+ continue;
+ }
+ $value = false; // Key does not exist, so there is no value.
+ }
+
+
+ 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 = array();
+
+ /**
+ * 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 = (boolean) $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}}
+{{/}}
+
+<h1>Partials</h1>
+{{> mycoolpartial}}
+
+<h1>Changing Delimiters</h1>
+Default: {{name}}
+{{=$( )=}}
+Bash-Style: $(name)
+Default should not work here: {{name}}
+
+$(={{ }}=)
+Default again: {{name}}
+
+<h1>Dot notation</h1>
+this will not work: {{building}}
+but this is the color of the roof: {{building.roof.color}}
+
+
+SRC;
+
+$m = new Mustache();
+$m->partialLoader = function($name) {
+ return "\nThis is a partial named ".$name.". It may include variables, like the name '{{name}}'.\n\n";
+};
+$m->parse( $source );
+
+echo 'Object: <pre><code>'; print_r($m); echo '</code></pre>';
+
+$data = array(
+ 'planet' => '<b>world</b>',
+ 'test' => 'Test',
+ 'name' => 'Mallory',
+ '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); },
+ 'building' => array('roof'=>array('color'=>'gray'))
+
+);
+
+echo '<pre>'.$m->render( $data ).'</pre>';
+*/+
\ No newline at end of file
diff --git a/Mustache.test.php b/Mustache.test.php
@@ -0,0 +1,89 @@
+<?php
+
+use cms\mustache\Mustache;
+
+require_once('./Mustache.class.php');
+
+
+error_reporting(E_ALL);
+ini_set('display_errors', 1);
+
+$source = <<<SRC
+Zahl: {{Zahl}}.{{Zahl}}.{{Zahl}}.
+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}}
+{{/}}
+
+<h1>Partials</h1>
+{{> mycoolpartial}}
+
+<h1>Changing Delimiters</h1>
+Default: {{name}}
+{{=$( )=}}
+Bash-Style: $(name)
+Default should not work here: {{name}}
+
+$(={{ }}=)
+Default again: {{name}}
+
+<h1>Dot notation</h1>
+this will not work: {{building}}
+but this is the color of the roof: {{building.roof.color}}
+
+
+SRC;
+
+$m = new Mustache();
+$m->partialLoader = function($name) {
+ //return '';
+ return "\n{{Zahl}}{{Zahl}}{{Zahl}} {{Zahl}} {{Zahl}}.This is a partial named ".$name.". It may include variables, like the name '{{name}}'.\n\n";
+};
+$m->parse( $source );
+
+echo 'Object: <pre><code>'; print_r($m); echo '</code></pre>';
+
+$data = array(
+ 'planet' => '<b>world</b>',
+ 'Zahl' => '112-000,00',
+ 'test' => 'Test',
+ 'name' => 'Mallory',
+ '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); },
+ 'building' => array('roof'=>array('color'=>'gray'))
+
+);
+
+echo '<pre>'.$m->render( $data ).'</pre>';