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
  • PHP

Classes

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

Interfaces

  • IMacro

Exceptions

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