8889841cPKk[V"6{{Type.phpnuW+A */ private $types; /** @var bool */ private $simple; /** @var string |, & */ private $kind; /** * Creates a Type object based on reflection. Resolves self, static and parent to the actual class name. * If the subject has no type, it returns null. * @param \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $reflection */ public static function fromReflection($reflection): ?self { if ($reflection instanceof \ReflectionProperty && PHP_VERSION_ID < 70400) { return null; } elseif ($reflection instanceof \ReflectionMethod) { $type = $reflection->getReturnType() ?? (PHP_VERSION_ID >= 80100 ? $reflection->getTentativeReturnType() : null); } else { $type = $reflection instanceof \ReflectionFunctionAbstract ? $reflection->getReturnType() : $reflection->getType(); } return $type ? self::fromReflectionType($type, $reflection, true) : null; } private static function fromReflectionType(\ReflectionType $type, $of, bool $asObject) { if ($type instanceof \ReflectionNamedType) { $name = self::resolve($type->getName(), $of); return $asObject ? new self($type->allowsNull() && $name !== 'mixed' ? [$name, 'null'] : [$name]) : $name; } elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { return new self( array_map( function ($t) use ($of) { return self::fromReflectionType($t, $of, false); }, $type->getTypes() ), $type instanceof \ReflectionUnionType ? '|' : '&' ); } else { throw new Nette\InvalidStateException('Unexpected type of ' . Reflection::toString($of)); } } /** * Creates the Type object according to the text notation. */ public static function fromString(string $type): self { if (!Validators::isTypeDeclaration($type)) { throw new Nette\InvalidArgumentException("Invalid type '$type'."); } if ($type[0] === '?') { return new self([substr($type, 1), 'null']); } $unions = []; foreach (explode('|', $type) as $part) { $part = explode('&', trim($part, '()')); $unions[] = count($part) === 1 ? $part[0] : new self($part, '&'); } return count($unions) === 1 && $unions[0] instanceof self ? $unions[0] : new self($unions); } /** * Resolves 'self', 'static' and 'parent' to the actual class name. * @param \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $of */ public static function resolve(string $type, $of): string { $lower = strtolower($type); if ($of instanceof \ReflectionFunction) { return $type; } elseif ($lower === 'self' || $lower === 'static') { return $of->getDeclaringClass()->name; } elseif ($lower === 'parent' && $of->getDeclaringClass()->getParentClass()) { return $of->getDeclaringClass()->getParentClass()->name; } else { return $type; } } private function __construct(array $types, string $kind = '|') { $o = array_search('null', $types, true); if ($o !== false) { // null as last array_splice($types, $o, 1); $types[] = 'null'; } $this->types = $types; $this->simple = is_string($types[0]) && ($types[1] ?? 'null') === 'null'; $this->kind = count($types) > 1 ? $kind : ''; } public function __toString(): string { $multi = count($this->types) > 1; if ($this->simple) { return ($multi ? '?' : '') . $this->types[0]; } $res = []; foreach ($this->types as $type) { $res[] = $type instanceof self && $multi ? "($type)" : $type; } return implode($this->kind, $res); } /** * Returns the array of subtypes that make up the compound type as strings. * @return array */ public function getNames(): array { return array_map(function ($t) { return $t instanceof self ? $t->getNames() : $t; }, $this->types); } /** * Returns the array of subtypes that make up the compound type as Type objects: * @return self[] */ public function getTypes(): array { return array_map(function ($t) { return $t instanceof self ? $t : new self([$t]); }, $this->types); } /** * Returns the type name for simple types, otherwise null. */ public function getSingleName(): ?string { return $this->simple ? $this->types[0] : null; } /** * Returns true whether it is a union type. */ public function isUnion(): bool { return $this->kind === '|'; } /** * Returns true whether it is an intersection type. */ public function isIntersection(): bool { return $this->kind === '&'; } /** * Returns true whether it is a simple type. Single nullable types are also considered to be simple types. */ public function isSimple(): bool { return $this->simple; } /** @deprecated use isSimple() */ public function isSingle(): bool { return $this->simple; } /** * Returns true whether the type is both a simple and a PHP built-in type. */ public function isBuiltin(): bool { return $this->simple && Validators::isBuiltinType($this->types[0]); } /** * Returns true whether the type is both a simple and a class name. */ public function isClass(): bool { return $this->simple && !Validators::isBuiltinType($this->types[0]); } /** * Determines if type is special class name self/parent/static. */ public function isClassKeyword(): bool { return $this->simple && Validators::isClassKeyword($this->types[0]); } /** * Verifies type compatibility. For example, it checks if a value of a certain type could be passed as a parameter. */ public function allows(string $subtype): bool { if ($this->types === ['mixed']) { return true; } $subtype = self::fromString($subtype); return $subtype->isUnion() ? Arrays::every($subtype->types, function ($t) { return $this->allows2($t instanceof self ? $t->types : [$t]); }) : $this->allows2($subtype->types); } private function allows2(array $subtypes): bool { return $this->isUnion() ? Arrays::some($this->types, function ($t) use ($subtypes) { return $this->allows3($t instanceof self ? $t->types : [$t], $subtypes); }) : $this->allows3($this->types, $subtypes); } private function allows3(array $types, array $subtypes): bool { return Arrays::every($types, function ($type) use ($subtypes) { $builtin = Validators::isBuiltinType($type); return Arrays::some($subtypes, function ($subtype) use ($type, $builtin) { return $builtin ? strcasecmp($type, $subtype) === 0 : is_a($subtype, $type, true); }); }); } } PKk[""Json.phpnuW+A 'Internal error', PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit was exhausted', PREG_RECURSION_LIMIT_ERROR => 'Recursion limit was exhausted', PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 data', PREG_BAD_UTF8_OFFSET_ERROR => 'Offset didn\'t correspond to the begin of a valid UTF-8 code point', 6 => 'Failed due to limited JIT stack space', // PREG_JIT_STACKLIMIT_ERROR ]; } /** * The exception that indicates assertion error. */ class AssertionException extends \Exception { } PKk[lUA DateTime.phpnuW+Aformat('Y-m-d H:i:s.u'), $time->getTimezone()); } elseif (is_numeric($time)) { if ($time <= self::YEAR) { $time += time(); } return (new static('@' . $time))->setTimezone(new \DateTimeZone(date_default_timezone_get())); } else { // textual or null return new static((string) $time); } } /** * Creates DateTime object. * @return static * @throws Nette\InvalidArgumentException if the date and time are not valid. */ public static function fromParts( int $year, int $month, int $day, int $hour = 0, int $minute = 0, float $second = 0.0 ) { $s = sprintf('%04d-%02d-%02d %02d:%02d:%02.5F', $year, $month, $day, $hour, $minute, $second); if ( !checkdate($month, $day, $year) || $hour < 0 || $hour > 23 || $minute < 0 || $minute > 59 || $second < 0 || $second >= 60 ) { throw new Nette\InvalidArgumentException("Invalid date '$s'"); } return new static($s); } /** * Returns new DateTime object formatted according to the specified format. * @param string $format The format the $time parameter should be in * @param string $time * @param string|\DateTimeZone $timezone (default timezone is used if null is passed) * @return static|false */ #[\ReturnTypeWillChange] public static function createFromFormat($format, $time, $timezone = null) { if ($timezone === null) { $timezone = new \DateTimeZone(date_default_timezone_get()); } elseif (is_string($timezone)) { $timezone = new \DateTimeZone($timezone); } elseif (!$timezone instanceof \DateTimeZone) { throw new Nette\InvalidArgumentException('Invalid timezone given'); } $date = parent::createFromFormat($format, $time, $timezone); return $date ? static::from($date) : false; } /** * Returns JSON representation in ISO 8601 (used by JavaScript). */ public function jsonSerialize(): string { return $this->format('c'); } /** * Returns the date and time in the format 'Y-m-d H:i:s'. */ public function __toString(): string { return $this->format('Y-m-d H:i:s'); } /** * Creates a copy with a modified time. * @return static */ public function modifyClone(string $modify = '') { $dolly = clone $this; return $modify ? $dolly->modify($modify) : $dolly; } } PKk[޽Z%% Helpers.phpnuW+A $max) { throw new Nette\InvalidArgumentException("Minimum ($min) is not less than maximum ($max)."); } return min(max($value, $min), $max); } /** * Looks for a string from possibilities that is most similar to value, but not the same (for 8-bit encoding). * @param string[] $possibilities */ public static function getSuggestion(array $possibilities, string $value): ?string { $best = null; $min = (strlen($value) / 4 + 1) * 10 + .1; foreach (array_unique($possibilities) as $item) { if ($item !== $value && ($len = levenshtein($item, $value, 10, 11, 10)) < $min) { $min = $len; $best = $item; } } return $best; } } PKk[p7Ç** Arrays.phpnuW+A $array * @param array-key|array-key[] $key * @param ?T $default * @return ?T * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided */ public static function get(array $array, $key, $default = null) { foreach (is_array($key) ? $key : [$key] as $k) { if (is_array($array) && array_key_exists($k, $array)) { $array = $array[$k]; } else { if (func_num_args() < 3) { throw new Nette\InvalidArgumentException("Missing item '$k'."); } return $default; } } return $array; } /** * Returns reference to array item. If the index does not exist, new one is created with value null. * @template T * @param array $array * @param array-key|array-key[] $key * @return ?T * @throws Nette\InvalidArgumentException if traversed item is not an array */ public static function &getRef(array &$array, $key) { foreach (is_array($key) ? $key : [$key] as $k) { if (is_array($array) || $array === null) { $array = &$array[$k]; } else { throw new Nette\InvalidArgumentException('Traversed item is not an array.'); } } return $array; } /** * Recursively merges two fields. It is useful, for example, for merging tree structures. It behaves as * the + operator for array, ie. it adds a key/value pair from the second array to the first one and retains * the value from the first array in the case of a key collision. * @template T1 * @template T2 * @param array $array1 * @param array $array2 * @return array */ public static function mergeTree(array $array1, array $array2): array { $res = $array1 + $array2; foreach (array_intersect_key($array1, $array2) as $k => $v) { if (is_array($v) && is_array($array2[$k])) { $res[$k] = self::mergeTree($v, $array2[$k]); } } return $res; } /** * Returns zero-indexed position of given array key. Returns null if key is not found. * @param array-key $key * @return int|null offset if it is found, null otherwise */ public static function getKeyOffset(array $array, $key): ?int { return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), true)); } /** * @deprecated use getKeyOffset() */ public static function searchKey(array $array, $key): ?int { return self::getKeyOffset($array, $key); } /** * Tests an array for the presence of value. * @param mixed $value */ public static function contains(array $array, $value): bool { return in_array($value, $array, true); } /** * Returns the first item from the array or null if array is empty. * @template T * @param array $array * @return ?T */ public static function first(array $array) { return count($array) ? reset($array) : null; } /** * Returns the last item from the array or null if array is empty. * @template T * @param array $array * @return ?T */ public static function last(array $array) { return count($array) ? end($array) : null; } /** * Inserts the contents of the $inserted array into the $array immediately after the $key. * If $key is null (or does not exist), it is inserted at the beginning. * @param array-key|null $key */ public static function insertBefore(array &$array, $key, array $inserted): void { $offset = $key === null ? 0 : (int) self::getKeyOffset($array, $key); $array = array_slice($array, 0, $offset, true) + $inserted + array_slice($array, $offset, count($array), true); } /** * Inserts the contents of the $inserted array into the $array before the $key. * If $key is null (or does not exist), it is inserted at the end. * @param array-key|null $key */ public static function insertAfter(array &$array, $key, array $inserted): void { if ($key === null || ($offset = self::getKeyOffset($array, $key)) === null) { $offset = count($array) - 1; } $array = array_slice($array, 0, $offset + 1, true) + $inserted + array_slice($array, $offset + 1, count($array), true); } /** * Renames key in array. * @param array-key $oldKey * @param array-key $newKey */ public static function renameKey(array &$array, $oldKey, $newKey): bool { $offset = self::getKeyOffset($array, $oldKey); if ($offset === null) { return false; } $val = &$array[$oldKey]; $keys = array_keys($array); $keys[$offset] = $newKey; $array = array_combine($keys, $array); $array[$newKey] = &$val; return true; } /** * Returns only those array items, which matches a regular expression $pattern. * @param string[] $array * @return string[] */ public static function grep( array $array, #[Language('RegExp')] string $pattern, int $flags = 0 ): array { return Strings::pcre('preg_grep', [$pattern, $array, $flags]); } /** * Transforms multidimensional array to flat array. */ public static function flatten(array $array, bool $preserveKeys = false): array { $res = []; $cb = $preserveKeys ? function ($v, $k) use (&$res): void { $res[$k] = $v; } : function ($v) use (&$res): void { $res[] = $v; }; array_walk_recursive($array, $cb); return $res; } /** * Checks if the array is indexed in ascending order of numeric keys from zero, a.k.a list. * @param mixed $value */ public static function isList($value): bool { return is_array($value) && (PHP_VERSION_ID < 80100 ? !$value || array_keys($value) === range(0, count($value) - 1) : array_is_list($value) ); } /** * Reformats table to associative tree. Path looks like 'field|field[]field->field=field'. * @param string|string[] $path * @return array|\stdClass */ public static function associate(array $array, $path) { $parts = is_array($path) ? $path : preg_split('#(\[\]|->|=|\|)#', $path, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); if (!$parts || $parts === ['->'] || $parts[0] === '=' || $parts[0] === '|') { throw new Nette\InvalidArgumentException("Invalid path '$path'."); } $res = $parts[0] === '->' ? new \stdClass : []; foreach ($array as $rowOrig) { $row = (array) $rowOrig; $x = &$res; for ($i = 0; $i < count($parts); $i++) { $part = $parts[$i]; if ($part === '[]') { $x = &$x[]; } elseif ($part === '=') { if (isset($parts[++$i])) { $x = $row[$parts[$i]]; $row = null; } } elseif ($part === '->') { if (isset($parts[++$i])) { if ($x === null) { $x = new \stdClass; } $x = &$x->{$row[$parts[$i]]}; } else { $row = is_object($rowOrig) ? $rowOrig : (object) $row; } } elseif ($part !== '|') { $x = &$x[(string) $row[$part]]; } } if ($x === null) { $x = $row; } } return $res; } /** * Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling. * @param mixed $filling */ public static function normalize(array $array, $filling = null): array { $res = []; foreach ($array as $k => $v) { $res[is_int($k) ? $v : $k] = is_int($k) ? $filling : $v; } return $res; } /** * Returns and removes the value of an item from an array. If it does not exist, it throws an exception, * or returns $default, if provided. * @template T * @param array $array * @param array-key $key * @param ?T $default * @return ?T * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided */ public static function pick(array &$array, $key, $default = null) { if (array_key_exists($key, $array)) { $value = $array[$key]; unset($array[$key]); return $value; } elseif (func_num_args() < 3) { throw new Nette\InvalidArgumentException("Missing item '$key'."); } else { return $default; } } /** * Tests whether at least one element in the array passes the test implemented by the * provided callback with signature `function ($value, $key, array $array): bool`. */ public static function some(iterable $array, callable $callback): bool { foreach ($array as $k => $v) { if ($callback($v, $k, $array)) { return true; } } return false; } /** * Tests whether all elements in the array pass the test implemented by the provided function, * which has the signature `function ($value, $key, array $array): bool`. */ public static function every(iterable $array, callable $callback): bool { foreach ($array as $k => $v) { if (!$callback($v, $k, $array)) { return false; } } return true; } /** * Calls $callback on all elements in the array and returns the array of return values. * The callback has the signature `function ($value, $key, array $array): bool`. */ public static function map(iterable $array, callable $callback): array { $res = []; foreach ($array as $k => $v) { $res[$k] = $callback($v, $k, $array); } return $res; } /** * Invokes all callbacks and returns array of results. * @param callable[] $callbacks */ public static function invoke(iterable $callbacks, ...$args): array { $res = []; foreach ($callbacks as $k => $cb) { $res[$k] = $cb(...$args); } return $res; } /** * Invokes method on every object in an array and returns array of results. * @param object[] $objects */ public static function invokeMethod(iterable $objects, string $method, ...$args): array { $res = []; foreach ($objects as $k => $obj) { $res[$k] = $obj->$method(...$args); } return $res; } /** * Copies the elements of the $array array to the $object object and then returns it. * @template T of object * @param T $object * @return T */ public static function toObject(iterable $array, $object) { foreach ($array as $k => $v) { $object->$k = $v; } return $object; } /** * Converts value to array key. * @param mixed $value * @return array-key */ public static function toKey($value) { return key([$value => null]); } /** * Returns copy of the $array where every item is converted to string * and prefixed by $prefix and suffixed by $suffix. * @param string[] $array * @return string[] */ public static function wrap(array $array, string $prefix = '', string $suffix = ''): array { $res = []; foreach ($array as $k => $v) { $res[$k] = $prefix . $v . $suffix; } return $res; } } PKk[eW Callback.phpnuW+AgetMessage()); } } /** * Invokes callback. * @return mixed * @deprecated */ public static function invoke($callable, ...$args) { trigger_error(__METHOD__ . '() is deprecated, use native invoking.', E_USER_DEPRECATED); self::check($callable); return $callable(...$args); } /** * Invokes callback with an array of parameters. * @return mixed * @deprecated */ public static function invokeArgs($callable, array $args = []) { trigger_error(__METHOD__ . '() is deprecated, use native invoking.', E_USER_DEPRECATED); self::check($callable); return $callable(...$args); } /** * Invokes internal PHP function with own error handler. * @return mixed */ public static function invokeSafe(string $function, array $args, callable $onError) { $prev = set_error_handler(function ($severity, $message, $file) use ($onError, &$prev, $function): ?bool { if ($file === __FILE__) { $msg = ini_get('html_errors') ? Html::htmlToText($message) : $message; $msg = preg_replace("#^$function\\(.*?\\): #", '', $msg); if ($onError($msg, $severity) !== false) { return null; } } return $prev ? $prev(...func_get_args()) : false; }); try { return $function(...$args); } finally { restore_error_handler(); } } /** * Checks that $callable is valid PHP callback. Otherwise throws exception. If the $syntax is set to true, only verifies * that $callable has a valid structure to be used as a callback, but does not verify if the class or method actually exists. * @param mixed $callable * @return callable * @throws Nette\InvalidArgumentException */ public static function check($callable, bool $syntax = false) { if (!is_callable($callable, $syntax)) { throw new Nette\InvalidArgumentException( $syntax ? 'Given value is not a callable type.' : sprintf("Callback '%s' is not callable.", self::toString($callable)) ); } return $callable; } /** * Converts PHP callback to textual form. Class or method may not exists. * @param mixed $callable */ public static function toString($callable): string { if ($callable instanceof \Closure) { $inner = self::unwrap($callable); return '{closure' . ($inner instanceof \Closure ? '}' : ' ' . self::toString($inner) . '}'); } elseif (is_string($callable) && $callable[0] === "\0") { return '{lambda}'; } else { is_callable(is_object($callable) ? [$callable, '__invoke'] : $callable, true, $textual); return $textual; } } /** * Returns reflection for method or function used in PHP callback. * @param callable $callable type check is escalated to ReflectionException * @return \ReflectionMethod|\ReflectionFunction * @throws \ReflectionException if callback is not valid */ public static function toReflection($callable): \ReflectionFunctionAbstract { if ($callable instanceof \Closure) { $callable = self::unwrap($callable); } if (is_string($callable) && strpos($callable, '::')) { return new \ReflectionMethod($callable); } elseif (is_array($callable)) { return new \ReflectionMethod($callable[0], $callable[1]); } elseif (is_object($callable) && !$callable instanceof \Closure) { return new \ReflectionMethod($callable, '__invoke'); } else { return new \ReflectionFunction($callable); } } /** * Checks whether PHP callback is function or static method. */ public static function isStatic(callable $callable): bool { return is_string(is_array($callable) ? $callable[0] : $callable); } /** * Unwraps closure created by Closure::fromCallable(). * @return callable|array */ public static function unwrap(\Closure $closure) { $r = new \ReflectionFunction($closure); if (substr($r->name, -1) === '}') { return $closure; } elseif ($obj = $r->getClosureThis()) { return [$obj, $r->name]; } elseif ($class = $r->getClosureScopeClass()) { return [$class->name, $r->name]; } else { return $r->name; } } } PKk[nn'' ArrayHash.phpnuW+A $array * @return static */ public static function from(array $array, bool $recursive = true) { $obj = new static; foreach ($array as $key => $value) { $obj->$key = $recursive && is_array($value) ? static::from($value, true) : $value; } return $obj; } /** * Returns an iterator over all items. * @return \RecursiveArrayIterator */ public function getIterator(): \RecursiveArrayIterator { return new \RecursiveArrayIterator((array) $this); } /** * Returns items count. */ public function count(): int { return count((array) $this); } /** * Replaces or appends a item. * @param string|int $key * @param T $value */ public function offsetSet($key, $value): void { if (!is_scalar($key)) { // prevents null throw new Nette\InvalidArgumentException(sprintf('Key must be either a string or an integer, %s given.', gettype($key))); } $this->$key = $value; } /** * Returns a item. * @param string|int $key * @return T */ #[\ReturnTypeWillChange] public function offsetGet($key) { return $this->$key; } /** * Determines whether a item exists. * @param string|int $key */ public function offsetExists($key): bool { return isset($this->$key); } /** * Removes the element from this list. * @param string|int $key */ public function offsetUnset($key): void { unset($this->$key); } } PKk[8ۡObjectMixin.phpnuW+A= 0xD800 && $code <= 0xDFFF) || $code > 0x10FFFF) { throw new Nette\InvalidArgumentException('Code point must be in range 0x0 to 0xD7FF or 0xE000 to 0x10FFFF.'); } elseif (!extension_loaded('iconv')) { throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.'); } return iconv('UTF-32BE', 'UTF-8//IGNORE', pack('N', $code)); } /** * Starts the $haystack string with the prefix $needle? */ public static function startsWith(string $haystack, string $needle): bool { return strncmp($haystack, $needle, strlen($needle)) === 0; } /** * Ends the $haystack string with the suffix $needle? */ public static function endsWith(string $haystack, string $needle): bool { return $needle === '' || substr($haystack, -strlen($needle)) === $needle; } /** * Does $haystack contain $needle? */ public static function contains(string $haystack, string $needle): bool { return strpos($haystack, $needle) !== false; } /** * Returns a part of UTF-8 string specified by starting position and length. If start is negative, * the returned string will start at the start'th character from the end of string. */ public static function substring(string $s, int $start, ?int $length = null): string { if (function_exists('mb_substr')) { return mb_substr($s, $start, $length, 'UTF-8'); // MB is much faster } elseif (!extension_loaded('iconv')) { throw new Nette\NotSupportedException(__METHOD__ . '() requires extension ICONV or MBSTRING, neither is loaded.'); } elseif ($length === null) { $length = self::length($s); } elseif ($start < 0 && $length < 0) { $start += self::length($s); // unifies iconv_substr behavior with mb_substr } return iconv_substr($s, $start, $length, 'UTF-8'); } /** * Removes control characters, normalizes line breaks to `\n`, removes leading and trailing blank lines, * trims end spaces on lines, normalizes UTF-8 to the normal form of NFC. */ public static function normalize(string $s): string { // convert to compressed normal form (NFC) if (class_exists('Normalizer', false) && ($n = \Normalizer::normalize($s, \Normalizer::FORM_C)) !== false) { $s = $n; } $s = self::normalizeNewLines($s); // remove control characters; leave \t + \n $s = self::pcre('preg_replace', ['#[\x00-\x08\x0B-\x1F\x7F-\x9F]+#u', '', $s]); // right trim $s = self::pcre('preg_replace', ['#[\t ]+$#m', '', $s]); // leading and trailing blank lines $s = trim($s, "\n"); return $s; } /** * Standardize line endings to unix-like. */ public static function normalizeNewLines(string $s): string { return str_replace(["\r\n", "\r"], "\n", $s); } /** * Converts UTF-8 string to ASCII, ie removes diacritics etc. */ public static function toAscii(string $s): string { $iconv = defined('ICONV_IMPL') ? trim(ICONV_IMPL, '"\'') : null; static $transliterator = null; if ($transliterator === null) { if (class_exists('Transliterator', false)) { $transliterator = \Transliterator::create('Any-Latin; Latin-ASCII'); } else { trigger_error(__METHOD__ . "(): it is recommended to enable PHP extensions 'intl'.", E_USER_NOTICE); $transliterator = false; } } // remove control characters and check UTF-8 validity $s = self::pcre('preg_replace', ['#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{2FF}\x{370}-\x{10FFFF}]#u', '', $s]); // transliteration (by Transliterator and iconv) is not optimal, replace some characters directly $s = strtr($s, ["\u{201E}" => '"', "\u{201C}" => '"', "\u{201D}" => '"', "\u{201A}" => "'", "\u{2018}" => "'", "\u{2019}" => "'", "\u{B0}" => '^', "\u{42F}" => 'Ya', "\u{44F}" => 'ya', "\u{42E}" => 'Yu', "\u{44E}" => 'yu', "\u{c4}" => 'Ae', "\u{d6}" => 'Oe', "\u{dc}" => 'Ue', "\u{1e9e}" => 'Ss', "\u{e4}" => 'ae', "\u{f6}" => 'oe', "\u{fc}" => 'ue', "\u{df}" => 'ss']); // „ “ ” ‚ ‘ ’ ° Я я Ю ю Ä Ö Ü ẞ ä ö ü ß if ($iconv !== 'libiconv') { $s = strtr($s, ["\u{AE}" => '(R)', "\u{A9}" => '(c)', "\u{2026}" => '...', "\u{AB}" => '<<', "\u{BB}" => '>>', "\u{A3}" => 'lb', "\u{A5}" => 'yen', "\u{B2}" => '^2', "\u{B3}" => '^3', "\u{B5}" => 'u', "\u{B9}" => '^1', "\u{BA}" => 'o', "\u{BF}" => '?', "\u{2CA}" => "'", "\u{2CD}" => '_', "\u{2DD}" => '"', "\u{1FEF}" => '', "\u{20AC}" => 'EUR', "\u{2122}" => 'TM', "\u{212E}" => 'e', "\u{2190}" => '<-', "\u{2191}" => '^', "\u{2192}" => '->', "\u{2193}" => 'V', "\u{2194}" => '<->']); // ® © … « » £ ¥ ² ³ µ ¹ º ¿ ˊ ˍ ˝ ` € ™ ℮ ← ↑ → ↓ ↔ } if ($transliterator) { $s = $transliterator->transliterate($s); // use iconv because The transliterator leaves some characters out of ASCII, eg → ʾ if ($iconv === 'glibc') { $s = strtr($s, '?', "\x01"); // temporarily hide ? to distinguish them from the garbage that iconv creates $s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s); $s = str_replace(['?', "\x01"], ['', '?'], $s); // remove garbage and restore ? characters } elseif ($iconv === 'libiconv') { $s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s); } else { // null or 'unknown' (#216) $s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars } } elseif ($iconv === 'glibc' || $iconv === 'libiconv') { // temporarily hide these characters to distinguish them from the garbage that iconv creates $s = strtr($s, '`\'"^~?', "\x01\x02\x03\x04\x05\x06"); if ($iconv === 'glibc') { // glibc implementation is very limited. transliterate into Windows-1250 and then into ASCII, so most Eastern European characters are preserved $s = iconv('UTF-8', 'WINDOWS-1250//TRANSLIT//IGNORE', $s); $s = strtr( $s, "\xa5\xa3\xbc\x8c\xa7\x8a\xaa\x8d\x8f\x8e\xaf\xb9\xb3\xbe\x9c\x9a\xba\x9d\x9f\x9e\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf8\xf9\xfa\xfb\xfc\xfd\xfe\x96\xa0\x8b\x97\x9b\xa6\xad\xb7", 'ALLSSSSTZZZallssstzzzRAAAALCCCEEEEIIDDNNOOOOxRUUUUYTsraaaalccceeeeiiddnnooooruuuuyt- <->|-.' ); $s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); } else { $s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s); } // remove garbage that iconv creates during transliteration (eg Ý -> Y') $s = str_replace(['`', "'", '"', '^', '~', '?'], '', $s); // restore temporarily hidden characters $s = strtr($s, "\x01\x02\x03\x04\x05\x06", '`\'"^~?'); } else { $s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars } return $s; } /** * Modifies the UTF-8 string to the form used in the URL, ie removes diacritics and replaces all characters * except letters of the English alphabet and numbers with a hyphens. */ public static function webalize(string $s, ?string $charlist = null, bool $lower = true): string { $s = self::toAscii($s); if ($lower) { $s = strtolower($s); } $s = self::pcre('preg_replace', ['#[^a-z0-9' . ($charlist !== null ? preg_quote($charlist, '#') : '') . ']+#i', '-', $s]); $s = trim($s, '-'); return $s; } /** * Truncates a UTF-8 string to given maximal length, while trying not to split whole words. Only if the string is truncated, * an ellipsis (or something else set with third argument) is appended to the string. */ public static function truncate(string $s, int $maxLen, string $append = "\u{2026}"): string { if (self::length($s) > $maxLen) { $maxLen -= self::length($append); if ($maxLen < 1) { return $append; } elseif ($matches = self::match($s, '#^.{1,' . $maxLen . '}(?=[\s\x00-/:-@\[-`{-~])#us')) { return $matches[0] . $append; } else { return self::substring($s, 0, $maxLen) . $append; } } return $s; } /** * Indents a multiline text from the left. Second argument sets how many indentation chars should be used, * while the indent itself is the third argument (*tab* by default). */ public static function indent(string $s, int $level = 1, string $chars = "\t"): string { if ($level > 0) { $s = self::replace($s, '#(?:^|[\r\n]+)(?=[^\r\n])#', '$0' . str_repeat($chars, $level)); } return $s; } /** * Converts all characters of UTF-8 string to lower case. */ public static function lower(string $s): string { return mb_strtolower($s, 'UTF-8'); } /** * Converts the first character of a UTF-8 string to lower case and leaves the other characters unchanged. */ public static function firstLower(string $s): string { return self::lower(self::substring($s, 0, 1)) . self::substring($s, 1); } /** * Converts all characters of a UTF-8 string to upper case. */ public static function upper(string $s): string { return mb_strtoupper($s, 'UTF-8'); } /** * Converts the first character of a UTF-8 string to upper case and leaves the other characters unchanged. */ public static function firstUpper(string $s): string { return self::upper(self::substring($s, 0, 1)) . self::substring($s, 1); } /** * Converts the first character of every word of a UTF-8 string to upper case and the others to lower case. */ public static function capitalize(string $s): string { return mb_convert_case($s, MB_CASE_TITLE, 'UTF-8'); } /** * Compares two UTF-8 strings or their parts, without taking character case into account. If length is null, whole strings are compared, * if it is negative, the corresponding number of characters from the end of the strings is compared, * otherwise the appropriate number of characters from the beginning is compared. */ public static function compare(string $left, string $right, ?int $length = null): bool { if (class_exists('Normalizer', false)) { $left = \Normalizer::normalize($left, \Normalizer::FORM_D); // form NFD is faster $right = \Normalizer::normalize($right, \Normalizer::FORM_D); // form NFD is faster } if ($length < 0) { $left = self::substring($left, $length, -$length); $right = self::substring($right, $length, -$length); } elseif ($length !== null) { $left = self::substring($left, 0, $length); $right = self::substring($right, 0, $length); } return self::lower($left) === self::lower($right); } /** * Finds the common prefix of strings or returns empty string if the prefix was not found. * @param string[] $strings */ public static function findPrefix(array $strings): string { $first = array_shift($strings); for ($i = 0; $i < strlen($first); $i++) { foreach ($strings as $s) { if (!isset($s[$i]) || $first[$i] !== $s[$i]) { while ($i && $first[$i - 1] >= "\x80" && $first[$i] >= "\x80" && $first[$i] < "\xC0") { $i--; } return substr($first, 0, $i); } } } return $first; } /** * Returns number of characters (not bytes) in UTF-8 string. * That is the number of Unicode code points which may differ from the number of graphemes. */ public static function length(string $s): int { return function_exists('mb_strlen') ? mb_strlen($s, 'UTF-8') : strlen(utf8_decode($s)); } /** * Removes all left and right side spaces (or the characters passed as second argument) from a UTF-8 encoded string. */ public static function trim(string $s, string $charlist = self::TRIM_CHARACTERS): string { $charlist = preg_quote($charlist, '#'); return self::replace($s, '#^[' . $charlist . ']+|[' . $charlist . ']+$#Du', ''); } /** * Pads a UTF-8 string to given length by prepending the $pad string to the beginning. */ public static function padLeft(string $s, int $length, string $pad = ' '): string { $length = max(0, $length - self::length($s)); $padLen = self::length($pad); return str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen) . $s; } /** * Pads UTF-8 string to given length by appending the $pad string to the end. */ public static function padRight(string $s, int $length, string $pad = ' '): string { $length = max(0, $length - self::length($s)); $padLen = self::length($pad); return $s . str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen); } /** * Reverses UTF-8 string. */ public static function reverse(string $s): string { if (!extension_loaded('iconv')) { throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.'); } return iconv('UTF-32LE', 'UTF-8', strrev(iconv('UTF-8', 'UTF-32BE', $s))); } /** * Returns part of $haystack before $nth occurence of $needle or returns null if the needle was not found. * Negative value means searching from the end. */ public static function before(string $haystack, string $needle, int $nth = 1): ?string { $pos = self::pos($haystack, $needle, $nth); return $pos === null ? null : substr($haystack, 0, $pos); } /** * Returns part of $haystack after $nth occurence of $needle or returns null if the needle was not found. * Negative value means searching from the end. */ public static function after(string $haystack, string $needle, int $nth = 1): ?string { $pos = self::pos($haystack, $needle, $nth); return $pos === null ? null : substr($haystack, $pos + strlen($needle)); } /** * Returns position in characters of $nth occurence of $needle in $haystack or null if the $needle was not found. * Negative value of `$nth` means searching from the end. */ public static function indexOf(string $haystack, string $needle, int $nth = 1): ?int { $pos = self::pos($haystack, $needle, $nth); return $pos === null ? null : self::length(substr($haystack, 0, $pos)); } /** * Returns position in characters of $nth occurence of $needle in $haystack or null if the needle was not found. */ private static function pos(string $haystack, string $needle, int $nth = 1): ?int { if (!$nth) { return null; } elseif ($nth > 0) { if ($needle === '') { return 0; } $pos = 0; while (($pos = strpos($haystack, $needle, $pos)) !== false && --$nth) { $pos++; } } else { $len = strlen($haystack); if ($needle === '') { return $len; } elseif ($len === 0) { return null; } $pos = $len - 1; while (($pos = strrpos($haystack, $needle, $pos - $len)) !== false && ++$nth) { $pos--; } } return Helpers::falseToNull($pos); } /** * Splits a string into array by the regular expression. Parenthesized expression in the delimiter are captured. * Parameter $flags can be any combination of PREG_SPLIT_NO_EMPTY and PREG_OFFSET_CAPTURE flags. */ public static function split( string $subject, #[Language('RegExp')] string $pattern, int $flags = 0 ): array { return self::pcre('preg_split', [$pattern, $subject, -1, $flags | PREG_SPLIT_DELIM_CAPTURE]); } /** * Checks if given string matches a regular expression pattern and returns an array with first found match and each subpattern. * Parameter $flags can be any combination of PREG_OFFSET_CAPTURE and PREG_UNMATCHED_AS_NULL flags. */ public static function match( string $subject, #[Language('RegExp')] string $pattern, int $flags = 0, int $offset = 0 ): ?array { if ($offset > strlen($subject)) { return null; } return self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset]) ? $m : null; } /** * Finds all occurrences matching regular expression pattern and returns a two-dimensional array. Result is array of matches (ie uses by default PREG_SET_ORDER). * Parameter $flags can be any combination of PREG_OFFSET_CAPTURE, PREG_UNMATCHED_AS_NULL and PREG_PATTERN_ORDER flags. */ public static function matchAll( string $subject, #[Language('RegExp')] string $pattern, int $flags = 0, int $offset = 0 ): array { if ($offset > strlen($subject)) { return []; } self::pcre('preg_match_all', [ $pattern, $subject, &$m, ($flags & PREG_PATTERN_ORDER) ? $flags : ($flags | PREG_SET_ORDER), $offset, ]); return $m; } /** * Replaces all occurrences matching regular expression $pattern which can be string or array in the form `pattern => replacement`. * @param string|array $pattern * @param string|callable $replacement */ public static function replace( string $subject, #[Language('RegExp')] $pattern, $replacement = '', int $limit = -1 ): string { if (is_object($replacement) || is_array($replacement)) { if (!is_callable($replacement, false, $textual)) { throw new Nette\InvalidStateException("Callback '$textual' is not callable."); } return self::pcre('preg_replace_callback', [$pattern, $replacement, $subject, $limit]); } elseif (is_array($pattern) && is_string(key($pattern))) { $replacement = array_values($pattern); $pattern = array_keys($pattern); } return self::pcre('preg_replace', [$pattern, $replacement, $subject, $limit]); } /** @internal */ public static function pcre(string $func, array $args) { $res = Callback::invokeSafe($func, $args, function (string $message) use ($args): void { // compile-time error, not detectable by preg_last_error throw new RegexpException($message . ' in pattern: ' . implode(' or ', (array) $args[0])); }); if (($code = preg_last_error()) // run-time error, but preg_last_error & return code are liars && ($res === null || !in_array($func, ['preg_filter', 'preg_replace_callback', 'preg_replace'], true)) ) { throw new RegexpException((RegexpException::MESSAGES[$code] ?? 'Unknown error') . ' (pattern: ' . implode(' or ', (array) $args[0]) . ')', $code); } return $res; } } PKk[-9t Paginator.phpnuW+Apage = $page; return $this; } /** * Returns current page number. */ public function getPage(): int { return $this->base + $this->getPageIndex(); } /** * Returns first page number. */ public function getFirstPage(): int { return $this->base; } /** * Returns last page number. */ public function getLastPage(): ?int { return $this->itemCount === null ? null : $this->base + max(0, $this->getPageCount() - 1); } /** * Returns the sequence number of the first element on the page */ public function getFirstItemOnPage(): int { return $this->itemCount !== 0 ? $this->offset + 1 : 0; } /** * Returns the sequence number of the last element on the page */ public function getLastItemOnPage(): int { return $this->offset + $this->length; } /** * Sets first page (base) number. * @return static */ public function setBase(int $base) { $this->base = $base; return $this; } /** * Returns first page (base) number. */ public function getBase(): int { return $this->base; } /** * Returns zero-based page number. */ protected function getPageIndex(): int { $index = max(0, $this->page - $this->base); return $this->itemCount === null ? $index : min($index, max(0, $this->getPageCount() - 1)); } /** * Is the current page the first one? */ public function isFirst(): bool { return $this->getPageIndex() === 0; } /** * Is the current page the last one? */ public function isLast(): bool { return $this->itemCount === null ? false : $this->getPageIndex() >= $this->getPageCount() - 1; } /** * Returns the total number of pages. */ public function getPageCount(): ?int { return $this->itemCount === null ? null : (int) ceil($this->itemCount / $this->itemsPerPage); } /** * Sets the number of items to display on a single page. * @return static */ public function setItemsPerPage(int $itemsPerPage) { $this->itemsPerPage = max(1, $itemsPerPage); return $this; } /** * Returns the number of items to display on a single page. */ public function getItemsPerPage(): int { return $this->itemsPerPage; } /** * Sets the total number of items. * @return static */ public function setItemCount(?int $itemCount = null) { $this->itemCount = $itemCount === null ? null : max(0, $itemCount); return $this; } /** * Returns the total number of items. */ public function getItemCount(): ?int { return $this->itemCount; } /** * Returns the absolute index of the first item on current page. */ public function getOffset(): int { return $this->getPageIndex() * $this->itemsPerPage; } /** * Returns the absolute index of the first item on current page in countdown paging. */ public function getCountdownOffset(): ?int { return $this->itemCount === null ? null : max(0, $this->itemCount - ($this->getPageIndex() + 1) * $this->itemsPerPage); } /** * Returns the number of items on current page. */ public function getLength(): int { return $this->itemCount === null ? $this->itemsPerPage : min($this->itemsPerPage, $this->itemCount - $this->getPageIndex() * $this->itemsPerPage); } } PKk[XіSVV Image.phpnuW+A * $image = Image::fromFile('nette.jpg'); * $image->resize(150, 100); * $image->sharpen(); * $image->send(); * * * @method Image affine(array $affine, array $clip = null) * @method array affineMatrixConcat(array $m1, array $m2) * @method array affineMatrixGet(int $type, mixed $options = null) * @method void alphaBlending(bool $on) * @method void antialias(bool $on) * @method void arc($x, $y, $w, $h, $start, $end, $color) * @method void char(int $font, $x, $y, string $char, $color) * @method void charUp(int $font, $x, $y, string $char, $color) * @method int colorAllocate($red, $green, $blue) * @method int colorAllocateAlpha($red, $green, $blue, $alpha) * @method int colorAt($x, $y) * @method int colorClosest($red, $green, $blue) * @method int colorClosestAlpha($red, $green, $blue, $alpha) * @method int colorClosestHWB($red, $green, $blue) * @method void colorDeallocate($color) * @method int colorExact($red, $green, $blue) * @method int colorExactAlpha($red, $green, $blue, $alpha) * @method void colorMatch(Image $image2) * @method int colorResolve($red, $green, $blue) * @method int colorResolveAlpha($red, $green, $blue, $alpha) * @method void colorSet($index, $red, $green, $blue) * @method array colorsForIndex($index) * @method int colorsTotal() * @method int colorTransparent($color = null) * @method void convolution(array $matrix, float $div, float $offset) * @method void copy(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH) * @method void copyMerge(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity) * @method void copyMergeGray(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity) * @method void copyResampled(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH) * @method void copyResized(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH) * @method Image cropAuto(int $mode = -1, float $threshold = .5, int $color = -1) * @method void ellipse($cx, $cy, $w, $h, $color) * @method void fill($x, $y, $color) * @method void filledArc($cx, $cy, $w, $h, $s, $e, $color, $style) * @method void filledEllipse($cx, $cy, $w, $h, $color) * @method void filledPolygon(array $points, $numPoints, $color) * @method void filledRectangle($x1, $y1, $x2, $y2, $color) * @method void fillToBorder($x, $y, $border, $color) * @method void filter($filtertype) * @method void flip(int $mode) * @method array ftText($size, $angle, $x, $y, $col, string $fontFile, string $text, array $extrainfo = null) * @method void gammaCorrect(float $inputgamma, float $outputgamma) * @method array getClip() * @method int interlace($interlace = null) * @method bool isTrueColor() * @method void layerEffect($effect) * @method void line($x1, $y1, $x2, $y2, $color) * @method void openPolygon(array $points, int $num_points, int $color) * @method void paletteCopy(Image $source) * @method void paletteToTrueColor() * @method void polygon(array $points, $numPoints, $color) * @method array psText(string $text, $font, $size, $color, $backgroundColor, $x, $y, $space = null, $tightness = null, float $angle = null, $antialiasSteps = null) * @method void rectangle($x1, $y1, $x2, $y2, $col) * @method mixed resolution(int $res_x = null, int $res_y = null) * @method Image rotate(float $angle, $backgroundColor) * @method void saveAlpha(bool $saveflag) * @method Image scale(int $newWidth, int $newHeight = -1, int $mode = IMG_BILINEAR_FIXED) * @method void setBrush(Image $brush) * @method void setClip(int $x1, int $y1, int $x2, int $y2) * @method void setInterpolation(int $method = IMG_BILINEAR_FIXED) * @method void setPixel($x, $y, $color) * @method void setStyle(array $style) * @method void setThickness($thickness) * @method void setTile(Image $tile) * @method void string($font, $x, $y, string $s, $col) * @method void stringUp($font, $x, $y, string $s, $col) * @method void trueColorToPalette(bool $dither, $ncolors) * @method array ttfText($size, $angle, $x, $y, $color, string $fontfile, string $text) * @property-read int $width * @property-read int $height * @property-read resource|\GdImage $imageResource */ class Image { use Nette\SmartObject; /** {@link resize()} only shrinks images */ public const SHRINK_ONLY = 0b0001; /** {@link resize()} will ignore aspect ratio */ public const STRETCH = 0b0010; /** {@link resize()} fits in given area so its dimensions are less than or equal to the required dimensions */ public const FIT = 0b0000; /** {@link resize()} fills given area so its dimensions are greater than or equal to the required dimensions */ public const FILL = 0b0100; /** {@link resize()} fills given area exactly */ public const EXACT = 0b1000; /** image types */ public const JPEG = IMAGETYPE_JPEG, PNG = IMAGETYPE_PNG, GIF = IMAGETYPE_GIF, WEBP = IMAGETYPE_WEBP, AVIF = 19, // IMAGETYPE_AVIF, BMP = IMAGETYPE_BMP; public const EMPTY_GIF = "GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;"; private const Formats = [self::JPEG => 'jpeg', self::PNG => 'png', self::GIF => 'gif', self::WEBP => 'webp', self::AVIF => 'avif', self::BMP => 'bmp']; /** @var resource|\GdImage */ private $image; /** * Returns RGB color (0..255) and transparency (0..127). */ public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array { return [ 'red' => max(0, min(255, $red)), 'green' => max(0, min(255, $green)), 'blue' => max(0, min(255, $blue)), 'alpha' => max(0, min(127, $transparency)), ]; } /** * Reads an image from a file and returns its type in $type. * @throws Nette\NotSupportedException if gd extension is not loaded * @throws UnknownImageFileException if file not found or file type is not known * @return static */ public static function fromFile(string $file, ?int &$type = null) { if (!extension_loaded('gd')) { throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); } $type = self::detectTypeFromFile($file); if (!$type) { throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '$file'." : "File '$file' not found."); } return self::invokeSafe('imagecreatefrom' . self::Formats[$type], $file, "Unable to open file '$file'.", __METHOD__); } /** * Reads an image from a string and returns its type in $type. * @return static * @throws Nette\NotSupportedException if gd extension is not loaded * @throws ImageException */ public static function fromString(string $s, ?int &$type = null) { if (!extension_loaded('gd')) { throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); } $type = self::detectTypeFromString($s); if (!$type) { throw new UnknownImageFileException('Unknown type of image.'); } return self::invokeSafe('imagecreatefromstring', $s, 'Unable to open image from string.', __METHOD__); } private static function invokeSafe(string $func, string $arg, string $message, string $callee): self { $errors = []; $res = Callback::invokeSafe($func, [$arg], function (string $message) use (&$errors): void { $errors[] = $message; }); if (!$res) { throw new ImageException($message . ' Errors: ' . implode(', ', $errors)); } elseif ($errors) { trigger_error($callee . '(): ' . implode(', ', $errors), E_USER_WARNING); } return new static($res); } /** * Creates a new true color image of the given dimensions. The default color is black. * @return static * @throws Nette\NotSupportedException if gd extension is not loaded */ public static function fromBlank(int $width, int $height, ?array $color = null) { if (!extension_loaded('gd')) { throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); } if ($width < 1 || $height < 1) { throw new Nette\InvalidArgumentException('Image width and height must be greater than zero.'); } $image = imagecreatetruecolor($width, $height); if ($color) { $color += ['alpha' => 0]; $color = imagecolorresolvealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']); imagealphablending($image, false); imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color); imagealphablending($image, true); } return new static($image); } /** * Returns the type of image from file. */ public static function detectTypeFromFile(string $file, &$width = null, &$height = null): ?int { [$width, $height, $type] = @getimagesize($file); // @ - files smaller than 12 bytes causes read error return isset(self::Formats[$type]) ? $type : null; } /** * Returns the type of image from string. */ public static function detectTypeFromString(string $s, &$width = null, &$height = null): ?int { [$width, $height, $type] = @getimagesizefromstring($s); // @ - strings smaller than 12 bytes causes read error return isset(self::Formats[$type]) ? $type : null; } /** * Returns the file extension for the given `Image::XXX` constant. */ public static function typeToExtension(int $type): string { if (!isset(self::Formats[$type])) { throw new Nette\InvalidArgumentException("Unsupported image type '$type'."); } return self::Formats[$type]; } /** * Returns the `Image::XXX` constant for given file extension. */ public static function extensionToType(string $extension): int { $extensions = array_flip(self::Formats) + ['jpg' => self::JPEG]; $extension = strtolower($extension); if (!isset($extensions[$extension])) { throw new Nette\InvalidArgumentException("Unsupported file extension '$extension'."); } return $extensions[$extension]; } /** * Returns the mime type for the given `Image::XXX` constant. */ public static function typeToMimeType(int $type): string { return 'image/' . self::typeToExtension($type); } /** * Wraps GD image. * @param resource|\GdImage $image */ public function __construct($image) { $this->setImageResource($image); imagesavealpha($image, true); } /** * Returns image width. */ public function getWidth(): int { return imagesx($this->image); } /** * Returns image height. */ public function getHeight(): int { return imagesy($this->image); } /** * Sets image resource. * @param resource|\GdImage $image * @return static */ protected function setImageResource($image) { if (!$image instanceof \GdImage && !(is_resource($image) && get_resource_type($image) === 'gd')) { throw new Nette\InvalidArgumentException('Image is not valid.'); } $this->image = $image; return $this; } /** * Returns image GD resource. * @return resource|\GdImage */ public function getImageResource() { return $this->image; } /** * Scales an image. * @param int|string|null $width in pixels or percent * @param int|string|null $height in pixels or percent * @return static */ public function resize($width, $height, int $flags = self::FIT) { if ($flags & self::EXACT) { return $this->resize($width, $height, self::FILL)->crop('50%', '50%', $width, $height); } [$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $flags); if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize $newImage = static::fromBlank($newWidth, $newHeight, self::rgb(0, 0, 0, 127))->getImageResource(); imagecopyresampled( $newImage, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->getWidth(), $this->getHeight() ); $this->image = $newImage; } if ($width < 0 || $height < 0) { imageflip($this->image, $width < 0 ? ($height < 0 ? IMG_FLIP_BOTH : IMG_FLIP_HORIZONTAL) : IMG_FLIP_VERTICAL); } return $this; } /** * Calculates dimensions of resized image. * @param int|string|null $newWidth in pixels or percent * @param int|string|null $newHeight in pixels or percent */ public static function calculateSize( int $srcWidth, int $srcHeight, $newWidth, $newHeight, int $flags = self::FIT ): array { if ($newWidth === null) { } elseif (self::isPercent($newWidth)) { $newWidth = (int) round($srcWidth / 100 * abs($newWidth)); $percents = true; } else { $newWidth = abs($newWidth); } if ($newHeight === null) { } elseif (self::isPercent($newHeight)) { $newHeight = (int) round($srcHeight / 100 * abs($newHeight)); $flags |= empty($percents) ? 0 : self::STRETCH; } else { $newHeight = abs($newHeight); } if ($flags & self::STRETCH) { // non-proportional if (!$newWidth || !$newHeight) { throw new Nette\InvalidArgumentException('For stretching must be both width and height specified.'); } if ($flags & self::SHRINK_ONLY) { $newWidth = (int) round($srcWidth * min(1, $newWidth / $srcWidth)); $newHeight = (int) round($srcHeight * min(1, $newHeight / $srcHeight)); } } else { // proportional if (!$newWidth && !$newHeight) { throw new Nette\InvalidArgumentException('At least width or height must be specified.'); } $scale = []; if ($newWidth > 0) { // fit width $scale[] = $newWidth / $srcWidth; } if ($newHeight > 0) { // fit height $scale[] = $newHeight / $srcHeight; } if ($flags & self::FILL) { $scale = [max($scale)]; } if ($flags & self::SHRINK_ONLY) { $scale[] = 1; } $scale = min($scale); $newWidth = (int) round($srcWidth * $scale); $newHeight = (int) round($srcHeight * $scale); } return [max($newWidth, 1), max($newHeight, 1)]; } /** * Crops image. * @param int|string $left in pixels or percent * @param int|string $top in pixels or percent * @param int|string $width in pixels or percent * @param int|string $height in pixels or percent * @return static */ public function crop($left, $top, $width, $height) { [$r['x'], $r['y'], $r['width'], $r['height']] = static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height); if (gd_info()['GD Version'] === 'bundled (2.1.0 compatible)') { $this->image = imagecrop($this->image, $r); imagesavealpha($this->image, true); } else { $newImage = static::fromBlank($r['width'], $r['height'], self::RGB(0, 0, 0, 127))->getImageResource(); imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']); $this->image = $newImage; } return $this; } /** * Calculates dimensions of cutout in image. * @param int|string $left in pixels or percent * @param int|string $top in pixels or percent * @param int|string $newWidth in pixels or percent * @param int|string $newHeight in pixels or percent */ public static function calculateCutout(int $srcWidth, int $srcHeight, $left, $top, $newWidth, $newHeight): array { if (self::isPercent($newWidth)) { $newWidth = (int) round($srcWidth / 100 * $newWidth); } if (self::isPercent($newHeight)) { $newHeight = (int) round($srcHeight / 100 * $newHeight); } if (self::isPercent($left)) { $left = (int) round(($srcWidth - $newWidth) / 100 * $left); } if (self::isPercent($top)) { $top = (int) round(($srcHeight - $newHeight) / 100 * $top); } if ($left < 0) { $newWidth += $left; $left = 0; } if ($top < 0) { $newHeight += $top; $top = 0; } $newWidth = min($newWidth, $srcWidth - $left); $newHeight = min($newHeight, $srcHeight - $top); return [$left, $top, $newWidth, $newHeight]; } /** * Sharpens image a little bit. * @return static */ public function sharpen() { imageconvolution($this->image, [ // my magic numbers ;) [-1, -1, -1], [-1, 24, -1], [-1, -1, -1], ], 16, 0); return $this; } /** * Puts another image into this image. * @param int|string $left in pixels or percent * @param int|string $top in pixels or percent * @param int $opacity 0..100 * @return static */ public function place(self $image, $left = 0, $top = 0, int $opacity = 100) { $opacity = max(0, min(100, $opacity)); if ($opacity === 0) { return $this; } $width = $image->getWidth(); $height = $image->getHeight(); if (self::isPercent($left)) { $left = (int) round(($this->getWidth() - $width) / 100 * $left); } if (self::isPercent($top)) { $top = (int) round(($this->getHeight() - $height) / 100 * $top); } $output = $input = $image->image; if ($opacity < 100) { $tbl = []; for ($i = 0; $i < 128; $i++) { $tbl[$i] = round(127 - (127 - $i) * $opacity / 100); } $output = imagecreatetruecolor($width, $height); imagealphablending($output, false); if (!$image->isTrueColor()) { $input = $output; imagefilledrectangle($output, 0, 0, $width, $height, imagecolorallocatealpha($output, 0, 0, 0, 127)); imagecopy($output, $image->image, 0, 0, 0, 0, $width, $height); } for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $c = \imagecolorat($input, $x, $y); $c = ($c & 0xFFFFFF) + ($tbl[$c >> 24] << 24); \imagesetpixel($output, $x, $y, $c); } } imagealphablending($output, true); } imagecopy( $this->image, $output, $left, $top, 0, 0, $width, $height ); return $this; } /** * Saves image to the file. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). * @throws ImageException */ public function save(string $file, ?int $quality = null, ?int $type = null): void { $type = $type ?? self::extensionToType(pathinfo($file, PATHINFO_EXTENSION)); $this->output($type, $quality, $file); } /** * Outputs image to string. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). */ public function toString(int $type = self::JPEG, ?int $quality = null): string { return Helpers::capture(function () use ($type, $quality) { $this->output($type, $quality); }); } /** * Outputs image to string. */ public function __toString(): string { try { return $this->toString(); } catch (\Throwable $e) { if (func_num_args() || PHP_VERSION_ID >= 70400) { throw $e; } trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); return ''; } } /** * Outputs image to browser. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). * @throws ImageException */ public function send(int $type = self::JPEG, ?int $quality = null): void { header('Content-Type: ' . self::typeToMimeType($type)); $this->output($type, $quality); } /** * Outputs image to browser or file. * @throws ImageException */ private function output(int $type, ?int $quality, ?string $file = null): void { switch ($type) { case self::JPEG: $quality = $quality === null ? 85 : max(0, min(100, $quality)); $success = @imagejpeg($this->image, $file, $quality); // @ is escalated to exception break; case self::PNG: $quality = $quality === null ? 9 : max(0, min(9, $quality)); $success = @imagepng($this->image, $file, $quality); // @ is escalated to exception break; case self::GIF: $success = @imagegif($this->image, $file); // @ is escalated to exception break; case self::WEBP: $quality = $quality === null ? 80 : max(0, min(100, $quality)); $success = @imagewebp($this->image, $file, $quality); // @ is escalated to exception break; case self::AVIF: $quality = $quality === null ? 30 : max(0, min(100, $quality)); $success = @imageavif($this->image, $file, $quality); // @ is escalated to exception break; case self::BMP: $success = @imagebmp($this->image, $file); // @ is escalated to exception break; default: throw new Nette\InvalidArgumentException("Unsupported image type '$type'."); } if (!$success) { throw new ImageException(Helpers::getLastError() ?: 'Unknown error'); } } /** * Call to undefined method. * @return mixed * @throws Nette\MemberAccessException */ public function __call(string $name, array $args) { $function = 'image' . $name; if (!function_exists($function)) { ObjectHelpers::strictCall(static::class, $name); } foreach ($args as $key => $value) { if ($value instanceof self) { $args[$key] = $value->getImageResource(); } elseif (is_array($value) && isset($value['red'])) { // rgb $args[$key] = imagecolorallocatealpha( $this->image, $value['red'], $value['green'], $value['blue'], $value['alpha'] ) ?: imagecolorresolvealpha( $this->image, $value['red'], $value['green'], $value['blue'], $value['alpha'] ); } } $res = $function($this->image, ...$args); return $res instanceof \GdImage || (is_resource($res) && get_resource_type($res) === 'gd') ? $this->setImageResource($res) : $res; } public function __clone() { ob_start(function () {}); imagepng($this->image, null, 0); $this->setImageResource(imagecreatefromstring(ob_get_clean())); } /** * @param int|string $num in pixels or percent */ private static function isPercent(&$num): bool { if (is_string($num) && substr($num, -1) === '%') { $num = (float) substr($num, 0, -1); return true; } elseif (is_int($num) || $num === (string) (int) $num) { $num = (int) $num; return false; } throw new Nette\InvalidArgumentException("Expected dimension in int|string, '$num' given."); } /** * Prevents serialization. */ public function __sleep(): array { throw new Nette\NotSupportedException('You cannot serialize or unserialize ' . self::class . ' instances.'); } } PKk[\z(33 Random.phpnuW+A $array * @return static */ public static function from(array $array) { if (!Arrays::isList($array)) { throw new Nette\InvalidArgumentException('Array is not valid list.'); } $obj = new static; $obj->list = $array; return $obj; } /** * Returns an iterator over all items. * @return \ArrayIterator */ public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->list); } /** * Returns items count. */ public function count(): int { return count($this->list); } /** * Replaces or appends a item. * @param int|null $index * @param T $value * @throws Nette\OutOfRangeException */ public function offsetSet($index, $value): void { if ($index === null) { $this->list[] = $value; } elseif (!is_int($index) || $index < 0 || $index >= count($this->list)) { throw new Nette\OutOfRangeException('Offset invalid or out of range'); } else { $this->list[$index] = $value; } } /** * Returns a item. * @param int $index * @return T * @throws Nette\OutOfRangeException */ #[\ReturnTypeWillChange] public function offsetGet($index) { if (!is_int($index) || $index < 0 || $index >= count($this->list)) { throw new Nette\OutOfRangeException('Offset invalid or out of range'); } return $this->list[$index]; } /** * Determines whether a item exists. * @param int $index */ public function offsetExists($index): bool { return is_int($index) && $index >= 0 && $index < count($this->list); } /** * Removes the element at the specified position in this list. * @param int $index * @throws Nette\OutOfRangeException */ public function offsetUnset($index): void { if (!is_int($index) || $index < 0 || $index >= count($this->list)) { throw new Nette\OutOfRangeException('Offset invalid or out of range'); } array_splice($this->list, $index, 1); } /** * Prepends a item. * @param T $value */ public function prepend($value): void { $first = array_slice($this->list, 0, 1); $this->offsetSet(0, $value); array_splice($this->list, 1, 0, $first); } } PKk[fHH Floats.phpnuW+A $b it returns 1 * @throws \LogicException if one of parameters is NAN */ public static function compare(float $a, float $b): int { if (is_nan($a) || is_nan($b)) { throw new \LogicException('Trying to compare NAN'); } elseif (!is_finite($a) && !is_finite($b) && $a === $b) { return 0; } $diff = abs($a - $b); if (($diff < self::Epsilon || ($diff / max(abs($a), abs($b)) < self::Epsilon))) { return 0; } return $a < $b ? -1 : 1; } /** * Returns true if $a = $b * @throws \LogicException if one of parameters is NAN */ public static function areEqual(float $a, float $b): bool { return self::compare($a, $b) === 0; } /** * Returns true if $a < $b * @throws \LogicException if one of parameters is NAN */ public static function isLessThan(float $a, float $b): bool { return self::compare($a, $b) < 0; } /** * Returns true if $a <= $b * @throws \LogicException if one of parameters is NAN */ public static function isLessThanOrEqualTo(float $a, float $b): bool { return self::compare($a, $b) <= 0; } /** * Returns true if $a > $b * @throws \LogicException if one of parameters is NAN */ public static function isGreaterThan(float $a, float $b): bool { return self::compare($a, $b) > 0; } /** * Returns true if $a >= $b * @throws \LogicException if one of parameters is NAN */ public static function isGreaterThanOrEqualTo(float $a, float $b): bool { return self::compare($a, $b) >= 0; } } PKk[(T))Validators.phpnuW+A 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1, 'callable' => 1, 'iterable' => 1, 'void' => 1, 'null' => 1, 'mixed' => 1, 'false' => 1, 'never' => 1, 'true' => 1, ]; /** @var array */ protected static $validators = [ // PHP types 'array' => 'is_array', 'bool' => 'is_bool', 'boolean' => 'is_bool', 'float' => 'is_float', 'int' => 'is_int', 'integer' => 'is_int', 'null' => 'is_null', 'object' => 'is_object', 'resource' => 'is_resource', 'scalar' => 'is_scalar', 'string' => 'is_string', // pseudo-types 'callable' => [self::class, 'isCallable'], 'iterable' => 'is_iterable', 'list' => [Arrays::class, 'isList'], 'mixed' => [self::class, 'isMixed'], 'none' => [self::class, 'isNone'], 'number' => [self::class, 'isNumber'], 'numeric' => [self::class, 'isNumeric'], 'numericint' => [self::class, 'isNumericInt'], // string patterns 'alnum' => 'ctype_alnum', 'alpha' => 'ctype_alpha', 'digit' => 'ctype_digit', 'lower' => 'ctype_lower', 'pattern' => null, 'space' => 'ctype_space', 'unicode' => [self::class, 'isUnicode'], 'upper' => 'ctype_upper', 'xdigit' => 'ctype_xdigit', // syntax validation 'email' => [self::class, 'isEmail'], 'identifier' => [self::class, 'isPhpIdentifier'], 'uri' => [self::class, 'isUri'], 'url' => [self::class, 'isUrl'], // environment validation 'class' => 'class_exists', 'interface' => 'interface_exists', 'directory' => 'is_dir', 'file' => 'is_file', 'type' => [self::class, 'isType'], ]; /** @var array */ protected static $counters = [ 'string' => 'strlen', 'unicode' => [Strings::class, 'length'], 'array' => 'count', 'list' => 'count', 'alnum' => 'strlen', 'alpha' => 'strlen', 'digit' => 'strlen', 'lower' => 'strlen', 'space' => 'strlen', 'upper' => 'strlen', 'xdigit' => 'strlen', ]; /** * Verifies that the value is of expected types separated by pipe. * @param mixed $value * @throws AssertionException */ public static function assert($value, string $expected, string $label = 'variable'): void { if (!static::is($value, $expected)) { $expected = str_replace(['|', ':'], [' or ', ' in range '], $expected); $translate = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float', 'NULL' => 'null']; $type = $translate[gettype($value)] ?? gettype($value); if (is_int($value) || is_float($value) || (is_string($value) && strlen($value) < 40)) { $type .= ' ' . var_export($value, true); } elseif (is_object($value)) { $type .= ' ' . get_class($value); } throw new AssertionException("The $label expects to be $expected, $type given."); } } /** * Verifies that element $key in array is of expected types separated by pipe. * @param mixed[] $array * @param int|string $key * @throws AssertionException */ public static function assertField( array $array, $key, ?string $expected = null, string $label = "item '%' in array" ): void { if (!array_key_exists($key, $array)) { throw new AssertionException('Missing ' . str_replace('%', $key, $label) . '.'); } elseif ($expected) { static::assert($array[$key], $expected, str_replace('%', $key, $label)); } } /** * Verifies that the value is of expected types separated by pipe. * @param mixed $value */ public static function is($value, string $expected): bool { foreach (explode('|', $expected) as $item) { if (substr($item, -2) === '[]') { if (is_iterable($value) && self::everyIs($value, substr($item, 0, -2))) { return true; } continue; } elseif (substr($item, 0, 1) === '?') { $item = substr($item, 1); if ($value === null) { return true; } } [$type] = $item = explode(':', $item, 2); if (isset(static::$validators[$type])) { try { if (!static::$validators[$type]($value)) { continue; } } catch (\TypeError $e) { continue; } } elseif ($type === 'pattern') { if (Strings::match($value, '|^' . ($item[1] ?? '') . '$|D')) { return true; } continue; } elseif (!$value instanceof $type) { continue; } if (isset($item[1])) { $length = $value; if (isset(static::$counters[$type])) { $length = static::$counters[$type]($value); } $range = explode('..', $item[1]); if (!isset($range[1])) { $range[1] = $range[0]; } if (($range[0] !== '' && $length < $range[0]) || ($range[1] !== '' && $length > $range[1])) { continue; } } return true; } return false; } /** * Finds whether all values are of expected types separated by pipe. * @param mixed[] $values */ public static function everyIs(iterable $values, string $expected): bool { foreach ($values as $value) { if (!static::is($value, $expected)) { return false; } } return true; } /** * Checks if the value is an integer or a float. * @param mixed $value */ public static function isNumber($value): bool { return is_int($value) || is_float($value); } /** * Checks if the value is an integer or a integer written in a string. * @param mixed $value */ public static function isNumericInt($value): bool { return is_int($value) || (is_string($value) && preg_match('#^[+-]?[0-9]+$#D', $value)); } /** * Checks if the value is a number or a number written in a string. * @param mixed $value */ public static function isNumeric($value): bool { return is_float($value) || is_int($value) || (is_string($value) && preg_match('#^[+-]?([0-9]++\.?[0-9]*|\.[0-9]+)$#D', $value)); } /** * Checks if the value is a syntactically correct callback. * @param mixed $value */ public static function isCallable($value): bool { return $value && is_callable($value, true); } /** * Checks if the value is a valid UTF-8 string. * @param mixed $value */ public static function isUnicode($value): bool { return is_string($value) && preg_match('##u', $value); } /** * Checks if the value is 0, '', false or null. * @param mixed $value */ public static function isNone($value): bool { return $value == null; // intentionally == } /** @internal */ public static function isMixed(): bool { return true; } /** * Checks if a variable is a zero-based integer indexed array. * @param mixed $value * @deprecated use Nette\Utils\Arrays::isList */ public static function isList($value): bool { return Arrays::isList($value); } /** * Checks if the value is in the given range [min, max], where the upper or lower limit can be omitted (null). * Numbers, strings and DateTime objects can be compared. * @param mixed $value */ public static function isInRange($value, array $range): bool { if ($value === null || !(isset($range[0]) || isset($range[1]))) { return false; } $limit = $range[0] ?? $range[1]; if (is_string($limit)) { $value = (string) $value; } elseif ($limit instanceof \DateTimeInterface) { if (!$value instanceof \DateTimeInterface) { return false; } } elseif (is_numeric($value)) { $value *= 1; } else { return false; } return (!isset($range[0]) || ($value >= $range[0])) && (!isset($range[1]) || ($value <= $range[1])); } /** * Checks if the value is a valid email address. It does not verify that the domain actually exists, only the syntax is verified. */ public static function isEmail(string $value): bool { $atom = "[-a-z0-9!#$%&'*+/=?^_`{|}~]"; // RFC 5322 unquoted characters in local-part $alpha = "a-z\x80-\xFF"; // superset of IDN return (bool) preg_match(<< \\? (? [a-zA-Z_\x7f-\xff][\w\x7f-\xff]*) (\\ (?&name))* ) | (? (?&type) (& (?&type))+ ) | (? (?&type) | \( (?&intersection) \) ) (\| (?&upart))+ )$~xAD XX , $type); } } PKk[9 Y>ObjectHelpers.phpnuW+AgetProperties(\ReflectionProperty::IS_PUBLIC), function ($p) { return !$p->isStatic(); }), self::parseFullDoc($rc, '~^[ \t*]*@property(?:-read)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m') ), $name); throw new MemberAccessException("Cannot read an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); } /** * @return never * @throws MemberAccessException */ public static function strictSet(string $class, string $name): void { $rc = new \ReflectionClass($class); $hint = self::getSuggestion(array_merge( array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), function ($p) { return !$p->isStatic(); }), self::parseFullDoc($rc, '~^[ \t*]*@property(?:-write)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m') ), $name); throw new MemberAccessException("Cannot write to an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); } /** * @return never * @throws MemberAccessException */ public static function strictCall(string $class, string $method, array $additionalMethods = []): void { $trace = debug_backtrace(0, 3); // suppose this method is called from __call() $context = ($trace[1]['function'] ?? null) === '__call' ? ($trace[2]['class'] ?? null) : null; if ($context && is_a($class, $context, true) && method_exists($context, $method)) { // called parent::$method() $class = get_parent_class($context); } if (method_exists($class, $method)) { // insufficient visibility $rm = new \ReflectionMethod($class, $method); $visibility = $rm->isPrivate() ? 'private ' : ($rm->isProtected() ? 'protected ' : ''); throw new MemberAccessException("Call to {$visibility}method $class::$method() from " . ($context ? "scope $context." : 'global scope.')); } else { $hint = self::getSuggestion(array_merge( get_class_methods($class), self::parseFullDoc(new \ReflectionClass($class), '~^[ \t*]*@method[ \t]+(?:static[ \t]+)?(?:\S+[ \t]+)??(\w+)\(~m'), $additionalMethods ), $method); throw new MemberAccessException("Call to undefined method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.')); } } /** * @return never * @throws MemberAccessException */ public static function strictStaticCall(string $class, string $method): void { $trace = debug_backtrace(0, 3); // suppose this method is called from __callStatic() $context = ($trace[1]['function'] ?? null) === '__callStatic' ? ($trace[2]['class'] ?? null) : null; if ($context && is_a($class, $context, true) && method_exists($context, $method)) { // called parent::$method() $class = get_parent_class($context); } if (method_exists($class, $method)) { // insufficient visibility $rm = new \ReflectionMethod($class, $method); $visibility = $rm->isPrivate() ? 'private ' : ($rm->isProtected() ? 'protected ' : ''); throw new MemberAccessException("Call to {$visibility}method $class::$method() from " . ($context ? "scope $context." : 'global scope.')); } else { $hint = self::getSuggestion( array_filter((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC), function ($m) { return $m->isStatic(); }), $method ); throw new MemberAccessException("Call to undefined static method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.')); } } /** * Returns array of magic properties defined by annotation @property. * @return array of [name => bit mask] * @internal */ public static function getMagicProperties(string $class): array { static $cache; $props = &$cache[$class]; if ($props !== null) { return $props; } $rc = new \ReflectionClass($class); preg_match_all( '~^ [ \t*]* @property(|-read|-write|-deprecated) [ \t]+ [^\s$]+ [ \t]+ \$ (\w+) ()~mx', (string) $rc->getDocComment(), $matches, PREG_SET_ORDER ); $props = []; foreach ($matches as [, $type, $name]) { $uname = ucfirst($name); $write = $type !== '-read' && $rc->hasMethod($nm = 'set' . $uname) && ($rm = $rc->getMethod($nm))->name === $nm && !$rm->isPrivate() && !$rm->isStatic(); $read = $type !== '-write' && ($rc->hasMethod($nm = 'get' . $uname) || $rc->hasMethod($nm = 'is' . $uname)) && ($rm = $rc->getMethod($nm))->name === $nm && !$rm->isPrivate() && !$rm->isStatic(); if ($read || $write) { $props[$name] = $read << 0 | ($nm[0] === 'g') << 1 | $rm->returnsReference() << 2 | $write << 3 | ($type === '-deprecated') << 4; } } foreach ($rc->getTraits() as $trait) { $props += self::getMagicProperties($trait->name); } if ($parent = get_parent_class($class)) { $props += self::getMagicProperties($parent); } return $props; } /** * Finds the best suggestion (for 8-bit encoding). * @param (\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionClass|\ReflectionProperty|string)[] $possibilities * @internal */ public static function getSuggestion(array $possibilities, string $value): ?string { $norm = preg_replace($re = '#^(get|set|has|is|add)(?=[A-Z])#', '+', $value); $best = null; $min = (strlen($value) / 4 + 1) * 10 + .1; foreach (array_unique($possibilities, SORT_REGULAR) as $item) { $item = $item instanceof \Reflector ? $item->name : $item; if ($item !== $value && ( ($len = levenshtein($item, $value, 10, 11, 10)) < $min || ($len = levenshtein(preg_replace($re, '*', $item), $norm, 10, 11, 10)) < $min )) { $min = $len; $best = $item; } } return $best; } private static function parseFullDoc(\ReflectionClass $rc, string $pattern): array { do { $doc[] = $rc->getDocComment(); $traits = $rc->getTraits(); while ($trait = array_pop($traits)) { $doc[] = $trait->getDocComment(); $traits += $trait->getTraits(); } } while ($rc = $rc->getParentClass()); return preg_match_all($pattern, implode($doc), $m) ? $m[1] : []; } /** * Checks if the public non-static property exists. * @return bool|string returns 'event' if the property exists and has event like name * @internal */ public static function hasProperty(string $class, string $name) { static $cache; $prop = &$cache[$class][$name]; if ($prop === null) { $prop = false; try { $rp = new \ReflectionProperty($class, $name); if ($rp->isPublic() && !$rp->isStatic()) { $prop = $name >= 'onA' && $name < 'on_' ? 'event' : true; } } catch (\ReflectionException $e) { } } return $prop; } } PKk[FileSystem.phpnuW+AgetPathname()); } foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($origin, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST) as $item) { if ($item->isDir()) { static::createDir($target . '/' . $iterator->getSubPathName()); } else { static::copy($item->getPathname(), $target . '/' . $iterator->getSubPathName()); } } } else { static::createDir(dirname($target)); if ( ($s = @fopen($origin, 'rb')) && ($d = @fopen($target, 'wb')) && @stream_copy_to_stream($s, $d) === false ) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to copy file '%s' to '%s'. %s", self::normalizePath($origin), self::normalizePath($target), Helpers::getLastError() )); } } } /** * Deletes a file or directory if exists. * @throws Nette\IOException on error occurred */ public static function delete(string $path): void { if (is_file($path) || is_link($path)) { $func = DIRECTORY_SEPARATOR === '\\' && is_dir($path) ? 'rmdir' : 'unlink'; if (!@$func($path)) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to delete '%s'. %s", self::normalizePath($path), Helpers::getLastError() )); } } elseif (is_dir($path)) { foreach (new \FilesystemIterator($path) as $item) { static::delete($item->getPathname()); } if (!@rmdir($path)) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to delete directory '%s'. %s", self::normalizePath($path), Helpers::getLastError() )); } } } /** * Renames or moves a file or a directory. Overwrites existing files and directories by default. * @throws Nette\IOException on error occurred * @throws Nette\InvalidStateException if $overwrite is set to false and destination already exists */ public static function rename(string $origin, string $target, bool $overwrite = true): void { if (!$overwrite && file_exists($target)) { throw new Nette\InvalidStateException(sprintf("File or directory '%s' already exists.", self::normalizePath($target))); } elseif (!file_exists($origin)) { throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($origin))); } else { static::createDir(dirname($target)); if (realpath($origin) !== realpath($target)) { static::delete($target); } if (!@rename($origin, $target)) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to rename file or directory '%s' to '%s'. %s", self::normalizePath($origin), self::normalizePath($target), Helpers::getLastError() )); } } } /** * Reads the content of a file. * @throws Nette\IOException on error occurred */ public static function read(string $file): string { $content = @file_get_contents($file); // @ is escalated to exception if ($content === false) { throw new Nette\IOException(sprintf( "Unable to read file '%s'. %s", self::normalizePath($file), Helpers::getLastError() )); } return $content; } /** * Writes the string to a file. * @throws Nette\IOException on error occurred */ public static function write(string $file, string $content, ?int $mode = 0666): void { static::createDir(dirname($file)); if (@file_put_contents($file, $content) === false) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to write file '%s'. %s", self::normalizePath($file), Helpers::getLastError() )); } if ($mode !== null && !@chmod($file, $mode)) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to chmod file '%s' to mode %s. %s", self::normalizePath($file), decoct($mode), Helpers::getLastError() )); } } /** * Fixes permissions to a specific file or directory. Directories can be fixed recursively. * @throws Nette\IOException on error occurred */ public static function makeWritable(string $path, int $dirMode = 0777, int $fileMode = 0666): void { if (is_file($path)) { if (!@chmod($path, $fileMode)) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to chmod file '%s' to mode %s. %s", self::normalizePath($path), decoct($fileMode), Helpers::getLastError() )); } } elseif (is_dir($path)) { foreach (new \FilesystemIterator($path) as $item) { static::makeWritable($item->getPathname(), $dirMode, $fileMode); } if (!@chmod($path, $dirMode)) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to chmod directory '%s' to mode %s. %s", self::normalizePath($path), decoct($dirMode), Helpers::getLastError() )); } } else { throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($path))); } } /** * Determines if the path is absolute. */ public static function isAbsolute(string $path): bool { return (bool) preg_match('#([a-z]:)?[/\\\\]|[a-z][a-z0-9+.-]*://#Ai', $path); } /** * Normalizes `..` and `.` and directory separators in path. */ public static function normalizePath(string $path): string { $parts = $path === '' ? [] : preg_split('~[/\\\\]+~', $path); $res = []; foreach ($parts as $part) { if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') { array_pop($res); } elseif ($part !== '.') { $res[] = $part; } } return $res === [''] ? DIRECTORY_SEPARATOR : implode(DIRECTORY_SEPARATOR, $res); } /** * Joins all segments of the path and normalizes the result. */ public static function joinPaths(string ...$paths): string { return self::normalizePath(implode('/', $paths)); } } PKk[=$&-&-Reflection.phpnuW+AgetReturnType() ?? (PHP_VERSION_ID >= 80100 && $func instanceof \ReflectionMethod ? $func->getTentativeReturnType() : null); return self::getType($func, $type); } /** * @deprecated */ public static function getReturnTypes(\ReflectionFunctionAbstract $func): array { $type = Type::fromReflection($func); return $type ? $type->getNames() : []; } /** * Returns the type of given parameter and normalizes `self` and `parent` to the actual class names. * If the parameter does not have a type, it returns null. * If the parameter has union or intersection type, it throws Nette\InvalidStateException. */ public static function getParameterType(\ReflectionParameter $param): ?string { return self::getType($param, $param->getType()); } /** * @deprecated */ public static function getParameterTypes(\ReflectionParameter $param): array { $type = Type::fromReflection($param); return $type ? $type->getNames() : []; } /** * Returns the type of given property and normalizes `self` and `parent` to the actual class names. * If the property does not have a type, it returns null. * If the property has union or intersection type, it throws Nette\InvalidStateException. */ public static function getPropertyType(\ReflectionProperty $prop): ?string { return self::getType($prop, PHP_VERSION_ID >= 70400 ? $prop->getType() : null); } /** * @deprecated */ public static function getPropertyTypes(\ReflectionProperty $prop): array { $type = Type::fromReflection($prop); return $type ? $type->getNames() : []; } /** * @param \ReflectionFunction|\ReflectionMethod|\ReflectionParameter|\ReflectionProperty $reflection */ private static function getType($reflection, ?\ReflectionType $type): ?string { if ($type === null) { return null; } elseif ($type instanceof \ReflectionNamedType) { return Type::resolve($type->getName(), $reflection); } elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { throw new Nette\InvalidStateException('The ' . self::toString($reflection) . ' is not expected to have a union or intersection type.'); } else { throw new Nette\InvalidStateException('Unexpected type of ' . self::toString($reflection)); } } /** * Returns the default value of parameter. If it is a constant, it returns its value. * @return mixed * @throws \ReflectionException If the parameter does not have a default value or the constant cannot be resolved */ public static function getParameterDefaultValue(\ReflectionParameter $param) { if ($param->isDefaultValueConstant()) { $const = $orig = $param->getDefaultValueConstantName(); $pair = explode('::', $const); if (isset($pair[1])) { $pair[0] = Type::resolve($pair[0], $param); try { $rcc = new \ReflectionClassConstant($pair[0], $pair[1]); } catch (\ReflectionException $e) { $name = self::toString($param); throw new \ReflectionException("Unable to resolve constant $orig used as default value of $name.", 0, $e); } return $rcc->getValue(); } elseif (!defined($const)) { $const = substr((string) strrchr($const, '\\'), 1); if (!defined($const)) { $name = self::toString($param); throw new \ReflectionException("Unable to resolve constant $orig used as default value of $name."); } } return constant($const); } return $param->getDefaultValue(); } /** * Returns a reflection of a class or trait that contains a declaration of given property. Property can also be declared in the trait. */ public static function getPropertyDeclaringClass(\ReflectionProperty $prop): \ReflectionClass { foreach ($prop->getDeclaringClass()->getTraits() as $trait) { if ($trait->hasProperty($prop->name) // doc-comment guessing as workaround for insufficient PHP reflection && $trait->getProperty($prop->name)->getDocComment() === $prop->getDocComment() ) { return self::getPropertyDeclaringClass($trait->getProperty($prop->name)); } } return $prop->getDeclaringClass(); } /** * Returns a reflection of a method that contains a declaration of $method. * Usually, each method is its own declaration, but the body of the method can also be in the trait and under a different name. */ public static function getMethodDeclaringMethod(\ReflectionMethod $method): \ReflectionMethod { // file & line guessing as workaround for insufficient PHP reflection $decl = $method->getDeclaringClass(); if ($decl->getFileName() === $method->getFileName() && $decl->getStartLine() <= $method->getStartLine() && $decl->getEndLine() >= $method->getEndLine() ) { return $method; } $hash = [$method->getFileName(), $method->getStartLine(), $method->getEndLine()]; if (($alias = $decl->getTraitAliases()[$method->name] ?? null) && ($m = new \ReflectionMethod($alias)) && $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()] ) { return self::getMethodDeclaringMethod($m); } foreach ($decl->getTraits() as $trait) { if ($trait->hasMethod($method->name) && ($m = $trait->getMethod($method->name)) && $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()] ) { return self::getMethodDeclaringMethod($m); } } return $method; } /** * Finds out if reflection has access to PHPdoc comments. Comments may not be available due to the opcode cache. */ public static function areCommentsAvailable(): bool { static $res; return $res ?? $res = (bool) (new \ReflectionMethod(__METHOD__))->getDocComment(); } public static function toString(\Reflector $ref): string { if ($ref instanceof \ReflectionClass) { return $ref->name; } elseif ($ref instanceof \ReflectionMethod) { return $ref->getDeclaringClass()->name . '::' . $ref->name . '()'; } elseif ($ref instanceof \ReflectionFunction) { return $ref->name . '()'; } elseif ($ref instanceof \ReflectionProperty) { return self::getPropertyDeclaringClass($ref)->name . '::$' . $ref->name; } elseif ($ref instanceof \ReflectionParameter) { return '$' . $ref->name . ' in ' . self::toString($ref->getDeclaringFunction()); } else { throw new Nette\InvalidArgumentException; } } /** * Expands the name of the class to full name in the given context of given class. * Thus, it returns how the PHP parser would understand $name if it were written in the body of the class $context. * @throws Nette\InvalidArgumentException */ public static function expandClassName(string $name, \ReflectionClass $context): string { $lower = strtolower($name); if (empty($name)) { throw new Nette\InvalidArgumentException('Class name must not be empty.'); } elseif (Validators::isBuiltinType($lower)) { return $lower; } elseif ($lower === 'self' || $lower === 'static') { return $context->name; } elseif ($lower === 'parent') { return $context->getParentClass() ? $context->getParentClass()->name : 'parent'; } elseif ($name[0] === '\\') { // fully qualified name return ltrim($name, '\\'); } $uses = self::getUseStatements($context); $parts = explode('\\', $name, 2); if (isset($uses[$parts[0]])) { $parts[0] = $uses[$parts[0]]; return implode('\\', $parts); } elseif ($context->inNamespace()) { return $context->getNamespaceName() . '\\' . $name; } else { return $name; } } /** @return array of [alias => class] */ public static function getUseStatements(\ReflectionClass $class): array { if ($class->isAnonymous()) { throw new Nette\NotImplementedException('Anonymous classes are not supported.'); } static $cache = []; if (!isset($cache[$name = $class->name])) { if ($class->isInternal()) { $cache[$name] = []; } else { $code = file_get_contents($class->getFileName()); $cache = self::parseUseStatements($code, $name) + $cache; } } return $cache[$name]; } /** * Parses PHP code to [class => [alias => class, ...]] */ private static function parseUseStatements(string $code, ?string $forClass = null): array { try { $tokens = token_get_all($code, TOKEN_PARSE); } catch (\ParseError $e) { trigger_error($e->getMessage(), E_USER_NOTICE); $tokens = []; } $namespace = $class = $classLevel = $level = null; $res = $uses = []; $nameTokens = PHP_VERSION_ID < 80000 ? [T_STRING, T_NS_SEPARATOR] : [T_STRING, T_NS_SEPARATOR, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED]; while ($token = current($tokens)) { next($tokens); switch (is_array($token) ? $token[0] : $token) { case T_NAMESPACE: $namespace = ltrim(self::fetch($tokens, $nameTokens) . '\\', '\\'); $uses = []; break; case T_CLASS: case T_INTERFACE: case T_TRAIT: case PHP_VERSION_ID < 80100 ? T_CLASS : T_ENUM: if ($name = self::fetch($tokens, T_STRING)) { $class = $namespace . $name; $classLevel = $level + 1; $res[$class] = $uses; if ($class === $forClass) { return $res; } } break; case T_USE: while (!$class && ($name = self::fetch($tokens, $nameTokens))) { $name = ltrim($name, '\\'); if (self::fetch($tokens, '{')) { while ($suffix = self::fetch($tokens, $nameTokens)) { if (self::fetch($tokens, T_AS)) { $uses[self::fetch($tokens, T_STRING)] = $name . $suffix; } else { $tmp = explode('\\', $suffix); $uses[end($tmp)] = $name . $suffix; } if (!self::fetch($tokens, ',')) { break; } } } elseif (self::fetch($tokens, T_AS)) { $uses[self::fetch($tokens, T_STRING)] = $name; } else { $tmp = explode('\\', $name); $uses[end($tmp)] = $name; } if (!self::fetch($tokens, ',')) { break; } } break; case T_CURLY_OPEN: case T_DOLLAR_OPEN_CURLY_BRACES: case '{': $level++; break; case '}': if ($level === $classLevel) { $class = $classLevel = null; } $level--; } } return $res; } private static function fetch(array &$tokens, $take): ?string { $res = null; while ($token = current($tokens)) { [$token, $s] = is_array($token) ? $token : [$token, $token]; if (in_array($token, (array) $take, true)) { $res .= $s; } elseif (!in_array($token, [T_DOC_COMMENT, T_WHITESPACE, T_COMMENT], true)) { break; } next($tokens); } return $res; } } PKk[ EPPHtml.phpnuW+A element's attributes */ public $attrs = []; /** @var bool use XHTML syntax? */ public static $xhtml = false; /** @var array void elements */ public static $emptyElements = [ 'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1, 'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1, 'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1, ]; /** @var array nodes */ protected $children = []; /** @var string element's name */ private $name; /** @var bool is element empty? */ private $isEmpty; /** * Constructs new HTML element. * @param array|string $attrs element's attributes or plain text content * @return static */ public static function el(?string $name = null, $attrs = null) { $el = new static; $parts = explode(' ', (string) $name, 2); $el->setName($parts[0]); if (is_array($attrs)) { $el->attrs = $attrs; } elseif ($attrs !== null) { $el->setText($attrs); } if (isset($parts[1])) { foreach (Strings::matchAll($parts[1] . ' ', '#([a-z0-9:-]+)(?:=(["\'])?(.*?)(?(2)\2|\s))?#i') as $m) { $el->attrs[$m[1]] = $m[3] ?? true; } } return $el; } /** * Returns an object representing HTML text. */ public static function fromHtml(string $html): self { return (new static)->setHtml($html); } /** * Returns an object representing plain text. */ public static function fromText(string $text): self { return (new static)->setText($text); } /** * Converts to HTML. */ final public function toHtml(): string { return $this->render(); } /** * Converts to plain text. */ final public function toText(): string { return $this->getText(); } /** * Converts given HTML code to plain text. */ public static function htmlToText(string $html): string { return html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8'); } /** * Changes element's name. * @return static */ final public function setName(string $name, ?bool $isEmpty = null) { $this->name = $name; $this->isEmpty = $isEmpty ?? isset(static::$emptyElements[$name]); return $this; } /** * Returns element's name. */ final public function getName(): string { return $this->name; } /** * Is element empty? */ final public function isEmpty(): bool { return $this->isEmpty; } /** * Sets multiple attributes. * @return static */ public function addAttributes(array $attrs) { $this->attrs = array_merge($this->attrs, $attrs); return $this; } /** * Appends value to element's attribute. * @param mixed $value * @param mixed $option * @return static */ public function appendAttribute(string $name, $value, $option = true) { if (is_array($value)) { $prev = isset($this->attrs[$name]) ? (array) $this->attrs[$name] : []; $this->attrs[$name] = $value + $prev; } elseif ((string) $value === '') { $tmp = &$this->attrs[$name]; // appending empty value? -> ignore, but ensure it exists } elseif (!isset($this->attrs[$name]) || is_array($this->attrs[$name])) { // needs array $this->attrs[$name][$value] = $option; } else { $this->attrs[$name] = [$this->attrs[$name] => true, $value => $option]; } return $this; } /** * Sets element's attribute. * @param mixed $value * @return static */ public function setAttribute(string $name, $value) { $this->attrs[$name] = $value; return $this; } /** * Returns element's attribute. * @return mixed */ public function getAttribute(string $name) { return $this->attrs[$name] ?? null; } /** * Unsets element's attribute. * @return static */ public function removeAttribute(string $name) { unset($this->attrs[$name]); return $this; } /** * Unsets element's attributes. * @return static */ public function removeAttributes(array $attributes) { foreach ($attributes as $name) { unset($this->attrs[$name]); } return $this; } /** * Overloaded setter for element's attribute. * @param mixed $value */ final public function __set(string $name, $value): void { $this->attrs[$name] = $value; } /** * Overloaded getter for element's attribute. * @return mixed */ final public function &__get(string $name) { return $this->attrs[$name]; } /** * Overloaded tester for element's attribute. */ final public function __isset(string $name): bool { return isset($this->attrs[$name]); } /** * Overloaded unsetter for element's attribute. */ final public function __unset(string $name): void { unset($this->attrs[$name]); } /** * Overloaded setter for element's attribute. * @return mixed */ final public function __call(string $m, array $args) { $p = substr($m, 0, 3); if ($p === 'get' || $p === 'set' || $p === 'add') { $m = substr($m, 3); $m[0] = $m[0] | "\x20"; if ($p === 'get') { return $this->attrs[$m] ?? null; } elseif ($p === 'add') { $args[] = true; } } if (count($args) === 0) { // invalid } elseif (count($args) === 1) { // set $this->attrs[$m] = $args[0]; } else { // add $this->appendAttribute($m, $args[0], $args[1]); } return $this; } /** * Special setter for element's attribute. * @return static */ final public function href(string $path, ?array $query = null) { if ($query) { $query = http_build_query($query, '', '&'); if ($query !== '') { $path .= '?' . $query; } } $this->attrs['href'] = $path; return $this; } /** * Setter for data-* attributes. Booleans are converted to 'true' resp. 'false'. * @param mixed $value * @return static */ public function data(string $name, $value = null) { if (func_num_args() === 1) { $this->attrs['data'] = $name; } else { $this->attrs["data-$name"] = is_bool($value) ? json_encode($value) : $value; } return $this; } /** * Sets element's HTML content. * @param HtmlStringable|string $html * @return static */ final public function setHtml($html) { $this->children = [(string) $html]; return $this; } /** * Returns element's HTML content. */ final public function getHtml(): string { return implode('', $this->children); } /** * Sets element's textual content. * @param HtmlStringable|string|int|float $text * @return static */ final public function setText($text) { if (!$text instanceof HtmlStringable) { $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8'); } $this->children = [(string) $text]; return $this; } /** * Returns element's textual content. */ final public function getText(): string { return self::htmlToText($this->getHtml()); } /** * Adds new element's child. * @param HtmlStringable|string $child Html node or raw HTML string * @return static */ final public function addHtml($child) { return $this->insert(null, $child); } /** * Appends plain-text string to element content. * @param HtmlStringable|string|int|float $text * @return static */ public function addText($text) { if (!$text instanceof HtmlStringable) { $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8'); } return $this->insert(null, $text); } /** * Creates and adds a new Html child. * @param array|string $attrs element's attributes or raw HTML string * @return static created element */ final public function create(string $name, $attrs = null) { $this->insert(null, $child = static::el($name, $attrs)); return $child; } /** * Inserts child node. * @param HtmlStringable|string $child Html node or raw HTML string * @return static */ public function insert(?int $index, $child, bool $replace = false) { $child = $child instanceof self ? $child : (string) $child; if ($index === null) { // append $this->children[] = $child; } else { // insert or replace array_splice($this->children, $index, $replace ? 1 : 0, [$child]); } return $this; } /** * Inserts (replaces) child node (\ArrayAccess implementation). * @param int|null $index position or null for appending * @param Html|string $child Html node or raw HTML string */ final public function offsetSet($index, $child): void { $this->insert($index, $child, true); } /** * Returns child node (\ArrayAccess implementation). * @param int $index * @return HtmlStringable|string */ #[\ReturnTypeWillChange] final public function offsetGet($index) { return $this->children[$index]; } /** * Exists child node? (\ArrayAccess implementation). * @param int $index */ final public function offsetExists($index): bool { return isset($this->children[$index]); } /** * Removes child node (\ArrayAccess implementation). * @param int $index */ public function offsetUnset($index): void { if (isset($this->children[$index])) { array_splice($this->children, $index, 1); } } /** * Returns children count. */ final public function count(): int { return count($this->children); } /** * Removes all children. */ public function removeChildren(): void { $this->children = []; } /** * Iterates over elements. * @return \ArrayIterator */ final public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->children); } /** * Returns all children. */ final public function getChildren(): array { return $this->children; } /** * Renders element's start tag, content and end tag. */ final public function render(?int $indent = null): string { $s = $this->startTag(); if (!$this->isEmpty) { // add content if ($indent !== null) { $indent++; } foreach ($this->children as $child) { if ($child instanceof self) { $s .= $child->render($indent); } else { $s .= $child; } } // add end tag $s .= $this->endTag(); } if ($indent !== null) { return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2)); } return $s; } final public function __toString(): string { try { return $this->render(); } catch (\Throwable $e) { if (PHP_VERSION_ID >= 70400) { throw $e; } trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); return ''; } } /** * Returns element's start tag. */ final public function startTag(): string { return $this->name ? '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>') : ''; } /** * Returns element's end tag. */ final public function endTag(): string { return $this->name && !$this->isEmpty ? 'name . '>' : ''; } /** * Returns element's attributes. * @internal */ final public function attributes(): string { if (!is_array($this->attrs)) { return ''; } $s = ''; $attrs = $this->attrs; foreach ($attrs as $key => $value) { if ($value === null || $value === false) { continue; } elseif ($value === true) { if (static::$xhtml) { $s .= ' ' . $key . '="' . $key . '"'; } else { $s .= ' ' . $key; } continue; } elseif (is_array($value)) { if (strncmp($key, 'data-', 5) === 0) { $value = Json::encode($value); } else { $tmp = null; foreach ($value as $k => $v) { if ($v != null) { // intentionally ==, skip nulls & empty string // composite 'style' vs. 'others' $tmp[] = $v === true ? $k : (is_string($k) ? $k . ':' . $v : $v); } } if ($tmp === null) { continue; } $value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp); } } elseif (is_float($value)) { $value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.'); } else { $value = (string) $value; } $q = strpos($value, '"') === false ? '"' : "'"; $s .= ' ' . $key . '=' . $q . str_replace( ['&', $q, '<'], ['&', $q === '"' ? '"' : ''', self::$xhtml ? '<' : '<'], $value ) . (strpos($value, '`') !== false && strpbrk($value, ' <>"\'') === false ? ' ' : '') . $q; } $s = str_replace('@', '@', $s); return $s; } /** * Clones all children too. */ public function __clone() { foreach ($this->children as $key => $value) { if (is_object($value)) { $this->children[$key] = clone $value; } } } } PK‚[݉(AA Stack.phpnu[ => [, , , ], * => [, , , ], * => [, , , ], * => [, , , ], * ] * * Where each array is a column of the stack. Visually the stack above would render like this (columns to rows): * * |1|1|1|1|_| * |2|2|_|_|_| * |_|3|3|_|_| * |_|_|_|4|4| * * Looking at this last representation there is some "wasted" visual space rigth of 2 that might be filled by 4; if the * stack is set ot "recycle" space then this would be the representation of the same events: * * |1|1|1|1|_| * |2|2|_|4|4| * |_|3|3|_|_| * * The stack takes now one less row (i.e. the stack columns all have one less element). * * @since 4.9.7 * @package Tribe\Events\Views\V2\Utils */ namespace Tribe\Events\Views\V2\Utils; use Tribe__Date_Utils as Dates; /** * Class Stack * * @since 4.9.7 * @package Tribe\Events\Views\V2\Utils */ class Stack { /** * The current day, in the `Y-m-d` format. * * @since 4.9.7 * * @var int|string */ protected $current_day; /** * The current day events, a list of event post IDs. * * @since 4.9.7 * * @var array */ protected $day_events; /** * An associative array relating each event ID to its current position in the stack. * * @var array */ protected $stack_positions; /** * The current stack. * * @since 4.9.7 * * @var array */ protected $stack; /** * Whether to "recycle" the empty stack spaces, where possible, or not.. * * @since 4.9.7 * * @var bool */ protected $recycle_space; /** * The spacer currently used to mark empty spaces in the stack. * * @since 4.9.7 * * @var mixed */ protected $spacer; /** * A flag to indicate whether the stack elements should be normalized or not. * * @since 4.9.7 * * @var bool */ protected $normalize_stack; /** * Builds and returns the stack for a group of events, divided by days. * * @since 4.9.7 * * @param array $events_by_day An array of events, per-day, in the shape `[ => [ ... ] ]`. * * @param null|mixed $spacer The spacer that should be used to indicate an empty space in the stack. * Defaults to the filtered spacer. * @param null|bool $recycle_space Whether to recycle spaces or not; defaults to the filtered value. * @param null|bool $normalize_stack Whether to normalize the stack by padding the bottom of it with spacers or * not; defaults to the filtered value. * * @return array An associative array of days, each with the events "stacked", including spacers, in the shape: * `[ * => [, , ], * => [, , ], * => [, , ], * ]` * and so on. Each stack column (a day) will be padded with spacers to have consistent stack height * which means that all arrays in the stack will have the same length. */ public function build_from_events( array $events_by_day = [], $spacer = null, $recycle_space = null, $normalize_stack = null ) { if ( empty( $events_by_day ) ) { return []; } // @todo [BTRIA-597]: @be We use the spacer someplace, find it and refer it to this value. $this->spacer = null !== $spacer ? $spacer : $this->get_spacer(); $this->recycle_space = null !== $recycle_space ? (bool) $recycle_space : $this->should_recycle_spaces( $events_by_day ); $this->normalize_stack = null !== $normalize_stack ? (bool) $normalize_stack : $this->should_normalize_stack( $events_by_day ); // Init the working properties. $this->stack = []; $this->stack_positions = []; // Make sure all days in the period will make it to the stack; even if empty. $events_by_day = $this->add_missing_days( $events_by_day ); /* * Calculate each multi-day event_id stack position in the stack. */ foreach ( $events_by_day as $current_day => $the_day_events ) { $this->stack[ $current_day ] = $this->build_day_stack( $current_day, $the_day_events ); } if ( $this->normalize_stack ) { $this->normalize_stack(); } return $this->stack; } /** * Returns the "spacer" used to indicate an empty space in the stack. * * @since 4.9.7 * * @return mixed The spacer used to indicate an empty space in the stack. */ public function get_spacer() { /** * Filters the spacer that will be used to indicate an empty space in a stack. * * @since 4.9.7 * * @param mixed $spacer The spacer that will be used to indicate an empty space in ths stack; default `false`. */ $spacer = apply_filters( 'tribe_events_views_v2_stack_spacer', false ); return false; } /** * Filters and returns a value indicating whether the stack should be built "recycling" spaces or not. * * @since 4.9.7 * * @param array $events_by_day An array of event IDs, divided by day, with shape `[ => [...] ]`. * * @return bool Whether the stack should be built "recycling" spaces or not. */ protected function should_recycle_spaces( array $events_by_day = [] ) { /** * Filters whether to "recycle" the available spaces or not while building the week stack. * * As an example we have the events: * 1 => [2019-7-1, 2019-7-3] * 2 => [2019-7-2, 2019-7-6] * 3 => [2019-7-5, 2019-7-6] * The week stack would look like this not recycling space: * |1|1|1|-|-|-|-| * |-|2|2|2|2|2|-| * |-|-|-|-|3|3|-| * The week stack would look like this recycling space: * |1|1|1|-|3|3|-| * |-|2|2|2|2|2|-| * The space is "recycled" in the sense that we try to avoid higher stacks, when possible, recycling them. * * @since 4.9.7 * * @param bool $recycle_spaces Whether to recycle space in the week stack or not; default `true`. * @param array $events_by_day An array of event IDs, divided by day, with shape `[ => [...] ]`. */ return (bool) apply_filters( 'tribe_events_views_v2_stack_recycle_spaces', true, $events_by_day ); } /** * Builds and returns the stack for the current day. * * @since 4.9.7 * * @param string $current_day The current day date, in the `Y-m-d` format. * @param array $the_day_events All the current day event post IDs. * * @return array The stack for the current day in the shape `[ , , , ...]`. */ protected function build_day_stack( $current_day, array $the_day_events ) { $day_events = $this->filter_stack_events( $the_day_events ); if ( 0 === count( $day_events ) ) { return []; } // Set some properties we'll use in the methods to avoid having to pass them back and forth. $this->current_day = $current_day; $this->day_events = $day_events; $this->assign_day_events_position(); return $this->fill_day_stack(); } /** * Filters an array of events to remove any event that should not be in the stack. * * The default filtering strategy, in the `filter_stack_event` method, will filter out any non multi-day event. * If, in the future, we'll need to change this strategy then either extend the class or use the . * * @since 4.9.7 * * @param array $events An array of events, post objects or post IDs, to filter. * * @return array The filtered array of events. */ protected function filter_stack_events( $events ) { $filtered = array_values( array_filter( $events, [ $this, 'filter_stack_event' ] ) ); /** * Filters the array of events that should be part of the stack. * * By default any non multi-day event will not be part of the stack. * * @since 4.9.7 * * @param array $filtered The events as filtered from the default strategy. * @param array $events The unfiltered events. */ $filtered = apply_filters( 'tribe_events_views_v2_stack_events', $filtered, $events ); return $filtered; } /** * Parses, and sets if required, the stack positions of each event, in the current day, in the stack. * * @since 4.9.7 */ protected function assign_day_events_position() { /* * The events come, in the context of the day, sorted by the sorting criteria; e.g. ASC date and time. * In the context of a multi-day stack we might want to maximize the use of space and use empty rows * whenever possible. */ if ( $this->recycle_space ) { $this->stack_positions = $this->assign_open_positions( $this->stack_positions, $this->day_events ); return; } $this->stack_positions = $this->assign_next_positions( $this->stack_positions, $this->day_events ); } /** * Normalizes the day stack by adding spacers in each empty position. * * @since 4.9.7 * * @return array The day stack with each position, starting from the `0` position, filled with either an event ID or * a spacer. */ protected function fill_day_stack() { $day_stack = []; $day_positions = array_intersect_key( $this->stack_positions, array_combine( $this->day_events, $this->day_events ) ); $max_day_position = count( $day_positions ) ? max( $day_positions ) : 0; foreach ( range( 0, $max_day_position ) as $j ) { if ( in_array( $j, $day_positions, true ) ) { $day_stack[ $j ] = array_search( $j, $day_positions, true ); } else { $day_stack[ $j ] = $this->spacer; } } return $day_stack; } /** * Normalize the stack by adding padding each stack day to make sure all days are present and have the same length. * * @since 4.9.7 */ protected function normalize_stack() { // Calculate the max stack height: we'll need it to pad each day stack. $stack_height = array_reduce( $this->stack, static function ( $current_max, array $day_stack ) { return max( $current_max, count( $day_stack ) ); }, 0 ); // Finally add to the stacks collection. foreach ( $this->stack as $current_day => $day_stack ) { $this->stack[ $current_day ] = array_pad( $day_stack, $stack_height, $this->spacer ); } } /** * Checks an event to ensure it should be part of the stack. * * The default strategy is to filter out any non multi-day event, but extending classes can change this. * * @since 4.9.7 * * @param int|\WP_Post $event The event post object or ID. * * @return bool Whether the event should be part of the stack or not. */ protected function filter_stack_event( $event ) { $post = tribe_get_event( $event ); $keep = $post instanceof \WP_Post && ( ( ! empty( $post->multiday ) && $post->multiday > 1 ) || ! empty( $post->all_day ) ); /** * Filters the result of the check to keep or discard an event from the stack. * * @since 4.9.11 * * @param bool $keep Whether the event should be part of the stack or not. * @param mixed $event An event post object, post ID, or any possible representation an event might have. */ return apply_filters( 'tribe_events_views_v2_stack_filter_event', $keep, $event ); } /** * Returns the filtered value to decide if the stack should be normalized or not padding each element with spacers * to the same height as the one of the stack elements with more events in it or not. * * @since 4.9.7 * * @param array $events_by_day An array of event IDs, divided by day, with shape `[ => [...] ]`. * * @return bool Whether the stack should be normalized by padding each one of its elements with spacers at the * bottom or not. */ protected function should_normalize_stack( array $events_by_day = [] ) { /** * Filters the value to decide if the stack should be normalized or not padding each element with spacers * to the same height as the one of the stack elements with more events in it or not. * * As an example we have the events: * 1 => [2019-7-1, 2019-7-3] * 2 => [2019-7-2, 2019-7-6] * 3 => [2019-7-5, 2019-7-6] * The week stack would look like this not normalizing it: * |1|1|1|-|-|-| * |2|2|2|2|2| * |3|3| * The week stack would look like this normalizing it: * |1|1|1|-|-|-| * |-|2|2|2|2|2| * |-|-|-|-|3|3| * The space is "normalized " by adding spacers at the bottom of any stack element until it reaches the same * height as the one with more elements (the last two days in the example). * * @since 4.9.7 * * @param bool $normalize_stack Whether the stack should be normalized by padding each one of its elements with * spacers at the bottom or not; defaults to `false`. * @param array $events_by_day An array of event IDs, divided by day, with shape `[ => [...] ]`. */ return apply_filters( 'tribe_events_views_v2_stack_normalize', false, $events_by_day ); } /** * Adds the missing days in the passed events by day to make sure all dates in the period will appear. * * @since 4.9.7 * * @param array $events_by_day The events part of the stack, divided by day. * * @return array The events part of the stack, divided by day with added missing days, if any. */ protected function add_missing_days( array $events_by_day ) { $days = array_keys( $events_by_day ); $first_day = reset( $days ); $last_day = end( $days ); try { // The timezone is not relevant here. $period = new \DatePeriod( Dates::build_date_object( $first_day ), new \DateInterval( 'P1D' ), Dates::build_date_object( $last_day )->setTime( 23, 59, 59 ) ); $missing = []; /** @var \DateTime $date */ foreach ( $period as $date ) { $date_string = $date->format( 'Y-m-d' ); if ( in_array( $date_string, $days, true ) ) { continue; } $missing[$date_string] = []; } } catch ( \Exception $e ) { // If there's any issue just return the events by day as they are. return $events_by_day; } $events_by_day = array_merge( $events_by_day, $missing ); ksort( $events_by_day ); return $events_by_day; } /** * Assigns to each event the first available position in the day stack. * * This method will "fill" empty spaces in the stack to recycle the space. * * @since 4.9.9 * * @param array $stack_positions The currently assigned stack positions, in the shape * `[ => ]`. * @param array $wo_position An array of event post IDs for events that do not have a position assigned * in the day stack. * * @return array An updated array of stack positions, in the shape `[ => ]`. */ protected function assign_open_positions( array $stack_positions, array $events ) { $wo_position = array_diff( $events, array_keys( $stack_positions ) ); if ( ! count( $wo_position ) ) { return $stack_positions; } $taken_day_positions = array_intersect_key( $stack_positions, array_flip( $events ) ); $all_possible_positions = range( 0, count( $stack_positions ) + count( $wo_position ) ); $open_day_positions = array_values( array_diff( $all_possible_positions, $taken_day_positions ) ); $assigned_day_positions = array_combine( $wo_position, array_slice( $open_day_positions, 0, count( $wo_position ) ) ); // Use the `+` to avoid the re-indexing: the indexes here are the event post IDs. $stack_positions += $assigned_day_positions; return $stack_positions; } /** * Assigns a stack postion to each event w/o one not recycling space. * * @since 4.9.9 * * @param array $stack_positions The current stack positions. * @param array $event_ids The events to position in the stack, events that already have a position will not * be re-positioned. * * @return array The finalized stack positions, where each event has been assigned a position in the stack. */ protected function assign_next_positions( array $stack_positions, array $event_ids ) { $wo_position = array_diff( $event_ids, array_keys( $stack_positions ) ); foreach ( $wo_position as $position_in_day => $event_id ) { // The event position is the next one. $stack_positions[ $event_id ] = count( $stack_positions ) ? max( $stack_positions ) + 1 : 0; } return $stack_positions; } } PK‚[u!Separators.phpnu[ID === reset( $events )->ID ) { // The first event in a set should always trigger the month separator display. return true; } if ( null !== $request_date ) { $request_date = Dates::build_date_object( $request_date ); } // Reduce events to only keep the starting ones. $start_events_ids = array_unique( array_combine( wp_list_pluck( $events, 'ID' ), array_map( static function ( \WP_Post $event ) use ( $request_date ) { /* * If we have a request date we "move forward" the event to it. * If the event is in this set, then we assume it fits. * This is usually the case w/ multi-day events that start "in the past" in relation to a request * date; in that case we display them not in their original month, but in the request date one. */ $the_date = null !== $request_date ? max( $event->dates->start_display, $request_date ) : $event->dates->start_display; return $the_date->format( 'Y-m' ); }, $events ) ) ); return $event->ID === array_search( $event->dates->start_display->format( 'Y-m' ), $start_events_ids, true ); } /** * Determines if a given event from a list of events should have a day separator * for some List view template structures (such as month view mobile/widget). * * Note that events will NOT be sorted by date for this check: this is by design. There are other criteria by which * events might be sorted this method should not interfere with. * The method will perform the check using the "display" date of the events since this is a front-end facing method. * * @since 4.6.0 * * @param array $events WP_Post or numeric ID for events. * @param \WP_Post|int $event Event we want to check. * @param string|\DateTimeInterface|null $request_date A request date that should be used as context for the * evaluation. * * @return boolean Whether the event, in the context of this event set and request date, should show the separator or not. */ public static function should_have_day( $events, $event, $request_date = null ) { if ( ! is_array( $events ) ) { return false; } $events = array_filter( array_map( 'tribe_get_event', $events ), static function ( $event ) { return $event instanceof \WP_Post; } ); $event = tribe_get_event( $event ); if ( empty( $events ) || ! $event instanceof \WP_Post ) { return false; } if ( $event->ID === reset( $events )->ID ) { // The first event in a set should always trigger the separator display. return true; } if ( null !== $request_date ) { $request_date = Dates::build_date_object( $request_date ); } // Reduce events to only keep the starting ones. $start_events_ids = array_unique( array_combine( wp_list_pluck( $events, 'ID' ), array_map( static function ( \WP_Post $event ) use ( $request_date ) { /* * If we have a request date we "move the event forward" to it. * If the event is in this set, then we assume it fits. * This is usually the case w/ multi-day events that start "in the past" in relation to a request * date; in that case we display them not on their original day, but on the request date. */ $the_date = null !== $request_date ? max( $event->dates->start_display, $request_date ) : $event->dates->start_display; return $the_date->format( 'd' ); }, $events ) ) ); return $event->ID === array_search( $event->dates->start_display->format( 'd' ), $start_events_ids, true ); } /** * Determines if a given event from a list of events should have a time separator * for the Day view template structure. Rounded down to the hour. * * @since 4.9.5 * * @param array $events WP_Post or numeric ID for events. * @param \WP_Post|int $event Event we want to check. * * @return boolean */ public static function should_have_time( $events, $event ) { if ( ! is_array( $events ) ) { return false; } $ids = array_map( static function( $event ) { return absint( is_numeric( $event ) ? $event : $event->ID ); }, $events ); $event_id = is_numeric( $event ) ? $event : $event->ID; $start_hours = array_map( static function( $id ) { return Dates::round_nearest_half_hour( tribe_get_start_date( $id, true, Dates::DBDATETIMEFORMAT ) ); }, $ids ); $start_hours_ids = array_unique( array_combine( $ids, $start_hours ) ); return isset( $start_hours_ids[ $event_id ] ); } /** * Determines if a given event from a list of events should have a type separator * for the day view template structure. * * @since 4.9.11 * * @param array $events WP_Post or numeric ID for events. * @param WP_Post|int $event Event we want to determine. * * @return boolean */ public static function should_have_type( array $events, \WP_Post $event ) { if ( ! is_array( $events ) ) { return false; } $event_id = $event->ID; if ( empty( $event->timeslot ) ) { return false; } $ids = array_map( static function( $event ) { return absint( is_numeric( $event ) ? $event : $event->ID ); }, $events ); $index = array_search( $event_id, $ids ); // Return false if it wasn't found. if ( false === $index ) { return $index; } $is_first = 0 === $index; $is_new_timeslot = ! $is_first && $events[ $index ]->timeslot !== $events[ $index - 1 ]->timeslot; // Should have type separator if it's the first element or if it's a new timeslot. $should_have = $is_first || $is_new_timeslot; return $should_have; } } PK‚[DP View.phpnu[ $view_data The initial View data. * * @return array The filtered View data, some entries removed from it to avoid the data script * being mangled by escaping and texturizing functions running on it. */ public static function clean_data( $view_data ) { if ( ! is_array( $view_data ) ) { return $view_data; } /* * Remove the JSON-LD data, it's already printed by the `components/json-ld-data.php` template. Printing a * `