Compiler.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. <?php
  2. /*
  3. * This file is part of Mustache.php.
  4. *
  5. * (c) 2012 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 $sections;
  18. private $source;
  19. private $indentNextLine;
  20. private $customEscape;
  21. private $charset;
  22. private $pragmas;
  23. /**
  24. * Compile a Mustache token parse tree into PHP source code.
  25. *
  26. * @param string $source Mustache Template source code
  27. * @param string $tree Parse tree of Mustache tokens
  28. * @param string $name Mustache Template class name
  29. * @param bool $customEscape (default: false)
  30. * @param string $charset (default: 'UTF-8')
  31. *
  32. * @return string Generated PHP source code
  33. */
  34. public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8')
  35. {
  36. $this->pragmas = array();
  37. $this->sections = array();
  38. $this->source = $source;
  39. $this->indentNextLine = true;
  40. $this->customEscape = $customEscape;
  41. $this->charset = $charset;
  42. return $this->writeCode($tree, $name);
  43. }
  44. /**
  45. * Helper function for walking the Mustache token parse tree.
  46. *
  47. * @throws Mustache_Exception_SyntaxException upon encountering unknown token types.
  48. *
  49. * @param array $tree Parse tree of Mustache tokens
  50. * @param int $level (default: 0)
  51. *
  52. * @return string Generated PHP source code
  53. */
  54. private function walk(array $tree, $level = 0)
  55. {
  56. $code = '';
  57. $level++;
  58. foreach ($tree as $node) {
  59. switch ($node[Mustache_Tokenizer::TYPE]) {
  60. case Mustache_Tokenizer::T_PRAGMA:
  61. $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true;
  62. break;
  63. case Mustache_Tokenizer::T_SECTION:
  64. $code .= $this->section(
  65. $node[Mustache_Tokenizer::NODES],
  66. $node[Mustache_Tokenizer::NAME],
  67. $node[Mustache_Tokenizer::INDEX],
  68. $node[Mustache_Tokenizer::END],
  69. $node[Mustache_Tokenizer::OTAG],
  70. $node[Mustache_Tokenizer::CTAG],
  71. $level
  72. );
  73. break;
  74. case Mustache_Tokenizer::T_INVERTED:
  75. $code .= $this->invertedSection(
  76. $node[Mustache_Tokenizer::NODES],
  77. $node[Mustache_Tokenizer::NAME],
  78. $level
  79. );
  80. break;
  81. case Mustache_Tokenizer::T_PARTIAL:
  82. case Mustache_Tokenizer::T_PARTIAL_2:
  83. $code .= $this->partial(
  84. $node[Mustache_Tokenizer::NAME],
  85. isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
  86. $level
  87. );
  88. break;
  89. case Mustache_Tokenizer::T_UNESCAPED:
  90. case Mustache_Tokenizer::T_UNESCAPED_2:
  91. $code .= $this->variable($node[Mustache_Tokenizer::NAME], false, $level);
  92. break;
  93. case Mustache_Tokenizer::T_COMMENT:
  94. break;
  95. case Mustache_Tokenizer::T_ESCAPED:
  96. $code .= $this->variable($node[Mustache_Tokenizer::NAME], true, $level);
  97. break;
  98. case Mustache_Tokenizer::T_TEXT:
  99. $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
  100. break;
  101. default:
  102. throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node);
  103. }
  104. }
  105. return $code;
  106. }
  107. const KLASS = '<?php
  108. class %s extends Mustache_Template
  109. {
  110. private $lambdaHelper;
  111. public function renderInternal(Mustache_Context $context, $indent = \'\')
  112. {
  113. $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
  114. $buffer = \'\';
  115. %s
  116. return $buffer;
  117. }
  118. %s
  119. }';
  120. const KLASS_NO_LAMBDAS = '<?php
  121. class %s extends Mustache_Template
  122. {
  123. public function renderInternal(Mustache_Context $context, $indent = \'\')
  124. {
  125. $buffer = \'\';
  126. %s
  127. return $buffer;
  128. }
  129. %s
  130. }';
  131. /**
  132. * Generate Mustache Template class PHP source.
  133. *
  134. * @param array $tree Parse tree of Mustache tokens
  135. * @param string $name Mustache Template class name
  136. *
  137. * @return string Generated PHP source code
  138. */
  139. private function writeCode($tree, $name)
  140. {
  141. $code = $this->walk($tree);
  142. $sections = implode("\n", $this->sections);
  143. $klass = empty($this->sections) ? self::KLASS_NO_LAMBDAS : self::KLASS;
  144. return sprintf($this->prepare($klass, 0, false), $name, $code, $sections);
  145. }
  146. const SECTION_CALL = '
  147. // %s section
  148. $buffer .= $this->section%s($context, $indent, $context->%s(%s));
  149. ';
  150. const SECTION = '
  151. private function section%s(Mustache_Context $context, $indent, $value) {
  152. $buffer = \'\';
  153. if (!is_string($value) && is_callable($value)) {
  154. $source = %s;
  155. $buffer .= $this->mustache
  156. ->loadLambda((string) call_user_func($value, $source, $this->lambdaHelper)%s)
  157. ->renderInternal($context, $indent);
  158. } elseif (!empty($value)) {
  159. $values = $this->isIterable($value) ? $value : array($value);
  160. foreach ($values as $value) {
  161. $context->push($value);%s
  162. $context->pop();
  163. }
  164. }
  165. return $buffer;
  166. }';
  167. /**
  168. * Generate Mustache Template section PHP source.
  169. *
  170. * @param array $nodes Array of child tokens
  171. * @param string $id Section name
  172. * @param int $start Section start offset
  173. * @param int $end Section end offset
  174. * @param string $otag Current Mustache opening tag
  175. * @param string $ctag Current Mustache closing tag
  176. * @param int $level
  177. *
  178. * @return string Generated section PHP source code
  179. */
  180. private function section($nodes, $id, $start, $end, $otag, $ctag, $level)
  181. {
  182. $method = $this->getFindMethod($id);
  183. $id = var_export($id, true);
  184. $source = var_export(substr($this->source, $start, $end - $start), true);
  185. if ($otag !== '{{' || $ctag !== '}}') {
  186. $delims = ', '.var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
  187. } else {
  188. $delims = '';
  189. }
  190. $key = ucfirst(md5($delims."\n".$source));
  191. if (!isset($this->sections[$key])) {
  192. $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $source, $delims, $this->walk($nodes, 2));
  193. }
  194. return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $key, $method, $id);
  195. }
  196. const INVERTED_SECTION = '
  197. // %s inverted section
  198. $value = $context->%s(%s);
  199. if (empty($value)) {
  200. %s
  201. }';
  202. /**
  203. * Generate Mustache Template inverted section PHP source.
  204. *
  205. * @param array $nodes Array of child tokens
  206. * @param string $id Section name
  207. * @param int $level
  208. *
  209. * @return string Generated inverted section PHP source code
  210. */
  211. private function invertedSection($nodes, $id, $level)
  212. {
  213. $method = $this->getFindMethod($id);
  214. $id = var_export($id, true);
  215. return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $this->walk($nodes, $level));
  216. }
  217. const PARTIAL = '
  218. if ($partial = $this->mustache->loadPartial(%s)) {
  219. $buffer .= $partial->renderInternal($context, %s);
  220. }
  221. ';
  222. /**
  223. * Generate Mustache Template partial call PHP source.
  224. *
  225. * @param string $id Partial name
  226. * @param string $indent Whitespace indent to apply to partial
  227. * @param int $level
  228. *
  229. * @return string Generated partial call PHP source code
  230. */
  231. private function partial($id, $indent, $level)
  232. {
  233. return sprintf(
  234. $this->prepare(self::PARTIAL, $level),
  235. var_export($id, true),
  236. var_export($indent, true)
  237. );
  238. }
  239. const VARIABLE = '
  240. $value = $context->%s(%s);
  241. if (!is_string($value) && is_callable($value)) {
  242. $value = $this->mustache
  243. ->loadLambda((string) call_user_func($value))
  244. ->renderInternal($context, $indent);
  245. }%s
  246. $buffer .= %s%s;
  247. ';
  248. /**
  249. * Generate Mustache Template variable interpolation PHP source.
  250. *
  251. * @param string $id Variable name
  252. * @param boolean $escape Escape the variable value for output?
  253. * @param int $level
  254. *
  255. * @return string Generated variable interpolation PHP source
  256. */
  257. private function variable($id, $escape, $level)
  258. {
  259. $filters = '';
  260. if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) {
  261. list($id, $filters) = $this->getFilters($id, $level);
  262. }
  263. $method = $this->getFindMethod($id);
  264. $id = ($method !== 'last') ? var_export($id, true) : '';
  265. $value = $escape ? $this->getEscape() : '$value';
  266. return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value);
  267. }
  268. /**
  269. * Generate Mustache Template variable filtering PHP source.
  270. *
  271. * @param string $id Variable name
  272. * @param int $level
  273. *
  274. * @return string Generated variable filtering PHP source
  275. */
  276. private function getFilters($id, $level)
  277. {
  278. $filters = array_map('trim', explode('|', $id));
  279. $id = array_shift($filters);
  280. return array($id, $this->getFilter($filters, $level));
  281. }
  282. const FILTER = '
  283. $filter = $context->%s(%s);
  284. if (is_string($filter) || !is_callable($filter)) {
  285. throw new Mustache_Exception_UnknownFilterException(%s);
  286. }
  287. $value = call_user_func($filter, $value);%s
  288. ';
  289. /**
  290. * Generate PHP source for a single filter.
  291. *
  292. * @param array $filters
  293. * @param int $level
  294. *
  295. * @return string Generated filter PHP source
  296. */
  297. private function getFilter(array $filters, $level)
  298. {
  299. if (empty($filters)) {
  300. return '';
  301. }
  302. $name = array_shift($filters);
  303. $method = $this->getFindMethod($name);
  304. $filter = ($method !== 'last') ? var_export($name, true) : '';
  305. $msg = var_export($name, true);
  306. return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $msg, $this->getFilter($filters, $level));
  307. }
  308. const LINE = '$buffer .= "\n";';
  309. const TEXT = '$buffer .= %s%s;';
  310. /**
  311. * Generate Mustache Template output Buffer call PHP source.
  312. *
  313. * @param string $text
  314. * @param int $level
  315. *
  316. * @return string Generated output Buffer call PHP source
  317. */
  318. private function text($text, $level)
  319. {
  320. if ($text === "\n") {
  321. $this->indentNextLine = true;
  322. return $this->prepare(self::LINE, $level);
  323. } else {
  324. return sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
  325. }
  326. }
  327. /**
  328. * Prepare PHP source code snippet for output.
  329. *
  330. * @param string $text
  331. * @param int $bonus Additional indent level (default: 0)
  332. * @param boolean $prependNewline Prepend a newline to the snippet? (default: true)
  333. *
  334. * @return string PHP source code snippet
  335. */
  336. private function prepare($text, $bonus = 0, $prependNewline = true)
  337. {
  338. $text = ($prependNewline ? "\n" : '').trim($text);
  339. if ($prependNewline) {
  340. $bonus++;
  341. }
  342. return preg_replace("/\n( {8})?/", "\n".str_repeat(" ", $bonus * 4), $text);
  343. }
  344. const DEFAULT_ESCAPE = 'htmlspecialchars(%s, ENT_COMPAT, %s)';
  345. const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)';
  346. /**
  347. * Get the current escaper.
  348. *
  349. * @param string $value (default: '$value')
  350. *
  351. * @return string Either a custom callback, or an inline call to `htmlspecialchars`
  352. */
  353. private function getEscape($value = '$value')
  354. {
  355. if ($this->customEscape) {
  356. return sprintf(self::CUSTOM_ESCAPE, $value);
  357. } else {
  358. return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->charset, true));
  359. }
  360. }
  361. /**
  362. * Select the appropriate Context `find` method for a given $id.
  363. *
  364. * The return value will be one of `find`, `findDot` or `last`.
  365. *
  366. * @see Mustache_Context::find
  367. * @see Mustache_Context::findDot
  368. * @see Mustache_Context::last
  369. *
  370. * @param string $id Variable name
  371. *
  372. * @return string `find` method name
  373. */
  374. private function getFindMethod($id)
  375. {
  376. if ($id === '.') {
  377. return 'last';
  378. } elseif (strpos($id, '.') === false) {
  379. return 'find';
  380. } else {
  381. return 'findDot';
  382. }
  383. }
  384. const LINE_INDENT = '$indent . ';
  385. /**
  386. * Get the current $indent prefix to write to the buffer.
  387. *
  388. * @return string "$indent . " or ""
  389. */
  390. private function flushIndent()
  391. {
  392. if ($this->indentNextLine) {
  393. $this->indentNextLine = false;
  394. return self::LINE_INDENT;
  395. } else {
  396. return '';
  397. }
  398. }
  399. }