openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

Mustache.class.php (16215B)


      1 <?php
      2 
      3 
      4 namespace util;
      5 
      6 
      7 /**
      8  * Class Mustache.
      9  *
     10  * This is a simple Mustache template implementation,
     11  * See https://mustache.github.io/ for the specification.
     12  *
     13  * This implementation has the following advantages:
     14  * - no temporary files
     15  * - no require calls or includes
     16  * - no eval calls
     17  * - no weird ob_start calls
     18  *
     19  * But this implementation is probably slower than "Bog the cow's" implementation:
     20  * https://github.com/bobthecow/mustache.php
     21  *
     22  * But for some situations (i.e. for statifying CMS) this is not a problem ;)
     23  *
     24  * Features:
     25  * - Simple values
     26  * - Comments
     27  * - Blocks (normal and negating blocks)
     28  * - Lists with arrays and objects
     29  * - Wrapper functions
     30  * - Partials (you need to define a partial loader)
     31  * - Delimiter change
     32  * - no dot notation on property names
     33  *
     34  * Have a look for an example at the end of this file.
     35  *
     36  * Author: Jan Dankert
     37  * License: GPL v3.
     38  *
     39  * @package cms\mustache
     40  */
     41 class Mustache
     42 {
     43     /**
     44      * Open Tag.
     45      * @var string
     46      */
     47     public $openTag = '{{';
     48 
     49     /**
     50      * Closing tag.
     51      * @var string
     52      */
     53     public $closeTag = '}}';
     54 
     55     /**
     56      * Escape function, feel free to set it to a function of your own.
     57      * @var \Closure
     58      */
     59     public $escape;
     60 
     61     /**
     62      * Partial loader. If you want to use partials, you have to define a partial loader in this member.
     63      * @var \Closure
     64      */
     65     public $partialLoader;
     66 
     67     /**
     68      * Root block.
     69      * @var MustacheBlock
     70      */
     71     private $root;
     72 
     73 
     74     /**
     75      * Creates a new Mustache parser with a given source.
     76      *
     77      * @param $source
     78      */
     79     public function __construct( $source=null ) {
     80 
     81         $escape = static function( $text) {
     82 
     83          	if   ( is_scalar($text))
     84             	return htmlentities( $text);
     85          	else
     86          	 	return false;
     87         };
     88         $this->escape = $escape;
     89 
     90         if  ( $source )
     91             $this->parse($source);
     92     }
     93 
     94 
     95     /**
     96      * Render the template.
     97      *
     98      * @param $data data
     99      * @return string
    100      */
    101     public function render( $data ) {
    102 
    103         return $this->root->render( $data );
    104     }
    105 
    106     /**
    107      * Parsing the source.
    108      *
    109      * @param $source
    110      */
    111     public function parse($source)
    112     {
    113         $tagList = array();
    114         $pos = 0;
    115 
    116 	 	$nextOpenTag = $this->openTag;
    117 	 	$nextClosTag = $this->closeTag;
    118 
    119         while (true) {
    120 
    121          	$openTag = $nextOpenTag;
    122          	$closTag = $nextClosTag;
    123 
    124             // searching for:  {{#name}}
    125             //                 ^
    126             $begin = strpos($source, $openTag, $pos);
    127             if   ( $begin === FALSE )
    128                 break;
    129 
    130 			// searching for:  {{#name}}
    131 			//                        ^
    132             $end = strpos($source, $closTag, $begin + strlen($openTag));
    133             if   ( $end === FALSE )
    134                 break;
    135 
    136             // looking for: {{#name}}
    137             //                +---+
    138             $tagText = substr($source, $begin + strlen($openTag), $end - $begin - strlen($openTag));
    139 
    140             // Calculating line/column for error messages
    141             $line   = @substr_count($source, "\n", 0, $begin) + 1;
    142             $column = $begin - strrpos(substr($source, 0, $begin), "\n") + ($line==1?1:0);
    143 
    144             // Creating new Mustache tag.
    145             $tag = new MustacheTag($tagText, $begin, $end+strlen($closTag) , $line, $column);
    146 
    147             if ( $tag->type == MustacheTag::DELIM_CHANGE ) {
    148             	$nextDelimiters = explode(' ',$tag->propertyName);
    149 			 	if   ( sizeof($nextDelimiters ) >= 2 ) {
    150 			  		$nextOpenTag =        $nextDelimiters[0];
    151 			  		$nextClosTag = substr($nextDelimiters[1],0,-1);
    152 			 	}
    153 			 	// Removing tag from source
    154 			 	$source        = substr_replace($source,'',$begin,$end-$begin+strlen($closTag));
    155 			 	// Delimiter-Tag is not added to the taglist, we don't need it.
    156 				$pos = $begin;
    157 			}
    158             elseif ( $tag->type == MustacheTag::PARTIAL ) {
    159                 if   ( !is_callable($this->partialLoader) )
    160                     throw new \RuntimeException('No loader is defined, unable to inject a partial at '.$tag->__toString() );
    161 
    162                 $loader        = $this->partialLoader;
    163                 $partialSource = $loader( $tag->propertyName );
    164 				// Removing tag from source
    165                 $source        = substr_replace($source,$partialSource,$begin,$end-$begin+strlen($closTag));
    166                 // Partial-Tag is not added to the taglist, we don't need it.
    167 			 	$pos = $begin;
    168             }
    169             else {
    170                 // All other tags are added to our list.
    171                 $tagList[] = $tag;
    172 				$pos = $end + strlen($closTag);
    173             }
    174 
    175         }
    176 
    177         $this->parseStripTags( $source, $tagList );
    178     }
    179 
    180 
    181     /**
    182      * @param $source
    183      * @param $tagList
    184      */
    185     private function parseStripTags($source, $tagList)
    186     {
    187         $newTagList = array();
    188         $removedBytes = 0;
    189 
    190         /** @var MustacheTag $tag */
    191         foreach($tagList as $tag ) {
    192 
    193             $tagLength = $tag ->end - $tag->position;
    194 
    195             $tag->position -= $removedBytes;
    196             $tag->end = $tag->position;
    197 
    198             $source = substr_replace($source,'',$tag->position,$tagLength);
    199             $newTagList[] = $tag;
    200 
    201             $removedBytes += $tagLength;
    202         }
    203 
    204         $this->root = $this->parseBlock($newTagList,$source);
    205     }
    206 
    207     /**
    208      * @param $tagList
    209      * @param $source
    210      * @return MustacheBlock
    211      */
    212     public function parseBlock($tagList, $source ) {
    213 
    214         $block = new MustacheBlock(null);
    215         $indent = 0;
    216         $subtagList = array();
    217         $removedChars = 0;
    218         $openTag = null;
    219 
    220         /** @var MustacheTag $tag */
    221         foreach($tagList as $tag ) {
    222 
    223             if   ( $tag->kind == MustacheTag::OPEN ) {
    224                 if   ( $indent == 0 )
    225                     $openTag = $tag;
    226                 $indent++;
    227             }
    228             elseif   ( $tag->kind == MustacheTag::CLOSE ) {
    229                 $indent--;
    230                 if   ( $indent < 0) {
    231                     throw new \RuntimeException('Superfluous closing tag: '.$tag->__toString() );
    232                 }
    233                 if   ( $indent == 0 ) {
    234                     if   ( !empty($tag->propertyName) && $tag->propertyName != $openTag->propertyName )
    235                         throw new \RuntimeException('Start tag: '.$openTag->__toString().' does not match the closing tag: '.$tag->__toString() );
    236 
    237                     $subSourceLength = $tag->position-$openTag->position;
    238                     $subsource = substr($source,$openTag->position-$removedChars,$subSourceLength);
    239 
    240                     $source = substr_replace($source,'',$openTag->position-$removedChars,$subSourceLength);
    241                     $openTag->position -= $removedChars;
    242                     $removedChars += $subSourceLength;
    243 
    244                     // Append new subblock.
    245                     $subBlock = $this->parseBlock($subtagList,$subsource);
    246                     $subtagList = array(); // resetting...
    247                     $subBlock->tag = $openTag;
    248 
    249                     $block->nodes[] = $subBlock;
    250                 }
    251             } else {
    252                 if   ( $indent == 0) {
    253                     // This tag belongs to this block.
    254                     $tag->position -= $removedChars;
    255 
    256                     switch( $tag->type ) {
    257                         case MustacheTag::COMMENT;
    258                             $node = new MustacheComment($tag);
    259                             break;
    260                         case MustacheTag::UNESCAPED;
    261                             $node = new MustacheValue($tag);
    262                             $node->escape = null;
    263                             break;
    264                         case MustacheTag::VARIABLE;
    265                             $node = new MustacheValue($tag);
    266                             $node->escape = $this->escape;
    267                             break;
    268                         default:
    269                             throw new \RuntimeException('Unsupported tag: '.$tag->__toString() );
    270                     }
    271                     $block->nodes[] = $node;
    272 
    273                 } else {
    274                     // This is a tag of a subblock
    275                     $tag->position -= $openTag->position;
    276                     $subtagList[] = $tag;
    277                 }
    278             }
    279         }
    280 
    281         if   ( $indent > 0) {
    282             throw new \RuntimeException('Missing closing tag for: '.$openTag->__toString() );
    283         }
    284 
    285         $block->source = $source;
    286         return $block;
    287     }
    288 
    289 }
    290 
    291 class MustacheTag
    292 {
    293 	/**
    294 	 * The type of the tag.
    295 	 * for example '#' (for sections) or '' (for variables).
    296 	 * see the constants in this class.
    297 	 * @var string
    298 	 */
    299     public $type;
    300 
    301 
    302 	/**
    303 	 * the kind of the tag.
    304 	 * One of SIMPLE,OPEN,CLOSE
    305 	 * @var int
    306 	 */
    307     public $kind;
    308     public $propertyName;
    309 
    310     public $position;
    311     public $end;
    312 
    313     private $sourceLine;
    314     private $sourceColumn;
    315 
    316     const SIMPLE = 0;
    317     const OPEN = 1;
    318     const CLOSE = 2;
    319 
    320     const CLOSING = '/';
    321     const NEGATION = '^';
    322     const SECTION = '#';
    323     const COMMENT = '!';
    324     const PARTIAL = '>';
    325     const PARENT = '<';
    326     const DELIM_CHANGE = '='; // Changing delimiter
    327     const UNESCAPED_2 = '{';
    328     const UNESCAPED = '&';
    329     const PRAGMA = '%';
    330     const BLOCK_VAR = '$';
    331 
    332     const VARIABLE = '';
    333 
    334     private $VALID_TYPES = array(
    335         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
    336     );
    337 
    338 
    339 	/**
    340 	 * MustacheTag constructor.
    341 	 * @param $tagText string the tag, for example: "#name"
    342 	 * @param $position
    343 	 * @param $end
    344 	 * @param $line
    345 	 * @param $column
    346 	 */
    347     public function __construct($tagText, $position, $end, $line, $column)
    348     {
    349         $this->sourceLine   = $line;
    350         $this->sourceColumn = $column;
    351         $this->position     = $position;
    352         $this->end          = $end;
    353 
    354         $this->parseTag($tagText);
    355     }
    356 
    357     /**
    358      * Textual representation of a Mustache tag, suitable for error reporting.
    359      * @return string
    360      */
    361     public function __toString()
    362     {
    363         return 'tag "'.$this->type . $this->propertyName.'" (@ pos ' . $this->sourceLine . ':' . $this->sourceColumn . ') ';
    364     }
    365 
    366     private function parseTag($tagText)
    367     {
    368         $t = substr($tagText, 0, 1);
    369         if (in_array($t, $this->VALID_TYPES)) {
    370             $this->type = $t;
    371             $property = substr($tagText, 1);
    372             $this->propertyName = trim($property);
    373             if   ( $t == self::SECTION || $t == self::NEGATION )
    374                 $this->kind = self::OPEN;
    375             elseif   ( $t == self::CLOSING )
    376                 $this->kind = self::CLOSE;
    377             else
    378                 $this->kind = self::SIMPLE;
    379         } else {
    380             $this->type = self::VARIABLE;
    381             $this->propertyName = trim($tagText);
    382             $this->kind = self::SIMPLE;
    383         }
    384 
    385     }
    386 }
    387 
    388 class MustacheNode {
    389 
    390     public $type;
    391 
    392     /**
    393      * @var MustacheTag
    394      */
    395     public $tag;
    396 
    397     public function __construct( $tag )
    398     {
    399         $this->tag = $tag;
    400     }
    401 
    402     public function render( $data ) {
    403         return '';
    404     }
    405 
    406     public function getValue( $data ) {
    407 
    408         if  ( !is_object($this->tag))
    409             return false; // on root-block, there is no tag.
    410 
    411         $value = $data;
    412 
    413 		// Evaluate "dot notation"
    414 		foreach( explode('.',$this->tag->propertyName ) as $key )
    415 	  	{
    416 	   		if   ( is_array($value) && isset($value[$key]) ) {
    417 			 	$value = $value[$key];
    418 			 	continue;
    419 			}
    420 			$value = false; // Key does not exist, so there is no value.
    421 		}
    422 
    423 
    424 	 	if   ( is_object($value))
    425         {
    426             if   ($value instanceof \Closure)
    427                 ; // anonymous functions
    428             else
    429                 $value = get_object_vars($value);
    430         }
    431 
    432         return $value;
    433     }
    434 }
    435 
    436 
    437 class MustacheBlock extends MustacheNode {
    438 
    439     /**
    440      * @var String
    441      */
    442     public $source;
    443 
    444     /**
    445      * @var MustacheNode
    446      */
    447     public $nodes = array();
    448 
    449     /**
    450      * Should this block be rendered?
    451      *
    452      * @param $data data
    453      * @return bool
    454      */
    455     public function isRendered( $data ) {
    456 
    457         if  ( !is_object($this->tag))
    458             return true; // on root-block, there is no tag.
    459 
    460         $propIsTrue = (boolean) $this->getValue( $data );
    461 
    462         if  ( $this->tag->type == MustacheTag::NEGATION )
    463             $propIsTrue = ! $propIsTrue;
    464 
    465         return $propIsTrue;
    466     }
    467 
    468     public function render($data)
    469     {
    470         if ( $this->isRendered($data ) ) {
    471 
    472             $values = $this->getValue($data);
    473             if   ( !is_array($values) || !isset($values[0]) )
    474                 $values = array( $values);
    475 
    476             $sumOutput = '';
    477             foreach( $values as $value) {
    478 
    479                 $data = array_merge($data,(array) $value );
    480                 $output = $this->source;
    481                 $insertedBytes = 0;
    482 
    483 
    484                 /** @var MustacheNode $node */
    485                 foreach($this->nodes as $node) {
    486 
    487                     if   ( $node instanceof MustacheBlock )
    488                         $o = $node->render( $data);
    489                     else
    490                     {
    491                         $o = $node->render($data);
    492                         if   ( is_callable($value) )
    493                             $o = $value( $o );
    494                     }
    495 
    496                     $value = $this->getValue($data);
    497                     if   ( is_callable($value) )
    498                         $o = $value($o);
    499 
    500                     $output = substr_replace($output, $o, $node->tag->position+$insertedBytes, 0);
    501                     $insertedBytes += strlen($o);
    502                 }
    503                 $sumOutput .= $output;
    504             }
    505 
    506             return $sumOutput;
    507         }
    508         else {
    509             return '';
    510         }
    511 
    512     }
    513 }
    514 
    515 class MustacheValue extends MustacheNode {
    516 
    517     /**
    518      * escaping function.
    519      */
    520     public $escape;
    521 
    522     public function render($data)
    523     {
    524         $value = $this->getValue($data);
    525 
    526         if   ( is_callable( $this->escape)) {
    527 
    528             $escape = $this->escape;
    529             $value = $escape($value);
    530         }
    531 
    532         return $value;
    533     }
    534 }
    535 
    536 class MustacheComment extends MustacheNode {
    537 
    538     public function render($data)
    539     {
    540         return '';
    541     }
    542 }
    543 
    544 
    545 /*
    546  * Example.
    547  *
    548  * Uncomment the following for a working example.
    549  */
    550 
    551 /*
    552 error_reporting(E_ALL);
    553 ini_set('display_errors', 1);
    554 
    555 $source = <<<SRC
    556 Hello {{planet}}, {{& planet }}.{{! Simple example with a simple property }}
    557 
    558 {{#test}}
    559 Yes, this is a {{test}}. {{! yes, it is}}
    560 {{/test}}
    561 {{^test}}
    562 No, this is not a {{test}}. {{ ! will not be displayed, because test is not false }}
    563 {{/test}}
    564 
    565 {{#car}}
    566 My Car is {{color}}. {{! this is a property of the array car }}
    567 It drives on {{& planet }}.{{! this property is inherited from the upper context }}
    568 {{/}}
    569 
    570 {{#house}}
    571 My House is {{size}}. {{! this property is read from an object }}
    572 {{/}} {{! short closing tags are allowed }}
    573 
    574 Some names:
    575 {{#names}}
    576 my name is {{ name }}.{{! yes, spaces are allowed}}
    577 {{/names}}
    578 
    579 {{#empty}}
    580 this is not displayed {{! because the list is empty }}
    581 {{/empty}}
    582 
    583 {{#upper}}
    584 Hello again, {{planet}}. {{!displayed in uppercase}}
    585 {{/}}
    586 
    587 <h1>Partials</h1>
    588 {{> mycoolpartial}}
    589 
    590 <h1>Changing Delimiters</h1>
    591 Default: {{name}}
    592 {{=$( )=}}
    593 Bash-Style: $(name)
    594 Default should not work here: {{name}}
    595 
    596 $(={{ }}=)
    597 Default again: {{name}}
    598 
    599 <h1>Dot notation</h1>
    600 this will not work: {{building}}
    601 but this is the color of the roof: {{building.roof.color}}
    602 
    603 
    604 SRC;
    605 
    606 $m = new Mustache();
    607 $m->partialLoader = function($name) {
    608  return "\nThis is a partial named ".$name.". It may include variables, like the name '{{name}}'.\n\n";
    609 };
    610 $m->parse( $source );
    611 
    612 echo 'Object: <pre><code>'; print_r($m); echo '</code></pre>';
    613 
    614 $data = array(
    615     'planet'  => '<b>world</b>',
    616     'test'  => 'Test',
    617     'name'  => 'Mallory',
    618     'car'   => array('color'=>'red'),
    619     'house' => (object) array('size'=>'big' ),
    620     'names' => array(
    621         array('name'=>'Alice'),
    622         array('name'=>'Bob')
    623     ),
    624     'empty' => array(),
    625     'upper' => static function($text) { return strtoupper($text); },
    626     'building' => array('roof'=>array('color'=>'gray'))
    627 
    628 );
    629 
    630 echo '<pre>'.$m->render( $data ).'</pre>';
    631 */