NOTE: This is now deprecated and superceded by SmartyDoc.

[Discuss at the forums]

NOTE: probably requires most recent CVS version of Smarty (2.6.10-dev)

This is my most recent attempt at giving template users control over the "header" problem. This Smarty class extension has two built-in plugins and a special outputfilter. It also declares two new methods for invoking fetching/display. It was written under PHP5 but should work well with PHP4 if you are willing to downgrade the PHP5 specific keywords. It is additionally suggested that you extend your custom Smarty class rather than extending Smarty directly as shown.

Use fetchDoc() or displayDoc() when you want to invoke the SmartyDocInfo features. This should only be done if you want to create an actual and complete document as it will automatically generate a doctype signature and appropriate HTML/XHTML for the header and body of the document. Use fetch()/display() to use Smarty's default behaviour, for example, from plugins.

There are two built-in plugins which can be used at any time and from any template regardless of the inclusion order. These two plugins are used to communicate to the SmartyDocInfo class information that should be generated in the document head of the generated output during a fetchDoc()/displayDoc() call.

The first plugin is a block plugin, {doc_raw} ... {doc_raw}. Everything produced by this block is moved to the document header instead of being emitted in the template that specifies it. This is useful if you want to include some javascript or styling information from a template.

The main plugin is {doc_info} and it is used to communicate most of the needed information. It has a special usage pattern to simplify things. Mainly it only requires a single parameter which is dependant on the type of information you are trying to send. Typically, this parameter will be the main parameter of the associated tag that you are trying to specify. Optional parameters can also be specified and all of the HTML4 and XHTML parameters are supported for most tags. See the code for the various parameters that are available. Note that like all Smarty plugins, parameter casing is significant. Note that duplicates are automatically purged and defined {doc_info} data can be overwritten by a later specification.

Examples best show its usage.

{* set a specific doctype signature -- once set, it can not be reset *}
{* Supported types: DOCTYPE: XHTML, HTML *}
{* Supported types: LEVEL: Strict, Transitional, Frameset *}
{doc_info DOCTYPE=XHTML LEVEL=Transitional}

{* include some javascript *}
{doc_info script="/foo/bar.js"}
{doc_info script="/foo/bar.js" defer="1"}
{doc_info script="/foo/bar.js" type="text/javascript"} {* this is the default type so it doesn't need to be specified *}

{* include a base tag with optional target *}
{doc_info base="http://localhost/" target="foo"}

{* append some body onload info *}
{doc_info body="MyFunction();"}

{* include some CSS *}
{doc_info link="mystyle.css"} {* defaults to rel="stylesheet" *}

{* change the document title *}
{doc_info title="A custom Title From some template"}

{* add some meta tag info *}
{doc_info meta="foobar" content="I am foobar"}

{* show off doc_raw *}
{doc_raw}
<script>
  alert('this is in the document head!');
</script>
{/doc_raw}

BUT WAIT, there's more! There is a public API which you use to control various features of the SmartyDocInfo class.

setIndent($string): used to specificy the amount and type of indenting you wish to use for generated tags. The default is 4 spaces.

$smarty->setIndent('  '); // two spaces

setJavascriptUrl($url): base Javascript Url

setCSSUrl($url): base CSS Url

$smarty->setJavascriptUrl('http://localhost/Public/Javascript/');
---
{doc_info script="foo.js"}

causes the following to be generated in the document head:

    <script type="text/javascript" src="http://localhost/Public/Javascript/foo.js"></script>

...you can override this behaviour by using an absolute path:

    {doc_info script="http://localhost/foo.js"}
    {doc_info script="/localhost/bar.js"}

generates:

    <script type="text/javascript" src="http://localhost/foo.js"></script>
    <script type="text/javascript" src="/localhost/bar.js"></script>

resetDocInfo(): clears the internal doc info array

setDocTypeFamily($family): specifies the DOCTYPE family (recognized values: 'HTML', 'XHTML')

setDocTypeLevel($level): specifies the DOCTYPE level (recognized values: 'Strict', 'Transitional', 'Frameset')

addDocInfoRaw($content): same as {doc_raw}

addDocInfo($params): same as {doc_info}

getDocTypeInfo(): returns an array describing the current DOCTYPE (currently a single element, 'signature')

fetchDoc($resource_name, $cache_id=null, $compile_id=null, $display=false)

displayDoc($resource_name, $cache_id=null, $compile_id=null)

ENJOY!

<?php
/* $Id: $ */
/**
 * Project:     LAF: the Lazy Abstract Framework
 *
 * @package     LAF
 * @author      boots [jayboots ~ yahoo com]
 * @version     0.3.1 2005-Jul-27 (@since 2005-May-16)
 * @copyright   brainpower, boots, 2002-2005
 * @license     LGPL
 */

class Render_SmartyDocInfo extends Smarty
{
    protected $doc_info = array();
    protected $DOCTYPES = array(
      'HTML' => array(
          'Strict' => array(
            'signature' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
          )
        , 'Transitional' => array(
            'signature' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
          )
        , 'Frameset' => array(
            'signature' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
          )
        )
    , 'XHTML' => array(
          'Strict' => array(
            'signature' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
          )
        , 'Transitional' => array(
            'signature' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
          )
        , 'Frameset' => array(
            'signature' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
          )
        )
    );
    protected $doc_info_types = array(
      'title' => array(
          'renameto' => null
        , 'optional' => array('id', 'class', 'dir', 'lang', 'style', 'xml__lang')
        , 'defaults' => array()
        )
    , 'meta' => array(
          'renameto' => 'name'
        , 'optional' => array('content', 'http_equiv', 'scheme', 'dir', 'lang', 'xml__lang')
        , 'defaults' => array('content'=>'')
        )
    , 'link' => array(
          'renameto' => 'href'
        , 'optional' => array('rel', 'id', 'class', 'dir', 'lang', 'style', 'xml__lang', 'type', 'target', 'rev', 'media', 'hreflang', 'charset')
        , 'defaults' => array('rel'=>'stylesheet')
        )
    , 'script' => array(
          'renameto' => 'src'
        , 'optional' => array('type', 'defer', 'xml__space', 'charset', 'language')
        , 'defaults' => array('type'=>'text/javascript')
        )
    , 'base' => array(
          'renameto' => 'href'
        , 'optional' => array('target')
        , 'defaults' => array()
        )
    , 'body' => array(
          'renameto' => 'onload'
        , 'optional' => array()
        , 'defaults' => array()
        )
    , 'DOCTYPE' => array(
          'renameto' => 'FAMILY'
        , 'optional' => array('LEVEL')
        , 'defaults' => array('LEVEL'=>'Transitional')
        )
    );
    private $doc_indent = '    ';
    private $doc_css_url = '';
    private $doc_javascript_url = '';

    public function __construct()
    {
        Render_LocalSmarty::__construct();
        $this->register_function('doc_info', array($this, 'smarty_function_doc_info'), false);
        $this->register_block('doc_raw', array($this, 'smarty_block_doc_raw'), false);
    }

    public function fetch($resource_name, $cache_id=null, $compile_id=null, $display=false)
    {
        $outputfilter_loaded = array_key_exists('smarty_outputfilter_DocInfo', $this->_plugins['outputfilter']);
        if ($outputfilter_loaded) {
            $this->unregister_outputfilter('smarty_outputfilter_DocInfo');
        }
        $output = parent::fetch($resource_name, $cache_id, $compile_id);
        if ($outputfilter_loaded) {
            $this->register_outputfilter(array($this, 'smarty_outputfilter_DocInfo'));
        }
        if (!$display) return $output;
        echo $output;
    }

    public function displayDoc($resource_name, $cache_id=null, $compile_id=null)
    {
        $this->fetchDoc($resource_name, $cache_id, $compile_id, true);
    }

    public function fetchDoc($resource_name, $cache_id=null, $compile_id=null, $display=false)
    {
        $this->register_outputfilter(array($this, 'smarty_outputfilter_DocInfo'));
        $output = parent::fetch($resource_name, $cache_id, $compile_id);
        $this->unregister_outputfilter('smarty_outputfilter_DocInfo');
        $this->resetDocInfo();
        if (!$display) return $output;
        echo $output;
    }

    public function resetDocInfo()
    {
        $this->doc_info = array();
    }

    public function setDocTypeFamily($family)
    {
        if (isset($family) && !empty($family) && is_string($family)) {
            $this->doc_info['DOCTYPE']['FAMILY'] = $family;
        }
    }

    public function setDocTypeLevel($level)
    {
        if (isset($level) && !empty($level) && is_string($level)) {
            $this->doc_info['DOCTYPE']['LEVEL'] = $level;
        }
    }

    public function setCSSUrl($url)
    {
        if (isset($url) && !empty($url) && is_string($url)) {
            $this->doc_css_url = $url;
        }
    }

    public function setJavascriptUrl($url)
    {
        if (isset($url) && !empty($url) && is_string($url)) {
            $this->doc_javascript_url = $url;
        }
    }

    public function addDocInfoRaw($content)
    {
        if (isset($content) && !empty($content) && is_string($content)) {
            $this->doc_info['RAW'][] = $content;
        }
    }

    public function addDocInfo($params=array())
    {
        $element = array();
        foreach ($this->doc_info_types as $allowed=>$rules) {
            if (array_key_exists($allowed, $params) && isset($params[$allowed])) {
                foreach ($rules['optional'] as $attribute) {
                    $_attribute = str_replace('_', '-', str_replace('__', ':', $attribute));
                    if (array_key_exists($attribute, $params) && isset($params[$attribute])) {
                        $element[$_attribute] = $params[$attribute];
                    } else if (array_key_exists($attribute, $rules['defaults'])) {
                        $element[$_attribute] = $rules['defaults'][$attribute];
                    }
                    if ($attribute == 'href' or $attribute == 'src') {
                        $element[$_attribute] = urlencode($element[$_attribute]);
                    }
                }
                $renameto = (is_null($rules['renameto'])) ? '_content' : $rules['renameto'];
                $element[$renameto] = $params[$allowed];
                if ($allowed == 'title' || $allowed == 'base' || $allowed == 'DOCTYPE') {
                    $this->doc_info[$allowed] = $element;
                } else {
                    $this->doc_info[$allowed][$element[$renameto]] = $element;
                }
                break;
            }
        }
        return;
    }

    public function setIndent($indent)
    {
        if (isset($indent) && !empty($indent) && is_string($indent)) {
            $this->indent = $indent;
        }
    }

    public function getDoctypeInfo()
    {
        if (!isset($this->doc_info['DOCTYPE'])) {
            $this->doc_info['DOCTYPE']['FAMILY'] = '';
            $this->doc_info['DOCTYPE']['LEVEL'] = '';
        }

        $family = $this->doc_info['DOCTYPE']['FAMILY'];
        $level  = $this->doc_info['DOCTYPE']['LEVEL'];

        if (array_key_exists($family, $this->DOCTYPES)) {
            if (array_key_exists($level, $this->DOCTYPES[$family])) {
                $signature = $this->DOCTYPES[$family][$level]['signature']."\n";
            }
        } else {
            $signature = '';
        }
        return compact('signature');
    }

    /**
     * SMARTY PLUGIN CALLBACKS
     */

    /**
     * Smarty {doc_raw} block plugin
     *
     * Insert some raw text into the html header from anywhere at anytime
     *
     * {doc_raw}
     *   {literal}
     *     <script>body.onload = foo; function foo() {alert("I'm in the header");} </script>
     *   {/literal}
     *  {/doc_raw}
     *
     * @param array
     * @param Smarty
     * @return nothing
     */
    public function smarty_block_doc_raw($params, $content, &$smarty)
    {
        if (isset($content))
            $smarty->addDocInfoRaw($content);
    }

    /**
     * Smarty {doc_info} function plugin
     *
     * Insert html header items from anywhere at anytime
     *
     * {doc_info DOCTYPE=HTML LEVEL=Strict}
     * {doc_info meta=robots content="nofollow"} <-- 'meta' transcribed to 'name'
     * {doc_info title="my homepage"}
     * {doc_info link="styles.css"}         <-- 'link' transcribed to 'href', 'rel' defaults to "stylesheet"
     * {doc_info script="my.js"}            <-- 'script' transcribed to 'src', 'type' defaults to "text/javascript"
     * {doc_info script="my.js" defer=1}    <-- 'script' with optional 'defer'
     *
     * @param array
     * @param Smarty
     * @return nothing
     */
    public function smarty_function_doc_info($params, &$smarty)
    {
        $smarty->addDocInfo($params);
    }

    public function smarty_outputfilter_DocInfo($source, &$smarty)
    {
        $indent = $smarty->doc_indent;

        if (!empty($smarty->doc_info)) {
            $doctype_info = $smarty->getDocTypeInfo();
            $HEAD =& $smarty->doc_info;

            $doc_source = $doctype_info['signature'] . "<html>\n<head>\n";

            if (isset($HEAD['title'])) {
                $doc_source .= $indent;
                $doc_source .= '<title';
                foreach ($HEAD['title'] as $a=>$v) {
                    if ($a != '_content') {
                        $doc_source .= " {$a}=\"{$v}\"";
                    }
                }
                $doc_source .= ">{$HEAD['title']['_content']}</title>\n";
            }

            if (isset($HEAD['base'])) {
                $doc_source .= $indent;
                $doc_source .= '<base';
                foreach ($HEAD['base'] as $a=>$v) {
                    $doc_source .= " {$a}=\"{$v}\"";
                }
                $doc_source .= " />\n";
            }

            if (isset($HEAD['meta'])) {
                foreach ($HEAD['meta'] as $meta) {
                    $doc_source .= $indent;
                    $doc_source .= '<meta';
                    foreach ($meta as $a=>$v) {
                        $doc_source .= " {$a}=\"{$v}\"";
                    }
                $doc_source .= " />\n";
                }
            }

            if (isset($HEAD['link'])) {
                foreach ($HEAD['link'] as $link) {
                    $href = $link['href'];
                    if (!(substr($href, 0, 1) == '/' || substr($href, 0, 7) == 'http://')) {
                        $href = $smarty->doc_css_url . $href;
                    }
                    $doc_source .= $indent;
                    $doc_source .= '<link';
                    unset($link['href']);
                    foreach ($link as $a=>$v) {
                        $doc_source .= " {$a}=\"{$v}\"";
                    }
                    $doc_source .= " href=\"{$href}\" />\n";
                }
            }

            if (isset($HEAD['script'])) {
                foreach ($HEAD['script'] as $link) {
                    $src = $link['src'];
                    if (!(substr($src, 0, 1) == '/' || substr($src, 0, 7) == 'http://')) {
                        $src = $smarty->doc_javascript_url . $src;
                    }
                    $doc_source .= $indent;
                    $doc_source .= '<script';
                    unset($link['src']);
                    foreach ($link as $a=>$v) {
                        $doc_source .= " $a=\"$v\"";
                    }
                    $doc_source .= " src=\"{$src}\"></script>\n";
                }
            }

            if (isset($HEAD['RAW'])) {
                foreach ($HEAD['RAW'] as $raw) {
                    $doc_source .= "$raw\n";
                }
            }

            $doc_source .= "</head><body";
            if (isset($HEAD['body'])) {
                $doc_source .= ' onload="';
                foreach ($HEAD['body'] as $body) {
                    $doc_source .= "{$body['onload']};";
                }
                $doc_source .= '"';
            }
            $doc_source .= ">\n{$source}\n</body>\n</html>";

            return $doc_source;
        }
        return $source;
    }

}

/* vim: set expandtab: */

?>