Location: PHPKode > projects > Maintainable PHP Framework > vendor/Mad/View/Helper/AssetTag.php
<?php
/**
 * @category   Mad
 * @package    Mad_View
 * @subpackage Helper
 * @copyright  (c) 2007-2009 Maintainable Software, LLC
 * @license    http://opensource.org/licenses/bsd-license.php BSD
 */


/**
 * This class provides methods for generating HTML that links views to assets such
 * as images, javascripts, stylesheets, and feeds. These methods do not verify 
 * the assets exist before linking to them. 
 *
 * === Using asset hosts
 * By default, Rails links to these assets on the current host in the public
 * folder, but you can direct Rails to link to assets from a dedicated assets server by 
 * setting ActionController::Base.asset_host in your environment.rb.  For example,
 * let's say your asset host is assets.example.com. 
 *
 *   ActionController::Base.asset_host = "assets.example.com"
 *   image_tag("rails.png")
 *     => <img src="http://assets.example.com/images/rails.png" alt="Rails" />
 *   stylesheet_include_tag("application")
 *     => <link href="http://assets.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
 *
 * This is useful since browsers typically open at most two connections to a single host,
 * which means your assets often wait in single file for their turn to load.  You can
 * alleviate this by using a %d wildcard in <tt>asset_host</tt> (for example, "assets%d.example.com") 
 * to automatically distribute asset requests among four hosts (e.g., assets0.example.com through assets3.example.com)
 * so browsers will open eight connections rather than two.  
 *
 *   image_tag("rails.png")
 *     => <img src="http://assets0.example.com/images/rails.png" alt="Rails" />
 *   stylesheet_include_tag("application")
 *     => <link href="http://assets3.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
 *
 * To do this, you can either setup four actual hosts, or you can use wildcard DNS to CNAME 
 * the wildcard to a single asset host.  You can read more about setting up your DNS CNAME records from
 * your ISP.
 *
 * Note: This is purely a browser performance optimization and is not meant
 * for server load balancing. See http://www.die.net/musings/page_load_time/
 * for background.
 *
 * @category   Mad
 * @package    Mad_View
 * @subpackage Helper
 * @copyright  (c) 2007-2009 Maintainable Software, LLC
 * @license    http://opensource.org/licenses/bsd-license.php BSD
 */
class Mad_View_Helper_AssetTag extends Mad_View_Helper_Base
{
    /**
     * All stylesheet filenames in the stylesheets/ directory
     * @var null|array
     */
    protected $_allStylesheetSources;
    
    /**
     * Returns the directory where the assets are stored.
     * @return string
     */
    public function getAssetsDir() 
    {
        return defined('MAD_ROOT') ? MAD_ROOT.'/public' : 'public';
    }
    
    /**
     * Returns the directory where the Javascript assets are stored.
     * @return string
     */
    public function getJavascriptsDir() 
    {
        $assetsDir = $this->getAssetsDir();
        return "$assetsDir/javascripts";
    }

    /**
     * Returns the directory where the stylesheet assets are stored.
     * @return string
     */
    public function getStylesheetsDir() 
    {
        $assetsDir = $this->getAssetsDir();
        return "$assetsDir/stylesheets";
    }

    /**
     * Computes the path to a javascript asset in the public javascripts directory.
     * If the +source+ filename has no extension, .js will be appended.
     * Full paths from the document root will be passed through.
     * Used internally by javascript_include_tag to build the script path.
     *
     * ==== Examples
     *   javascriptPath("xmlhr")                                       # => /javascripts/xmlhr.js
     *   javascriptPath("dir/xmlhr.js")                                # => /javascripts/dir/xmlhr.js
     *   javascriptPath("/dir/xmlhr")                                  # => /dir/xmlhr.js
     *   javascriptPath("http://www.railsapplication.com/js/xmlhr")    # => http://www.railsapplication.com/js/xmlhr.js
     *   javascriptPath("http://www.railsapplication.com/js/xmlhr.js") # => http://www.railsapplication.com/js/xmlhr.js
     *
     * @param  string  $source  Source filename
     * @return string           Computed path to source filename
     */
    public function javascriptPath($source)
    {
        return $this->_computePublicPath($source, 'javascripts', 'js');
    }
    
    /**
     * Returns an array with the default Javascript sources.
     *
     * @return array
     */
    public function getJavascriptDefaultSources() 
    {
        return array('prototype', 'effects', 'dragdrop', 'controls');
    }

    /**
     * Computes the path to a stylesheet asset in the public stylesheets directory.
     * If the +source+ filename has no extension, .css will be appended.
     * Full paths from the document root will be passed through.
     * Used internally by stylesheet_link_tag to build the stylesheet path.
     *
     * ==== Examples
     *   stylesheetPath("style") # => /stylesheets/style.css
     *   stylesheetPath("dir/style.css") # => /stylesheets/dir/style.css
     *   stylesheetPath("/dir/style.css") # => /dir/style.css
     *   stylesheetPath("http://www.railsapplication.com/css/style") # => http://www.railsapplication.com/css/style.css
     *   stylesheetPath("http://www.railsapplication.com/css/style.js") # => http://www.railsapplication.com/css/style.css
     */
    public function stylesheetPath($source)
    {
        return $this->_computePublicPath($source, 'stylesheets', 'css');        
    }

    /**
     * Returns a stylesheet link tag for the sources specified as arguments. If
     * you don't specify an extension, .css will be appended automatically.
     * You can modify the link attributes by passing a hash as the last argument.
     *
     * ==== Examples
     *   stylesheetLinkTag("style") # =>
     *     <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" />
     *
     *   stylesheetLinkTag("style.css") # =>
     *     <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" />
     *
     *   stylesheetLinkTag("http://www.railsapplication.com/style.css") # =>
     *     <link href="http://www.railsapplication.com/style.css" media="screen" rel="stylesheet" type="text/css" />
     *
     *   stylesheetLinkTag("style", :media => "all") # =>
     *     <link href="/stylesheets/style.css" media="all" rel="stylesheet" type="text/css" />
     *
     *   stylesheetLinkTag("style", :media => "print") # =>
     *     <link href="/stylesheets/style.css" media="print" rel="stylesheet" type="text/css" />
     *
     *   stylesheetLinkTag("random.styles", "/css/stylish") # =>
     *     <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" />
     *     <link href="/css/stylish.css" media="screen" rel="stylesheet" type="text/css" />
     *
     * You can also include all styles in the stylesheet directory using 'all' as the source:
     *
     *   stylesheetLinkTag('all') # =>
     *     <link href="/stylesheets/style1.css"  media="screen" rel="stylesheet" type="text/css" />
     *     <link href="/stylesheets/styleB.css"  media="screen" rel="stylesheet" type="text/css" />
     *     <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" />
     *
     * == Caching multiple stylesheets into one
     *
     * You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be
     * compressed by gzip (leading to faster transfers). Caching will only happen if 'perform_caching'
     * is set to true (which is the case by default for the Mad production environment, but not for the development
     * environment). Examples:
     *
     * ==== Examples
     *   stylesheetLinkTag('all', array('cache' => true)) # when 'perform_caching' is false =>
     *     <link href="/stylesheets/style1.css"  media="screen" rel="stylesheet" type="text/css" />
     *     <link href="/stylesheets/styleB.css"  media="screen" rel="stylesheet" type="text/css" />
     *     <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" />
     *
     *   stylesheetLinkTag('all', array('cache' => true)) # when 'perform_caching' is true =>
     *     <link href="/stylesheets/all.css"  media="screen" rel="stylesheet" type="text/css" />
     *
     *   stylesheetLinkTag("shop", "cart", "checkout", array('cache' => "payment")) # when 'perform_caching' is false =>
     *     <link href="/stylesheets/shop.css"  media="screen" rel="stylesheet" type="text/css" />
     *     <link href="/stylesheets/cart.css"  media="screen" rel="stylesheet" type="text/css" />
     *     <link href="/stylesheets/checkout.css" media="screen" rel="stylesheet" type="text/css" />
     *
     *   stylesheetLinkTag("shop", "cart", "checkout", array('cache' => "payment")) # when ActionController::Base.perform_caching is true =>
     *     <link href="/stylesheets/payment.css"  media="screen" rel="stylesheet" type="text/css" />
     *
     * @todo caching needs to be implemented
     */
    public function stylesheetLinkTag($sources) 
    {
        $sources = func_get_args();
        $options = (is_array(end($sources))) ? array_pop($sources) : array();
        
        $sources = $this->_expandStylesheetSources($sources);
        $tags = array();
        foreach ($sources as $source) {
            $defaults = array('rel'   => 'stylesheet',
                              'type'  => 'text/css',
                              'media' => 'screen',
                              'href'  => $this->h($this->stylesheetPath($source)));
            $tags[] = $this->tag('link', array_merge($defaults, $options), false, false);
        }
        $tags = implode("\n", $tags);
        return $tags;
    }
    
    /**
     * @todo caching needs to be implemented
     */
    public function javascriptIncludeTag($sources)
    {
        $sources = func_get_args();
        $options = (is_array(end($sources))) ? array_pop($sources) : array();
   
        $sources = $this->_expandJavascriptSources($sources);

        $tags = array();
        foreach ($sources as $source) {
            $defaults = array('type'  => 'text/javascript',
                              'src'   => $this->javascriptPath($source));
            $tags[] = $this->contentTag('script', '', array_merge($defaults, $options));
        }                
        $tags = implode("\n", $tags);
        return $tags;
    }
    
    /**
     * Computes the path to an image asset in the public images directory.
     * Full paths from the document root will be passed through.
     * Used internally by image_tag to build the image path.
     *
     * ==== Examples
     *   image_path("edit")                                         # => /images/edit
     *   image_path("edit.png")                                     # => /images/edit.png
     *   image_path("icons/edit.png")                               # => /images/icons/edit.png
     *   image_path("/icons/edit.png")                              # => /icons/edit.png
     *   image_path("http://www.railsapplication.com/img/edit.png") # => http://www.railsapplication.com/img/edit.png
     * 
     * @todo implement this
     * @param   string  $source
     */
    public function imagePath($source)
    {
        return $this->_computePublicPath($source, 'images');
    }
    
    /**
     * Returns an html image tag for the +source+. The +source+ can be a full
     * path or a file that exists in your public images directory.
     *
     * ==== Options
     * You can add HTML attributes using the +options+. The +options+ supports
     * two additional keys for convienence and conformance:
     *
     * * <tt>:alt</tt>  - If no alt text is given, the file name part of the
     *   +source+ is used (capitalized and without the extension)
     * * <tt>:size</tt> - Supplied as "{Width}x{Height}", so "30x45" becomes
     *   width="30" and height="45". <tt>:size</tt> will be ignored if the
     *   value is not in the correct format.
     *
     * ==== Examples
     *  $this->imageTag("icon")  # =>
     *    <img src="/images/icon" alt="Icon" />
     * 
     *  $this->imageTag("icon.png")  # =>
     *    <img src="/images/icon.png" alt="Icon" />
     * 
     *  $this->imageTag("icon.png", :size => "16x10", :alt => "Edit Entry")  # =>
     *    <img src="/images/icon.png" width="16" height="10" alt="Edit Entry" />
     * 
     *  $this->imageTag("/icons/icon.gif", :size => "16x16")  # =>
     *    <img src="/icons/icon.gif" width="16" height="16" alt="Icon" />
     * 
     *  $this->imageTag("/icons/icon.gif", :height => '32', :width => '32') # =>
     *    <img alt="Icon" height="32" src="/icons/icon.gif" width="32" />
     * 
     *  $this->imageTag("/icons/icon.gif", :class => "menu_icon") # =>
     *    <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
     */
    public function imageTag($source, $options = array())
    {
        $options['src'] = $this->imagePath($source);
        $options['alt'] = isset($options['alt']) ? $options['alt'] : '';
        
        // set alt based on src name
        if (empty($options['alt'])) {
            // pathinfo() has pathinfo_filename support only since php 5.2
            if (defined('PATHINFO_FILENAME')) {
                $name = pathinfo($options['src'], PATHINFO_FILENAME);
            } else {
                // pathinfo() array keys are not guaranteed to be present
                $pathstub = array('basename' => '', 'extension' => '');
                $pathinfo = array_merge($pathstub, pathinfo($options['src']));

                // "logo.gif?1190248363" -> "logo"
                $name = substr($pathinfo['basename'], 0, -(strlen($pathinfo['extension'])+1));
            }
            
            $options['alt'] = ucfirst($name);
        }

        // calc width/height
        if (isset($options['size'])) {
            if (preg_match('/^\d+x\d+$/', $options['size'])) {
                $dimensions = explode('x', $options['size']);
                list($options['width'], $options['height']) = $dimensions;
            }
            unset($options['size']);
        }
        return $this->tag("img", $options);
    }
    
    /**
     * Add the .ext if not present. Return full URLs otherwise untouched.
     * Prefix with /dir/ if lacking a leading /. Account for relative URL
     * roots. Rewrite the asset path for cache-busting asset ids. Include
     * a single or wildcarded asset host, if configured, with the correct
     * request protocol.
     */
    protected function _computePublicPath($source, $dir, $ext = null, $includeHost = true) 
    {
        $extension = pathinfo($source, PATHINFO_EXTENSION);
        if (empty($extension) && !empty($ext)) {
            $source .= ".$ext";
        }
        
        if (preg_match("/^[-a-z]+:\/\//", $source)) {
            return $source;
        } else {
             if (substr($source, 0, 1) != '/') {
                 $source = "/$dir/$source";
             }
             $source = $this->_rewriteAssetPath($source);
             
             if ($includeHost) {
                 $host = $this->_computeAssetHost($source);
                 if (!empty($host) && !preg_match("/^[-a-z]+:\/\//", $host)) {
                     $host = $this->controller->getRequest()->getProtocol().$host;
                 }
                 return $host.$source;
             } else {
                 return $source;
             }
        }
    }

    /**
     * Pick an asset host for this source. Returns nil if no host is set,
     * the host if no wildcard is set, or the host interpolated with the
     * numbers 0-3 if it contains %d. The number is the source hash mod 4.
     * 
     * @todo    implement
     * @param   string  $source
     */
    protected function _computeAssetHost($source)
    {
        return null;
    }

    /**
     * Use the MAD_ASSET_ID environment variable or the source's
     * modification time as its cache-busting asset id.
     * 
     * @todo    implement
     * @param   string  $source
     */
    protected function _madAssetId($source)
    { 
        if (isset($_SERVER['MAD_ASSET_ID'])) {
            return $_SERVER['MAD_ASSET_ID'];
        }
        return @filemtime($this->getAssetsDir() . "/$source");
    }
    
    /**
     * Break out the asset path rewrite so you wish to put the asset id
     * someplace other than the query string.
     * 
     * @param   string  $source
     */
    protected function _rewriteAssetPath($source)
    {
        $assetId = $this->_madAssetId($source);
        if (!empty($assetId)) { $source .= "?$assetId"; }
        return $source;
    }

    /**
     * @todo finish defaults
     */
    protected function _expandJavascriptSources($sources)
    {
        if (in_array('all', $sources)) {
            $dir = $this->getJavascriptsDir();
            $allJavascriptFiles = $this->_filesInDirWithExt($dir, 'js');
            
            $sources = array_intersect($allJavascriptFiles, $this->getJavascriptDefaultSources());
            $sources = array_merge($sources, $allJavascriptFiles);
            
        } else if (in_array('defaults', $sources)) {
            // @todo finish defaults
        }

        return $sources;
    }
    
    /**
     * @todo in the future these should be cached on the server
     */
    protected function _expandStylesheetSources($sources)
    {
        if ($sources[0] != 'all') { return $sources; }

        if ($this->_allStylesheetSources === null) {
            $dir = $this->getStylesheetsDir();
            $this->_allStylesheetSources = $this->_filesInDirWithExt($dir, 'css');
        }
        return $this->_allStylesheetSources;
    }

    /**
     * Returns a sorted array of files in $path that have an extension $ext.
     *
     * @param  string  $path  Path to directory
     * @param  string  $ext   Extension to find ('ext' not '.ext')
     * @return array          Array of files, or empty array
     */
    protected function _filesInDirWithExt($path, $ext)
    {
        $files = array();
        $extpos = -(strlen($ext) +1);
        foreach(new DirectoryIterator($path) as $f) {
            $filename = $f->getFilename();
            if ($f->isFile() && substr($filename, $extpos) == ".$ext") {
                $files[] = basename($filename);
            }            
        }        
        sort($files);
        return $files;
    }
}
Return current item: Maintainable PHP Framework