Namespaces

  • Nette
    • Application
      • Diagnostics
      • Responses
      • Routers
      • UI
    • Caching
      • Storages
    • ComponentModel
    • Config
      • Adapters
      • Extensions
    • Database
      • Diagnostics
      • Drivers
      • Reflection
      • Table
    • DI
      • Diagnostics
    • Diagnostics
    • Forms
      • Controls
      • Rendering
    • Http
    • Iterators
    • Latte
      • Macros
    • Loaders
    • Localization
    • Mail
    • Reflection
    • Security
      • Diagnostics
    • Templating
    • Utils
      • PhpGenerator
  • NetteModule
  • none

Classes

  • Compiler
  • Engine
  • HtmlNode
  • MacroNode
  • MacroTokenizer
  • Parser
  • PhpWriter
  • Token

Interfaces

  • IMacro

Exceptions

  • CompileException
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Other releases
  • Nette homepage
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Nette Framework (https://nette.org)
  5:  * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
  6:  */
  7: 
  8: namespace Nette\Latte;
  9: 
 10: use Nette,
 11:     Nette\Utils\Strings;
 12: 
 13: 
 14: /**
 15:  * Latte parser.
 16:  *
 17:  * @author     David Grudl
 18:  */
 19: class Parser extends Nette\Object
 20: {
 21:     /** @internal regular expression for single & double quoted PHP string */
 22:     const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';
 23: 
 24:     /** @internal special HTML attribute prefix */
 25:     const N_PREFIX = 'n:';
 26: 
 27:     /** @var string default macro tag syntax */
 28:     public $defaultSyntax = 'latte';
 29: 
 30:     /** @var array */
 31:     public $syntaxes = array(
 32:         'latte' => array('\\{(?![\\s\'"{}])', '\\}'), // {...}
 33:         'double' => array('\\{\\{(?![\\s\'"{}])', '\\}\\}'), // {{...}}
 34:         'asp' => array('<%\s*', '\s*%>'), /* <%...%> */
 35:         'python' => array('\\{[{%]\s*', '\s*[%}]\\}'), // {% ... %} | {{ ... }}
 36:         'off' => array('[^\x00-\xFF]', ''),
 37:     );
 38: 
 39:     /** @var string */
 40:     private $macroRe;
 41: 
 42:     /** @var string source template */
 43:     private $input;
 44: 
 45:     /** @var Token[] */
 46:     private $output;
 47: 
 48:     /** @var int  position on source template */
 49:     private $offset;
 50: 
 51:     /** @var array */
 52:     private $context;
 53: 
 54:     /** @var string */
 55:     private $lastHtmlTag;
 56: 
 57:     /** @var string used by filter() */
 58:     private $syntaxEndTag;
 59: 
 60:     /** @var bool */
 61:     private $xmlMode;
 62: 
 63:     /** @internal states */
 64:     const CONTEXT_TEXT = 'text',
 65:         CONTEXT_CDATA = 'cdata',
 66:         CONTEXT_TAG = 'tag',
 67:         CONTEXT_ATTRIBUTE = 'attribute',
 68:         CONTEXT_NONE = 'none',
 69:         CONTEXT_COMMENT = 'comment';
 70: 
 71: 
 72:     /**
 73:      * Process all {macros} and <tags/>.
 74:      * @param  string
 75:      * @return array
 76:      */
 77:     public function parse($input)
 78:     {
 79:         if (substr($input, 0, 3) === "\xEF\xBB\xBF") { // BOM
 80:             $input = substr($input, 3);
 81:         }
 82:         if (!Strings::checkEncoding($input)) {
 83:             throw new Nette\InvalidArgumentException('Template is not valid UTF-8 stream.');
 84:         }
 85:         $input = str_replace("\r\n", "\n", $input);
 86:         $this->input = $input;
 87:         $this->output = array();
 88:         $this->offset = 0;
 89: 
 90:         $this->setSyntax($this->defaultSyntax);
 91:         $this->setContext(self::CONTEXT_TEXT);
 92:         $this->lastHtmlTag = $this->syntaxEndTag = NULL;
 93: 
 94:         while ($this->offset < strlen($input)) {
 95:             $matches = $this->{"context".$this->context[0]}();
 96: 
 97:             if (!$matches) { // EOF
 98:                 break;
 99: 
100:             } elseif (!empty($matches['comment'])) { // {* *}
101:                 $this->addToken(Token::COMMENT, $matches[0]);
102: 
103:             } elseif (!empty($matches['macro'])) { // {macro}
104:                 $token = $this->addToken(Token::MACRO_TAG, $matches[0]);
105:                 list($token->name, $token->value, $token->modifiers) = $this->parseMacroTag($matches['macro']);
106:             }
107: 
108:             $this->filter();
109:         }
110: 
111:         if ($this->offset < strlen($input)) {
112:             $this->addToken(Token::TEXT, substr($this->input, $this->offset));
113:         }
114:         return $this->output;
115:     }
116: 
117: 
118:     /**
119:      * Handles CONTEXT_TEXT.
120:      */
121:     private function contextText()
122:     {
123:         $matches = $this->match('~
124:             (?:(?<=\n|^)[ \t]*)?<(?P<closing>/?)(?P<tag>[a-z0-9:]+)|  ##  begin of HTML tag <tag </tag - ignores <!DOCTYPE
125:             <(?P<htmlcomment>!--(?!>))|     ##  begin of HTML comment <!--, but not <!-->
126:             '.$this->macroRe.'              ##  macro tag
127:         ~xsi');
128: 
129:         if (!empty($matches['htmlcomment'])) { // <!--
130:             $this->addToken(Token::HTML_TAG_BEGIN, $matches[0]);
131:             $this->setContext(self::CONTEXT_COMMENT);
132: 
133:         } elseif (!empty($matches['tag'])) { // <tag or </tag
134:             $token = $this->addToken(Token::HTML_TAG_BEGIN, $matches[0]);
135:             $token->name = $matches['tag'];
136:             $token->closing = (bool) $matches['closing'];
137:             $this->lastHtmlTag = $matches['closing'] . strtolower($matches['tag']);
138:             $this->setContext(self::CONTEXT_TAG);
139:         }
140:         return $matches;
141:     }
142: 
143: 
144:     /**
145:      * Handles CONTEXT_CDATA.
146:      */
147:     private function contextCData()
148:     {
149:         $matches = $this->match('~
150:             </(?P<tag>'.$this->lastHtmlTag.')(?![a-z0-9:])| ##  end HTML tag </tag
151:             '.$this->macroRe.'              ##  macro tag
152:         ~xsi');
153: 
154:         if (!empty($matches['tag'])) { // </tag
155:             $token = $this->addToken(Token::HTML_TAG_BEGIN, $matches[0]);
156:             $token->name = $this->lastHtmlTag;
157:             $token->closing = TRUE;
158:             $this->lastHtmlTag = '/' . $this->lastHtmlTag;
159:             $this->setContext(self::CONTEXT_TAG);
160:         }
161:         return $matches;
162:     }
163: 
164: 
165:     /**
166:      * Handles CONTEXT_TAG.
167:      */
168:     private function contextTag()
169:     {
170:         $matches = $this->match('~
171:             (?P<end>\ ?/?>)([ \t]*\n)?|  ##  end of HTML tag
172:             '.$this->macroRe.'|          ##  macro tag
173:             \s*(?P<attr>[^\s/>={]+)(?:\s*=\s*(?P<value>["\']|[^\s/>{]+))? ## begin of HTML attribute
174:         ~xsi');
175: 
176:         if (!empty($matches['end'])) { // end of HTML tag />
177:             $this->addToken(Token::HTML_TAG_END, $matches[0]);
178:             $this->setContext(!$this->xmlMode && in_array($this->lastHtmlTag, array('script', 'style'), TRUE) ? self::CONTEXT_CDATA : self::CONTEXT_TEXT);
179: 
180:         } elseif (isset($matches['attr']) && $matches['attr'] !== '') { // HTML attribute
181:             $token = $this->addToken(Token::HTML_ATTRIBUTE, $matches[0]);
182:             $token->name = $matches['attr'];
183:             $token->value = isset($matches['value']) ? $matches['value'] : '';
184: 
185:             if ($token->value === '"' || $token->value === "'") { // attribute = "'
186:                 if (Strings::startsWith($token->name, self::N_PREFIX)) {
187:                     $token->value = '';
188:                     if ($m = $this->match('~(.*?)' . $matches['value'] . '~xsi')) {
189:                         $token->value = $m[1];
190:                         $token->text .= $m[0];
191:                     }
192:                 } else {
193:                     $this->setContext(self::CONTEXT_ATTRIBUTE, $matches['value']);
194:                 }
195:             }
196:         }
197:         return $matches;
198:     }
199: 
200: 
201:     /**
202:      * Handles CONTEXT_ATTRIBUTE.
203:      */
204:     private function contextAttribute()
205:     {
206:         $matches = $this->match('~
207:             (?P<quote>'.$this->context[1].')|  ##  end of HTML attribute
208:             '.$this->macroRe.'                 ##  macro tag
209:         ~xsi');
210: 
211:         if (!empty($matches['quote'])) { // (attribute end) '"
212:             $this->addToken(Token::TEXT, $matches[0]);
213:             $this->setContext(self::CONTEXT_TAG);
214:         }
215:         return $matches;
216:     }
217: 
218: 
219:     /**
220:      * Handles CONTEXT_COMMENT.
221:      */
222:     private function contextComment()
223:     {
224:         $matches = $this->match('~
225:             (?P<htmlcomment>-->)|   ##  end of HTML comment
226:             '.$this->macroRe.'      ##  macro tag
227:         ~xsi');
228: 
229:         if (!empty($matches['htmlcomment'])) { // -->
230:             $this->addToken(Token::HTML_TAG_END, $matches[0]);
231:             $this->setContext(self::CONTEXT_TEXT);
232:         }
233:         return $matches;
234:     }
235: 
236: 
237:     /**
238:      * Handles CONTEXT_NONE.
239:      */
240:     private function contextNone()
241:     {
242:         $matches = $this->match('~
243:             '.$this->macroRe.'     ##  macro tag
244:         ~xsi');
245:         return $matches;
246:     }
247: 
248: 
249:     /**
250:      * Matches next token.
251:      * @param  string
252:      * @return array
253:      */
254:     private function match($re)
255:     {
256:         if ($matches = Strings::match($this->input, $re, PREG_OFFSET_CAPTURE, $this->offset)) {
257:             $value = substr($this->input, $this->offset, $matches[0][1] - $this->offset);
258:             if ($value !== '') {
259:                 $this->addToken(Token::TEXT, $value);
260:             }
261:             $this->offset = $matches[0][1] + strlen($matches[0][0]);
262:             foreach ($matches as $k => $v) $matches[$k] = $v[0];
263:         }
264:         return $matches;
265:     }
266: 
267: 
268:     /**
269:      * @return self
270:      */
271:     public function setContext($context, $quote = NULL)
272:     {
273:         $this->context = array($context, $quote);
274:         return $this;
275:     }
276: 
277: 
278:     /**
279:      * Changes macro tag delimiters.
280:      * @param  string
281:      * @return self
282:      */
283:     public function setSyntax($type)
284:     {
285:         $type = $type ?: $this->defaultSyntax;
286:         if (isset($this->syntaxes[$type])) {
287:             $this->setDelimiters($this->syntaxes[$type][0], $this->syntaxes[$type][1]);
288:         } else {
289:             throw new Nette\InvalidArgumentException("Unknown syntax '$type'");
290:         }
291:         return $this;
292:     }
293: 
294: 
295:     /**
296:      * Changes macro tag delimiters.
297:      * @param  string  left regular expression
298:      * @param  string  right regular expression
299:      * @return self
300:      */
301:     public function setDelimiters($left, $right)
302:     {
303:         $this->macroRe = '
304:             (?P<comment>' . $left . '\\*.*?\\*' . $right . '\n{0,2})|
305:             ' . $left . '
306:                 (?P<macro>(?:' . self::RE_STRING . '|\{
307:                         (?P<inner>' . self::RE_STRING . '|\{(?P>inner)\}|[^\'"{}])*+
308:                 \}|[^\'"{}])+?)
309:             ' . $right . '
310:             (?P<rmargin>[ \t]*(?=\n))?
311:         ';
312:         return $this;
313:     }
314: 
315: 
316:     /**
317:      * Parses macro tag to name, arguments a modifiers parts.
318:      * @param  string {name arguments | modifiers}
319:      * @return array
320:      */
321:     public function parseMacroTag($tag)
322:     {
323:         $match = Strings::match($tag, '~^
324:             (
325:                 (?P<name>\?|/?[a-z]\w*+(?:[.:]\w+)*+(?!::|\(|\\\\))|   ## ?, name, /name, but not function( or class:: or namespace\
326:                 (?P<noescape>!?)(?P<shortname>/?[=\~#%^&_]?)      ## !expression, !=expression, ...
327:             )(?P<args>.*?)
328:             (?P<modifiers>\|[a-z](?:'.Parser::RE_STRING.'|[^\'"])*)?
329:         ()\z~isx');
330: 
331:         if (!$match) {
332:             return FALSE;
333:         }
334:         $modifiers = preg_replace('#\|noescape\s?(?=\||\z)#i', '', $match['modifiers'], -1, $noescape);
335:         if ($match['name'] === '') {
336:             $match['name'] = $match['shortname'] ?: '=';
337:             if (!$noescape && !$match['noescape'] && substr($match['shortname'], 0, 1) !== '/') {
338:                 $modifiers .= '|escape';
339:             }
340:         }
341:         return array($match['name'], trim($match['args']), $modifiers);
342:     }
343: 
344: 
345:     private function addToken($type, $text)
346:     {
347:         $this->output[] = $token = new Token;
348:         $token->type = $type;
349:         $token->text = $text;
350:         $token->line = substr_count($this->input, "\n", 0, max(1, $this->offset - 1)) + 1;
351:         return $token;
352:     }
353: 
354: 
355:     /**
356:      * Process low-level macros.
357:      */
358:     protected function filter()
359:     {
360:         $token = end($this->output);
361:         if ($token->type === Token::MACRO_TAG && $token->name === '/syntax') {
362:             $this->setSyntax($this->defaultSyntax);
363:             $token->type = Token::COMMENT;
364: 
365:         } elseif ($token->type === Token::MACRO_TAG && $token->name === 'syntax') {
366:             $this->setSyntax($token->value);
367:             $token->type = Token::COMMENT;
368: 
369:         } elseif ($token->type === Token::HTML_ATTRIBUTE && $token->name === 'n:syntax') {
370:             $this->setSyntax($token->value);
371:             $this->syntaxEndTag = '/' . $this->lastHtmlTag;
372:             $token->type = Token::COMMENT;
373: 
374:         } elseif ($token->type === Token::HTML_TAG_END && $this->lastHtmlTag === $this->syntaxEndTag) {
375:             $this->setSyntax($this->defaultSyntax);
376: 
377:         } elseif ($token->type === Token::MACRO_TAG && $token->name === 'contentType') {
378:             if (preg_match('#html|xml#', $token->value, $m)) {
379:                 $this->xmlMode = $m[0] === 'xml';
380:                 $this->setContext(self::CONTEXT_TEXT);
381:             } else {
382:                 $this->setContext(self::CONTEXT_NONE);
383:             }
384:         }
385:     }
386: 
387: }
388: 
Nette 2.0 API documentation generated by ApiGen 2.8.0