root/fHTML.php

Revision 729, 10.8 kB (checked in by wbond, 4 months ago)

Fixed ticket #339 - some conditional comments were causing the regex in fHTML::prepare() to break

LineHide Line Numbers
1 <?php
2 /**
3  * Provides HTML-related methods
4  *
5  * This class is implemented to use the UTF-8 character encoding. Please see
6  * http://flourishlib.com/docs/UTF-8 for more information.
7  *
8  * @copyright  Copyright (c) 2007-2009 Will Bond
9  * @author     Will Bond [wb] <will@flourishlib.com>
10  * @license    http://flourishlib.com/license
11  *
12  * @package    Flourish
13  * @link       http://flourishlib.com/fHTML
14  *
15  * @version    1.0.0b7
16  * @changes    1.0.0b7  Fixed a bug where some conditional comments were causing the regex in ::prepare() to break [wb, 2009-11-04]
17  * @changes    1.0.0b6  Updated ::showChecked() to require strict equality if one parameter is `NULL` [wb, 2009-06-02]
18  * @changes    1.0.0b5  Fixed ::prepare() so it does not encode multi-line HTML comments [wb, 2009-05-09]
19  * @changes    1.0.0b4  Added methods ::printOption() and ::showChecked() that were in fCRUD [wb, 2009-05-08]
20  * @changes    1.0.0b3  Fixed a bug where ::makeLinks() would double-link some URLs [wb, 2009-01-08]
21  * @changes    1.0.0b2  Fixed a bug where ::makeLinks() would create links out of URLs in HTML tags [wb, 2008-12-05]
22  * @changes    1.0.0b   The initial implementation [wb, 2007-09-25]
23  */
24 class fHTML
25 {
26     // The following constants allow for nice looking callbacks to static methods
27     const containsBlockLevelHTML = 'fHTML::containsBlockLevelHTML';
28     const convertNewlines        = 'fHTML::convertNewlines';
29     const decode                 = 'fHTML::decode';
30     const encode                 = 'fHTML::encode';
31     const makeLinks              = 'fHTML::makeLinks';
32     const prepare                = 'fHTML::prepare';
33     const printOption            = 'fHTML::printOption';
34     const sendHeader             = 'fHTML::sendHeader';
35     const show                   = 'fHTML::show';
36     const showChecked            = 'fHTML::showChecked';
37    
38    
39     /**
40     * Checks a string of HTML for block level elements
41     *
42     * @param  string $content  The HTML content to check
43     * @return boolean  If the content contains a block level tag
44     */
45     static public function containsBlockLevelHTML($content)
46     {
47         static $inline_tags = '<a><abbr><acronym><b><big><br><button><cite><code><del><dfn><em><font><i><img><input><ins><kbd><label><q><s><samp><select><small><span><strike><strong><sub><sup><textarea><tt><u><var>';
48         return strip_tags($content, $inline_tags) != $content;
49     }
50    
51    
52     /**
53     * Converts newlines into `br` tags as long as there aren't any block-level HTML tags present
54     *
55     * @param  string $content  The content to display
56     * @return void
57     */
58     static public function convertNewlines($content)
59     {
60         static $inline_tags_minus_br = '<a><abbr><acronym><b><big><button><cite><code><del><dfn><em><font><i><img><input><ins><kbd><label><q><s><samp><select><small><span><strike><strong><sub><sup><textarea><tt><u><var>';
61         return (strip_tags($content, $inline_tags_minus_br) != $content) ? $content : nl2br($content);
62     }
63    
64    
65     /**
66     * Converts all HTML entities to normal characters, using UTF-8
67     *
68     * @param  string $content  The content to decode
69     * @return string  The decoded content
70     */
71     static public function decode($content)
72     {
73         return html_entity_decode($content, ENT_QUOTES, 'UTF-8');
74     }
75    
76    
77     /**
78     * Converts all special characters to entites, using UTF-8.
79     *
80     * @param  string $content  The content to encode
81     * @return string  The encoded content
82     */
83     static public function encode($content)
84     {
85         return htmlentities($content, ENT_QUOTES, 'UTF-8');
86     }
87    
88    
89     /**
90     * Takes a block of text and converts all URLs into HTML links
91     *
92     * @param  string  $content           The content to parse for links
93     * @param  integer $link_text_length  If non-zero, all link text will be truncated to this many characters
94     * @return string  The content with all URLs converted to HTML link
95     */
96     static public function makeLinks($content, $link_text_length=0)
97     {
98         // Find all a tags with contents, individual HTML tags and HTML comments
99         $reg_exp = "/<\s*a(?:\s+[\w:]+(?:\s*=\s*(?:\"[^\"]*?\"|'[^']*?'|[^'\">\s]+))?)*\s*>.*?<\s*\/\s*a\s*>|<\s*\/?\s*[\w:]+(?:\s+[\w:]+(?:\s*=\s*(?:\"[^\"]*?\"|'[^']*?'|[^'\">\s]+))?)*\s*\/?\s*>|<\!--.*?-->/s";
100         preg_match_all($reg_exp, $content, $html_matches, PREG_SET_ORDER);
101        
102         // Find all text
103         $text_matches = preg_split($reg_exp, $content);
104        
105         // For each chunk of text and create the links
106         foreach($text_matches as $key => $text) {
107             preg_match_all(
108                 '~
109                   \b([a-z]{3,}://[a-z0-9%\$\-_.+!*;/?:@=&\'\#,]+[a-z0-9\$\-_+!*;/?:@=&\'\#,])\b                           | # Fully URLs
110                   \b(www\.(?:[a-z0-9\-]+\.)+[a-z]{2,}(?:/[a-z0-9%\$\-_.+!*;/?:@=&\'\#,]+[a-z0-9\$\-_+!*;/?:@=&\'\#,])?)\b | # www. domains
111                   \b([a-z0-9\\.+\'_\\-]+@(?:[a-z0-9\\-]+\.)+[a-z]{2,})\b                                                    # email addresses
112                  ~ix',
113                 $text,
114                 $matches,
115                 PREG_SET_ORDER
116             );
117            
118             // For each match we find the first occurence, replace it and then
119             // start from the end of that finding the next occurence. This
120             // prevents double linking of matches for http://www.example.com and
121             // www.example.com
122             $last_pos = 0;
123             foreach ($matches as $match) {
124                 $match_pos = strpos($text, $match[0], $last_pos);
125                 $length    = strlen($match[0]);
126                 $prefix    = '';
127                
128                 if (!empty($match[3])) {
129                     $prefix = 'mailto:';
130                 } elseif (!empty($match[2])) {
131                     $prefix = 'http://';
132                 }
133                
134                 $replacement  = '<a href="' . $prefix . $match[0] . '">';
135                 $replacement .= ($link_text_length && strlen($match[0]) > $link_text_length) ? substr($match[0], 0, $link_text_length) . "" : $match[0];
136                 $replacement .= '</a>';
137                
138                 $text = substr_replace(
139                     $text,
140                     $replacement,
141                     $match_pos,
142                     $length
143                 );
144                
145                 $last_pos = $match_pos + strlen($replacement);   
146             }
147            
148             $text_matches[$key] = $text;
149         }
150        
151         // Merge the text and html back together
152         for ($i = 0; $i < sizeof($html_matches); $i++) {
153             $text_matches[$i] .= $html_matches[$i][0];
154         }
155        
156         return implode($text_matches);
157     }
158    
159    
160     /**
161     * Prepares content for display in UTF-8 encoded HTML - allows HTML tags
162     *
163     * @param  string $content  The content to prepare
164     * @return string  The encoded html
165     */
166     static public function prepare($content)
167     {
168         // Find all html tags, entities and comments
169         $reg_exp = "/<\s*\/?\s*[\w:]+(?:\s+[\w:]+(?:\s*=\s*(?:\"[^\"]*?\"|'[^']*?'|[^'\">\s]+))?)*\s*\/?\s*>|&(?:#\d+|\w+);|<\!--.*?-->/s";
170         preg_match_all($reg_exp, $content, $html_matches, PREG_SET_ORDER);
171        
172         // Find all text
173         $text_matches = preg_split($reg_exp, $content);
174        
175         // For each chunk of text, make sure it is converted to entities
176         foreach($text_matches as $key => $value) {
177             $text_matches[$key] = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
178         }
179        
180         // Merge the text and html back together
181         for ($i = 0; $i < sizeof($html_matches); $i++) {
182             $text_matches[$i] .= $html_matches[$i][0];
183         }
184        
185         return implode($text_matches);
186     }
187    
188    
189     /**
190     * Prints an `option` tag with the provided value, using the selected value to determine if the option should be marked as selected
191     *
192     * @param  string $text            The text to display in the option tag
193     * @param  string $value           The value for the option
194     * @param  string $selected_value  If the value is the same as this, the option will be marked as selected
195     * @return void
196     */
197     static public function printOption($text, $value, $selected_value=NULL)
198     {
199         $selected = FALSE;
200         if ($value == $selected_value || (is_array($selected_value) && in_array($value, $selected_value))) {
201             $selected = TRUE;
202         }
203        
204         echo '<option value="' . fHTML::encode($value) . '"';
205         if ($selected) {
206             echo ' selected="selected"';
207         }
208         echo '>' . fHTML::prepare($text) . '</option>';
209     }
210    
211    
212     /**
213     * Sets the proper Content-Type header for a UTF-8 HTML (or pseudo-XHTML) page
214     *
215     * @return void
216     */
217     static public function sendHeader()
218     {
219         header('Content-Type: text/html; charset=utf-8');
220     }
221    
222    
223     /**
224     * Prints a `p` (or `div` if the content has block-level HTML) tag with the contents and the class specified - will not print if no content
225     *
226     * @param  string $content    The content to display
227     * @param  string $css_class  The CSS class to apply
228     * @return boolean  If the content was shown
229     */
230     static public function show($content, $css_class='')
231     {
232         if ((!is_string($content) && !is_object($content) && !is_numeric($content)) || !strlen(trim($content))) {
233             return FALSE;
234         }
235        
236         $class = ($css_class) ? ' class="' . $css_class . '"' : '';
237         if (self::containsBlockLevelHTML($content)) {
238             echo '<div' . $class . '>' . self::prepare($content) . '</div>';
239         } else {
240             echo '<p' . $class . '>' . self::prepare($content) . '</p>';
241         }
242        
243         return TRUE;
244     }
245    
246    
247     /**
248     * Prints a `checked="checked"` HTML input attribute if `$value` equals `$checked_value`, or if `$value` is in `$checked_value`
249     *
250     * Please note that if either `$value` or `$checked_value` is `NULL`, a
251     * strict comparison will be performed, whereas normally a non-strict
252     * comparison is made. Thus `0` and `FALSE` will cause the checked
253     * attribute to be printed, but `0` and `NULL` will not.
254     *
255     * @param  string       $value          The value for the current HTML input tag
256     * @param  string|array $checked_value  The value (or array of values) that has been checked
257     * @return boolean  If the checked attribute was printed
258     */
259     static public function showChecked($value, $checked_value)
260     {
261         $checked  = FALSE;
262        
263         $one_null = $value === NULL || $checked_value === NULL;
264         $equal    = ($one_null) ? $value === $checked_value : $value == $checked_value;
265         $in_array = is_array($checked_value) && in_array($value, $checked_value, $one_null ? TRUE : FALSE);
266        
267         if ($equal || $in_array) {
268             $checked = TRUE;
269         }
270        
271         if ($checked) {
272             echo ' checked="checked"';
273             return TRUE;
274         }
275        
276         return FALSE;
277     }
278    
279    
280     /**
281     * Forces use as a static class
282     *
283     * @return fHTML
284     */
285     private function __construct() { }
286 }
287  
288  
289  
290 /**
291  * Copyright (c) 2007-2009 Will Bond <will@flourishlib.com>
292  *
293  * Permission is hereby granted, free of charge, to any person obtaining a copy
294  * of this software and associated documentation files (the "Software"), to deal
295  * in the Software without restriction, including without limitation the rights
296  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
297  * copies of the Software, and to permit persons to whom the Software is
298  * furnished to do so, subject to the following conditions:
299  *
300  * The above copyright notice and this permission notice shall be included in
301  * all copies or substantial portions of the Software.
302  *
303  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
304  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
305  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
306  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
307  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
308  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
309  * THE SOFTWARE.
310  */