PluginProbe
Extendify / trunk
Extendify vtrunk
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
← All changes | app/Agent/Controllers/ContentController.php +54 -13 3.1.5 → trunk View file →
@@ -260,31 +260,72 @@
260 260 * @return array The snippets, and how many times the text says it.
261 261 */
262 262 private static function snippets($text, $query, $context)
263 263 {
264 - $length = mb_strlen($query);
265 - $end = mb_strlen($text);
264 + // Turkish İ lower cases to two characters, so a folded copy's offsets miss the match.
265 + $pattern = '/' . preg_quote($query, '/') . '/iu';
266 + if (!preg_match_all($pattern, $text, $found, PREG_OFFSET_CAPTURE)) {
267 + return ['matches' => [], 'count' => 0];
268 + }
266 269
270 + $end = strlen($text);
267 271 $matches = [];
268 - $count = 0;
269 272 $quoted = 0;
270 - $offset = 0;
271 - // Turkish İ lower cases to two characters, so a folded copy's offsets miss the match.
272 - while (($at = mb_stripos($text, $query, $offset)) !== false) {
273 - $count++;
274 - $offset = $at + $length;
273 + foreach ($found[0] as $occurrence) {
274 + list($match, $at) = $occurrence;
275 275 if (count($matches) >= self::MAX_MATCHES || $at < $quoted) {
276 276 continue;
277 277 }
278 278
279 - $from = max(0, $at - $context);
280 - $quoted = min($end, $at + $length + $context);
281 - $matches[] = ($from > 0 ? '…' : '')
282 - . mb_substr($text, $from, $quoted - $from)
279 + $before = self::quoteBefore($text, $at, $context);
280 + $after = self::quoteAfter($text, $at + strlen($match), $context);
281 + $quoted = $at + strlen($match) + strlen($after);
282 + $matches[] = ($at > strlen($before) ? '…' : '')
283 + . $before . $match . $after
283 284 . ($quoted < $end ? '…' : '');
284 285 }
285 286
286 - return ['matches' => $matches, 'count' => $count];
287 + return ['matches' => $matches, 'count' => count($found[0])];
288 + }
289 +
290 + /**
291 + * Up to $context characters of $text ending at byte offset $at.
292 + *
293 + * @param string $text The text a match was found in.
294 + * @param integer $at Where the match begins, in bytes.
295 + * @param integer $context How many characters to quote.
296 + * @return string
297 + */
298 + private static function quoteBefore($text, $at, $context)
299 + {
300 + // A character is at most four bytes, so the window always holds enough.
301 + $slice = substr($text, max(0, $at - (4 * $context)), min($at, 4 * $context));
302 + // A slice starting mid-character would fail the /u match and empty the quote.
303 + $slice = (string) preg_replace('/^[\x80-\xBF]+/', '', $slice);
304 + preg_match('/.{0,' . $context . '}$/su', $slice, $match);
305 +
306 + return $match[0] ?? '';
307 + }
308 +
309 + /**
310 + * Up to $context characters of $text starting at byte offset $at.
311 + *
312 + * @param string $text The text a match was found in.
313 + * @param integer $at Where the match ends, in bytes.
314 + * @param integer $context How many characters to quote.
315 + * @return string
316 + */
317 + private static function quoteAfter($text, $at, $context)
318 + {
319 + // A slice ending mid-character would fail the /u match and empty the quote.
320 + $slice = (string) preg_replace(
321 + '/(?:[\xC2-\xDF]|[\xE0-\xEF][\x80-\xBF]?|[\xF0-\xF4][\x80-\xBF]{0,2})$/D',
322 + '',
323 + substr($text, $at, 4 * $context)
324 + );
325 + preg_match('/^.{0,' . $context . '}/su', $slice, $match);
326 +
327 + return $match[0] ?? '';
287 328 }
288 329
289 330 /**
290 331 * @param integer $id A post id.