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