/home/mip/mip/public/img/credit/datatables/Utils.tar
Floats.php000064400000004110151246271750006513 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * Floating-point numbers comparison.
 */
class Floats
{
	use Nette\StaticClass;

	private const Epsilon = 1e-10;


	public static function isZero(float $value): bool
	{
		return abs($value) < self::Epsilon;
	}


	public static function isInteger(float $value): bool
	{
		return abs(round($value) - $value) < self::Epsilon;
	}


	/**
	 * Compare two floats. If $a < $b it returns -1, if they are equal it returns 0 and if $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;
	}
}
ImageType.php000064400000000645151246271750007160 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;


/**
 * Type of image file.
 */
/*enum*/ final class ImageType
{
	public const
		JPEG = IMAGETYPE_JPEG,
		PNG = IMAGETYPE_PNG,
		GIF = IMAGETYPE_GIF,
		WEBP = IMAGETYPE_WEBP,
		AVIF = 19, // IMAGETYPE_AVIF,
		BMP = IMAGETYPE_BMP;
}
Json.php000064400000004342151246271750006203 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * JSON encoder and decoder.
 */
final class Json
{
	use Nette\StaticClass;

	/** @deprecated use Json::decode(..., forceArrays: true) */
	public const FORCE_ARRAY = JSON_OBJECT_AS_ARRAY;

	/** @deprecated use Json::encode(..., pretty: true) */
	public const PRETTY = JSON_PRETTY_PRINT;

	/** @deprecated use Json::encode(..., asciiSafe: true) */
	public const ESCAPE_UNICODE = 1 << 19;


	/**
	 * Converts value to JSON format. Use $pretty for easier reading and clarity, $asciiSafe for ASCII output
	 * and $htmlSafe for HTML escaping, $forceObjects enforces the encoding of non-associateve arrays as objects.
	 * @throws JsonException
	 */
	public static function encode(
		mixed $value,
		bool|int $pretty = false,
		bool $asciiSafe = false,
		bool $htmlSafe = false,
		bool $forceObjects = false,
	): string
	{
		if (is_int($pretty)) { // back compatibility
			$flags = ($pretty & self::ESCAPE_UNICODE ? 0 : JSON_UNESCAPED_UNICODE) | ($pretty & ~self::ESCAPE_UNICODE);
		} else {
			$flags = ($asciiSafe ? 0 : JSON_UNESCAPED_UNICODE)
				| ($pretty ? JSON_PRETTY_PRINT : 0)
				| ($forceObjects ? JSON_FORCE_OBJECT : 0)
				| ($htmlSafe ? JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG : 0);
		}

		$flags |= JSON_UNESCAPED_SLASHES
			| (defined('JSON_PRESERVE_ZERO_FRACTION') ? JSON_PRESERVE_ZERO_FRACTION : 0); // since PHP 5.6.6 & PECL JSON-C 1.3.7

		$json = json_encode($value, $flags);
		if ($error = json_last_error()) {
			throw new JsonException(json_last_error_msg(), $error);
		}

		return $json;
	}


	/**
	 * Parses JSON to PHP value. The $forceArrays enforces the decoding of objects as arrays.
	 * @throws JsonException
	 */
	public static function decode(string $json, bool|int $forceArrays = false): mixed
	{
		$flags = is_int($forceArrays) // back compatibility
			? $forceArrays
			: ($forceArrays ? JSON_OBJECT_AS_ARRAY : 0);
		$flags |= JSON_BIGINT_AS_STRING;

		$value = json_decode($json, flags: $flags);
		if ($error = json_last_error()) {
			throw new JsonException(json_last_error_msg(), $error);
		}

		return $value;
	}
}
ArrayList.php000064400000005162151246271750007205 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * Provides the base class for a generic list (items can be accessed by index).
 * @template T
 * @implements \IteratorAggregate<int, T>
 * @implements \ArrayAccess<int, T>
 */
class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
{
	use Nette\SmartObject;

	private array $list = [];


	/**
	 * Transforms array to ArrayList.
	 * @param  list<T>  $array
	 */
	public static function from(array $array): static
	{
		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 \Iterator<int, T>
	 */
	public function &getIterator(): \Iterator
	{
		foreach ($this->list as &$item) {
			yield $item;
		}
	}


	/**
	 * 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
	 */
	public function offsetGet($index): mixed
	{
		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(mixed $value): void
	{
		$first = array_slice($this->list, 0, 1);
		$this->offsetSet(0, $value);
		array_splice($this->list, 1, 0, $first);
	}
}
DateTime.php000064400000005553151246271750006773 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * DateTime.
 */
class DateTime extends \DateTime implements \JsonSerializable
{
	use Nette\SmartObject;

	/** minute in seconds */
	public const MINUTE = 60;

	/** hour in seconds */
	public const HOUR = 60 * self::MINUTE;

	/** day in seconds */
	public const DAY = 24 * self::HOUR;

	/** week in seconds */
	public const WEEK = 7 * self::DAY;

	/** average month in seconds */
	public const MONTH = 2_629_800;

	/** average year in seconds */
	public const YEAR = 31_557_600;


	/**
	 * Creates a DateTime object from a string, UNIX timestamp, or other DateTimeInterface object.
	 * @throws \Exception if the date and time are not valid.
	 */
	public static function from(string|int|\DateTimeInterface|null $time): static
	{
		if ($time instanceof \DateTimeInterface) {
			return new static($time->format('Y-m-d H:i:s.u'), $time->getTimezone());

		} elseif (is_numeric($time)) {
			if ($time <= self::YEAR) {
				$time += time();
			}

			return (new static)->setTimestamp((int) $time);

		} else { // textual or null
			return new static((string) $time);
		}
	}


	/**
	 * Creates DateTime object.
	 * @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,
	): static
	{
		$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.
	 */
	public static function createFromFormat(
		string $format,
		string $time,
		string|\DateTimeZone|null $timezone = null,
	): static|false
	{
		if ($timezone === null) {
			$timezone = new \DateTimeZone(date_default_timezone_get());

		} elseif (is_string($timezone)) {
			$timezone = new \DateTimeZone($timezone);
		}

		$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');
	}


	/**
	 * You'd better use: (clone $dt)->modify(...)
	 */
	public function modifyClone(string $modify = ''): static
	{
		$dolly = clone $this;
		return $modify ? $dolly->modify($modify) : $dolly;
	}
}
Random.php000064400000002315151246271750006510 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;
use Random\Randomizer;


/**
 * Secure random string generator.
 */
final class Random
{
	use Nette\StaticClass;

	/**
	 * Generates a random string of given length from characters specified in second argument.
	 * Supports intervals, such as `0-9` or `A-Z`.
	 */
	public static function generate(int $length = 10, string $charlist = '0-9a-z'): string
	{
		$charlist = preg_replace_callback(
			'#.-.#',
			fn(array $m): string => implode('', range($m[0][0], $m[0][2])),
			$charlist,
		);
		$charlist = count_chars($charlist, mode: 3);
		$chLen = strlen($charlist);

		if ($length < 1) {
			throw new Nette\InvalidArgumentException('Length must be greater than zero.');
		} elseif ($chLen < 2) {
			throw new Nette\InvalidArgumentException('Character list must contain at least two chars.');
		} elseif (PHP_VERSION_ID >= 80300) {
			return (new Randomizer)->getBytesFromString($charlist, $length);
		}

		$res = '';
		for ($i = 0; $i < $length; $i++) {
			$res .= $charlist[random_int(0, $chLen - 1)];
		}

		return $res;
	}
}
Validators.php000064400000024650151246271750007406 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * Validation utilities.
 */
class Validators
{
	use Nette\StaticClass;

	private const BuiltinTypes = [
		'string' => 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<string,?callable> */
	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<string,callable> */
	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.
	 * @throws AssertionException
	 */
	public static function assert(mixed $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, return: true);
			} elseif (is_object($value)) {
				$type .= ' ' . $value::class;
			}

			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
	 * @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.
	 */
	public static function is(mixed $value, string $expected): bool
	{
		foreach (explode('|', $expected) as $item) {
			if (str_ends_with($item, '[]')) {
				if (is_iterable($value) && self::everyIs($value, substr($item, 0, -2))) {
					return true;
				}

				continue;
			} elseif (str_starts_with($item, '?')) {
				$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.
	 * @return ($value is int|float ? true : false)
	 */
	public static function isNumber(mixed $value): bool
	{
		return is_int($value) || is_float($value);
	}


	/**
	 * Checks if the value is an integer or a integer written in a string.
	 * @return ($value is non-empty-string ? bool : ($value is int ? true : false))
	 */
	public static function isNumericInt(mixed $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.
	 * @return ($value is non-empty-string ? bool : ($value is int|float ? true : false))
	 */
	public static function isNumeric(mixed $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.
	 */
	public static function isCallable(mixed $value): bool
	{
		return $value && is_callable($value, syntax_only: true);
	}


	/**
	 * Checks if the value is a valid UTF-8 string.
	 */
	public static function isUnicode(mixed $value): bool
	{
		return is_string($value) && preg_match('##u', $value);
	}


	/**
	 * Checks if the value is 0, '', false or null.
	 * @return ($value is 0|''|false|null ? true : false)
	 */
	public static function isNone(mixed $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.
	 * @deprecated  use Nette\Utils\Arrays::isList
	 * @return ($value is list ? true : false)
	 */
	public static function isList(mixed $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.
	 */
	public static function isInRange(mixed $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(<<<XX
			(^(?n)
				("([ !#-[\\]-~]*|\\\\[ -~])+"|$atom+(\\.$atom+)*)  # quoted or unquoted
				@
				([0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)+  # domain - RFC 1034
				[$alpha]([-0-9$alpha]{0,17}[$alpha])?              # top domain
			$)Dix
			XX, $value);
	}


	/**
	 * Checks if the value is a valid URL address.
	 */
	public static function isUrl(string $value): bool
	{
		$alpha = "a-z\x80-\xFF";
		return (bool) preg_match(<<<XX
			(^(?n)
				https?://(
					(([-_0-9$alpha]+\\.)*                       # subdomain
						[0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)?  # domain
						[$alpha]([-0-9$alpha]{0,17}[$alpha])?   # top domain
					|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}  # IPv4
					|\\[[0-9a-f:]{3,39}\\]                      # IPv6
				)(:\\d{1,5})?                                   # port
				(/\\S*)?                                        # path
				(\\?\\S*)?                                      # query
				(\\#\\S*)?                                      # fragment
			$)Dix
			XX, $value);
	}


	/**
	 * Checks if the value is a valid URI address, that is, actually a string beginning with a syntactically valid schema.
	 */
	public static function isUri(string $value): bool
	{
		return (bool) preg_match('#^[a-z\d+\.-]+:\S+$#Di', $value);
	}


	/**
	 * Checks whether the input is a class, interface or trait.
	 * @deprecated
	 */
	public static function isType(string $type): bool
	{
		return class_exists($type) || interface_exists($type) || trait_exists($type);
	}


	/**
	 * Checks whether the input is a valid PHP identifier.
	 */
	public static function isPhpIdentifier(string $value): bool
	{
		return preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#D', $value) === 1;
	}


	/**
	 * Determines if type is PHP built-in type. Otherwise, it is the class name.
	 */
	public static function isBuiltinType(string $type): bool
	{
		return isset(self::BuiltinTypes[strtolower($type)]);
	}


	/**
	 * Determines if type is special class name self/parent/static.
	 */
	public static function isClassKeyword(string $name): bool
	{
		return (bool) preg_match('#^(self|parent|static)$#Di', $name);
	}


	/**
	 * Checks whether the given type declaration is syntactically valid.
	 */
	public static function isTypeDeclaration(string $type): bool
	{
		return (bool) preg_match(<<<'XX'
			~((?n)
				\?? (?<type> \\? (?<name> [a-zA-Z_\x7f-\xff][\w\x7f-\xff]*) (\\ (?&name))* ) |
				(?<intersection> (?&type) (& (?&type))+ ) |
				(?<upart> (?&type) | \( (?&intersection) \) )  (\| (?&upart))+
			)$~xAD
			XX, $type);
	}
}
Image.php000064400000060047151246271750006320 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * Basic manipulation with images. Supported types are JPEG, PNG, GIF, WEBP, AVIF and BMP.
 *
 * <code>
 * $image = Image::fromFile('nette.jpg');
 * $image->resize(150, 100);
 * $image->sharpen();
 * $image->send();
 * </code>
 *
 * @method Image affine(array $affine, ?array $clip = null)
 * @method void alphaBlending(bool $enable)
 * @method void antialias(bool $enable)
 * @method void arc(int $centerX, int $centerY, int $width, int $height, int $startAngle, int $endAngle, ImageColor $color)
 * @method int colorAllocate(int $red, int $green, int $blue)
 * @method int colorAllocateAlpha(int $red, int $green, int $blue, int $alpha)
 * @method int colorAt(int $x, int $y)
 * @method int colorClosest(int $red, int $green, int $blue)
 * @method int colorClosestAlpha(int $red, int $green, int $blue, int $alpha)
 * @method int colorClosestHWB(int $red, int $green, int $blue)
 * @method void colorDeallocate(int $color)
 * @method int colorExact(int $red, int $green, int $blue)
 * @method int colorExactAlpha(int $red, int $green, int $blue, int $alpha)
 * @method void colorMatch(Image $image2)
 * @method int colorResolve(int $red, int $green, int $blue)
 * @method int colorResolveAlpha(int $red, int $green, int $blue, int $alpha)
 * @method void colorSet(int $index, int $red, int $green, int $blue, int $alpha = 0)
 * @method array colorsForIndex(int $color)
 * @method int colorsTotal()
 * @method int colorTransparent(?int $color = null)
 * @method void convolution(array $matrix, float $div, float $offset)
 * @method void copy(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH)
 * @method void copyMerge(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct)
 * @method void copyMergeGray(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct)
 * @method void copyResampled(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $dstW, int $dstH, int $srcW, int $srcH)
 * @method void copyResized(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $dstW, int $dstH, int $srcW, int $srcH)
 * @method Image cropAuto(int $mode = IMG_CROP_DEFAULT, float $threshold = .5, ?ImageColor $color = null)
 * @method void ellipse(int $centerX, int $centerY, int $width, int $height, ImageColor $color)
 * @method void fill(int $x, int $y, ImageColor $color)
 * @method void filledArc(int $centerX, int $centerY, int $width, int $height, int $startAngle, int $endAngle, ImageColor $color, int $style)
 * @method void filledEllipse(int $centerX, int $centerY, int $width, int $height, ImageColor $color)
 * @method void filledPolygon(array $points, ImageColor $color)
 * @method void filledRectangle(int $x1, int $y1, int $x2, int $y2, ImageColor $color)
 * @method void fillToBorder(int $x, int $y, ImageColor $borderColor, ImageColor $color)
 * @method void filter(int $filter, ...$args)
 * @method void flip(int $mode)
 * @method array ftText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontFile, string $text, array $options = [])
 * @method void gammaCorrect(float $inputgamma, float $outputgamma)
 * @method array getClip()
 * @method int getInterpolation()
 * @method int interlace(?bool $enable = null)
 * @method bool isTrueColor()
 * @method void layerEffect(int $effect)
 * @method void line(int $x1, int $y1, int $x2, int $y2, ImageColor $color)
 * @method void openPolygon(array $points, ImageColor $color)
 * @method void paletteCopy(Image $source)
 * @method void paletteToTrueColor()
 * @method void polygon(array $points, ImageColor $color)
 * @method void rectangle(int $x1, int $y1, int $x2, int $y2, ImageColor $color)
 * @method mixed resolution(?int $resolutionX = null, ?int $resolutionY = null)
 * @method Image rotate(float $angle, ImageColor $backgroundColor)
 * @method void saveAlpha(bool $enable)
 * @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(int $x, int $y, ImageColor $color)
 * @method void setStyle(array $style)
 * @method void setThickness(int $thickness)
 * @method void setTile(Image $tile)
 * @method void trueColorToPalette(bool $dither, int $ncolors)
 * @method array ttfText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontfile, string $text, array $options = [])
 * @property-read positive-int $width
 * @property-read positive-int $height
 * @property-read \GdImage $imageResource
 */
class Image
{
	use Nette\SmartObject;

	/** Prevent from getting resized to a bigger size than the original */
	public const ShrinkOnly = 0b0001;

	/** Resizes to a specified width and height without keeping aspect ratio */
	public const Stretch = 0b0010;

	/** Resizes to fit into a specified width and height and preserves aspect ratio */
	public const OrSmaller = 0b0000;

	/** Resizes while bounding the smaller dimension to the specified width or height and preserves aspect ratio */
	public const OrBigger = 0b0100;

	/** Resizes to the smallest possible size to completely cover specified width and height and reserves aspect ratio */
	public const Cover = 0b1000;

	/** @deprecated use Image::ShrinkOnly */
	public const SHRINK_ONLY = self::ShrinkOnly;

	/** @deprecated use Image::Stretch */
	public const STRETCH = self::Stretch;

	/** @deprecated use Image::OrSmaller */
	public const FIT = self::OrSmaller;

	/** @deprecated use Image::OrBigger */
	public const FILL = self::OrBigger;

	/** @deprecated use Image::Cover */
	public const EXACT = self::Cover;

	/** @deprecated use Image::EmptyGIF */
	public const EMPTY_GIF = self::EmptyGIF;

	/** image types */
	public const
		JPEG = ImageType::JPEG,
		PNG = ImageType::PNG,
		GIF = ImageType::GIF,
		WEBP = ImageType::WEBP,
		AVIF = ImageType::AVIF,
		BMP = ImageType::BMP;

	public const EmptyGIF = "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 = [ImageType::JPEG => 'jpeg', ImageType::PNG => 'png', ImageType::GIF => 'gif', ImageType::WEBP => 'webp', ImageType::AVIF => 'avif', ImageType::BMP => 'bmp'];

	private \GdImage $image;


	/**
	 * Returns RGB color (0..255) and transparency (0..127).
	 * @deprecated use ImageColor::rgb()
	 */
	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
	 */
	public static function fromFile(string $file, ?int &$type = null): static
	{
		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.
	 * @throws Nette\NotSupportedException if gd extension is not loaded
	 * @throws ImageException
	 */
	public static function fromString(string $s, ?int &$type = null): static
	{
		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): static
	{
		$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.
	 * @param  positive-int  $width
	 * @param  positive-int  $height
	 * @throws Nette\NotSupportedException if gd extension is not loaded
	 */
	public static function fromBlank(int $width, int $height, ImageColor|array|null $color = null): static
	{
		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 = new static(imagecreatetruecolor($width, $height));
		if ($color) {
			$image->alphablending(false);
			$image->filledrectangle(0, 0, $width - 1, $height - 1, $color);
			$image->alphablending(true);
		}

		return $image;
	}


	/**
	 * Returns the type of image from file.
	 * @return ImageType::*|null
	 */
	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.
	 * @return ImageType::*|null
	 */
	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 type.
	 * @param  ImageType::*  $type
	 * @return value-of<self::Formats>
	 */
	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 type for given file extension.
	 * @return ImageType::*
	 */
	public static function extensionToType(string $extension): int
	{
		$extensions = array_flip(self::Formats) + ['jpg' => ImageType::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 type.
	 * @param  ImageType::*  $type
	 */
	public static function typeToMimeType(int $type): string
	{
		return 'image/' . self::typeToExtension($type);
	}


	/**
	 * @param  ImageType::*  $type
	 */
	public static function isTypeSupported(int $type): bool
	{
		return (bool) (imagetypes() & match ($type) {
			ImageType::JPEG => IMG_JPG,
			ImageType::PNG => IMG_PNG,
			ImageType::GIF => IMG_GIF,
			ImageType::WEBP => IMG_WEBP,
			ImageType::AVIF => 256, // IMG_AVIF,
			ImageType::BMP => IMG_BMP,
			default => 0,
		});
	}


	/** @return  ImageType[] */
	public static function getSupportedTypes(): array
	{
		$flag = imagetypes();
		return array_filter([
			$flag & IMG_GIF ? ImageType::GIF : null,
			$flag & IMG_JPG ? ImageType::JPEG : null,
			$flag & IMG_PNG ? ImageType::PNG : null,
			$flag & IMG_WEBP ? ImageType::WEBP : null,
			$flag & 256 ? ImageType::AVIF : null, // IMG_AVIF
			$flag & IMG_BMP ? ImageType::BMP : null,
		]);
	}


	/**
	 * Wraps GD image.
	 */
	public function __construct(\GdImage $image)
	{
		$this->setImageResource($image);
		imagesavealpha($image, true);
	}


	/**
	 * Returns image width.
	 * @return positive-int
	 */
	public function getWidth(): int
	{
		return imagesx($this->image);
	}


	/**
	 * Returns image height.
	 * @return positive-int
	 */
	public function getHeight(): int
	{
		return imagesy($this->image);
	}


	/**
	 * Sets image resource.
	 */
	protected function setImageResource(\GdImage $image): static
	{
		$this->image = $image;
		return $this;
	}


	/**
	 * Returns image GD resource.
	 */
	public function getImageResource(): \GdImage
	{
		return $this->image;
	}


	/**
	 * Scales an image. Width and height accept pixels or percent.
	 * @param  int-mask-of<self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly>  $mode
	 */
	public function resize(int|string|null $width, int|string|null $height, int $mode = self::OrSmaller): static
	{
		if ($mode & self::Cover) {
			return $this->resize($width, $height, self::OrBigger)->crop('50%', '50%', $width, $height);
		}

		[$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $mode);

		if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize
			$newImage = static::fromBlank($newWidth, $newHeight, ImageColor::rgb(0, 0, 0, 0))->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. Width and height accept pixels or percent.
	 * @param  int-mask-of<self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly>  $mode
	 */
	public static function calculateSize(
		int $srcWidth,
		int $srcHeight,
		$newWidth,
		$newHeight,
		int $mode = self::OrSmaller,
	): 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));
			$mode |= empty($percents) ? 0 : self::Stretch;
		} else {
			$newHeight = abs($newHeight);
		}

		if ($mode & self::Stretch) { // non-proportional
			if (!$newWidth || !$newHeight) {
				throw new Nette\InvalidArgumentException('For stretching must be both width and height specified.');
			}

			if ($mode & self::ShrinkOnly) {
				$newWidth = min($srcWidth, $newWidth);
				$newHeight = min($srcHeight, $newHeight);
			}
		} 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 ($mode & self::OrBigger) {
				$scale = [max($scale)];
			}

			if ($mode & self::ShrinkOnly) {
				$scale[] = 1;
			}

			$scale = min($scale);
			$newWidth = (int) round($srcWidth * $scale);
			$newHeight = (int) round($srcHeight * $scale);
		}

		return [max($newWidth, 1), max($newHeight, 1)];
	}


	/**
	 * Crops image. Arguments accepts pixels or percent.
	 */
	public function crop(int|string $left, int|string $top, int|string $width, int|string $height): static
	{
		[$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'], ImageColor::rgb(0, 0, 0, 0))->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. Arguments accepts pixels or percent.
	 */
	public static function calculateCutout(
		int $srcWidth,
		int $srcHeight,
		int|string $left,
		int|string $top,
		int|string $newWidth,
		int|string $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.
	 */
	public function sharpen(): static
	{
		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. Left and top accepts pixels or percent.
	 * @param  int<0, 100>  $opacity 0..100
	 */
	public function place(self $image, int|string $left = 0, int|string $top = 0, int $opacity = 100): static
	{
		$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;
	}


	/**
	 * Calculates the bounding box for a TrueType text. Returns keys left, top, width and height.
	 */
	public static function calculateTextBox(
		string $text,
		string $fontFile,
		float $size,
		float $angle = 0,
		array $options = [],
	): array
	{
		$box = imagettfbbox($size, $angle, $fontFile, $text, $options);
		return [
			'left' => $minX = min([$box[0], $box[2], $box[4], $box[6]]),
			'top' => $minY = min([$box[1], $box[3], $box[5], $box[7]]),
			'width' => max([$box[0], $box[2], $box[4], $box[6]]) - $minX + 1,
			'height' => max([$box[1], $box[3], $box[5], $box[7]]) - $minY + 1,
		];
	}


	/**
	 * Draw a rectangle.
	 */
	public function rectangleWH(int $x, int $y, int $width, int $height, ImageColor $color): void
	{
		if ($width !== 0 && $height !== 0) {
			$this->rectangle($x, $y, $x + $width + ($width > 0 ? -1 : 1), $y + $height + ($height > 0 ? -1 : 1), $color);
		}
	}


	/**
	 * Draw a filled rectangle.
	 */
	public function filledRectangleWH(int $x, int $y, int $width, int $height, ImageColor $color): void
	{
		if ($width !== 0 && $height !== 0) {
			$this->filledRectangle($x, $y, $x + $width + ($width > 0 ? -1 : 1), $y + $height + ($height > 0 ? -1 : 1), $color);
		}
	}


	/**
	 * 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).
	 * @param  ImageType::*|null  $type
	 * @throws ImageException
	 */
	public function save(string $file, ?int $quality = null, ?int $type = null): void
	{
		$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).
	 * @param  ImageType::*  $type
	 */
	public function toString(int $type = ImageType::JPEG, ?int $quality = null): string
	{
		return Helpers::capture(function () use ($type, $quality): void {
			$this->output($type, $quality);
		});
	}


	/**
	 * Outputs image to string.
	 */
	public function __toString(): string
	{
		return $this->toString();
	}


	/**
	 * 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).
	 * @param  ImageType::*  $type
	 * @throws ImageException
	 */
	public function send(int $type = ImageType::JPEG, ?int $quality = null): void
	{
		header('Content-Type: ' . self::typeToMimeType($type));
		$this->output($type, $quality);
	}


	/**
	 * Outputs image to browser or file.
	 * @param  ImageType::*  $type
	 * @throws ImageException
	 */
	private function output(int $type, ?int $quality, ?string $file = null): void
	{
		switch ($type) {
			case ImageType::JPEG:
				$quality = $quality === null ? 85 : max(0, min(100, $quality));
				$success = @imagejpeg($this->image, $file, $quality); // @ is escalated to exception
				break;

			case ImageType::PNG:
				$quality = $quality === null ? 9 : max(0, min(9, $quality));
				$success = @imagepng($this->image, $file, $quality); // @ is escalated to exception
				break;

			case ImageType::GIF:
				$success = @imagegif($this->image, $file); // @ is escalated to exception
				break;

			case ImageType::WEBP:
				$quality = $quality === null ? 80 : max(0, min(100, $quality));
				$success = @imagewebp($this->image, $file, $quality); // @ is escalated to exception
				break;

			case ImageType::AVIF:
				$quality = $quality === null ? 30 : max(0, min(100, $quality));
				$success = @imageavif($this->image, $file, $quality); // @ is escalated to exception
				break;

			case ImageType::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.
	 * @throws Nette\MemberAccessException
	 */
	public function __call(string $name, array $args): mixed
	{
		$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 ($value instanceof ImageColor || (is_array($value) && isset($value['red']))) {
				$args[$key] = $this->resolveColor($value);
			}
		}

		$res = $function($this->image, ...$args);
		return $res instanceof \GdImage
			? $this->setImageResource($res)
			: $res;
	}


	public function __clone()
	{
		ob_start(function () {});
		imagepng($this->image, null, 0);
		$this->setImageResource(imagecreatefromstring(ob_get_clean()));
	}


	private static function isPercent(int|string &$num): bool
	{
		if (is_string($num) && str_ends_with($num, '%')) {
			$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.');
	}


	public function resolveColor(ImageColor|array $color): int
	{
		$color = $color instanceof ImageColor ? $color->toRGBA() : array_values($color);
		return imagecolorallocatealpha($this->image, ...$color) ?: imagecolorresolvealpha($this->image, ...$color);
	}
}
Callback.php000064400000006756151246271750007001 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;
use function is_array, is_object, is_string;


/**
 * PHP callable tools.
 */
final class Callback
{
	use Nette\StaticClass;

	/**
	 * Invokes internal PHP function with own error handler.
	 */
	public static function invokeSafe(string $function, array $args, callable $onError): mixed
	{
		$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.
	 * @return callable
	 * @throws Nette\InvalidArgumentException
	 */
	public static function check(mixed $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.
	 */
	public static function toString(mixed $callable): string
	{
		if ($callable instanceof \Closure) {
			$inner = self::unwrap($callable);
			return '{closure' . ($inner instanceof \Closure ? '}' : ' ' . self::toString($inner) . '}');
		} 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
	 * @throws \ReflectionException  if callback is not valid
	 */
	public static function toReflection($callable): \ReflectionMethod|\ReflectionFunction
	{
		if ($callable instanceof \Closure) {
			$callable = self::unwrap($callable);
		}

		if (is_string($callable) && str_contains($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().
	 */
	public static function unwrap(\Closure $closure): callable|array
	{
		$r = new \ReflectionFunction($closure);
		$class = $r->getClosureScopeClass()?->name;
		if (str_ends_with($r->name, '}')) {
			return $closure;

		} elseif (($obj = $r->getClosureThis()) && $obj::class === $class) {
			return [$obj, $r->name];

		} elseif ($class) {
			return [$class, $r->name];

		} else {
			return $r->name;
		}
	}
}
FileSystem.php000064400000022112151246271750007351 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * File system tool.
 */
final class FileSystem
{
	/**
	 * Creates a directory if it does not exist, including parent directories.
	 * @throws Nette\IOException  on error occurred
	 */
	public static function createDir(string $dir, int $mode = 0777): void
	{
		if (!is_dir($dir) && !@mkdir($dir, $mode, recursive: true) && !is_dir($dir)) { // @ - dir may already exist
			throw new Nette\IOException(sprintf(
				"Unable to create directory '%s' with mode %s. %s",
				self::normalizePath($dir),
				decoct($mode),
				Helpers::getLastError(),
			));
		}
	}


	/**
	 * Copies a file or an entire 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 copy(string $origin, string $target, bool $overwrite = true): void
	{
		if (stream_is_local($origin) && !file_exists($origin)) {
			throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($origin)));

		} elseif (!$overwrite && file_exists($target)) {
			throw new Nette\InvalidStateException(sprintf("File or directory '%s' already exists.", self::normalizePath($target)));

		} elseif (is_dir($origin)) {
			static::createDir($target);
			foreach (new \FilesystemIterator($target) as $item) {
				static::delete($item->getPathname());
			}

			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 (@stream_copy_to_stream(static::open($origin, 'rb'), static::open($target, 'wb')) === 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(),
				));
			}
		}
	}


	/**
	 * Opens file and returns resource.
	 * @return resource
	 * @throws Nette\IOException  on error occurred
	 */
	public static function open(string $path, string $mode)
	{
		$f = @fopen($path, $mode); // @ is escalated to exception
		if (!$f) {
			throw new Nette\IOException(sprintf(
				"Unable to open file '%s'. %s",
				self::normalizePath($path),
				Helpers::getLastError(),
			));
		}
		return $f;
	}


	/**
	 * Deletes a file or an entire directory if exists. If the directory is not empty, it deletes its contents first.
	 * @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;
	}


	/**
	 * Reads the file content line by line. Because it reads continuously as we iterate over the lines,
	 * it is possible to read files larger than the available memory.
	 * @return \Generator<int, string>
	 * @throws Nette\IOException  on error occurred
	 */
	public static function readLines(string $file, bool $stripNewLines = true): \Generator
	{
		return (function ($f) use ($file, $stripNewLines) {
			$counter = 0;
			do {
				$line = Callback::invokeSafe('fgets', [$f], fn($error) => throw new Nette\IOException(sprintf(
					"Unable to read file '%s'. %s",
					self::normalizePath($file),
					$error,
				)));
				if ($line === false) {
					fclose($f);
					break;
				}
				if ($stripNewLines) {
					$line = rtrim($line, "\r\n");
				}

				yield $counter++ => $line;

			} while (true);
		})(static::open($file, 'r'));
	}


	/**
	 * 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(),
			));
		}
	}


	/**
	 * Sets file permissions to `$fileMode` or directory permissions to `$dirMode`.
	 * Recursively traverses and sets permissions on the entire contents of the directory as well.
	 * @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));
	}


	/**
	 * Converts backslashes to slashes.
	 */
	public static function unixSlashes(string $path): string
	{
		return strtr($path, '\\', '/');
	}


	/**
	 * Converts slashes to platform-specific directory separators.
	 */
	public static function platformSlashes(string $path): string
	{
		return DIRECTORY_SEPARATOR === '/'
			? strtr($path, '\\', '/')
			: str_replace(':\\\\', '://', strtr($path, '/', '\\')); // protocol://
	}
}
Html.php000064400000046041151246271750006200 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;
use Nette\HtmlStringable;
use function is_array, is_float, is_object, is_string;


/**
 * HTML helper.
 *
 * @property string|null $accept
 * @property string|null $accesskey
 * @property string|null $action
 * @property string|null $align
 * @property string|null $allow
 * @property string|null $alt
 * @property bool|null   $async
 * @property string|null $autocapitalize
 * @property string|null $autocomplete
 * @property bool|null   $autofocus
 * @property bool|null   $autoplay
 * @property string|null $charset
 * @property bool|null   $checked
 * @property string|null $cite
 * @property string|null $class
 * @property int|null    $cols
 * @property int|null    $colspan
 * @property string|null $content
 * @property bool|null   $contenteditable
 * @property bool|null   $controls
 * @property string|null $coords
 * @property string|null $crossorigin
 * @property string|null $data
 * @property string|null $datetime
 * @property string|null $decoding
 * @property bool|null   $default
 * @property bool|null   $defer
 * @property string|null $dir
 * @property string|null $dirname
 * @property bool|null   $disabled
 * @property bool|null   $download
 * @property string|null $draggable
 * @property string|null $dropzone
 * @property string|null $enctype
 * @property string|null $for
 * @property string|null $form
 * @property string|null $formaction
 * @property string|null $formenctype
 * @property string|null $formmethod
 * @property bool|null   $formnovalidate
 * @property string|null $formtarget
 * @property string|null $headers
 * @property int|null    $height
 * @property bool|null   $hidden
 * @property float|null  $high
 * @property string|null $href
 * @property string|null $hreflang
 * @property string|null $id
 * @property string|null $integrity
 * @property string|null $inputmode
 * @property bool|null   $ismap
 * @property string|null $itemprop
 * @property string|null $kind
 * @property string|null $label
 * @property string|null $lang
 * @property string|null $list
 * @property bool|null   $loop
 * @property float|null  $low
 * @property float|null  $max
 * @property int|null    $maxlength
 * @property int|null    $minlength
 * @property string|null $media
 * @property string|null $method
 * @property float|null  $min
 * @property bool|null   $multiple
 * @property bool|null   $muted
 * @property string|null $name
 * @property bool|null   $novalidate
 * @property bool|null   $open
 * @property float|null  $optimum
 * @property string|null $pattern
 * @property string|null $ping
 * @property string|null $placeholder
 * @property string|null $poster
 * @property string|null $preload
 * @property string|null $radiogroup
 * @property bool|null   $readonly
 * @property string|null $rel
 * @property bool|null   $required
 * @property bool|null   $reversed
 * @property int|null    $rows
 * @property int|null    $rowspan
 * @property string|null $sandbox
 * @property string|null $scope
 * @property bool|null   $selected
 * @property string|null $shape
 * @property int|null    $size
 * @property string|null $sizes
 * @property string|null $slot
 * @property int|null    $span
 * @property string|null $spellcheck
 * @property string|null $src
 * @property string|null $srcdoc
 * @property string|null $srclang
 * @property string|null $srcset
 * @property int|null    $start
 * @property float|null  $step
 * @property string|null $style
 * @property int|null    $tabindex
 * @property string|null $target
 * @property string|null $title
 * @property string|null $translate
 * @property string|null $type
 * @property string|null $usemap
 * @property string|null $value
 * @property int|null    $width
 * @property string|null $wrap
 *
 * @method self accept(?string $val)
 * @method self accesskey(?string $val, bool $state = null)
 * @method self action(?string $val)
 * @method self align(?string $val)
 * @method self allow(?string $val, bool $state = null)
 * @method self alt(?string $val)
 * @method self async(?bool $val)
 * @method self autocapitalize(?string $val)
 * @method self autocomplete(?string $val)
 * @method self autofocus(?bool $val)
 * @method self autoplay(?bool $val)
 * @method self charset(?string $val)
 * @method self checked(?bool $val)
 * @method self cite(?string $val)
 * @method self class(?string $val, bool $state = null)
 * @method self cols(?int $val)
 * @method self colspan(?int $val)
 * @method self content(?string $val)
 * @method self contenteditable(?bool $val)
 * @method self controls(?bool $val)
 * @method self coords(?string $val)
 * @method self crossorigin(?string $val)
 * @method self datetime(?string $val)
 * @method self decoding(?string $val)
 * @method self default(?bool $val)
 * @method self defer(?bool $val)
 * @method self dir(?string $val)
 * @method self dirname(?string $val)
 * @method self disabled(?bool $val)
 * @method self download(?bool $val)
 * @method self draggable(?string $val)
 * @method self dropzone(?string $val)
 * @method self enctype(?string $val)
 * @method self for(?string $val)
 * @method self form(?string $val)
 * @method self formaction(?string $val)
 * @method self formenctype(?string $val)
 * @method self formmethod(?string $val)
 * @method self formnovalidate(?bool $val)
 * @method self formtarget(?string $val)
 * @method self headers(?string $val, bool $state = null)
 * @method self height(?int $val)
 * @method self hidden(?bool $val)
 * @method self high(?float $val)
 * @method self hreflang(?string $val)
 * @method self id(?string $val)
 * @method self integrity(?string $val)
 * @method self inputmode(?string $val)
 * @method self ismap(?bool $val)
 * @method self itemprop(?string $val)
 * @method self kind(?string $val)
 * @method self label(?string $val)
 * @method self lang(?string $val)
 * @method self list(?string $val)
 * @method self loop(?bool $val)
 * @method self low(?float $val)
 * @method self max(?float $val)
 * @method self maxlength(?int $val)
 * @method self minlength(?int $val)
 * @method self media(?string $val)
 * @method self method(?string $val)
 * @method self min(?float $val)
 * @method self multiple(?bool $val)
 * @method self muted(?bool $val)
 * @method self name(?string $val)
 * @method self novalidate(?bool $val)
 * @method self open(?bool $val)
 * @method self optimum(?float $val)
 * @method self pattern(?string $val)
 * @method self ping(?string $val, bool $state = null)
 * @method self placeholder(?string $val)
 * @method self poster(?string $val)
 * @method self preload(?string $val)
 * @method self radiogroup(?string $val)
 * @method self readonly(?bool $val)
 * @method self rel(?string $val)
 * @method self required(?bool $val)
 * @method self reversed(?bool $val)
 * @method self rows(?int $val)
 * @method self rowspan(?int $val)
 * @method self sandbox(?string $val, bool $state = null)
 * @method self scope(?string $val)
 * @method self selected(?bool $val)
 * @method self shape(?string $val)
 * @method self size(?int $val)
 * @method self sizes(?string $val)
 * @method self slot(?string $val)
 * @method self span(?int $val)
 * @method self spellcheck(?string $val)
 * @method self src(?string $val)
 * @method self srcdoc(?string $val)
 * @method self srclang(?string $val)
 * @method self srcset(?string $val)
 * @method self start(?int $val)
 * @method self step(?float $val)
 * @method self style(?string $property, string $val = null)
 * @method self tabindex(?int $val)
 * @method self target(?string $val)
 * @method self title(?string $val)
 * @method self translate(?string $val)
 * @method self type(?string $val)
 * @method self usemap(?string $val)
 * @method self value(?string $val)
 * @method self width(?int $val)
 * @method self wrap(?string $val)
 */
class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringable
{
	use Nette\SmartObject;

	/** @var array<string, mixed>  element's attributes */
	public $attrs = [];

	/** 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<int, HtmlStringable|string> nodes */
	protected $children = [];

	/** element's name */
	private string $name = '';

	private bool $isEmpty = false;


	/**
	 * Constructs new HTML element.
	 * @param  array|string $attrs element's attributes or plain text content
	 */
	public static function el(?string $name = null, array|string|null $attrs = null): static
	{
		$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): static
	{
		return (new static)->setHtml($html);
	}


	/**
	 * Returns an object representing plain text.
	 */
	public static function fromText(string $text): static
	{
		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.
	 */
	final public function setName(string $name, ?bool $isEmpty = null): static
	{
		$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.
	 */
	public function addAttributes(array $attrs): static
	{
		$this->attrs = array_merge($this->attrs, $attrs);
		return $this;
	}


	/**
	 * Appends value to element's attribute.
	 */
	public function appendAttribute(string $name, mixed $value, mixed $option = true): static
	{
		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.
	 */
	public function setAttribute(string $name, mixed $value): static
	{
		$this->attrs[$name] = $value;
		return $this;
	}


	/**
	 * Returns element's attribute.
	 */
	public function getAttribute(string $name): mixed
	{
		return $this->attrs[$name] ?? null;
	}


	/**
	 * Unsets element's attribute.
	 */
	public function removeAttribute(string $name): static
	{
		unset($this->attrs[$name]);
		return $this;
	}


	/**
	 * Unsets element's attributes.
	 */
	public function removeAttributes(array $attributes): static
	{
		foreach ($attributes as $name) {
			unset($this->attrs[$name]);
		}

		return $this;
	}


	/**
	 * Overloaded setter for element's attribute.
	 */
	final public function __set(string $name, mixed $value): void
	{
		$this->attrs[$name] = $value;
	}


	/**
	 * Overloaded getter for element's attribute.
	 */
	final public function &__get(string $name): mixed
	{
		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.
	 */
	final public function __call(string $m, array $args): mixed
	{
		$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.
	 */
	final public function href(string $path, array $query = []): static
	{
		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'.
	 */
	public function data(string $name, mixed $value = null): static
	{
		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.
	 */
	final public function setHtml(mixed $html): static
	{
		$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.
	 */
	final public function setText(mixed $text): static
	{
		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.
	 */
	final public function addHtml(mixed $child): static
	{
		return $this->insert(null, $child);
	}


	/**
	 * Appends plain-text string to element content.
	 */
	public function addText(mixed $text): static
	{
		if (!$text instanceof HtmlStringable) {
			$text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
		}

		return $this->insert(null, $text);
	}


	/**
	 * Creates and adds a new Html child.
	 */
	final public function create(string $name, array|string|null $attrs = null): static
	{
		$this->insert(null, $child = static::el($name, $attrs));
		return $child;
	}


	/**
	 * Inserts child node.
	 */
	public function insert(?int $index, HtmlStringable|string $child, bool $replace = false): static
	{
		$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, replace: true);
	}


	/**
	 * Returns child node (\ArrayAccess implementation).
	 * @param  int  $index
	 */
	final public function offsetGet($index): HtmlStringable|string
	{
		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<int, HtmlStringable|string>
	 */
	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
	{
		return $this->render();
	}


	/**
	 * Returns element's start tag.
	 */
	final public function startTag(): string
	{
		return $this->name
			? '<' . $this->name . $this->attributes() . '>'
			: '';
	}


	/**
	 * Returns element's end tag.
	 */
	final public function endTag(): string
	{
		return $this->name && !$this->isEmpty ? '</' . $this->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) {
				$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 = str_contains($value, '"') ? "'" : '"';
			$s .= ' ' . $key . '=' . $q
				. str_replace(
					['&', $q, '<'],
					['&amp;', $q === '"' ? '&quot;' : '&#39;', '<'],
					$value,
				)
				. (str_contains($value, '`') && strpbrk($value, ' <>"\'') === false ? ' ' : '')
				. $q;
		}

		$s = str_replace('@', '&#64;', $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;
			}
		}
	}
}
ArrayHash.php000064400000003561151246271750007156 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * Provides objects to work as array.
 * @template T
 * @implements \IteratorAggregate<array-key, T>
 * @implements \ArrayAccess<array-key, T>
 */
class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \IteratorAggregate
{
	/**
	 * Transforms array to ArrayHash.
	 * @param  array<T>  $array
	 */
	public static function from(array $array, bool $recursive = true): static
	{
		$obj = new static;
		foreach ($array as $key => $value) {
			$obj->$key = $recursive && is_array($value)
				? static::from($value)
				: $value;
		}

		return $obj;
	}


	/**
	 * Returns an iterator over all items.
	 * @return \Iterator<array-key, T>
	 */
	public function &getIterator(): \Iterator
	{
		foreach ((array) $this as $key => $foo) {
			yield $key => $this->$key;
		}
	}


	/**
	 * Returns items count.
	 */
	public function count(): int
	{
		return count((array) $this);
	}


	/**
	 * Replaces or appends a item.
	 * @param  array-key  $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.', get_debug_type($key)));
		}

		$this->$key = $value;
	}


	/**
	 * Returns a item.
	 * @param  array-key  $key
	 * @return T
	 */
	#[\ReturnTypeWillChange]
	public function offsetGet($key)
	{
		return $this->$key;
	}


	/**
	 * Determines whether a item exists.
	 * @param  array-key  $key
	 */
	public function offsetExists($key): bool
	{
		return isset($this->$key);
	}


	/**
	 * Removes the element from this list.
	 * @param  array-key  $key
	 */
	public function offsetUnset($key): void
	{
		unset($this->$key);
	}
}
Arrays.php000064400000032406151246271750006535 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use JetBrains\PhpStorm\Language;
use Nette;
use function is_array, is_int, is_object, count;


/**
 * Array tools library.
 */
class Arrays
{
	use Nette\StaticClass;

	/**
	 * Returns item from array. If it does not exist, it throws an exception, unless a default value is set.
	 * @template T
	 * @param  array<T>  $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, string|int|array $key, mixed $default = null): mixed
	{
		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<T>  $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, string|int|array $key): mixed
	{
		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<T1>  $array1
	 * @param  array<T2>  $array2
	 * @return array<T1|T2>
	 */
	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.
	 */
	public static function getKeyOffset(array $array, string|int $key): ?int
	{
		return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), strict: 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.
	 */
	public static function contains(array $array, mixed $value): bool
	{
		return in_array($value, $array, true);
	}


	/**
	 * Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
	 * The $predicate has the signature `function (mixed $value, int|string $key, array $array): bool`.
	 * @template T
	 * @param  array<T>  $array
	 * @return ?T
	 */
	public static function first(array $array, ?callable $predicate = null, ?callable $else = null): mixed
	{
		$key = self::firstKey($array, $predicate);
		return $key === null
			? ($else ? $else() : null)
			: $array[$key];
	}


	/**
	 * Returns the last item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
	 * The $predicate has the signature `function (mixed $value, int|string $key, array $array): bool`.
	 * @template T
	 * @param  array<T>  $array
	 * @return ?T
	 */
	public static function last(array $array, ?callable $predicate = null, ?callable $else = null): mixed
	{
		$key = self::lastKey($array, $predicate);
		return $key === null
			? ($else ? $else() : null)
			: $array[$key];
	}


	/**
	 * Returns the key of first item (matching the specified predicate if given) or null if there is no such item.
	 * The $predicate has the signature `function (mixed $value, int|string $key, array $array): bool`.
	 */
	public static function firstKey(array $array, ?callable $predicate = null): int|string|null
	{
		if (!$predicate) {
			return array_key_first($array);
		}
		foreach ($array as $k => $v) {
			if ($predicate($v, $k, $array)) {
				return $k;
			}
		}
		return null;
	}


	/**
	 * Returns the key of last item (matching the specified predicate if given) or null if there is no such item.
	 * The $predicate has the signature `function (mixed $value, int|string $key, array $array): bool`.
	 */
	public static function lastKey(array $array, ?callable $predicate = null): int|string|null
	{
		return $predicate
			? self::firstKey(array_reverse($array, preserve_keys: true), $predicate)
			: array_key_last($array);
	}


	/**
	 * 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.
	 */
	public static function insertBefore(array &$array, string|int|null $key, array $inserted): void
	{
		$offset = $key === null ? 0 : (int) self::getKeyOffset($array, $key);
		$array = array_slice($array, 0, $offset, preserve_keys: true)
			+ $inserted
			+ array_slice($array, $offset, count($array), preserve_keys: 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.
	 */
	public static function insertAfter(array &$array, string|int|null $key, array $inserted): void
	{
		if ($key === null || ($offset = self::getKeyOffset($array, $key)) === null) {
			$offset = count($array) - 1;
		}

		$array = array_slice($array, 0, $offset + 1, preserve_keys: true)
			+ $inserted
			+ array_slice($array, $offset + 1, count($array), preserve_keys: true);
	}


	/**
	 * Renames key in array.
	 */
	public static function renameKey(array &$array, string|int $oldKey, string|int $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,
		bool|int $invert = false,
	): array
	{
		$flags = $invert ? PREG_GREP_INVERT : 0;
		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.
	 * @return ($value is list ? true : false)
	 */
	public static function isList(mixed $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
	 */
	public static function associate(array $array, $path): array|\stdClass
	{
		$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.
	 */
	public static function normalize(array $array, mixed $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<T>  $array
	 * @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, string|int $key, mixed $default = null): mixed
	{
		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 function,
	 * which has the signature `function ($value, $key, array $array): bool`.
	 * @template K
	 * @template V
	 * @param  iterable<K, V> $array
	 * @param  callable(V, K, ($array is array ? array<K, V> : iterable<K, V>)): bool $predicate
	 */
	public static function some(iterable $array, callable $predicate): bool
	{
		foreach ($array as $k => $v) {
			if ($predicate($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`.
	 * @template K
	 * @template V
	 * @param  iterable<K, V> $array
	 * @param  callable(V, K, ($array is array ? array<K, V> : iterable<K, V>)): bool $predicate
	 */
	public static function every(iterable $array, callable $predicate): bool
	{
		foreach ($array as $k => $v) {
			if (!$predicate($v, $k, $array)) {
				return false;
			}
		}

		return true;
	}


	/**
	 * Returns a new array containing all key-value pairs matching the given $predicate.
	 * The callback has the signature `function (mixed $value, int|string $key, array $array): bool`.
	 * @template K of array-key
	 * @template V
	 * @param  array<K, V> $array
	 * @param  callable(V, K, array<K, V>): bool $predicate
	 * @return array<K, V>
	 */
	public static function filter(array $array, callable $predicate): array
	{
		$res = [];
		foreach ($array as $k => $v) {
			if ($predicate($v, $k, $array)) {
				$res[$k] = $v;
			}
		}
		return $res;
	}


	/**
	 * Returns an array containing the original keys and results of applying the given transform function to each element.
	 * The function has signature `function ($value, $key, array $array): mixed`.
	 * @template K of array-key
	 * @template V
	 * @template R
	 * @param  iterable<K, V> $array
	 * @param  callable(V, K, ($array is array ? array<K, V> : iterable<K, V>)): R $transformer
	 * @return array<K, R>
	 */
	public static function map(iterable $array, callable $transformer): array
	{
		$res = [];
		foreach ($array as $k => $v) {
			$res[$k] = $transformer($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 $object): object
	{
		foreach ($array as $k => $v) {
			$object->$k = $v;
		}

		return $object;
	}


	/**
	 * Converts value to array key.
	 */
	public static function toKey(mixed $value): int|string
	{
		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;
	}
}
Reflection.php000064400000020574151246271750007371 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * PHP reflection helpers.
 */
final class Reflection
{
	use Nette\StaticClass;

	/** @deprecated use Nette\Utils\Validator::isBuiltinType() */
	public static function isBuiltinType(string $type): bool
	{
		return Validators::isBuiltinType($type);
	}


	/** @deprecated use Nette\Utils\Validator::isClassKeyword() */
	public static function isClassKeyword(string $name): bool
	{
		return Validators::isClassKeyword($name);
	}


	/** @deprecated use native ReflectionParameter::getDefaultValue() */
	public static function getParameterDefaultValue(\ReflectionParameter $param): mixed
	{
		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<string, class-string> 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 = \PhpToken::tokenize($code, TOKEN_PARSE);
		} catch (\ParseError $e) {
			trigger_error($e->getMessage(), E_USER_NOTICE);
			$tokens = [];
		}

		$namespace = $class = null;
		$classLevel = $level = 0;
		$res = $uses = [];

		$nameTokens = [T_STRING, T_NS_SEPARATOR, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED];

		while ($token = current($tokens)) {
			next($tokens);
			switch ($token->id) {
				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 ord('{'):
					$level++;
					break;

				case ord('}'):
					if ($level === $classLevel) {
						$class = $classLevel = 0;
					}

					$level--;
			}
		}

		return $res;
	}


	private static function fetch(array &$tokens, string|int|array $take): ?string
	{
		$res = null;
		while ($token = current($tokens)) {
			if ($token->is($take)) {
				$res .= $token->text;
			} elseif (!$token->is([T_DOC_COMMENT, T_WHITESPACE, T_COMMENT])) {
				break;
			}

			next($tokens);
		}

		return $res;
	}
}
Type.php000064400000014554151246271750006221 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * PHP type reflection.
 */
final class Type
{
	/** @var array<int, string|self> */
	private array $types;
	private bool $simple;
	private string $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.
	 */
	public static function fromReflection(
		\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $reflection,
	): ?self
	{
		$type = $reflection instanceof \ReflectionFunctionAbstract
			? $reflection->getReturnType() ?? (PHP_VERSION_ID >= 80100 && $reflection instanceof \ReflectionMethod ? $reflection->getTentativeReturnType() : null)
			: $reflection->getType();

		return $type ? self::fromReflectionType($type, $reflection, asObject: true) : null;
	}


	private static function fromReflectionType(\ReflectionType $type, $of, bool $asObject): self|string
	{
		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(fn($t) => self::fromReflectionType($t, $of, asObject: 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.
	 */
	public static function resolve(
		string $type,
		\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $of,
	): string
	{
		$lower = strtolower($type);
		if ($of instanceof \ReflectionFunction) {
			return $type;
		} elseif ($lower === 'self') {
			return $of->getDeclaringClass()->name;
		} elseif ($lower === 'static') {
			return ($of instanceof ReflectionMethod ? $of->getOriginalClass() : $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, strict: 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<int, string|string[]>
	 */
	public function getNames(): array
	{
		return array_map(fn($t) => $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(fn($t) => $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, fn($t) => $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, fn($t) => $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,
			fn($type) => Arrays::some(
				$subtypes,
				fn($subtype) => Validators::isBuiltinType($type)
					? strcasecmp($type, $subtype) === 0
					: is_a($subtype, $type, allow_string: true)
			)
		);
	}
}
Paginator.php000064400000010611151246271750007212 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * Paginating math.
 *
 * @property   int $page
 * @property-read int $firstPage
 * @property-read int|null $lastPage
 * @property-read int<0,max> $firstItemOnPage
 * @property-read int<0,max> $lastItemOnPage
 * @property   int $base
 * @property-read bool $first
 * @property-read bool $last
 * @property-read int<0,max>|null $pageCount
 * @property   positive-int $itemsPerPage
 * @property   int<0,max>|null $itemCount
 * @property-read int<0,max> $offset
 * @property-read int<0,max>|null $countdownOffset
 * @property-read int<0,max> $length
 */
class Paginator
{
	use Nette\SmartObject;

	private int $base = 1;

	/** @var positive-int */
	private int $itemsPerPage = 1;

	private int $page = 1;

	/** @var int<0, max>|null */
	private ?int $itemCount = null;


	/**
	 * Sets current page number.
	 */
	public function setPage(int $page): static
	{
		$this->page = $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
	 * @return int<0, max>
	 */
	public function getFirstItemOnPage(): int
	{
		return $this->itemCount !== 0
			? $this->offset + 1
			: 0;
	}


	/**
	 * Returns the sequence number of the last element on the page
	 * @return int<0, max>
	 */
	public function getLastItemOnPage(): int
	{
		return $this->offset + $this->length;
	}


	/**
	 * Sets first page (base) number.
	 */
	public function setBase(int $base): static
	{
		$this->base = $base;
		return $this;
	}


	/**
	 * Returns first page (base) number.
	 */
	public function getBase(): int
	{
		return $this->base;
	}


	/**
	 * Returns zero-based page number.
	 * @return int<0, max>
	 */
	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.
	 * @return int<0, max>|null
	 */
	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.
	 */
	public function setItemsPerPage(int $itemsPerPage): static
	{
		$this->itemsPerPage = max(1, $itemsPerPage);
		return $this;
	}


	/**
	 * Returns the number of items to display on a single page.
	 * @return positive-int
	 */
	public function getItemsPerPage(): int
	{
		return $this->itemsPerPage;
	}


	/**
	 * Sets the total number of items.
	 */
	public function setItemCount(?int $itemCount = null): static
	{
		$this->itemCount = $itemCount === null ? null : max(0, $itemCount);
		return $this;
	}


	/**
	 * Returns the total number of items.
	 * @return int<0, max>|null
	 */
	public function getItemCount(): ?int
	{
		return $this->itemCount;
	}


	/**
	 * Returns the absolute index of the first item on current page.
	 * @return int<0, max>
	 */
	public function getOffset(): int
	{
		return $this->getPageIndex() * $this->itemsPerPage;
	}


	/**
	 * Returns the absolute index of the first item on current page in countdown paging.
	 * @return int<0, max>|null
	 */
	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.
	 * @return int<0, max>
	 */
	public function getLength(): int
	{
		return $this->itemCount === null
			? $this->itemsPerPage
			: min($this->itemsPerPage, $this->itemCount - $this->getPageIndex() * $this->itemsPerPage);
	}
}
ImageColor.php000064400000003151151246271750007310 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * Represent RGB color (0..255) with opacity (0..1).
 */
class ImageColor
{
	public static function rgb(int $red, int $green, int $blue, float $opacity = 1): self
	{
		return new self($red, $green, $blue, $opacity);
	}


	/**
	 * Accepts formats #RRGGBB, #RRGGBBAA, #RGB, #RGBA
	 */
	public static function hex(string $hex): self
	{
		$hex = ltrim($hex, '#');
		$len = strlen($hex);
		if ($len === 3 || $len === 4) {
			return new self(
				(int) hexdec($hex[0]) * 17,
				(int) hexdec($hex[1]) * 17,
				(int) hexdec($hex[2]) * 17,
				(int) hexdec($hex[3] ?? 'F') * 17 / 255,
			);
		} elseif ($len === 6 || $len === 8) {
			return new self(
				(int) hexdec($hex[0] . $hex[1]),
				(int) hexdec($hex[2] . $hex[3]),
				(int) hexdec($hex[4] . $hex[5]),
				(int) hexdec(($hex[6] ?? 'F') . ($hex[7] ?? 'F')) / 255,
			);
		} else {
			throw new Nette\InvalidArgumentException('Invalid hex color format.');
		}
	}


	private function __construct(
		public int $red,
		public int $green,
		public int $blue,
		public float $opacity = 1,
	) {
		$this->red = max(0, min(255, $red));
		$this->green = max(0, min(255, $green));
		$this->blue = max(0, min(255, $blue));
		$this->opacity = max(0, min(1, $opacity));
	}


	public function toRGBA(): array
	{
		return [
			max(0, min(255, $this->red)),
			max(0, min(255, $this->green)),
			max(0, min(255, $this->blue)),
			max(0, min(127, (int) round(127 - $this->opacity * 127))),
		];
	}
}
Strings.php000064400000053334151246271750006730 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use JetBrains\PhpStorm\Language;
use Nette;
use function is_array, is_object, strlen;


/**
 * String tools library.
 */
class Strings
{
	use Nette\StaticClass;

	public const TrimCharacters = " \t\n\r\0\x0B\u{A0}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{200B}";

	/** @deprecated use Strings::TrimCharacters */
	public const TRIM_CHARACTERS = self::TrimCharacters;


	/**
	 * @deprecated use Nette\Utils\Validator::isUnicode()
	 */
	public static function checkEncoding(string $s): bool
	{
		return $s === self::fixEncoding($s);
	}


	/**
	 * Removes all invalid UTF-8 characters from a string.
	 */
	public static function fixEncoding(string $s): string
	{
		// removes xD800-xDFFF, x110000 and higher
		return htmlspecialchars_decode(htmlspecialchars($s, ENT_NOQUOTES | ENT_IGNORE, 'UTF-8'), ENT_NOQUOTES);
	}


	/**
	 * Returns a specific character in UTF-8 from code point (number in range 0x0000..D7FF or 0xE000..10FFFF).
	 * @throws Nette\InvalidArgumentException if code point is not in valid range
	 */
	public static function chr(int $code): string
	{
		if ($code < 0 || ($code >= 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));
	}


	/**
	 * Returns a code point of specific character in UTF-8 (number in range 0x0000..D7FF or 0xE000..10FFFF).
	 */
	public static function ord(string $c): int
	{
		if (!extension_loaded('iconv')) {
			throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.');
		}

		$tmp = iconv('UTF-8', 'UTF-32BE//IGNORE', $c);
		if (!$tmp) {
			throw new Nette\InvalidArgumentException('Invalid UTF-8 character "' . ($c === '' ? '' : '\x' . strtoupper(bin2hex($c))) . '".');
		}

		return unpack('N', $tmp)[1];
	}


	/**
	 * @deprecated use str_starts_with()
	 */
	public static function startsWith(string $haystack, string $needle): bool
	{
		return str_starts_with($haystack, $needle);
	}


	/**
	 * @deprecated use str_ends_with()
	 */
	public static function endsWith(string $haystack, string $needle): bool
	{
		return str_ends_with($haystack, $needle);
	}


	/**
	 * @deprecated use str_contains()
	 */
	public static function contains(string $haystack, string $needle): bool
	{
		return str_contains($haystack, $needle);
	}


	/**
	 * 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::unixNewLines($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;
	}


	/** @deprecated use Strings::unixNewLines() */
	public static function normalizeNewLines(string $s): string
	{
		return self::unixNewLines($s);
	}


	/**
	 * Converts line endings to \n used on Unix-like systems.
	 * Line endings are: \n, \r, \r\n, U+2028 line separator, U+2029 paragraph separator.
	 */
	public static function unixNewLines(string $s): string
	{
		return preg_replace("~\r\n?|\u{2028}|\u{2029}~", "\n", $s);
	}


	/**
	 * Converts line endings to platform-specific, i.e. \r\n on Windows and \n elsewhere.
	 * Line endings are: \n, \r, \r\n, U+2028 line separator, U+2029 paragraph separator.
	 */
	public static function platformNewLines(string $s): string
	{
		return preg_replace("~\r\n?|\n|\u{2028}|\u{2029}~", PHP_EOL, $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 match (true) {
			extension_loaded('mbstring') => mb_strlen($s, 'UTF-8'),
			extension_loaded('iconv') => iconv_strlen($s, 'UTF-8'),
			default => strlen(@utf8_decode($s)), // deprecated
		};
	}


	/**
	 * 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::TrimCharacters): 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.
	 * @param  non-empty-string  $pad
	 */
	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.
	 * @param  non-empty-string  $pad
	 */
	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);
	}


	/**
	 * Divides the string into arrays according to the regular expression. Expressions in parentheses will be captured and returned as well.
	 */
	public static function split(
		string $subject,
		#[Language('RegExp')]
		string $pattern,
		bool|int $captureOffset = false,
		bool $skipEmpty = false,
		int $limit = -1,
		bool $utf8 = false,
	): array
	{
		$flags = is_int($captureOffset)  // back compatibility
			? $captureOffset
			: ($captureOffset ? PREG_SPLIT_OFFSET_CAPTURE : 0) | ($skipEmpty ? PREG_SPLIT_NO_EMPTY : 0);

		$pattern .= $utf8 ? 'u' : '';
		$m = self::pcre('preg_split', [$pattern, $subject, $limit, $flags | PREG_SPLIT_DELIM_CAPTURE]);
		return $utf8 && $captureOffset
			? self::bytesToChars($subject, [$m])[0]
			: $m;

	}


	/**
	 * Searches the string for the part matching the regular expression and returns
	 * an array with the found expression and individual subexpressions, or `null`.
	 */
	public static function match(
		string $subject,
		#[Language('RegExp')]
		string $pattern,
		bool|int $captureOffset = false,
		int $offset = 0,
		bool $unmatchedAsNull = false,
		bool $utf8 = false,
	): ?array
	{
		$flags = is_int($captureOffset) // back compatibility
			? $captureOffset
			: ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0);

		if ($utf8) {
			$offset = strlen(self::substring($subject, 0, $offset));
			$pattern .= 'u';
		}

		if ($offset > strlen($subject)) {
			return null;
		} elseif (!self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])) {
			return null;
		} elseif ($utf8 && $captureOffset) {
			return self::bytesToChars($subject, [$m])[0];
		} else {
			return $m;
		}
	}


	/**
	 * Searches the string for all occurrences matching the regular expression and
	 * returns an array of arrays containing the found expression and each subexpression.
	 */
	public static function matchAll(
		string $subject,
		#[Language('RegExp')]
		string $pattern,
		bool|int $captureOffset = false,
		int $offset = 0,
		bool $unmatchedAsNull = false,
		bool $patternOrder = false,
		bool $utf8 = false,
	): array
	{
		$flags = is_int($captureOffset) // back compatibility
			? $captureOffset
			: ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) | ($patternOrder ? PREG_PATTERN_ORDER : 0);

		if ($utf8) {
			$offset = strlen(self::substring($subject, 0, $offset));
			$pattern .= 'u';
		}

		if ($offset > strlen($subject)) {
			return [];
		}

		self::pcre('preg_match_all', [
			$pattern, $subject, &$m,
			($flags & PREG_PATTERN_ORDER) ? $flags : ($flags | PREG_SET_ORDER),
			$offset,
		]);
		return $utf8 && $captureOffset
			? self::bytesToChars($subject, $m)
			: $m;

	}


	/**
	 * Replaces all occurrences matching regular expression $pattern which can be string or array in the form `pattern => replacement`.
	 */
	public static function replace(
		string $subject,
		#[Language('RegExp')]
		string|array $pattern,
		string|callable $replacement = '',
		int $limit = -1,
		bool $captureOffset = false,
		bool $unmatchedAsNull = false,
		bool $utf8 = false,
	): string
	{
		if (is_object($replacement) || is_array($replacement)) {
			if (!is_callable($replacement, false, $textual)) {
				throw new Nette\InvalidStateException("Callback '$textual' is not callable.");
			}

			$flags = ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0);
			if ($utf8) {
				$pattern .= 'u';
				if ($captureOffset) {
					$replacement = fn($m) => $replacement(self::bytesToChars($subject, [$m])[0]);
				}
			}

			return self::pcre('preg_replace_callback', [$pattern, $replacement, $subject, $limit, 0, $flags]);

		} elseif (is_array($pattern) && is_string(key($pattern))) {
			$replacement = array_values($pattern);
			$pattern = array_keys($pattern);
		}

		if ($utf8) {
			$pattern = array_map(fn($item) => $item . 'u', (array) $pattern);
		}

		return self::pcre('preg_replace', [$pattern, $replacement, $subject, $limit]);
	}


	private static function bytesToChars(string $s, array $groups): array
	{
		$lastBytes = $lastChars = 0;
		foreach ($groups as &$matches) {
			foreach ($matches as &$match) {
				if ($match[1] > $lastBytes) {
					$lastChars += self::length(substr($s, $lastBytes, $match[1] - $lastBytes));
				} elseif ($match[1] < $lastBytes) {
					$lastChars -= self::length(substr($s, $match[1], $lastBytes - $match[1]));
				}

				$lastBytes = $match[1];
				$match[1] = $lastChars;
			}
		}

		return $groups;
	}


	/** @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(preg_last_error_msg()
				. ' (pattern: ' . implode(' or ', (array) $args[0]) . ')', $code);
		}

		return $res;
	}
}
ReflectionMethod.php000064400000001454151246271750010526 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;


/**
 * ReflectionMethod preserving the original class name.
 * @internal
 */
final class ReflectionMethod extends \ReflectionMethod
{
	private \ReflectionClass $originalClass;


	public function __construct(object|string $objectOrMethod, ?string $method = null)
	{
		if (is_string($objectOrMethod) && str_contains($objectOrMethod, '::')) {
			[$objectOrMethod, $method] = explode('::', $objectOrMethod, 2);
		}
		parent::__construct($objectOrMethod, $method);
		$this->originalClass = new \ReflectionClass($objectOrMethod);
	}


	public function getOriginalClass(): \ReflectionClass
	{
		return $this->originalClass;
	}
}
Finder.php000064400000032066151246271750006505 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * Finder allows searching through directory trees using iterator.
 *
 * Finder::findFiles('*.php')
 *     ->size('> 10kB')
 *     ->from('.')
 *     ->exclude('temp');
 *
 * @implements \IteratorAggregate<string, FileInfo>
 */
class Finder implements \IteratorAggregate
{
	use Nette\SmartObject;

	/** @var array<array{string, string}> */
	private array $find = [];

	/** @var string[] */
	private array $in = [];

	/** @var \Closure[] */
	private array $filters = [];

	/** @var \Closure[] */
	private array $descentFilters = [];

	/** @var array<string|self> */
	private array $appends = [];
	private bool $childFirst = false;

	/** @var ?callable */
	private $sort;
	private int $maxDepth = -1;
	private bool $ignoreUnreadableDirs = true;


	/**
	 * Begins search for files and directories matching mask.
	 */
	public static function find(string|array $masks = ['*']): static
	{
		$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
		return (new static)->addMask($masks, 'dir')->addMask($masks, 'file');
	}


	/**
	 * Begins search for files matching mask.
	 */
	public static function findFiles(string|array $masks = ['*']): static
	{
		$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
		return (new static)->addMask($masks, 'file');
	}


	/**
	 * Begins search for directories matching mask.
	 */
	public static function findDirectories(string|array $masks = ['*']): static
	{
		$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
		return (new static)->addMask($masks, 'dir');
	}


	/**
	 * Finds files matching the specified masks.
	 */
	public function files(string|array $masks = ['*']): static
	{
		return $this->addMask((array) $masks, 'file');
	}


	/**
	 * Finds directories matching the specified masks.
	 */
	public function directories(string|array $masks = ['*']): static
	{
		return $this->addMask((array) $masks, 'dir');
	}


	private function addMask(array $masks, string $mode): static
	{
		foreach ($masks as $mask) {
			$mask = FileSystem::unixSlashes($mask);
			if ($mode === 'dir') {
				$mask = rtrim($mask, '/');
			}
			if ($mask === '' || ($mode === 'file' && str_ends_with($mask, '/'))) {
				throw new Nette\InvalidArgumentException("Invalid mask '$mask'");
			}
			if (str_starts_with($mask, '**/')) {
				$mask = substr($mask, 3);
			}
			$this->find[] = [$mask, $mode];
		}
		return $this;
	}


	/**
	 * Searches in the given directories. Wildcards are allowed.
	 */
	public function in(string|array $paths): static
	{
		$paths = is_array($paths) ? $paths : func_get_args(); // compatibility with variadic
		$this->addLocation($paths, '');
		return $this;
	}


	/**
	 * Searches recursively from the given directories. Wildcards are allowed.
	 */
	public function from(string|array $paths): static
	{
		$paths = is_array($paths) ? $paths : func_get_args(); // compatibility with variadic
		$this->addLocation($paths, '/**');
		return $this;
	}


	private function addLocation(array $paths, string $ext): void
	{
		foreach ($paths as $path) {
			if ($path === '') {
				throw new Nette\InvalidArgumentException("Invalid directory '$path'");
			}
			$path = rtrim(FileSystem::unixSlashes($path), '/');
			$this->in[] = $path . $ext;
		}
	}


	/**
	 * Lists directory's contents before the directory itself. By default, this is disabled.
	 */
	public function childFirst(bool $state = true): static
	{
		$this->childFirst = $state;
		return $this;
	}


	/**
	 * Ignores unreadable directories. By default, this is enabled.
	 */
	public function ignoreUnreadableDirs(bool $state = true): static
	{
		$this->ignoreUnreadableDirs = $state;
		return $this;
	}


	/**
	 * Set a compare function for sorting directory entries. The function will be called to sort entries from the same directory.
	 * @param  callable(FileInfo, FileInfo): int  $callback
	 */
	public function sortBy(callable $callback): static
	{
		$this->sort = $callback;
		return $this;
	}


	/**
	 * Sorts files in each directory naturally by name.
	 */
	public function sortByName(): static
	{
		$this->sort = fn(FileInfo $a, FileInfo $b): int => strnatcmp($a->getBasename(), $b->getBasename());
		return $this;
	}


	/**
	 * Adds the specified paths or appends a new finder that returns.
	 */
	public function append(string|array|null $paths = null): static
	{
		if ($paths === null) {
			return $this->appends[] = new static;
		}

		$this->appends = array_merge($this->appends, (array) $paths);
		return $this;
	}


	/********************* filtering ****************d*g**/


	/**
	 * Skips entries that matches the given masks relative to the ones defined with the in() or from() methods.
	 */
	public function exclude(string|array $masks): static
	{
		$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
		foreach ($masks as $mask) {
			$mask = FileSystem::unixSlashes($mask);
			if (!preg_match('~^/?(\*\*/)?(.+)(/\*\*|/\*|/|)$~D', $mask, $m)) {
				throw new Nette\InvalidArgumentException("Invalid mask '$mask'");
			}
			$end = $m[3];
			$re = $this->buildPattern($m[2]);
			$filter = fn(FileInfo $file): bool => ($end && !$file->isDir())
				|| !preg_match($re, FileSystem::unixSlashes($file->getRelativePathname()));

			$this->descentFilter($filter);
			if ($end !== '/*') {
				$this->filter($filter);
			}
		}

		return $this;
	}


	/**
	 * Yields only entries which satisfy the given filter.
	 * @param  callable(FileInfo): bool  $callback
	 */
	public function filter(callable $callback): static
	{
		$this->filters[] = \Closure::fromCallable($callback);
		return $this;
	}


	/**
	 * It descends only to directories that match the specified filter.
	 * @param  callable(FileInfo): bool  $callback
	 */
	public function descentFilter(callable $callback): static
	{
		$this->descentFilters[] = \Closure::fromCallable($callback);
		return $this;
	}


	/**
	 * Sets the maximum depth of entries.
	 */
	public function limitDepth(?int $depth): static
	{
		$this->maxDepth = $depth ?? -1;
		return $this;
	}


	/**
	 * Restricts the search by size. $operator accepts "[operator] [size] [unit]" example: >=10kB
	 */
	public function size(string $operator, ?int $size = null): static
	{
		if (func_num_args() === 1) { // in $operator is predicate
			if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?$#Di', $operator, $matches)) {
				throw new Nette\InvalidArgumentException('Invalid size predicate format.');
			}

			[, $operator, $size, $unit] = $matches;
			$units = ['' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9];
			$size *= $units[strtolower($unit)];
			$operator = $operator ?: '=';
		}

		return $this->filter(fn(FileInfo $file): bool => !$file->isFile() || Helpers::compare($file->getSize(), $operator, $size));
	}


	/**
	 * Restricts the search by modified time. $operator accepts "[operator] [date]" example: >1978-01-23
	 */
	public function date(string $operator, string|int|\DateTimeInterface|null $date = null): static
	{
		if (func_num_args() === 1) { // in $operator is predicate
			if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)$#Di', $operator, $matches)) {
				throw new Nette\InvalidArgumentException('Invalid date predicate format.');
			}

			[, $operator, $date] = $matches;
			$operator = $operator ?: '=';
		}

		$date = DateTime::from($date)->format('U');
		return $this->filter(fn(FileInfo $file): bool => !$file->isFile() || Helpers::compare($file->getMTime(), $operator, $date));
	}


	/********************* iterator generator ****************d*g**/


	/**
	 * Returns an array with all found files and directories.
	 * @return list<FileInfo>
	 */
	public function collect(): array
	{
		return iterator_to_array($this->getIterator(), preserve_keys: false);
	}


	/** @return \Generator<string, FileInfo> */
	public function getIterator(): \Generator
	{
		$plan = $this->buildPlan();
		foreach ($plan as $dir => $searches) {
			yield from $this->traverseDir($dir, $searches);
		}

		foreach ($this->appends as $item) {
			if ($item instanceof self) {
				yield from $item->getIterator();
			} else {
				$item = FileSystem::platformSlashes($item);
				yield $item => new FileInfo($item);
			}
		}
	}


	/**
	 * @param  array<object{pattern: string, mode: string, recursive: bool}>  $searches
	 * @param  string[]  $subdirs
	 * @return \Generator<string, FileInfo>
	 */
	private function traverseDir(string $dir, array $searches, array $subdirs = []): \Generator
	{
		if ($this->maxDepth >= 0 && count($subdirs) > $this->maxDepth) {
			return;
		} elseif (!is_dir($dir)) {
			throw new Nette\InvalidStateException(sprintf("Directory '%s' does not exist.", rtrim($dir, '/\\')));
		}

		try {
			$pathNames = new \FilesystemIterator($dir, \FilesystemIterator::FOLLOW_SYMLINKS | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::UNIX_PATHS);
		} catch (\UnexpectedValueException $e) {
			if ($this->ignoreUnreadableDirs) {
				return;
			} else {
				throw new Nette\InvalidStateException($e->getMessage());
			}
		}

		$files = $this->convertToFiles($pathNames, implode('/', $subdirs), FileSystem::isAbsolute($dir));

		if ($this->sort) {
			$files = iterator_to_array($files);
			usort($files, $this->sort);
		}

		foreach ($files as $file) {
			$pathName = $file->getPathname();
			$cache = $subSearch = [];

			if ($file->isDir()) {
				foreach ($searches as $search) {
					if ($search->recursive && $this->proveFilters($this->descentFilters, $file, $cache)) {
						$subSearch[] = $search;
					}
				}
			}

			if ($this->childFirst && $subSearch) {
				yield from $this->traverseDir($pathName, $subSearch, array_merge($subdirs, [$file->getBasename()]));
			}

			$relativePathname = FileSystem::unixSlashes($file->getRelativePathname());
			foreach ($searches as $search) {
				if (
					$file->{'is' . $search->mode}()
					&& preg_match($search->pattern, $relativePathname)
					&& $this->proveFilters($this->filters, $file, $cache)
				) {
					yield $pathName => $file;
					break;
				}
			}

			if (!$this->childFirst && $subSearch) {
				yield from $this->traverseDir($pathName, $subSearch, array_merge($subdirs, [$file->getBasename()]));
			}
		}
	}


	private function convertToFiles(iterable $pathNames, string $relativePath, bool $absolute): \Generator
	{
		foreach ($pathNames as $pathName) {
			if (!$absolute) {
				$pathName = preg_replace('~\.?/~A', '', $pathName);
			}
			$pathName = FileSystem::platformSlashes($pathName);
			yield new FileInfo($pathName, $relativePath);
		}
	}


	private function proveFilters(array $filters, FileInfo $file, array &$cache): bool
	{
		foreach ($filters as $filter) {
			$res = &$cache[spl_object_id($filter)];
			$res ??= $filter($file);
			if (!$res) {
				return false;
			}
		}

		return true;
	}


	/** @return array<string, array<object{pattern: string, mode: string, recursive: bool}>> */
	private function buildPlan(): array
	{
		$plan = $dirCache = [];
		foreach ($this->find as [$mask, $mode]) {
			$splits = [];
			if (FileSystem::isAbsolute($mask)) {
				if ($this->in) {
					throw new Nette\InvalidStateException("You cannot combine the absolute path in the mask '$mask' and the directory to search '{$this->in[0]}'.");
				}
				$splits[] = self::splitRecursivePart($mask);
			} else {
				foreach ($this->in ?: ['.'] as $in) {
					$in = strtr($in, ['[' => '[[]', ']' => '[]]']); // in path, do not treat [ and ] as a pattern by glob()
					$splits[] = self::splitRecursivePart($in . '/' . $mask);
				}
			}

			foreach ($splits as [$base, $rest, $recursive]) {
				$base = $base === '' ? '.' : $base;
				$dirs = $dirCache[$base] ??= strpbrk($base, '*?[')
					? glob($base, GLOB_NOSORT | GLOB_ONLYDIR | GLOB_NOESCAPE)
					: [strtr($base, ['[[]' => '[', '[]]' => ']'])]; // unescape [ and ]

				if (!$dirs) {
					throw new Nette\InvalidStateException(sprintf("Directory '%s' does not exist.", rtrim($base, '/\\')));
				}

				$search = (object) ['pattern' => $this->buildPattern($rest), 'mode' => $mode, 'recursive' => $recursive];
				foreach ($dirs as $dir) {
					$plan[$dir][] = $search;
				}
			}
		}

		return $plan;
	}


	/**
	 * Since glob() does not know ** wildcard, we divide the path into a part for glob and a part for manual traversal.
	 */
	private static function splitRecursivePart(string $path): array
	{
		$a = strrpos($path, '/');
		$parts = preg_split('~(?<=^|/)\*\*($|/)~', substr($path, 0, $a + 1), 2);
		return isset($parts[1])
			? [$parts[0], $parts[1] . substr($path, $a + 1), true]
			: [$parts[0], substr($path, $a + 1), false];
	}


	/**
	 * Converts wildcards to regular expression.
	 */
	private function buildPattern(string $mask): string
	{
		if ($mask === '*') {
			return '##';
		} elseif (str_starts_with($mask, './')) {
			$anchor = '^';
			$mask = substr($mask, 2);
		} else {
			$anchor = '(?:^|/)';
		}

		$pattern = strtr(
			preg_quote($mask, '#'),
			[
				'\*\*/' => '(.+/)?',
				'\*' => '[^/]*',
				'\?' => '[^/]',
				'\[\!' => '[^',
				'\[' => '[',
				'\]' => ']',
				'\-' => '-',
			],
		);
		return '#' . $anchor . $pattern . '$#D' . (defined('PHP_WINDOWS_VERSION_BUILD') ? 'i' : '');
	}
}
Helpers.php000064400000005000151246271750006664 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


class Helpers
{
	/**
	 * Executes a callback and returns the captured output as a string.
	 */
	public static function capture(callable $func): string
	{
		ob_start(function () {});
		try {
			$func();
			return ob_get_clean();
		} catch (\Throwable $e) {
			ob_end_clean();
			throw $e;
		}
	}


	/**
	 * Returns the last occurred PHP error or an empty string if no error occurred. Unlike error_get_last(),
	 * it is nit affected by the PHP directive html_errors and always returns text, not HTML.
	 */
	public static function getLastError(): string
	{
		$message = error_get_last()['message'] ?? '';
		$message = ini_get('html_errors') ? Html::htmlToText($message) : $message;
		$message = preg_replace('#^\w+\(.*?\): #', '', $message);
		return $message;
	}


	/**
	 * Converts false to null, does not change other values.
	 */
	public static function falseToNull(mixed $value): mixed
	{
		return $value === false ? null : $value;
	}


	/**
	 * Returns value clamped to the inclusive range of min and max.
	 */
	public static function clamp(int|float $value, int|float $min, int|float $max): int|float
	{
		if ($min > $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;
	}


	/**
	 * Compares two values in the same way that PHP does. Recognizes operators: >, >=, <, <=, =, ==, ===, !=, !==, <>
	 */
	public static function compare(mixed $left, string $operator, mixed $right): bool
	{
		return match ($operator) {
			'>' => $left > $right,
			'>=' => $left >= $right,
			'<' => $left < $right,
			'<=' => $left <= $right,
			'=', '==' => $left == $right,
			'===' => $left === $right,
			'!=', '<>' => $left != $right,
			'!==' => $left !== $right,
			default => throw new Nette\InvalidArgumentException("Unknown operator '$operator'"),
		};
	}
}
FileInfo.php000064400000002413151246271750006762 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * Represents the file or directory returned by the Finder.
 * @internal do not create instances directly
 */
final class FileInfo extends \SplFileInfo
{
	private string $relativePath;


	public function __construct(string $file, string $relativePath = '')
	{
		parent::__construct($file);
		$this->setInfoClass(static::class);
		$this->relativePath = $relativePath;
	}


	/**
	 * Returns the relative directory path.
	 */
	public function getRelativePath(): string
	{
		return $this->relativePath;
	}


	/**
	 * Returns the relative path including file name.
	 */
	public function getRelativePathname(): string
	{
		return ($this->relativePath === '' ? '' : $this->relativePath . DIRECTORY_SEPARATOR)
			. $this->getBasename();
	}


	/**
	 * Returns the contents of the file.
	 * @throws Nette\IOException
	 */
	public function read(): string
	{
		return FileSystem::read($this->getPathname());
	}


	/**
	 * Writes the contents to the file.
	 * @throws Nette\IOException
	 */
	public function write(string $content): void
	{
		FileSystem::write($this->getPathname(), $content);
	}
}
Iterables.php000064400000010011151246271750007172 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * Utilities for iterables.
 */
final class Iterables
{
	use Nette\StaticClass;

	/**
	 * Tests for the presence of value.
	 */
	public static function contains(iterable $iterable, mixed $value): bool
	{
		foreach ($iterable as $v) {
			if ($v === $value) {
				return true;
			}
		}
		return false;
	}


	/**
	 * Tests for the presence of key.
	 */
	public static function containsKey(iterable $iterable, mixed $key): bool
	{
		foreach ($iterable as $k => $v) {
			if ($k === $key) {
				return true;
			}
		}
		return false;
	}


	/**
	 * Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
	 * The $predicate has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
	 * @template T
	 * @param  iterable<T>  $iterable
	 * @return ?T
	 */
	public static function first(iterable $iterable, ?callable $predicate = null, ?callable $else = null): mixed
	{
		foreach ($iterable as $k => $v) {
			if (!$predicate || $predicate($v, $k, $iterable)) {
				return $v;
			}
		}
		return $else ? $else() : null;
	}


	/**
	 * Returns the key of first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
	 * The $predicate has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
	 * @template T
	 * @param  iterable<T, mixed>  $iterable
	 * @return ?T
	 */
	public static function firstKey(iterable $iterable, ?callable $predicate = null, ?callable $else = null): mixed
	{
		foreach ($iterable as $k => $v) {
			if (!$predicate || $predicate($v, $k, $iterable)) {
				return $k;
			}
		}
		return $else ? $else() : null;
	}


	/**
	 * Tests whether at least one element in the iterator passes the test implemented by the
	 * provided callback with signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
	 * @template K
	 * @template V
	 * @param  iterable<K, V> $iterable
	 * @param  callable(V, K, iterable<K, V>): bool  $predicate
	 */
	public static function some(iterable $iterable, callable $predicate): bool
	{
		foreach ($iterable as $k => $v) {
			if ($predicate($v, $k, $iterable)) {
				return true;
			}
		}
		return false;
	}


	/**
	 * Tests whether all elements in the iterator pass the test implemented by the provided function,
	 * which has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
	 * @template K
	 * @template V
	 * @param  iterable<K, V> $iterable
	 * @param  callable(V, K, iterable<K, V>): bool  $predicate
	 */
	public static function every(iterable $iterable, callable $predicate): bool
	{
		foreach ($iterable as $k => $v) {
			if (!$predicate($v, $k, $iterable)) {
				return false;
			}
		}
		return true;
	}


	/**
	 * Iterator that filters elements according to a given $predicate. Maintains original keys.
	 * The callback has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
	 * @template K
	 * @template V
	 * @param  iterable<K, V> $iterable
	 * @param  callable(V, K, iterable<K, V>): bool $predicate
	 * @return \Generator<K, V>
	 */
	public static function filter(iterable $iterable, callable $predicate): \Generator
	{
		foreach ($iterable as $k => $v) {
			if ($predicate($v, $k, $iterable)) {
				yield $k => $v;
			}
		}
	}


	/**
	 * Iterator that transforms values by calling $transformer. Maintains original keys.
	 * The callback has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
	 * @template K
	 * @template V
	 * @template R
	 * @param  iterable<K, V> $iterable
	 * @param  callable(V, K, iterable<K, V>): R $transformer
	 * @return \Generator<K, R>
	 */
	public static function map(iterable $iterable, callable $transformer): \Generator
	{
		foreach ($iterable as $k => $v) {
			yield $k => $transformer($v, $k, $iterable);
		}
	}
}
ObjectHelpers.php000064400000015544151246271750010031 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;
use Nette\MemberAccessException;


/**
 * Nette\SmartObject helpers.
 * @internal
 */
final class ObjectHelpers
{
	use Nette\StaticClass;

	/**
	 * @return never
	 * @throws MemberAccessException
	 */
	public static function strictGet(string $class, string $name): void
	{
		$rc = new \ReflectionClass($class);
		$hint = self::getSuggestion(array_merge(
			array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), fn($p) => !$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), fn($p) => !$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), fn($m) => $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.
	 * Returns 'event' if the property exists and has event like name
	 * @internal
	 */
	public static function hasProperty(string $class, string $name): bool|string
	{
		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;
	}
}
exceptions.php000064400000001407151246271750007452 0ustar00<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;


/**
 * The exception that is thrown when an image error occurs.
 */
class ImageException extends \Exception
{
}


/**
 * The exception that indicates invalid image file.
 */
class UnknownImageFileException extends ImageException
{
}


/**
 * The exception that indicates error of JSON encoding/decoding.
 */
class JsonException extends \JsonException
{
}


/**
 * The exception that indicates error of the last Regexp execution.
 */
class RegexpException extends \Exception
{
}


/**
 * The exception that indicates assertion error.
 */
class AssertionException extends \Exception
{
}