# frozen_string_literal: true require "sanitize" class Search class GroupedSearchResults include ActiveModel::Serialization class TextHelper extend ActionView::Helpers::TextHelper end attr_reader( :type_filter, :posts, :categories, :extra_categories, :users, :tags, :groups, :more_posts, :more_categories, :more_users, :term, :search_context, :more_full_page_results, :error, :use_pg_headlines_for_excerpt, :can_lazy_load_categories, ) attr_accessor :search_log_id BLURB_LENGTH = 200 def initialize( type_filter:, term:, search_context:, blurb_length: nil, blurb_term: nil, is_header_search: false, use_pg_headlines_for_excerpt: SiteSetting.use_pg_headlines_for_excerpt, can_lazy_load_categories: false ) @type_filter = type_filter @term = term @blurb_term = blurb_term || term @search_context = search_context @blurb_length = blurb_length || BLURB_LENGTH @posts = [] @categories = [] @extra_categories = Set.new @users = [] @tags = [] @groups = [] @error = nil @is_header_search = is_header_search @use_pg_headlines_for_excerpt = use_pg_headlines_for_excerpt @can_lazy_load_categories = can_lazy_load_categories end def error=(error) @error = error end def find_user_data(guardian) if user = guardian.user topics = @posts.map(&:topic) topic_lookup = TopicUser.lookup_for(user, topics) topics.each { |ft| ft.user_data = topic_lookup[ft.id] } end end OMISSION = "..." SCRUB_HEADLINE_REGEXP = %r{([^<]*)} def blurb(post) opts = { term: @blurb_term, blurb_length: @blurb_length } post_search_data_version = post&.post_search_data&.version if post_search_data_version.present? && post_search_data_version >= SearchIndexer::MIN_POST_BLURB_INDEX_VERSION && !Search.segment_chinese? && !Search.segment_japanese? if use_pg_headlines_for_excerpt scrubbed_headline = post.headline.gsub(SCRUB_HEADLINE_REGEXP, '\1') prefix_omission = scrubbed_headline.start_with?(post.leading_raw_data) ? "" : OMISSION postfix_omission = scrubbed_headline.end_with?(post.trailing_raw_data) ? "" : OMISSION return "#{prefix_omission}#{post.headline}#{postfix_omission}" else opts[:cooked] = post.post_search_data.raw_data opts[:scrub] = false end else opts[:cooked] = post.cooked end GroupedSearchResults.blurb_for(**opts) end def add(object) type = object.class.to_s.downcase.pluralize if !@is_header_search && public_send(type).length == Search.per_filter @more_full_page_results = true elsif @is_header_search && public_send(type).length == Search.per_facet instance_variable_set("@more_#{type}".to_sym, true) else (self.public_send(type)) << object end if can_lazy_load_categories category = case type when "posts" object.topic.category when "topics" object.category end if category extra_categories << category.parent_category if category.parent_category extra_categories << category end end end def self.blurb_for(cooked: nil, term: nil, blurb_length: BLURB_LENGTH, scrub: true) blurb = nil if scrub cooked = SearchIndexer::HtmlScrubber.scrub(cooked) urls = Set.new cooked.scan(Discourse::Utils::URI_REGEXP) { urls << $& } urls.each do |url| begin case File.extname(URI(url).path || "") when Oneboxer::VIDEO_REGEX cooked.gsub!(url, I18n.t("search.video")) when Oneboxer::AUDIO_REGEX cooked.gsub!(url, I18n.t("search.audio")) end rescue URI::InvalidURIError end end end if term term = Regexp.last_match[1] if term =~ Regexp.new(Search::PHRASE_MATCH_REGEXP_PATTERN) blurb = TextHelper.excerpt(cooked, term, radius: blurb_length / 2) end blurb = TextHelper.truncate(cooked, length: blurb_length) if blurb.blank? Sanitize.clean(blurb) end end end