'?', '\*' => '*', '\+' => '+', '\{' => '{', '\}' => '}', '\(' => '(', '\)' => ')', 'd' => '\d', 'F' => '-?\d+(\.\d+)?', 'D' => '-?\d+', 'a' => '[[:alpha:]]', '\\\\' => '\\', ]); $mask = strtr($mask, [ '\\\\' => '\\', ]); return $mask; } /** * Match a string against a mask. * * Special symbols: * - `*` repeat previous any number of times * - `?` previous is optional * - `()` grouping * - `a` alpha * - `d` digit * - `F` float value d, d.ddd with optional leading - * - `+` repeat previous symbol any number of times * - Number - repeat N times * - {N}, {M,N} - repeat n times, like in regex * * Any other characters are matched literally. * * @param string $mask mask to match against * @param string $string tested string * @return bool matches */ public static function maskMatch($mask, $string) { $mask = self::_pregMaskPrepare($mask); return 1 === preg_match('|^' . $mask . '$|u', $string); } /** * Format a string with {0} {foo} or {} * * TODO unit test * * @param string $format * @param array ...$args substitutions. Can also be an explicit array. * @return string */ public static function format($format, ...$args) { $args = func_get_args(); $format = array_shift($args); // explicit array given if (is_array($args[0])) { $args = $args[0]; } $format = preg_replace_callback('#\{\}#', function () { static $i = 0; return '{' . ($i++) . '}'; }, $format); return str_replace( array_map(function ($k) { return '{' . $k . '}'; }, array_keys($args)), array_values($args), $format ); } /** * Remove whitespace on the left side of a block. * This function is quite expensive, so it's recommended * to cache the result, if possible. * * NOTE: The current implementation converts all leading tabs to spaces. * * @param string $txt text to trim * @param int $tab_size number of spaces per tab, default 4 * @param bool $ltrim_nl remove a leading newline * @param bool $ign_noindent ignore lines with zero indentation * @return string left-trimmed block. */ public static function unindentBlock($txt, $tab_size = 4, $ltrim_nl = true, $ign_noindent = true) { $pad = 1024; // max indent size $tabsp = str_repeat(' ', $tab_size); if ($ltrim_nl) { // also make sure first line is not blank $txt = ltrim($txt, "\n"); } $txt = preg_replace_callback('/^([ \t]*)(?=[^ \t\n]|$)/m', function ($m) use (&$pad, $tabsp, $ign_noindent) { static $i = 0; $indent = $m[1]; if ($indent == '' && $ign_noindent) { // no indentation, perhaps stripped by editor? return ''; } // normalize tab to 4 spaces $normalized = strtr($indent, ["\t" => $tabsp]); $len = strlen($normalized); $pad = min($pad, $len); $i++; return $normalized; }, $txt ); return preg_replace("/^ {{$pad}}/m", '', $txt); } /** * Get a simple string representation of an array, similar to json_encode(), * except without the obnoxious quotes and escapes. * * @param array $array * @return string [a, b, c] */ public static function arr($array) { if ($array instanceof Collection || Utils::isAssoc($array)) { $x = '['; foreach ($array as $k => $v) { $x .= "$k:" . json_encode($v) . ', '; } $x = rtrim($x, ', ') . ']'; return $x; } else { return '[' . implode(', ', collect($array)->map(function ($x) { return is_array($x) ? json_encode($x, JSON_UNESCAPED_UNICODE) : (string) $x; })->toArray()) . ']'; } } /** * Get substring from last occurrence of token * * TODO unit test * * @param string $token token delimiting the last chunk from left. Not included. * @param string $haystack * @return string part of haystack after token. */ public static function fromLast($token, $haystack) { $rpos = strrpos($haystack, $token); if ($rpos === false) return $haystack; return substr($haystack, $rpos + strlen($token)); } public static function rpad($str, $len, $fill = ' ') { $filln = max(0, $len - mb_strlen($str)); return $str . str_repeat(mb_substr($fill, 0, 1), $filln); } public static function lpad($str, $len, $fill = ' ') { $filln = max(0, $len - mb_strlen($str)); return str_repeat(mb_substr($fill, 0, 1), $filln) . $str; } /** * Remove wrapping quotes from a string. * C-slashes will also be removed. * * @param $str * @return string */ public static function unquote($str) { if (!$str || !is_string($str)) { return $str; } $a = $str[0]; $b = $str[strlen($str) - 1]; if ($a == $b && $a == '"' || $b == "'") { $str = substr($str, 1, strlen($str) - 2); } $str = stripcslashes($str); return $str; } /** * Apply a rewrite. * * - Simple foo|bar rewrites 0|1 (or false|true) * - Key-value rewrite is possible with 7=foo|9=bar * - '*' (asterisk) matches everything (9=foo|*=other) * - '\*' - match literal asterisk * - starts with % - format using sprintf * - Compare funcs can also be used: lt, gt, le, ge, range, in * example: lt(100)=Foo|range(100,200)=Bar|gt(200)=Baz * * @param mixed $value value from expression * @param string $rewrite rewrite patterns, | separated * @return mixed result to show */ public static function rewrite($value, $rewrite) { // TODO předělat na jednodušší zápis if ($rewrite[0] == '%') return sprintf($rewrite, $value); $rewrite_map = []; foreach (explode('|', trim($rewrite)) as $i => $rw) { $ar = preg_split('/(? $replacement) { if (is_numeric($k) && (((int)$k == (int)$value) || abs((float)$k - (float)$value)<0.00001)) { // exact match return $replacement; } else if (($k==='true'||$k==='false') && (Utils::parseBool($k) == Utils::parseBool($value))) { // bool match return $replacement; } else if (preg_match('/([a-z]+)\(([^)]+)\)/i', $k, $mm)) { // we have a comparing function if (static::testCompareFunc($value, $mm[1], $mm[2])) { return $replacement; } } else if ($k === '*') { return $replacement; // catch-all } else if ($k === '\\*') { if ($value === '*') { return $replacement; // literal asterisk } } } return $value; } /** * Check if compare function matches value, copied from FB2 * * @param mixed $value value to format * @param string $func function name * @param mixed $argument extra argument for the func * @return bool */ private static function testCompareFunc($value, $func, $argument) { $value_f = floatval($value); $arg_f = floatval($argument); $fun = trim(strtolower($func)); switch ($fun) { case 'lt': return $value_f < $arg_f; case 'gt': return $value_f > $arg_f; case 'le': return $value_f <= $arg_f; case 'ge': return $value_f >= $arg_f; case 'eq': return $value_f == $arg_f; // this is kinda useless, but to make the set complete case 'range': // range(-10,0) = zima $bounds = array_map(function ($x) { return floatval(trim($x)); }, explode(',', $argument)); if (count($bounds) != 2) { Log::error("Invalid range bounds: $argument"); return false; } return $value_f >= $bounds[0] && $value_f < $bounds[1]; case 'in': // in(10,20,30) = 10, 20 or 30 *funguje i pro string in(ZAP,VYP) = ZAP nebo VYP $bounds = array_map('trim', explode(',', $argument)); return in_array(trim($value), $bounds); default: Log::error("Invalid rewrite function: $func"); return false; } } /** * Apply multiple sets of substitutions to a format and get all the results. * * @param string $format Format same as for Str::format(). Fields are marked {}, {0} or {key} * @param array $subs_arrays array of arrays of substitutions - eg. [[a1, b1], [a2, b2], ...] * @return array */ public static function mapFormat($format, $subs_arrays) { $gather = []; foreach ($subs_arrays as $subs) { if (!is_array($subs)) $subs = [$subs]; $gather[] = Str::format($format, $subs); } return $gather; } /** * Find all needle positions within a haystack * * @param string $haystack * @param string $needle * @return array */ public static function findPositions($haystack, $needle) { $lastPos = 0; $positions = []; while (($lastPos = strpos($haystack, $needle, $lastPos)) !== false) { $positions[] = $lastPos; $lastPos = $lastPos + strlen($needle); } return $positions; } /** * Discard positions in a string that are preceded by an unescaped backslash. * * @param string $str * @param int[] $positions * @return int[] */ public static function discardEscapedPositions($str, array $positions) { $actualPos = []; foreach ($positions as $pos) { if ($pos >= 1) { if ($str[$pos - 1] == '\\') { if ($pos >= 2 && $str[$pos - 2] == '\\') { // escaped backslash before - it's a valid quote } else { // this quote is escaped continue; } } } $actualPos[] = $pos; } return $actualPos; } /** * Split a string at given positions * * @param string $string * @param int[] $positions * @return string[] */ public static function splitAt($string, $positions) { $chunks = []; array_push($positions, strlen($string)); array_unshift($positions, -1); foreach ($positions as $i => $position) { if ($position >= strlen($string)) break; $chunks[] = substr($string, $position + 1, $positions[$i + 1] - $position - 1); } return $chunks; } /** * Split a string to pieces by commas, ignoring commas within strings delimited by double quotes. * A double quote can be escaped using a backslash. * * @param string $str * @param string $delimiter * @return mixed */ public static function splitCommandArgs($str, $delimiter = ',') { if ($str === '') return []; // Find unescaped quotes $quotes = Str::findPositions($str, '"'); $quotes = Str::discardEscapedPositions($str, $quotes); if (count($quotes) % 2 != 0) { throw new FormatException("Unmatched quote in command arguments: $str"); } $commas = Str::findPositions($str, $delimiter); $commas = Utils::discardPositionsWithinPairs($commas, $quotes); $chunks = Str::splitAt($str, $commas); $arr = collect($chunks)->trim()->toArray(); return $arr; } /** * Get substring to first match of a token * * @param string $token token delimiting the parts * @param string $haystack full string * @return string portion of the string until the first token; or the whole string if token is not present. */ public static function toFirst($token, $haystack) { $lpos = strpos($haystack, $token); if ($lpos === false) return $haystack; return substr($haystack, 0, $lpos); } /** * Get substring from first match of a token * * @param string $token token delimiting the parts * @param string $haystack full string * @return string portion of the string until the first token; or empty string if token is not present. */ public static function fromFirst($token, $haystack, $exclusive = false) { $lpos = strpos($haystack, $token); if ($lpos === false) return ''; return substr($haystack, $lpos+($exclusive?1:0)); } public static function toLast($token, $haystack) { $rpos = strrpos($haystack, $token); if ($rpos === false) return $haystack; return self::substr($haystack, 0, $rpos); } /** * Check if translation exists * * @param string $descrKey tested transl key * @return bool exists */ public static function translationExists($descrKey) { $tr = trans($descrKey); return !preg_match('/^(\w+\.)+(\w+)$/', $tr); } /** * Expand array of strings using bash-style repeat patterns {a,b,c}, {from..to} * * ie. to produce ad1 to ad4, use ad{1..4}. Supports multiple patterns, producing all permutations. * * @param string|string[] $sourceStrings * @return string[] */ public static function expandBashRepeat($sourceStrings) { if (is_string($sourceStrings)) $sourceStrings = [$sourceStrings]; $outputs = []; foreach($sourceStrings as $str) { $i=0; $arrays = []; $str2 = preg_replace_callback('/\{([^{}]+)\}/', function($m) use(&$i, &$arrays) { $seq = explode(',',$m[1]); if(count($seq)>=2) { $arrays[$i] = $seq; } else { $ab = explode('..',$m[1]); if (!$ab) { return $m[0]; } $a = intval($ab[0]); $b = intval($ab[1]); $arrays[$i] = range($a, $b); } return '{{'.($i++).'}}'; }, $str); $strs = [$str2]; for($n=count($arrays)-1;$n>=0;$n--) { $arr = $arrays[$n]; $tmpstrs = []; foreach($arr as $subs) { foreach($strs as $ss) { $tmpstrs[] = str_replace('{{'.$n.'}}', $subs, $ss); } } $strs = $tmpstrs; } $outputs = array_merge($outputs, $strs); } return $outputs; } /** * Remove trailing commas from JSON string * * @param string $str * @return string */ public static function cleanJson($str) { return preg_replace('/,\s*([}\]])/s','\1', $str); } public static function ellipsis($str, $maxlen) { $len = mb_strlen($str); if ($len > $maxlen) { return mb_substr($str, 0, $maxlen) . '…'; } return $str; } }