1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette;
13:
14: use Nette;
15:
16:
17:
18: 19: 20: 21: 22:
23: class Neon extends 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) . ':') . (strpos($v, "\n") === FALSE ? ' ' . $v : "\n\t" . str_replace("\n", "\n\t", $v)) . "\n";
78: continue;
79: }
80: return $s;
81:
82: } else {
83: foreach ($var as $k => $v) {
84: $s .= ($isArray ? '' : self::encode($k) . ': ') . self::encode($v) . ', ';
85: }
86: return ($isArray ? '[' : '{') . substr($s, 0, -2) . ($isArray ? ']' : '}');
87: }
88:
89: } elseif (is_string($var) && !is_numeric($var) && !preg_match('~[\x00-\x1F]|^\d{4}|^(true|false|yes|no|on|off|null)$~i', $var) && preg_match('~^' . self::$patterns[5] . '$~', $var)) {
90: return $var;
91:
92: } else {
93: return json_encode($var);
94: }
95: }
96:
97:
98:
99: 100: 101: 102: 103:
104: public static function decode($input)
105: {
106: if (!is_string($input)) {
107: throw new \InvalidArgumentException("Argument must be a string, " . gettype($input) . " given.");
108: }
109: if (!self::$tokenizer) {
110: self::$tokenizer = new Tokenizer(self::$patterns, 'mi');
111: }
112:
113: $input = str_replace("\r", '', $input);
114: self::$tokenizer->tokenize($input);
115:
116: $parser = new self;
117: $res = $parser->parse(0);
118:
119: while (isset(self::$tokenizer->tokens[$parser->n])) {
120: if (self::$tokenizer->tokens[$parser->n][0] === "\n") {
121: $parser->n++;
122: } else {
123: $parser->error();
124: }
125: }
126: return $res;
127: }
128:
129:
130:
131: 132: 133: 134: 135:
136: private function parse($indent = NULL, $result = NULL)
137: {
138: $inlineParser = $indent === NULL;
139: $value = $key = $object = NULL;
140: $hasValue = $hasKey = FALSE;
141: $tokens = self::$tokenizer->tokens;
142: $n = & $this->n;
143: $count = count($tokens);
144:
145: for (; $n < $count; $n++) {
146: $t = $tokens[$n];
147:
148: if ($t === ',') { 149: if (!$hasValue || !$inlineParser) {
150: $this->error();
151: }
152: if ($hasKey) $result[$key] = $value; else $result[] = $value;
153: $hasKey = $hasValue = FALSE;
154:
155: } elseif ($t === ':' || $t === '=') { 156: if ($hasKey || !$hasValue) {
157: $this->error();
158: }
159: if (is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
160: $this->error('Unacceptable key');
161: } else {
162: $key = (string) $value;
163: }
164: $hasKey = TRUE;
165: $hasValue = FALSE;
166:
167: } elseif ($t === '-') { 168: if ($hasKey || $hasValue || $inlineParser) {
169: $this->error();
170: }
171: $key = NULL;
172: $hasKey = TRUE;
173:
174: } elseif (isset(self::$brackets[$t])) { 175: if ($hasValue) {
176: $this->error();
177: }
178: $endBracket = self::$brackets[$tokens[$n++]];
179: $hasValue = TRUE;
180: $value = $this->parse(NULL, array());
181: if (!isset($tokens[$n]) || $tokens[$n] !== $endBracket) { 182: $this->error();
183: }
184:
185: } elseif ($t === ']' || $t === '}' || $t === ')') { 186: if (!$inlineParser) {
187: $this->error();
188: }
189: break;
190:
191: } elseif ($t[0] === '@') { 192: $object = $t; 193:
194: } elseif ($t[0] === "\n") { 195: if ($inlineParser) {
196: if ($hasValue) {
197: if ($hasKey) $result[$key] = $value; else $result[] = $value;
198: $hasKey = $hasValue = FALSE;
199: }
200:
201: } else {
202: while (isset($tokens[$n+1]) && $tokens[$n+1][0] === "\n") $n++; 203: if (!isset($tokens[$n+1])) break;
204:
205: $newIndent = strlen($tokens[$n]) - 1;
206: if ($indent === NULL) { 207: $indent = $newIndent;
208: }
209: if ($newIndent) {
210: if ($this->indentTabs === NULL) {
211: $this->indentTabs = $tokens[$n][1] === "\t";
212: }
213: if (strpos($tokens[$n], $this->indentTabs ? ' ' : "\t")) {
214: $this->error('Either tabs or spaces may be used as indenting chars, but not both.');
215: }
216: }
217:
218: if ($newIndent > $indent) { 219: if ($hasValue || !$hasKey) {
220: $n++;
221: $this->error('Unexpected indentation.');
222: } elseif ($key === NULL) {
223: $result[] = $this->parse($newIndent);
224: } else {
225: $result[$key] = $this->parse($newIndent);
226: }
227: $newIndent = isset($tokens[$n]) ? strlen($tokens[$n]) - 1 : 0;
228: $hasKey = FALSE;
229:
230: } else {
231: if ($hasValue && !$hasKey) { 232: break;
233:
234: } elseif ($hasKey) {
235: $value = $hasValue ? $value : NULL;
236: if ($key === NULL) $result[] = $value; else $result[$key] = $value;
237: $hasKey = $hasValue = FALSE;
238: }
239: }
240:
241: if ($newIndent < $indent) { 242: return $result; 243: }
244: }
245:
246: } else { 247: if ($hasValue) {
248: $this->error();
249: }
250: static $consts = array(
251: 'true' => TRUE, 'True' => TRUE, 'TRUE' => TRUE, 'yes' => TRUE, 'Yes' => TRUE, 'YES' => TRUE, 'on' => TRUE, 'On' => TRUE, 'ON' => TRUE,
252: 'false' => FALSE, 'False' => FALSE, 'FALSE' => FALSE, 'no' => FALSE, 'No' => FALSE, 'NO' => FALSE, 'off' => FALSE, 'Off' => FALSE, 'OFF' => FALSE,
253: );
254: if ($t[0] === '"') {
255: $value = preg_replace_callback('#\\\\(?:u[0-9a-f]{4}|x[0-9a-f]{2}|.)#i', array($this, 'cbString'), substr($t, 1, -1));
256: } elseif ($t[0] === "'") {
257: $value = substr($t, 1, -1);
258: } elseif (isset($consts[$t])) {
259: $value = $consts[$t];
260: } elseif ($t === 'null' || $t === 'Null' || $t === 'NULL') {
261: $value = NULL;
262: } elseif (is_numeric($t)) {
263: $value = $t * 1;
264: } 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)) {
265: $value = new \DateTime($t);
266: } else { 267: $value = $t;
268: }
269: $hasValue = TRUE;
270: }
271: }
272:
273: if ($inlineParser) {
274: if ($hasValue) {
275: if ($hasKey) $result[$key] = $value; else $result[] = $value;
276: } elseif ($hasKey) {
277: $this->error();
278: }
279: } else {
280: if ($hasValue && !$hasKey) { 281: if ($result === NULL) {
282: $result = $value; 283: } else {
284: $this->error();
285: }
286: } elseif ($hasKey) {
287: $value = $hasValue ? $value : NULL;
288: if ($key === NULL) $result[] = $value; else $result[$key] = $value;
289: }
290: }
291: return $result;
292: }
293:
294:
295:
296: private function cbString($m)
297: {
298: static $mapping = array('t' => "\t", 'n' => "\n", '"' => '"', '\\' => '\\', '/' => '/', '_' => "\xc2\xa0");
299: $sq = $m[0];
300: if (isset($mapping[$sq[1]])) {
301: return $mapping[$sq[1]];
302: } elseif ($sq[1] === 'u' && strlen($sq) === 6) {
303: return String::chr(hexdec(substr($sq, 2)));
304: } elseif ($sq[1] === 'x' && strlen($sq) === 4) {
305: return chr(hexdec(substr($sq, 2)));
306: } else {
307: $this->error("Invalid escaping sequence $sq");
308: }
309: }
310:
311:
312:
313: private function error($message = "Unexpected '%s'")
314: {
315: list(, $line, $col) = self::$tokenizer->getOffset($this->n);
316: $token = isset(self::$tokenizer->tokens[$this->n]) ? str_replace("\n", '<new line>', Nette\String::truncate(self::$tokenizer->tokens[$this->n], 40)) : 'end';
317: throw new NeonException(str_replace('%s', $token, $message) . " on line $line, column $col.");
318: }
319:
320: }
321:
322:
323:
324: 325: 326:
327: class NeonException extends \Exception
328: {
329: }
330: