mustache

Unnamed repository; edit this file 'description' to name the repository.
git clone http://git.code.weiherhei.de/mustache.git
Log | Files | Refs

Mustache.class.php (15666B)


      1 <?php
      2 
      3 
      4 namespace cms\mustache;
      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             $end = strpos($source, $closTag, $begin + strlen($openTag));
    131             if   ( $end === FALSE )
    132                 break;
    133 
    134             // Example     {{#name}}
    135             // Looking for   +---+
    136             $tagText = substr($source, $begin + strlen($openTag), $end - $begin - strlen($openTag));
    137 
    138             $line = substr_count($source, "\n", 0, $begin) + 1;
    139             $column = $begin - strrpos(substr($source, 0, $begin), "\n") + ($line==1?1:0);
    140 
    141             $tag = new MustacheTag($tagText, $begin, $end+strlen($closTag) , $line, $column);
    142 
    143             if ( $tag->type == MustacheTag::DELIM_CHANGE ) {
    144             	$parts = explode(' ',$tag->propertyName);
    145 			 	if   ( sizeof($parts ) >= 2 ) {
    146 			  		$nextOpenTag =        $parts[0];
    147 			  		$nextClosTag = substr($parts[1],0,-1);
    148 			 	}
    149 			 	$source        = substr_replace($source,'',$begin,$end-$begin+strlen($closTag));
    150 			 	// Delimiter-Tag is not added to the taglist, we don't need it.
    151 				$pos = $begin;
    152 			}
    153             elseif ( $tag->type == MustacheTag::PARTIAL ) {
    154                 if   ( !is_callable($this->partialLoader) )
    155                     throw new \RuntimeException('No loader is defined, unable to inject a partial at '.$tag->__toString() );
    156 
    157                 $loader        = $this->partialLoader;
    158                 $partialSource = $loader( $tag->propertyName );
    159                 $source        = substr_replace($source,$partialSource,$begin,$end-$begin+strlen($closTag));
    160                 // Partial-Tag is not added to the taglist, we don't need it.
    161 			 	$pos = $begin;
    162             }
    163             else {
    164                 // All other tags are added to our list.
    165                 $tagList[] = $tag;
    166 				$pos = $end + strlen($closTag);
    167             }
    168 
    169 
    170             //$source = substr($source,0,$begin-1).substr($source,$end+1);
    171         }
    172 
    173         //echo '<pre>'; echo var_dump($this); echo '</pre';
    174 
    175         $this->parseStripTags( $source, $tagList );
    176     }
    177 
    178 
    179     /**
    180      * @param $source
    181      * @param $tagList
    182      */
    183     private function parseStripTags($source, $tagList)
    184     {
    185         $newTagList = array();
    186         $removedBytes = 0;
    187 
    188         /** @var MustacheTag $tag */
    189         foreach($tagList as $tag ) {
    190 
    191             $tagLength = $tag ->end - $tag->position;
    192 
    193             $tag->position -= $removedBytes;
    194             $tag->end = $tag->position;
    195 
    196             $source = substr_replace($source,'',$tag->position,$tagLength);
    197             $newTagList[] = $tag;
    198 
    199             $removedBytes += $tagLength;
    200         }
    201 
    202         $this->root = $this->parseBlock($newTagList,$source);
    203     }
    204 
    205     /**
    206      * @param $tagList
    207      * @param $source
    208      * @return MustacheBlock
    209      */
    210     public function parseBlock($tagList, $source ) {
    211 
    212         $block = new MustacheBlock(null);
    213         $indent = 0;
    214         $subtagList = array();
    215         $removedChars = 0;
    216         $openTag = null;
    217 
    218         /** @var MustacheTag $tag */
    219         foreach($tagList as $tag ) {
    220 
    221             if   ( $tag->kind == MustacheTag::OPEN ) {
    222                 if   ( $indent == 0 )
    223                     $openTag = $tag;
    224                 $indent++;
    225             }
    226             elseif   ( $tag->kind == MustacheTag::CLOSE ) {
    227                 $indent--;
    228                 if   ( $indent < 0) {
    229                     throw new \RuntimeException('Superfluous closing tag: '.$tag->__toString() );
    230                 }
    231                 if   ( $indent == 0 ) {
    232                     if   ( !empty($tag->propertyName) && $tag->propertyName != $openTag->propertyName )
    233                         throw new \RuntimeException('Start tag: '.$openTag->__toString().' does not match the closing tag: '.$tag->__toString() );
    234 
    235                     $subSourceLength = $tag->position-$openTag->position;
    236                     $subsource = substr($source,$openTag->position-$removedChars,$subSourceLength);
    237 
    238                     $source = substr_replace($source,'',$openTag->position-$removedChars,$subSourceLength);
    239                     $openTag->position -= $removedChars;
    240                     $removedChars += $subSourceLength;
    241 
    242                     // Append new subblock.
    243                     $subBlock = $this->parseBlock($subtagList,$subsource);
    244                     $subtagList = array(); // resetting...
    245                     $subBlock->tag = $openTag;
    246 
    247                     $block->nodes[] = $subBlock;
    248                 }
    249             } else {
    250                 if   ( $indent == 0) {
    251                     // This tag belongs to this block.
    252                     $tag->position -= $removedChars;
    253 
    254                     switch( $tag->type ) {
    255                         case MustacheTag::COMMENT;
    256                             $node = new MustacheComment($tag);
    257                             break;
    258                         case MustacheTag::UNESCAPED;
    259                             $node = new MustacheValue($tag);
    260                             $node->escape = null;
    261                             break;
    262                         case MustacheTag::VARIABLE;
    263                             $node = new MustacheValue($tag);
    264                             $node->escape = $this->escape;
    265                             break;
    266                         default:
    267                             throw new \RuntimeException('Unsupported tag: '.$tag->__toString() );
    268                     }
    269                     $block->nodes[] = $node;
    270 
    271                 } else {
    272                     // This is a tag of a subblock
    273                     $tag->position -= $openTag->position;
    274                     $subtagList[] = $tag;
    275                 }
    276             }
    277         }
    278 
    279         if   ( $indent > 0) {
    280             throw new \RuntimeException('Missing closing tag for: '.$openTag->__toString() );
    281         }
    282 
    283         $block->source = $source;
    284         return $block;
    285     }
    286 
    287 }
    288 
    289 class MustacheTag
    290 {
    291     public $type;
    292     public $kind;
    293     public $propertyName;
    294 
    295     public $position;
    296     public $end;
    297 
    298     private $sourceLine;
    299     private $sourceColumn;
    300 
    301     const SIMPLE = 0;
    302     const OPEN = 1;
    303     const CLOSE = 2;
    304 
    305     const CLOSING = '/';
    306     const NEGATION = '^';
    307     const SECTION = '#';
    308     const COMMENT = '!';
    309     const PARTIAL = '>';
    310     const PARENT = '<';
    311     const DELIM_CHANGE = '=';
    312     const UNESCAPED_2 = '{';
    313     const UNESCAPED = '&';
    314     const PRAGMA = '%';
    315     const BLOCK_VAR = '$';
    316 
    317     const VARIABLE = '';
    318 
    319     private $VALID_TYPES = array(
    320         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
    321     );
    322 
    323     public function __construct($tagText, $position, $end, $line, $column)
    324     {
    325         $this->sourceLine = $line;
    326         $this->sourceColumn = $column;
    327         $this->position = $position;
    328         $this->end = $end;
    329 
    330         $this->parseTag($tagText);
    331     }
    332 
    333     /**
    334      * Textual representation of a Mustache tag, suitable for error reporting.
    335      * @return string
    336      */
    337     public function __toString()
    338     {
    339         return 'tag "'.$this->type . $this->propertyName.'" (@ pos ' . $this->sourceLine . ':' . $this->sourceColumn . ') ';
    340     }
    341 
    342     private function parseTag($tagText)
    343     {
    344         $t = substr($tagText, 0, 1);
    345         if (in_array($t, $this->VALID_TYPES)) {
    346             $this->type = $t;
    347             $property = substr($tagText, 1);
    348             $this->propertyName = trim($property);
    349             if   ( $t == self::SECTION || $t == self::NEGATION )
    350                 $this->kind = self::OPEN;
    351             elseif   ( $t == self::CLOSING )
    352                 $this->kind = self::CLOSE;
    353             else
    354                 $this->kind = self::SIMPLE;
    355         } else {
    356             $this->type = self::VARIABLE;
    357             $this->propertyName = trim($tagText);
    358             $this->kind = self::SIMPLE;
    359         }
    360 
    361     }
    362 }
    363 
    364 class MustacheNode {
    365 
    366     public $type;
    367 
    368     /**
    369      * @var MustacheTag
    370      */
    371     public $tag;
    372 
    373     public function __construct( $tag )
    374     {
    375         $this->tag = $tag;
    376     }
    377 
    378     public function render( $data ) {
    379         return '';
    380     }
    381 
    382     public function getValue( $data ) {
    383 
    384         if  ( !is_object($this->tag))
    385             return false; // on root-block, there is no tag.
    386 
    387         $value = $data;
    388 
    389 		// Evaluate "dot notation"
    390 		foreach( explode('.',$this->tag->propertyName ) as $key )
    391 	  	{
    392 	   		if   ( is_array($value) && isset($value[$key]) ) {
    393 			 	$value = $value[$key];
    394 			 	continue;
    395 			}
    396 			$value = false; // Key does not exist, so there is no value.
    397 		}
    398 
    399 
    400 	 	if   ( is_object($value))
    401         {
    402             if   ($value instanceof \Closure)
    403                 ; // anonymous functions
    404             else
    405                 $value = get_object_vars($value);
    406         }
    407 
    408         return $value;
    409     }
    410 }
    411 
    412 
    413 class MustacheBlock extends MustacheNode {
    414 
    415     /**
    416      * @var String
    417      */
    418     public $source;
    419 
    420     /**
    421      * @var MustacheNode
    422      */
    423     public $nodes = array();
    424 
    425     /**
    426      * Should this block be rendered?
    427      *
    428      * @param $data data
    429      * @return bool
    430      */
    431     public function isRendered( $data ) {
    432 
    433         if  ( !is_object($this->tag))
    434             return true; // on root-block, there is no tag.
    435 
    436         $propIsTrue = (boolean) $this->getValue( $data );
    437 
    438         if  ( $this->tag->type == MustacheTag::NEGATION )
    439             $propIsTrue = ! $propIsTrue;
    440 
    441         return $propIsTrue;
    442     }
    443 
    444     public function render($data)
    445     {
    446         if ( $this->isRendered($data ) ) {
    447 
    448             $values = $this->getValue($data);
    449             if   ( !is_array($values) || !isset($values[0]) )
    450                 $values = array( $values);
    451 
    452             $sumOutput = '';
    453             foreach( $values as $value) {
    454 
    455                 $data = array_merge($data,(array) $value );
    456                 $output = $this->source;
    457                 $insertedBytes = 0;
    458 
    459 
    460                 /** @var MustacheNode $node */
    461                 foreach($this->nodes as $node) {
    462 
    463                     if   ( $node instanceof MustacheBlock )
    464                         $o = $node->render( $data);
    465                     else
    466                     {
    467                         $o = $node->render($data);
    468                         if   ( is_callable($value) )
    469                             $o = $value( $o );
    470                     }
    471 
    472                     $value = $this->getValue($data);
    473                     if   ( is_callable($value) )
    474                         $o = $value($o);
    475 
    476                     $output = substr_replace($output, $o, $node->tag->position+$insertedBytes, 0);
    477                     $insertedBytes += strlen($o);
    478                 }
    479                 $sumOutput .= $output;
    480             }
    481 
    482             return $sumOutput;
    483         }
    484         else {
    485             return '';
    486         }
    487 
    488     }
    489 }
    490 
    491 class MustacheValue extends MustacheNode {
    492 
    493     /**
    494      * escaping function.
    495      */
    496     public $escape;
    497 
    498     public function render($data)
    499     {
    500         $value = $this->getValue($data);
    501 
    502         if   ( is_callable( $this->escape)) {
    503 
    504             $escape = $this->escape;
    505             $value = $escape($value);
    506         }
    507 
    508         return $value;
    509     }
    510 }
    511 
    512 class MustacheComment extends MustacheNode {
    513 
    514     public function render($data)
    515     {
    516         return '';
    517     }
    518 }
    519 
    520 
    521 /*
    522  * Example.
    523  *
    524  * Uncomment the following for a working example.
    525  */
    526 
    527 /*
    528 error_reporting(E_ALL);
    529 ini_set('display_errors', 1);
    530 
    531 $source = <<<SRC
    532 Hello {{planet}}, {{& planet }}.{{! Simple example with a simple property }}
    533 
    534 {{#test}}
    535 Yes, this is a {{test}}. {{! yes, it is}}
    536 {{/test}}
    537 {{^test}}
    538 No, this is not a {{test}}. {{ ! will not be displayed, because test is not false }}
    539 {{/test}}
    540 
    541 {{#car}}
    542 My Car is {{color}}. {{! this is a property of the array car }}
    543 It drives on {{& planet }}.{{! this property is inherited from the upper context }}
    544 {{/}}
    545 
    546 {{#house}}
    547 My House is {{size}}. {{! this property is read from an object }}
    548 {{/}} {{! short closing tags are allowed }}
    549 
    550 Some names:
    551 {{#names}}
    552 my name is {{ name }}.{{! yes, spaces are allowed}}
    553 {{/names}}
    554 
    555 {{#empty}}
    556 this is not displayed {{! because the list is empty }}
    557 {{/empty}}
    558 
    559 {{#upper}}
    560 Hello again, {{planet}}. {{!displayed in uppercase}}
    561 {{/}}
    562 
    563 <h1>Partials</h1>
    564 {{> mycoolpartial}}
    565 
    566 <h1>Changing Delimiters</h1>
    567 Default: {{name}}
    568 {{=$( )=}}
    569 Bash-Style: $(name)
    570 Default should not work here: {{name}}
    571 
    572 $(={{ }}=)
    573 Default again: {{name}}
    574 
    575 <h1>Dot notation</h1>
    576 this will not work: {{building}}
    577 but this is the color of the roof: {{building.roof.color}}
    578 
    579 
    580 SRC;
    581 
    582 $m = new Mustache();
    583 $m->partialLoader = function($name) {
    584  return "\nThis is a partial named ".$name.". It may include variables, like the name '{{name}}'.\n\n";
    585 };
    586 $m->parse( $source );
    587 
    588 echo 'Object: <pre><code>'; print_r($m); echo '</code></pre>';
    589 
    590 $data = array(
    591     'planet'  => '<b>world</b>',
    592     'test'  => 'Test',
    593     'name'  => 'Mallory',
    594     'car'   => array('color'=>'red'),
    595     'house' => (object) array('size'=>'big' ),
    596     'names' => array(
    597         array('name'=>'Alice'),
    598         array('name'=>'Bob')
    599     ),
    600     'empty' => array(),
    601     'upper' => static function($text) { return strtoupper($text); },
    602     'building' => array('roof'=>array('color'=>'gray'))
    603 
    604 );
    605 
    606 echo '<pre>'.$m->render( $data ).'</pre>';
    607 */