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: '\'[^\'\n]*\'|"(?:\\\\.|[^"\\\\\n])*"',
30: '@[a-zA-Z_0-9\\\\]+',
31: '[:-](?=\s|$)|[,=[\]{}()]',
32: '?:#.*',
33: '\n[\t ]*',
34: '[^#"\',:=@[\]{}()<>\x00-\x20!`](?:[^#,:=\]})>\x00-\x1F]+|:(?!\s|$)|(?<!\s)#)*(?<!\s)',
35: '?:[\t ]+',
36: );
37:
38:
39: private static $tokenizer;
40:
41: private static $brackets = array(
42: '[' => ']',
43: '{' => '}',
44: '(' => ')',
45: );
46:
47:
48: private $n = 0;
49:
50:
51: private $indentTabs;
52:
53:
54: 55: 56: 57: 58: 59:
60: public static function encode($var, $options = NULL)
61: {
62: if ($var instanceof \DateTime) {
63: return $var->format('Y-m-d H:i:s O');
64: }
65: if (is_object($var)) {
66: $obj = $var; $var = array();
67: foreach ($obj as $k => $v) {
68: $var[$k] = $v;
69: }
70: }
71: if (is_array($var)) {
72: $isArray = array_keys($var) === range(0, count($var) - 1);
73: $s = '';
74: if ($options & self::BLOCK) {
75: foreach ($var as $k => $v) {
76: $v = self::encode($v, self::BLOCK);
77: $s .= ($isArray ? '-' : self::encode($k) . ':')
78: . (strpos($v, "\n") === FALSE ? ' ' . $v : "\n\t" . str_replace("\n", "\n\t", $v))
79: . "\n";
80: continue;
81: }
82: return $s;
83:
84: } else {
85: foreach ($var as $k => $v) {
86: $s .= ($isArray ? '' : self::encode($k) . ': ') . self::encode($v) . ', ';
87: }
88: return ($isArray ? '[' : '{') . substr($s, 0, -2) . ($isArray ? ']' : '}');
89: }
90:
91: } elseif (is_string($var) && !is_numeric($var)
92: && !preg_match('~[\x00-\x1F]|^\d{4}|^(true|false|yes|no|on|off|null)$~i', $var)
93: && preg_match('~^' . self::$patterns[5] . '$~', $var)
94: ) {
95: return $var;
96:
97: } else {
98: return json_encode($var);
99: }
100: }
101:
102:
103:
104: 105: 106: 107: 108:
109: public static function decode($input)
110: {
111: if (!is_string($input)) {
112: throw new Nette\InvalidArgumentException("Argument must be a string, " . gettype($input) . " given.");
113: }
114: if (!self::$tokenizer) {
115: self::$tokenizer = new Tokenizer(self::$patterns, 'mi');
116: }
117:
118: $input = str_replace("\r", '', $input);
119: self::$tokenizer->tokenize($input);
120:
121: $parser = new static;
122: $res = $parser->parse(0);
123:
124: while (isset(self::$tokenizer->tokens[$parser->n])) {
125: if (self::$tokenizer->tokens[$parser->n][0] === "\n") {
126: $parser->n++;
127: } else {
128: $parser->error();
129: }
130: }
131: return $res;
132: }
133:
134:
135:
136: 137: 138: 139: 140:
141: private function parse($indent = NULL, $result = NULL)
142: {
143: $inlineParser = $indent === NULL;
144: $value = $key = $object = NULL;
145: $hasValue = $hasKey = FALSE;
146: $tokens = self::$tokenizer->tokens;
147: $n = & $this->n;
148: $count = count($tokens);
149:
150: for (; $n < $count; $n++) {
151: $t = $tokens[$n];
152:
153: if ($t === ',') {
154: if (!$hasValue || !$inlineParser) {
155: $this->error();
156: }
157: if ($hasKey) {
158: $result[$key] = $value;
159: } else {
160: $result[] = $value;
161: }
162: $hasKey = $hasValue = FALSE;
163:
164: } elseif ($t === ':' || $t === '=') {
165: if ($hasKey || !$hasValue) {
166: $this->error();
167: }
168: if (is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
169: $this->error('Unacceptable key');
170: } else {
171: $key = (string) $value;
172: }
173: $hasKey = TRUE;
174: $hasValue = FALSE;
175:
176: } elseif ($t === '-') {
177: if ($hasKey || $hasValue || $inlineParser) {
178: $this->error();
179: }
180: $key = NULL;
181: $hasKey = TRUE;
182:
183: } elseif (isset(self::$brackets[$t])) {
184: if ($hasValue) {
185: if ($value[0] === '@' && $t === '(') {
186: $n++;
187: $value = $this->parse(NULL, array('@' => substr($value, 1)));
188: } else {
189: $this->error();
190: }
191: } else {
192: $n++;
193: $value = $this->parse(NULL, array());
194: }
195: $hasValue = TRUE;
196: if (!isset($tokens[$n]) || $tokens[$n] !== self::$brackets[$t]) {
197: $this->error();
198: }
199:
200: } elseif ($t === ']' || $t === '}' || $t === ')') {
201: if (!$inlineParser) {
202: $this->error();
203: }
204: break;
205:
206: } elseif ($t[0] === "\n") {
207: if ($inlineParser) {
208: if ($hasValue) {
209: if ($hasKey) {
210: $result[$key] = $value;
211: } else {
212: $result[] = $value;
213: }
214: $hasKey = $hasValue = FALSE;
215: }
216:
217: } else {
218: while (isset($tokens[$n+1]) && $tokens[$n+1][0] === "\n") $n++;
219: if (!isset($tokens[$n+1])) {
220: break;
221: }
222:
223: $newIndent = strlen($tokens[$n]) - 1;
224: if ($indent === NULL) {
225: $indent = $newIndent;
226: }
227: if ($newIndent) {
228: if ($this->indentTabs === NULL) {
229: $this->indentTabs = $tokens[$n][1] === "\t";
230: }
231: if (strpos($tokens[$n], $this->indentTabs ? ' ' : "\t")) {
232: $n++;
233: $this->error('Either tabs or spaces may be used as indenting chars, but not both.');
234: }
235: }
236:
237: if ($newIndent > $indent) {
238: if ($hasValue || !$hasKey) {
239: $n++;
240: $this->error('Unexpected indentation.');
241: } elseif ($key === NULL) {
242: $result[] = $this->parse($newIndent);
243: } else {
244: $result[$key] = $this->parse($newIndent);
245: }
246: $newIndent = isset($tokens[$n]) ? strlen($tokens[$n]) - 1 : 0;
247: $hasKey = FALSE;
248:
249: } else {
250: if ($hasValue && !$hasKey) {
251: break;
252:
253: } elseif ($hasKey) {
254: $value = $hasValue ? $value : NULL;
255: if ($key === NULL) {
256: $result[] = $value;
257: } else {
258: $result[$key] = $value;
259: }
260: $hasKey = $hasValue = FALSE;
261: }
262: }
263:
264: if ($newIndent < $indent) {
265: return $result;
266: }
267: }
268:
269: } else {
270: if ($hasValue) {
271: $this->error();
272: }
273: static $consts = array(
274: 'true' => TRUE, 'True' => TRUE, 'TRUE' => TRUE, 'yes' => TRUE, 'Yes' => TRUE, 'YES' => TRUE, 'on' => TRUE, 'On' => TRUE, 'ON' => TRUE,
275: 'false' => FALSE, 'False' => FALSE, 'FALSE' => FALSE, 'no' => FALSE, 'No' => FALSE, 'NO' => FALSE, 'off' => FALSE, 'Off' => FALSE, 'OFF' => FALSE,
276: );
277: if ($t[0] === '"') {
278: $value = preg_replace_callback('#\\\\(?:u[0-9a-f]{4}|x[0-9a-f]{2}|.)#i', array($this, 'cbString'), substr($t, 1, -1));
279: } elseif ($t[0] === "'") {
280: $value = substr($t, 1, -1);
281: } elseif (isset($consts[$t])) {
282: $value = $consts[$t];
283: } elseif ($t === 'null' || $t === 'Null' || $t === 'NULL') {
284: $value = NULL;
285: } elseif (is_numeric($t)) {
286: $value = $t * 1;
287: } elseif (preg_match('#\d\d\d\d-\d\d?-\d\d?(?:(?:[Tt]| +)\d\d?:\d\d:\d\d(?:\.\d*)? *(?:Z|[-+]\d\d?(?::\d\d)?)?)?$#A', $t)) {
288: $value = new Nette\DateTime($t);
289: } else {
290: $value = $t;
291: }
292: $hasValue = TRUE;
293: }
294: }
295:
296: if ($inlineParser) {
297: if ($hasValue) {
298: if ($hasKey) {
299: $result[$key] = $value;
300: } else {
301: $result[] = $value;
302: }
303: } elseif ($hasKey) {
304: $this->error();
305: }
306: } else {
307: if ($hasValue && !$hasKey) {
308: if ($result === NULL) {
309: $result = $value;
310: } else {
311: $this->error();
312: }
313: } elseif ($hasKey) {
314: $value = $hasValue ? $value : NULL;
315: if ($key === NULL) {
316: $result[] = $value;
317: } else {
318: $result[$key] = $value;
319: }
320: }
321: }
322: return $result;
323: }
324:
325:
326:
327: private function cbString($m)
328: {
329: static $mapping = array('t' => "\t", 'n' => "\n", '"' => '"', '\\' => '\\', '/' => '/', '_' => "\xc2\xa0");
330: $sq = $m[0];
331: if (isset($mapping[$sq[1]])) {
332: return $mapping[$sq[1]];
333: } elseif ($sq[1] === 'u' && strlen($sq) === 6) {
334: return Strings::chr(hexdec(substr($sq, 2)));
335: } elseif ($sq[1] === 'x' && strlen($sq) === 4) {
336: return chr(hexdec(substr($sq, 2)));
337: } else {
338: $this->error("Invalid escaping sequence $sq");
339: }
340: }
341:
342:
343:
344: private function error($message = "Unexpected '%s'")
345: {
346: list(, $line, $col) = self::$tokenizer->getOffset($this->n);
347: $token = isset(self::$tokenizer->tokens[$this->n])
348: ? str_replace("\n", '<new line>', Strings::truncate(self::$tokenizer->tokens[$this->n], 40))
349: : 'end';
350: throw new NeonException(str_replace('%s', $token, $message) . " on line $line, column $col.");
351: }
352:
353: }
354:
355:
356:
357: 358: 359:
360: class NeonException extends \Exception
361: {
362: }
363: