readme.md000064400000005051150250173420006323 0ustar00Nette Utility Classes ===================== [![Downloads this Month](https://img.shields.io/packagist/dm/nette/utils.svg)](https://packagist.org/packages/nette/utils) [![Tests](https://github.com/nette/utils/workflows/Tests/badge.svg?branch=master)](https://github.com/nette/utils/actions) [![Coverage Status](https://coveralls.io/repos/github/nette/utils/badge.svg?branch=master)](https://coveralls.io/github/nette/utils?branch=master) [![Latest Stable Version](https://poser.pugx.org/nette/utils/v/stable)](https://github.com/nette/utils/releases) [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/utils/blob/master/license.md) Introduction ------------ In package nette/utils you will find a set of [useful classes](https://doc.nette.org/utils) for everyday use: - [Arrays](https://doc.nette.org/utils/arrays) - manipulate arrays - [Callback](https://doc.nette.org/utils/callback) - PHP callbacks - [Date and Time](https://doc.nette.org/utils/datetime) - modify times and dates - [Filesystem](https://doc.nette.org/utils/filesystem) - copying, renaming, … - [Finder](https://doc.nette.org/utils/finder) - finds files and directories - [Helper Functions](https://doc.nette.org/utils/helpers) - [HTML elements](https://doc.nette.org/utils/html-elements) - generate HTML - [Images](https://doc.nette.org/utils/images) - crop, resize, rotate images - [JSON](https://doc.nette.org/utils/json) - encoding and decoding - [Generating Random Strings](https://doc.nette.org/utils/random) - [Paginator](https://doc.nette.org/utils/paginator) - pagination math - [PHP Reflection](https://doc.nette.org/utils/reflection) - [Strings](https://doc.nette.org/utils/strings) - useful text functions - [SmartObject](https://doc.nette.org/utils/smartobject) - PHP object enhancements - [Validation](https://doc.nette.org/utils/validators) - validate inputs - [Type](https://doc.nette.org/utils/type) - PHP data type Installation ------------ The recommended way to install is via Composer: ``` composer require nette/utils ``` - Nette Utils 4.0 is compatible with PHP 8.0 to 8.2 - Nette Utils 3.2 is compatible with PHP 7.2 to 8.2 - Nette Utils 3.1 is compatible with PHP 7.1 to 8.0 - Nette Utils 3.0 is compatible with PHP 7.1 to 8.0 - Nette Utils 2.5 is compatible with PHP 5.6 to 8.0 [Support Me](https://github.com/sponsors/dg) -------------------------------------------- Do you like Nette Utils? Are you looking forward to the new features? [![Buy me a coffee](https://files.nette.org/icons/donation-3.svg)](https://github.com/sponsors/dg) Thank you! composer.json000064400000003056150250173420007271 0ustar00{ "name": "nette/utils", "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", "keywords": ["nette", "images", "json", "password", "validation", "utility", "string", "array", "core", "slugify", "utf-8", "unicode", "paginator", "datetime"], "homepage": "https://nette.org", "license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"], "authors": [ { "name": "David Grudl", "homepage": "https://davidgrudl.com" }, { "name": "Nette Community", "homepage": "https://nette.org/contributors" } ], "require": { "php": ">=8.0 <8.3" }, "require-dev": { "nette/tester": "^2.4", "tracy/tracy": "^2.9", "phpstan/phpstan": "^1.0", "jetbrains/phpstorm-attributes": "dev-master" }, "conflict": { "nette/finder": "<3", "nette/schema": "<1.2.2" }, "suggest": { "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", "ext-json": "to use Nette\\Utils\\Json", "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", "ext-mbstring": "to use Strings::lower() etc...", "ext-xml": "to use Strings::length() etc. when mbstring is not available", "ext-gd": "to use Image", "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" }, "autoload": { "classmap": ["src/"] }, "minimum-stability": "dev", "scripts": { "phpstan": "phpstan analyse", "tester": "tester tests -s" }, "extra": { "branch-alias": { "dev-master": "4.0-dev" } } } src/Iterators/Mapper.php000064400000001131150250173420011237 0ustar00callback = $callback; } public function current(): mixed { return ($this->callback)(parent::current(), parent::key()); } } src/Iterators/CachingIterator.php000064400000005762150250173420013077 0ustar00getIterator(); } while ($iterator instanceof \IteratorAggregate); assert($iterator instanceof \Iterator); } elseif ($iterator instanceof \Iterator) { } elseif ($iterator instanceof \Traversable) { $iterator = new \IteratorIterator($iterator); } else { throw new Nette\InvalidArgumentException(sprintf('Invalid argument passed to %s; array or Traversable expected, %s given.', self::class, is_object($iterator) ? $iterator::class : gettype($iterator))); } parent::__construct($iterator, 0); } /** * Is the current element the first one? */ public function isFirst(?int $gridWidth = null): bool { return $this->counter === 1 || ($gridWidth && $this->counter !== 0 && (($this->counter - 1) % $gridWidth) === 0); } /** * Is the current element the last one? */ public function isLast(?int $gridWidth = null): bool { return !$this->hasNext() || ($gridWidth && ($this->counter % $gridWidth) === 0); } /** * Is the iterator empty? */ public function isEmpty(): bool { return $this->counter === 0; } /** * Is the counter odd? */ public function isOdd(): bool { return $this->counter % 2 === 1; } /** * Is the counter even? */ public function isEven(): bool { return $this->counter % 2 === 0; } /** * Returns the counter. */ public function getCounter(): int { return $this->counter; } /** * Returns the count of elements. */ public function count(): int { $inner = $this->getInnerIterator(); if ($inner instanceof \Countable) { return $inner->count(); } else { throw new Nette\NotSupportedException('Iterator is not countable.'); } } /** * Forwards to the next element. */ public function next(): void { parent::next(); if (parent::valid()) { $this->counter++; } } /** * Rewinds the Iterator. */ public function rewind(): void { parent::rewind(); $this->counter = parent::valid() ? 1 : 0; } /** * Returns the next key. */ public function getNextKey(): mixed { return $this->getInnerIterator()->key(); } /** * Returns the next element. */ public function getNextValue(): mixed { return $this->getInnerIterator()->current(); } } src/Translator.php000064400000000654150250173420010201 0ustar00$name ?? null; if (is_iterable($handlers)) { foreach ($handlers as $handler) { $handler(...$args); } } elseif ($handlers !== null) { throw new UnexpectedValueException("Property $class::$$name must be iterable or null, " . gettype($handlers) . ' given.'); } return null; } ObjectHelpers::strictCall($class, $name); } /** * @throws MemberAccessException */ public static function __callStatic(string $name, array $args) { ObjectHelpers::strictStaticCall(static::class, $name); } /** * @return mixed * @throws MemberAccessException if the property is not defined. */ public function &__get(string $name) { $class = static::class; if ($prop = ObjectHelpers::getMagicProperties($class)[$name] ?? null) { // property getter if (!($prop & 0b0001)) { throw new MemberAccessException("Cannot read a write-only property $class::\$$name."); } $m = ($prop & 0b0010 ? 'get' : 'is') . ucfirst($name); if ($prop & 0b10000) { $trace = debug_backtrace(0, 1)[0]; // suppose this method is called from __call() $loc = isset($trace['file'], $trace['line']) ? " in $trace[file] on line $trace[line]" : ''; trigger_error("Property $class::\$$name is deprecated, use $class::$m() method$loc.", E_USER_DEPRECATED); } if ($prop & 0b0100) { // return by reference return $this->$m(); } else { $val = $this->$m(); return $val; } } else { ObjectHelpers::strictGet($class, $name); } } /** * @throws MemberAccessException if the property is not defined or is read-only */ public function __set(string $name, mixed $value): void { $class = static::class; if (ObjectHelpers::hasProperty($class, $name)) { // unsetted property $this->$name = $value; } elseif ($prop = ObjectHelpers::getMagicProperties($class)[$name] ?? null) { // property setter if (!($prop & 0b1000)) { throw new MemberAccessException("Cannot write to a read-only property $class::\$$name."); } $m = 'set' . ucfirst($name); if ($prop & 0b10000) { $trace = debug_backtrace(0, 1)[0]; // suppose this method is called from __call() $loc = isset($trace['file'], $trace['line']) ? " in $trace[file] on line $trace[line]" : ''; trigger_error("Property $class::\$$name is deprecated, use $class::$m() method$loc.", E_USER_DEPRECATED); } $this->$m($value); } else { ObjectHelpers::strictSet($class, $name); } } /** * @throws MemberAccessException */ public function __unset(string $name): void { $class = static::class; if (!ObjectHelpers::hasProperty($class, $name)) { throw new MemberAccessException("Cannot unset the property $class::\$$name."); } } public function __isset(string $name): bool { return isset(ObjectHelpers::getMagicProperties(static::class)[$name]); } } src/compatibility.php000064400000001256150250173420010720 0ustar00 $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; } } src/Utils/Image.php000064400000052425150250173420010175 0ustar00 * $image = Image::fromFile('nette.jpg'); * $image->resize(150, 100); * $image->sharpen(); * $image->send(); * * * @method Image affine(array $affine, array $clip = null) * @method array affineMatrixConcat(array $m1, array $m2) * @method array affineMatrixGet(int $type, mixed $options = null) * @method void alphaBlending(bool $on) * @method void antialias(bool $on) * @method void arc($x, $y, $w, $h, $start, $end, $color) * @method void char(int $font, $x, $y, string $char, $color) * @method void charUp(int $font, $x, $y, string $char, $color) * @method int colorAllocate($red, $green, $blue) * @method int colorAllocateAlpha($red, $green, $blue, $alpha) * @method int colorAt($x, $y) * @method int colorClosest($red, $green, $blue) * @method int colorClosestAlpha($red, $green, $blue, $alpha) * @method int colorClosestHWB($red, $green, $blue) * @method void colorDeallocate($color) * @method int colorExact($red, $green, $blue) * @method int colorExactAlpha($red, $green, $blue, $alpha) * @method void colorMatch(Image $image2) * @method int colorResolve($red, $green, $blue) * @method int colorResolveAlpha($red, $green, $blue, $alpha) * @method void colorSet($index, $red, $green, $blue) * @method array colorsForIndex($index) * @method int colorsTotal() * @method int colorTransparent($color = null) * @method void convolution(array $matrix, float $div, float $offset) * @method void copy(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH) * @method void copyMerge(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity) * @method void copyMergeGray(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity) * @method void copyResampled(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH) * @method void copyResized(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH) * @method Image cropAuto(int $mode = -1, float $threshold = .5, int $color = -1) * @method void ellipse($cx, $cy, $w, $h, $color) * @method void fill($x, $y, $color) * @method void filledArc($cx, $cy, $w, $h, $s, $e, $color, $style) * @method void filledEllipse($cx, $cy, $w, $h, $color) * @method void filledPolygon(array $points, $numPoints, $color) * @method void filledRectangle($x1, $y1, $x2, $y2, $color) * @method void fillToBorder($x, $y, $border, $color) * @method void filter($filtertype) * @method void flip(int $mode) * @method array ftText($size, $angle, $x, $y, $col, string $fontFile, string $text, array $extrainfo = null) * @method void gammaCorrect(float $inputgamma, float $outputgamma) * @method array getClip() * @method int interlace($interlace = null) * @method bool isTrueColor() * @method void layerEffect($effect) * @method void line($x1, $y1, $x2, $y2, $color) * @method void openPolygon(array $points, int $num_points, int $color) * @method void paletteCopy(Image $source) * @method void paletteToTrueColor() * @method void polygon(array $points, $numPoints, $color) * @method array psText(string $text, $font, $size, $color, $backgroundColor, $x, $y, $space = null, $tightness = null, float $angle = null, $antialiasSteps = null) * @method void rectangle($x1, $y1, $x2, $y2, $col) * @method mixed resolution(int $res_x = null, int $res_y = null) * @method Image rotate(float $angle, $backgroundColor) * @method void saveAlpha(bool $saveflag) * @method Image scale(int $newWidth, int $newHeight = -1, int $mode = IMG_BILINEAR_FIXED) * @method void setBrush(Image $brush) * @method void setClip(int $x1, int $y1, int $x2, int $y2) * @method void setInterpolation(int $method = IMG_BILINEAR_FIXED) * @method void setPixel($x, $y, $color) * @method void setStyle(array $style) * @method void setThickness($thickness) * @method void setTile(Image $tile) * @method void string($font, $x, $y, string $s, $col) * @method void stringUp($font, $x, $y, string $s, $col) * @method void trueColorToPalette(bool $dither, $ncolors) * @method array ttfText($size, $angle, $x, $y, $color, string $fontfile, string $text) * @property-read int $width * @property-read int $height * @property-read \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 = 19, // 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 = [self::JPEG => 'jpeg', self::PNG => 'png', self::GIF => 'gif', self::WEBP => 'webp', self::AVIF => 'avif', self::BMP => 'bmp']; private \GdImage $image; /** * Returns RGB color (0..255) and transparency (0..127). */ public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array { return [ 'red' => max(0, min(255, $red)), 'green' => max(0, min(255, $green)), 'blue' => max(0, min(255, $blue)), 'alpha' => max(0, min(127, $transparency)), ]; } /** * Reads an image from a file and returns its type in $type. * @throws Nette\NotSupportedException if gd extension is not loaded * @throws UnknownImageFileException if file not found or file type is not known */ 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. * @throws Nette\NotSupportedException if gd extension is not loaded */ public static function fromBlank(int $width, int $height, ?array $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 = imagecreatetruecolor($width, $height); if ($color) { $color += ['alpha' => 0]; $color = imagecolorresolvealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']); imagealphablending($image, false); imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color); imagealphablending($image, true); } return new static($image); } /** * Returns the type of image from file. */ public static function detectTypeFromFile(string $file, &$width = null, &$height = null): ?int { [$width, $height, $type] = @getimagesize($file); // @ - files smaller than 12 bytes causes read error return isset(self::Formats[$type]) ? $type : null; } /** * Returns the type of image from string. */ public static function detectTypeFromString(string $s, &$width = null, &$height = null): ?int { [$width, $height, $type] = @getimagesizefromstring($s); // @ - strings smaller than 12 bytes causes read error return isset(self::Formats[$type]) ? $type : null; } /** * Returns the file extension for the given `Image::XXX` constant. */ public static function typeToExtension(int $type): string { if (!isset(self::Formats[$type])) { throw new Nette\InvalidArgumentException("Unsupported image type '$type'."); } return self::Formats[$type]; } /** * Returns the `Image::XXX` constant for given file extension. */ public static function extensionToType(string $extension): int { $extensions = array_flip(self::Formats) + ['jpg' => self::JPEG]; $extension = strtolower($extension); if (!isset($extensions[$extension])) { throw new Nette\InvalidArgumentException("Unsupported file extension '$extension'."); } return $extensions[$extension]; } /** * Returns the mime type for the given `Image::XXX` constant. */ public static function typeToMimeType(int $type): string { return 'image/' . self::typeToExtension($type); } /** * Wraps GD image. */ public function __construct(\GdImage $image) { $this->setImageResource($image); imagesavealpha($image, true); } /** * Returns image width. */ public function getWidth(): int { return imagesx($this->image); } /** * Returns image height. */ public function getHeight(): int { return imagesy($this->image); } /** * Sets image resource. */ 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 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, self::rgb(0, 0, 0, 127))->getImageResource(); imagecopyresampled( $newImage, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->getWidth(), $this->getHeight(), ); $this->image = $newImage; } if ($width < 0 || $height < 0) { imageflip($this->image, $width < 0 ? ($height < 0 ? IMG_FLIP_BOTH : IMG_FLIP_HORIZONTAL) : IMG_FLIP_VERTICAL); } return $this; } /** * Calculates dimensions of resized image. Width and height accept pixels or percent. * @param 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'], self::RGB(0, 0, 0, 127))->getImageResource(); imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']); $this->image = $newImage; } return $this; } /** * Calculates dimensions of cutout in image. 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 $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; } /** * Saves image to the file. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). * @throws ImageException */ public function save(string $file, ?int $quality = null, ?int $type = null): void { $type ??= self::extensionToType(pathinfo($file, PATHINFO_EXTENSION)); $this->output($type, $quality, $file); } /** * Outputs image to string. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). */ public function toString(int $type = self::JPEG, ?int $quality = null): string { return Helpers::capture(function () use ($type, $quality): 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). * @throws ImageException */ public function send(int $type = self::JPEG, ?int $quality = null): void { header('Content-Type: ' . self::typeToMimeType($type)); $this->output($type, $quality); } /** * Outputs image to browser or file. * @throws ImageException */ private function output(int $type, ?int $quality, ?string $file = null): void { switch ($type) { case self::JPEG: $quality = $quality === null ? 85 : max(0, min(100, $quality)); $success = @imagejpeg($this->image, $file, $quality); // @ is escalated to exception break; case self::PNG: $quality = $quality === null ? 9 : max(0, min(9, $quality)); $success = @imagepng($this->image, $file, $quality); // @ is escalated to exception break; case self::GIF: $success = @imagegif($this->image, $file); // @ is escalated to exception break; case self::WEBP: $quality = $quality === null ? 80 : max(0, min(100, $quality)); $success = @imagewebp($this->image, $file, $quality); // @ is escalated to exception break; case self::AVIF: $quality = $quality === null ? 30 : max(0, min(100, $quality)); $success = @imageavif($this->image, $file, $quality); // @ is escalated to exception break; case self::BMP: $success = @imagebmp($this->image, $file); // @ is escalated to exception break; default: throw new Nette\InvalidArgumentException("Unsupported image type '$type'."); } if (!$success) { throw new ImageException(Helpers::getLastError() ?: 'Unknown error'); } } /** * Call to undefined method. * @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 (is_array($value) && isset($value['red'])) { // rgb $args[$key] = imagecolorallocatealpha( $this->image, $value['red'], $value['green'], $value['blue'], $value['alpha'], ) ?: imagecolorresolvealpha( $this->image, $value['red'], $value['green'], $value['blue'], $value['alpha'], ); } } $res = $function($this->image, ...$args); return $res instanceof \GdImage ? $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.'); } } src/Utils/Arrays.php000064400000025041150250173420010406 0ustar00 $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 $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 $array1 * @param array $array2 * @return array */ public static function mergeTree(array $array1, array $array2): array { $res = $array1 + $array2; foreach (array_intersect_key($array1, $array2) as $k => $v) { if (is_array($v) && is_array($array2[$k])) { $res[$k] = self::mergeTree($v, $array2[$k]); } } return $res; } /** * Returns zero-indexed position of given array key. Returns null if key is not found. */ public static function getKeyOffset(array $array, string|int $key): ?int { return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), true)); } /** * @deprecated use getKeyOffset() */ public static function searchKey(array $array, $key): ?int { return self::getKeyOffset($array, $key); } /** * Tests an array for the presence of value. */ public static function contains(array $array, mixed $value): bool { return in_array($value, $array, true); } /** * Returns the first item from the array or null if array is empty. * @template T * @param array $array * @return ?T */ public static function first(array $array): mixed { return count($array) ? reset($array) : null; } /** * Returns the last item from the array or null if array is empty. * @template T * @param array $array * @return ?T */ public static function last(array $array): mixed { return count($array) ? end($array) : null; } /** * Inserts the contents of the $inserted array into the $array immediately after the $key. * If $key is null (or does not exist), it is inserted at the beginning. */ 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, true) + $inserted + array_slice($array, $offset, count($array), true); } /** * Inserts the contents of the $inserted array into the $array before the $key. * If $key is null (or does not exist), it is inserted at the end. */ 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, true) + $inserted + array_slice($array, $offset + 1, count($array), 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. */ 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 $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 callback with signature `function ($value, $key, array $array): bool`. */ public static function some(iterable $array, callable $callback): bool { foreach ($array as $k => $v) { if ($callback($v, $k, $array)) { return true; } } return false; } /** * Tests whether all elements in the array pass the test implemented by the provided function, * which has the signature `function ($value, $key, array $array): bool`. */ public static function every(iterable $array, callable $callback): bool { foreach ($array as $k => $v) { if (!$callback($v, $k, $array)) { return false; } } return true; } /** * Calls $callback on all elements in the array and returns the array of return values. * The callback has the signature `function ($value, $key, array $array): bool`. */ public static function map(iterable $array, callable $callback): array { $res = []; foreach ($array as $k => $v) { $res[$k] = $callback($v, $k, $array); } return $res; } /** * Invokes all callbacks and returns array of results. * @param callable[] $callbacks */ public static function invoke(iterable $callbacks, ...$args): array { $res = []; foreach ($callbacks as $k => $cb) { $res[$k] = $cb(...$args); } return $res; } /** * Invokes method on every object in an array and returns array of results. * @param object[] $objects */ public static function invokeMethod(iterable $objects, string $method, ...$args): array { $res = []; foreach ($objects as $k => $obj) { $res[$k] = $obj->$method(...$args); } return $res; } /** * Copies the elements of the $array array to the $object object and then returns it. * @template T of object * @param T $object * @return T */ public static function toObject(iterable $array, object $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; } } src/Utils/Type.php000064400000014316150250173420010071 0ustar00 */ 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, 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, 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' || $lower === 'static') { return $of->getDeclaringClass()->name; } elseif ($lower === 'parent' && $of->getDeclaringClass()->getParentClass()) { return $of->getDeclaringClass()->getParentClass()->name; } else { return $type; } } private function __construct(array $types, string $kind = '|') { $o = array_search('null', $types, true); if ($o !== false) { // null as last array_splice($types, $o, 1); $types[] = 'null'; } $this->types = $types; $this->simple = is_string($types[0]) && ($types[1] ?? 'null') === 'null'; $this->kind = count($types) > 1 ? $kind : ''; } public function __toString(): string { $multi = count($this->types) > 1; if ($this->simple) { return ($multi ? '?' : '') . $this->types[0]; } $res = []; foreach ($this->types as $type) { $res[] = $type instanceof self && $multi ? "($type)" : $type; } return implode($this->kind, $res); } /** * Returns the array of subtypes that make up the compound type as strings. * @return array */ public function getNames(): array { return array_map(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, true) ) ); } } src/Utils/Validators.php000064400000024127150250173420011261 0ustar00 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1, 'callable' => 1, 'iterable' => 1, 'void' => 1, 'null' => 1, 'mixed' => 1, 'false' => 1, 'never' => 1, 'true' => 1, ]; /** @var array */ protected static $validators = [ // PHP types 'array' => 'is_array', 'bool' => 'is_bool', 'boolean' => 'is_bool', 'float' => 'is_float', 'int' => 'is_int', 'integer' => 'is_int', 'null' => 'is_null', 'object' => 'is_object', 'resource' => 'is_resource', 'scalar' => 'is_scalar', 'string' => 'is_string', // pseudo-types 'callable' => [self::class, 'isCallable'], 'iterable' => 'is_iterable', 'list' => [Arrays::class, 'isList'], 'mixed' => [self::class, 'isMixed'], 'none' => [self::class, 'isNone'], 'number' => [self::class, 'isNumber'], 'numeric' => [self::class, 'isNumeric'], 'numericint' => [self::class, 'isNumericInt'], // string patterns 'alnum' => 'ctype_alnum', 'alpha' => 'ctype_alpha', 'digit' => 'ctype_digit', 'lower' => 'ctype_lower', 'pattern' => null, 'space' => 'ctype_space', 'unicode' => [self::class, 'isUnicode'], 'upper' => 'ctype_upper', 'xdigit' => 'ctype_xdigit', // syntax validation 'email' => [self::class, 'isEmail'], 'identifier' => [self::class, 'isPhpIdentifier'], 'uri' => [self::class, 'isUri'], 'url' => [self::class, 'isUrl'], // environment validation 'class' => 'class_exists', 'interface' => 'interface_exists', 'directory' => 'is_dir', 'file' => 'is_file', 'type' => [self::class, 'isType'], ]; /** @var array */ protected static $counters = [ 'string' => 'strlen', 'unicode' => [Strings::class, 'length'], 'array' => 'count', 'list' => 'count', 'alnum' => 'strlen', 'alpha' => 'strlen', 'digit' => 'strlen', 'lower' => 'strlen', 'space' => 'strlen', 'upper' => 'strlen', 'xdigit' => 'strlen', ]; /** * Verifies that the value is of expected types separated by pipe. * @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, 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. */ 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. */ 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. */ 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, 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. */ 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 */ 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(<< \\? (? [a-zA-Z_\x7f-\xff][\w\x7f-\xff]*) (\\ (?&name))* ) | (? (?&type) (& (?&type))+ ) | (? (?&type) | \( (?&intersection) \) ) (\| (?&upart))+ )$~xAD XX, $type); } } src/Utils/exceptions.php000064400000001407150250173420011326 0ustar00 $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, true) : $value; } return $obj; } /** * Returns an iterator over all items. * @return \Iterator */ 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 string|int $key * @param T $value */ public function offsetSet($key, $value): void { if (!is_scalar($key)) { // prevents null throw new Nette\InvalidArgumentException(sprintf('Key must be either a string or an integer, %s given.', gettype($key))); } $this->$key = $value; } /** * Returns a item. * @param string|int $key * @return T */ #[\ReturnTypeWillChange] public function offsetGet($key) { return $this->$key; } /** * Determines whether a item exists. * @param string|int $key */ public function offsetExists($key): bool { return isset($this->$key); } /** * Removes the element from this list. * @param string|int $key */ public function offsetUnset($key): void { unset($this->$key); } } src/Utils/DateTime.php000064400000005624150250173420010646 0ustar00format('Y-m-d H:i:s.u'), $time->getTimezone()); } elseif (is_numeric($time)) { if ($time <= self::YEAR) { $time += time(); } return (new static('@' . $time))->setTimezone(new \DateTimeZone(date_default_timezone_get())); } else { // textual or null return new static((string) $time); } } /** * Creates DateTime object. * @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'); } /** * Creates a copy with a modified time. */ public function modifyClone(string $modify = ''): static { $dolly = clone $this; return $modify ? $dolly->modify($modify) : $dolly; } } src/Utils/Helpers.php000064400000005000150250173420010540 0ustar00 $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'"), }; } } src/Utils/Html.php000064400000046030150250173420010052 0ustar00 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 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, 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 */ 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 ? '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, '<'], ['&', $q === '"' ? '"' : ''', '<'], $value, ) . (str_contains($value, '`') && strpbrk($value, ' <>"\'') === false ? ' ' : '') . $q; } $s = str_replace('@', '@', $s); return $s; } /** * Clones all children too. */ public function __clone() { foreach ($this->children as $key => $value) { if (is_object($value)) { $this->children[$key] = clone $value; } } } } src/Utils/Reflection.php000064400000020544150250173420011242 0ustar00isDefaultValueConstant()) { $const = $orig = $param->getDefaultValueConstantName(); $pair = explode('::', $const); if (isset($pair[1])) { $pair[0] = Type::resolve($pair[0], $param); try { $rcc = new \ReflectionClassConstant($pair[0], $pair[1]); } catch (\ReflectionException $e) { $name = self::toString($param); throw new \ReflectionException("Unable to resolve constant $orig used as default value of $name.", 0, $e); } return $rcc->getValue(); } elseif (!defined($const)) { $const = substr((string) strrchr($const, '\\'), 1); if (!defined($const)) { $name = self::toString($param); throw new \ReflectionException("Unable to resolve constant $orig used as default value of $name."); } } return constant($const); } return $param->getDefaultValue(); } /** * Returns a reflection of a class or trait that contains a declaration of given property. Property can also be declared in the trait. */ public static function getPropertyDeclaringClass(\ReflectionProperty $prop): \ReflectionClass { foreach ($prop->getDeclaringClass()->getTraits() as $trait) { if ($trait->hasProperty($prop->name) // doc-comment guessing as workaround for insufficient PHP reflection && $trait->getProperty($prop->name)->getDocComment() === $prop->getDocComment() ) { return self::getPropertyDeclaringClass($trait->getProperty($prop->name)); } } return $prop->getDeclaringClass(); } /** * Returns a reflection of a method that contains a declaration of $method. * Usually, each method is its own declaration, but the body of the method can also be in the trait and under a different name. */ public static function getMethodDeclaringMethod(\ReflectionMethod $method): \ReflectionMethod { // file & line guessing as workaround for insufficient PHP reflection $decl = $method->getDeclaringClass(); if ($decl->getFileName() === $method->getFileName() && $decl->getStartLine() <= $method->getStartLine() && $decl->getEndLine() >= $method->getEndLine() ) { return $method; } $hash = [$method->getFileName(), $method->getStartLine(), $method->getEndLine()]; if (($alias = $decl->getTraitAliases()[$method->name] ?? null) && ($m = new \ReflectionMethod($alias)) && $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()] ) { return self::getMethodDeclaringMethod($m); } foreach ($decl->getTraits() as $trait) { if ($trait->hasMethod($method->name) && ($m = $trait->getMethod($method->name)) && $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()] ) { return self::getMethodDeclaringMethod($m); } } return $method; } /** * Finds out if reflection has access to PHPdoc comments. Comments may not be available due to the opcode cache. */ public static function areCommentsAvailable(): bool { static $res; return $res ?? $res = (bool) (new \ReflectionMethod(__METHOD__))->getDocComment(); } public static function toString(\Reflector $ref): string { if ($ref instanceof \ReflectionClass) { return $ref->name; } elseif ($ref instanceof \ReflectionMethod) { return $ref->getDeclaringClass()->name . '::' . $ref->name . '()'; } elseif ($ref instanceof \ReflectionFunction) { return $ref->name . '()'; } elseif ($ref instanceof \ReflectionProperty) { return self::getPropertyDeclaringClass($ref)->name . '::$' . $ref->name; } elseif ($ref instanceof \ReflectionParameter) { return '$' . $ref->name . ' in ' . self::toString($ref->getDeclaringFunction()); } else { throw new Nette\InvalidArgumentException; } } /** * Expands the name of the class to full name in the given context of given class. * Thus, it returns how the PHP parser would understand $name if it were written in the body of the class $context. * @throws Nette\InvalidArgumentException */ public static function expandClassName(string $name, \ReflectionClass $context): string { $lower = strtolower($name); if (empty($name)) { throw new Nette\InvalidArgumentException('Class name must not be empty.'); } elseif (Validators::isBuiltinType($lower)) { return $lower; } elseif ($lower === 'self' || $lower === 'static') { return $context->name; } elseif ($lower === 'parent') { return $context->getParentClass() ? $context->getParentClass()->name : 'parent'; } elseif ($name[0] === '\\') { // fully qualified name return ltrim($name, '\\'); } $uses = self::getUseStatements($context); $parts = explode('\\', $name, 2); if (isset($uses[$parts[0]])) { $parts[0] = $uses[$parts[0]]; return implode('\\', $parts); } elseif ($context->inNamespace()) { return $context->getNamespaceName() . '\\' . $name; } else { return $name; } } /** @return array of [alias => class] */ public static function getUseStatements(\ReflectionClass $class): array { if ($class->isAnonymous()) { throw new Nette\NotImplementedException('Anonymous classes are not supported.'); } static $cache = []; if (!isset($cache[$name = $class->name])) { if ($class->isInternal()) { $cache[$name] = []; } else { $code = file_get_contents($class->getFileName()); $cache = self::parseUseStatements($code, $name) + $cache; } } return $cache[$name]; } /** * Parses PHP code to [class => [alias => class, ...]] */ private static function parseUseStatements(string $code, ?string $forClass = null): array { try { $tokens = \PhpToken::tokenize($code, TOKEN_PARSE); } catch (\ParseError $e) { trigger_error($e->getMessage(), E_USER_NOTICE); $tokens = []; } $namespace = $class = $classLevel = $level = null; $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 = null; } $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; } } src/Utils/Json.php000064400000004342150250173420010057 0ustar00page = $page; return $this; } /** * Returns current page number. */ public function getPage(): int { return $this->base + $this->getPageIndex(); } /** * Returns first page number. */ public function getFirstPage(): int { return $this->base; } /** * Returns last page number. */ public function getLastPage(): ?int { return $this->itemCount === null ? null : $this->base + max(0, $this->getPageCount() - 1); } /** * Returns the sequence number of the first element on the page */ public function getFirstItemOnPage(): int { return $this->itemCount !== 0 ? $this->offset + 1 : 0; } /** * Returns the sequence number of the last element on the page */ public function getLastItemOnPage(): int { return $this->offset + $this->length; } /** * Sets first page (base) number. */ 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. */ protected function getPageIndex(): int { $index = max(0, $this->page - $this->base); return $this->itemCount === null ? $index : min($index, max(0, $this->getPageCount() - 1)); } /** * Is the current page the first one? */ public function isFirst(): bool { return $this->getPageIndex() === 0; } /** * Is the current page the last one? */ public function isLast(): bool { return $this->itemCount === null ? false : $this->getPageIndex() >= $this->getPageCount() - 1; } /** * Returns the total number of pages. */ public function getPageCount(): ?int { return $this->itemCount === null ? null : (int) ceil($this->itemCount / $this->itemsPerPage); } /** * Sets the number of items to display on a single page. */ public function setItemsPerPage(int $itemsPerPage): static { $this->itemsPerPage = max(1, $itemsPerPage); return $this; } /** * Returns the number of items to display on a single page. */ public function getItemsPerPage(): int { return $this->itemsPerPage; } /** * Sets the total number of items. */ public function setItemCount(?int $itemCount = null): static { $this->itemCount = $itemCount === null ? null : max(0, $itemCount); return $this; } /** * Returns the total number of items. */ public function getItemCount(): ?int { return $this->itemCount; } /** * Returns the absolute index of the first item on current page. */ public function getOffset(): int { return $this->getPageIndex() * $this->itemsPerPage; } /** * Returns the absolute index of the first item on current page in countdown paging. */ public function getCountdownOffset(): ?int { return $this->itemCount === null ? null : max(0, $this->itemCount - ($this->getPageIndex() + 1) * $this->itemsPerPage); } /** * Returns the number of items on current page. */ public function getLength(): int { return $this->itemCount === null ? $this->itemsPerPage : min($this->itemsPerPage, $this->itemCount - $this->getPageIndex() * $this->itemsPerPage); } } src/Utils/Finder.php000064400000031501150250173420010352 0ustar00size('> 10kB') * ->from('.') * ->exclude('temp'); * * @implements \IteratorAggregate */ class Finder implements \IteratorAggregate { use Nette\SmartObject; /** @var array */ private array $find = []; /** @var string[] */ private array $in = []; /** @var \Closure[] */ private array $filters = []; /** @var \Closure[] */ private array $descentFilters = []; /** @var array */ 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. */ public function collect(): array { return iterator_to_array($this->getIterator()); } /** @return \Generator */ 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<\stdClass{pattern: string, mode: string, recursive: bool}> $searches * @param string[] $subdirs * @return \Generator */ 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("Directory '$dir' not found."); } 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->getType() === $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> */ 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 ] $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' : ''); } } src/Utils/ArrayList.php000064400000005045150250173420011061 0ustar00 $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 */ 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); } } src/Utils/ObjectHelpers.php000064400000015544150250173420011705 0ustar00getProperties(\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; } } src/Utils/Strings.php000064400000052715150250173420010606 0ustar00= 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 function_exists('mb_strlen') ? mb_strlen($s, 'UTF-8') : strlen(utf8_decode($s)); } /** * Removes all left and right side spaces (or the characters passed as second argument) from a UTF-8 encoded string. */ public static function trim(string $s, string $charlist = self::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. */ public static function padLeft(string $s, int $length, string $pad = ' '): string { $length = max(0, $length - self::length($s)); $padLen = self::length($pad); return str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen) . $s; } /** * Pads UTF-8 string to given length by appending the $pad string to the end. */ public static function padRight(string $s, int $length, string $pad = ' '): string { $length = max(0, $length - self::length($s)); $padLen = self::length($pad); return $s . str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen); } /** * Reverses UTF-8 string. */ public static function reverse(string $s): string { if (!extension_loaded('iconv')) { throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.'); } return iconv('UTF-32LE', 'UTF-8', strrev(iconv('UTF-8', 'UTF-32BE', $s))); } /** * Returns part of $haystack before $nth occurence of $needle or returns null if the needle was not found. * Negative value means searching from the end. */ public static function before(string $haystack, string $needle, int $nth = 1): ?string { $pos = self::pos($haystack, $needle, $nth); return $pos === null ? null : substr($haystack, 0, $pos); } /** * Returns part of $haystack after $nth occurence of $needle or returns null if the needle was not found. * Negative value means searching from the end. */ public static function after(string $haystack, string $needle, int $nth = 1): ?string { $pos = self::pos($haystack, $needle, $nth); return $pos === null ? null : substr($haystack, $pos + strlen($needle)); } /** * Returns position in characters of $nth occurence of $needle in $haystack or null if the $needle was not found. * Negative value of `$nth` means searching from the end. */ public static function indexOf(string $haystack, string $needle, int $nth = 1): ?int { $pos = self::pos($haystack, $needle, $nth); return $pos === null ? null : self::length(substr($haystack, 0, $pos)); } /** * Returns position in characters of $nth occurence of $needle in $haystack or null if the needle was not found. */ private static function pos(string $haystack, string $needle, int $nth = 1): ?int { if (!$nth) { return null; } elseif ($nth > 0) { if ($needle === '') { return 0; } $pos = 0; while (($pos = strpos($haystack, $needle, $pos)) !== false && --$nth) { $pos++; } } else { $len = strlen($haystack); if ($needle === '') { return $len; } elseif ($len === 0) { return null; } $pos = $len - 1; while (($pos = strrpos($haystack, $needle, $pos - $len)) !== false && ++$nth) { $pos--; } } return Helpers::falseToNull($pos); } /** * 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; } } src/Utils/FileInfo.php000064400000002413150250173420010636 0ustar00setInfoClass(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); } } src/Utils/FileSystem.php000064400000022130150250173420011225 0ustar00getPathname()); } 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 * @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:// } } src/Utils/Random.php000064400000002113150250173420010360 0ustar00 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.'); } $res = ''; for ($i = 0; $i < $length; $i++) { $res .= $charlist[random_int(0, $chLen - 1)]; } return $res; } } src/Utils/Callback.php000064400000006712150250173420010645 0ustar00name, '}')) { return $closure; } elseif ($obj = $r->getClosureThis()) { return [$obj, $r->name]; } elseif ($class = $r->getClosureScopeClass()) { return [$class->name, $r->name]; } else { return $r->name; } } } src/HtmlStringable.php000064400000000517150250173420010765 0ustar00