File Mustache.class.php
Last commit: Mon Jan 27 22:43:32 2020 +0100 Jan Dankert Exporting the mustache parser from OpenRat-CMS to this new workspace.
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 */
DownloadMustache.class.php
History Mon, 27 Jan 2020 22:43:32 +0100 Jan Dankert Exporting the mustache parser from OpenRat-CMS to this new workspace.