PageRenderTime 56ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/treeview-master/libs/Nette/Templates/Filters/LatteFilter.php

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