src/Twig/Theme.php
<?php
namespace BootPress\Blog\Twig;
use BootPress\Page\Component as Page;
use BootPress\Asset\Component as Asset;
use BootPress\Pagination\Component as Pagination;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Aptoma\Twig\Extension\MarkdownExtension;
class Theme
{
/** @var object This self if you don't have ready access to the Blog that called it. */
public static $instance;
/** @var array Twig template files and the specific vars passed to them. */
public static $templates = array();
/** @var array Global vars that are available in every Twig template. */
public $vars = array();
/** @var object A BootPress\Blog\Blog instance. */
private $blog;
/** @var object A Twig_Environment instance. */
private $twig;
/** @var array URL info relative to the current Twig template. */
private $asset;
/** @var array Twig macro namespaced properties. */
private $plugin;
public function __construct(\BootPress\Blog\Blog $blog)
{
self::$instance = $this;
$this->blog = $blog;
$this->vars['blog'] = new \BootPress\Blog\Twig\Object($this->blog->config('blog'), array(
'query' => array($this->blog, 'query'),
));
$this->vars['page'] = new \BootPress\Blog\Twig\Page();
$this->vars['pagination'] = new Pagination();
}
/**
* Get the Twig_Environment instance. If you call this before we do, then you can customize the ``$options``.
*
* @param array $options
*
* @return object
*
* @link http://twig.sensiolabs.org/doc/api.html#environment-options
*/
public function getTwig(array $options = array())
{
if (is_null($this->twig)) {
$page = Page::html();
foreach (array('content', 'plugins', 'themes') as $dir) {
if (!is_dir($this->blog->folder.$dir)) {
mkdir($this->blog->folder.$dir, 0755, true);
}
}
$loader = new \Twig_Loader_Filesystem($page->dir['page'], $page->dir['page']);
$loader->addPath($this->blog->folder.'plugins/', 'plugin');
$this->twig = new \Twig_Environment($loader, array_merge(array(
'cache' => $this->blog->folder.'cache/twig/',
'auto_reload' => true,
'autoescape' => false,
), $options));
$this->twig->addExtension(new MarkdownExtension(new Markdown($this)));
$this->twig->addFilter(new \Twig_SimpleFilter('asset', array($this, 'asset')));
$this->twig->addFunction(new \Twig_SimpleFunction('dir', array($this, 'dir')));
$this->twig->addFunction(new \Twig_SimpleFunction('this', array($this, 'this')));
$this->twig->addFunction(new \Twig_SimpleFunction('dump', array($this, 'dump'), array('is_safe' => 'html')));
$this->twig->registerUndefinedFunctionCallback(function ($name) {
switch ($name) {
// Array Functions
case 'array_change_key_case': // Changes the case of all keys in an array
case 'array_chunk': // Split an array into chunks
case 'array_column': // Return the values from a single column in the input array
case 'array_combine': // Creates an array by using one array for keys and another for its values
case 'array_count_values': // Counts all the values of an array
case 'array_diff_assoc': // Computes the difference of arrays with additional index check
case 'array_diff_key': // Computes the difference of arrays using keys for comparison
case 'array_diff': // Computes the difference of arrays
case 'array_fill_keys': // Fill an array with values, specifying keys
case 'array_fill': // Fill an array with values
case 'array_filter': // Filters elements of an array using a callback function
case 'array_flip': // Exchanges all keys with their associated values in an array
case 'array_intersect_assoc': // Computes the intersection of arrays with additional index check
case 'array_intersect_key': // Computes the intersection of arrays using keys for comparison
case 'array_intersect': // Computes the intersection of arrays
case 'array_key_exists': // Checks if the given key or index exists in the array
case 'array_keys': // Return all the keys or a subset of the keys of an array
case 'array_map': // Applies the callback to the elements of the given arrays
case 'array_merge_recursive': // Merge two or more arrays recursively
case 'array_merge': // Merge one or more arrays
case 'array_pad': // Pad array to the specified length with a value
case 'array_product': // Calculate the product of values in an array
case 'array_rand': // Pick one or more random entries out of an array
case 'array_replace_recursive': // Replaces elements from passed arrays into the first array recursively
case 'array_replace': // Replaces elements from passed arrays into the first array
case 'array_reverse': // Return an array with elements in reverse order
case 'array_search': // Searches the array for a given value and returns the first corresponding key if successful
case 'array_slice': // Extract a slice of the array
case 'array_sum': // Calculate the sum of values in an array
case 'array_unique': // Removes duplicate values from an array
case 'array_values': // Return all the values of an array
case 'count': // Count all elements in an array, or something in an object
case 'in_array': // Checks if a value exists in an array
// Date/Time Functions
case 'date_parse': // Returns associative array with detailed info about given date
case 'date_sun_info': // Returns an array with information about sunset/sunrise and twilight begin/end
case 'getdate': // Get date/time information
case 'gettimeofday': // Get current time
case 'gmdate': // Format a GMT/UTC date/time
case 'gmmktime': // Get Unix timestamp for a GMT date
case 'microtime': // Return current Unix timestamp with microseconds
case 'mktime': // Get Unix timestamp for a date
case 'strtotime': // Parse about any English textual datetime description into a Unix timestamp
case 'time': // Return current Unix timestamp
// JSON Functions
case 'json_decode': // Decodes a JSON string
case 'json_encode': // Returns the JSON representation of a value
// Mail Functions
case 'mail': // Send mail
// Math Functions
case 'abs': // Absolute value
case 'acos': // Arc cosine
case 'acosh': // Inverse hyperbolic cosine
case 'asin': // Arc sine
case 'asinh': // Inverse hyperbolic sine
case 'atan2': // Arc tangent of two variables
case 'atan': // Arc tangent
case 'atanh': // Inverse hyperbolic tangent
case 'base_convert': // Convert a number between arbitrary bases
case 'bindec': // Binary to decimal
case 'ceil': // Round fractions up
case 'cos': // Cosine
case 'cosh': // Hyperbolic cosine
case 'decbin': // Decimal to binary
case 'dechex': // Decimal to hexadecimal
case 'decoct': // Decimal to octal
case 'deg2rad': // Converts the number in degrees to the radian equivalent
case 'exp': // Calculates the exponent of e
case 'expm1': // Returns exp(number) - 1, computed in a way that is accurate even when the value of number is close to zero
case 'floor': // Round fractions down
case 'fmod': // Returns the floating point remainder (modulo) of the division of the arguments
case 'getrandmax': // Show largest possible random value
case 'hexdec': // Hexadecimal to decimal
case 'hypot': // Calculate the length of the hypotenuse of a right-angle triangle
case 'is_finite': // Finds whether a value is a legal finite number
case 'is_infinite': // Finds whether a value is infinite
case 'is_nan': // Finds whether a value is not a number
case 'lcg_value': // Combined linear congruential generator
case 'log10': // Base-10 logarithm
case 'log1p': // Returns log(1 + number), computed in a way that is accurate even when the value of number is close to zero
case 'log': // Natural logarithm
case 'mt_getrandmax': // Show largest possible random value
case 'mt_rand': // Generate a better random value
case 'mt_srand': // Seed the better random number generator
case 'octdec': // Octal to decimal
case 'pi': // Get value of pi
case 'pow': // Exponential expression
case 'rad2deg': // Converts the radian number to the equivalent number in degrees
case 'rand': // Generate a random integer
case 'round': // Rounds a float
case 'sin': // Sine
case 'sinh': // Hyperbolic sine
case 'sqrt': // Square root
case 'srand': // Seed the random number generator
case 'tan': // Tangent
case 'tanh': // Hyperbolic tangent
// Misc Functions
case 'pack': // Pack data into binary string
case 'unpack': // Unpack data from binary string
// Multibyte String Functions
case 'mb_convert_case': // Perform case folding on a string
case 'mb_convert_encoding': // Convert character encoding
case 'mb_strimwidth': // Get truncated string with specified width
case 'mb_stripos': // Finds position of first occurrence of a string within another, case insensitive
case 'mb_stristr': // Finds first occurrence of a string within another, case insensitive
case 'mb_strlen': // Get string length
case 'mb_strpos': // Find position of first occurrence of string in a string
case 'mb_strrchr': // Finds the last occurrence of a character in a string within another
case 'mb_strrichr': // Finds the last occurrence of a character in a string within another, case insensitive
case 'mb_strripos': // Finds position of last occurrence of a string within another, case insensitive
case 'mb_strrpos': // Find position of last occurrence of a string in a string
case 'mb_strstr': // Finds first occurrence of a string within another
case 'mb_strtolower': // Make a string lowercase
case 'mb_strtoupper': // Make a string uppercase
case 'mb_strwidth': // Return width of string
case 'mb_substr_count': // Count the number of substring occurrences
case 'mb_substr': // Get part of string
// String Functions
case 'addcslashes': // Quote string with slashes in a C style
case 'addslashes': // Quote string with slashes
case 'bin2hex': // Convert binary data into hexadecimal representation
case 'chr': // Return a specific character - complements ord()
case 'chunk_split': // Split a string into smaller chunks
case 'explode': // Split a string by string
case 'hex2bin': // Decodes a hexadecimally encoded binary string
case 'htmlspecialchars': // Convert special characters to HTML entities
case 'implode': // Join array elements with a string
case 'lcfirst': // Make a string's first character lowercase
case 'ltrim': // Strip whitespace (or other characters) from the beginning of a string
case 'nl2br': // Inserts HTML line breaks before all newlines in a string
case 'number_format': // Format a number with grouped thousands
case 'ord': // Return ASCII value of character - complements chr()
case 'rtrim': // Strip whitespace (or other characters) from the end of a string
case 'str_ireplace': // Case-insensitive version of str_replace()
case 'str_pad': // Pad a string to a certain length with another string
case 'str_repeat': // Repeat a string
case 'str_replace': // Replace all occurrences of the search string with the replacement string
case 'str_rot13': // Perform the rot13 transform on a string
case 'str_shuffle': // Randomly shuffles a string
case 'str_split': // Convert a string to an array
case 'str_word_count': // Return information about words used in a string
case 'strip_tags': // Strip HTML and PHP tags from a string
case 'stripos': // Find the position of the first occurrence of a case-insensitive substring in a string
case 'stristr': // Case-insensitive strstr()
case 'strlen': // Get string length
case 'strpos': // Find the position of the first occurrence of a substring in a string
case 'strrchr': // Find the last occurrence of a character in a string
case 'strrev': // Reverse a string
case 'strripos': // Find the position of the last occurrence of a case-insensitive substring in a string
case 'strrpos': // Find the position of the last occurrence of a substring in a string
case 'strstr': // Find the first occurrence of a string
case 'strtok': // Tokenize string
case 'strtolower': // Make a string lowercase
case 'strtoupper': // Make a string uppercase
case 'strtr': // Translate characters or replace substrings
case 'substr_count': // Count the number of substring occurrences
case 'substr': // Return part of a string
case 'trim': // Strip whitespace (or other characters) from the beginning and end of a string
case 'ucfirst': // Make a string's first character uppercase
case 'ucwords': // Uppercase the first character of each word in a string
case 'wordwrap': // Wraps a string to a given number of characters
// Variable handling Functions
case 'gettype': // Get the type of a variable
case 'is_array': // Finds whether a variable is an array
case 'is_bool': // Finds out whether a variable is a boolean
case 'is_float': // Finds whether the type of a variable is float
case 'is_int': // Find whether the type of a variable is integer
case 'is_null': // Finds whether a variable is NULL
case 'is_numeric': // Finds whether a variable is a number or a numeric string
case 'is_string': // Find whether the type of a variable is string
case 'serialize': // Generates a storable representation of a value
case 'unserialize': // Creates a PHP value from a stored representation
return new \Twig_SimpleFunction($name, $name);
break;
// Return by reference, so we return directly
case 'array_pop': // Pop the element off the end of array
case 'array_shift': // Shift an element off the beginning of array
case 'array_splice': // Remove a portion of the array and replace it with something else
case 'arsort': // Sort an array in reverse order and maintain index association
case 'asort': // Sort an array and maintain index association
case 'krsort': // Sort an array by key in reverse order
case 'ksort': // Sort an array by key
case 'natcasesort': // Sort an array using a case insensitive "natural order" algorithm
case 'natsort': // Sort an array using a "natural order" algorithm
case 'rsort': // Sort an array in reverse order
case 'shuffle': // Shuffle an array
case 'sort': // Sort an array
case 'settype': // Set the type of a variable
return new \Twig_SimpleFunction($name, function () use ($name) {
$args = func_get_args();
$reference = array_shift($args);
switch (count($args)) {
case 0: $name($reference); break;
case 1: $name($reference, array_shift($args)); break;
case 2: $name($reference, array_shift($args), array_shift($args)); break;
default: $name($reference, array_shift($args), array_shift($args), array_shift($args)); break;
}
return $reference;
});
break;
// PCRE Functions
case 'preg_filter': // Perform a regular expression search and replace - 4
case 'preg_grep': // Return array entries that match the pattern
case 'preg_match_all': // Perform a global regular expression match
case 'preg_match': // Perform a regular expression match
case 'preg_quote': // Quote regular expression characters
case 'preg_replace': // Perform a regular expression search and replace - 4
case 'preg_split': // Split string by a regular expression - 3
return new \Twig_SimpleFunction($name, function () use ($name) {
$args = func_get_args();
$pattern = array_shift($args);
if ($name != 'preg_quote') {
$this->removeEval($pattern);
}
if (strpos($name, 'match')) {
$name($pattern, array_shift($args), $matches);
return $matches;
} else {
array_unshift($args, $pattern);
return call_user_func_array($name, $args);
}
});
break;
}
return false;
});
}
return $this->twig;
}
/**
* Render a Twig template.
*
* @param string|array $file The template file.
* @param array $vars To pass to the Twig template.
*
* @return string
*
* @throws LogicException If the **$file** is not in the ``$page->dir()``, or if it doesn't exist.
*/
public function renderTwig($file, array $vars = array())
{
$page = Page::html();
if (is_array($file)) {
$vars = (isset($file['vars']) && is_array($file['vars'])) ? $file['vars'] : array();
$default = (isset($file['default']) && is_dir($file['default'])) ? rtrim($file['default'], '/').'/' : null;
$file = (isset($file['file']) && is_string($file['file'])) ? $file['file'] : '';
if ($default && $template = $this->getFiles($file, $default)) {
$file = array_pop($template);
}
}
$base = $page->dir['page'];
$cut = strlen($base);
if (strpos($file, $this->blog->folder.'themes/') === 0) {
$dir = $this->blog->folder.'themes/';
$file = substr($file, strlen($dir));
$theme = strstr($file, '/', true).'/';
$dir .= $theme;
$file = substr($file, strlen($theme));
$loader = $this->getTwig()->getLoader();
$loader->addPath($dir, 'theme');
$dir = substr($dir, $cut);
} elseif (strpos($file, $base) === 0) {
$dir = substr(dirname($file).'/', $cut);
$file = basename($file);
} else {
throw new \LogicException("The '{$file}' is not in your website's Page::dir folder.");
}
if (!is_file($base.$dir.$file)) {
throw new \LogicException("The '{$dir}{$file}' file does not exist.");
}
if (is_null($this->asset)) { // our first time here
$this->getTwig()->setParser(new Parser($this->getTwig()));
}
$this->asset = array(
'dir' => $dir,
'chars' => $page->url['chars'],
);
$save = array(
'template' => $dir.$file,
'vars' => $vars,
'start' => microtime(true),
);
try {
$html = $this->getTwig()->render($dir.$file, array_merge($vars, $this->vars));
$save['output'] = $html;
} catch (\Exception $e) {
$html = '<p>'.$e->getMessage().'</p>';
$save['error'] = $e->getMessage();
}
$save['end'] = microtime(true);
$save['time'] = $save['end'] - $save['start'];
self::$templates[] = $save;
return $html;
}
/**
* Returns an HTML string from your Markdown **$content**, and allows you to set your preferred Markdown provider.
*
* @param string|callable $content
*
* @return string|null
*
* @example
*
* ```twig
* <div>
* <h1 class="someClass">{{ page.title }}</h1>
*
* {% markdown %}
* This is a list that is indented to match the context around the markdown tag:
*
* * List item 1
* * List item 2
* * Sub List Item
* * Sub Sub List Item
*
* The following block will be transformed as code, as it is indented more than the
* surrounding content:
*
* $code = "good";
*
* {% endmarkdown %}
*
* </div>
* ```
*/
public function markdown($content)
{
static $markdown = null;
if (is_callable($content)) {
$markdown = $content;
} elseif (is_null($markdown)) {
$parsedown = new \ParsedownExtra();
$markdown = function ($content) use ($parsedown) {
return $parsedown->text($content);
};
}
return (is_string($content)) ? $markdown($content) : null;
}
/**
* Prepends a url to **$path**, relative to the main index.html.twig being fetched.
*
* @param string|array $path An asset string eg. image.jpg
* @param object $twig ``_self`` if the **$path** is relative to the current template. This is useful for plugin macros and child themes.
*
* @return string|array Whatever the **$path** was. If the **$path**'s string is not a relative asset, then it is just returned as is. If the **$path** is an array, then every key and value in it will be turned into a url if it is a relative asset, and the rest of the array will remain the same.
*
* @example
*
* ```twig
* <img src="{{ 'image.jpg'|asset }}">
* ```
*/
public function asset($path, \Twig_Template $twig = null)
{
if (is_string($path)) {
if ($this->asset && preg_match('/^'.implode('', array(
'(?!((f|ht)tps?:)?\/\/)',
'['.$this->asset['chars'].'.\/]+',
'\.('.Asset::PREG_TYPES.')',
'.*',
)).'$/i', ltrim($path), $matches)) {
$page = Page::html();
$dir = ($twig) ? $this->dir($twig, $matches[0]) : \phpUri::parse($this->asset['dir'])->join($matches[0]);
$asset = $page->url('page', $dir);
}
} elseif ($this->asset && is_array($path)) {
$asset = array();
foreach ($path as $key => $value) {
$asset[$this->asset($key, $twig)] = $this->asset($value, $twig);
}
}
return (isset($asset)) ? $asset : $path;
}
/**
* Get an absolute **$path** relative to the **$twig** template.
*
* @param object $twig ``_self`` so we know who you are.
* @param mixed $path Relative to current folder.
*
* @return string An absolute file **$path**.
*
* @example
*
* ```twig
* {% include dir(_self, '../file.html.twig') %}
* ```
*/
public function dir(\Twig_Template $twig, $path = '')
{
return \phpUri::parse(str_replace('\\', '/', dirname($this->getTwig()->getLoader()->getCacheKey($twig->getTemplateName()))).'/')->join($path);
}
/**
* A reference to the current template, sort of. This enables your plugin macros to behave more like a class, and these are your properties.
*
* @param object $twig ``_self`` so we know who you are.
* @param mixed $key What you want to either set or retrieve. Pass ``null`` to remove them all. Make it an array to set multiple values at once.
* @param mixed $value Of the **$key** if you are setting it. Pass ``null`` to remove only this one.
*
* @return mixed If you don't specify **$key** or **$value**, then all of the "properties" (an array) will be returned. If you don't include a **$value**, then the **$key** will be returned if it exists. Otherwise you get ``null``.
*
* @example
*
* ```twig
* {{ this(_self, 'key', 'value') }}
* ```
*/
public function this(\Twig_Template $twig, $key = null, $value = null)
{
$name = $twig->getTemplateName();
if (!isset($this->plugin[$name])) {
$this->plugin[$name] = array();
}
if (func_num_args() == 1) {
return $this->plugin[$name]; // return all values
} elseif (is_null($key)) {
$this->plugin[$name] = array(); // remove all values
} elseif (is_array($key)) {
$this->plugin[$name] = $key + $this->plugin[$name]; // set multiple values
} elseif (func_num_args() == 3) {
if (is_null($value)) {
unset($this->plugin[$name][$key]); // remove a single value
} else {
$this->plugin[$name][$key] = $value; // set a single value
}
} elseif (isset($this->plugin[$name][$key])) {
return $this->plugin[$name][$key]; // return a single value
}
}
/**
* Dumps a beautifully formatted debug string of your **$var**.
*
* @param mixed $var If you don't have one, then we will pass the current template name and vars that we initially gave you. Objects will only be named, and not displayed.
*
* @return string
*
* @example
*
* ```twig
* {{ dump() }}
* ```
*/
public function dump($var = null)
{
if (func_num_args() == 0) {
$var = array_slice(self::$templates, -1);
$var = array_shift($var);
$var = array('global' => $this->vars, 'vars' => $var['vars']);
}
return self::dumper($var, array('dumper' => 'html'));
}
/**
* Creates a layout using the ``$page->theme`` you have specified.
*
* @param string $html The main content of your page.
*
* @return string
*/
public function layout($html)
{
$page = Page::html();
$theme = $page->theme;
if ($theme === false) {
return $html;
} elseif (is_callable($theme)) {
return $theme($html, $this->vars);
} elseif (is_file($theme)) {
return $page->load($theme, array(
'content' => $html,
'vars' => $this->vars,
));
} elseif (!$index = $this->getFiles('index.html.twig', __DIR__.'/theme/')) {
return $html;
}
$vars = array(
'content' => $html,
'config' => array(),
);
if ($config = $this->getFiles('config.yml')) {
foreach ($config as $file) { // any child values will override the parents
$vars['config'] = array_merge($vars['config'], (array) Yaml::parse(file_get_contents($file)));
}
}
return $this->renderTwig(array_pop($index), $vars);
}
public static function dumper($var, array $options = array())
{
extract(array_merge(array(
'dumper' => ('cli' === PHP_SAPI) ? 'cli' : 'html',
'items' => 100,
'string' => 100,
'indent' => ' ',
), $options));
$output = '';
$cloner = new VarCloner();
$cloner->setMaxItems($items);
$cloner->setMaxString($string);
$dumper = ($dumper == 'cli') ? new CliDumper() : new HtmlDumper();
$dumper->dump($cloner->cloneVar(self::wringer($var)), function ($line, $depth) use (&$output, $indent) {
$output .= ($depth >= 0) ? str_repeat($indent, $depth).$line."\n" : '';
});
return trim($output);
}
/**
* Returns **$data** suitable for displaying.
*
* @param mixed $data
*
* @return mixed
*
* @used-by dumper
*/
private static function wringer($data)
{
if (is_object($data)) {
if ($data instanceof \BootPress\Blog\Twig\Object) {
$data = $data->properties + $data->methods;
} elseif ($data instanceof \BootPress\Blog\Twig\Page) {
$data = $data->html + $data->methods;
} else {
$data = get_class($data).' Object';
}
}
if (is_array($data)) {
$cloner = array();
foreach ($data as $key => $value) {
$cloner[$key] = self::wringer($value);
}
return $cloner;
}
if (is_string($data)) {
$data = str_replace(array("\r", "\n", "\t"), array('\\r', '\\n', '\\t'), $data);
}
return $data;
}
/**
* Gets all the file **$name**'s within the selected theme.
*
* @param string $name The file you are looking for eg. 'index.html.twig'
* @param string $default The file path to a default template if no other is available. It will be copied to the theme folder's root. Must include a trailing slash.
*
* @return array|null
*/
private function getFiles($name, $default = null)
{
$files = array();
if (!empty($name)) {
$page = Page::html();
$themes = $page->dir($this->blog->folder, 'themes');
if (!empty($page->theme) && is_string($page->theme) && is_dir($themes.$page->theme)) {
$path = str_replace('\\', '/', $page->theme);
} else {
$path = $this->blog->config('blog', 'theme');
}
$paths = array_filter(explode('/', preg_replace('/[^a-z0-9-\/]/', '', $path)));
if (!empty($paths)) {
$previous = '';
foreach ($paths as $level => $dir) {
$previous .= $dir.'/';
if (is_file($themes.$previous.$name)) {
$files[$level] = $themes.$previous.$name;
}
}
if (empty($files) && !empty($default) && is_file($default.$name)) {
$root = array_shift($paths);
if (!is_dir($themes.$root)) {
mkdir($themes.$root, 0755, true);
}
$files[] = $page->file($themes, $root, $name);
copy($default.$name, $files[0]);
}
}
}
return (!empty($files)) ? $files : null;
}
/**
* Remove 'e' eval modifier from pattern.
*
* @param mixed $pattern Regex backslashes must be double-escaped to work properly ie. '//'
*
* @return mixed
*
* @link http://php.net/manual/en/reference.pcre.pattern.modifiers.php#reference.pcre.pattern.modifiers.eval
* @link http://twig.sensiolabs.org/doc/templates.html#comparisons
*/
private function removeEval(&$pattern)
{
if (is_array($pattern)) {
foreach ($pattern as $key => $value) {
$pattern[$key] = $this->removeEval($value);
}
} elseif (is_string($pattern)) {
$pattern = trim($pattern);
$mods = strrpos($pattern, substr($pattern, 0, 1));
$pattern = substr($pattern, 0, $mods).str_replace('e', '', substr($pattern, $mods));
}
return $pattern;
}
}