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