PageRenderTime 78ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/Nette/Templates/Filters/LatteFilter.php

https://github.com/DocX/nette
PHP | 573 lines | 336 code | 112 blank | 125 comment | 64 complexity | 33ca0931bd21b76683d2b77f019de3a1 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Nette Framework
  4. *
  5. * Copyright (c) 2004, 2009 David Grudl (http://davidgrudl.com)
  6. *
  7. * This source file is subject to the "Nette license" that is bundled
  8. * with this package in the file license.txt.
  9. *
  10. * For more information please see http://nettephp.com
  11. *
  12. * @copyright Copyright (c) 2004, 2009 David Grudl
  13. * @license http://nettephp.com/license Nette license
  14. * @link http://nettephp.com
  15. * @category Nette
  16. * @package Nette\Templates
  17. */
  18. /*namespace Nette\Templates;*/
  19. require_once dirname(__FILE__) . '/../../Object.php';
  20. /**
  21. * Compile-time filter Latte.
  22. *
  23. * @author David Grudl
  24. * @copyright Copyright (c) 2004, 2009 David Grudl
  25. * @package Nette\Templates
  26. */
  27. class LatteFilter extends /*Nette\*/Object
  28. {
  29. /** single & double quoted PHP string */
  30. const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';
  31. /** PHP identifier */
  32. const RE_IDENTIFIER = '[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF]*';
  33. /** spcial HTML tag or attribute prefix */
  34. const HTML_PREFIX = 'n:';
  35. /** @var ILatteHandler */
  36. private $handler;
  37. /** @var string */
  38. private $macroRe;
  39. /** @var string */
  40. private $input, $output;
  41. /** @var int */
  42. private $offset;
  43. /** @var strng (for CONTEXT_ATTRIBUTE) */
  44. private $quote;
  45. /** @var array */
  46. private $tags;
  47. /** @var string */
  48. public $context, $escape;
  49. /**#@+ Context-aware escaping states */
  50. const CONTEXT_TEXT = 'text';
  51. const CONTEXT_CDATA = 'cdata';
  52. const CONTEXT_TAG = 'tag';
  53. const CONTEXT_ATTRIBUTE = 'attribute';
  54. const CONTEXT_NONE = 'none';
  55. const CONTEXT_COMMENT = 'comment';
  56. /**#@-*/
  57. /**
  58. * Sets a macro handler.
  59. * @param ILatteHandler
  60. * @return LatteFilter provides a fluent interface
  61. */
  62. public function setHandler($handler)
  63. {
  64. $this->handler = $handler;
  65. return $this;
  66. }
  67. /**
  68. * Returns macro handler.
  69. * @return ILatteHandler
  70. */
  71. public function getHandler()
  72. {
  73. if ($this->handler === NULL) {
  74. $this->handler = new LatteMacros;
  75. }
  76. return $this->handler;
  77. }
  78. /**
  79. * Invokes filter.
  80. * @param string
  81. * @return string
  82. */
  83. public function __invoke($s)
  84. {
  85. if (!$this->macroRe) {
  86. $this->setDelimiters('\\{(?![\\s\'"{}])', '\\}');
  87. }
  88. // context-aware escaping
  89. $this->context = LatteFilter::CONTEXT_NONE;
  90. $this->escape = '$template->escape';
  91. // initialize handlers
  92. $this->getHandler()->initialize($this, $s);
  93. // process all {tags} and <tags/>
  94. $s = $this->parse("\n" . $s);
  95. $this->getHandler()->finalize($s);
  96. return $s;
  97. }
  98. /**
  99. * Searches for curly brackets, HTML tags and attributes.
  100. * @param string
  101. * @return string
  102. */
  103. private function parse($s)
  104. {
  105. $this->input = & $s;
  106. $this->offset = 0;
  107. $this->output = '';
  108. $this->tags = array();
  109. $len = strlen($s);
  110. while ($this->offset < $len) {
  111. $matches = $this->{"context$this->context"}();
  112. if (!$matches) { // EOF
  113. break;
  114. } elseif (!empty($matches['macro'])) { // {macro|modifiers}
  115. preg_match('#^(/?[a-z]+)?(.*?)(\\|[a-z](?:'.self::RE_STRING.'|[^\'"\s]+)*)?$()#is', $matches['macro'], $m2);
  116. list(, $macro, $value, $modifiers) = $m2;
  117. $code = $this->handler->macro($macro, trim($value), isset($modifiers) ? $modifiers : '');
  118. if ($code === NULL) {
  119. throw new /*\*/InvalidStateException("Unknown macro {{$matches['macro']}} on line $this->line.");
  120. }
  121. $nl = isset($matches['newline']) ? "\n" : ''; // double newline
  122. if ($nl && $matches['indent'] && strncmp($code, '<?php echo ', 11)) {
  123. $this->output .= "\n" . $code; // remove indent, single newline
  124. } else {
  125. $this->output .= $matches['indent'] . $code . (substr($code, -2) === '?>' ? $nl : '');
  126. }
  127. } else { // common behaviour
  128. $this->output .= $matches[0];
  129. }
  130. }
  131. foreach ($this->tags as $tag) {
  132. if (!$tag->isMacro && !empty($tag->attrs)) {
  133. throw new /*\*/InvalidStateException("Missing end tag </$tag->name> for macro-attribute " . self::HTML_PREFIX . implode(' and ' . self::HTML_PREFIX, array_keys($tag->attrs)) . ".");
  134. }
  135. }
  136. return $this->output . substr($this->input, $this->offset);
  137. }
  138. /**
  139. * Handles CONTEXT_TEXT.
  140. */
  141. private function contextText()
  142. {
  143. $matches = $this->match('~
  144. (?:\n[ \t]*)?<(?P<closing>/?)(?P<tag>[a-z0-9:]+)| ## begin of HTML tag <tag </tag - ignores <!DOCTYPE
  145. <(?P<comment>!--)| ## begin of HTML comment <!--
  146. '.$this->macroRe.' ## curly tag
  147. ~xsi');
  148. if (!$matches || !empty($matches['macro'])) { // EOF or {macro}
  149. } elseif (!empty($matches['comment'])) { // <!--
  150. $this->context = self::CONTEXT_COMMENT;
  151. $this->escape = 'TemplateHelpers::escapeHtmlComment';
  152. } elseif (empty($matches['closing'])) { // <tag
  153. $tag = $this->tags[] = (object) NULL;
  154. $tag->name = $matches['tag'];
  155. $tag->closing = FALSE;
  156. $tag->isMacro = /*Nette\*/String::startsWith($tag->name, self::HTML_PREFIX);
  157. $tag->attrs = array();
  158. $tag->pos = strlen($this->output);
  159. $this->context = self::CONTEXT_TAG;
  160. $this->escape = 'TemplateHelpers::escapeHtml';
  161. } else { // </tag
  162. do {
  163. $tag = array_pop($this->tags);
  164. if (!$tag) {
  165. //throw new /*\*/InvalidStateException("End tag for element '$matches[tag]' which is not open on line $this->line.");
  166. $tag = (object) NULL;
  167. $tag->name = $matches['tag'];
  168. $tag->isMacro = /*Nette\*/String::startsWith($tag->name, self::HTML_PREFIX);
  169. }
  170. } while (strcasecmp($tag->name, $matches['tag']));
  171. $this->tags[] = $tag;
  172. $tag->closing = TRUE;
  173. $tag->pos = strlen($this->output);
  174. $this->context = self::CONTEXT_TAG;
  175. $this->escape = 'TemplateHelpers::escapeHtml';
  176. }
  177. return $matches;
  178. }
  179. /**
  180. * Handles CONTEXT_CDATA.
  181. */
  182. private function contextCData()
  183. {
  184. $tag = end($this->tags);
  185. $matches = $this->match('~
  186. </'.$tag->name.'(?![a-z0-9:])| ## end HTML tag </tag
  187. '.$this->macroRe.' ## curly tag
  188. ~xsi');
  189. if ($matches && empty($matches['macro'])) { // </tag
  190. $tag->closing = TRUE;
  191. $tag->pos = strlen($this->output);
  192. $this->context = self::CONTEXT_TAG;
  193. $this->escape = 'TemplateHelpers::escapeHtml';
  194. }
  195. return $matches;
  196. }
  197. /**
  198. * Handles CONTEXT_TAG.
  199. */
  200. private function contextTag()
  201. {
  202. $matches = $this->match('~
  203. (?P<end>/?>)(?P<tagnewline>[\ \t]*(?=\r|\n))?| ## end of HTML tag
  204. '.$this->macroRe.'| ## curly tag
  205. \s*(?P<attr>[^\s/>={]+)(?:\s*=\s*(?P<value>["\']|[^\s/>{]+))? ## begin of HTML attribute
  206. ~xsi');
  207. if (!$matches || !empty($matches['macro'])) { // EOF or {macro}
  208. } elseif (!empty($matches['end'])) { // end of HTML tag />
  209. $tag = end($this->tags);
  210. $isEmpty = !$tag->closing && ($matches['end'][0] === '/' || isset(/*Nette\Web\*/Html::$emptyElements[strtolower($tag->name)]));
  211. if ($tag->isMacro || !empty($tag->attrs)) {
  212. if ($tag->isMacro) {
  213. $code = $this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, $tag->closing);
  214. if ($code === NULL) {
  215. throw new /*\*/InvalidStateException("Unknown tag-macro <$tag->name> on line $this->line.");
  216. }
  217. if ($isEmpty) {
  218. $code .= $this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, TRUE);
  219. }
  220. } else {
  221. $code = substr($this->output, $tag->pos) . $matches[0] . (isset($matches['tagnewline']) ? "\n" : '');
  222. $code = $this->handler->attrsMacro($code, $tag->attrs, $tag->closing);
  223. if ($code === NULL) {
  224. throw new /*\*/InvalidStateException("Unknown macro-attribute " . self::HTML_PREFIX . implode(' or ' . self::HTML_PREFIX, array_keys($tag->attrs)) . " on line $this->line.");
  225. }
  226. if ($isEmpty) {
  227. $code = $this->handler->attrsMacro($code, $tag->attrs, TRUE);
  228. }
  229. }
  230. $this->output = substr_replace($this->output, $code, $tag->pos);
  231. $matches[0] = ''; // remove from output
  232. }
  233. if ($isEmpty) {
  234. $tag->closing = TRUE;
  235. }
  236. if (!$tag->closing && (strcasecmp($tag->name, 'script') === 0 || strcasecmp($tag->name, 'style') === 0)) {
  237. $this->context = self::CONTEXT_CDATA;
  238. $this->escape = strcasecmp($tag->name, 'style') ? 'TemplateHelpers::escapeJs' : 'TemplateHelpers::escapeCss';
  239. } else {
  240. $this->context = self::CONTEXT_TEXT;
  241. $this->escape = 'TemplateHelpers::escapeHtml';
  242. if ($tag->closing) array_pop($this->tags);
  243. }
  244. } else { // HTML attribute
  245. $name = $matches['attr'];
  246. $value = empty($matches['value']) ? TRUE : $matches['value'];
  247. // special attribute?
  248. if ($isSpecial = /*Nette\*/String::startsWith($name, self::HTML_PREFIX)) {
  249. $name = substr($name, strlen(self::HTML_PREFIX));
  250. }
  251. $tag = end($this->tags);
  252. if ($isSpecial || $tag->isMacro) {
  253. if ($value === '"' || $value === "'") {
  254. if ($matches = $this->match('~(.*?)' . $value . '~xsi')) { // overwrites $matches
  255. $value = $matches[1];
  256. }
  257. }
  258. $tag->attrs[$name] = $value;
  259. $matches[0] = ''; // remove from output
  260. } elseif ($value === '"' || $value === "'") { // attribute = "'
  261. $this->context = self::CONTEXT_ATTRIBUTE;
  262. $this->quote = $value;
  263. $this->escape = strncasecmp($name, 'on', 2)
  264. ? (strcasecmp($name, 'style') ? 'TemplateHelpers::escapeHtml' : 'TemplateHelpers::escapeHtmlCss')
  265. : 'TemplateHelpers::escapeHtmlJs';
  266. }
  267. }
  268. return $matches;
  269. }
  270. /**
  271. * Handles CONTEXT_ATTRIBUTE.
  272. */
  273. private function contextAttribute()
  274. {
  275. $matches = $this->match('~
  276. (' . $this->quote . ')| ## 1) end of HTML attribute
  277. '.$this->macroRe.' ## curly tag
  278. ~xsi');
  279. if ($matches && empty($matches['macro'])) { // (attribute end) '"
  280. $this->context = self::CONTEXT_TAG;
  281. $this->escape = 'TemplateHelpers::escapeHtml';
  282. }
  283. return $matches;
  284. }
  285. /**
  286. * Handles CONTEXT_COMMENT.
  287. */
  288. private function contextComment()
  289. {
  290. $matches = $this->match('~
  291. (--\s*>)| ## 1) end of HTML comment
  292. '.$this->macroRe.' ## curly tag
  293. ~xsi');
  294. if ($matches && empty($matches['macro'])) { // --\s*>
  295. $this->context = self::CONTEXT_TEXT;
  296. $this->escape = 'TemplateHelpers::escapeHtml';
  297. }
  298. return $matches;
  299. }
  300. /**
  301. * Handles CONTEXT_NONE.
  302. */
  303. private function contextNone()
  304. {
  305. $matches = $this->match('~
  306. '.$this->macroRe.' ## curly tag
  307. ~xsi');
  308. return $matches;
  309. }
  310. /**
  311. * Matches next token.
  312. * @param string
  313. * @return array
  314. */
  315. private function match($re)
  316. {
  317. if (preg_match($re, $this->input, $matches, PREG_OFFSET_CAPTURE, $this->offset)) {
  318. $this->output .= substr($this->input, $this->offset, $matches[0][1] - $this->offset);
  319. $this->offset = $matches[0][1] + strlen($matches[0][0]);
  320. foreach ($matches as $k => $v) $matches[$k] = $v[0];
  321. }
  322. return $matches;
  323. }
  324. /**
  325. * Returns current line number.
  326. * @return int
  327. */
  328. public function getLine()
  329. {
  330. return substr_count($this->input, "\n", 0, $this->offset);
  331. }
  332. /**
  333. * Changes macro delimiters.
  334. * @param string left regular expression
  335. * @param string right regular expression
  336. * @return LatteFilter provides a fluent interface
  337. */
  338. public function setDelimiters($left, $right)
  339. {
  340. $this->macroRe = '
  341. (?P<indent>\n[\ \t]*)?
  342. ' . $left . '
  343. (?P<macro>(?:' . self::RE_STRING . '|[^\'"]+?)*?)
  344. ' . $right . '
  345. (?P<newline>[\ \t]*(?=\r|\n))?
  346. ';
  347. return $this;
  348. }
  349. /********************* compile-time helpers ****************d*g**/
  350. /**
  351. * Applies modifiers.
  352. * @param string
  353. * @param string
  354. * @return string
  355. */
  356. public static function formatModifiers($var, $modifiers)
  357. {
  358. if (!$modifiers) return $var;
  359. preg_match_all(
  360. '~
  361. '.self::RE_STRING.'| ## single or double quoted string
  362. [^\'"|:,]+| ## symbol
  363. [|:,] ## separator
  364. ~xs',
  365. $modifiers . '|',
  366. $tokens
  367. );
  368. $inside = FALSE;
  369. $prev = '';
  370. foreach ($tokens[0] as $token) {
  371. if ($token === '|' || $token === ':' || $token === ',') {
  372. if ($prev === '') {
  373. } elseif (!$inside) {
  374. if (!preg_match('#^'.self::RE_IDENTIFIER.'$#', $prev)) {
  375. throw new /*\*/InvalidStateException("Modifier name must be alphanumeric string, '$prev' given.");
  376. }
  377. $var = "\$template->$prev($var";
  378. $prev = '';
  379. $inside = TRUE;
  380. } else {
  381. $var .= ', ' . self::formatString($prev);
  382. $prev = '';
  383. }
  384. if ($token === '|' && $inside) {
  385. $var .= ')';
  386. $inside = FALSE;
  387. }
  388. } else {
  389. $prev .= $token;
  390. }
  391. }
  392. return $var;
  393. }
  394. /**
  395. * Reads single token (optionally delimited by comma) from string.
  396. * @param string
  397. * @return string
  398. */
  399. public static function fetchToken(& $s)
  400. {
  401. if (preg_match('#^((?>'.self::RE_STRING.'|[^\'"\s,]+)+)\s*,?\s*(.*)$#', $s, $matches)) { // token [,] tail
  402. $s = $matches[2];
  403. return $matches[1];
  404. }
  405. return NULL;
  406. }
  407. /**
  408. * Formats parameters to PHP array.
  409. * @param string
  410. * @param string
  411. * @return string
  412. */
  413. public static function formatArray($s, $prefix = '')
  414. {
  415. $s = preg_replace_callback(
  416. '~
  417. '.self::RE_STRING.'| ## single or double quoted string
  418. (?<=[,=(]|=>|^)\s*([a-z\d_]+)(?=\s*[,=)]|$) ## 1) symbol
  419. ~xi',
  420. array(__CLASS__, 'cbArgs'),
  421. trim($s)
  422. );
  423. return $s === '' ? '' : $prefix . "array($s)";
  424. }
  425. /**
  426. * Callback for formatArgs().
  427. */
  428. private static function cbArgs($matches)
  429. {
  430. // [1] => symbol
  431. if (!empty($matches[1])) { // symbol
  432. list(, $symbol) = $matches;
  433. static $keywords = array('true'=>1, 'false'=>1, 'null'=>1, 'and'=>1, 'or'=>1, 'xor'=>1, 'clone'=>1, 'new'=>1);
  434. return is_numeric($symbol) || isset($keywords[strtolower($symbol)]) ? $matches[0] : "'$symbol'";
  435. } else {
  436. return $matches[0];
  437. }
  438. }
  439. /**
  440. * Formats parameter to PHP string.
  441. * @param string
  442. * @return string
  443. */
  444. public static function formatString($s)
  445. {
  446. return (is_numeric($s) || strspn($s, '\'"$')) ? $s : '"' . $s . '"';
  447. }
  448. /**
  449. * Invokes filter.
  450. * @deprecated
  451. */
  452. public static function invoke($s)
  453. {
  454. $filter = new self;
  455. return $filter->__invoke($s);
  456. }
  457. }
  458. /** @deprecated */
  459. class CurlyBracketsFilter extends LatteFilter {}
  460. class CurlyBracketsMacros extends LatteMacros {}