Open-Source PHP Framework - Designed for rapid development of performance-oriented scalable applications

/mvc/helpers/html

[return to app]
1 <?php
2
/**
3  * HTML helper tools
4  */
5
class htmlHelper {
6     
/**
7      * Internal storage of the link-prefix and hypertext protocol values
8      * @var string
9      */
10     
protected $_linkPrefix$_protocol;
11
12     
/**
13      * Internal list of included CSS & JS files used by $this->_tagBuilder() to assure that files are not included
 
twice
14      
* @var array
15      */
16     protected 
$_includedFiles = array();
17
18     
/**
19      * Flag array to avoid defining singleton JavaScript & CSS snippets more than once
20      * @var array
21      */
22     
protected $_jsSingleton = array(), $_cssSingleton = array();
23
24     
/**
25      * Data to load at the end of the output, just before </body></html>
26      * @var array
27      */
28     
public $eof = array();
29
30     
/**
31      * Sets the protocol (http/https)
32      */
33     
public function __construct() {
34         if (isset(
$_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
35             
$this->_linkPrefix 'http://' $_SERVER['HTTP_HOST'];
36             
$this->_protocol 'https://';
37         } else {
38             
$this->_protocol 'http://';
39         }
40     }
41
42     
/**
43      * Creates simple HTML wrappers, accessed via $this->__call()
44      *
45      * JS and CSS files are never included more than once even if requested twice. If DEBUG mode is enabled than
 
the
46      
second request will be added to the debug log as a duplicateThe jsSingleton and cssSingleton methods
 
operate
47      
the same as the js css methods except that they will silently skip duplicate requests instead of logging
 
them.
48      *
49      * 
jsInlineSingleton and cssInlineSingleton makes sure a JavaScript or CSS snippet will only be output once,
 
even
50      
* if echoed out multiple timesEg.:
51      * 
$helloJs "function helloWorld() {alert('Hello World');}";
52      * echo 
$html->jsInlineSingleton($helloJs);
53      *
54      * 
Adding an optional extra argument to jsInlineSingleton/cssInlineSingleton will return the inline code bare
 
(plus
55      
a trailing linebreak), this is used for joint JS/CSS statements:
56      * echo 
$html->jsInline($html->jsInlineSingleton($helloJstrue) . 'helloWorld();');
57      *
58      * @
param string $tagType
59      
* @param array $args
60      
* @return string
61      
*/
62     protected function 
_tagBuilder($tagType$args = array()) {
63         
$arg current($args);
64         if (
DEBUG_MODE && ($arg === '' || (is_array($arg) && empty($arg)))) {
65             
$errorMsg 'Missing argument for ' __CLASS__ '::' $tagType '()';
66             
debug::log($errorMsg'warn');
67         }
68
69         if (
is_array($arg)) {
70             
$baseArray $args//maintain potential-existence of $args[1]...[n]
71             
foreach ($arg as $thisArg) {
72                 
$baseArray[0] = $thisArg;
73                 
$return[] = $this->_tagBuilder($tagType$baseArray);
74             }
75             
$return implode(PHP_EOL$return);
76         } else {
77             switch (
$tagType) {
78                 case 
'js'//Optional extra argument to delay output until the end of the file
79                 
case 'jsSingleton':
80                 case 
'css'//Optional extra argument to define CSS media type
81                 
case 'cssSingleton':
82                 case 
'jqueryTheme':
83                     if (
$tagType == 'jqueryTheme') {
84                         
$arg 'http://ajax.googleapis.com/ajax/libs/jqueryui/1/themes/'
85                              
str_replace(' ''-'strtolower($arg)) . '/jquery-ui.css';
86                         
$tagType 'css';
87                     }
88                     if (!isset(
$this->_includedFiles[$tagType][$arg])) {
89                         if (
$tagType == 'css' || $tagType == 'cssSingleton') {
90                             
$return '<link rel="stylesheet" type="text/css" href="' $arg '"'
91                                     
' media="' . (isset($args[1]) ? $args[1] : 'all') . '" />';
92                         } else {
93                             
$return '<script type="text/javascript" src="' $arg '"></script>';
94                             if (isset(
$args[1])) {
95                                 
$this->eof[] = $return;
96                                 
$return null;
97                             }
98                         }
99                         
$this->_includedFiles[$tagType][$arg] = true;
100                     } else {
101                         
$return null;
102                         if (
DEBUG_MODE && ($tagType == 'js' || $tagType == 'css')) {
103                             
debug::log($arg $tagType ' file has already been included''warn');
104                         }
105                     }
106                     break;
107                 case 
'cssInline'//Optional extra argument to define CSS media type
108                     
$return '<style type="text/css" media="' . (isset($args[1]) ? $args[1] : 'all') . '">'
109                             
PHP_EOL '/*<![CDATA[*/'
110                             
PHP_EOL '<!--'
111                             
PHP_EOL $arg
112                             
PHP_EOL '//-->'
113                             
PHP_EOL '/*]]>*/'
114                             
PHP_EOL '</style>';
115                     break;
116                 case 
'jsInline'//Optional extra argument to delay output until the end of the file
117                     
$return '<script type="text/javascript">'
118                             
PHP_EOL '//<![CDATA['
119                             
PHP_EOL '<!--'
120                             
PHP_EOL $arg
121                             
PHP_EOL '//-->'
122                             
PHP_EOL '//]]>'
123                             
PHP_EOL '</script>';
124                     if (isset(
$args[1])) {
125                         
$this->eof[] = $return;
126                         
$return null;
127                     }
128                     break;
129                 case 
'jsInlineSingleton'//Optional extra argument to supress adding of inline JS/CSS wrapper
130                 
case 'cssInlineSingleton':
131                     
$tagTypeBase substr($tagType0, -15);
132                     
$return null;
133                     
$md5 md5($arg);
134                     if (!isset(
$this->{'_' $tagTypeBase 'Singleton'}[$md5])) {
135                         
$this->{'_' $tagTypeBase 'Singleton'}[$md5] = true;
136                         
$return = (!isset($args[1]) || !$args[1] ? $this->{$tagTypeBase 'Inline'}($arg)
137                                                                  : 
$arg PHP_EOL);
138                     }
139                     break;
140                 case 
'div':
141                 case 
'li':
142                 case 
'p':
143                 case 
'h1':
144                 case 
'h2':
145                 case 
'h3':
146                 case 
'h4':
147                 case 
'ul':
148                 case 
'ol':
149                     
$return '<' $tagType;
150                     if (isset(
$args[1]) && is_array($args[1]) && $args[1]) {
151                         
$return .= ' ' self::formatProperties($args[1]);
152                     }
153                     
$return .= '>' $arg '</' $tagType '>' PHP_EOL;
154                     break;
155                 default:
156                     
$errorMsg 'TagType ' $tagType ' not valid in ' __CLASS__ '::' __METHOD__;
157                     throw new 
Exception($errorMsg);
158                     break;
159             }
160         }
161         return 
$return;
162     }
163
164     
/**
165      * Creates virtual wrapper methods via $this->_tagBuilder() for the simple wrapper functions including:
166      * $html->css, js, cssInline, jsInline, div, li, p and h1-h4
167      *
168      * @param string $method
169      * @param array $arg
170      * @return string
171      */
172     
public function __call($method$args) {
173         
$validTags = array('css''js''cssSingleton''jsSingleton''jqueryTheme',
174                            
'cssInline''jsInline''jsInlineSingleton''cssInlineSingleton',
175                            
'div''li''p''h1''h2''h3''h4''ul''ol');
176         if (
in_array($method$validTags)) {
177             return 
$this->_tagBuilder($method$args);
178         } else {
179             
$errorMsg 'Call to undefined method ' __CLASS__ '::' $method '()';
180             
trigger_error($errorMsgE_USER_ERROR);
181         }
182     }
183
184     
/**
185      * Flag to make sure that header() can only be opened one-at-a-time and footer() can only be used after
 
header()
186      * @var 
boolean
187      
*/
188     private 
$_bodyOpen false;
189
190     
/**
191      * Silently updates common XHTML 1.1 invalidations with the proper XHTML 1.1 markup
192      * @var Boolean Default true
193      */
194     
public $xhtmlMode true;
195
196     
/**
197      * Internal cache for the doctype data
198      * @var string
199      */
200     
protected $_docTypeDeclaration$_isHtml5;
201
202     
/**
203      * Enables modification of the docType
204      *
205      * Can either set to an actual doctype definition or to one of the presets (case-insensitive):
206      *
207      * HTML 5 (this is the default DTD, there is no need to explicitely call setDocType() for HTML 5)
208      * XHTML 1.1
209      * XHTML (Alias for XHTML 1.1)
210      * XHTML 1.0 Strict
211      * XHTML 1.0 Transitional
212      * XHTML 1.0 Frameset
213      * XHTML 1.0 (Alias for XHTML 1.0 Strict)
214      * HTML 4.01
215      * HTML (Alias for HTML 4.01)
216      * XHTML Mobile 1.2
217      * XHTML Mobile 1.1
218      * XHTML Mobile 1.0
219      * Mobile 1.2 (alias for XHTML Mobile 1.2)
220      * Mobile 1.1 (alias for XHTML Mobile 1.1)
221      * Mobile 1.0 (alias for XHTML Mobile 1.0)
222      * Mobile (alias for the most-strict Mobile DTD, currently 1.2)
223      *
224      * @param string $docType
225      */
226     
public function setDocType($docType) {
227         
$docType str_replace(' '''strtolower($docType));
228         if (
$docType == 'xhtml1.0') {
229             
$docType 'strict';
230         }
231         
$docType str_replace(array('xhtmlmobile''xhtml1.0'), array('mobile'''), $docType);
232         
$this->_isHtml5 = ($docType == 'html5');
233         
$docTypes = array(
234             
'html5'        => '<!DOCTYPE html>',
235             
'xhtml1.1'     => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" '
236                            
.  '"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
237             
'strict'       => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" '
238                            
.  '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
239             
'transitional' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '
240                            
.  '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
241             
'frameset'     => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" '
242                            
.  '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
243             
'html4.01'     => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" '
244                            
.  '"http://www.w3.org/TR/html4/strict.dtd">',
245             
'mobile1.2'    => '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" '
246                             
'"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">',
247             
'mobile1.1'    => '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.1//EN '
248                             
'"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile11.dtd">',
249             
'mobile1.0'    => '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" '
250                             
'"http://www.wapforum.org/DTD/xhtml-mobile10.dtd">'
251         
);
252         
//create greatest-version aliases for mobile, xhtml and html
253         
$docTypes['mobile'] = $docTypes['mobile1.2'];
254         
$docTypes['xhtml'] = $docTypes['xhtml1.1'];
255         
$docTypes['html'] = $docTypes['html4.01'];
256         
$this->_docTypeDeclaration = (isset($docTypes[$docType]) ? $docTypes[$docType] : $docType);
257     }
258
259     
/**
260      * Array used internally by Vork to cache JavaScript and CSS snippets and place them in the head section
261      * Changing the contents of this property may cause Vork components to be rendered incorrectly.
262      * @var array
263      */
264     
public $vorkHead = array();
265
266     
/**
267      * Adds a JavaScript or CSS snippet to the head of the document if it has not yet been rendered
268      *
269      * @param string $container only valid options are: jsInline, cssInline
270      * @param string $obj
271      * @return string Returns an empty string if snippet is added to head, returns snippet if head is already
 
rendered
272      
*/
273     public function 
addSnippetToHead($container$obj) {
274         
$this->vorkHead[$container][] = $obj;
275         return (!
$this->_bodyOpen '' $obj);
276     }
277
278     
/**
279      * Returns an HTML header and opens the body container
280      * This method will trigger an error if executed more than once without first calling
281      * the footer() method on the prior usage
282      * This is meant to be utilized within layouts, not views (but will work in either)
283      *
284      * @param array $args Options: (array) metaheader, (array) meta, favicon, animatedFavicon, (string/array)
 
icon,
285      *                             
headshowBrowserBars
286      
* @return string
287      
*/
288     public function 
header(array $args) {
289         if (!
$this->_bodyOpen) {
290             
$this->_bodyOpen true;
291             
extract($args);
292             if (!
$this->_docTypeDeclaration) {
293                 
$this->setDocType('html5');
294             }
295             
$return $this->_docTypeDeclaration
296                     
PHP_EOL '<html xmlns="http://www.w3.org/1999/xhtml">'
297                     
PHP_EOL '<head>'
298                     
PHP_EOL '<title>' $title '</title>';
299
300             if (!isset(
$metaheader['Content-Type'])) {
301                 
$metaheader['Content-Type'] = ($this->_isHtml5 'text/html' 'application/xhtml+xml')
302                                             . 
'; charset=utf-8';
303             }
304             foreach (
$metaheader as $name => $content) {
305                 
$return .= PHP_EOL '<meta http-equiv="' $name '" content="' $content '" />';
306             }
307
308             if (
$this->_isHtml5 && empty($showBrowserBars)) { //makes iOS utilize fullscreen
309                 
$meta['apple-mobile-web-app-capable'] = 'yes';
310             }
311             
$meta['generator'] = 'Vork 3.00';
312             foreach (
$meta as $name => $content) {
313                 
$return .= PHP_EOL '<meta name="' $name '" content="' $content '" />';
314             }
315
316             if (isset(
$favicon)) {
317                 
$return .= PHP_EOL '<link rel="shortcut icon" href="' $favicon '" type="image/x-icon" />';
318             }
319             if (isset(
$animatedFavicon)) {
320                 
$return .= PHP_EOL '<link rel="icon" href="' $animatedFavicon '" type="image/gif" />';
321             }
322             if (isset(
$icon)) {
323                 if (!
is_array($icon)) {
324                     
$icon[144] = $icon;
325                 }
326                 
krsort($icon);
327                 
$largestIcon current($icon);
328                 foreach (
$icon as $size => $iconImage) {
329                     
$return .= PHP_EOL '<link rel="apple-touch-icon-precomposed" href="' $iconImage
330                              
'" sizes="' $size 'x' $size '">';
331                 }
332                 
$return .= PHP_EOL '<link rel="apple-touch-icon-precomposed" href="' $largestIcon '">';
333             }
334
335             if (
$this->vorkHead) { //used internally by Vork tools
336                 
foreach ($this->vorkHead as $container => $objArray) { //works only for inline code, not external
 files
337                     
$return .= PHP_EOL $this->$container(implode(PHP_EOL$objArray));
338                 }
339             }
340
341             if (
$this->_isHtml5 && is::mobile() && empty($showAddressBar)) {
342                 
$hideAddressBarJs 'window.addEventListener("load", function(e) {
343                                         setTimeout(function() {
344                                             window.scrollTo(0, 1);
345                                         }, 1);
346                                     }, false);'
;
347                 
$return .= PHP_EOL $this->jsInline($hideAddressBarJs);
348             }
349             
$containers = array('css''cssInline''js''jsInline''jqueryTheme');
350             foreach (
$containers as $container) {
351                 if (isset($
$container)) {
352                     
$return .= PHP_EOL $this->$container($$container);
353                 }
354             }
355
356             if (isset(
$head)) {
357                 
$return .= PHP_EOL . (is_array($head) ? implode(PHP_EOL$head) : $head);
358             }
359
360             
$return .= PHP_EOL '</head>' PHP_EOL '<body>';
361             return 
$return;
362         } else {
363             
$errorMsg 'Invalid usage of ' __METHOD__ '() - the header has already been returned';
364             
trigger_error($errorMsgE_USER_NOTICE);
365         }
366     }
367
368     
/**
369      * Returns an HTML footer and optional Google Analytics
370      * This method will trigger an error if executed without first calling the header() method
371      * This is meant to be utilized within layouts, not views (but will work in either)
372      *
373      * @param array $args
374      * @return string
375      */
376     
public function footer(array $args = array()) {
377         if (
$this->_bodyOpen) {
378             
$this->_bodyOpen false;
379             
$return '</body></html>';
380
381             if (isset(
$args['GoogleAnalytics'])) {
382                 
$return $this->jsInline('var _gaq = _gaq || []; _gaq.push(["_setAccount", "'
383                         
$args['GoogleAnalytics'] . '"]); _gaq.push(["_trackPageview"]); (function() {
384     var ga = document.createElement("script"); ga.type = "text/javascript"; ga.async = true;
385     ga.src = ("https:" == document.location.protocol ? "https://ssl" : "http://www") +
 ".google-analytics.com/ga.js";
386     (document.getElementsByTagName("head")[0] || document.getElementsByTagName("body")[0]).appendChild(ga);'
387                         
'})();') . $return;
388             }
389
390             if (
$this->eof) {
391                 if (!
is_array($this->eof)) {
392                     
$this->eof = array($this->eof);
393                 }
394                 
$return implode($this->eof) . $return;
395             }
396             return 
$return;
397         } else {
398             
$errorMsg 'Invalid usage of ' __METHOD__ '() - header() has not been called';
399             
trigger_error($errorMsgE_USER_NOTICE);
400         }
401     }
402
403     
/**
404      * Establishes a basic set of JavaScript tools, just echo $html->jsTools() before any JavaScript code that
405      * will use the tools.
406      *
407      * This method will only operate from the first occurrence in your code, subsequent calls will not output
 
anything
408      
but you should add it anyway as it will make sure that your code continues to work if you later remove a
409      
previous call to jsTools.
410      *
411      * 
Tools provided:
412      *
413      * 
dom() method is a direct replacement for document.getElementById() that works in all JS-capable
414      
browsers Y2k and newer.
415      *
416      * 
vork object defines a global vork storage space; use by appending your own propertieseg.:
 
vork.widgetCount
417      
*
418      * @
param Boolean $noJsWrapper set to True if calling from within a $html->jsInline() wrapper
419      
* @return string
420      
*/
421     public function 
jsTools($noJsWrapper false) {
422         return 
$this->addSnippetToHead('jsInline'$this->jsInlineSingleton("var vork = function() {}
423 var dom = function(id) {
424     if (typeof document.getElementById != 'undefined') {
425         dom = function(id) {return document.getElementById(id);}
426     } else if (typeof document.all != 'undefined') {
427         dom = function(id) {return document.all[id];}
428     } else {
429         return false;
430     }
431     return dom(id);
432 }"
$noJsWrapper));
433     }
434
435     
/**
436      * Load a JavaScript library via Google's AJAX API
437      * http://code.google.com/apis/ajaxlibs/documentation/
438      *
439      * Version is optional and can be exact (1.8.2) or just version-major (1 or 1.8)
440      *
441      * Usage:
442      * echo $html->jsLoad('jquery');
443      * echo $html->jsLoad(array('yui', 'mootools'));
444      * echo $html->jsLoad(array('yui' => 2.7, 'jquery', 'dojo' => '1.3.1', 'scriptaculous'));
445      *
446      * //You can also use the Google API format JSON-decoded in which case version is required & name must be
 
lowercase
447      
$jsLibs = array(array('name' => 'mootools''version' => 1.2'base_domain' => 'ditu.google.cn'),
 
array(...));
448      * echo 
$html->jsLoad($jsLibs);
449      *
450      * @
param mixed $library Can be a string, array(str1str2...) or , array(name1 => version1name2 =>
 
version2...)
451      *                       or 
JSON-decoded Google API syntax array(array('name' => 'yui''version' => 2),
 
array(...))
452      * @
param mixed $version Optionalint or strthis is only used if $library is a string
453      
* @param array $options Optionalpassed to Google "optionalSettings" argumentonly used if $library == str
454      
* @return str
455      
*/
456     public function 
jsLoad($library$version null, array $options = array()) {
457         
$versionDefaults = array('swfobject' => 2'yui' => 2'ext-core' => 3'mootools' => 1.2);
458         if (!
is_array($library)) { //jsLoad('yui')
459             
$library strtolower($library);
460             if (!
$version) {
461                 
$version = (!isset($versionDefaults[$library]) ? $versionDefaults[$library]);
462             }
463             
$library = array('name' => $library'version' => $version);
464             
$library = array(!$options $library array_merge($library$options));
465         } else {
466             foreach (
$library as $key => $val) {
467                 if (!
is_array($val)) {
468                     if (
is_int($key)) { //jsLoad(array('yui', 'prototype'))
469                         
$val strtolower($val);
470                         
$version = (!isset($versionDefaults[$val]) ? $versionDefaults[$val]);
471                         
$library[$key] = array('name' => $val'version' => $version);
472                     } else if (!
is_array($val)) { // //jsLoad(array('yui' => '2.8.0r4', 'prototype' => 1.6))
473                         
$library[$key] = array('name' => strtolower($key), 'version' => $val);
474                     }
475                 }
476             }
477         }
478         
$url $this->_protocol 'www.google.com/jsapi';
479         if (!isset(
$this->_includedFiles['js'][$url])) { //autoload library
480             
$this->_includedFiles['js'][$url] = true;
481             
$url .= '?autoload=' urlencode(json_encode(array('modules' => array_values($library))));
482             
$return $this->js($url);
483         } else { 
//load inline
484             
foreach ($library as $lib) {
485                 
$js 'google.load("' $lib['name'] . '", "' $lib['version'] . '"';
486                 if (
count($lib) > 2) {
487                     unset(
$lib['name'], $lib['version']);
488                     
$js .= ', ' json_encode($lib);
489                 }
490                 
$jsLoads[] = $js ');';
491             }
492             
$return $this->jsInline(implode(PHP_EOL$jsLoads));
493         }
494         return 
$return;
495     }
496
497     
/**
498      * Takes an array of key-value pairs and formats them in the syntax of HTML-container properties
499      *
500      * @param array $properties
501      * @return string
502      */
503     
public static function formatProperties(array $properties) {
504         
$return = array();
505         foreach (
$properties as $name => $value) {
506             
$return[] = $name '="' get::htmlentities($value) . '"';
507         }
508         return 
implode(' '$return);
509     }
510
511     
/**
512      * Creates an anchor or link container
513      *
514      * @param array $args
515      * @return string
516      */
517     
public function anchor(array $args) {
518         if (!isset(
$args['text']) && isset($args['href'])) {
519             
$args['text'] = $args['href'];
520         }
521         if (!isset(
$args['title']) && isset($args['text'])) {
522             
$args['title'] = $args['text'];
523         }
524         if (isset(
$args['title'])) {
525             
$args['title'] = str_replace(array("\n""\r"), ' 'strip_tags($args['title']));
526         }
527         
$return '';
528         if (isset(
$args['ajax'])) {
529             
$return $this->jsSingleton('/js/ajax.js');
530             
$onclick "return ajax.load('" $args['ajax'] . "', this.href);";
531             
$args['onclick'] = (!isset($args['onclick']) ? $onclick $args['onclick'] . '; ' $onclick);
532             unset(
$args['ajax']);
533         }
534         if (isset(
$args['target'])) {
535             if (
$args['target'] == '_auto') {
536                 if (isset(
$args['href']) && strpos($args['href'], '://')
537                     && !
preg_match('#^..?tps?\:\/\/.*' get::$config->SITE_DOMAIN '(\/.*)?$#i'$args['href']))
 
{
538                     
$args['target'] = '_blank';
539                 }
540             }
541             if (
$args['target'] == '_blank' && $this->xhtmlMode) {
542                 
$onclick 'window.open(this.href); return false;';
543                 
$args['onclick'] = (!isset($args['onclick']) ? $onclick $args['onclick'] . '; ' $onclick);
544             }
545             if (isset(
$onclick) || $args['target'] == '_auto') {
546                 unset(
$args['target']);
547             }
548         }
549         
$text = (isset($args['text']) ? $args['text'] : null);
550         unset(
$args['text']);
551         return 
$return '<a ' self::formatProperties($args) . '>' $text '</a>';
552     }
553
554     
/**
555      * Shortcut to access the anchor method
556      *
557      * @param str $href
558      * @param str $text
559      * @param array $args
560      * @return str
561      */
562     
public function link($href$text null, array $args = array()) {
563         if (
strpos($href'http') !== 0) {
564             
$href $this->_linkPrefix $href;
565         }
566         
$args['href'] = $href;
567         if (
$text !== null) {
568             
$args['text'] = $text;
569         }
570         return 
$this->anchor($args);
571     }
572
573     
/**
574      * Returns an image in accessible-XHTML syntax
575      *
576      * @param array $args
577      * @return string
578      */
579     
public function img(array $args) {
580         
$args['alt'] = (isset($args['alt']) ? str_replace(array("\n""\r"), ' 'strip_tags($args['alt'])) :
 
'');
581         return 
'<img ' self::formatProperties($args) . ' />';
582     }
583
584     
/**
585      * Convenience function to simplify access the img() helper method
586      *
587      * @param string $src
588      * @param int $width
589      * @param int $height
590      * @param string $alt
591      * @param array $args
592      * @return string
593      */
594     
public function image($src$width null$height null$alt '', array $args = array()) {
595         
$args['src'] = $src;
596         
$options = array('width''height''alt');
597         foreach (
$options as $option) {
598             if ($
$option) {
599                 
$args[$option] = $$option;
600             }
601         }
602         return 
$this->img($args);
603     }
604
605     
/**
606      * Adds PHP syntax highlighting - if no PHP-open <? tag is found the entire string gets treated as PHP
607      *
608      * @param string $str
609      * @return string
610      */
611     
public function phpcode($str) {
612         if (
strpos($str'<?') === false) {
613             
$return substr(strstr(highlight_string("<?php\n" $strtrue), '<br />'), 6);
614             if (
substr($return07) == '</span>') {
615                 
$return '<code><span style="color: #000000">' substr($return7);
616             } else {
617                 
$return '<code><span style="color: #0000BB">' $return;
618             }
619         } else {
620             
$return highlight_string($strtrue);
621         }
622         return 
$return;
623     }
624
625     
/**
626      * Wrapper display computer-code samples
627      *
628      * @param str $str
629      * @return str
630      */
631     
public function code($str) {
632         return 
'<code>' str_replace('  ''&nbsp;&nbsp;'nl2br(get::htmlentities($str))) . '</code>';
633     }
634
635     
/**
636      * Creates a list from an array with automatic nesting and linking.
637      * If the keys are URLs then the elements will be linked; be sure that elements to remain unlinked have
 
numeric keys
638      
Notethe word "list" is a reserved word in PHPso thus the name "linkList"
639      
*
640      * @
param array $links
641      
* @param string $listType Optional, if ommitted then an unordered (ul) list will be returned, if an empty
 
string or
642      *                         
Bool-false is used then no list-wrapper is used (do not mix this with nested
 
lists)
643      * @return 
string
644      
*/
645     public function 
linkList(array $links$listType 'ul'$linkArgs = array()) {
646         
$return = ($listType '<' $listType '>' '');
647         foreach (
$links as $url => $title) {
648             
$class = array('class' => ($this->alternator() ? 'odd' 'even'));
649             
$return .= $this->li(!is_int($url) ? $this->link($url$title$linkArgs) :
650                                                  (
is_array($title) ? $this->linkList($title$listType) : $title),
 
$class);
651         }
652         
$return .= ($listType '</' $listType '>' '');
653         return 
$return;
654     }
655
656     
/**
657      * Display a definition list
658      *
659      * @param array $definitions Array of key-val definitions, both val can be a string or an array of
 
descriptions
660      
*                           if "dt" key exists within the array-val then it will be used as the DT value
 
instead
661      
*                           of the array-keyThe DT value can be either a string or an array of strings.
662      * @return 
string
663      
*/
664     public function 
dl(array $definitions) {
665         foreach (
$definitions as $term => $desc) {
666             if (
is_array($desc) && isset($desc['dt'])) {
667                 
$term = (is_array($desc['dt']) ? implode('</dt>' PHP_EOL '<dt>'$desc['dt']) : $desc['dt']);
668                 unset(
$desc['dt']);
669             }
670             
$return[] = '<dl class="' . ($this->alternator() ? 'odd' 'even') . '">';
671             
$return[] = '<dt>' $term '</dt>';
672             
$return[] = '<dd>' . (!is_array($desc) ? $desc implode('</dd>' PHP_EOL '<dd>'$desc)) .
 
'</dd>';
673             
$return[] = '</dl>';
674         }
675         return 
implode(PHP_EOL$return);
676     }
677
678     
/**
679      * Creates a "breadcrumb trail" of links
680      *
681      * @param array $links
682      * @param string $delimiter Optional, greater-than sign is used if ommitted
683      * @return string
684      */
685     
public function crumbs(array $links$delimiter ' &gt;&gt; ') {
686         if (
$links) {
687             foreach (
$links as $url => $title) {
688                 
$return[] = (!is_int($url) ? $this->link($url$title) : get::htmlentities($title));
689             }
690             return 
implode($delimiter$return);
691         }
692     }
693
694     
/**
695      * Create an embedded Flash movie
696      *
697      * @param string $filename
698      * @param array $args
699      * @return string
700      */
701     
public function flash($filename, array $args = array()) {
702         
$args['object']['type'] = 'application/x-shockwave-flash';
703         
$args['params']['movie'] = $args['object']['data'] = $filename;
704         
$args['params']['wmode'] = (!isset($args['wmode']) ? 'opaque' $args['wmode']);
705
706         
$dimensions = array('height''width');
707         foreach (
$dimensions as $dimension) {
708             if (isset(
$args[$dimension])) {
709                 
$args['params'][$dimension] = $args['object'][$dimension] = $args[$dimension];
710         }
711         }
712
713         
$objectProperties = array('id''class''style');
714         foreach (
$objectProperties as $property) {
715             if (isset(
$args[$property])) {
716                 
$args['object'][$property] = $args[$property];
717             }
718         }
719
720         
$return '<object ' self::formatProperties($args['object']) . '>';
721         foreach (
$args['params'] as $key => $val) {
722             
$return .= PHP_EOL '<param name="' $key '" ' 'value="' $val '" />';
723         }
724
725         if (!isset(
$args['noFlash'])) {
726             
$args['noFlash'] = 'Flash file is missing or Flash plugin is not installed';
727         }
728         
$return .= PHP_EOL $args['noFlash'] . '</object>';
729         return 
$return;
730     }
731
732     
/**
733      * Embeds a PDF file
734      *
735      * @param string $filename
736      * @param array $args
737      * @return string
738      */
739     
public function pdf($filename, array $args = array()) {
740         
$defaults = array('height' => 400'width' => 400'noPdf' => $this->link($filename'Download PDF'));
741         
$urlParams array_diff_key($args$defaults);
742         
$urlParams array_merge(array('navpanes' => 0'toolbar' => 0), $urlParams); //defaults
743         
if (isset($urlParams['search'])) {
744             
$urlParams['search'] = '"' urlencode($urlParams['search']) . '"';
745         }
746         if (isset(
$urlParams['fdf'])) { //fdf must be last in the URL
747             
$fdf $urlParams['fdf'];
748             unset(
$urlParams['fdf']);
749             
$urlParams['fdf'] = $fdf;
750         }
751         
$filename .= '#' str_replace('%2B'' 'http_build_query($urlParams'''&amp;'));
752         
$args array_merge($defaults$args);
753         
$return '<object type="application/pdf" data="' $filename
754                 
'" width="' $args['width'] . '" height="' $args['height'] . '">'
755                 
'<param name="src" value="' $filename '" />' $args['noPdf'] . '</object>';
756         return 
$return;
757     }
758
759     public function 
video($filename, array $args = array()) {
760         
$args['src'] = $filename;
761         if (!isset(
$args['autostart'])) {
762             
$args['autostart'] = 'false';
763         }
764         
$return '<object id="MediaPlayer" classid="CLSID:22D6F312-B0F6-11D0-94AB-0080C74C7E95"
 type="application/x-oleobject">'
;
765         foreach (
$args as $key => $val) {
766             
$return .= '<param name="' $key '" value="' $val '" />';
767         }
768         
$return .= '</object>';
769         return 
$return;
770     }
771
772     
/**
773      * Will return true if the number passed in is even, false if odd.
774      *
775      * @param int $number
776      * @return boolean
777      */
778     
public function isEven($number) {
779         return (Boolean) (
$number == 0);
780     }
781
782     
/**
783      * Internal incrementing integar for the alternator() method
784      * @var int
785      */
786     
private $alternator 1;
787
788     
/**
789      * Returns an alternating Boolean, useful to generate alternating background colors
790      * Eg.:
791      * $colors = array(true => 'gray', false => 'white');
792      * echo '<div style="background: ' . $colors[$html->alternator()] . ';">...</div>'; //gray background
793      * echo '<div style="background: ' . $colors[$html->alternator()] . ';">...</div>'; //white background
794      * echo '<div style="background: ' . $colors[$html->alternator()] . ';">...</div>'; //gray background
795      *
796      * @return Boolean
797      */
798     
public function alternator() {
799         return 
$this->isEven(++$this->alternator);
800     }
801
802     
/**
803      * Converts a string to its ascii equivalent
804      *
805      * @param str $str
806      * @return str
807      */
808     
public function str2ascii($str) {
809         
$ascii '';
810         
$strLen strlen($str);
811         for (
$i 0$i $strLen$i++) {
812             
$ascii .= '&#' ord($str[$i]) . ';';
813         }
814         return 
$ascii;
815     }
816
817     
/**
818      * Assures a unique ID is used for all the IDs used in the email() method
819      * @var int
820      */
821     
private $_emailCounter 0;
822
823     
/**
824      * Returns a spam-resistant email link
825      *
826      * @param str $email Must be a valid email address
827      * @return str
828      */
829     
public function email($email) {
830         
$var = (!$this->_emailCounter 'var ' '');
831         
$emailHalves explode('@'$email);
832         if (!isset(
$emailHalves[1])) {
833             return 
$email;
834         }
835         
$emailDomainParts explode('.'$emailHalves[1]);
836         if (!
$this->_emailCounter) {
837             
$initJs $this->addSnippetToHead('jsInline''var doar, noSpm; var doarlink = function(doartext,
 noSpm) {
838     doartext.style.cursor = "pointer";
839     doartext.onclick = function() {window.onerror = function() {return true;}; '
840                                 
'window.location = "mai" + "lto:" + noSpm.replace("&#0064;", "@");};
841     doartext.onmouseover = function() {doartext.style.textDecoration = "underline";}
842     doartext.onmouseout = function() {doartext.style.textDecoration = "none";}
843 }'
);
844         }
845         
$id = ++$this->_emailCounter;
846         
$noScript $emailHalves[0] . ' -&#0064;- ' implode('.'$emailDomainParts);
847         if (!
$this->_isHtml5) {
848             
$noScript '<div style="display: inline;">' $noScript '</div>';
849         }
850         return 
'<span id="doar' $id '" class="textLink"></span><noscript>' $noScript '</noscript>'
851              
$this->jsInline($this->jsTools(true) . (isset($initJs) ? $initJs '') . '
852 doar = dom("doar' 
$id '");
853 if (doar) {
854     noSpm = "' 
$emailHalves[0] . '";
855     noSpm += "&#0064;";
856     noSpm += "' 
implode('." + "'$emailDomainParts) . '";
857     doar.innerHTML = "<span id=\"doartext' 
$id '\">" + noSpm + "</span>";
858     doarlink(dom("doartext' 
$id '"), noSpm);
859 }'
);
860     }
861
862     
/**
863      * Returns a list of notifications if there are any - similar to the Flash feature of Ruby on Rails
864      *
865      * @param mixed $messages String or an array of strings
866      * @param string $class
867      * @return string Returns null if there are no notifications to return
868      */
869     
public function getNotifications($messages$class 'errormessage') {
870         if (
$messages) {
871             return 
$this->div((is_array($messages) ? implode('<br />'$messages) : $messages),
 
compact('class'));
872         }
873     }
874
875     
/**
876      * Formats a timestamp in human-readable proximity-since/until format
877      *
878      * @param int $ts Timestamp or a date/time that is in a format that can be cast into a timestamp
879      * @return string
880      */
881     
public function howLongAgo($ts) {
882         
$now time();
883         if (!
is_numeric($ts)) {
884             
$ts strtotime($ts);
885         }
886         
$ago = ($now $ts 'ago' 'from now');
887         
$secondsAgo abs($now $ts);
888         if (
$secondsAgo 5) {
889             
$return 'just now';
890         } else if (
$secondsAgo 91) { //5 to 90-seconds
891             
$return $secondsAgo ' seconds ' $ago;
892         } else if (
$secondsAgo 5400) { //2-90 minutes
893             
$return round($secondsAgo 60) . ' minutes ' $ago;
894         } else if (
$secondsAgo 86400) { //2-24 hours
895             
$return round($secondsAgo 3600) . ' hours ' $ago;
896         } else if (
$secondsAgo 172800) { //24-48 hours
897             
$return = ($now $ts 'yesterday' 'tomorrow');
898         } else if (
$secondsAgo 31536000) { //up to 1-year
899             
$return round($secondsAgo 86400) . ' days ' $ago;
900         } else { 
//1+ year
901             
$return date('M. j, Y'$ts);
902         }
903         return 
$return;
904     }
905
906     
/**
907      * Results of last usage of maxlength()
908      * @var boolean
909      */
910     
public $withinMaxLength;
911
912     
/**
913      * Returns a string no longer than a fixed length
914      * If the original string exceeds the set length then it is trimmed and then $append string is appended to it
915      *
916      * @param string $str
917      * @param int $len
918      * @param string $append Optional, defaults to ...
919      * @return string
920      */
921     
public function maxlength($str$len$append '...') {
922         
$strLen strlen($str);
923         
$this->withinMaxLength = ($strLen <= $len);
924         if (!
$this->withinMaxLength) {
925             
$appendLen strlen($append);
926             
$str substr($str0, ($len $appendLen));
927             
$str substr($str0strrpos($str' ')) . $append;
928         }
929         return 
$str;
930     }
931
932     
/**
933      * Formats price, including reducing to two-digit precision
934      *
935      * @param mixed $price
936      * @return float (values under 1,000) or string (values 1,000+) - this is due to the comma-separator
937      */
938     
public function price($price) {
939         return 
number_format(round((float) $price2), 2);
940     }
941
942     
/**
943      * Formats telephone numbers containing 9 or 10 numeric digits, all other numbers pass through unmodified
944      *
945      * @param mixed $telephone
946      * @return string
947      */
948     
public function telephone($telephone) {
949         
$tel preg_replace('/\D/'''$telephone);
950         
$telLength strlen($tel);
951         switch (
$telLength) {
952             case 
11:
953                 if (
substring($tel01) == '1') { //1-800-555-1212 format
954                     
$tel substring($tel1);
955                 } else {
956                     break;
957                 }
958             case 
10// N. American format
959                 
$telephone '(' substr($tel03) . ') ' substr($tel33) . '-' substr($tel6);
960                 break;
961             case 
9:  // S. American/Mid-East landline format
962                 
$telephone '(' substr($tel02) . ') ' substr($tel23) . '-' substr($tel5);
963                 break;
964         }
965         return 
$telephone;
966     }
967 }