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