1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette\Utils;
13:
14: use Nette;
15:
16:
17:
18: 19: 20: 21: 22:
23: class Neon extends Nette\Object
24: {
25: const BLOCK = 1;
26:
27:
28: private static $patterns = array(
29: '
30: \'[^\'\n]*\' |
31: "(?: \\\\. | [^"\\\\\n] )*"
32: ',
33: '
34: -(?= \s | $ ) |
35: :(?= [\s,\]})] | $ ) |
36: [,=[\]{}()]
37: ',
38: '?:\#.*',
39: '\n[\t\ ]*',
40: '
41: [^#"\',=[\]{}()\x00-\x20!`]
42: (?:
43: [^,:=\]})(\x00-\x20]+ |
44: :(?! [\s,\]})] | $ ) |
45: [\ \t]+ [^#,:=\]})(\x00-\x20]
46: )*
47: ',
48: '?:[\t\ ]+',
49: );
50:
51:
52: private static $tokenizer;
53:
54: private static $brackets = array(
55: '[' => ']',
56: '{' => '}',
57: '(' => ')',
58: );
59:
60:
61: private $n = 0;
62:
63:
64: private $indentTabs;
65:
66:
67: 68: 69: 70: 71: 72:
73: public static function encode($var, $options = NULL)
74: {
75: if ($var instanceof \DateTime) {
76: return $var->format('Y-m-d H:i:s O');
77:
78: } elseif ($var instanceof NeonEntity) {
79: return self::encode($var->value) . '(' . substr(self::encode($var->attributes), 1, -1) . ')';
80: }
81:
82: if (is_object($var)) {
83: $obj = $var; $var = array();
84: foreach ($obj as $k => $v) {
85: $var[$k] = $v;
86: }
87: }
88:
89: if (is_array($var)) {
90: $isList = Validators::isList($var);
91: $s = '';
92: if ($options & self::BLOCK) {
93: if (count($var) === 0){
94: return "[]";
95: }
96: foreach ($var as $k => $v) {
97: $v = self::encode($v, self::BLOCK);
98: $s .= ($isList ? '-' : self::encode($k) . ':')
99: . (Strings::contains($v, "\n") ? "\n\t" . str_replace("\n", "\n\t", $v) : ' ' . $v)
100: . "\n";
101: continue;
102: }
103: return $s;
104:
105: } else {
106: foreach ($var as $k => $v) {
107: $s .= ($isList ? '' : self::encode($k) . ': ') . self::encode($v) . ', ';
108: }
109: return ($isList ? '[' : '{') . substr($s, 0, -2) . ($isList ? ']' : '}');
110: }
111:
112: } elseif (is_string($var) && !is_numeric($var)
113: && !preg_match('~[\x00-\x1F]|^\d{4}|^(true|false|yes|no|on|off|null)\z~i', $var)
114: && preg_match('~^' . self::$patterns[4] . '\z~x', $var)
115: ) {
116: return $var;
117:
118: } elseif (is_float($var)) {
119: $var = var_export($var, TRUE);
120: return Strings::contains($var, '.') ? $var : $var . '.0';
121:
122: } else {
123: return json_encode($var);
124: }
125: }
126:
127:
128:
129: 130: 131: 132: 133:
134: public static function decode($input)
135: {
136: if (!is_string($input)) {
137: throw new Nette\InvalidArgumentException("Argument must be a string, " . gettype($input) . " given.");
138: }
139: if (!self::$tokenizer) {
140: self::$tokenizer = new Tokenizer(self::$patterns, 'mix');
141: }
142:
143: if (substr($input, 0, 3) === "\xEF\xBB\xBF") {
144: $input = substr($input, 3);
145: }
146: $input = str_replace("\r", '', $input);
147: self::$tokenizer->tokenize($input);
148:
149: $parser = new static;
150: $res = $parser->parse(0);
151:
152: while (isset(self::$tokenizer->tokens[$parser->n])) {
153: if (self::$tokenizer->tokens[$parser->n][0] === "\n") {
154: $parser->n++;
155: } else {
156: $parser->error();
157: }
158: }
159: return $res;
160: }
161:
162:
163:
164: 165: 166: 167: 168:
169: private function parse($indent = NULL, $result = NULL)
170: {
171: $inlineParser = $indent === NULL;
172: $value = $key = $object = NULL;
173: $hasValue = $hasKey = FALSE;
174: $tokens = self::$tokenizer->tokens;
175: $n = & $this->n;
176: $count = count($tokens);
177:
178: for (; $n < $count; $n++) {
179: $t = $tokens[$n];
180:
181: if ($t === ',') {
182: if ((!$hasKey && !$hasValue) || !$inlineParser) {
183: $this->error();
184: }
185: $this->addValue($result, $hasKey, $key, $hasValue ? $value : NULL);
186: $hasKey = $hasValue = FALSE;
187:
188: } elseif ($t === ':' || $t === '=') {
189: if ($hasKey || !$hasValue) {
190: $this->error();
191: }
192: if (is_array($value) || is_object($value)) {
193: $this->error('Unacceptable key');
194: }
195: $key = (string) $value;
196: $hasKey = TRUE;
197: $hasValue = FALSE;
198:
199: } elseif ($t === '-') {
200: if ($hasKey || $hasValue || $inlineParser) {
201: $this->error();
202: }
203: $key = NULL;
204: $hasKey = TRUE;
205:
206: } elseif (isset(self::$brackets[$t])) {
207: if ($hasValue) {
208: if ($t !== '(') {
209: $this->error();
210: }
211: $n++;
212: $entity = new NeonEntity;
213: $entity->value = $value;
214: $entity->attributes = $this->parse(NULL, array());
215: $value = $entity;
216: } else {
217: $n++;
218: $value = $this->parse(NULL, array());
219: }
220: $hasValue = TRUE;
221: if (!isset($tokens[$n]) || $tokens[$n] !== self::$brackets[$t]) {
222: $this->error();
223: }
224:
225: } elseif ($t === ']' || $t === '}' || $t === ')') {
226: if (!$inlineParser) {
227: $this->error();
228: }
229: break;
230:
231: } elseif ($t[0] === "\n") {
232: if ($inlineParser) {
233: if ($hasKey || $hasValue) {
234: $this->addValue($result, $hasKey, $key, $hasValue ? $value : NULL);
235: $hasKey = $hasValue = FALSE;
236: }
237:
238: } else {
239: while (isset($tokens[$n+1]) && $tokens[$n+1][0] === "\n") $n++;
240: if (!isset($tokens[$n+1])) {
241: break;
242: }
243:
244: $newIndent = strlen($tokens[$n]) - 1;
245: if ($indent === NULL) {
246: $indent = $newIndent;
247: }
248: if ($newIndent) {
249: if ($this->indentTabs === NULL) {
250: $this->indentTabs = $tokens[$n][1] === "\t";
251: }
252: if (strpos($tokens[$n], $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) {
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]) - 1 : 0;
266: $hasKey = FALSE;
267:
268: } else {
269: if ($hasValue && !$hasKey) {
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) {
279: return $result;
280: }
281: }
282:
283: } else {
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 {
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) {
316: if ($result === NULL) {
317: $result = $value;
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:
330: private function addValue(&$result, $hasKey, $key, $value)
331: {
332: if ($hasKey) {
333: if ($result && array_key_exists($key, $result)) {
334: $this->error("Duplicated key '$key'");
335: }
336: $result[$key] = $value;
337: } else {
338: $result[] = $value;
339: }
340: }
341:
342:
343:
344: private function cbString($m)
345: {
346: static $mapping = array('t' => "\t", 'n' => "\n", 'r' => "\r", 'f' => "\x0C", 'b' => "\x08", '"' => '"', '\\' => '\\', '/' => '/', '_' => "\xc2\xa0");
347: $sq = $m[0];
348: if (isset($mapping[$sq[1]])) {
349: return $mapping[$sq[1]];
350: } elseif ($sq[1] === 'u' && strlen($sq) === 6) {
351: return Strings::chr(hexdec(substr($sq, 2)));
352: } elseif ($sq[1] === 'x' && strlen($sq) === 4) {
353: return chr(hexdec(substr($sq, 2)));
354: } else {
355: $this->error("Invalid escaping sequence $sq");
356: }
357: }
358:
359:
360:
361: private function error($message = "Unexpected '%s'")
362: {
363: list(, $line, $col) = self::$tokenizer->getOffset($this->n);
364: $token = isset(self::$tokenizer->tokens[$this->n])
365: ? str_replace("\n", '<new line>', Strings::truncate(self::$tokenizer->tokens[$this->n], 40))
366: : 'end';
367: throw new NeonException(str_replace('%s', $token, $message) . " on line $line, column $col.");
368: }
369:
370: }
371:
372:
373:
374: 375: 376:
377: class NeonEntity extends \stdClass
378: {
379: public $value;
380: public $attributes;
381: }
382:
383:
384:
385: 386: 387:
388: class NeonException extends \Exception
389: {
390: }
391: