Source for file LatteFilter.php
Documentation is available at LatteFilter.php
6: * @copyright Copyright (c) 2004, 2010 David Grudl
7: * @license http://nettephp.com/license Nette license
8: * @link http://nettephp.com
10: * @package Nette\Templates
16: * Compile-time filter Latte.
18: * @copyright Copyright (c) 2004, 2010 David Grudl
19: * @package Nette\Templates
23: /** @ignore internal single & double quoted PHP string */
24: const RE_STRING =
'\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';
26: /** @ignore internal PHP identifier */
27: const RE_IDENTIFIER =
'[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF]*';
29: /** @ignore internal special HTML tag or attribute prefix */
30: const HTML_PREFIX =
'n:';
32: /** @var ILatteHandler */
39: private $input, $output;
44: /** @var strng (for CONTEXT_ATTRIBUTE) */
53: /**#@+ @ignore internal Context-aware escaping states */
54: const CONTEXT_TEXT =
'text';
55: const CONTEXT_CDATA =
'cdata';
56: const CONTEXT_TAG =
'tag';
57: const CONTEXT_ATTRIBUTE =
'attribute';
58: const CONTEXT_NONE =
'none';
59: const CONTEXT_COMMENT =
'comment';
65: * Sets a macro handler.
66: * @param ILatteHandler
67: * @return LatteFilter provides a fluent interface
71: $this->handler =
$handler;
78: * Returns macro handler.
79: * @return ILatteHandler
83: if ($this->handler ===
NULL) {
86: return $this->handler;
98: if (!$this->macroRe) {
102: // context-aware escaping
106: // initialize handlers
109: // process all {tags} and <tags/>
110: $s =
$this->parse("\n" .
$s);
120: * Searches for curly brackets, HTML tags and attributes.
124: private function parse($s)
126: $this->input =
& $s;
129: $this->tags =
array();
132: while ($this->offset <
$len) {
135: if (!$matches) { // EOF
138: } elseif (!empty($matches['macro'])) { // {macro|modifiers}
139: preg_match('#^(/?[a-z]+)?(.*?)(\\|[a-z](?:'.
self::RE_STRING.
'|[^\'"]+)*)?$()#is', $matches['macro'], $m2);
140: list(, $macro, $value, $modifiers) =
$m2;
141: $code =
$this->handler->macro($macro, trim($value), isset($modifiers) ?
$modifiers :
'');
142: if ($code ===
NULL) {
145: $nl =
isset($matches['newline']) ?
"\n" :
''; // double newline
146: if ($nl &&
$matches['indent'] &&
strncmp($code, '<?php echo ', 11)) {
147: $this->output .=
"\n" .
$code; // remove indent, single newline
149: $this->output .=
$matches['indent'] .
$code .
(substr($code, -
2) ===
'?>' ?
$nl :
'');
152: } else { // common behaviour
153: $this->output .=
$matches[0];
157: foreach ($this->tags as $tag) {
158: if (!$tag->isMacro &&
!empty($tag->attrs)) {
159: throw new InvalidStateException("Missing end tag </
$tag->name> for macro-attribute
" .
self::HTML_PREFIX .
implode(' and ' .
self::HTML_PREFIX, array_keys($tag->attrs)) .
".");
163: return $this->output .
substr($this->input, $this->offset);
169: * Handles CONTEXT_TEXT.
171: private function contextText()
173: $matches =
$this->match('~
174: (?:\n[ \t]*)?<(?P<closing>/?)(?P<tag>[a-z0-9:]+)| ## begin of HTML tag <tag </tag - ignores <!DOCTYPE
175: <(?P<comment>!--)| ## begin of HTML comment <!--
176: '.
$this->macroRe.
' ## curly tag
179: if (!$matches ||
!empty($matches['macro'])) { // EOF or {macro}
181: } elseif (!empty($matches['comment'])) { // <!--
183: $this->escape =
'TemplateHelpers::escapeHtmlComment';
185: } elseif (empty($matches['closing'])) { // <tag
186: $tag =
$this->tags[] = (object)
NULL;
187: $tag->name =
$matches['tag'];
188: $tag->closing =
FALSE;
190: $tag->attrs =
array();
193: $this->escape =
'TemplateHelpers::escapeHtml';
199: //throw new InvalidStateException("End tag for element '$matches[tag]' which is not open on line $this->line.");
200: $tag = (object)
NULL;
201: $tag->name =
$matches['tag'];
205: $this->tags[] =
$tag;
206: $tag->closing =
TRUE;
209: $this->escape =
'TemplateHelpers::escapeHtml';
217: * Handles CONTEXT_CDATA.
219: private function contextCData()
222: $matches =
$this->match('~
223: </'.
$tag->name.
'(?![a-z0-9:])| ## end HTML tag </tag
224: '.
$this->macroRe.
' ## curly tag
227: if ($matches &&
empty($matches['macro'])) { // </tag
228: $tag->closing =
TRUE;
231: $this->escape =
'TemplateHelpers::escapeHtml';
239: * Handles CONTEXT_TAG.
241: private function contextTag()
243: $matches =
$this->match('~
244: (?P<end>/?>)(?P<tagnewline>[\ \t]*(?=\r|\n))?| ## end of HTML tag
245: '.
$this->macroRe.
'| ## curly tag
246: \s*(?P<attr>[^\s/>={]+)(?:\s*=\s*(?P<value>["\']|[^\s/>{]+))? ## begin of HTML attribute
249: if (!$matches ||
!empty($matches['macro'])) { // EOF or {macro}
251: } elseif (!empty($matches['end'])) { // end of HTML tag />
253: $isEmpty =
!$tag->closing &&
($matches['end'][0] ===
'/' ||
isset(Html::$emptyElements[strtolower($tag->name)]));
255: if ($tag->isMacro ||
!empty($tag->attrs)) {
256: if ($tag->isMacro) {
257: $code =
$this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, $tag->closing);
258: if ($code ===
NULL) {
262: $code .=
$this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, TRUE);
265: $code =
substr($this->output, $tag->pos) .
$matches[0] .
(isset($matches['tagnewline']) ?
"\n" :
'');
266: $code =
$this->handler->attrsMacro($code, $tag->attrs, $tag->closing);
267: if ($code ===
NULL) {
271: $code =
$this->handler->attrsMacro($code, $tag->attrs, TRUE);
275: $matches[0] =
''; // remove from output
279: $tag->closing =
TRUE;
284: $this->escape =
strcasecmp($tag->name, 'style') ?
'TemplateHelpers::escapeJs' :
'TemplateHelpers::escapeCss';
287: $this->escape =
'TemplateHelpers::escapeHtml';
291: } else { // HTML attribute
292: $name =
$matches['attr'];
293: $value =
empty($matches['value']) ?
TRUE :
$matches['value'];
295: // special attribute?
300: if ($isSpecial ||
$tag->isMacro) {
301: if ($value ===
'"' ||
$value ===
"'") {
302: if ($matches =
$this->match('~(.*?)' .
$value .
'~xsi')) { // overwrites $matches
303: $value =
$matches[1];
306: $tag->attrs[$name] =
$value;
307: $matches[0] =
''; // remove from output
309: } elseif ($value ===
'"' ||
$value ===
"'") { // attribute = "'
311: $this->quote =
$value;
313: ?
(strcasecmp($name, 'style') ?
'TemplateHelpers::escapeHtml' :
'TemplateHelpers::escapeHtmlCss')
314: :
'TemplateHelpers::escapeHtmlJs';
323: * Handles CONTEXT_ATTRIBUTE.
325: private function contextAttribute()
327: $matches =
$this->match('~
328: (' .
$this->quote .
')| ## 1) end of HTML attribute
329: '.
$this->macroRe.
' ## curly tag
332: if ($matches &&
empty($matches['macro'])) { // (attribute end) '"
334: $this->escape =
'TemplateHelpers::escapeHtml';
342: * Handles CONTEXT_COMMENT.
344: private function contextComment()
346: $matches =
$this->match('~
347: (--\s*>)| ## 1) end of HTML comment
348: '.
$this->macroRe.
' ## curly tag
351: if ($matches &&
empty($matches['macro'])) { // --\s*>
353: $this->escape =
'TemplateHelpers::escapeHtml';
361: * Handles CONTEXT_NONE.
363: private function contextNone()
365: $matches =
$this->match('~
366: '.
$this->macroRe.
' ## curly tag
374: * Matches next token.
378: private function match($re)
380: if (preg_match($re, $this->input, $matches, PREG_OFFSET_CAPTURE, $this->offset)) {
381: $this->output .=
substr($this->input, $this->offset, $matches[0][1] -
$this->offset);
382: $this->offset =
$matches[0][1] +
strlen($matches[0][0]);
383: foreach ($matches as $k =>
$v) $matches[$k] =
$v[0];
391: * Returns current line number.
402: * Changes macro delimiters.
403: * @param string left regular expression
404: * @param string right regular expression
405: * @return LatteFilter provides a fluent interface
410: (?P<indent>\n[\ \t]*)?
412: (?P<macro>(?:' .
self::RE_STRING .
'|[^\'"]+?)*?)
414: (?P<newline>[\ \t]*(?=\r|\n))?
421: /********************* compile-time helpers ****************d*g**/
426: * Applies modifiers.
433: if (!$modifiers) return $var;
436: '.
self::RE_STRING.
'| ## single or double quoted string
437: [^\'"|:,\s]+| ## symbol
445: foreach ($tokens[0] as $token) {
446: if ($token ===
'|' ||
$token ===
':' ||
$token ===
',') {
449: } elseif (!$inside) {
453: $var =
"\$template->$prev($var";
458: $var .=
', ' .
self::formatString($prev);
462: if ($token ===
'|' &&
$inside) {
476: * Reads single token (optionally delimited by comma) from string.
482: if (preg_match('#^((?>'.
self::RE_STRING.
'|[^\'"\s,]+)+)\s*,?\s*(.*)$#', $s, $matches)) { // token [,] tail
492: * Formats parameters to PHP array.
501: '.
self::RE_STRING.
'| ## single or double quoted string
502: (?<=[,=(]|=>|^)\s*([a-z\d_]+)(?=\s*[,=)]|$) ## 1) symbol
504: array(__CLASS__
, 'cbArgs'),
507: $s =
preg_replace('#\$(' .
self::RE_IDENTIFIER .
')\s*=>#', '"$1" =>', $s);
508: return $s ===
'' ?
'' :
$prefix .
"array($s)";
514: * Callback for formatArgs().
516: private static function cbArgs($matches)
520: if (!empty($matches[1])) { // symbol
521: list(, $symbol) =
$matches;
522: static $keywords =
array('true'=>
1, 'false'=>
1, 'null'=>
1, 'and'=>
1, 'or'=>
1, 'xor'=>
1, 'clone'=>
1, 'new'=>
1);
523: return is_numeric($symbol) ||
isset($keywords[strtolower($symbol)]) ?
$matches[0] :
"'
$symbol'
";
533: * Formats parameter to PHP string.
539: static $keywords =
array('true'=>
1, 'false'=>
1, 'null'=>
1);
540: return (is_numeric($s) ||
strspn($s, '\'"$') ||
isset($keywords[strtolower($s)])) ?
$s :
'"' .
$s .
'"';
551: trigger_error(__METHOD__ .
'() is deprecated; use non-static __invoke() instead.', E_USER_WARNING);