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