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