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