Source for file LatteFilter.php
Documentation is available at LatteFilter.php
6: * Copyright (c) 2004, 2009 David Grudl (http://davidgrudl.com)
8: * This source file is subject to the "Nette license" that is bundled
9: * with this package in the file license.txt.
11: * For more information please see http://nettephp.com
13: * @copyright Copyright (c) 2004, 2009 David Grudl
14: * @license http://nettephp.com/license Nette license
15: * @link http://nettephp.com
17: * @package Nette\Templates
22: require_once dirname(__FILE__) .
'/../../Object.php';
27: * Compile-time filter Latte.
29: * @author David Grudl
30: * @copyright Copyright (c) 2004, 2009 David Grudl
31: * @package Nette\Templates
35: /** @ignore internal single & double quoted PHP string */
36: const RE_STRING =
'\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';
38: /** @ignore internal PHP identifier */
39: const RE_IDENTIFIER =
'[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF]*';
41: /** @ignore internal special HTML tag or attribute prefix */
42: const HTML_PREFIX =
'n:';
44: /** @var ILatteHandler */
51: private $input, $output;
56: /** @var strng (for CONTEXT_ATTRIBUTE) */
65: /**#@+ @ignore internal Context-aware escaping states */
66: const CONTEXT_TEXT =
'text';
67: const CONTEXT_CDATA =
'cdata';
68: const CONTEXT_TAG =
'tag';
69: const CONTEXT_ATTRIBUTE =
'attribute';
70: const CONTEXT_NONE =
'none';
71: const CONTEXT_COMMENT =
'comment';
77: * Sets a macro handler.
78: * @param ILatteHandler
79: * @return LatteFilter provides a fluent interface
83: $this->handler =
$handler;
90: * Returns macro handler.
91: * @return ILatteHandler
95: if ($this->handler ===
NULL) {
98: return $this->handler;
110: if (!$this->macroRe) {
114: // context-aware escaping
118: // initialize handlers
121: // process all {tags} and <tags/>
122: $s =
$this->parse("\n" .
$s);
132: * Searches for curly brackets, HTML tags and attributes.
136: private function parse($s)
138: $this->input =
& $s;
141: $this->tags =
array();
144: while ($this->offset <
$len) {
147: if (!$matches) { // EOF
150: } elseif (!empty($matches['macro'])) { // {macro|modifiers}
151: preg_match('#^(/?[a-z]+)?(.*?)(\\|[a-z](?:'.
self::RE_STRING.
'|[^\'"\s]+)*)?$()#is', $matches['macro'], $m2);
152: list(, $macro, $value, $modifiers) =
$m2;
153: $code =
$this->handler->macro($macro, trim($value), isset($modifiers) ?
$modifiers :
'');
154: if ($code ===
NULL) {
157: $nl =
isset($matches['newline']) ?
"\n" :
''; // double newline
158: if ($nl &&
$matches['indent'] &&
strncmp($code, '<?php echo ', 11)) {
159: $this->output .=
"\n" .
$code; // remove indent, single newline
161: $this->output .=
$matches['indent'] .
$code .
(substr($code, -
2) ===
'?>' ?
$nl :
'');
164: } else { // common behaviour
165: $this->output .=
$matches[0];
169: foreach ($this->tags as $tag) {
170: if (!$tag->isMacro &&
!empty($tag->attrs)) {
171: throw new InvalidStateException("Missing end tag </
$tag->name> for macro-attribute
" .
self::HTML_PREFIX .
implode(' and ' .
self::HTML_PREFIX, array_keys($tag->attrs)) .
".");
175: return $this->output .
substr($this->input, $this->offset);
181: * Handles CONTEXT_TEXT.
183: private function contextText()
185: $matches =
$this->match('~
186: (?:\n[ \t]*)?<(?P<closing>/?)(?P<tag>[a-z0-9:]+)| ## begin of HTML tag <tag </tag - ignores <!DOCTYPE
187: <(?P<comment>!--)| ## begin of HTML comment <!--
188: '.
$this->macroRe.
' ## curly tag
191: if (!$matches ||
!empty($matches['macro'])) { // EOF or {macro}
193: } elseif (!empty($matches['comment'])) { // <!--
195: $this->escape =
'TemplateHelpers::escapeHtmlComment';
197: } elseif (empty($matches['closing'])) { // <tag
198: $tag =
$this->tags[] = (object)
NULL;
199: $tag->name =
$matches['tag'];
200: $tag->closing =
FALSE;
202: $tag->attrs =
array();
205: $this->escape =
'TemplateHelpers::escapeHtml';
211: //throw new InvalidStateException("End tag for element '$matches[tag]' which is not open on line $this->line.");
212: $tag = (object)
NULL;
213: $tag->name =
$matches['tag'];
217: $this->tags[] =
$tag;
218: $tag->closing =
TRUE;
221: $this->escape =
'TemplateHelpers::escapeHtml';
229: * Handles CONTEXT_CDATA.
231: private function contextCData()
234: $matches =
$this->match('~
235: </'.
$tag->name.
'(?![a-z0-9:])| ## end HTML tag </tag
236: '.
$this->macroRe.
' ## curly tag
239: if ($matches &&
empty($matches['macro'])) { // </tag
240: $tag->closing =
TRUE;
243: $this->escape =
'TemplateHelpers::escapeHtml';
251: * Handles CONTEXT_TAG.
253: private function contextTag()
255: $matches =
$this->match('~
256: (?P<end>/?>)(?P<tagnewline>[\ \t]*(?=\r|\n))?| ## end of HTML tag
257: '.
$this->macroRe.
'| ## curly tag
258: \s*(?P<attr>[^\s/>={]+)(?:\s*=\s*(?P<value>["\']|[^\s/>{]+))? ## begin of HTML attribute
261: if (!$matches ||
!empty($matches['macro'])) { // EOF or {macro}
263: } elseif (!empty($matches['end'])) { // end of HTML tag />
265: $isEmpty =
!$tag->closing &&
($matches['end'][0] ===
'/' ||
isset(Html::$emptyElements[strtolower($tag->name)]));
267: if ($tag->isMacro ||
!empty($tag->attrs)) {
268: if ($tag->isMacro) {
269: $code =
$this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, $tag->closing);
270: if ($code ===
NULL) {
274: $code .=
$this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, TRUE);
277: $code =
substr($this->output, $tag->pos) .
$matches[0] .
(isset($matches['tagnewline']) ?
"\n" :
'');
278: $code =
$this->handler->attrsMacro($code, $tag->attrs, $tag->closing);
279: if ($code ===
NULL) {
283: $code =
$this->handler->attrsMacro($code, $tag->attrs, TRUE);
287: $matches[0] =
''; // remove from output
291: $tag->closing =
TRUE;
296: $this->escape =
strcasecmp($tag->name, 'style') ?
'TemplateHelpers::escapeJs' :
'TemplateHelpers::escapeCss';
299: $this->escape =
'TemplateHelpers::escapeHtml';
303: } else { // HTML attribute
304: $name =
$matches['attr'];
305: $value =
empty($matches['value']) ?
TRUE :
$matches['value'];
307: // special attribute?
312: if ($isSpecial ||
$tag->isMacro) {
313: if ($value ===
'"' ||
$value ===
"'") {
314: if ($matches =
$this->match('~(.*?)' .
$value .
'~xsi')) { // overwrites $matches
315: $value =
$matches[1];
318: $tag->attrs[$name] =
$value;
319: $matches[0] =
''; // remove from output
321: } elseif ($value ===
'"' ||
$value ===
"'") { // attribute = "'
323: $this->quote =
$value;
325: ?
(strcasecmp($name, 'style') ?
'TemplateHelpers::escapeHtml' :
'TemplateHelpers::escapeHtmlCss')
326: :
'TemplateHelpers::escapeHtmlJs';
335: * Handles CONTEXT_ATTRIBUTE.
337: private function contextAttribute()
339: $matches =
$this->match('~
340: (' .
$this->quote .
')| ## 1) end of HTML attribute
341: '.
$this->macroRe.
' ## curly tag
344: if ($matches &&
empty($matches['macro'])) { // (attribute end) '"
346: $this->escape =
'TemplateHelpers::escapeHtml';
354: * Handles CONTEXT_COMMENT.
356: private function contextComment()
358: $matches =
$this->match('~
359: (--\s*>)| ## 1) end of HTML comment
360: '.
$this->macroRe.
' ## curly tag
363: if ($matches &&
empty($matches['macro'])) { // --\s*>
365: $this->escape =
'TemplateHelpers::escapeHtml';
373: * Handles CONTEXT_NONE.
375: private function contextNone()
377: $matches =
$this->match('~
378: '.
$this->macroRe.
' ## curly tag
386: * Matches next token.
390: private function match($re)
392: if (preg_match($re, $this->input, $matches, PREG_OFFSET_CAPTURE, $this->offset)) {
393: $this->output .=
substr($this->input, $this->offset, $matches[0][1] -
$this->offset);
394: $this->offset =
$matches[0][1] +
strlen($matches[0][0]);
395: foreach ($matches as $k =>
$v) $matches[$k] =
$v[0];
403: * Returns current line number.
414: * Changes macro delimiters.
415: * @param string left regular expression
416: * @param string right regular expression
417: * @return LatteFilter provides a fluent interface
422: (?P<indent>\n[\ \t]*)?
424: (?P<macro>(?:' .
self::RE_STRING .
'|[^\'"]+?)*?)
426: (?P<newline>[\ \t]*(?=\r|\n))?
433: /********************* compile-time helpers ****************d*g**/
438: * Applies modifiers.
445: if (!$modifiers) return $var;
448: '.
self::RE_STRING.
'| ## single or double quoted string
449: [^\'"|:,]+| ## symbol
457: foreach ($tokens[0] as $token) {
458: if ($token ===
'|' ||
$token ===
':' ||
$token ===
',') {
461: } elseif (!$inside) {
465: $var =
"\$template->$prev($var";
470: $var .=
', ' .
self::formatString($prev);
474: if ($token ===
'|' &&
$inside) {
488: * Reads single token (optionally delimited by comma) from string.
494: if (preg_match('#^((?>'.
self::RE_STRING.
'|[^\'"\s,]+)+)\s*,?\s*(.*)$#', $s, $matches)) { // token [,] tail
504: * Formats parameters to PHP array.
513: '.
self::RE_STRING.
'| ## single or double quoted string
514: (?<=[,=(]|=>|^)\s*([a-z\d_]+)(?=\s*[,=)]|$) ## 1) symbol
516: array(__CLASS__
, 'cbArgs'),
519: return $s ===
'' ?
'' :
$prefix .
"array($s)";
525: * Callback for formatArgs().
527: private static function cbArgs($matches)
531: if (!empty($matches[1])) { // symbol
532: list(, $symbol) =
$matches;
533: static $keywords =
array('true'=>
1, 'false'=>
1, 'null'=>
1, 'and'=>
1, 'or'=>
1, 'xor'=>
1, 'clone'=>
1, 'new'=>
1);
534: return is_numeric($symbol) ||
isset($keywords[strtolower($symbol)]) ?
$matches[0] :
"'
$symbol'
";
544: * Formats parameter to PHP string.