From 68d1b7424a904e34b3ee1f815ffb4b4e03b89057 Mon Sep 17 00:00:00 2001 From: Chuck Hagenbuch Date: Tue, 17 Feb 2009 00:14:47 -0500 Subject: [PATCH] revamp helpers to use horde_view_helper_base base class, add tag and javascript helpers, add tests, add url docs --- framework/View/lib/Horde/View/Helper.php | 46 ------ framework/View/lib/Horde/View/Helper/Base.php | 69 +++++++++ framework/View/lib/Horde/View/Helper/Block.php | 16 +- .../View/lib/Horde/View/Helper/Javascript.php | 46 ++++++ framework/View/lib/Horde/View/Helper/Tag.php | 159 ++++++++++++++++++++ framework/View/lib/Horde/View/Helper/Url.php | 166 +++++++++++---------- framework/View/package.xml | 14 +- framework/View/test/Horde/View/AllTests.php | 17 +++ .../View/test/Horde/View/Helper/JavascriptTest.php | 40 +++++ framework/View/test/Horde/View/Helper/TagTest.php | 101 +++++++++++++ framework/View/test/Horde/View/InterfaceTest.php | 18 ++- 11 files changed, 552 insertions(+), 140 deletions(-) delete mode 100644 framework/View/lib/Horde/View/Helper.php create mode 100644 framework/View/lib/Horde/View/Helper/Base.php create mode 100644 framework/View/lib/Horde/View/Helper/Javascript.php create mode 100644 framework/View/lib/Horde/View/Helper/Tag.php create mode 100644 framework/View/test/Horde/View/Helper/JavascriptTest.php create mode 100644 framework/View/test/Horde/View/Helper/TagTest.php diff --git a/framework/View/lib/Horde/View/Helper.php b/framework/View/lib/Horde/View/Helper.php deleted file mode 100644 index d4a95778c..000000000 --- a/framework/View/lib/Horde/View/Helper.php +++ /dev/null @@ -1,46 +0,0 @@ -_view = $view; - $view->addHelper($this); - } - - /** - * Call chaining so other helpers can be called transparently. - * - * @param string $method The helper method. - * @param array $args The parameters for the helper. - * - * @return string The result of the helper method. - */ - public function __call($method, $args) - { - return call_user_func_array(array($this->_view, $method), $args); - } - -} diff --git a/framework/View/lib/Horde/View/Helper/Base.php b/framework/View/lib/Horde/View/Helper/Base.php new file mode 100644 index 000000000..aeb2aed10 --- /dev/null +++ b/framework/View/lib/Horde/View/Helper/Base.php @@ -0,0 +1,69 @@ +_view = $view; + $view->addHelper($this); + } + + /** + * Proxy on undefined property access (get) + */ + public function __get($name) + { + return $this->_view->$name; + } + + /** + * Proxy on undefined property access (set) + */ + public function __set($name, $value) + { + return $this->_view->$name = $value; + } + + /** + * Call chaining so other helpers can be called transparently. + * + * @param string $method The helper method. + * @param array $args The parameters for the helper. + * + * @return string The result of the helper method. + */ + public function __call($method, $args) + { + return $this->_view->__call($method, $args); + } + +} diff --git a/framework/View/lib/Horde/View/Helper/Block.php b/framework/View/lib/Horde/View/Helper/Block.php index f08784369..1292faf4e 100644 --- a/framework/View/lib/Horde/View/Helper/Block.php +++ b/framework/View/lib/Horde/View/Helper/Block.php @@ -1,18 +1,20 @@ + * @category Horde + * @package Horde_View + * @subpackage Helper */ /** * View helper for displaying Horde_Block objects * - * @category Horde - * @package Horde_View - * @subpackage Helpers + * @author Chuck Hagenbuch + * @category Horde + * @package Horde_View + * @subpackage Helper */ class Horde_View_Helper_Block extends Horde_View_Helper { diff --git a/framework/View/lib/Horde/View/Helper/Javascript.php b/framework/View/lib/Horde/View/Helper/Javascript.php new file mode 100644 index 000000000..8aaa499b2 --- /dev/null +++ b/framework/View/lib/Horde/View/Helper/Javascript.php @@ -0,0 +1,46 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_View + * @subpackage Helper + */ + +/** + * @author Mike Naberezny + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_View + * @subpackage Helper + */ +class Horde_View_Helper_Javascript extends Horde_View_Helper_Base +{ + public function escapeJavascript($javascript) + { + return str_replace(array('\\', "\r\n", "\r", "\n", '"', "'"), + array('\0\0', "\\n", "\\n", "\\n", '\"', "\'"), + $javascript); + } + + public function javascriptTag($content, $htmlOptions = array()) + { + return $this->contentTag('script', + $this->javascriptCdataSection($content), + array_merge($htmlOptions, array('type' => 'text/javascript'))); + } + + // @todo nodoc + public function javascriptCdataSection($content) + { + return "\n//" . $this->cdataSection("\n$content\n//") . "\n"; + } + +} diff --git a/framework/View/lib/Horde/View/Helper/Tag.php b/framework/View/lib/Horde/View/Helper/Tag.php new file mode 100644 index 000000000..705e48809 --- /dev/null +++ b/framework/View/lib/Horde/View/Helper/Tag.php @@ -0,0 +1,159 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_View + * @subpackage Helper + */ + +/** + * Use these methods to generate HTML tags programmatically. + * By default, they output XHTML compliant tags. + * + * @author Mike Naberezny + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_View + * @subpackage Helper + */ +class Horde_View_Helper_Tag extends Horde_View_Helper_Base +{ + /** + * HTML attributes that get converted from boolean to the attribute name: + * array('disabled' => true) becomes array('disabled' => 'disabled') + * + * @var array + */ + private $_booleanAttributes = array('disabled', 'readonly', 'multiple'); + + /** + * Returns an empty HTML tag of type $name which by default is XHTML + * compliant. Setting $open to true will create an open tag compatible + * with HTML 4.0 and below. Add HTML attributes by passing an attributes + * hash to $options. For attributes with no value (like disabled and + * readonly), give it a value of TRUE in the $options array. + * + * $this->tag("br") + * # =>
+ * $this->tag("br", null, true) + * # =>
+ * $this->tag("input", array('type' => 'text', 'disabled' => true)) + * # => + * + * @param string $name Tag name + * @param string $options Tag attributes + * @param boolean $open Leave tag open for HTML 4.0 and below? + * @param string Generated HTML tag + */ + public function tag($name, $options = null, $open = false) + { + return "<$name" + . ($options ? $this->tagOptions($options) : '') + . ($open ? '>' : ' />'); + } + + /** + * Returns an HTML block tag of type $name surrounding the $content. Add + * HTML attributes by passing an attributes hash to $options. For attributes + * with no value (like disabled and readonly), give it a value of TRUE in + * the $options array. + * + * $this->contentTag("p", "Hello world!") + * # =>

Hello world!

+ * $this->contentTag("div", $this->contentTag("p", "Hello world!"), array("class" => "strong")) + * # =>

Hello world!

+ * $this->contentTag("select", $options, array("multiple" => true)) + * # => + * + * @param string $name Tag name + * @param string $content Content to place between the tags + * @param array $options Tag attributes + * @return string Genereated HTML tags with content between + */ + public function contentTag($name, $content, $options = null) + { + $tagOptions = ($options ? $this->tagOptions($options) : ''); + return "<$name$tagOptions>$content"; + } + + /** + * Returns a CDATA section with the given $content. CDATA sections + * are used to escape blocks of text containing characters which would + * otherwise be recognized as markup. CDATA sections begin with the string + * and end with (and may not contain) the string ]]>. + * + * $this->cdataSection("") + * # => ]]> + * + * @param string $content Content for inside CDATA section + * @return string CDATA section with content + */ + public function cdataSection($content) + { + return ""; + } + + /** + * Returns the escaped $html without affecting existing escaped entities. + * + * $this->escapeOnce("1 > 2 & 3") + * # => "1 < 2 & 3" + * + * @param string $html HTML to be escaped + * @return string Escaped HTML without affecting existing escaped entities + */ + public function escapeOnce($html) + { + return $this->_fixDoubleEscape(htmlspecialchars($html)); + } + + /** + * Converts an associative array of $options into + * a string of HTML attributes + * + * @param array $options key/value pairs + * @param string key1="value1" key2="value2" + */ + public function tagOptions($options) + { + foreach ($options as $k => &$v) { + if ($v === null || $v === false) { + unset($options[$k]); + } else { + if (in_array($k, $this->_booleanAttributes)) { + $v = $k; + } + } + } + + if (! empty($options)) { + foreach ($options as $k => &$v) { + $v = $k . '="' . $this->escapeOnce($v) . '"'; + } + sort($options); + return ' ' . implode(' ', $options); + } else { + return ''; + } + } + + /** + * Fix double-escaped entities, such as &amp;, &#123;, etc. + * + * @param string $escaped Double-escaped entities + * @return string Entities fixed + */ + private function _fixDoubleEscape($escaped) + { + return preg_replace('/&([a-z]+|(#\d+));/i', '&\\1;', $escaped); + } + +} diff --git a/framework/View/lib/Horde/View/Helper/Url.php b/framework/View/lib/Horde/View/Helper/Url.php index 27e449e21..2fe14808d 100644 --- a/framework/View/lib/Horde/View/Helper/Url.php +++ b/framework/View/lib/Horde/View/Helper/Url.php @@ -1,18 +1,27 @@ + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_View + * @subpackage Helper */ /** - * View helper for URLs + * View helpers for URLs * - * @category Horde - * @package Horde_View - * @subpackage Helpers + * @author Mike Naberezny + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_View + * @subpackage Helper */ class Horde_View_Helper_Url extends Horde_View_Helper { @@ -66,9 +75,44 @@ class Horde_View_Helper_Url extends Horde_View_Helper } /** - * Creates a link tag of the given $name using $url unless the current - * request URI is the same as the links, in which case only the name is - * returned. + * Creates a link tag of the given +name+ using a URL created by the set of + * +options+ unless the current request URI is the same as the links, in + * which case only the name is returned (or the given block is yielded, if + * one exists). You can give link_to_unless_current a block which will + * specialize the default behavior (e.g., show a "Start Here" link rather + * than the link's text). + * + * ==== Examples + * Let's say you have a navigation menu... + * + * + * + * If in the "about" action, it will render... + * + * + * + * ...but if in the "home" action, it will render: + * + * + * + * The implicit block given to link_to_unless_current is evaluated if the current + * action is the action given. So, if we had a comments page and wanted to render a + * "Go Back" link instead of a link to the comments page, we could do something like this... + * + * <%= + * link_to_unless_current("Comment", { :controller => 'comments', :action => 'new}) do + * link_to("Go back", { :controller => 'posts', :action => 'index' }) + * end + * %> */ public function linkToUnlessCurrent($name, $url, $htmlOptions = array()) { @@ -79,9 +123,24 @@ class Horde_View_Helper_Url extends Horde_View_Helper /** * Creates a link tag of the given +name+ using a URL created by the set of * +options+ unless +condition+ is true, in which case only the name is - * returned. To specialize the default behavior (i.e., show a login link rather - * than just the plaintext link text), you can pass a block that + * returned. To specialize the default behavior (i.e., show a login link + * rather than just the plaintext link text), you can pass a block that * accepts the name or the full argument list for link_to_unless. + * + * ==== Examples + * <%= link_to_unless(@current_user.nil?, "Reply", { :action => "reply" }) %> + * # If the user is logged in... + * # => Reply + * + * <%= + * link_to_unless(@current_user.nil?, "Reply", { :action => "reply" }) do |name| + * link_to(name, { :controller => "accounts", :action => "signup" }) + * end + * %> + * # If the user is logged in... + * # => Reply + * # If not... + * # => Reply */ public function linkToUnless($condition, $name, $url, $htmlOptions = array()) { @@ -94,6 +153,21 @@ class Horde_View_Helper_Url extends Horde_View_Helper * returned. To specialize the default behavior, you can pass a block that * accepts the name or the full argument list for link_to_unless (see the examples * in link_to_unless). + * + * ==== Examples + * <%= link_to_if(@current_user.nil?, "Login", { :controller => "sessions", :action => "new" }) %> + * # If the user isn't logged in... + * # => Login + * + * <%= + * link_to_if(@current_user.nil?, "Login", { :controller => "sessions", :action => "new" }) do + * link_to(@current_user.login, { :controller => "accounts", :action => "show", :id => @current_user }) + * end + * %> + * # If the user isn't logged in... + * # => Login + * # If they are logged in... + * # => my_username */ public function linkToIf($condition, $name, $url, $htmlOptions = array()) { @@ -110,70 +184,4 @@ class Horde_View_Helper_Url extends Horde_View_Helper return $url == $_SERVER['REQUEST_URI']; } - // @TODO Move these methods to a generic HTML/Tag helper - - /** - * HTML attributes that get converted from boolean to the attribute name: - * array('disabled' => true) becomes array('disabled' => 'disabled') - * - * @var array - */ - private $_booleanAttributes = array('disabled', 'readonly', 'multiple', 'selected', 'checked'); - - /** - * Converts an associative array of $options into - * a string of HTML attributes - * - * @param array $options key/value pairs - * @param string key1="value1" key2="value2" - */ - public function tagOptions($options) - { - foreach ($options as $k => &$v) { - if ($v === null || $v === false) { - unset($options[$k]); - } else { - if (in_array($k, $this->_booleanAttributes)) { - $v = $k; - } - } - } - - if (!empty($options)) { - foreach ($options as $k => &$v) { - $v = $k . '="' . $this->escapeOnce($v) . '"'; - } - sort($options); - return ' ' . implode(' ', $options); - } else { - return ''; - } - } - - /** - * Returns the escaped $html without affecting existing escaped entities. - * - * $this->escapeOnce("1 > 2 & 3") - * => "1 < 2 & 3" - * - * @param string $html HTML to be escaped - * - * @return string Escaped HTML without affecting existing escaped entities - */ - public function escapeOnce($html) - { - return $this->_fixDoubleEscape(htmlspecialchars($html, ENT_QUOTES, $this->getEncoding())); - } - - /** - * Fix double-escaped entities, such as &amp; - * - * @param string $escaped Double-escaped entities - * @return string Entities fixed - */ - private function _fixDoubleEscape($escaped) - { - return preg_replace('/&([a-z]+|(#\d+));/i', '&\\1;', $escaped); - } - } diff --git a/framework/View/package.xml b/framework/View/package.xml index a64f09077..282d6c286 100644 --- a/framework/View/package.xml +++ b/framework/View/package.xml @@ -14,6 +14,12 @@ http://pear.php.net/dtd/package-2.0.xsd"> chuck@horde.org yes + + Mike Naberezny + mnaberez + mike@maintainable.com + yes + 2008-02-12 @@ -33,12 +39,14 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + - @@ -58,11 +66,13 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + - diff --git a/framework/View/test/Horde/View/AllTests.php b/framework/View/test/Horde/View/AllTests.php index dbebda25d..ead96dcc5 100644 --- a/framework/View/test/Horde/View/AllTests.php +++ b/framework/View/test/Horde/View/AllTests.php @@ -1,5 +1,9 @@ + * @category Horde * @package Horde_View * @subpackage UnitTests */ @@ -20,6 +24,19 @@ class Horde_View_AllTests { public static function suite() { + // Catch strict standards + error_reporting(E_ALL | E_STRICT); + + // Ensure a default timezone is set. + date_default_timezone_set('America/New_York'); + + // Set up autoload + set_include_path(dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'lib' . PATH_SEPARATOR . get_include_path()); + if (!spl_autoload_functions()) { + spl_autoload_register(create_function('$class', '$filename = str_replace(array(\'::\', \'_\'), \'/\', $class); @include_once "$filename.php";')); + } + + // Build the suite $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Horde_View'); $basedir = dirname(__FILE__); diff --git a/framework/View/test/Horde/View/Helper/JavascriptTest.php b/framework/View/test/Horde/View/Helper/JavascriptTest.php new file mode 100644 index 000000000..546cca782 --- /dev/null +++ b/framework/View/test/Horde/View/Helper/JavascriptTest.php @@ -0,0 +1,40 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_View + * @subpackage UnitTests + */ + +/** + * @group view + * @author Mike Naberezny + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_View + * @subpackage UnitTests + */ +class Horde_View_Helper_JavascriptTest extends PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->view = new Horde_View(); + $this->view->addHelper(new Horde_View_Helper_Tag($this->view)); + $this->view->addHelper(new Horde_View_Helper_Javascript($this->view)); + } + + public function testJavascriptTag() + { + $this->assertEquals("", + $this->view->javascriptTag('foo = 1;')); + } + +} diff --git a/framework/View/test/Horde/View/Helper/TagTest.php b/framework/View/test/Horde/View/Helper/TagTest.php new file mode 100644 index 000000000..49ebe9c6e --- /dev/null +++ b/framework/View/test/Horde/View/Helper/TagTest.php @@ -0,0 +1,101 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_View + * @subpackage UnitTests + */ + +/** + * @group view + * @author Mike Naberezny + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_View + * @subpackage UnitTests + */ +class Horde_View_Helper_TagTest extends PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->helper = new Horde_View_Helper_Tag(new Horde_View()); + } + + public function testTag() + { + $this->assertEquals('
', $this->helper->tag('br')); + $this->assertEquals('
', + $this->helper->tag('br', array('clear' => 'left'))); + $this->assertEquals('
', + $this->helper->tag('br', null, true)); + } + + public function testTagOptions() + { + $this->assertRegExp('/\A

\z/', + $this->helper->tag('p', array('class' => 'show', + 'class' => 'elsewhere'))); + } + + public function testTagOptionsRejectsNullOption() + { + $this->assertEquals('

', + $this->helper->tag('p', array('ignored' => null))); + } + + public function testTagOptionsAcceptsBlankOption() + { + $this->assertEquals('

', + $this->helper->tag('p', array('included' => ''))); + } + + public function testTagOptionsConvertsBooleanOption() + { + $this->assertEquals('

', + $this->helper->tag('p', array('disabled' => true, + 'multiple' => true, + 'readonly' => true))); + } + + public function testContentTag() + { + $this->assertEquals('Create', + $this->helper->contentTag('a', 'Create', array('href' => 'create'))); + } + + public function testCdataSection() + { + $this->assertEquals(']]>', $this->helper->cdataSection('')); + } + + public function testEscapeOnce() + { + $this->assertEquals('1 < 2 & 3', $this->helper->escapeOnce('1 < 2 & 3')); + } + + public function testDoubleEscapingAttributes() + { + $attributes = array('1&2', '1 < 2', '“test“'); + foreach ($attributes as $escaped) { + $this->assertEquals("", + $this->helper->tag('a', array('href' => $escaped))); + } + } + + public function testSkipInvalidEscapedAttributes() + { + $attributes = array('&1;', 'dfa3;', '& #123;'); + foreach ($attributes as $escaped) { + $this->assertEquals('', + $this->helper->tag('a', array('href' => $escaped))); + } + } +} diff --git a/framework/View/test/Horde/View/InterfaceTest.php b/framework/View/test/Horde/View/InterfaceTest.php index bcbc15834..44536af54 100644 --- a/framework/View/test/Horde/View/InterfaceTest.php +++ b/framework/View/test/Horde/View/InterfaceTest.php @@ -1,14 +1,20 @@ + * @category Horde + * @package Horde_View * @subpackage UnitTests */ -require_once dirname(__FILE__) . '/../../../lib/Horde/View/Interface.php'; -require_once dirname(__FILE__) . '/../../../lib/Horde/View/Base.php'; -require_once dirname(__FILE__) . '/../../../lib/Horde/View.php'; - +/** + * @group view + * @author Chuck Hagenbuch + * @category Horde + * @package Horde_View + * @subpackage UnitTests + */ class Horde_View_InterfaceTest extends PHPUnit_Framework_TestCase { public function testViewInterface() -- 2.11.0