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