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: '-(?=\s|$)|:(?=[\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: } elseif ($var instanceof NNeonEntity) {
63: return self::encode($var->value) . '(' . substr(self::encode($var->attributes), 1, -1) . ')';
64: }
65:
66: if (is_object($var)) {
67: $obj = $var; $var = array();
68: foreach ($obj as $k => $v) {
69: $var[$k] = $v;
70: }
71: }
72:
73: if (is_array($var)) {
74: $isList = NValidators::isList($var);
75: $s = '';
76: if ($options & self::BLOCK) {
77: if (count($var) === 0){
78: return "[]";
79: }
80: foreach ($var as $k => $v) {
81: $v = self::encode($v, self::BLOCK);
82: $s .= ($isList ? '-' : self::encode($k) . ':')
83: . (NStrings::contains($v, "\n") ? "\n\t" . str_replace("\n", "\n\t", $v) : ' ' . $v)
84: . "\n";
85: continue;
86: }
87: return $s;
88:
89: } else {
90: foreach ($var as $k => $v) {
91: $s .= ($isList ? '' : self::encode($k) . ': ') . self::encode($v) . ', ';
92: }
93: return ($isList ? '[' : '{') . substr($s, 0, -2) . ($isList ? ']' : '}');
94: }
95:
96: } elseif (is_string($var) && !is_numeric($var)
97: && !preg_match('~[\x00-\x1F]|^\d{4}|^(true|false|yes|no|on|off|null)\z~i', $var)
98: && preg_match('~^' . self::$patterns[4] . '\z~', $var)
99: ) {
100: return $var;
101:
102: } elseif (is_float($var)) {
103: $var = var_export($var, TRUE);
104: return NStrings::contains($var, '.') ? $var : $var . '.0';
105:
106: } else {
107: return json_encode($var);
108: }
109: }
110:
111:
112:
113: 114: 115: 116: 117:
118: public static function decode($input)
119: {
120: if (!is_string($input)) {
121: throw new InvalidArgumentException("Argument must be a string, " . gettype($input) . " given.");
122: }
123: if (!self::$tokenizer) {
124: self::$tokenizer = new NTokenizer(self::$patterns, 'mi');
125: }
126:
127: $input = str_replace("\r", '', $input);
128: self::$tokenizer->tokenize($input);
129:
130: $parser = new self;
131: $res = $parser->parse(0);
132:
133: while (isset(self::$tokenizer->tokens[$parser->n])) {
134: if (self::$tokenizer->tokens[$parser->n][0] === "\n") {
135: $parser->n++;
136: } else {
137: $parser->error();
138: }
139: }
140: return $res;
141: }
142:
143:
144:
145: 146: 147: 148: 149:
150: private function parse($indent = NULL, $result = NULL)
151: {
152: $inlineParser = $indent === NULL;
153: $value = $key = $object = NULL;
154: $hasValue = $hasKey = FALSE;
155: $tokens = self::$tokenizer->tokens;
156: $n = & $this->n;
157: $count = count($tokens);
158:
159: for (; $n < $count; $n++) {
160: $t = $tokens[$n];
161:
162: if ($t === ',') {
163: if ((!$hasKey && !$hasValue) || !$inlineParser) {
164: $this->error();
165: }
166: $this->addValue($result, $hasKey, $key, $hasValue ? $value : NULL);
167: $hasKey = $hasValue = FALSE;
168:
169: } elseif ($t === ':' || $t === '=') {
170: if ($hasKey || !$hasValue) {
171: $this->error();
172: }
173: if (is_array($value) || is_object($value)) {
174: $this->error('Unacceptable key');
175: }
176: $key = (string) $value;
177: $hasKey = TRUE;
178: $hasValue = FALSE;
179:
180: } elseif ($t === '-') {
181: if ($hasKey || $hasValue || $inlineParser) {
182: $this->error();
183: }
184: $key = NULL;
185: $hasKey = TRUE;
186:
187: } elseif (isset(self::$brackets[$t])) {
188: if ($hasValue) {
189: if ($t !== '(') {
190: $this->error();
191: }
192: $n++;
193: $entity = new NNeonEntity;
194: $entity->value = $value;
195: $entity->attributes = $this->parse(NULL, array());
196: $value = $entity;
197: } else {
198: $n++;
199: $value = $this->parse(NULL, array());
200: }
201: $hasValue = TRUE;
202: if (!isset($tokens[$n]) || $tokens[$n] !== self::$brackets[$t]) {
203: $this->error();
204: }
205:
206: } elseif ($t === ']' || $t === '}' || $t === ')') {
207: if (!$inlineParser) {
208: $this->error();
209: }
210: break;
211:
212: } elseif ($t[0] === "\n") {
213: if ($inlineParser) {
214: if ($hasKey || $hasValue) {
215: $this->addValue($result, $hasKey, $key, $hasValue ? $value : NULL);
216: $hasKey = $hasValue = FALSE;
217: }
218:
219: } else {
220: while (isset($tokens[$n+1]) && $tokens[$n+1][0] === "\n") $n++;
221: if (!isset($tokens[$n+1])) {
222: break;
223: }
224:
225: $newIndent = strlen($tokens[$n]) - 1;
226: if ($indent === NULL) {
227: $indent = $newIndent;
228: }
229: if ($newIndent) {
230: if ($this->indentTabs === NULL) {
231: $this->indentTabs = $tokens[$n][1] === "\t";
232: }
233: if (strpos($tokens[$n], $this->indentTabs ? ' ' : "\t")) {
234: $n++;
235: $this->error('Either tabs or spaces may be used as indenting chars, but not both.');
236: }
237: }
238:
239: if ($newIndent > $indent) {
240: if ($hasValue || !$hasKey) {
241: $n++;
242: $this->error('Unexpected indentation.');
243: } else {
244: $this->addValue($result, $key !== NULL, $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: $this->addValue($result, $key !== NULL, $key, $hasValue ? $value : NULL);
255: $hasKey = $hasValue = FALSE;
256: }
257: }
258:
259: if ($newIndent < $indent) {
260: return $result;
261: }
262: }
263:
264: } else {
265: if ($hasValue) {
266: $this->error();
267: }
268: static $consts = array(
269: 'true' => TRUE, 'True' => TRUE, 'TRUE' => TRUE, 'yes' => TRUE, 'Yes' => TRUE, 'YES' => TRUE, 'on' => TRUE, 'On' => TRUE, 'ON' => TRUE,
270: 'false' => FALSE, 'False' => FALSE, 'FALSE' => FALSE, 'no' => FALSE, 'No' => FALSE, 'NO' => FALSE, 'off' => FALSE, 'Off' => FALSE, 'OFF' => FALSE,
271: );
272: if ($t[0] === '"') {
273: $value = preg_replace_callback('#\\\\(?:u[0-9a-f]{4}|x[0-9a-f]{2}|.)#i', array($this, 'cbString'), substr($t, 1, -1));
274: } elseif ($t[0] === "'") {
275: $value = substr($t, 1, -1);
276: } elseif (isset($consts[$t])) {
277: $value = $consts[$t];
278: } elseif ($t === 'null' || $t === 'Null' || $t === 'NULL') {
279: $value = NULL;
280: } elseif (is_numeric($t)) {
281: $value = $t * 1;
282: } 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)) {
283: $value = new NDateTime53($t);
284: } else {
285: $value = $t;
286: }
287: $hasValue = TRUE;
288: }
289: }
290:
291: if ($inlineParser) {
292: if ($hasKey || $hasValue) {
293: $this->addValue($result, $hasKey, $key, $hasValue ? $value : NULL);
294: }
295: } else {
296: if ($hasValue && !$hasKey) {
297: if ($result === NULL) {
298: $result = $value;
299: } else {
300: $this->error();
301: }
302: } elseif ($hasKey) {
303: $this->addValue($result, $key !== NULL, $key, $hasValue ? $value : NULL);
304: }
305: }
306: return $result;
307: }
308:
309:
310:
311: private function addValue(&$result, $hasKey, $key, $value)
312: {
313: if ($hasKey) {
314: if ($result && array_key_exists($key, $result)) {
315: $this->error("Duplicated key '$key'");
316: }
317: $result[$key] = $value;
318: } else {
319: $result[] = $value;
320: }
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 NNeonEntity extends stdClass
360: {
361: public $value;
362: public $attributes;
363: }
364:
365:
366:
367: 368: 369: 370:
371: class NNeonException extends Exception
372: {
373: }
374: