Compiler.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. <?php
  2. /*
  3. * This file is part of Mustache.php.
  4. *
  5. * (c) 2010-2014 Justin Hileman
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. /**
  11. * Mustache Compiler class.
  12. *
  13. * This class is responsible for turning a Mustache token parse tree into normal PHP source code.
  14. */
  15. class Mustache_Compiler
  16. {
  17. private $pragmas;
  18. private $defaultPragmas = array();
  19. private $sections;
  20. private $source;
  21. private $indentNextLine;
  22. private $customEscape;
  23. private $entityFlags;
  24. private $charset;
  25. private $strictCallables;
  26. /**
  27. * Compile a Mustache token parse tree into PHP source code.
  28. *
  29. * @param string $source Mustache Template source code
  30. * @param string $tree Parse tree of Mustache tokens
  31. * @param string $name Mustache Template class name
  32. * @param bool $customEscape (default: false)
  33. * @param string $charset (default: 'UTF-8')
  34. * @param bool $strictCallables (default: false)
  35. * @param int $entityFlags (default: ENT_COMPAT)
  36. *
  37. * @return string Generated PHP source code
  38. */
  39. public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
  40. {
  41. $this->pragmas = $this->defaultPragmas;
  42. $this->sections = array();
  43. $this->source = $source;
  44. $this->indentNextLine = true;
  45. $this->customEscape = $customEscape;
  46. $this->entityFlags = $entityFlags;
  47. $this->charset = $charset;
  48. $this->strictCallables = $strictCallables;
  49. return $this->writeCode($tree, $name);
  50. }
  51. /**
  52. * Enable pragmas across all templates, regardless of the presence of pragma
  53. * tags in the individual templates.
  54. *
  55. * @internal Users should set global pragmas in Mustache_Engine, not here :)
  56. *
  57. * @param array $pragmas
  58. */
  59. public function setPragmas(array $pragmas)
  60. {
  61. $this->pragmas = array();
  62. foreach ($pragmas as $pragma) {
  63. $this->pragmas[$pragma] = true;
  64. }
  65. $this->defaultPragmas = $this->pragmas;
  66. }
  67. /**
  68. * Helper function for walking the Mustache token parse tree.
  69. *
  70. * @throws Mustache_Exception_SyntaxException upon encountering unknown token types.
  71. *
  72. * @param array $tree Parse tree of Mustache tokens
  73. * @param int $level (default: 0)
  74. *
  75. * @return string Generated PHP source code
  76. */
  77. private function walk(array $tree, $level = 0)
  78. {
  79. $code = '';
  80. $level++;
  81. foreach ($tree as $node) {
  82. switch ($node[Mustache_Tokenizer::TYPE]) {
  83. case Mustache_Tokenizer::T_PRAGMA:
  84. $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true;
  85. break;
  86. case Mustache_Tokenizer::T_SECTION:
  87. $code .= $this->section(
  88. $node[Mustache_Tokenizer::NODES],
  89. $node[Mustache_Tokenizer::NAME],
  90. $node[Mustache_Tokenizer::INDEX],
  91. $node[Mustache_Tokenizer::END],
  92. $node[Mustache_Tokenizer::OTAG],
  93. $node[Mustache_Tokenizer::CTAG],
  94. $level
  95. );
  96. break;
  97. case Mustache_Tokenizer::T_INVERTED:
  98. $code .= $this->invertedSection(
  99. $node[Mustache_Tokenizer::NODES],
  100. $node[Mustache_Tokenizer::NAME],
  101. $level
  102. );
  103. break;
  104. case Mustache_Tokenizer::T_PARTIAL:
  105. $code .= $this->partial(
  106. $node[Mustache_Tokenizer::NAME],
  107. isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
  108. $level
  109. );
  110. break;
  111. case Mustache_Tokenizer::T_PARENT:
  112. $code .= $this->parent(
  113. $node[Mustache_Tokenizer::NAME],
  114. isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
  115. $node[Mustache_Tokenizer::NODES],
  116. $level
  117. );
  118. break;
  119. case Mustache_Tokenizer::T_BLOCK_ARG:
  120. $code .= $this->blockArg(
  121. $node[Mustache_Tokenizer::NODES],
  122. $node[Mustache_Tokenizer::NAME],
  123. $node[Mustache_Tokenizer::INDEX],
  124. $node[Mustache_Tokenizer::END],
  125. $node[Mustache_Tokenizer::OTAG],
  126. $node[Mustache_Tokenizer::CTAG],
  127. $level
  128. );
  129. break;
  130. case Mustache_Tokenizer::T_BLOCK_VAR:
  131. $code .= $this->blockVar(
  132. $node[Mustache_Tokenizer::NODES],
  133. $node[Mustache_Tokenizer::NAME],
  134. $node[Mustache_Tokenizer::INDEX],
  135. $node[Mustache_Tokenizer::END],
  136. $node[Mustache_Tokenizer::OTAG],
  137. $node[Mustache_Tokenizer::CTAG],
  138. $level
  139. );
  140. break;
  141. case Mustache_Tokenizer::T_UNESCAPED:
  142. case Mustache_Tokenizer::T_UNESCAPED_2:
  143. $code .= $this->variable($node[Mustache_Tokenizer::NAME], false, $level);
  144. break;
  145. case Mustache_Tokenizer::T_COMMENT:
  146. break;
  147. case Mustache_Tokenizer::T_ESCAPED:
  148. $code .= $this->variable($node[Mustache_Tokenizer::NAME], true, $level);
  149. break;
  150. case Mustache_Tokenizer::T_TEXT:
  151. $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
  152. break;
  153. default:
  154. throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node);
  155. }
  156. }
  157. return $code;
  158. }
  159. const KLASS = '<?php
  160. class %s extends Mustache_Template
  161. {
  162. private $lambdaHelper;%s
  163. public function renderInternal(Mustache_Context $context, $indent = \'\')
  164. {
  165. $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
  166. $buffer = \'\';
  167. $newContext = array();
  168. %s
  169. return $buffer;
  170. }
  171. %s
  172. }';
  173. const KLASS_NO_LAMBDAS = '<?php
  174. class %s extends Mustache_Template
  175. {%s
  176. public function renderInternal(Mustache_Context $context, $indent = \'\')
  177. {
  178. $buffer = \'\';
  179. $newContext = array();
  180. %s
  181. return $buffer;
  182. }
  183. }';
  184. const STRICT_CALLABLE = 'protected $strictCallables = true;';
  185. /**
  186. * Generate Mustache Template class PHP source.
  187. *
  188. * @param array $tree Parse tree of Mustache tokens
  189. * @param string $name Mustache Template class name
  190. *
  191. * @return string Generated PHP source code
  192. */
  193. private function writeCode($tree, $name)
  194. {
  195. $code = $this->walk($tree);
  196. $sections = implode("\n", $this->sections);
  197. $klass = empty($this->sections) ? self::KLASS_NO_LAMBDAS : self::KLASS;
  198. $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
  199. return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections);
  200. }
  201. const BLOCK_VAR = '
  202. $value = $this->resolveValue($context->findInBlock(%s), $context, $indent);
  203. if ($value && !is_array($value) && !is_object($value)) {
  204. $buffer .= $value;
  205. } else {
  206. %s
  207. }
  208. ';
  209. /**
  210. * Generate Mustache Template inheritance block variable PHP source.
  211. *
  212. * @param array $nodes Array of child tokens
  213. * @param string $id Section name
  214. * @param int $start Section start offset
  215. * @param int $end Section end offset
  216. * @param string $otag Current Mustache opening tag
  217. * @param string $ctag Current Mustache closing tag
  218. * @param int $level
  219. *
  220. * @return string Generated PHP source code
  221. */
  222. private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level)
  223. {
  224. $id_str = var_export($id, true);
  225. return sprintf($this->prepare(self::BLOCK_VAR, $level), $id_str, $this->walk($nodes, 2));
  226. }
  227. const BLOCK_ARG = '
  228. // %s block_arg
  229. $value = $this->section%s($context, $indent, true);
  230. $newContext[%s] = %s$value;
  231. ';
  232. /**
  233. * Generate Mustache Template inheritance block argument PHP source.
  234. *
  235. * @param array $nodes Array of child tokens
  236. * @param string $id Section name
  237. * @param int $start Section start offset
  238. * @param int $end Section end offset
  239. * @param string $otag Current Mustache opening tag
  240. * @param string $ctag Current Mustache closing tag
  241. * @param int $level
  242. *
  243. * @return string Generated PHP source code
  244. */
  245. private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
  246. {
  247. $key = $this->section($nodes, $id, $start, $end, $otag, $ctag, $level, true);
  248. $filters = '';
  249. if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) {
  250. list($id, $filters) = $this->getFilters($id, $level);
  251. }
  252. $method = $this->getFindMethod($id);
  253. $id = var_export($id, true);
  254. return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key, $id, $this->flushIndent());
  255. }
  256. const SECTION_CALL = '
  257. // %s section
  258. $value = $context->%s(%s);%s
  259. $buffer .= $this->section%s($context, $indent, $value);
  260. ';
  261. const SECTION = '
  262. private function section%s(Mustache_Context $context, $indent, $value)
  263. {
  264. $buffer = \'\';
  265. if (%s) {
  266. $source = %s;
  267. $result = call_user_func($value, $source, $this->lambdaHelper);
  268. if (strpos($result, \'{{\') === false) {
  269. $buffer .= $result;
  270. } else {
  271. $buffer .= $this->mustache
  272. ->loadLambda((string) $result%s)
  273. ->renderInternal($context);
  274. }
  275. } elseif (!empty($value)) {
  276. $values = $this->isIterable($value) ? $value : array($value);
  277. foreach ($values as $value) {
  278. $context->push($value);
  279. %s
  280. $context->pop();
  281. }
  282. }
  283. return $buffer;
  284. }';
  285. /**
  286. * Generate Mustache Template section PHP source.
  287. *
  288. * @param array $nodes Array of child tokens
  289. * @param string $id Section name
  290. * @param int $start Section start offset
  291. * @param int $end Section end offset
  292. * @param string $otag Current Mustache opening tag
  293. * @param string $ctag Current Mustache closing tag
  294. * @param int $level
  295. * @param bool $arg (default: false)
  296. *
  297. * @return string Generated section PHP source code
  298. */
  299. private function section($nodes, $id, $start, $end, $otag, $ctag, $level, $arg = false)
  300. {
  301. $filters = '';
  302. if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) {
  303. list($id, $filters) = $this->getFilters($id, $level);
  304. }
  305. $source = var_export(substr($this->source, $start, $end - $start), true);
  306. $callable = $this->getCallable();
  307. if ($otag !== '{{' || $ctag !== '}}') {
  308. $delims = ', '.var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
  309. } else {
  310. $delims = '';
  311. }
  312. $key = ucfirst(md5($delims."\n".$source));
  313. if (!isset($this->sections[$key])) {
  314. $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $delims, $this->walk($nodes, 2));
  315. }
  316. if ($arg === true) {
  317. return $key;
  318. } else {
  319. $method = $this->getFindMethod($id);
  320. $id = var_export($id, true);
  321. return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key);
  322. }
  323. }
  324. const INVERTED_SECTION = '
  325. // %s inverted section
  326. $value = $context->%s(%s);%s
  327. if (empty($value)) {
  328. %s
  329. }';
  330. /**
  331. * Generate Mustache Template inverted section PHP source.
  332. *
  333. * @param array $nodes Array of child tokens
  334. * @param string $id Section name
  335. * @param int $level
  336. *
  337. * @return string Generated inverted section PHP source code
  338. */
  339. private function invertedSection($nodes, $id, $level)
  340. {
  341. $filters = '';
  342. if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) {
  343. list($id, $filters) = $this->getFilters($id, $level);
  344. }
  345. $method = $this->getFindMethod($id);
  346. $id = var_export($id, true);
  347. return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $filters, $this->walk($nodes, $level));
  348. }
  349. const PARTIAL = '
  350. if ($partial = $this->mustache->loadPartial(%s)) {
  351. $buffer .= $partial->renderInternal($context, $indent . %s);
  352. }
  353. ';
  354. /**
  355. * Generate Mustache Template partial call PHP source.
  356. *
  357. * @param string $id Partial name
  358. * @param string $indent Whitespace indent to apply to partial
  359. * @param int $level
  360. *
  361. * @return string Generated partial call PHP source code
  362. */
  363. private function partial($id, $indent, $level)
  364. {
  365. return sprintf(
  366. $this->prepare(self::PARTIAL, $level),
  367. var_export($id, true),
  368. var_export($indent, true)
  369. );
  370. }
  371. const PARENT = '
  372. %s
  373. if ($parent = $this->mustache->LoadPartial(%s)) {
  374. $context->pushBlockContext($newContext);
  375. $buffer .= $parent->renderInternal($context, $indent);
  376. $context->popBlockContext();
  377. }
  378. ';
  379. /**
  380. * Generate Mustache Template inheritance parent call PHP source.
  381. *
  382. * @param string $id Parent tag name
  383. * @param string $indent Whitespace indent to apply to parent
  384. * @param array $children Child nodes
  385. * @param int $level
  386. *
  387. * @return string Generated PHP source code
  388. */
  389. private function parent($id, $indent, array $children, $level)
  390. {
  391. $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs'));
  392. return sprintf(
  393. $this->prepare(self::PARENT, $level),
  394. $this->walk($realChildren, $level),
  395. var_export($id, true),
  396. var_export($indent, true)
  397. );
  398. }
  399. /**
  400. * Helper method for filtering out non-block-arg tokens.
  401. *
  402. * @param array $node
  403. *
  404. * @return boolean True if $node is a block arg token.
  405. */
  406. private static function onlyBlockArgs(array $node)
  407. {
  408. return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG;
  409. }
  410. const VARIABLE = '
  411. $value = $this->resolveValue($context->%s(%s), $context, $indent);%s
  412. $buffer .= %s%s;
  413. ';
  414. /**
  415. * Generate Mustache Template variable interpolation PHP source.
  416. *
  417. * @param string $id Variable name
  418. * @param boolean $escape Escape the variable value for output?
  419. * @param int $level
  420. *
  421. * @return string Generated variable interpolation PHP source
  422. */
  423. private function variable($id, $escape, $level)
  424. {
  425. $filters = '';
  426. if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) {
  427. list($id, $filters) = $this->getFilters($id, $level);
  428. }
  429. $method = $this->getFindMethod($id);
  430. $id = ($method !== 'last') ? var_export($id, true) : '';
  431. $value = $escape ? $this->getEscape() : '$value';
  432. return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value);
  433. }
  434. /**
  435. * Generate Mustache Template variable filtering PHP source.
  436. *
  437. * @param string $id Variable name
  438. * @param int $level
  439. *
  440. * @return string Generated variable filtering PHP source
  441. */
  442. private function getFilters($id, $level)
  443. {
  444. $filters = array_map('trim', explode('|', $id));
  445. $id = array_shift($filters);
  446. return array($id, $this->getFilter($filters, $level));
  447. }
  448. const FILTER = '
  449. $filter = $context->%s(%s);
  450. if (!(%s)) {
  451. throw new Mustache_Exception_UnknownFilterException(%s);
  452. }
  453. $value = call_user_func($filter, $value);%s
  454. ';
  455. /**
  456. * Generate PHP source for a single filter.
  457. *
  458. * @param array $filters
  459. * @param int $level
  460. *
  461. * @return string Generated filter PHP source
  462. */
  463. private function getFilter(array $filters, $level)
  464. {
  465. if (empty($filters)) {
  466. return '';
  467. }
  468. $name = array_shift($filters);
  469. $method = $this->getFindMethod($name);
  470. $filter = ($method !== 'last') ? var_export($name, true) : '';
  471. $callable = $this->getCallable('$filter');
  472. $msg = var_export($name, true);
  473. return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilter($filters, $level));
  474. }
  475. const LINE = '$buffer .= "\n";';
  476. const TEXT = '$buffer .= %s%s;';
  477. /**
  478. * Generate Mustache Template output Buffer call PHP source.
  479. *
  480. * @param string $text
  481. * @param int $level
  482. *
  483. * @return string Generated output Buffer call PHP source
  484. */
  485. private function text($text, $level)
  486. {
  487. $indentNextLine = (substr($text, -1) === "\n");
  488. $code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
  489. $this->indentNextLine = $indentNextLine;
  490. return $code;
  491. }
  492. /**
  493. * Prepare PHP source code snippet for output.
  494. *
  495. * @param string $text
  496. * @param int $bonus Additional indent level (default: 0)
  497. * @param boolean $prependNewline Prepend a newline to the snippet? (default: true)
  498. * @param boolean $appendNewline Append a newline to the snippet? (default: false)
  499. *
  500. * @return string PHP source code snippet
  501. */
  502. private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false)
  503. {
  504. $text = ($prependNewline ? "\n" : '').trim($text);
  505. if ($prependNewline) {
  506. $bonus++;
  507. }
  508. if ($appendNewline) {
  509. $text .= "\n";
  510. }
  511. return preg_replace("/\n( {8})?/", "\n".str_repeat(" ", $bonus * 4), $text);
  512. }
  513. const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)';
  514. const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)';
  515. /**
  516. * Get the current escaper.
  517. *
  518. * @param string $value (default: '$value')
  519. *
  520. * @return string Either a custom callback, or an inline call to `htmlspecialchars`
  521. */
  522. private function getEscape($value = '$value')
  523. {
  524. if ($this->customEscape) {
  525. return sprintf(self::CUSTOM_ESCAPE, $value);
  526. } else {
  527. return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true));
  528. }
  529. }
  530. /**
  531. * Select the appropriate Context `find` method for a given $id.
  532. *
  533. * The return value will be one of `find`, `findDot` or `last`.
  534. *
  535. * @see Mustache_Context::find
  536. * @see Mustache_Context::findDot
  537. * @see Mustache_Context::last
  538. *
  539. * @param string $id Variable name
  540. *
  541. * @return string `find` method name
  542. */
  543. private function getFindMethod($id)
  544. {
  545. if ($id === '.') {
  546. return 'last';
  547. } elseif (strpos($id, '.') === false) {
  548. return 'find';
  549. } else {
  550. return 'findDot';
  551. }
  552. }
  553. const IS_CALLABLE = '!is_string(%s) && is_callable(%s)';
  554. const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';
  555. /**
  556. * Helper function to compile strict vs lax "is callable" logic.
  557. *
  558. * @param string $variable (default: '$value')
  559. *
  560. * @return string "is callable" logic
  561. */
  562. private function getCallable($variable = '$value')
  563. {
  564. $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE;
  565. return sprintf($tpl, $variable, $variable);
  566. }
  567. const LINE_INDENT = '$indent . ';
  568. /**
  569. * Get the current $indent prefix to write to the buffer.
  570. *
  571. * @return string "$indent . " or ""
  572. */
  573. private function flushIndent()
  574. {
  575. if (!$this->indentNextLine) {
  576. return '';
  577. }
  578. $this->indentNextLine = false;
  579. return self::LINE_INDENT;
  580. }
  581. }