PageRenderTime 44ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/sys/lib/ym.php

https://gitlab.com/VxMxPx/mysli
PHP | 587 lines | 503 code | 29 blank | 55 comment | 22 complexity | 067b7ee8662e15199f64e933285cea37 MD5 | raw file
  1. <?php
  2. namespace sys;
  3. use \sys\fs\file;
  4. class ym
  5. {
  6. /**
  7. * Decode .ym file and return string.
  8. * @param string $filename
  9. * @return array
  10. */
  11. static function decode_file($filename)
  12. {
  13. return self::decode(file::read($filename));
  14. }
  15. static function decode($string)
  16. {
  17. # Empty string
  18. if (!trim($string)) return [];
  19. # So far converted string.
  20. $list = [];
  21. # Handle levels.
  22. $stack = [&$list];
  23. # Indentation type + size
  24. $indent = self::detect_indent($string);
  25. # Current indentation level.
  26. $level = 0;
  27. # To array
  28. $lines = str::lines($string);
  29. # Multiline
  30. # $type is QUOTE (""), INDENT (> )
  31. # $divider is either "\n" or ' ' (defines how new lines are divided).
  32. $multiline = [
  33. 'enabled' => false,
  34. 'buffer' => '',
  35. 'type' => 'INDENT',
  36. 'divider' => ' ',
  37. 'level' => 0,
  38. 'key' => null
  39. ];
  40. foreach ($lines as $lineno => $line)
  41. {
  42. // Get current indentation level
  43. $level = $indent ? self::get_level($line, $indent) : 0;
  44. // Buffering multiline text?
  45. if ($multiline['enabled'] === true)
  46. {
  47. if ($multiline['type'] === 'QUOTE')
  48. {
  49. $multiline['buffer'] .= $multiline['divider'].trim($line);
  50. if (substr($line, -1) === '"' && substr($line, -2) !== '\\"')
  51. {
  52. // End buffer
  53. $stack[$multiline['level']][$multiline['key']] = rtrim($multiline['buffer'], '"');
  54. $multiline['enabled'] = false;
  55. }
  56. continue;
  57. }
  58. elseif ($multiline['type'] === 'INDENT')
  59. {
  60. if ($level === $multiline['level']+1)
  61. {
  62. $multiline['buffer'] .= $multiline['divider'].trim($line);
  63. continue;
  64. }
  65. else
  66. {
  67. $stack[$multiline['level']][$multiline['key']] = ltrim($multiline['buffer']);
  68. $multiline['enabled'] = false;
  69. }
  70. }
  71. }
  72. // An empty line
  73. if (!trim($line))
  74. {
  75. continue;
  76. }
  77. // Comment
  78. if (substr(trim($line), 0, 1) === '#')
  79. {
  80. continue;
  81. }
  82. $stack = array_slice($stack, 0, $level+1);
  83. try
  84. {
  85. // List item...
  86. if (substr(trim($line), 0, 1) === '-')
  87. {
  88. list($_, $value) = self::proc_line(ltrim($line, "\t -"), true);
  89. // just one - meaning sub category
  90. if (!$value)
  91. {
  92. $key = count($stack[$level]);
  93. $stack[$level][$key] = [];
  94. $stack[] = &$stack[$level][$key];
  95. }
  96. else
  97. {
  98. $stack[$level][] = self::valufy($value);
  99. }
  100. continue;
  101. }
  102. list($key, $value) = self::proc_line($line, false);
  103. // Multiline?
  104. if (($value === '>' || $value === '|') ||
  105. substr($value, 0, 1) === '"' && substr($value, -1) !== '"')
  106. {
  107. // Define multiline
  108. $multiline = [
  109. 'enabled' => true,
  110. 'buffer' => trim($value, '">|'),
  111. 'type' => (substr($value, 0, 1) !== '"' ? 'INDENT' : 'QUOTE'),
  112. 'divider' => $value === '|' ? "\n" : ' ',
  113. 'level' => $level,
  114. 'key' => $key
  115. ];
  116. continue;
  117. }
  118. elseif (trim($value) !== '')
  119. {
  120. $value = self::valufy($value);
  121. $stack[$level][$key] = $value;
  122. }
  123. else
  124. {
  125. $stack[$level][$key] = [];
  126. $stack[] = &$stack[$level][$key];
  127. }
  128. }
  129. catch (\Exception $e)
  130. {
  131. err::t(
  132. 'decode_exception',
  133. $e->getMessage()."\n".err_lines($lines, $lineno));
  134. }
  135. }
  136. return $list;
  137. }
  138. /**
  139. * Encode an array to .ym file.
  140. * --
  141. * @param string $filename
  142. * @param array $in
  143. * --
  144. * @return boolean
  145. */
  146. static function encode_file($filename, array $in)
  147. {
  148. try
  149. {
  150. return !! file::write($filename, self::encode($in));
  151. }
  152. catch (exception\ym $e)
  153. {
  154. err::t('ecode_file_exception', $e->getMessage()."\nFile: {$filename}");
  155. }
  156. }
  157. /**
  158. * Encode an array to .ym string.
  159. * --
  160. * @param array $in
  161. * @param integer $lvl
  162. * Current indentation level (0).
  163. * --
  164. * @return string
  165. */
  166. static function encode(array $in, $lvl=0)
  167. {
  168. $output = '';
  169. foreach ($in as $key => $value)
  170. {
  171. $output .= str_repeat(' ', $lvl*sys_ym_indent);
  172. if (is_array($value))
  173. {
  174. if (is_integer($key))
  175. {
  176. $output .= "-";
  177. }
  178. else
  179. {
  180. $output .= "{$key}:";
  181. }
  182. if (empty($value))
  183. {
  184. $output .= " []\n";
  185. }
  186. else
  187. {
  188. $output .= "\n".self::encode($value, $lvl+1);
  189. }
  190. continue;
  191. }
  192. // Convert value
  193. if (is_numeric($value) && is_string($value))
  194. $value = '"'.$value.'"';
  195. elseif (in_array(strtolower($value), ['yes', 'true']))
  196. $value = '"Yes"';
  197. elseif (in_array(strtolower($value), ['no', 'false']))
  198. $value = '"No"';
  199. elseif (is_null($value))
  200. $value = 'null';
  201. elseif ($value === true)
  202. $value = 'Yes';
  203. elseif ($value === false)
  204. $value = 'No';
  205. if (is_integer($key))
  206. {
  207. $output .= "- {$value}\n";
  208. }
  209. else
  210. {
  211. if (strpos($value, "\n"))
  212. {
  213. $value = '|'.str_replace("\n", "\n".str_repeat(' ', ($lvl+1)*sys_ym_indent), "\n".$value);
  214. }
  215. $output .= "{$key}: {$value}\n";
  216. }
  217. }
  218. return $output;
  219. }
  220. /*
  221. --- Protected --------------------------------------------------------------
  222. */
  223. /**
  224. * Extract key / value from line!
  225. * --
  226. * @param string $line
  227. *
  228. * @param boolean $li
  229. * List item?
  230. * --
  231. * @return array
  232. * [ string $key, string $value ]
  233. */
  234. protected static function proc_line($line, $li)
  235. {
  236. if (!$li)
  237. {
  238. $segments = explode(':', trim($line), 2);
  239. $key = null;
  240. $value = null;
  241. if (!isset($segments[1]))
  242. {
  243. err::t('missing_colon');
  244. }
  245. else
  246. {
  247. $key = $segments[0];
  248. $value = $segments[1];
  249. }
  250. $key = trim($key, "\t \"");
  251. }
  252. else
  253. {
  254. $key = null;
  255. $value = $line;
  256. }
  257. $value = trim($value, "\t ");
  258. return [$key, $value];
  259. }
  260. /**
  261. * Resolve inline array and return array.
  262. * --
  263. * @param string $line
  264. * --
  265. * @return array
  266. */
  267. protected static function resolve_array($line)
  268. {
  269. // $line = trim($line, " \t[]");
  270. // Weather next character should be escaped \\
  271. $escaped = false;
  272. // Weather is is envelope ""
  273. $envelope = false;
  274. // Array key
  275. $key = null;
  276. // Current buffer
  277. $buffer = null;
  278. // Weather next item is needed
  279. // (in such case only acceptable characters are: ` `, `]`, `,`, EOL)
  280. $need_next = false;
  281. $collection = [];
  282. // Current array stack...
  283. $pocket = [ &$collection ];
  284. $characters = preg_split("//u", $line, -1, PREG_SPLIT_NO_EMPTY);
  285. foreach ($characters as $cpos => $char)
  286. {
  287. // Properly handle closed lists, proceeding with no comma...
  288. if ($need_next)
  289. {
  290. if ($char == ',' || $char == ']')
  291. $need_next = false;
  292. elseif ($char != ' ')
  293. err::t('unexpected_character',
  294. f_error(
  295. $line, $cpos,
  296. "Unexpected character, expecting: `]` or `,`"));
  297. }
  298. // Handle escaped or enveloped \ or "
  299. if ( ($escaped || $envelope) && ($char !== '"' && $char !== '\\'))
  300. {
  301. $buffer .= $char;
  302. continue;
  303. }
  304. // Handle special characters or add character to the list...
  305. switch ($char)
  306. {
  307. /*
  308. ESCAPE
  309. */
  310. case '\\':
  311. if ($escaped)
  312. {
  313. $buffer .= '\\';
  314. $escaped = false;
  315. }
  316. else
  317. $escaped = true;
  318. break;
  319. /*
  320. Buffer start
  321. */
  322. case '"':
  323. $buffer .= '"';
  324. if ($escaped)
  325. {
  326. $escaped = false;
  327. break;
  328. }
  329. $envelope = !$envelope;
  330. break;
  331. /*
  332. Key/value separator
  333. */
  334. case ':':
  335. if ($key !== null)
  336. {
  337. err::t(
  338. 'unexpected_colon_expecting_value',
  339. f_error($line, $cpos, "Unexpected colon, expecting value..."));
  340. }
  341. // Key : Value
  342. if (!empty(trim($buffer)))
  343. {
  344. $key = trim( trim($buffer), '"' );
  345. $buffer = null;
  346. }
  347. else
  348. {
  349. err::t(
  350. 'unexpected_colon',
  351. f_error($line, $cpos, "Unexpected colon..."));
  352. }
  353. break;
  354. /*
  355. Comma
  356. */
  357. case ',':
  358. $buffer = trim($buffer);
  359. if (!empty(trim($buffer, '"')))
  360. {
  361. $buffer = self::valufy($buffer);
  362. if ($key !== null)
  363. $pocket[count($pocket)-1][$key] = $buffer;
  364. else
  365. $pocket[count($pocket)-1][] = $buffer;
  366. }
  367. $buffer = $key = null;
  368. break;
  369. /*
  370. Sub Array, Open
  371. */
  372. case '[':
  373. $array = [];
  374. if ($key !== null)
  375. $pocket[count($pocket)-1][$key] = &$array;
  376. else
  377. $pocket[count($pocket)-1][] = &$array;
  378. $buffer = $key = null;
  379. $pocket[] = &$array;
  380. unset($array);
  381. break;
  382. /*
  383. Sub Array, Close
  384. */
  385. case ']':
  386. $buffer = trim($buffer);
  387. if (!empty(trim($buffer, '"')))
  388. {
  389. $buffer = self::valufy($buffer);
  390. if ($key !== null)
  391. $pocket[count($pocket)-1][$key] = $buffer;
  392. else
  393. $pocket[count($pocket)-1][] = $buffer;
  394. }
  395. $need_next = true;
  396. $key = $buffer = null;
  397. array_pop($pocket);
  398. break;
  399. /*
  400. Append!
  401. */
  402. default:
  403. $buffer .= $char;
  404. break;
  405. }
  406. }
  407. // Check of errors...
  408. if (count($pocket) > 1)
  409. {
  410. err::t('unclosed_array');
  411. }
  412. $buffer = $key = null;
  413. return $collection[0];
  414. }
  415. /**
  416. * Convert string representation of value to correct type.
  417. * --
  418. * @param string $value
  419. * --
  420. * @return mixed
  421. */
  422. protected static function valufy($value)
  423. {
  424. if (empty($value)) return '';
  425. if (is_numeric($value))
  426. {
  427. return strpos($value, '.')
  428. ? (float) $value
  429. : (int) $value;
  430. }
  431. elseif ($value === '[]')
  432. {
  433. return [];
  434. }
  435. elseif (substr($value, 0, 1) === '[')
  436. {
  437. if (substr($value, -1) !== ']')
  438. err::t('unclosed_array');
  439. // Resolve array
  440. return self::resolve_array($value);
  441. }
  442. elseif (in_array(strtolower($value), ['yes', 'true']))
  443. {
  444. return true;
  445. }
  446. elseif (in_array(strtolower($value), ['no', 'false']))
  447. {
  448. return false;
  449. }
  450. elseif (strtolower($value) === 'null')
  451. {
  452. return null;
  453. }
  454. else
  455. {
  456. return self::unquote(stripslashes($value));
  457. }
  458. }
  459. /**
  460. * Unquoate value in a smart way (if value starts and ends in same '' or "")
  461. * then remove first and last character.
  462. * --
  463. * @param string $value
  464. * -`-
  465. * @return string
  466. */
  467. protected static function unquote($value)
  468. {
  469. if (!$value)
  470. {
  471. return;
  472. }
  473. $start = substr($value, 0, 1);
  474. if (!in_array($start, ['"', "'"]))
  475. {
  476. return $value;
  477. }
  478. if ($start !== substr($value, -1))
  479. {
  480. return $value;
  481. }
  482. return substr($value, 1, -1);
  483. }
  484. /**
  485. * Get current indentation level.
  486. * --
  487. * @param string $line
  488. * @param string $indent
  489. * --
  490. * @return integer
  491. */
  492. protected static function get_level($line, $indent)
  493. {
  494. $level = 0;
  495. $indent_length = strlen($indent);
  496. while (substr($line, 0, $indent_length) === $indent)
  497. {
  498. $line = substr($line, $indent_length);
  499. $level++;
  500. }
  501. return $level;
  502. }
  503. /**
  504. * Get Indentation (type).
  505. * --
  506. * @param string $string
  507. * --
  508. * @return string
  509. */
  510. protected static function detect_indent($string)
  511. {
  512. if (preg_match('/(^[ \t]+)/m', $string, $matches))
  513. {
  514. return $matches[1];
  515. }
  516. }
  517. }