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