1<?php
2/**
3 * Copyright 2007 Maintainable Software, LLC
4 * Copyright 2006-2016 Horde LLC (http://www.horde.org/)
5 *
6 * @author     Mike Naberezny <mike@maintainable.com>
7 * @author     Derek DeVries <derek@maintainable.com>
8 * @author     Chuck Hagenbuch <chuck@horde.org>
9 * @license    http://www.horde.org/licenses/bsd
10 * @category   Horde
11 * @package    View
12 * @subpackage Helper
13 */
14
15/**
16 * View helpers for text
17 *
18 * @author     Mike Naberezny <mike@maintainable.com>
19 * @author     Derek DeVries <derek@maintainable.com>
20 * @author     Chuck Hagenbuch <chuck@horde.org>
21 * @license    http://www.horde.org/licenses/bsd
22 * @category   Horde
23 * @package    View
24 * @subpackage Helper
25 */
26class Horde_View_Helper_Text extends Horde_View_Helper_Base
27{
28    /**
29     * @var array
30     */
31    protected $_cycles = array();
32
33    /**
34     * @var Horde_Support_Inflector
35     */
36    protected $_inflector;
37
38    /**
39     * Escapes a value for output in a view template.
40     *
41     * <code>
42     * <p><?php echo $this->h($this->templateVar) ?></p>
43     * </code>
44     *
45     * @param mixed $var  The output to escape.
46     *
47     * @return mixed  The escaped value.
48     */
49    public function h($var)
50    {
51        return htmlspecialchars($var, ENT_QUOTES, $this->_view->getEncoding());
52    }
53
54    /**
55     * Pluralizes the $singular word unless $count is one. If $plural
56     * form is not supplied, inflector will be used.
57     *
58     * @param integer $count    Count determines singular or plural.
59     * @param string $singular  Singular form.
60     * @param string $plural    Plural form (optional).
61     */
62    public function pluralize($count, $singular, $plural = null)
63    {
64        if ($count == '1') {
65            $word = $singular;
66        } elseif ($plural) {
67            $word = $plural;
68        } else {
69            if (!$this->_inflector) {
70                $this->_inflector = new Horde_Support_Inflector();
71            }
72            $word = $this->_inflector->pluralize($singular);
73        }
74
75        return "$count $word";
76    }
77
78    /**
79     * Creates a Cycle object whose __toString() method cycles through elements
80     * of an array every time it is called.
81     *
82     * This can be used for example, to alternate classes for table rows:
83     *
84     * <code>
85     * <?php foreach($items as $item): ?>
86     *   <tr class="<?php echo $this->cycle("even", "odd") ?>">
87     *     <td>item</td>
88     *   </tr>
89     * <?php endforeach ?>
90     * </code>
91     *
92     * You can use named cycles to allow nesting in loops.  Passing an array as
93     * the last parameter with a <tt>name</tt> key will create a named cycle.
94     * You can manually reset a cycle by calling resetCycle() and passing the
95     * name of the cycle:
96     *
97     * <code>
98     * <?php foreach($items as $item): ?>
99     * <tr class="<?php echo $this->cycle('even', 'odd', array('name' => 'row_class')) ?>">
100     *   <td>
101     *     <?php foreach ($item->values as $value): ?>
102     *     <span style="color:<?php echo $this->cycle('red', 'green', 'blue', array('name' => 'colors')) ?>">
103     *       <?php echo $value ?>
104     *     </span>
105     *     <?php endforeach ?>
106     *     <?php $this->resetCycle('colors') ?>
107     *   </td>
108     * </tr>
109     * <?php endforeach ?>
110     * </code>
111     */
112    public function cycle($firstValue)
113    {
114        $values = func_get_args();
115
116        $last = end($values);
117        if (is_array($last)) {
118            $options = array_pop($values);
119            $name = isset($options['name']) ? $options['name'] : 'default';
120        } else {
121            $name = 'default';
122        }
123
124        if (empty($this->_cycles[$name]) ||
125            $this->_cycles[$name]->getValues() != $values) {
126            $this->_cycles[$name] = new Horde_View_Helper_Text_Cycle($values);
127        }
128
129        return $this->_cycles[$name];
130    }
131
132    /**
133     * Resets a cycle so that it starts from the first element the next time
134     * it is called.
135     *
136     * Pass in $name to reset a named cycle.
137     *
138     * @param string $name  Name of cycle to reset.
139     */
140    public function resetCycle($name = 'default')
141    {
142        if (isset($this->_cycles[$name])) {
143            $this->_cycles[$name]->reset();
144        }
145    }
146
147    /**
148     * Highlights a phrase where it is found in the text by surrounding it
149     * like <strong class="highlight">I'm highlighted</strong>.
150     *
151     * The Highlighter can be customized by passing $highlighter as a string
152     * containing $1 as a placeholder where the phrase is supposed to be
153     * inserted.
154     *
155     * @param string $text         A text containing phrases to highlight.
156     * @param string $phrase       A phrase to highlight in $text.
157     * @param string $highlighter  A highlighting replacement.
158     *
159     * @return string  The highlighted text.
160     */
161    public function highlight($text, $phrase, $highlighter = null)
162    {
163        if (empty($highlighter)) {
164            $highlighter = '<strong class="highlight">$1</strong>';
165        }
166        if (empty($phrase) || empty($text)) {
167            return $text;
168        }
169        return preg_replace('/(' . preg_quote($phrase, '/') . ')/',
170                            $highlighter,
171                            $text);
172    }
173
174    /**
175     * If $text is longer than $length, $text will be truncated to the length
176     * of $length and the last three characters will be replaced with the
177     * $truncateString.
178     *
179     * <code>
180     * $this->truncate('Once upon a time in a world far far away', 14);
181     * // => Once upon a...
182     * </code>
183     *
184     * @param string $text            A text to truncate.
185     * @param integer $length         The maximum length of the text
186     * @param string $truncateString  Replacement string for the truncated
187     *                                text.
188     *
189     * @return string  The truncated text.
190     */
191    public function truncate($text, $length = 30, $truncateString = '...')
192    {
193        if (empty($text)) {
194            return $text;
195        }
196        return Horde_String::length($text) > $length
197            ? rtrim(Horde_String::substr($text, 0, $length - Horde_String::length($truncateString))) . $truncateString
198            : $text;
199    }
200
201    /**
202     * Limits a string to a given maximum length in a smarter way than just
203     * using substr().
204     *
205     * Namely, cut from the MIDDLE instead of from the end so that if we're
206     * doing this on (for instance) a bunch of binder names that start off with
207     * the same verbose description, and then are different only at the very
208     * end, they'll still be different from one another after truncating.
209     *
210     * <code>
211     * $str = 'The quick brown fox jumps over the lazy dog tomorrow morning.';
212     * $shortStr = $this->truncateMiddle($str, 40);
213     * // $shortStr == 'The quick brown fox... tomorrow morning.'
214     * </code>
215     *
216     * @param string $str         A text to truncate.
217     * @param integer $maxLength  The maximum length of the text
218     * @param string $joiner      Replacement string for the truncated text.
219     *
220     * @return string  The truncated text.
221     */
222    public function truncateMiddle($str, $maxLength = 80, $joiner = '...')
223    {
224        if (Horde_String::length($str) <= $maxLength) {
225            return $str;
226        }
227        $maxLength = $maxLength - Horde_String::length($joiner);
228        if ($maxLength <= 0) {
229            return $str;
230        }
231        $startPieceLength = (int) ceil($maxLength / 2);
232        $endPieceLength = (int) floor($maxLength / 2);
233        $trimmedString = rtrim(Horde_String::substr($str, 0, $startPieceLength)) . $joiner;
234        if ($endPieceLength > 0) {
235            $trimmedString .= ltrim(Horde_String::substr($str, (-1 * $endPieceLength)));
236        }
237        return $trimmedString;
238    }
239
240    /**
241     * Inserts HTML code to allow linebreaks in a string after slashes or
242     * underscores.
243     *
244     * @param string $str  A string to mark up with linebreak markers.
245     *
246     * @return string  The marked-up string.
247     */
248    public function makeBreakable($str)
249    {
250        return str_replace(
251            array('/',      '_'),
252            array('/<wbr>', '_<wbr>'),
253            $str
254        );
255    }
256
257    /**
258     * Removes smart quotes.
259     *
260     * @see http://shiflett.org/blog/2005/oct/convert-smart-quotes-with-php
261     *
262     * @param string $str  A string with potential smart quotes.
263     *
264     * @return string  The cleaned-up string.
265     */
266    public function cleanSmartQuotes($str)
267    {
268        $search = array(
269            '/\x96/',
270            '/\xE2\x80\x93/',
271            '/\x97/',
272            '/\xE2\x80\x94/',
273            '/\x91/',
274            '/\xE2\x80\x98/',
275            '/\x92/',
276            '/\xE2\x80\x99/',
277            '/\x93/',
278            '/\xE2\x80\x9C/',
279            '/\x94/',
280            '/\xE2\x80\x9D/',
281            '/\x85/',
282            '/\xE2\x80\xA6/',
283            '/\x95/',
284            '/\xE2\x80\xA2/',
285            '/\x09/',
286
287            // The order of these is very important.
288            '/\xC2\xBC/',
289            '/\xBC/',
290            '/\xC2\xBD/',
291            '/\xBD/',
292            '/\xC2\xBE/',
293            '/\xBE/',
294        );
295
296        $replace = array(
297            '-',
298            '-',
299            '--',
300            '--',
301            "'",
302            "'",
303            "'",
304            "'",
305            '"',
306            '"',
307            '"',
308            '"',
309            '...',
310            '...',
311            '*',
312            '*',
313            ' ',
314
315            '1/4',
316            '1/4',
317            '1/2',
318            '1/2',
319            '3/4',
320            '3/4',
321        );
322
323        return preg_replace($search, $replace, $str);
324    }
325}
326