1: <?php
2: /**
3: * Highlights quoted messages with different colors for the different quoting
4: * levels.
5: *
6: * CSS class names called "quoted1" ... "quoted{$cssLevels}" must be present.
7: *
8: * The text to be passed in must have already been passed through
9: * htmlspecialchars().
10: *
11: * Parameters:
12: * <pre>
13: * 'citeblock' -- Display cite blocks?
14: * DEFAULT: true
15: * 'cssLevels' -- Number of defined CSS class names.
16: * DEFAULT: 5
17: * 'hideBlocks' -- Hide large quoted text blocks by default?
18: * DEFAULT: false
19: * </pre>
20: *
21: * Copyright 2004-2012 Horde LLC (http://www.horde.org/)
22: *
23: * See the enclosed file COPYING for license information (LGPL). If you
24: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
25: *
26: * @author Michael Slusarz <slusarz@horde.org>
27: * @author Jan Schneider <jan@horde.org>
28: * @category Horde
29: * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
30: * @package Text_Filter
31: */
32: class Horde_Text_Filter_Highlightquotes extends Horde_Text_Filter_Base
33: {
34: /**
35: * Filter parameters.
36: *
37: * @var array
38: */
39: protected $_params = array(
40: 'citeblock' => true,
41: 'cssLevels' => 5,
42: 'hideBlocks' => false
43: );
44:
45: /**
46: * The number of quoted lines to exceed to trigger large block
47: * processing.
48: *
49: * @var integer
50: */
51: protected $_qlimit = 8;
52:
53: /**
54: * Executes any code necessaray before applying the filter patterns.
55: *
56: * @param string $text The text before the filtering.
57: *
58: * @return string The modified text.
59: */
60: public function preProcess($text)
61: {
62: /* Tack a newline onto the beginning of the string so that we
63: * correctly highlight when the first character in the string is a
64: * quote character. */
65: return "\n$text";
66: }
67:
68: /**
69: * Returns a hash with replace patterns.
70: *
71: * @return array Patterns hash.
72: */
73: public function getPatterns()
74: {
75: /* Remove extra spaces before quoted text as the CSS formatting will
76: * automatically add a bit of space for us. */
77: return ($this->_params['citeblock'])
78: ? array('regexp' => array("/<br \/>\s*\n\s*<br \/>\s*\n\s*((>\s?)+)/m" => "<br />\n\\1"))
79: : array();
80: }
81:
82: /**
83: * Executes any code necessaray after applying the filter patterns.
84: *
85: * @param string $text The text after the filtering.
86: *
87: * @return string The modified text.
88: */
89: public function postProcess($text)
90: {
91: /* Use cite blocks to display the different quoting levels? */
92: $cb = $this->_params['citeblock'];
93:
94: /* Cite level before parsing the current line. */
95: $qlevel = 0;
96:
97: /* Other loop variables. */
98: $text_out = '';
99: $lines = array();
100: $tmp = array('level' => 0, 'lines' => array());
101: $qcount = 0;
102:
103: /* Parse text line by line. */
104: foreach (explode("\n", $text) as $line) {
105: /* Cite level of current line. */
106: $clevel = 0;
107: $matches = array();
108:
109: /* Do we have a citation line? */
110: if (preg_match('/^\s*((>\s?)+)/m', $line, $matches)) {
111: /* Count number of > characters => cite level */
112: $clevel = count(preg_split('/>\s?/', $matches[1])) - 1;
113: }
114:
115: if ($cb && isset($matches[1])) {
116: /* Strip all > characters. */
117: $line = substr($line, Horde_String::length($matches[1]));
118: }
119:
120: /* Is this cite level lower than the current level? */
121: if ($clevel < $qlevel) {
122: $lines[] = $tmp;
123: if ($clevel == 0) {
124: $text_out .= $this->_process($lines, $qcount);
125: $lines = array();
126: $qcount = 0;
127: }
128: $tmp = array('level' => $clevel, 'lines' => array());
129:
130: /* Is this cite level higher than the current level? */
131: } elseif ($clevel > $qlevel) {
132: $lines[] = $tmp;
133: $tmp = array('level' => $clevel, 'lines' => array());
134: }
135:
136: $tmp['lines'][] = $line;
137: $qlevel = $clevel;
138:
139: if ($qlevel) {
140: ++$qcount;
141: }
142: }
143:
144: $lines[] = $tmp;
145: $text_out .= $this->_process($lines, $qcount);
146:
147: /* Remove the leading newline we added above, if it's still there. */
148: return ($text_out[0] == "\n")
149: ? substr($text_out, 1)
150: : $text_out;
151: }
152:
153: /**
154: * Process a batch of lines at the same quoted level.
155: *
156: * @param array $lines Lines.
157: * @param integer $qcount Number of lines in quoted level.
158: *
159: * @return string The rendered lines.
160: */
161: protected function _process($lines, $qcount)
162: {
163: $curr = reset($lines);
164: $out = implode("\n", $this->_removeBr($curr['lines']));
165:
166: if ($qcount > $this->_qlimit) {
167: $out .= $this->_beginLargeBlock($lines, $qcount);
168: }
169:
170: $level = 0;
171:
172: next($lines);
173: while (list(,$curr) = each($lines)) {
174: if ($level > $curr['level']) {
175: for ($i = $level; $i > $curr['level']; --$i) {
176: $out .= $this->_params['citeblock'] ? '</div>' : '</font>';
177: }
178: } else {
179: for ($i = $level; $i < $curr['level']; ++$i) {
180: /* Add quote block start tags for each cite level. */
181: $out .= ($this->_params['citeblock'] ? '<div class="citation ' : '<font class="') .
182: 'quoted' . (($i % $this->_params['cssLevels']) + 1) . '"' .
183: ((($i == 0) && ($qcount > $this->_qlimit) && $this->_params['hideBlocks']) ? ' style="display:none"' : '') .
184: '>';
185: }
186: }
187:
188: $out .= implode("\n", $this->_removeBr($curr['lines']));
189: $level = $curr['level'];
190: }
191:
192: for ($i = $level; $i > 0; --$i) {
193: $out .= $this->_params['citeblock'] ? '</div>' : '</font>';
194: }
195:
196: if ($qcount > $this->_qlimit) {
197: $out .= $this->_endLargeBlock($lines, $qcount);
198: }
199:
200: return $out;
201: }
202:
203: /**
204: * Add HTML code at the beginning of a large block of quoted lines.
205: *
206: * @param array $lines Lines.
207: * @param integer $qcount Number of lines in quoted level.
208: *
209: * @return string HTML code.
210: */
211: protected function _beginLargeBlock($lines, $qcount)
212: {
213: return '';
214: }
215:
216: /**
217: * Add HTML code at the end of a large block of quoted lines.
218: *
219: * @param array $lines Lines.
220: * @param integer $qcount Number of lines in quoted level.
221: *
222: * @return string HTML code.
223: */
224: protected function _endLargeBlock($lines, $qcount)
225: {
226: return '';
227: }
228:
229: /**
230: * Remove leading and trailing BR tags.
231: *
232: * @param array $lines An array of text.
233: *
234: * @return array The array with bare BR tags removed at the beginning and
235: * end.
236: */
237: protected function _removeBr($lines)
238: {
239: /* Remove leading/trailing line breaks. Spacing between quote blocks
240: * will be handled by div CSS. */
241: if (!$this->_params['citeblock']) {
242: return $lines;
243: }
244:
245: foreach (array_keys($lines) as $i) {
246: if (!preg_match("/^\s*<br\s*\/>\s*$/i", $lines[$i])) {
247: break;
248: }
249: unset($lines[$i]);
250: }
251:
252: foreach (array_reverse(array_keys($lines)) as $i) {
253: if (!preg_match("/^\s*<br\s*\/>\s*$/i", $lines[$i])) {
254: break;
255: }
256: unset($lines[$i]);
257: }
258:
259: return $lines;
260: }
261:
262: }
263: