Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
<?php
/**
* Allows for quick and flexible HTML templating
*
* @copyright Copyright (c) 2007-2011 Will Bond, others
* @author Will Bond [wb] <will@flourishlib.com>
* @author Matt Nowack [mn] <mdnowack@gmail.com>
* @author Jeff Turcotte [jt] <jeff@imarc.net>
* @license http://flourishlib.com/license
*
* @package Flourish
* @link http://flourishlib.com/fTemplating
*
* @version 1.0.0b24
* @changes 1.0.0b24 Added getRootPath() to retrieve the path fTemplating was instantiated with [jt, 2012-10-24]
* @changes 1.0.0b23 Added a default `$name` for ::retrieve() to mirror ::attach() [wb, 2011-08-31]
* @changes 1.0.0b22 Backwards Compatibility Break - removed the static method ::create(), added the static method ::attach() to fill its place [wb, 2011-08-31]
* @changes 1.0.0b21 Fixed a bug in ::enableMinification() where the minification cache directory was sometimes not properly converted to a web path [wb, 2011-08-31]
* @changes 1.0.0b20 Fixed a bug in CSS minification that would reduce multiple zeros that are part of a hex color code, fixed minification of `+ ++` and similar constructs in JS [wb, 2011-08-31]
* @changes 1.0.0b19 Corrected a bug in ::enablePHPShortTags() that would prevent proper translation inside of HTML tag attributes [wb, 2011-01-09]
* @changes 1.0.0b18 Fixed a bug with CSS minification and black hex codes [wb, 2010-10-10]
* @changes 1.0.0b17 Backwards Compatibility Break - ::delete() now returns the values of the element or elements that were deleted instead of returning the fTemplating instance [wb, 2010-09-19]
* @changes 1.0.0b16 Fixed another bug with minifying JS regex literals [wb, 2010-09-13]
* @changes 1.0.0b15 Fixed a bug with minifying JS regex literals that occur after a reserved word [wb, 2010-09-12]
* @changes 1.0.0b14 Added documentation about `[sub-key]` syntax [wb, 2010-09-12]
* @changes 1.0.0b13 Backwards Compatibility Break - ::add(), ::delete(), ::get() and ::set() now interpret `[` and `]` as array shorthand and thus they can not be used in element names, renamed ::remove() to ::filter() - added `$beginning` parameter to ::add() and added ::remove() method [wb, 2010-09-12]
* @changes 1.0.0b12 Added ::enableMinification(), ::enablePHPShortTags(), the ability to be able to place child fTemplating objects via a new magic element `__main__` and the `$main_element` parameter for ::__construct() [wb, 2010-08-31]
* @changes 1.0.0b11 Fixed a bug with the elements not being initialized to a blank array [wb, 2010-08-12]
* @changes 1.0.0b10 Updated ::place() to ignore URL query strings when detecting an element type [wb, 2010-07-26]
* @changes 1.0.0b9 Added the methods ::delete() and ::remove() [wb+mn, 2010-07-15]
* @changes 1.0.0b8 Fixed a bug with placing absolute file paths on Windows [wb, 2010-07-09]
* @changes 1.0.0b7 Removed `e` flag from preg_replace() calls [wb, 2010-06-08]
* @changes 1.0.0b6 Changed ::set() and ::add() to return the object for method chaining, changed ::set() and ::get() to accept arrays of elements [wb, 2010-06-02]
* @changes 1.0.0b5 Added ::encode() [wb, 2010-05-20]
* @changes 1.0.0b4 Added ::create() and ::retrieve() for named fTemplating instances [wb, 2010-05-11]
* @changes 1.0.0b3 Fixed an issue with placing relative file path [wb, 2010-04-23]
* @changes 1.0.0b2 Added the ::inject() method [wb, 2009-01-09]
* @changes 1.0.0b The initial implementation [wb, 2007-06-14]
*/
class fTemplating
{
const attach = 'fTemplating::attach';
const reset = 'fTemplating::reset';
const retrieve = 'fTemplating::retrieve';
/**
* Named fTemplating instances
*
* @var array
*/
static $instances = array();
/**
* Attaches a named template that can be accessed from any scope via ::retrieve()
*
* @param fTemplating $templating The fTemplating object to attach
* @param string $name The name for this templating instance
* @return void
*/
static public function attach($templating, $name='default')
{
self::$instances[$name] = $templating;
}
/**
* Resets the configuration of the class
*
* @internal
*
* @return void
*/
static public function reset()
{
self::$instances = array();
}
/**
* Retrieves a named template
*
* @param string $name The name of the template to retrieve
* @return fTemplating The specified fTemplating instance
*/
static public function retrieve($name='default')
{
if (!isset(self::$instances[$name])) {
throw new fProgrammerException(
'The named template specified, %s, has not been attached yet',
$name
);
}
return self::$instances[$name];
}
/**
* The buffered object id, used for differentiating different instances when doing replacements
*
* @var integer
*/
private $buffered_id;
/**
* A data store for templating
*
* @var array
*/
private $elements;
/**
* The directory to store minified code in
*
* @var string
*/
private $minification_directory;
/**
* The path prefix to prepend to CSS and JS paths to find them on the filesystem
*
* @var string
*/
private $minification_prefix;
/**
* The minification mode: development or production
*
* @var string
*/
private $minification_mode;
/**
* The directory to look for files
*
* @var string
*/
protected $root;
/**
* The directory to store PHP files with short tags fixed
*
* @var string
*/
private $short_tag_directory;
/**
* The short tag mode: development or production
*
* @var string
*/
private $short_tag_mode;
/**
* Initializes this templating engine
*
* @param string $root The filesystem path to use when accessing relative files, defaults to `$_SERVER['DOCUMENT_ROOT']`
* @param string $main_element The value for the `__main__` element - this is used when calling ::place() without an element, or when placing fTemplating objects as children
* @return fTemplating
*/
public function __construct($root=NULL, $main_element=NULL)
{
if ($root === NULL) {
$root = $_SERVER['DOCUMENT_ROOT'];
}
if (!file_exists($root)) {
throw new fProgrammerException(
'The root specified, %s, does not exist on the filesystem',
$root
);
}
if (!is_readable($root)) {
throw new fEnvironmentException(
'The root specified, %s, is not readable',
$root
);
}
if (substr($root, -1) != '/' && substr($root, -1) != '\\') {
$root .= DIRECTORY_SEPARATOR;
}
$this->buffered_id = NULL;
$this->elements = array();
$this->root = $root;
if ($main_element !== NULL) {
$this->set('__main__', $main_element);
}
}
/**
* Finishing placing elements if buffering was used
*
* @internal
*
* @return void
*/
public function __destruct()
{
// The __destruct method can't throw unhandled exceptions intelligently, so we will always catch here just in case
try {
$this->placeBuffered();
} catch (Exception $e) {
fCore::handleException($e);
}
}
/**
* All requests that hit this method should be requests for callbacks
*
* @internal
*
* @param string $method The method to create a callback for
* @return callback The callback for the method requested
*/
public function __get($method)
{
return array($this, $method);
}
/**
* Adds a value to an array element
*
* @param string $element The element to add to - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
* @param mixed $value The value to add
* @param boolean $beginning If the value should be added to the beginning of the element
* @return fTemplating The template object, to allow for method chaining
*/
public function add($element, $value, $beginning=FALSE)
{
$tip =& $this->elements;
if ($bracket_pos = strpos($element, '[')) {
$original_element = $element;
$array_dereference = substr($element, $bracket_pos);
$element = substr($element, 0, $bracket_pos);
preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
$array_keys = array_map('current', $array_keys);
array_unshift($array_keys, $element);
foreach (array_slice($array_keys, 0, -1) as $array_key) {
if (!isset($tip[$array_key])) {
$tip[$array_key] = array();
} elseif (!is_array($tip[$array_key])) {
throw new fProgrammerException(
'%1$s was called for an element, %2$s, which is not an array',
'add()',
$original_element
);
}
$tip =& $tip[$array_key];
}
$element = end($array_keys);
}
if (!isset($tip[$element])) {
$tip[$element] = array();
} elseif (!is_array($tip[$element])) {
throw new fProgrammerException(
'%1$s was called for an element, %2$s, which is not an array',
'add()',
$element
);
}
if ($beginning) {
array_unshift($tip[$element], $value);
} else {
$tip[$element][] = $value;
}
return $this;
}
/**
* Enables buffered output, allowing ::set() and ::add() to happen after a ::place() but act as if they were done before
*
* Please note that using buffered output will affect the order in which
* code is executed since the elements are not actually ::place()'ed until
* the destructor is called.
*
* If the non-template code depends on template code being executed
* sequentially before it, you may not want to use output buffering.
*
* @return void
*/
public function buffer()
{
static $id_sequence = 1;
if ($this->buffered_id) {
throw new fProgrammerException('Buffering has already been started');
}
if (!fBuffer::isStarted()) {
fBuffer::start();
}
$this->buffered_id = $id_sequence;
$id_sequence++;
}
/**
* Deletes an element from the template
*
* @param string $element The element to delete - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
* @param mixed $default_value The value to return if the `$element` is not set
* @param array |$elements The elements to delete - an array of element names or an associative array of keys being element names and the values being the default values
* @return mixed The value of the `$element` that was deleted - an associative array of deleted elements will be returned if an array of `$elements` was specified
*/
public function delete($element, $default_value=NULL)
{
if (is_array($element)) {
$elements = $element;
if (is_numeric(key($elements))) {
$new_elements = array();
foreach ($elements as $element) {
$new_elements[$element] = NULL;
}
$elements = $new_elements;
}
$output = array();
foreach ($elements as $key => $default_value) {
$output[$key] = $this->delete($key, $default_value);
}
return $output;
}
$tip =& $this->elements;
$value = $default_value;
if ($bracket_pos = strpos($element, '[')) {
$original_element = $element;
$array_dereference = substr($element, $bracket_pos);
$element = substr($element, 0, $bracket_pos);
preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
$array_keys = array_map('current', $array_keys);
array_unshift($array_keys, $element);
foreach (array_slice($array_keys, 0, -1) as $array_key) {
if (!isset($tip[$array_key])) {
return $value;
} elseif (!is_array($tip[$array_key])) {
throw new fProgrammerException(
'%1$s was called for an element, %2$s, which is not an array',
'delete()',
$original_element
);
}
$tip =& $tip[$array_key];
}
$element = end($array_keys);
}
if (isset($tip[$element])) {
$value = $tip[$element];
unset($tip[$element]);
}
return $value;
}
/**
* Erases all output since the invocation of the template - only works if buffering is on
*
* @return void
*/
public function destroy()
{
if (!$this->buffered_id) {
throw new fProgrammerException(
'A template can only be destroyed if buffering has been enabled'
);
}
$this->buffered_id = NULL;
fBuffer::erase();
fBuffer::stop();
$this->__destruct();
}
/**
* Enables minified output for CSS and JS elements
*
* For CSS and JS, compilation means that the file will be minified and
* cached. The filename will change whenever the content change, allowing
* for far-futures expire headers.
*
* Please note that this option requires that all CSS and JS paths be
* relative to the $_SERVER['DOCUMENT_ROOT'] and start with a `/`. Also
* this class will not clean up old cached files out of the cache
* directory.
*
* This functionality will be inherited by all child fTemplating objects
* that do not have their own explicit minification settings.
*
* @param string $mode The compilation mode - `'development'` means that file modification times will be checked on each load, `'production'` means that the cache files will only be regenerated when missing
* @param fDirectory|string $cache_directory The directory to cache the compiled files into - this needs to be inside the document root or a path added to fFilesystem::addWebPathTranslation()
* @param fDirectory|string $path_prefix The directory to prepend to all CSS and JS paths to load the files from the filesystem - this defaults to `$_SERVER['DOCUMENT_ROOT']`
* @return void
*/
public function enableMinification($mode, $cache_directory, $path_prefix=NULL)
{
$valid_modes = array('development', 'production');
if (!in_array($mode, $valid_modes)) {
throw new fProgrammerException(
'The mode specified, %1$s, is invalid. Must be one of: %2$s.',
$mode,
join(', ', $valid_modes)
);
}
$cache_directory = $cache_directory instanceof fDirectory ? $cache_directory->getPath() : realpath($cache_directory);
if (!is_writable($cache_directory)) {
throw new fEnvironmentException(
'The cache directory specified, %s, is not writable',
$cache_directory
);
}
$path_prefix = $path_prefix instanceof fDirectory ? $path_prefix->getPath() : $path_prefix;
if ($path_prefix === NULL) {
$path_prefix = $_SERVER['DOCUMENT_ROOT'];
}
$this->minification_mode = $mode;
$this->minification_directory = fDirectory::makeCanonical($cache_directory);
$this->minification_prefix = $path_prefix;
}
/**
* Converts PHP short tags to long tags when short tags are turned off
*
* Please note that this only affects PHP files that are **directly**
* evaluated with ::place() or ::inject(). It will not affect PHP files that
* have been evaluated via `include` or `require` statements inside of the
* directly evaluated PHP files.
*
* This functionality will be inherited by all child fTemplating objects
* that do not have their own explicit short tag settings.
*
* @param string $mode The compilation mode - `'development'` means that file modification times will be checked on each load, `'production'` means that the cache files will only be regenerated when missing
* @param fDirectory|string $cache_directory The directory to cache the compiled files into - this directory should not be accessible from the web
* @return void
*/
public function enablePHPShortTags($mode, $cache_directory)
{
// This does not need to be enabled if short tags are on
if (ini_get('short_open_tag') && strtolower(ini_get('short_open_tag')) != 'off') {
return;
}
$valid_modes = array('development', 'production');
if (!in_array($mode, $valid_modes)) {
throw new fProgrammerException(
'The mode specified, %1$s, is invalid. Must be one of: %2$s.',
$mode,
join(', ', $valid_modes)
);
}
$cache_directory = $cache_directory instanceof fDirectory ? $cache_directory->getPath() : $cache_directory;
if (!is_writable($cache_directory)) {
throw new fEnvironmentException(
'The cache directory specified, %s, is not writable',
$cache_directory
);
}
$this->short_tag_mode = $mode;
$this->short_tag_directory = fDirectory::makeCanonical($cache_directory);
}
/**
* Gets the value of an element and runs it through fHTML::encode()
*
* @param string $element The element to get - array elements can be accessed via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
* @param mixed $default_value The value to return if the element has not been set
* @return mixed The value of the element specified run through fHTML::encode(), or the default value if it has not been set
*/
public function encode($element, $default_value=NULL)
{
return fHTML::encode($this->get($element, $default_value));
}
/**
* Removes a value from an array element
*
* @param string $element The element to remove from - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
* @param mixed $value The value to remove - compared in a non-strict manner, such that removing `0` will remove a blank string and false also
* @return fTemplating The template object, to allow for method chaining
*/
public function filter($element, $value)
{
$tip =& $this->elements;
if ($bracket_pos = strpos($element, '[')) {
$original_element = $element;
$array_dereference = substr($element, $bracket_pos);
$element = substr($element, 0, $bracket_pos);
preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
$array_keys = array_map('current', $array_keys);
array_unshift($array_keys, $element);
foreach (array_slice($array_keys, 0, -1) as $array_key) {
if (!isset($tip[$array_key])) {
return $this;
} elseif (!is_array($tip[$array_key])) {
throw new fProgrammerException(
'%1$s was called for an element, %2$s, which is not an array',
'filter()',
$original_element
);
}
$tip =& $tip[$array_key];
}
$element = end($array_keys);
}
if (!isset($tip[$element])) {
return $this;
} elseif (!is_array($tip[$element])) {
throw new fProgrammerException(
'%1$s was called for an element, %2$s, which is not an array',
'filter()',
$element
);
}
$keys = array_keys($tip[$element], $value);
if ($keys) {
foreach ($keys as $key) {
unset($tip[$element][$key]);
}
$tip[$element] = array_values($tip[$element]);
}
return $this;
}
/**
* Takes an array of PHP files and caches a version with all short tags converted to regular tags
*
* @param array $values The file paths to the PHP files
* @return array An array of file paths to the corresponding converted PHP files
*/
private function fixShortTags($values)
{
$fixed_paths = array();
foreach ($values as $value) {
// Check to see if the element is a path relative to the template root
if (!preg_match('#^(/|\\\\|[a-z]:(\\\\|/)|\\\\|//|\./|\.\\\\)#i', $value)) {
$value = $this->root . $value;
}
$real_value = realpath($value);
$cache_path = $this->short_tag_directory . sha1($real_value) . '.php';
$fixed_paths[] = $cache_path;
if (file_exists($cache_path) && ($this->short_tag_mode == 'production' || filemtime($cache_path) >= filemtime($real_value))) {
continue;
}
$code = file_get_contents($real_value);
$output = '';
$in_php = FALSE;
do {
if (!$in_php) {
$token_regex = '<\?';
} else {
$token_regex .= '/\*|//|\\#|\'|"|<<<[a-z_]\w*|<<<\'[a-z_]\w*\'|\?>';
}
if (!preg_match('#' . $token_regex . '#i', $code, $match)) {
$part = $code;
$code = '';
$token = NULL;
} else {
$token = $match[0];
$pos = strpos($code, $token);
if ($pos === FALSE) {
break;
}
$part = substr($code, 0, $pos);
$code = substr($code, $pos);
}
$regex = NULL;
if ($token == "<?") {
$output .= $part;
$in_php = TRUE;
continue;
} elseif ($token == "?>") {
$regex = NULL;
$in_php = FALSE;
} elseif ($token == "//") {
$regex = '#^//.*(\n|$)#D';
} elseif ($token == "#") {
$regex = '@^#.*(\n|$)@D';
} elseif ($token == "/*") {
$regex = '#^.{2}.*?(\*/|$)#sD';
} elseif ($token == "'") {
$regex = '#^\'((\\\\.)+|[^\\\\\']+)*(\'|$)#sD';
} elseif ($token == '"') {
$regex = '#^"((\\\\.)+|[^\\\\"]+)*("|$)#sD';
} elseif ($token) {
$regex = '#\A<<<\'?([a-zA-Z_]\w*)\'?.*?^\1;\n#sm';
}
$part = str_replace('<?=', '<?php echo', $part);
$part = preg_replace('#<\?(?!php)#i', '<?php', $part);
// This makes sure that __FILE__ and __DIR__ stay as the
// original value since the cached file will be in a different
// place with a different filename
$part = preg_replace('#(?<=[^a-zA-Z0-9]|^)__FILE__(?=[^a-zA-Z0-9]|$)#iD', "'" . $real_value . "'", $part);
if (fCore::checkVersion('5.3')) {
$part = preg_replace('#(?<=[^a-zA-Z0-9]|^)__DIR__(?=[^a-zA-Z0-9]|$)#iD', "'" . dirname($real_value) . "'", $part);
}
$output .= $part;
if ($regex) {
preg_match($regex, $code, $match);
$output .= $match[0];
$code = substr($code, strlen($match[0]));
}
} while (strlen($code));
file_put_contents($cache_path, $output);
}
return $fixed_paths;
}
/**
* Gets the value of an element
*
* @param string $element The element to get - array elements can be accessed via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
* @param mixed $default_value The value to return if the element has not been set
* @param array |$elements An array of elements to get, or an associative array where a string key is the element to get and the value is the default value
* @return mixed The value of the element(s) specified, or the default value(s) if it has not been set
*/
public function get($element, $default_value=NULL)
{
if (is_array($element)) {
$elements = $element;
// Turn an array of elements into an array of elements with NULL default values
if (array_values($elements) === $elements) {
$elements = array_combine($elements, array_fill(0, count($elements), NULL));
}
$output = array();
foreach ($elements as $element => $default_value) {
$output[$element] = $this->get($element, $default_value);
}
return $output;
}
$array_dereference = NULL;
if ($bracket_pos = strpos($element, '[')) {
$array_dereference = substr($element, $bracket_pos);
$element = substr($element, 0, $bracket_pos);
}
if (!isset($this->elements[$element])) {
return $default_value;
}
$value = $this->elements[$element];
if ($array_dereference) {
preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
$array_keys = array_map('current', $array_keys);
foreach ($array_keys as $array_key) {
if (!is_array($value) || !isset($value[$array_key])) {
$value = $default_value;
break;
}
$value = $value[$array_key];
}
}
return $value;
}
/**
* Gets the root path
*
* @return string The root path for the instance
*/
public function getRootPath()
{
return $this->root;
}
/**
* Combines an array of CSS or JS files and places them as a single file
*
* @param string $type The type of compilation, 'css' or 'js'
* @param string $element The element name
* @param array $values An array of file paths
* @return void
*/
protected function handleMinified($type, $element, $values)
{
$paths = array();
$media = NULL;
foreach ($values as $value) {
if (is_array($value)) {
$paths[] = $this->minification_prefix . $value['path'];
if ($type == 'css') {
$media = !empty($value['media']) ? $value['media'] : NULL;
}
} else {
$paths[] = $this->minification_prefix . $value;
}
}
$hash = sha1(join('|', $paths));
$cache_file = $this->minification_directory . $hash . '.' . $type;
$regenerate = FALSE;
$checked_paths = FALSE;
if (!file_exists($cache_file)) {
$regenerate = TRUE;
} elseif ($this->minification_mode == 'development') {
$cache_mtime = filemtime($cache_file);
$checked_paths = TRUE;
foreach ($paths as $path) {
if (!file_exists($path)) {
throw new fEnvironmentException(
'The file specified, %s, does not exist under the $path_prefix specified',
preg_replace('#^' . preg_quote($this->minification_prefix, '#') . '#', '', $path)
);
}
if (filemtime($path) > $cache_mtime) {
$regenerate = TRUE;
break;
}
}
}
if ($regenerate) {
$minified = '';
foreach ($paths as $path) {
$path_cache_file = $this->minification_directory . sha1($path) . '.' . $type;
if ($checked_paths && !file_exists($path)) {
throw new fEnvironmentException(
'The file specified, %s, does not exist under the $path_prefix specified',
preg_replace('#^' . preg_quote($this->minification_prefix, '#') . '#', '', $path)
);
}
// Checks if this path has been cached
if (file_exists($path_cache_file) && filemtime($path_cache_file) >= filemtime($path)) {
$minified_path = file_get_contents($path_cache_file);
} else {
$minified_path = trim($this->minify(file_get_contents($path), $type));
file_put_contents($path_cache_file, $minified_path);
}
$minified .= "\n" . $minified_path;
}
file_put_contents($cache_file, substr($minified, 1));
}
$version = filemtime($cache_file);
$compiled_value = fFilesystem::translateToWebPath($cache_file) . '?v=' . $version;
if ($type == 'css' && $media) {
$compiled_value = array(
'path' => $compiled_value,
'media' => $media
);
}
$method = 'place' . strtoupper($type);
$this->$method($compiled_value);
}
/**
* Includes the file specified - this is identical to ::place() except a filename is specified instead of an element
*
* Please see the ::place() method for more details about functionality.
*
* @param string $file_path The file to place
* @param string $file_type Will force the file to be placed as this type of file instead of auto-detecting the file type. Valid types include: `'css'`, `'js'`, `'php'` and `'rss'`.
* @return void
*/
public function inject($file_path, $file_type=NULL)
{
$prefix = '__injected_';
$num = 1;
while (isset($this->elements[$prefix . $num])) {
$num++;
}
$element = $prefix . $num;
$this->set($element, $file_path);
$this->place($element, $file_type);
}
/**
* Minifies JS or CSS
*
* For JS, this function is based on the JSMin algorithm (not the code) from
* http://www.crockford.com/javascript/jsmin.html with the addition of
* preserving /*! comment blocks for things like licenses. Some other
* versions of JSMin change the contents of special comment blocks, but
* this version does not.
*
* @param string $code The code to minify
* @param string $type The type of code, 'css' or 'js'
* @return string The minified code
*/
protected function minify($code, $type)
{
$output = '';
$buffer = '';
$stack = array();
$token_regex = '#/\*|\'|"';
if ($type == 'js') {
$token_regex .= '|//';
$token_regex .= '|/';
} elseif ($type == 'css') {
$token_regex .= '|url\(';
}
$token_regex .= '#i';
do {
if (!preg_match($token_regex, $code, $match)) {
$part = $code;
$code = '';
$token = NULL;
} else {
$token = $match[0];
$pos = strpos($code, $token);
if ($pos === FALSE) {
break;
}
$part = substr($code, 0, $pos);
$code = substr($code, $pos);
}
$regex = NULL;
if ($token == '/') {
if (!preg_match('#([(,=:[!&|?{};\n]|\breturn)\s*$#D', $part)) {
$part .= $token;
$code = substr($code, 1);
} else {
$regex = '#^/((\\\\.)+|[^\\\\/]+)*(/|$)#sD';
}
} elseif ($token == "url(") {
$regex = '#^url\(((\\\\.)+|[^\\\\\\)]+)*(\)|$)#sD';
} elseif ($token == "//") {
$regex = '#^//.*(\n|$)#D';
} elseif ($token == "/*") {
$regex = '#^.{2}.*?(\*/|$)#sD';
} elseif ($token == "'") {
$regex = '#^\'((\\\\.)+|[^\\\\\']+)*(\'|$)#sD';
} elseif ($token == '"') {
$regex = '#^"((\\\\.)+|[^\\\\"]+)*("|$)#sD';
}
$this->minifyCode($part, $buffer, $stack, $type);
$output .= $buffer;
$buffer = $part;
if ($regex) {
preg_match($regex, $code, $match);
$code = substr($code, strlen($match[0]));
$this->minifyLiteral($match[0], $buffer, $type);
$output .= $buffer;
$buffer = $match[0];
} elseif (!$token) {
$output .= $buffer;
}
} while (strlen($code));
return $output;
}
/**
* Takes a block of CSS or JS and reduces the number of characters
*
* @param string &$part The part of code to minify
* @param string &$buffer A buffer containing the last code or literal encountered
* @param array $stack A stack used to keep track of the nesting level of CSS
* @param mixed $type The type of code, `'css'` or `'js'`
* @return void
*/
protected function minifyCode(&$part, &$buffer, &$stack, $type='js')
{
// This pulls in the end of the last match for useful context
$end_buffer = substr($buffer, -1);
$lookbehind = in_array($end_buffer, array(' ', "\n")) ? substr($buffer, -2) : $end_buffer;
$buffer = substr($buffer, 0, 0-strlen($lookbehind));
$part = $lookbehind . $part;
if ($type == 'js') {
// All whitespace and control characters are collapsed
$part = preg_replace('#[\x00-\x09\x0B\x0C\x0E-\x20]+#', ' ', $part);
$part = preg_replace('#[\n\r]+#', "\n", $part);
// Whitespace is removed where not needed
$part = preg_replace('#(?<![a-z0-9\x80-\xFF\\\\$_+\-])[ ]+#i', '', $part);
$part = preg_replace('#[ ]+(?![a-z0-9\x80-\xFF\\\\$_+\-])#i', '', $part);
$part = preg_replace('#(?<![a-z0-9\x80-\xFF\\\\$_}\\])"\'+-])\n+#i', '', $part);
$part = preg_replace('#\n+(?![a-z0-9\x80-\xFF\\\\$_{[(+-])#i', '', $part);
// Remove spaces around + and - unless they are followed by a plus or minus
$part = preg_replace('#(?<=[+-])[ ]+(?![+\-])#i', '', $part);
$part = preg_replace('#(?<![+-])[ ]+(?=[+\-])#i', '', $part);
} elseif ($type == 'css') {
// All whitespace is collapsed
$part = preg_replace('#\s+#', ' ', $part);
// Whitespace is removed where not needed
$part = preg_replace('#\s*([;{},>+])\s*#', '\1', $part);
// This keeps track of the current scope since some minification
// rules are different if inside or outside of a rule block
$new_part = '';
do {
if (!preg_match('#@media|\{|\}#', $part, $match)) {
$chunk = $part;
$part = '';
$token = NULL;
} else {
$token = $match[0];
$pos = strpos($part, $token);
if ($pos === FALSE) {
break;
}
$chunk = substr($part, 0, $pos+strlen($token));
$part = substr($part, $pos+strlen($token));
}
if (end($stack) == 'rule_block') {
// Colons don't need space inside of a block
$chunk = preg_replace('#\s*:\s*#', ':', $chunk);
// Useless semi-colons are removed
$chunk = str_replace(';}', '}', $chunk);
// All zero units are reduces to just 0
$chunk = preg_replace('#((?<!\d|\.|\#|\w)0+(\.0+)?|(?<!\d)\.0+)(?=\D|$)((%|in|cm|mm|em|ex|pt|pc|px)(\b|$))?#iD', '0', $chunk);
// All .0 decimals are removed
$chunk = preg_replace('#(\d+)\.0+(?=\D)#iD', '\1', $chunk);
// All leading zeros are removed
$chunk = preg_replace('#(?<!\d)0+(\.\d+)(?=\D)#iD', '\1', $chunk);
// All measurements that contain the same value 4 times are reduced to a single
$chunk = preg_replace('#(?<!\d|\.)([\d\.]+(?:%|in|cm|mm|em|ex|pt|pc|px))(\s*\1){3}#i', '\1', $chunk);
// Hex color codes are reduced if possible
$chunk = preg_replace('@#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3(?!\d)@iD', '#\1\2\3', $chunk);
$chunk = str_ireplace('! important', '!important', $chunk);
} else {
// This handles an IE6 edge-case
$chunk = preg_replace('#(:first-l(etter|ine))\{#', '\1 {', $chunk);
}
$new_part .= $chunk;
if ($token == '@media') {
$stack[] = 'media_rule';
} elseif ($token == '{' && end($stack) == 'media_rule') {
$stack = array('media_block');
} elseif ($token == '{') {
$stack[] = 'rule_block';
} elseif ($token) {
array_pop($stack);
}
} while (strlen($part));
$part = $new_part;
}
}
/**
* Takes a literal and either discards or keeps it
*
* @param mixed &$part The literal to process
* @param mixed &$buffer The last literal or code processed
* @param string $type The language the literal is in, `'css'` or `'js'`
* @return void
*/
protected function minifyLiteral(&$part, &$buffer, $type)
{
// Comments are skipped unless they are special
if (substr($part, 0, 2) == '/*' && substr($part, 0, 3) != '/*!') {
$part = $buffer . ' ';
$buffer = '';
}
if ($type == 'js' && substr($part, 0, 2) == '//') {
$part = $buffer . "\n";
$buffer = '';
}
}
/**
* Includes the element specified - element must be set through ::set() first
*
* If the element is a file path ending in `.css`, `.js`, `.rss` or `.xml`
* an appropriate HTML tag will be printed (files ending in `.xml` will be
* treated as an RSS feed). If the element is a file path ending in `.inc`,
* `.php` or `.php5` it will be included.
*
* Paths that start with `./` will be loaded relative to the current script.
* Paths that start with a file or directory name will be loaded relative
* to the `$root` passed in the constructor. Paths that start with `/` will
* be loaded from the root of the filesystem.
*
* You can pass the `media` attribute of a CSS file or the `title` attribute
* of an RSS feed by adding an associative array with the following formats:
*
* {{{
* array(
* 'path' => (string) {css file path},
* 'media' => (string) {media type}
* );
* array(
* 'path' => (string) {rss file path},
* 'title' => (string) {feed title}
* );
* }}}
*
* @param string $element The element to place
* @param string $file_type Will force the element to be placed as this type of file instead of auto-detecting the file type. Valid types include: `'css'`, `'js'`, `'php'` and `'rss'`.
* @return void
*/
public function place($element='__main__', $file_type=NULL)
{
// Put in a buffered placeholder
if ($this->buffered_id) {
echo '%%fTemplating::' . $this->buffered_id . '::' . $element . '::' . $file_type . '%%';
return;
}
if (!isset($this->elements[$element])) {
return;
}
$this->placeElement($element, $file_type);
}
/**
* Prints a CSS `link` HTML tag to the output
*
* @param mixed $info The path or array containing the `'path'` to the CSS file. Array can also contain a key `'media'`.
* @return void
*/
protected function placeCSS($info)
{
if (!is_array($info)) {
$info = array('path' => $info);
}
if (!isset($info['media'])) {
$info['media'] = 'all';
}
echo '<link rel="stylesheet" type="text/css" href="' . $info['path'] . '" media="' . $info['media'] . '" />' . "\n";
}
/**
* Performs the action of actually placing an element
*
* @param string $element The element that is being placed
* @param string $file_type The file type to treat all values as
* @return void
*/
protected function placeElement($element, $file_type)
{
$values = $this->elements[$element];
if (!is_object($values)) {
settype($values, 'array');
} else {
$values = array($values);
}
$values = array_values($values);
$value_groups = array();
$last_type = NULL;
$last_location = NULL;
$last_media = NULL;
foreach ($values as $i => $value) {
$type = $this->verifyValue($element, $value, $file_type);
$media = is_array($value) && isset($value['media']) ? $value['media'] : NULL;
$path = is_array($value) ? $value['path'] : $value;
$location = is_string($path) && preg_match('#^https?://#', $path) ? 'http' : 'local';
if ($type != $last_type || $location != $last_location || $media != $last_media) {
$value_groups[] = array(
'type' => $type,
'location' => $location,
'values' => array()
);
}
$value_groups[count($value_groups)-1]['values'][] = $value;
$last_type = $type;
$last_location = $location;
$last_media = $media;
}
foreach ($value_groups as $value_group) {
if ($value_group['location'] == 'local') {
if ($this->minification_directory && in_array($value_group['type'], array('js', 'css'))) {
$this->handleMinified($value_group['type'], $element, $value_group['values']);
continue;
}
if ($this->short_tag_directory && $value_group['type'] == 'php') {
$value_group['values'] = $this->fixShortTags($value_group['values']);
}
}
foreach ($value_group['values'] as $value) {
switch ($value_group['type']) {
case 'css':
$this->placeCSS($value);
break;
case 'fTemplating':
// This causes children to inherit settings if they aren't already set
if ($value->minification_directory === NULL) {
$value->minification_directory = $this->minification_directory;
$value->minification_mode = $this->minification_mode;
$value->minification_prefix = $this->minification_prefix;
}
if ($value->short_tag_directory === NULL) {
$value->short_tag_directory = $this->short_tag_directory;
$value->short_tag_mode = $this->short_tag_mode;
}
$value->place();
break;
case 'js':
$this->placeJS($value);
break;
case 'php':
$this->placePHP($element, $value);
break;
case 'rss':
$this->placeRSS($value);
break;
default:
throw new fProgrammerException(
'The file type specified, %1$s, is invalid. Must be one of: %2$s.',
$type,
'css, js, php, rss'
);
}
}
}
}
/**
* Prints a java`script` HTML tag to the output
*
* @param mixed $info The path or array containing the `'path'` to the javascript file
* @return void
*/
protected function placeJS($info)
{
if (!is_array($info)) {
$info = array('path' => $info);
}
echo '<script type="text/javascript" src="' . $info['path'] . '"></script>' . "\n";
}
/**
* Includes a PHP file
*
* @param string $element The element being placed
* @param string $path The path to the PHP file
* @return void
*/
protected function placePHP($element, $path)
{
// Check to see if the element is a path relative to the template root
if (!preg_match('#^(/|\\\\|[a-z]:(\\\\|/)|\\\\|//|\./|\.\\\\)#i', $path)) {
$path = $this->root . $path;
}
if (!file_exists($path)) {
throw new fProgrammerException(
'The path specified for %1$s, %2$s, does not exist on the filesystem',
$element,
$path
);
}
if (!is_readable($path)) {
throw new fEnvironmentException(
'The path specified for %1$s, %2$s, is not readable',
$element,
$path
);
}
include($path);
}
/**
* Prints an RSS `link` HTML tag to the output
*
* @param mixed $info The path or array containing the `'path'` to the RSS xml file. May also contain a `'title'` key for the title of the RSS feed.
* @return void
*/
protected function placeRSS($info)
{
if (!is_array($info)) {
$info = array(
'path' => $info,
'title' => fGrammar::humanize(
preg_replace('#.*?([^/]+).(rss|xml)$#iD', '\1', $info)
)
);
}
if (!isset($info['title'])) {
throw new fProgrammerException(
'The RSS value %s is missing the title key',
$info
);
}
echo '<link rel="alternate" type="application/rss+xml" href="' . $info['path'] . '" title="' . $info['title'] . '" />' . "\n";
}
/**
* Performs buffered replacements using a breadth-first technique
*
* @return void
*/
private function placeBuffered()
{
if (!$this->buffered_id) {
return;
}
$contents = fBuffer::get();
fBuffer::erase();
// We are gonna use a regex replacement that is eval()'ed as PHP code
$regex = '/%%fTemplating::' . $this->buffered_id . '::(.*?)::(.*?)%%/';
// Remove the buffered id, thus making any nested place() calls be executed immediately
$this->buffered_id = NULL;
echo preg_replace_callback($regex, array($this, 'placeBufferedCallback'), $contents);
}
/**
* Performs a captured place of an element to use with buffer placing
*
* @param array $match A regex match from ::placeBuffered()
* @return string The output of placing the element
*/
private function placeBufferedCallback($match)
{
fBuffer::startCapture();
$this->placeElement($match[1], $match[2]);
return fBuffer::stopCapture();
}
/**
* Gets the value of an element and runs it through fHTML::prepare()
*
* @param string $element The element to get - array elements can be access via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
* @param mixed $default_value The value to return if the element has not been set
* @return mixed The value of the element specified run through fHTML::prepare(), or the default value if it has not been set
*/
public function prepare($element, $default_value=NULL)
{
return fHTML::prepare($this->get($element, $default_value));
}
/**
* Removes and returns the value from the end of an array element
*
* @param string $element The element to remove from to - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
* @param boolean $beginning If the value should be removed from the beginning of the element
* @return mixed The value that was removed
*/
public function remove($element, $beginning=FALSE)
{
$tip =& $this->elements;
if ($bracket_pos = strpos($element, '[')) {
$original_element = $element;
$array_dereference = substr($element, $bracket_pos);
$element = substr($element, 0, $bracket_pos);
preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
$array_keys = array_map('current', $array_keys);
array_unshift($array_keys, $element);
foreach (array_slice($array_keys, 0, -1) as $array_key) {
if (!isset($tip[$array_key])) {
return NULL;
} elseif (!is_array($tip[$array_key])) {
throw new fProgrammerException(
'%1$s was called for an element, %2$s, which is not an array',
'remove()',
$original_element
);
}
$tip =& $tip[$array_key];
}
$element = end($array_keys);
}
if (!isset($tip[$element])) {
return NULL;
} elseif (!is_array($tip[$element])) {
throw new fProgrammerException(
'%1$s was called for an element, %2$s, which is not an array',
'remove()',
$element
);
}
if ($beginning) {
return array_shift($tip[$element]);
}
return array_pop($tip[$element]);
}
/**
* Sets the value for an element
*
* @param string $element The element to set - the magic element `__main__` is used for placing the current fTemplating object as a child of another fTemplating object - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
* @param mixed $value The value for the element
* @param array :$elements An associative array with the key being the `$element` to set and the value being the `$value` for that element
* @return fTemplating The template object, to allow for method chaining
*/
public function set($element, $value=NULL)
{
if ($value === NULL && is_array($element)) {
foreach ($element as $key => $value) {
$this->set($key, $value);
}
return $this;
}
$tip =& $this->elements;
if ($bracket_pos = strpos($element, '[')) {
$array_dereference = substr($element, $bracket_pos);
$element = substr($element, 0, $bracket_pos);
preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
$array_keys = array_map('current', $array_keys);
array_unshift($array_keys, $element);
foreach (array_slice($array_keys, 0, -1) as $array_key) {
if (!isset($tip[$array_key]) || !is_array($tip[$array_key])) {
$tip[$array_key] = array();
}
$tip =& $tip[$array_key];
}
$element = end($array_keys);
}
$tip[$element] = $value;
return $this;
}
/**
* Ensures the value is valid
*
* @param string $element The element that is being placed
* @param mixed $value A value to be placed
* @param string $file_type The file type that this element will be displayed as - skips checking file extension
* @return string The file type of the value being placed
*/
protected function verifyValue($element, $value, $file_type=NULL)
{
if (!$value && !is_numeric($value)) {
throw new fProgrammerException(
'The element specified, %s, has a value that is empty',
$value
);
}
if (is_array($value) && !isset($value['path'])) {
throw new fProgrammerException(
'The element specified, %1$s, has a value, %2$s, that is missing the path key',
$element,
$value
);
}
if ($file_type) {
return $file_type;
}
if ($value instanceof self) {
return 'fTemplating';
}
$path = (is_array($value)) ? $value['path'] : $value;
$path = preg_replace('#\?.*$#D', '', $path);
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
// Allow some common variations on file extensions
$extension_map = array(
'inc' => 'php',
'php5' => 'php',
'xml' => 'rss',
'sass' => 'css',
'scss' => 'css'
);
if (isset($extension_map[$extension])) {
$extension = $extension_map[$extension];
}
if (!in_array($extension, array('css', 'js', 'php', 'rss'))) {
throw new fProgrammerException(
'The element specified, %1$s, has a value whose path, %2$s, does not end with a recognized file extension: %3$s.',
$element,
$path,
'.css, .inc, .js, .php, .php5, .rss, .xml'
);
}
return $extension;
}
}
/**
* Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>, others
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/