web/Zend/View/Helper/Navigation/Links.php
changeset 0 4eba9c11703f
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/Zend/View/Helper/Navigation/Links.php	Mon Dec 13 18:29:26 2010 +0100
@@ -0,0 +1,783 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_View
+ * @subpackage Helper
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Links.php 20096 2010-01-06 02:05:09Z bkarwin $
+ */
+
+/**
+ * @see Zend_View_Helper_Navigation_HelperAbstract
+ */
+require_once 'Zend/View/Helper/Navigation/HelperAbstract.php';
+
+/**
+ * Helper for printing <link> elements
+ *
+ * @category   Zend
+ * @package    Zend_View
+ * @subpackage Helper
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_View_Helper_Navigation_Links
+    extends Zend_View_Helper_Navigation_HelperAbstract
+{
+    /**#@+
+     * Constants used for specifying which link types to find and render
+     *
+     * @var int
+     */
+    const RENDER_ALTERNATE  = 0x0001;
+    const RENDER_STYLESHEET = 0x0002;
+    const RENDER_START      = 0x0004;
+    const RENDER_NEXT       = 0x0008;
+    const RENDER_PREV       = 0x0010;
+    const RENDER_CONTENTS   = 0x0020;
+    const RENDER_INDEX      = 0x0040;
+    const RENDER_GLOSSARY   = 0x0080;
+    const RENDER_COPYRIGHT  = 0x0100;
+    const RENDER_CHAPTER    = 0x0200;
+    const RENDER_SECTION    = 0x0400;
+    const RENDER_SUBSECTION = 0x0800;
+    const RENDER_APPENDIX   = 0x1000;
+    const RENDER_HELP       = 0x2000;
+    const RENDER_BOOKMARK   = 0x4000;
+    const RENDER_CUSTOM     = 0x8000;
+    const RENDER_ALL        = 0xffff;
+    /**#@+**/
+
+    /**
+     * Maps render constants to W3C link types
+     *
+     * @var array
+     */
+    protected static $_RELATIONS = array(
+        self::RENDER_ALTERNATE  => 'alternate',
+        self::RENDER_STYLESHEET => 'stylesheet',
+        self::RENDER_START      => 'start',
+        self::RENDER_NEXT       => 'next',
+        self::RENDER_PREV       => 'prev',
+        self::RENDER_CONTENTS   => 'contents',
+        self::RENDER_INDEX      => 'index',
+        self::RENDER_GLOSSARY   => 'glossary',
+        self::RENDER_COPYRIGHT  => 'copyright',
+        self::RENDER_CHAPTER    => 'chapter',
+        self::RENDER_SECTION    => 'section',
+        self::RENDER_SUBSECTION => 'subsection',
+        self::RENDER_APPENDIX   => 'appendix',
+        self::RENDER_HELP       => 'help',
+        self::RENDER_BOOKMARK   => 'bookmark'
+    );
+
+    /**
+     * The helper's render flag
+     *
+     * @see render()
+     * @see setRenderFlag()
+     * @var int
+     */
+    protected $_renderFlag = self::RENDER_ALL;
+
+    /**
+     * Root container
+     *
+     * Used for preventing methods to traverse above the container given to
+     * the {@link render()} method.
+     *
+     * @see _findRoot()
+     *
+     * @var Zend_Navigation_Container
+     */
+    protected $_root;
+
+    /**
+     * View helper entry point:
+     * Retrieves helper and optionally sets container to operate on
+     *
+     * @param  Zend_Navigation_Container $container  [optional] container to
+     *                                               operate on
+     * @return Zend_View_Helper_Navigation_Links     fluent interface, returns
+     *                                               self
+     */
+    public function links(Zend_Navigation_Container $container = null)
+    {
+        if (null !== $container) {
+            $this->setContainer($container);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Magic overload: Proxy calls to {@link findRelation()} or container
+     *
+     * Examples of finder calls:
+     * <code>
+     * // METHOD                  // SAME AS
+     * $h->findRelNext($page);    // $h->findRelation($page, 'rel', 'next')
+     * $h->findRevSection($page); // $h->findRelation($page, 'rev', 'section');
+     * $h->findRelFoo($page);     // $h->findRelation($page, 'rel', 'foo');
+     * </code>
+     *
+     * @param  string $method             method name
+     * @param  array  $arguments          method arguments
+     * @throws Zend_Navigation_Exception  if method does not exist in container
+     */
+    public function __call($method, array $arguments = array())
+    {
+        if (@preg_match('/find(Rel|Rev)(.+)/', $method, $match)) {
+            return $this->findRelation($arguments[0],
+                                       strtolower($match[1]),
+                                       strtolower($match[2]));
+        }
+
+        return parent::__call($method, $arguments);
+    }
+
+    // Accessors:
+
+    /**
+     * Sets the helper's render flag
+     *
+     * The helper uses the bitwise '&' operator against the hex values of the
+     * render constants. This means that the flag can is "bitwised" value of
+     * the render constants. Examples:
+     * <code>
+     * // render all links except glossary
+     * $flag = Zend_View_Helper_Navigation_Links:RENDER_ALL ^
+     *         Zend_View_Helper_Navigation_Links:RENDER_GLOSSARY;
+     * $helper->setRenderFlag($flag);
+     *
+     * // render only chapters and sections
+     * $flag = Zend_View_Helper_Navigation_Links:RENDER_CHAPTER |
+     *         Zend_View_Helper_Navigation_Links:RENDER_SECTION;
+     * $helper->setRenderFlag($flag);
+     *
+     * // render only relations that are not native W3C relations
+     * $helper->setRenderFlag(Zend_View_Helper_Navigation_Links:RENDER_CUSTOM);
+     *
+     * // render all relations (default)
+     * $helper->setRenderFlag(Zend_View_Helper_Navigation_Links:RENDER_ALL);
+     * </code>
+     *
+     * Note that custom relations can also be rendered directly using the
+     * {@link renderLink()} method.
+     *
+     * @param  int $renderFlag                    render flag
+     * @return Zend_View_Helper_Navigation_Links  fluent interface, returns self
+     */
+    public function setRenderFlag($renderFlag)
+    {
+        $this->_renderFlag = (int) $renderFlag;
+        return $this;
+    }
+
+    /**
+     * Returns the helper's render flag
+     *
+     * @return int  render flag
+     */
+    public function getRenderFlag()
+    {
+        return $this->_renderFlag;
+    }
+
+    // Finder methods:
+
+    /**
+     * Finds all relations (forward and reverse) for the given $page
+     *
+     * The form of the returned array:
+     * <code>
+     * // $page denotes an instance of Zend_Navigation_Page
+     * $returned = array(
+     *     'rel' => array(
+     *         'alternate' => array($page, $page, $page),
+     *         'start'     => array($page),
+     *         'next'      => array($page),
+     *         'prev'      => array($page),
+     *         'canonical' => array($page)
+     *     ),
+     *     'rev' => array(
+     *         'section'   => array($page)
+     *     )
+     * );
+     * </code>
+     *
+     * @param  Zend_Navigation_Page $page  page to find links for
+     * @return array                       related pages
+     */
+    public function findAllRelations(Zend_Navigation_Page $page,
+                                     $flag = null)
+    {
+        if (!is_int($flag)) {
+            $flag = self::RENDER_ALL;
+        }
+
+        $result = array('rel' => array(), 'rev' => array());
+        $native = array_values(self::$_RELATIONS);
+
+        foreach (array_keys($result) as $rel) {
+            $meth = 'getDefined' . ucfirst($rel);
+            $types = array_merge($native, array_diff($page->$meth(), $native));
+
+            foreach ($types as $type) {
+                if (!$relFlag = array_search($type, self::$_RELATIONS)) {
+                    $relFlag = self::RENDER_CUSTOM;
+                }
+                if (!($flag & $relFlag)) {
+                    continue;
+                }
+                if ($found = $this->findRelation($page, $rel, $type)) {
+                    if (!is_array($found)) {
+                        $found = array($found);
+                    }
+                    $result[$rel][$type] = $found;
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Finds relations of the given $rel=$type from $page
+     *
+     * This method will first look for relations in the page instance, then
+     * by searching the root container if nothing was found in the page.
+     *
+     * @param  Zend_Navigation_Page $page       page to find relations for
+     * @param  string              $rel         relation, "rel" or "rev"
+     * @param  string              $type        link type, e.g. 'start', 'next'
+     * @return Zend_Navigaiton_Page|array|null  page(s), or null if not found
+     * @throws Zend_View_Exception              if $rel is not "rel" or "rev"
+     */
+    public function findRelation(Zend_Navigation_Page $page, $rel, $type)
+    {
+        if (!in_array($rel, array('rel', 'rev'))) {
+            require_once 'Zend/View/Exception.php';
+            $e = new Zend_View_Exception(sprintf(
+                'Invalid argument: $rel must be "rel" or "rev"; "%s" given',
+                $rel));
+            $e->setView($this->view);
+            throw $e;
+        }
+
+        if (!$result = $this->_findFromProperty($page, $rel, $type)) {
+            $result = $this->_findFromSearch($page, $rel, $type);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Finds relations of given $type for $page by checking if the
+     * relation is specified as a property of $page
+     *
+     * @param  Zend_Navigation_Page $page       page to find relations for
+     * @param  string              $rel         relation, 'rel' or 'rev'
+     * @param  string              $type        link type, e.g. 'start', 'next'
+     * @return Zend_Navigation_Page|array|null  page(s), or null if not found
+     */
+    protected function _findFromProperty(Zend_Navigation_Page $page, $rel, $type)
+    {
+        $method = 'get' . ucfirst($rel);
+        if ($result = $page->$method($type)) {
+            if ($result = $this->_convertToPages($result)) {
+                if (!is_array($result)) {
+                    $result = array($result);
+                }
+
+                foreach ($result as $key => $page) {
+                    if (!$this->accept($page)) {
+                        unset($result[$key]);
+                    }
+                }
+
+                return count($result) == 1 ? $result[0] : $result;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Finds relations of given $rel=$type for $page by using the helper to
+     * search for the relation in the root container
+     *
+     * @param  Zend_Navigation_Page $page  page to find relations for
+     * @param  string              $rel    relation, 'rel' or 'rev'
+     * @param  string              $type   link type, e.g. 'start', 'next', etc
+     * @return array|null                  array of pages, or null if not found
+     */
+    protected function _findFromSearch(Zend_Navigation_Page $page, $rel, $type)
+    {
+        $found = null;
+
+        $method = 'search' . ucfirst($rel) . ucfirst($type);
+        if (method_exists($this, $method)) {
+            $found = $this->$method($page);
+        }
+
+        return $found;
+    }
+
+    // Search methods:
+
+    /**
+     * Searches the root container for the forward 'start' relation of the given
+     * $page
+     *
+     * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+     * Refers to the first document in a collection of documents. This link type
+     * tells search engines which document is considered by the author to be the
+     * starting point of the collection.
+     *
+     * @param  Zend_Navigation_Page $page  page to find relation for
+     * @return Zend_Navigation_Page|null   page or null
+     */
+    public function searchRelStart(Zend_Navigation_Page $page)
+    {
+        $found = $this->_findRoot($page);
+        if (!$found instanceof Zend_Navigation_Page) {
+            $found->rewind();
+            $found = $found->current();
+        }
+
+        if ($found === $page || !$this->accept($found)) {
+            $found = null;
+        }
+
+        return $found;
+    }
+
+    /**
+     * Searches the root container for the forward 'next' relation of the given
+     * $page
+     *
+     * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+     * Refers to the next document in a linear sequence of documents. User
+     * agents may choose to preload the "next" document, to reduce the perceived
+     * load time.
+     *
+     * @param  Zend_Navigation_Page $page  page to find relation for
+     * @return Zend_Navigation_Page|null   page(s) or null
+     */
+    public function searchRelNext(Zend_Navigation_Page $page)
+    {
+        $found = null;
+        $break = false;
+        $iterator = new RecursiveIteratorIterator($this->_findRoot($page),
+                RecursiveIteratorIterator::SELF_FIRST);
+        foreach ($iterator as $intermediate) {
+            if ($intermediate === $page) {
+                // current page; break at next accepted page
+                $break = true;
+                continue;
+            }
+
+            if ($break && $this->accept($intermediate)) {
+                $found = $intermediate;
+                break;
+            }
+        }
+
+        return $found;
+    }
+
+    /**
+     * Searches the root container for the forward 'prev' relation of the given
+     * $page
+     *
+     * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+     * Refers to the previous document in an ordered series of documents. Some
+     * user agents also support the synonym "Previous".
+     *
+     * @param  Zend_Navigation_Page $page  page to find relation for
+     * @return Zend_Navigation_Page|null   page or null
+     */
+    public function searchRelPrev(Zend_Navigation_Page $page)
+    {
+        $found = null;
+        $prev = null;
+        $iterator = new RecursiveIteratorIterator(
+                $this->_findRoot($page),
+                RecursiveIteratorIterator::SELF_FIRST);
+        foreach ($iterator as $intermediate) {
+            if (!$this->accept($intermediate)) {
+                continue;
+            }
+            if ($intermediate === $page) {
+                $found = $prev;
+                break;
+            }
+
+            $prev = $intermediate;
+        }
+
+        return $found;
+    }
+
+    /**
+     * Searches the root container for forward 'chapter' relations of the given
+     * $page
+     *
+     * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+     * Refers to a document serving as a chapter in a collection of documents.
+     *
+     * @param  Zend_Navigation_Page $page       page to find relation for
+     * @return Zend_Navigation_Page|array|null  page(s) or null
+     */
+    public function searchRelChapter(Zend_Navigation_Page $page)
+    {
+        $found = array();
+
+        // find first level of pages
+        $root = $this->_findRoot($page);
+
+        // find start page(s)
+        $start = $this->findRelation($page, 'rel', 'start');
+        if (!is_array($start)) {
+            $start = array($start);
+        }
+
+        foreach ($root as $chapter) {
+            // exclude self and start page from chapters
+            if ($chapter !== $page &&
+                !in_array($chapter, $start) &&
+                $this->accept($chapter)) {
+                $found[] = $chapter;
+            }
+        }
+
+        switch (count($found)) {
+            case 0:
+                return null;
+            case 1:
+                return $found[0];
+            default:
+                return $found;
+        }
+    }
+
+    /**
+     * Searches the root container for forward 'section' relations of the given
+     * $page
+     *
+     * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+     * Refers to a document serving as a section in a collection of documents.
+     *
+     * @param  Zend_Navigation_Page $page       page to find relation for
+     * @return Zend_Navigation_Page|array|null  page(s) or null
+     */
+    public function searchRelSection(Zend_Navigation_Page $page)
+    {
+        $found = array();
+
+        // check if given page has pages and is a chapter page
+        if ($page->hasPages() && $this->_findRoot($page)->hasPage($page)) {
+            foreach ($page as $section) {
+                if ($this->accept($section)) {
+                    $found[] = $section;
+                }
+            }
+        }
+
+        switch (count($found)) {
+            case 0:
+                return null;
+            case 1:
+                return $found[0];
+            default:
+                return $found;
+        }
+    }
+
+    /**
+     * Searches the root container for forward 'subsection' relations of the
+     * given $page
+     *
+     * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+     * Refers to a document serving as a subsection in a collection of
+     * documents.
+     *
+     * @param  Zend_Navigation_Page $page       page to find relation for
+     * @return Zend_Navigation_Page|array|null  page(s) or null
+     */
+    public function searchRelSubsection(Zend_Navigation_Page $page)
+    {
+        $found = array();
+
+        if ($page->hasPages()) {
+            // given page has child pages, loop chapters
+            foreach ($this->_findRoot($page) as $chapter) {
+                // is page a section?
+                if ($chapter->hasPage($page)) {
+                    foreach ($page as $subsection) {
+                        if ($this->accept($subsection)) {
+                            $found[] = $subsection;
+                        }
+                    }
+                }
+            }
+        }
+
+        switch (count($found)) {
+            case 0:
+                return null;
+            case 1:
+                return $found[0];
+            default:
+                return $found;
+        }
+    }
+
+    /**
+     * Searches the root container for the reverse 'section' relation of the
+     * given $page
+     *
+     * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+     * Refers to a document serving as a section in a collection of documents.
+     *
+     * @param  Zend_Navigation_Page $page  page to find relation for
+     * @return Zend_Navigation_Page|null   page(s) or null
+     */
+    public function searchRevSection(Zend_Navigation_Page $page)
+    {
+        $found = null;
+
+        if ($parent = $page->getParent()) {
+            if ($parent instanceof Zend_Navigation_Page &&
+                $this->_findRoot($page)->hasPage($parent)) {
+                $found = $parent;
+            }
+        }
+
+        return $found;
+    }
+
+    /**
+     * Searches the root container for the reverse 'section' relation of the
+     * given $page
+     *
+     * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+     * Refers to a document serving as a subsection in a collection of
+     * documents.
+     *
+     * @param  Zend_Navigation_Page $page  page to find relation for
+     * @return Zend_Navigation_Page|null   page(s) or null
+     */
+    public function searchRevSubsection(Zend_Navigation_Page $page)
+    {
+        $found = null;
+
+        if ($parent = $page->getParent()) {
+            if ($parent instanceof Zend_Navigation_Page) {
+                $root = $this->_findRoot($page);
+                foreach ($root as $chapter) {
+                    if ($chapter->hasPage($parent)) {
+                        $found = $parent;
+                        break;
+                    }
+                }
+            }
+        }
+
+        return $found;
+    }
+
+    // Util methods:
+
+    /**
+     * Returns the root container of the given page
+     *
+     * When rendering a container, the render method still store the given
+     * container as the root container, and unset it when done rendering. This
+     * makes sure finder methods will not traverse above the container given
+     * to the render method.
+     *
+     * @param  Zend_Navigaiton_Page $page  page to find root for
+     * @return Zend_Navigation_Container   the root container of the given page
+     */
+    protected function _findRoot(Zend_Navigation_Page $page)
+    {
+        if ($this->_root) {
+            return $this->_root;
+        }
+
+        $root = $page;
+
+        while ($parent = $page->getParent()) {
+            $root = $parent;
+            if ($parent instanceof Zend_Navigation_Page) {
+                $page = $parent;
+            } else {
+                break;
+            }
+        }
+
+        return $root;
+    }
+
+    /**
+     * Converts a $mixed value to an array of pages
+     *
+     * @param  mixed $mixed                     mixed value to get page(s) from
+     * @param  bool  $recursive                 whether $value should be looped
+     *                                          if it is an array or a config
+     * @return Zend_Navigation_Page|array|null  empty if unable to convert
+     */
+    protected function _convertToPages($mixed, $recursive = true)
+    {
+        if (is_object($mixed)) {
+            if ($mixed instanceof Zend_Navigation_Page) {
+                // value is a page instance; return directly
+                return $mixed;
+            } elseif ($mixed instanceof Zend_Navigation_Container) {
+                // value is a container; return pages in it
+                $pages = array();
+                foreach ($mixed as $page) {
+                    $pages[] = $page;
+                }
+                return $pages;
+            } elseif ($mixed instanceof Zend_Config) {
+                // convert config object to array and extract
+                return $this->_convertToPages($mixed->toArray(), $recursive);
+            }
+        } elseif (is_string($mixed)) {
+            // value is a string; make an URI page
+            return Zend_Navigation_Page::factory(array(
+                'type' => 'uri',
+                'uri'  => $mixed
+            ));
+        } elseif (is_array($mixed) && !empty($mixed)) {
+            if ($recursive && is_numeric(key($mixed))) {
+                // first key is numeric; assume several pages
+                $pages = array();
+                foreach ($mixed as $value) {
+                    if ($value = $this->_convertToPages($value, false)) {
+                        $pages[] = $value;
+                    }
+                }
+                return $pages;
+            } else {
+                // pass array to factory directly
+                try {
+                    $page = Zend_Navigation_Page::factory($mixed);
+                    return $page;
+                } catch (Exception $e) {
+                }
+            }
+        }
+
+        // nothing found
+        return null;
+    }
+
+    // Render methods:
+
+    /**
+     * Renders the given $page as a link element, with $attrib = $relation
+     *
+     * @param  Zend_Navigation_Page $page      the page to render the link for
+     * @param  string               $attrib    the attribute to use for $type,
+     *                                         either 'rel' or 'rev'
+     * @param  string               $relation  relation type, muse be one of;
+     *                                         alternate, appendix, bookmark,
+     *                                         chapter, contents, copyright,
+     *                                         glossary, help, home, index, next,
+     *                                         prev, section, start, stylesheet,
+     *                                         subsection
+     * @return string                          rendered link element
+     * @throws Zend_View_Exception             if $attrib is invalid
+     */
+    public function renderLink(Zend_Navigation_Page $page, $attrib, $relation)
+    {
+        if (!in_array($attrib, array('rel', 'rev'))) {
+            require_once 'Zend/View/Exception.php';
+            $e = new Zend_View_Exception(sprintf(
+                    'Invalid relation attribute "%s", must be "rel" or "rev"',
+                    $attrib));
+            $e->setView($this->view);
+            throw $e;
+        }
+
+        if (!$href = $page->getHref()) {
+            return '';
+        }
+
+        // TODO: add more attribs
+        // http://www.w3.org/TR/html401/struct/links.html#h-12.2
+        $attribs = array(
+            $attrib  => $relation,
+            'href'   => $href,
+            'title'  => $page->getLabel()
+        );
+
+        return '<link' .
+               $this->_htmlAttribs($attribs) .
+               $this->getClosingBracket();
+    }
+
+    // Zend_View_Helper_Navigation_Helper:
+
+    /**
+     * Renders helper
+     *
+     * Implements {@link Zend_View_Helper_Navigation_Helper::render()}.
+     *
+     * @param  Zend_Navigation_Container $container  [optional] container to
+     *                                               render. Default is to
+     *                                               render the container
+     *                                               registered in the helper.
+     * @return string                                helper output
+     */
+    public function render(Zend_Navigation_Container $container = null)
+    {
+        if (null === $container) {
+            $container = $this->getContainer();
+        }
+
+        if ($active = $this->findActive($container)) {
+            $active = $active['page'];
+        } else {
+            // no active page
+            return '';
+        }
+
+        $output = '';
+        $indent = $this->getIndent();
+        $this->_root = $container;
+
+        $result = $this->findAllRelations($active, $this->getRenderFlag());
+        foreach ($result as $attrib => $types) {
+            foreach ($types as $relation => $pages) {
+                foreach ($pages as $page) {
+                    if ($r = $this->renderLink($page, $attrib, $relation)) {
+                        $output .= $indent . $r . self::EOL;
+                    }
+                }
+            }
+        }
+
+        $this->_root = null;
+
+        // return output (trim last newline by spec)
+        return strlen($output) ? rtrim($output, self::EOL) : '';
+    }
+}