PageRenderTime 60ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/libs/Nette/Latte/Compiler.php

https://bitbucket.org/Jedna/nette-sandbox
PHP | 504 lines | 396 code | 58 blank | 50 comment | 47 complexity | aa3df6d919c3005839c7b828a518fbb3 MD5 | raw file
  1. <?php
  2. /**
  3. * This file is part of the Nette Framework (http://nette.org)
  4. *
  5. * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
  6. *
  7. * For the full copyright and license information, please view
  8. * the file license.txt that was distributed with this source code.
  9. */
  10. namespace Nette\Latte;
  11. use Nette,
  12. Nette\Utils\Strings;
  13. /**
  14. * Latte compiler.
  15. *
  16. * @author David Grudl
  17. */
  18. class Compiler extends Nette\Object
  19. {
  20. /** @var string default content type */
  21. public $defaultContentType = self::CONTENT_XHTML;
  22. /** @var array of Token */
  23. private $tokens;
  24. /** @var string pointer to current node content */
  25. private $output;
  26. /** @var int position on source template */
  27. private $position;
  28. /** @var array of [name => array of IMacro] */
  29. private $macros;
  30. /** @var SplObjectStorage */
  31. private $macroHandlers;
  32. /** @var array of HtmlNode */
  33. private $htmlNodes = array();
  34. /** @var array of MacroNode */
  35. private $macroNodes = array();
  36. /** @var array of string */
  37. private $attrCodes = array();
  38. /** @var string */
  39. private $contentType;
  40. /** @var array */
  41. private $context;
  42. /** @var string */
  43. private $templateId;
  44. /** Context-aware escaping states */
  45. const CONTENT_HTML = 'html',
  46. CONTENT_XHTML = 'xhtml',
  47. CONTENT_XML = 'xml',
  48. CONTENT_JS = 'js',
  49. CONTENT_CSS = 'css',
  50. CONTENT_ICAL = 'ical',
  51. CONTENT_TEXT = 'text';
  52. /** @internal Context-aware escaping states */
  53. const CONTEXT_COMMENT = 'comment',
  54. CONTEXT_SINGLE_QUOTED = "'",
  55. CONTEXT_DOUBLE_QUOTED = '"';
  56. public function __construct()
  57. {
  58. $this->macroHandlers = new \SplObjectStorage;
  59. }
  60. /**
  61. * Adds new macro.
  62. * @param
  63. * @return Compiler provides a fluent interface
  64. */
  65. public function addMacro($name, IMacro $macro)
  66. {
  67. $this->macros[$name][] = $macro;
  68. $this->macroHandlers->attach($macro);
  69. return $this;
  70. }
  71. /**
  72. * Compiles tokens to PHP code.
  73. * @param array
  74. * @return string
  75. */
  76. public function compile(array $tokens)
  77. {
  78. $this->templateId = Strings::random();
  79. $this->tokens = $tokens;
  80. $output = '';
  81. $this->output = & $output;
  82. $this->htmlNodes = $this->macroNodes = array();
  83. $this->setContentType($this->defaultContentType);
  84. foreach ($this->macroHandlers as $handler) {
  85. $handler->initialize($this);
  86. }
  87. try {
  88. foreach ($tokens as $this->position => $token) {
  89. if ($token->type === Token::TEXT) {
  90. $this->output .= $token->text;
  91. } elseif ($token->type === Token::MACRO) {
  92. $isRightmost = !isset($tokens[$this->position + 1])
  93. || substr($tokens[$this->position + 1]->text, 0, 1) === "\n";
  94. $this->writeMacro($token->name, $token->value, $token->modifiers, $isRightmost);
  95. } elseif ($token->type === Token::TAG_BEGIN) {
  96. $this->processTagBegin($token);
  97. } elseif ($token->type === Token::TAG_END) {
  98. $this->processTagEnd($token);
  99. } elseif ($token->type === Token::ATTRIBUTE) {
  100. $this->processAttribute($token);
  101. }
  102. }
  103. } catch (CompileException $e) {
  104. $e->sourceLine = $token->line;
  105. throw $e;
  106. }
  107. foreach ($this->htmlNodes as $htmlNode) {
  108. if (!empty($htmlNode->macroAttrs)) {
  109. throw new CompileException("Missing end tag </$htmlNode->name> for macro-attribute " . Parser::N_PREFIX
  110. . implode(' and ' . Parser::N_PREFIX, array_keys($htmlNode->macroAttrs)) . ".", 0, $token->line);
  111. }
  112. }
  113. $prologs = $epilogs = '';
  114. foreach ($this->macroHandlers as $handler) {
  115. $res = $handler->finalize();
  116. $handlerName = get_class($handler);
  117. $prologs .= empty($res[0]) ? '' : "<?php\n// prolog $handlerName\n$res[0]\n?>";
  118. $epilogs = (empty($res[1]) ? '' : "<?php\n// epilog $handlerName\n$res[1]\n?>") . $epilogs;
  119. }
  120. $output = ($prologs ? $prologs . "<?php\n//\n// main template\n//\n?>\n" : '') . $output . $epilogs;
  121. if ($this->macroNodes) {
  122. throw new CompileException("There are unclosed macros.", 0, $token->line);
  123. }
  124. $output = $this->expandTokens($output);
  125. return $output;
  126. }
  127. /**
  128. * @return Compiler provides a fluent interface
  129. */
  130. public function setContentType($type)
  131. {
  132. $this->contentType = $type;
  133. $this->context = NULL;
  134. return $this;
  135. }
  136. /**
  137. * @return string
  138. */
  139. public function getContentType()
  140. {
  141. return $this->contentType;
  142. }
  143. /**
  144. * @return Compiler provides a fluent interface
  145. */
  146. public function setContext($context, $sub = NULL)
  147. {
  148. $this->context = array($context, $sub);
  149. return $this;
  150. }
  151. /**
  152. * @return array [context, spec]
  153. */
  154. public function getContext()
  155. {
  156. return $this->context;
  157. }
  158. /**
  159. * @return string
  160. */
  161. public function getTemplateId()
  162. {
  163. return $this->templateId;
  164. }
  165. /**
  166. * Returns current line number.
  167. * @return int
  168. */
  169. public function getLine()
  170. {
  171. return $this->tokens ? $this->tokens[$this->position]->line : NULL;
  172. }
  173. public function expandTokens($s)
  174. {
  175. return strtr($s, $this->attrCodes);
  176. }
  177. private function processTagBegin($token)
  178. {
  179. if ($token->closing) {
  180. do {
  181. $htmlNode = array_pop($this->htmlNodes);
  182. if (!$htmlNode) {
  183. $htmlNode = new HtmlNode($token->name);
  184. }
  185. } while (strcasecmp($htmlNode->name, $token->name));
  186. $this->htmlNodes[] = $htmlNode;
  187. $htmlNode->closing = TRUE;
  188. $htmlNode->offset = strlen($this->output);
  189. $this->setContext(NULL);
  190. } elseif ($token->text === '<!--') {
  191. $this->setContext(self::CONTEXT_COMMENT);
  192. } else {
  193. $this->htmlNodes[] = $htmlNode = new HtmlNode($token->name);
  194. $htmlNode->isEmpty = in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML))
  195. && isset(Nette\Utils\Html::$emptyElements[strtolower($token->name)]);
  196. $htmlNode->offset = strlen($this->output);
  197. $this->setContext(NULL);
  198. }
  199. $this->output .= $token->text;
  200. }
  201. private function processTagEnd($token)
  202. {
  203. if ($token->text === '-->') {
  204. $this->output .= $token->text;
  205. $this->setContext(NULL);
  206. return;
  207. }
  208. $htmlNode = end($this->htmlNodes);
  209. $isEmpty = !$htmlNode->closing && (Strings::contains($token->text, '/') || $htmlNode->isEmpty);
  210. if ($isEmpty && in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML))) { // auto-correct
  211. $token->text = preg_replace('#^.*>#', $this->contentType === self::CONTENT_XHTML ? ' />' : '>', $token->text);
  212. }
  213. if (empty($htmlNode->macroAttrs)) {
  214. $this->output .= $token->text;
  215. } else {
  216. $code = substr($this->output, $htmlNode->offset) . $token->text;
  217. $this->output = substr($this->output, 0, $htmlNode->offset);
  218. $this->writeAttrsMacro($code, $htmlNode);
  219. if ($isEmpty) {
  220. $htmlNode->closing = TRUE;
  221. $this->writeAttrsMacro('', $htmlNode);
  222. }
  223. }
  224. if ($isEmpty) {
  225. $htmlNode->closing = TRUE;
  226. }
  227. if (!$htmlNode->closing && (strcasecmp($htmlNode->name, 'script') === 0 || strcasecmp($htmlNode->name, 'style') === 0)) {
  228. $this->setContext(strcasecmp($htmlNode->name, 'style') ? self::CONTENT_JS : self::CONTENT_CSS);
  229. } else {
  230. $this->setContext(NULL);
  231. if ($htmlNode->closing) {
  232. array_pop($this->htmlNodes);
  233. }
  234. }
  235. }
  236. private function processAttribute($token)
  237. {
  238. $htmlNode = end($this->htmlNodes);
  239. if (Strings::startsWith($token->name, Parser::N_PREFIX)) {
  240. $htmlNode->macroAttrs[substr($token->name, strlen(Parser::N_PREFIX))] = $token->value;
  241. } else {
  242. $htmlNode->attrs[$token->name] = TRUE;
  243. $this->output .= $token->text;
  244. if ($token->value) { // quoted
  245. $context = NULL;
  246. if (strncasecmp($token->name, 'on', 2) === 0) {
  247. $context = self::CONTENT_JS;
  248. } elseif ($token->name === 'style') {
  249. $context = self::CONTENT_CSS;
  250. }
  251. $this->setContext($token->value, $context);
  252. }
  253. }
  254. }
  255. /********************* macros ****************d*g**/
  256. /**
  257. * Generates code for {macro ...} to the output.
  258. * @param string
  259. * @param string
  260. * @param string
  261. * @param bool
  262. * @return MacroNode
  263. */
  264. public function writeMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE, HtmlNode $htmlNode = NULL)
  265. {
  266. if ($name[0] === '/') { // closing
  267. $node = end($this->macroNodes);
  268. if (!$node || ("/$node->name" !== $name && '/' !== $name) || $modifiers
  269. || ($args && $node->args && !Strings::startsWith("$node->args ", "$args "))
  270. ) {
  271. $name .= $args ? ' ' : '';
  272. throw new CompileException("Unexpected macro {{$name}{$args}{$modifiers}}"
  273. . ($node ? ", expecting {/$node->name}" . ($args && $node->args ? " or eventually {/$node->name $node->args}" : '') : ''));
  274. }
  275. array_pop($this->macroNodes);
  276. if (!$node->args) {
  277. $node->setArgs($args);
  278. }
  279. $isLeftmost = $node->content ? trim(substr($this->output, strrpos("\n$this->output", "\n"))) === '' : FALSE;
  280. $node->closing = TRUE;
  281. $node->macro->nodeClosed($node);
  282. $this->output = & $node->saved[0];
  283. $this->writeCode($node->openingCode, $this->output, $node->saved[1]);
  284. $this->writeCode($node->closingCode, $node->content, $isRightmost, $isLeftmost);
  285. $this->output .= $node->content;
  286. } else { // opening
  287. $node = $this->expandMacro($name, $args, $modifiers, $htmlNode);
  288. if ($node->isEmpty) {
  289. $this->writeCode($node->openingCode, $this->output, $isRightmost);
  290. } else {
  291. $this->macroNodes[] = $node;
  292. $node->saved = array(& $this->output, $isRightmost);
  293. $this->output = & $node->content;
  294. }
  295. }
  296. return $node;
  297. }
  298. private function writeCode($code, & $output, $isRightmost, $isLeftmost = NULL)
  299. {
  300. if ($isRightmost) {
  301. $leftOfs = strrpos("\n$output", "\n");
  302. $isLeftmost = $isLeftmost === NULL ? trim(substr($output, $leftOfs)) === '' : $isLeftmost;
  303. if ($isLeftmost && substr($code, 0, 11) !== '<?php echo ') {
  304. $output = substr($output, 0, $leftOfs); // alone macro without output -> remove indentation
  305. } elseif (substr($code, -2) === '?>') {
  306. $code .= "\n"; // double newline to avoid newline eating by PHP
  307. }
  308. }
  309. $output .= $code;
  310. }
  311. /**
  312. * Generates code for macro <tag n:attr> to the output.
  313. * @param string
  314. * @param array
  315. * @param bool
  316. * @return void
  317. */
  318. public function writeAttrsMacro($code, HtmlNode $htmlNode)
  319. {
  320. $attrs = $htmlNode->macroAttrs;
  321. $left = $right = array();
  322. $attrCode = '';
  323. foreach ($this->macros as $name => $foo) {
  324. $macro = $htmlNode->closing ? "/$name" : $name;
  325. if (isset($attrs[$name])) {
  326. if ($htmlNode->closing) {
  327. $right[] = array($macro, '');
  328. } else {
  329. array_unshift($left, array($macro, $attrs[$name]));
  330. }
  331. }
  332. $innerName = "inner-$name";
  333. if (isset($attrs[$innerName])) {
  334. if ($htmlNode->closing) {
  335. $left[] = array($macro, '');
  336. } else {
  337. array_unshift($right, array($macro, $attrs[$innerName]));
  338. }
  339. }
  340. $tagName = "tag-$name";
  341. if (isset($attrs[$tagName])) {
  342. array_unshift($left, array($name, $attrs[$tagName]));
  343. $right[] = array("/$name", '');
  344. }
  345. unset($attrs[$name], $attrs[$innerName], $attrs[$tagName]);
  346. }
  347. if ($attrs) {
  348. throw new CompileException("Unknown macro-attribute " . Parser::N_PREFIX
  349. . implode(' and ' . Parser::N_PREFIX, array_keys($attrs)));
  350. }
  351. if (!$htmlNode->closing) {
  352. $htmlNode->attrCode = & $this->attrCodes[$uniq = ' n:' . Nette\Utils\Strings::random()];
  353. $code = substr_replace($code, $uniq, strrpos($code, '/>') ?: strrpos($code, '>'), 0);
  354. }
  355. foreach ($left as $item) {
  356. $node = $this->writeMacro($item[0], $item[1], NULL, NULL, $htmlNode);
  357. if ($node->closing || $node->isEmpty) {
  358. $htmlNode->attrCode .= $node->attrCode;
  359. if ($node->isEmpty) {
  360. unset($htmlNode->macroAttrs[$node->name]);
  361. }
  362. }
  363. }
  364. $this->output .= $code;
  365. foreach ($right as $item) {
  366. $node = $this->writeMacro($item[0], $item[1], NULL, NULL, $htmlNode);
  367. if ($node->closing) {
  368. $htmlNode->attrCode .= $node->attrCode;
  369. }
  370. }
  371. if ($right && substr($this->output, -2) === '?>') {
  372. $this->output .= "\n";
  373. }
  374. }
  375. /**
  376. * Expands macro and returns node & code.
  377. * @param string
  378. * @param string
  379. * @param string
  380. * @return MacroNode
  381. */
  382. public function expandMacro($name, $args, $modifiers = NULL, HtmlNode $htmlNode = NULL)
  383. {
  384. if (empty($this->macros[$name])) {
  385. $js = $this->htmlNodes && strtolower(end($this->htmlNodes)->name) === 'script';
  386. throw new CompileException("Unknown macro {{$name}}" . ($js ? " (in JavaScript, try to put a space after bracket.)" : ''));
  387. }
  388. foreach (array_reverse($this->macros[$name]) as $macro) {
  389. $node = new MacroNode($macro, $name, $args, $modifiers, $this->macroNodes ? end($this->macroNodes) : NULL, $htmlNode);
  390. if ($macro->nodeOpened($node) !== FALSE) {
  391. return $node;
  392. }
  393. }
  394. throw new CompileException("Unhandled macro {{$name}}");
  395. }
  396. }