Namespaces

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

Classes

  • Arrays
  • Callback
  • FileSystem
  • Finder
  • Html
  • Json
  • LimitedScope
  • MimeTypeDetector
  • Neon
  • NeonEntity
  • Paginator
  • Strings
  • Validators

Exceptions

  • AssertionException
  • JsonException
  • NeonException
  • RegexpException
  • TokenizerException
  • 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 (https://davidgrudl.com)
  6:  */
  7: 
  8: namespace Nette\Utils;
  9: 
 10: use Nette;
 11: 
 12: 
 13: /**
 14:  * Simple parser & generator for Nette Object Notation.
 15:  *
 16:  * @author     David Grudl
 17:  */
 18: class Neon extends Nette\Object
 19: {
 20:     const BLOCK = 1;
 21: 
 22:     /** @var array */
 23:     private static $patterns = array(
 24:         '
 25:             \'[^\'\n]*\' |
 26:             "(?: \\\\. | [^"\\\\\n] )*"
 27:         ', // string
 28:         '
 29:             (?: [^#"\',:=[\]{}()\x00-\x20!`-] | [:-][^"\',\]})\s] )
 30:             (?:
 31:                 [^,:=\]})(\x00-\x20]+ |
 32:                 :(?! [\s,\]})] | $ ) |
 33:                 [\ \t]+ [^#,:=\]})(\x00-\x20]
 34:             )*
 35:         ', // literal / boolean / integer / float
 36:         '
 37:             [,:=[\]{}()-]
 38:         ', // symbol
 39:         '?:\#.*', // comment
 40:         '\n[\t\ ]*', // new line + indent
 41:         '?:[\t\ ]+', // whitespace
 42:     );
 43: 
 44:     /** @var Tokenizer */
 45:     private static $tokenizer;
 46: 
 47:     private static $brackets = array(
 48:         '[' => ']',
 49:         '{' => '}',
 50:         '(' => ')',
 51:     );
 52: 
 53:     /** @var string */
 54:     private $input;
 55: 
 56:     /** @var array */
 57:     private $tokens;
 58: 
 59:     /** @var int */
 60:     private $n = 0;
 61: 
 62:     /** @var bool */
 63:     private $indentTabs;
 64: 
 65: 
 66:     /**
 67:      * Returns the NEON representation of a value.
 68:      * @param  mixed
 69:      * @param  int
 70:      * @return string
 71:      */
 72:     public static function encode($var, $options = NULL)
 73:     {
 74:         if ($var instanceof \DateTime) {
 75:             return $var->format('Y-m-d H:i:s O');
 76: 
 77:         } elseif ($var instanceof NeonEntity) {
 78:             return self::encode($var->value) . '(' . substr(self::encode($var->attributes), 1, -1) . ')';
 79:         }
 80: 
 81:         if (is_object($var)) {
 82:             $obj = $var; $var = array();
 83:             foreach ($obj as $k => $v) {
 84:                 $var[$k] = $v;
 85:             }
 86:         }
 87: 
 88:         if (is_array($var)) {
 89:             $isList = Arrays::isList($var);
 90:             $s = '';
 91:             if ($options & self::BLOCK) {
 92:                 if (count($var) === 0) {
 93:                     return "[]";
 94:                 }
 95:                 foreach ($var as $k => $v) {
 96:                     $v = self::encode($v, self::BLOCK);
 97:                     $s .= ($isList ? '-' : self::encode($k) . ':')
 98:                         . (Strings::contains($v, "\n") ? "\n\t" . str_replace("\n", "\n\t", $v) : ' ' . $v)
 99:                         . "\n";
100:                     continue;
101:                 }
102:                 return $s;
103: 
104:             } else {
105:                 foreach ($var as $k => $v) {
106:                     $s .= ($isList ? '' : self::encode($k) . ': ') . self::encode($v) . ', ';
107:                 }
108:                 return ($isList ? '[' : '{') . substr($s, 0, -2) . ($isList ? ']' : '}');
109:             }
110: 
111:         } elseif (is_string($var) && !is_numeric($var)
112:             && !preg_match('~[\x00-\x1F]|^\d{4}|^(true|false|yes|no|on|off|null)\z~i', $var)
113:             && preg_match('~^' . self::$patterns[1] . '\z~x', $var) // 1 = literals
114:         ) {
115:             return $var;
116: 
117:         } elseif (is_float($var)) {
118:             $var = json_encode($var);
119:             return Strings::contains($var, '.') ? $var : $var . '.0';
120: 
121:         } else {
122:             return json_encode($var, PHP_VERSION_ID >= 50400 ? JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES : 0);
123:         }
124:     }
125: 
126: 
127:     /**
128:      * Decodes a NEON string.
129:      * @param  string
130:      * @return mixed
131:      */
132:     public static function decode($input)
133:     {
134:         if (!is_string($input)) {
135:             throw new Nette\InvalidArgumentException("Argument must be a string, " . gettype($input) . " given.");
136:         }
137:         if (!self::$tokenizer) {
138:             self::$tokenizer = new Tokenizer(self::$patterns, 'mix');
139:         }
140: 
141:         if (substr($input, 0, 3) === "\xEF\xBB\xBF") { // BOM
142:             $input = substr($input, 3);
143:         }
144:         $input = str_replace("\r", '', $input);
145: 
146:         $parser = new static;
147:         $parser->input = $input;
148:         $parser->tokens = self::$tokenizer->tokenize($input);
149:         $res = $parser->parse(0);
150: 
151:         while (isset($parser->tokens[$parser->n])) {
152:             if ($parser->tokens[$parser->n][0][0] === "\n") {
153:                 $parser->n++;
154:             } else {
155:                 $parser->error();
156:             }
157:         }
158:         return $res;
159:     }
160: 
161: 
162:     /**
163:      * @param  int  indentation (for block-parser)
164:      * @param  mixed
165:      * @return array
166:      */
167:     private function parse($indent = NULL, $result = NULL)
168:     {
169:         $inlineParser = $indent === NULL;
170:         $value = $key = NULL;
171:         $hasValue = $hasKey = FALSE;
172:         $tokens = $this->tokens;
173:         $n = & $this->n;
174:         $count = count($tokens);
175: 
176:         for (; $n < $count; $n++) {
177:             $t = $tokens[$n][0];
178: 
179:             if ($t === ',') { // ArrayEntry separator
180:                 if ((!$hasKey && !$hasValue) || !$inlineParser) {
181:                     $this->error();
182:                 }
183:                 $this->addValue($result, $hasKey, $key, $hasValue ? $value : NULL);
184:                 $hasKey = $hasValue = FALSE;
185: 
186:             } elseif ($t === ':' || $t === '=') { // KeyValuePair separator
187:                 if ($hasKey || !$hasValue) {
188:                     $this->error();
189:                 }
190:                 if (is_array($value) || is_object($value)) {
191:                     $this->error('Unacceptable key');
192:                 }
193:                 $key = (string) $value;
194:                 $hasKey = TRUE;
195:                 $hasValue = FALSE;
196: 
197:             } elseif ($t === '-') { // BlockArray bullet
198:                 if ($hasKey || $hasValue || $inlineParser) {
199:                     $this->error();
200:                 }
201:                 $key = NULL;
202:                 $hasKey = TRUE;
203: 
204:             } elseif (isset(self::$brackets[$t])) { // Opening bracket [ ( {
205:                 if ($hasValue) {
206:                     if ($t !== '(') {
207:                         $this->error();
208:                     }
209:                     $n++;
210:                     $entity = new NeonEntity;
211:                     $entity->value = $value;
212:                     $entity->attributes = $this->parse(NULL, array());
213:                     $value = $entity;
214:                 } else {
215:                     $n++;
216:                     $value = $this->parse(NULL, array());
217:                 }
218:                 $hasValue = TRUE;
219:                 if (!isset($tokens[$n]) || $tokens[$n][0] !== self::$brackets[$t]) { // unexpected type of bracket or block-parser
220:                     $this->error();
221:                 }
222: 
223:             } elseif ($t === ']' || $t === '}' || $t === ')') { // Closing bracket ] ) }
224:                 if (!$inlineParser) {
225:                     $this->error();
226:                 }
227:                 break;
228: 
229:             } elseif ($t[0] === "\n") { // Indent
230:                 if ($inlineParser) {
231:                     if ($hasKey || $hasValue) {
232:                         $this->addValue($result, $hasKey, $key, $hasValue ? $value : NULL);
233:                         $hasKey = $hasValue = FALSE;
234:                     }
235: 
236:                 } else {
237:                     while (isset($tokens[$n+1]) && $tokens[$n+1][0][0] === "\n") {
238:                         $n++; // skip to last indent
239:                     }
240:                     if (!isset($tokens[$n+1])) {
241:                         break;
242:                     }
243: 
244:                     $newIndent = strlen($tokens[$n][0]) - 1;
245:                     if ($indent === NULL) { // first iteration
246:                         $indent = $newIndent;
247:                     }
248:                     if ($newIndent) {
249:                         if ($this->indentTabs === NULL) {
250:                             $this->indentTabs = $tokens[$n][0][1] === "\t";
251:                         }
252:                         if (strpos($tokens[$n][0], $this->indentTabs ? ' ' : "\t")) {
253:                             $n++;
254:                             $this->error('Either tabs or spaces may be used as indenting chars, but not both.');
255:                         }
256:                     }
257: 
258:                     if ($newIndent > $indent) { // open new block-array or hash
259:                         if ($hasValue || !$hasKey) {
260:                             $n++;
261:                             $this->error('Unexpected indentation.');
262:                         } else {
263:                             $this->addValue($result, $key !== NULL, $key, $this->parse($newIndent));
264:                         }
265:                         $newIndent = isset($tokens[$n]) ? strlen($tokens[$n][0]) - 1 : 0;
266:                         $hasKey = FALSE;
267: 
268:                     } else {
269:                         if ($hasValue && !$hasKey) { // block items must have "key"; NULL key means list item
270:                             break;
271: 
272:                         } elseif ($hasKey) {
273:                             $this->addValue($result, $key !== NULL, $key, $hasValue ? $value : NULL);
274:                             $hasKey = $hasValue = FALSE;
275:                         }
276:                     }
277: 
278:                     if ($newIndent < $indent) { // close block
279:                         return $result; // block parser exit point
280:                     }
281:                 }
282: 
283:             } else { // Value
284:                 if ($hasValue) {
285:                     $this->error();
286:                 }
287:                 static $consts = array(
288:                     'true' => TRUE, 'True' => TRUE, 'TRUE' => TRUE, 'yes' => TRUE, 'Yes' => TRUE, 'YES' => TRUE, 'on' => TRUE, 'On' => TRUE, 'ON' => TRUE,
289:                     'false' => FALSE, 'False' => FALSE, 'FALSE' => FALSE, 'no' => FALSE, 'No' => FALSE, 'NO' => FALSE, 'off' => FALSE, 'Off' => FALSE, 'OFF' => FALSE,
290:                 );
291:                 if ($t[0] === '"') {
292:                     $value = preg_replace_callback('#\\\\(?:u[0-9a-f]{4}|x[0-9a-f]{2}|.)#i', array($this, 'cbString'), substr($t, 1, -1));
293:                 } elseif ($t[0] === "'") {
294:                     $value = substr($t, 1, -1);
295:                 } elseif (isset($consts[$t])) {
296:                     $value = $consts[$t];
297:                 } elseif ($t === 'null' || $t === 'Null' || $t === 'NULL') {
298:                     $value = NULL;
299:                 } elseif (is_numeric($t)) {
300:                     $value = $t * 1;
301:                 } elseif (preg_match('#\d\d\d\d-\d\d?-\d\d?(?:(?:[Tt]| +)\d\d?:\d\d:\d\d(?:\.\d*)? *(?:Z|[-+]\d\d?(?::\d\d)?)?)?\z#A', $t)) {
302:                     $value = new Nette\DateTime($t);
303:                 } else { // literal
304:                     $value = $t;
305:                 }
306:                 $hasValue = TRUE;
307:             }
308:         }
309: 
310:         if ($inlineParser) {
311:             if ($hasKey || $hasValue) {
312:                 $this->addValue($result, $hasKey, $key, $hasValue ? $value : NULL);
313:             }
314:         } else {
315:             if ($hasValue && !$hasKey) { // block items must have "key"
316:                 if ($result === NULL) {
317:                     $result = $value; // simple value parser
318:                 } else {
319:                     $this->error();
320:                 }
321:             } elseif ($hasKey) {
322:                 $this->addValue($result, $key !== NULL, $key, $hasValue ? $value : NULL);
323:             }
324:         }
325:         return $result;
326:     }
327: 
328: 
329:     private function addValue(& $result, $hasKey, $key, $value)
330:     {
331:         if ($hasKey) {
332:             if ($result && array_key_exists($key, $result)) {
333:                 $this->error("Duplicated key '$key'");
334:             }
335:             $result[$key] = $value;
336:         } else {
337:             $result[] = $value;
338:         }
339:     }
340: 
341: 
342:     private function cbString($m)
343:     {
344:         static $mapping = array('t' => "\t", 'n' => "\n", 'r' => "\r", 'f' => "\x0C", 'b' => "\x08", '"' => '"', '\\' => '\\',  '/' => '/', '_' => "\xc2\xa0");
345:         $sq = $m[0];
346:         if (isset($mapping[$sq[1]])) {
347:             return $mapping[$sq[1]];
348:         } elseif ($sq[1] === 'u' && strlen($sq) === 6) {
349:             return Strings::chr(hexdec(substr($sq, 2)));
350:         } elseif ($sq[1] === 'x' && strlen($sq) === 4) {
351:             return chr(hexdec(substr($sq, 2)));
352:         } else {
353:             $this->error("Invalid escaping sequence $sq");
354:         }
355:     }
356: 
357: 
358:     private function error($message = "Unexpected '%s'")
359:     {
360:         $last = isset($this->tokens[$this->n]) ? $this->tokens[$this->n] : NULL;
361:         list($line, $col) = Tokenizer::getCoordinates($this->input, $last ? $last[Tokenizer::OFFSET] : strlen($this->input));
362:         $token = $last ? str_replace("\n", '<new line>', Strings::truncate($last[0], 40)) : 'end';
363:         throw new NeonException(str_replace('%s', $token, $message) . " on line $line, column $col.");
364:     }
365: 
366: }
367: 
368: 
369: /**
370:  * Representation of 'foo(bar=1)' literal
371:  */
372: class NeonEntity extends \stdClass
373: {
374:     public $value;
375:     public $attributes;
376: }
377: 
378: 
379: /**
380:  * The exception that indicates error of NEON decoding.
381:  */
382: class NeonException extends \Exception
383: {
384: }
385: 
Nette 2.1 API documentation generated by ApiGen 2.8.0