123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188 |
- <?php
- /**
- * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
- * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
- *
- * Licensed under The MIT License
- * For full copyright and license information, please see the LICENSE.txt
- * Redistributions of files must retain the above copyright notice.
- *
- * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
- * @link https://cakephp.org CakePHP(tm) Project
- * @since 1.2.0
- * @license https://opensource.org/licenses/mit-license.php MIT License
- */
- namespace Cake\Utility;
- use InvalidArgumentException;
- /**
- * Text handling methods.
- */
- class Text
- {
- /**
- * Default transliterator.
- *
- * @var \Transliterator Transliterator instance.
- */
- protected static $_defaultTransliterator;
- /**
- * Default transliterator id string.
- *
- * @var string $_defaultTransliteratorId Transliterator identifier string.
- */
- protected static $_defaultTransliteratorId = 'Any-Latin; Latin-ASCII; [\u0080-\u7fff] remove';
- /**
- * Default html tags who must not be count for truncate text.
- *
- * @var array
- */
- protected static $_defaultHtmlNoCount = [
- 'style',
- 'script'
- ];
- /**
- * Generate a random UUID version 4
- *
- * Warning: This method should not be used as a random seed for any cryptographic operations.
- * Instead you should use the openssl or mcrypt extensions.
- *
- * It should also not be used to create identifiers that have security implications, such as
- * 'unguessable' URL identifiers. Instead you should use `Security::randomBytes()` for that.
- *
- * @see https://www.ietf.org/rfc/rfc4122.txt
- * @return string RFC 4122 UUID
- * @copyright Matt Farina MIT License https://github.com/lootils/uuid/blob/master/LICENSE
- */
- public static function uuid()
- {
- $random = function_exists('random_int') ? 'random_int' : 'mt_rand';
- return sprintf(
- '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
- // 32 bits for "time_low"
- $random(0, 65535),
- $random(0, 65535),
- // 16 bits for "time_mid"
- $random(0, 65535),
- // 12 bits before the 0100 of (version) 4 for "time_hi_and_version"
- $random(0, 4095) | 0x4000,
- // 16 bits, 8 bits for "clk_seq_hi_res",
- // 8 bits for "clk_seq_low",
- // two most significant bits holds zero and one for variant DCE1.1
- $random(0, 0x3fff) | 0x8000,
- // 48 bits for "node"
- $random(0, 65535),
- $random(0, 65535),
- $random(0, 65535)
- );
- }
- /**
- * Tokenizes a string using $separator, ignoring any instance of $separator that appears between
- * $leftBound and $rightBound.
- *
- * @param string $data The data to tokenize.
- * @param string $separator The token to split the data on.
- * @param string $leftBound The left boundary to ignore separators in.
- * @param string $rightBound The right boundary to ignore separators in.
- * @return array|string Array of tokens in $data or original input if empty.
- */
- public static function tokenize($data, $separator = ',', $leftBound = '(', $rightBound = ')')
- {
- if (empty($data)) {
- return [];
- }
- $depth = 0;
- $offset = 0;
- $buffer = '';
- $results = [];
- $length = mb_strlen($data);
- $open = false;
- while ($offset <= $length) {
- $tmpOffset = -1;
- $offsets = [
- mb_strpos($data, $separator, $offset),
- mb_strpos($data, $leftBound, $offset),
- mb_strpos($data, $rightBound, $offset)
- ];
- for ($i = 0; $i < 3; $i++) {
- if ($offsets[$i] !== false && ($offsets[$i] < $tmpOffset || $tmpOffset == -1)) {
- $tmpOffset = $offsets[$i];
- }
- }
- if ($tmpOffset !== -1) {
- $buffer .= mb_substr($data, $offset, $tmpOffset - $offset);
- $char = mb_substr($data, $tmpOffset, 1);
- if (!$depth && $char === $separator) {
- $results[] = $buffer;
- $buffer = '';
- } else {
- $buffer .= $char;
- }
- if ($leftBound !== $rightBound) {
- if ($char === $leftBound) {
- $depth++;
- }
- if ($char === $rightBound) {
- $depth--;
- }
- } else {
- if ($char === $leftBound) {
- if (!$open) {
- $depth++;
- $open = true;
- } else {
- $depth--;
- $open = false;
- }
- }
- }
- $tmpOffset += 1;
- $offset = $tmpOffset;
- } else {
- $results[] = $buffer . mb_substr($data, $offset);
- $offset = $length + 1;
- }
- }
- if (empty($results) && !empty($buffer)) {
- $results[] = $buffer;
- }
- if (!empty($results)) {
- return array_map('trim', $results);
- }
- return [];
- }
- /**
- * Replaces variable placeholders inside a $str with any given $data. Each key in the $data array
- * corresponds to a variable placeholder name in $str.
- * Example:
- * ```
- * Text::insert(':name is :age years old.', ['name' => 'Bob', 'age' => '65']);
- * ```
- * Returns: Bob is 65 years old.
- *
- * Available $options are:
- *
- * - before: The character or string in front of the name of the variable placeholder (Defaults to `:`)
- * - after: The character or string after the name of the variable placeholder (Defaults to null)
- * - escape: The character or string used to escape the before character / string (Defaults to `\`)
- * - format: A regex to use for matching variable placeholders. Default is: `/(?<!\\)\:%s/`
- * (Overwrites before, after, breaks escape / clean)
- * - clean: A boolean or array with instructions for Text::cleanInsert
- *
- * @param string $str A string containing variable placeholders
- * @param array $data A key => val array where each key stands for a placeholder variable name
- * to be replaced with val
- * @param array $options An array of options, see description above
- * @return string
- */
- public static function insert($str, $data, array $options = [])
- {
- $defaults = [
- 'before' => ':', 'after' => null, 'escape' => '\\', 'format' => null, 'clean' => false
- ];
- $options += $defaults;
- $format = $options['format'];
- $data = (array)$data;
- if (empty($data)) {
- return $options['clean'] ? static::cleanInsert($str, $options) : $str;
- }
- if (!isset($format)) {
- $format = sprintf(
- '/(?<!%s)%s%%s%s/',
- preg_quote($options['escape'], '/'),
- str_replace('%', '%%', preg_quote($options['before'], '/')),
- str_replace('%', '%%', preg_quote($options['after'], '/'))
- );
- }
- if (strpos($str, '?') !== false && is_numeric(key($data))) {
- $offset = 0;
- while (($pos = strpos($str, '?', $offset)) !== false) {
- $val = array_shift($data);
- $offset = $pos + strlen($val);
- $str = substr_replace($str, $val, $pos, 1);
- }
- return $options['clean'] ? static::cleanInsert($str, $options) : $str;
- }
- $dataKeys = array_keys($data);
- $hashKeys = array_map('crc32', $dataKeys);
- $tempData = array_combine($dataKeys, $hashKeys);
- krsort($tempData);
- foreach ($tempData as $key => $hashVal) {
- $key = sprintf($format, preg_quote($key, '/'));
- $str = preg_replace($key, $hashVal, $str);
- }
- $dataReplacements = array_combine($hashKeys, array_values($data));
- foreach ($dataReplacements as $tmpHash => $tmpValue) {
- $tmpValue = is_array($tmpValue) ? '' : $tmpValue;
- $str = str_replace($tmpHash, $tmpValue, $str);
- }
- if (!isset($options['format']) && isset($options['before'])) {
- $str = str_replace($options['escape'] . $options['before'], $options['before'], $str);
- }
- return $options['clean'] ? static::cleanInsert($str, $options) : $str;
- }
- /**
- * Cleans up a Text::insert() formatted string with given $options depending on the 'clean' key in
- * $options. The default method used is text but html is also available. The goal of this function
- * is to replace all whitespace and unneeded markup around placeholders that did not get replaced
- * by Text::insert().
- *
- * @param string $str String to clean.
- * @param array $options Options list.
- * @return string
- * @see \Cake\Utility\Text::insert()
- */
- public static function cleanInsert($str, array $options)
- {
- $clean = $options['clean'];
- if (!$clean) {
- return $str;
- }
- if ($clean === true) {
- $clean = ['method' => 'text'];
- }
- if (!is_array($clean)) {
- $clean = ['method' => $options['clean']];
- }
- switch ($clean['method']) {
- case 'html':
- $clean += [
- 'word' => '[\w,.]+',
- 'andText' => true,
- 'replacement' => '',
- ];
- $kleenex = sprintf(
- '/[\s]*[a-z]+=(")(%s%s%s[\s]*)+\\1/i',
- preg_quote($options['before'], '/'),
- $clean['word'],
- preg_quote($options['after'], '/')
- );
- $str = preg_replace($kleenex, $clean['replacement'], $str);
- if ($clean['andText']) {
- $options['clean'] = ['method' => 'text'];
- $str = static::cleanInsert($str, $options);
- }
- break;
- case 'text':
- $clean += [
- 'word' => '[\w,.]+',
- 'gap' => '[\s]*(?:(?:and|or)[\s]*)?',
- 'replacement' => '',
- ];
- $kleenex = sprintf(
- '/(%s%s%s%s|%s%s%s%s)/',
- preg_quote($options['before'], '/'),
- $clean['word'],
- preg_quote($options['after'], '/'),
- $clean['gap'],
- $clean['gap'],
- preg_quote($options['before'], '/'),
- $clean['word'],
- preg_quote($options['after'], '/')
- );
- $str = preg_replace($kleenex, $clean['replacement'], $str);
- break;
- }
- return $str;
- }
- /**
- * Wraps text to a specific width, can optionally wrap at word breaks.
- *
- * ### Options
- *
- * - `width` The width to wrap to. Defaults to 72.
- * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true.
- * - `indent` String to indent with. Defaults to null.
- * - `indentAt` 0 based index to start indenting at. Defaults to 0.
- *
- * @param string $text The text to format.
- * @param array|int $options Array of options to use, or an integer to wrap the text to.
- * @return string Formatted text.
- */
- public static function wrap($text, $options = [])
- {
- if (is_numeric($options)) {
- $options = ['width' => $options];
- }
- $options += ['width' => 72, 'wordWrap' => true, 'indent' => null, 'indentAt' => 0];
- if ($options['wordWrap']) {
- $wrapped = self::wordWrap($text, $options['width'], "\n");
- } else {
- $wrapped = trim(chunk_split($text, $options['width'] - 1, "\n"));
- }
- if (!empty($options['indent'])) {
- $chunks = explode("\n", $wrapped);
- for ($i = $options['indentAt'], $len = count($chunks); $i < $len; $i++) {
- $chunks[$i] = $options['indent'] . $chunks[$i];
- }
- $wrapped = implode("\n", $chunks);
- }
- return $wrapped;
- }
- /**
- * Wraps a complete block of text to a specific width, can optionally wrap
- * at word breaks.
- *
- * ### Options
- *
- * - `width` The width to wrap to. Defaults to 72.
- * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true.
- * - `indent` String to indent with. Defaults to null.
- * - `indentAt` 0 based index to start indenting at. Defaults to 0.
- *
- * @param string $text The text to format.
- * @param array|int $options Array of options to use, or an integer to wrap the text to.
- * @return string Formatted text.
- */
- public static function wrapBlock($text, $options = [])
- {
- if (is_numeric($options)) {
- $options = ['width' => $options];
- }
- $options += ['width' => 72, 'wordWrap' => true, 'indent' => null, 'indentAt' => 0];
- if (!empty($options['indentAt']) && $options['indentAt'] === 0) {
- $indentLength = !empty($options['indent']) ? strlen($options['indent']) : 0;
- $options['width'] -= $indentLength;
- return self::wrap($text, $options);
- }
- $wrapped = self::wrap($text, $options);
- if (!empty($options['indent'])) {
- $indentationLength = mb_strlen($options['indent']);
- $chunks = explode("\n", $wrapped);
- $count = count($chunks);
- if ($count < 2) {
- return $wrapped;
- }
- $toRewrap = '';
- for ($i = $options['indentAt']; $i < $count; $i++) {
- $toRewrap .= mb_substr($chunks[$i], $indentationLength) . ' ';
- unset($chunks[$i]);
- }
- $options['width'] -= $indentationLength;
- $options['indentAt'] = 0;
- $rewrapped = self::wrap($toRewrap, $options);
- $newChunks = explode("\n", $rewrapped);
- $chunks = array_merge($chunks, $newChunks);
- $wrapped = implode("\n", $chunks);
- }
- return $wrapped;
- }
- /**
- * Unicode and newline aware version of wordwrap.
- *
- * @param string $text The text to format.
- * @param int $width The width to wrap to. Defaults to 72.
- * @param string $break The line is broken using the optional break parameter. Defaults to '\n'.
- * @param bool $cut If the cut is set to true, the string is always wrapped at the specified width.
- * @return string Formatted text.
- */
- public static function wordWrap($text, $width = 72, $break = "\n", $cut = false)
- {
- $paragraphs = explode($break, $text);
- foreach ($paragraphs as &$paragraph) {
- $paragraph = static::_wordWrap($paragraph, $width, $break, $cut);
- }
- return implode($break, $paragraphs);
- }
- /**
- * Unicode aware version of wordwrap as helper method.
- *
- * @param string $text The text to format.
- * @param int $width The width to wrap to. Defaults to 72.
- * @param string $break The line is broken using the optional break parameter. Defaults to '\n'.
- * @param bool $cut If the cut is set to true, the string is always wrapped at the specified width.
- * @return string Formatted text.
- */
- protected static function _wordWrap($text, $width = 72, $break = "\n", $cut = false)
- {
- if ($cut) {
- $parts = [];
- while (mb_strlen($text) > 0) {
- $part = mb_substr($text, 0, $width);
- $parts[] = trim($part);
- $text = trim(mb_substr($text, mb_strlen($part)));
- }
- return implode($break, $parts);
- }
- $parts = [];
- while (mb_strlen($text) > 0) {
- if ($width >= mb_strlen($text)) {
- $parts[] = trim($text);
- break;
- }
- $part = mb_substr($text, 0, $width);
- $nextChar = mb_substr($text, $width, 1);
- if ($nextChar !== ' ') {
- $breakAt = mb_strrpos($part, ' ');
- if ($breakAt === false) {
- $breakAt = mb_strpos($text, ' ', $width);
- }
- if ($breakAt === false) {
- $parts[] = trim($text);
- break;
- }
- $part = mb_substr($text, 0, $breakAt);
- }
- $part = trim($part);
- $parts[] = $part;
- $text = trim(mb_substr($text, mb_strlen($part)));
- }
- return implode($break, $parts);
- }
- /**
- * Highlights a given phrase in a text. You can specify any expression in highlighter that
- * may include the \1 expression to include the $phrase found.
- *
- * ### Options:
- *
- * - `format` The piece of HTML with that the phrase will be highlighted
- * - `html` If true, will ignore any HTML tags, ensuring that only the correct text is highlighted
- * - `regex` A custom regex rule that is used to match words, default is '|$tag|iu'
- * - `limit` A limit, optional, defaults to -1 (none)
- *
- * @param string $text Text to search the phrase in.
- * @param string|array $phrase The phrase or phrases that will be searched.
- * @param array $options An array of HTML attributes and options.
- * @return string The highlighted text
- * @link https://book.cakephp.org/3.0/en/core-libraries/text.html#highlighting-substrings
- */
- public static function highlight($text, $phrase, array $options = [])
- {
- if (empty($phrase)) {
- return $text;
- }
- $defaults = [
- 'format' => '<span class="highlight">\1</span>',
- 'html' => false,
- 'regex' => '|%s|iu',
- 'limit' => -1,
- ];
- $options += $defaults;
- $html = $format = $limit = null;
- /**
- * @var bool $html
- * @var string|array $format
- * @var int $limit
- */
- extract($options);
- if (is_array($phrase)) {
- $replace = [];
- $with = [];
- foreach ($phrase as $key => $segment) {
- $segment = '(' . preg_quote($segment, '|') . ')';
- if ($html) {
- $segment = "(?![^<]+>)$segment(?![^<]+>)";
- }
- $with[] = is_array($format) ? $format[$key] : $format;
- $replace[] = sprintf($options['regex'], $segment);
- }
- return preg_replace($replace, $with, $text, $limit);
- }
- $phrase = '(' . preg_quote($phrase, '|') . ')';
- if ($html) {
- $phrase = "(?![^<]+>)$phrase(?![^<]+>)";
- }
- return preg_replace(sprintf($options['regex'], $phrase), $format, $text, $limit);
- }
- /**
- * Strips given text of all links (<a href=....).
- *
- * *Warning* This method is not an robust solution in preventing XSS
- * or malicious HTML.
- *
- * @param string $text Text
- * @return string The text without links
- * @deprecated 3.2.12 This method will be removed in 4.0.0
- */
- public static function stripLinks($text)
- {
- deprecationWarning('This method will be removed in 4.0.0.');
- do {
- $text = preg_replace('#</?a([/\s][^>]*)?(>|$)#i', '', $text, -1, $count);
- } while ($count);
- return $text;
- }
- /**
- * Truncates text starting from the end.
- *
- * Cuts a string to the length of $length and replaces the first characters
- * with the ellipsis if the text is longer than length.
- *
- * ### Options:
- *
- * - `ellipsis` Will be used as beginning and prepended to the trimmed string
- * - `exact` If false, $text will not be cut mid-word
- *
- * @param string $text String to truncate.
- * @param int $length Length of returned string, including ellipsis.
- * @param array $options An array of options.
- * @return string Trimmed string.
- */
- public static function tail($text, $length = 100, array $options = [])
- {
- $default = [
- 'ellipsis' => '...', 'exact' => true
- ];
- $options += $default;
- $exact = $ellipsis = null;
- /**
- * @var string $ellipsis
- * @var bool $exact
- */
- extract($options);
- if (mb_strlen($text) <= $length) {
- return $text;
- }
- $truncate = mb_substr($text, mb_strlen($text) - $length + mb_strlen($ellipsis));
- if (!$exact) {
- $spacepos = mb_strpos($truncate, ' ');
- $truncate = $spacepos === false ? '' : trim(mb_substr($truncate, $spacepos));
- }
- return $ellipsis . $truncate;
- }
- /**
- * Truncates text.
- *
- * Cuts a string to the length of $length and replaces the last characters
- * with the ellipsis if the text is longer than length.
- *
- * ### Options:
- *
- * - `ellipsis` Will be used as ending and appended to the trimmed string
- * - `exact` If false, $text will not be cut mid-word
- * - `html` If true, HTML tags would be handled correctly
- * - `trimWidth` If true, $text will be truncated with the width
- *
- * @param string $text String to truncate.
- * @param int $length Length of returned string, including ellipsis.
- * @param array $options An array of HTML attributes and options.
- * @return string Trimmed string.
- * @link https://book.cakephp.org/3.0/en/core-libraries/text.html#truncating-text
- */
- public static function truncate($text, $length = 100, array $options = [])
- {
- $default = [
- 'ellipsis' => '...', 'exact' => true, 'html' => false, 'trimWidth' => false,
- ];
- if (!empty($options['html']) && strtolower(mb_internal_encoding()) === 'utf-8') {
- $default['ellipsis'] = "\xe2\x80\xa6";
- }
- $options += $default;
- $prefix = '';
- $suffix = $options['ellipsis'];
- if ($options['html']) {
- $ellipsisLength = self::_strlen(strip_tags($options['ellipsis']), $options);
- $truncateLength = 0;
- $totalLength = 0;
- $openTags = [];
- $truncate = '';
- preg_match_all('/(<\/?([\w+]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER);
- foreach ($tags as $tag) {
- $contentLength = 0;
- if (!in_array($tag[2], static::$_defaultHtmlNoCount, true)) {
- $contentLength = self::_strlen($tag[3], $options);
- }
- if ($truncate === '') {
- if (!preg_match('/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/i', $tag[2])) {
- if (preg_match('/<[\w]+[^>]*>/', $tag[0])) {
- array_unshift($openTags, $tag[2]);
- } elseif (preg_match('/<\/([\w]+)[^>]*>/', $tag[0], $closeTag)) {
- $pos = array_search($closeTag[1], $openTags);
- if ($pos !== false) {
- array_splice($openTags, $pos, 1);
- }
- }
- }
- $prefix .= $tag[1];
- if ($totalLength + $contentLength + $ellipsisLength > $length) {
- $truncate = $tag[3];
- $truncateLength = $length - $totalLength;
- } else {
- $prefix .= $tag[3];
- }
- }
- $totalLength += $contentLength;
- if ($totalLength > $length) {
- break;
- }
- }
- if ($totalLength <= $length) {
- return $text;
- }
- $text = $truncate;
- $length = $truncateLength;
- foreach ($openTags as $tag) {
- $suffix .= '</' . $tag . '>';
- }
- } else {
- if (self::_strlen($text, $options) <= $length) {
- return $text;
- }
- $ellipsisLength = self::_strlen($options['ellipsis'], $options);
- }
- $result = self::_substr($text, 0, $length - $ellipsisLength, $options);
- if (!$options['exact']) {
- if (self::_substr($text, $length - $ellipsisLength, 1, $options) !== ' ') {
- $result = self::_removeLastWord($result);
- }
- // If result is empty, then we don't need to count ellipsis in the cut.
- if (!strlen($result)) {
- $result = self::_substr($text, 0, $length, $options);
- }
- }
- return $prefix . $result . $suffix;
- }
- /**
- * Truncate text with specified width.
- *
- * @param string $text String to truncate.
- * @param int $length Length of returned string, including ellipsis.
- * @param array $options An array of HTML attributes and options.
- * @return string Trimmed string.
- * @see \Cake\Utility\Text::truncate()
- */
- public static function truncateByWidth($text, $length = 100, array $options = [])
- {
- return static::truncate($text, $length, ['trimWidth' => true] + $options);
- }
- /**
- * Get string length.
- *
- * ### Options:
- *
- * - `html` If true, HTML entities will be handled as decoded characters.
- * - `trimWidth` If true, the width will return.
- *
- * @param string $text The string being checked for length
- * @param array $options An array of options.
- * @return int
- */
- protected static function _strlen($text, array $options)
- {
- if (empty($options['trimWidth'])) {
- $strlen = 'mb_strlen';
- } else {
- $strlen = 'mb_strwidth';
- }
- if (empty($options['html'])) {
- return $strlen($text);
- }
- $pattern = '/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i';
- $replace = preg_replace_callback(
- $pattern,
- function ($match) use ($strlen) {
- $utf8 = html_entity_decode($match[0], ENT_HTML5 | ENT_QUOTES, 'UTF-8');
- return str_repeat(' ', $strlen($utf8, 'UTF-8'));
- },
- $text
- );
- return $strlen($replace);
- }
- /**
- * Return part of a string.
- *
- * ### Options:
- *
- * - `html` If true, HTML entities will be handled as decoded characters.
- * - `trimWidth` If true, will be truncated with specified width.
- *
- * @param string $text The input string.
- * @param int $start The position to begin extracting.
- * @param int $length The desired length.
- * @param array $options An array of options.
- * @return string
- */
- protected static function _substr($text, $start, $length, array $options)
- {
- if (empty($options['trimWidth'])) {
- $substr = 'mb_substr';
- } else {
- $substr = 'mb_strimwidth';
- }
- $maxPosition = self::_strlen($text, ['trimWidth' => false] + $options);
- if ($start < 0) {
- $start += $maxPosition;
- if ($start < 0) {
- $start = 0;
- }
- }
- if ($start >= $maxPosition) {
- return '';
- }
- if ($length === null) {
- $length = self::_strlen($text, $options);
- }
- if ($length < 0) {
- $text = self::_substr($text, $start, null, $options);
- $start = 0;
- $length += self::_strlen($text, $options);
- }
- if ($length <= 0) {
- return '';
- }
- if (empty($options['html'])) {
- return (string)$substr($text, $start, $length);
- }
- $totalOffset = 0;
- $totalLength = 0;
- $result = '';
- $pattern = '/(&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};)/i';
- $parts = preg_split($pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
- foreach ($parts as $part) {
- $offset = 0;
- if ($totalOffset < $start) {
- $len = self::_strlen($part, ['trimWidth' => false] + $options);
- if ($totalOffset + $len <= $start) {
- $totalOffset += $len;
- continue;
- }
- $offset = $start - $totalOffset;
- $totalOffset = $start;
- }
- $len = self::_strlen($part, $options);
- if ($offset !== 0 || $totalLength + $len > $length) {
- if (strpos($part, '&') === 0 && preg_match($pattern, $part)
- && $part !== html_entity_decode($part, ENT_HTML5 | ENT_QUOTES, 'UTF-8')
- ) {
- // Entities cannot be passed substr.
- continue;
- }
- $part = $substr($part, $offset, $length - $totalLength);
- $len = self::_strlen($part, $options);
- }
- $result .= $part;
- $totalLength += $len;
- if ($totalLength >= $length) {
- break;
- }
- }
- return $result;
- }
- /**
- * Removes the last word from the input text.
- *
- * @param string $text The input text
- * @return string
- */
- protected static function _removeLastWord($text)
- {
- $spacepos = mb_strrpos($text, ' ');
- if ($spacepos !== false) {
- $lastWord = mb_strrpos($text, $spacepos);
- // Some languages are written without word separation.
- // We recognize a string as a word if it doesn't contain any full-width characters.
- if (mb_strwidth($lastWord) === mb_strlen($lastWord)) {
- $text = mb_substr($text, 0, $spacepos);
- }
- return $text;
- }
- return '';
- }
- /**
- * Extracts an excerpt from the text surrounding the phrase with a number of characters on each side
- * determined by radius.
- *
- * @param string $text String to search the phrase in
- * @param string $phrase Phrase that will be searched for
- * @param int $radius The amount of characters that will be returned on each side of the founded phrase
- * @param string $ellipsis Ending that will be appended
- * @return string Modified string
- * @link https://book.cakephp.org/3.0/en/core-libraries/text.html#extracting-an-excerpt
- */
- public static function excerpt($text, $phrase, $radius = 100, $ellipsis = '...')
- {
- if (empty($text) || empty($phrase)) {
- return static::truncate($text, $radius * 2, ['ellipsis' => $ellipsis]);
- }
- $append = $prepend = $ellipsis;
- $phraseLen = mb_strlen($phrase);
- $textLen = mb_strlen($text);
- $pos = mb_stripos($text, $phrase);
- if ($pos === false) {
- return mb_substr($text, 0, $radius) . $ellipsis;
- }
- $startPos = $pos - $radius;
- if ($startPos <= 0) {
- $startPos = 0;
- $prepend = '';
- }
- $endPos = $pos + $phraseLen + $radius;
- if ($endPos >= $textLen) {
- $endPos = $textLen;
- $append = '';
- }
- $excerpt = mb_substr($text, $startPos, $endPos - $startPos);
- $excerpt = $prepend . $excerpt . $append;
- return $excerpt;
- }
- /**
- * Creates a comma separated list where the last two items are joined with 'and', forming natural language.
- *
- * @param array $list The list to be joined.
- * @param string|null $and The word used to join the last and second last items together with. Defaults to 'and'.
- * @param string $separator The separator used to join all the other items together. Defaults to ', '.
- * @return string The glued together string.
- * @link https://book.cakephp.org/3.0/en/core-libraries/text.html#converting-an-array-to-sentence-form
- */
- public static function toList(array $list, $and = null, $separator = ', ')
- {
- if ($and === null) {
- $and = __d('cake', 'and');
- }
- if (count($list) > 1) {
- return implode($separator, array_slice($list, null, -1)) . ' ' . $and . ' ' . array_pop($list);
- }
- return array_pop($list);
- }
- /**
- * Check if the string contain multibyte characters
- *
- * @param string $string value to test
- * @return bool
- */
- public static function isMultibyte($string)
- {
- $length = strlen($string);
- for ($i = 0; $i < $length; $i++) {
- $value = ord($string[$i]);
- if ($value > 128) {
- return true;
- }
- }
- return false;
- }
- /**
- * Converts a multibyte character string
- * to the decimal value of the character
- *
- * @param string $string String to convert.
- * @return array
- */
- public static function utf8($string)
- {
- $map = [];
- $values = [];
- $find = 1;
- $length = strlen($string);
- for ($i = 0; $i < $length; $i++) {
- $value = ord($string[$i]);
- if ($value < 128) {
- $map[] = $value;
- } else {
- if (empty($values)) {
- $find = ($value < 224) ? 2 : 3;
- }
- $values[] = $value;
- if (count($values) === $find) {
- if ($find == 3) {
- $map[] = (($values[0] % 16) * 4096) + (($values[1] % 64) * 64) + ($values[2] % 64);
- } else {
- $map[] = (($values[0] % 32) * 64) + ($values[1] % 64);
- }
- $values = [];
- $find = 1;
- }
- }
- }
- return $map;
- }
- /**
- * Converts the decimal value of a multibyte character string
- * to a string
- *
- * @param array $array Array
- * @return string
- */
- public static function ascii(array $array)
- {
- $ascii = '';
- foreach ($array as $utf8) {
- if ($utf8 < 128) {
- $ascii .= chr($utf8);
- } elseif ($utf8 < 2048) {
- $ascii .= chr(192 + (($utf8 - ($utf8 % 64)) / 64));
- $ascii .= chr(128 + ($utf8 % 64));
- } else {
- $ascii .= chr(224 + (($utf8 - ($utf8 % 4096)) / 4096));
- $ascii .= chr(128 + ((($utf8 % 4096) - ($utf8 % 64)) / 64));
- $ascii .= chr(128 + ($utf8 % 64));
- }
- }
- return $ascii;
- }
- /**
- * Converts filesize from human readable string to bytes
- *
- * @param string $size Size in human readable string like '5MB', '5M', '500B', '50kb' etc.
- * @param mixed $default Value to be returned when invalid size was used, for example 'Unknown type'
- * @return mixed Number of bytes as integer on success, `$default` on failure if not false
- * @throws \InvalidArgumentException On invalid Unit type.
- * @link https://book.cakephp.org/3.0/en/core-libraries/text.html#Cake\Utility\Text::parseFileSize
- */
- public static function parseFileSize($size, $default = false)
- {
- if (ctype_digit($size)) {
- return (int)$size;
- }
- $size = strtoupper($size);
- $l = -2;
- $i = array_search(substr($size, -2), ['KB', 'MB', 'GB', 'TB', 'PB']);
- if ($i === false) {
- $l = -1;
- $i = array_search(substr($size, -1), ['K', 'M', 'G', 'T', 'P']);
- }
- if ($i !== false) {
- $size = (float)substr($size, 0, $l);
- return $size * pow(1024, $i + 1);
- }
- if (substr($size, -1) === 'B' && ctype_digit(substr($size, 0, -1))) {
- $size = substr($size, 0, -1);
- return (int)$size;
- }
- if ($default !== false) {
- return $default;
- }
- throw new InvalidArgumentException('No unit type.');
- }
- /**
- * Get the default transliterator.
- *
- * @return \Transliterator|null Either a Transliterator instance, or `null`
- * in case no transliterator has been set yet.
- * @since 3.7.0
- */
- public static function getTransliterator()
- {
- return static::$_defaultTransliterator;
- }
- /**
- * Set the default transliterator.
- *
- * @param \Transliterator $transliterator A `Transliterator` instance.
- * @return void
- * @since 3.7.0
- */
- public static function setTransliterator(\Transliterator $transliterator)
- {
- static::$_defaultTransliterator = $transliterator;
- }
- /**
- * Get default transliterator identifier string.
- *
- * @return string Transliterator identifier.
- */
- public static function getTransliteratorId()
- {
- return static::$_defaultTransliteratorId;
- }
- /**
- * Set default transliterator identifier string.
- *
- * @param string $transliteratorId Transliterator identifier.
- * @return void
- */
- public static function setTransliteratorId($transliteratorId)
- {
- static::setTransliterator(transliterator_create($transliteratorId));
- static::$_defaultTransliteratorId = $transliteratorId;
- }
- /**
- * Transliterate string.
- *
- * @param string $string String to transliterate.
- * @param \Transliterator|string|null $transliterator Either a Transliterator
- * instance, or a transliterator identifier string. If `null`, the default
- * transliterator (identifier) set via `setTransliteratorId()` or
- * `setTransliterator()` will be used.
- * @return string
- * @see https://secure.php.net/manual/en/transliterator.transliterate.php
- */
- public static function transliterate($string, $transliterator = null)
- {
- if (!$transliterator) {
- $transliterator = static::$_defaultTransliterator ?: static::$_defaultTransliteratorId;
- }
- return transliterator_transliterate($transliterator, $string);
- }
- /**
- * Returns a string with all spaces converted to dashes (by default),
- * characters transliterated to ASCII characters, and non word characters removed.
- *
- * ### Options:
- *
- * - `replacement`: Replacement string. Default '-'.
- * - `transliteratorId`: A valid transliterator id string.
- * If `null` (default) the transliterator (identifier) set via
- * `setTransliteratorId()` or `setTransliterator()` will be used.
- * If `false` no transliteration will be done, only non words will be removed.
- * - `preserve`: Specific non-word character to preserve. Default `null`.
- * For e.g. this option can be set to '.' to generate clean file names.
- *
- * @param string $string the string you want to slug
- * @param array $options If string it will be use as replacement character
- * or an array of options.
- * @return string
- * @see setTransliterator()
- * @see setTransliteratorId()
- */
- public static function slug($string, $options = [])
- {
- if (is_string($options)) {
- $options = ['replacement' => $options];
- }
- $options += [
- 'replacement' => '-',
- 'transliteratorId' => null,
- 'preserve' => null
- ];
- if ($options['transliteratorId'] !== false) {
- $string = static::transliterate($string, $options['transliteratorId']);
- }
- $regex = '^\s\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}';
- if ($options['preserve']) {
- $regex .= preg_quote($options['preserve'], '/');
- }
- $quotedReplacement = preg_quote($options['replacement'], '/');
- $map = [
- '/[' . $regex . ']/mu' => ' ',
- '/[\s]+/mu' => $options['replacement'],
- sprintf('/^[%s]+|[%s]+$/', $quotedReplacement, $quotedReplacement) => '',
- ];
- $string = preg_replace(array_keys($map), $map, $string);
- return $string;
- }
- }
|