OSDN Git Service

AstroProfundis/embrr
[embrj/master.git] / common / HitHighlighter.php
1 <?php
2 /**
3  * @author     Nick Pope <nick@nickpope.me.uk>
4  * @copyright  Copyright © 2010, Nick Pope
5  * @license    http://www.apache.org/licenses/LICENSE-2.0  Apache License v2.0
6  * @package    Twitter
7  */
8
9 require_once 'Regex.php';
10
11 /**
12  * Twitter HitHighlighter Class
13  *
14  * Performs "hit highlighting" on tweets that have been auto-linked already.
15  * Useful with the results returned from the search API.
16  *
17  * Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
18  * is based on code by {@link http://github.com/mzsanford Matt Sanford} and
19  * heavily modified by {@link http://github.com/ngnpope Nick Pope}.
20  *
21  * @author     Nick Pope <nick@nickpope.me.uk>
22  * @copyright  Copyright © 2010, Nick Pope
23  * @license    http://www.apache.org/licenses/LICENSE-2.0  Apache License v2.0
24  * @package    Twitter
25  */
26 class Twitter_HitHighlighter extends Twitter_Regex {
27
28   /**
29    * The tag to surround hits with.
30    *
31    * @var  string
32    */
33   protected $tag = 'em';
34
35   /**
36    * Provides fluent method chaining.
37    *
38    * @param  string  $tweet        The tweet to be hit highlighted.
39    * @param  bool    $full_encode  Whether to encode all special characters.
40    *
41    * @see  __construct()
42    *
43    * @return  Twitter_HitHighlighter
44    */
45   public static function create($tweet, $full_encode = false) {
46     return new self($tweet, $full_encode);
47   }
48
49   /**
50    * Reads in a tweet to be parsed and hit highlighted.
51    *
52    * We take this opportunity to ensure that we escape user input.
53    *
54    * @see  htmlspecialchars()
55    *
56    * @param  string  $tweet        The tweet to be hit highlighted.
57    * @param  bool    $escape       Whether to escape the tweet (default: true).
58    * @param  bool    $full_encode  Whether to encode all special characters.
59    */
60   public function __construct($tweet, $escape = true, $full_encode = false) {
61     if ($escape) {
62       if ($full_encode) {
63         parent::__construct(htmlentities($tweet, ENT_QUOTES, 'UTF-8', false));
64       } else {
65         parent::__construct(htmlspecialchars($tweet, ENT_QUOTES, 'UTF-8', false));
66       }
67     } else {
68       parent::__construct($tweet);
69     }
70   }
71
72   /**
73    * Set the highlighting tag to surround hits with.  The default tag is 'em'.
74    *
75    * @return  string  The tag name.
76    */
77   public function getTag() {
78     return $this->tag;
79   }
80
81   /**
82    * Set the highlighting tag to surround hits with.  The default tag is 'em'.
83    *
84    * @param  string  $v  The tag name.
85    *
86    * @return  Twitter_HitHighlighter  Fluid method chaining.
87    */
88   public function setTag($v) {
89     $this->tag = $v;
90     return $this;
91   }
92
93   /**
94    * Hit highlights the tweet.
95    *
96    * @param  array  $hits  An array containing the start and end index pairs
97    *                       for the highlighting.
98    *
99    * @return  string  The hit highlighted tweet.
100    */
101   public function addHitHighlighting(array $hits) {
102     if (empty($hits)) return $this->tweet;
103     $tweet = '';
104     $tags = array('<'.$this->tag.'>', '</'.$this->tag.'>');
105     # Check whether we can simply replace or whether we need to chunk...
106     if (strpos($this->tweet, '<') === false) {
107       $ti = 0; // tag increment (for added tags)
108       $tweet = $this->tweet;
109       foreach ($hits as $hit) {
110         $tweet = self::mb_substr_replace($tweet, $tags[0], $hit[0] + $ti, 0);
111         $ti += mb_strlen($tags[0]);
112         $tweet = self::mb_substr_replace($tweet, $tags[1], $hit[1] + $ti, 0);
113         $ti += mb_strlen($tags[1]);
114       }
115     } else {
116       $chunks = preg_split('/[<>]/iu', $this->tweet);
117       $chunk = $chunks[0];
118       $chunk_index = 0;
119       $chunk_cursor = 0;
120       $offset = 0;
121       $start_in_chunk = false;
122       # Flatten the multidimensional hits array:
123       $hits_flat = array();
124       foreach ($hits as $hit) $hits_flat = array_merge($hits_flat, $hit);
125       # Loop over the hit indices:
126       for ($index = 0; $index < count($hits_flat); $index++) {
127         $hit = $hits_flat[$index];
128         $tag = $tags[$index % 2];
129         $placed = false;
130         while ($chunk !== null && $hit >= ($i = $offset + mb_strlen($chunk))) {
131           $tweet .= mb_substr($chunk, $chunk_cursor);
132           if ($start_in_chunk && $hit === $i) {
133             $tweet .= $tag;
134             $placed = true;
135           }
136           if (isset($chunks[$chunk_index+1])) $tweet .= '<' . $chunks[$chunk_index+1] . '>';
137           $offset += mb_strlen($chunk);
138           $chunk_cursor = 0;
139           $chunk_index += 2;
140           $chunk = (isset($chunks[$chunk_index]) ? $chunks[$chunk_index] : null);
141           $start_in_chunk = false;
142         }
143         if (!$placed && $chunk !== null) {
144           $hit_spot = $hit - $offset;
145           $tweet .= mb_substr($chunk, $chunk_cursor, $hit_spot - $chunk_cursor) . $tag;
146           $chunk_cursor = $hit_spot;
147           $start_in_chunk = ($index % 2 === 0);
148           $placed = true;
149         }
150         # Ultimate fallback - hits that run off the end get a closing tag:
151         if (!$placed) $tweet .= $tag;
152       }
153       if ($chunk !== null) {
154         if ($chunk_cursor < mb_strlen($chunk)) {
155           $tweet .= mb_substr($chunk, $chunk_cursor);
156         }
157         for ($index = $chunk_index + 1; $index < count($chunks); $index++) {
158           $tweet .= ($index % 2 === 0 ? $chunks[$index] : '<' . $chunks[$index] . '>');
159         }
160       }
161     }
162     return $tweet;
163   }
164
165   /**
166    * A multibyte-aware substring replacement function.
167    *
168    * @param  string  $string       The string to modify.
169    * @param  string  $replacement  The replacement string.
170    * @param  int     $start        The start of the replacement.
171    * @param  int     $length       The number of characters to replace.
172    * @param  string  $encoding     The encoding of the string.
173    *
174    * @return  string  The modified string.
175    *
176    * @see http://www.php.net/manual/en/function.substr-replace.php#90146
177    */
178   protected static function mb_substr_replace($string, $replacement, $start, $length = null, $encoding = null) {
179     if (extension_loaded('mbstring') === true) {
180       $string_length = (is_null($encoding) === true) ? mb_strlen($string) : mb_strlen($string, $encoding);
181       if ($start < 0) {
182         $start = max(0, $string_length + $start);
183       } else if ($start > $string_length) {
184         $start = $string_length;
185       }
186       if ($length < 0) {
187         $length = max(0, $string_length - $start + $length);
188       } else if ((is_null($length) === true) || ($length > $string_length)) {
189         $length = $string_length;
190       }
191       if (($start + $length) > $string_length) {
192         $length = $string_length - $start;
193       }
194       if (is_null($encoding) === true) {
195         return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length);
196       }
197       return mb_substr($string, 0, $start, $encoding) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length, $encoding);
198     }
199     return (is_null($length) === true) ? substr_replace($string, $replacement, $start) : substr_replace($string, $replacement, $start, $length);
200   }
201
202 }