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