1 # frozen_string_literal: true
5 "Business Description:", "Additional Keywords:"
8 MAX_DESCRIPTION_LENGTH = 500
10 def self.new(format, text)
12 when "html" then HTML.new(text || "")
13 when "markdown" then Markdown.new(text || "")
14 when "text" then Text.new(text || "")
19 include ActionView::Helpers::TextHelper
20 include ActionView::Helpers::OutputSafetyHelper
22 def sanitize(text, _options = {})
23 Sanitize.clean(text, Sanitize::Config::OSM).html_safe
32 doc = Nokogiri::HTML(to_html)
37 doc.xpath("//a").each do |link|
39 link_size += link.content.length
42 link_proportion = link_size.to_f / doc.content.length
45 spammy_phrases = SPAMMY_PHRASES.count do |phrase|
46 doc.content.include?(phrase)
49 ([link_proportion - 0.2, 0.0].max * 200) +
66 def truncate_html(max_length = nil, img_length = 1000)
68 return html_doc if max_length.nil?
70 doc = Nokogiri::HTML::DocumentFragment.parse(html_doc)
71 keep_or_discards = %w[p h1 h2 h3 h4 h5 h6 pre a table ul ol dl]
72 accumulated_length = 0
73 exceeded_node_parent = nil
76 doc.traverse do |node|
77 if accumulated_length >= max_length
78 if node == exceeded_node_parent
79 exceeded_node_parent = node.parent
80 node.remove if keep_or_discards.include?(node.name)
87 next unless node.children.empty?
90 accumulated_length += node.text.length
91 elsif node.name == "img"
92 accumulated_length += img_length
95 if accumulated_length >= max_length
97 exceeded_node_parent = node.parent
103 :truncated => truncated,
104 :html => doc.to_html.html_safe
110 def simple_format(text)
111 SimpleFormat.new.simple_format(text, :dir => "auto")
115 Sanitize.clean(text, Sanitize::Config::OSM).html_safe
118 def linkify(text, mode = :urls)
119 link_attr = 'rel="nofollow noopener noreferrer"'
120 Rinku.auto_link(ERB::Util.html_escape(text), mode, link_attr) do |url|
121 url = shorten_host(url, Settings.linkify_hosts, Settings.linkify_hosts_replacement)
122 shorten_host(url, Settings.linkify_wiki_hosts, Settings.linkify_wiki_hosts_replacement) do |path|
123 path.sub(Regexp.new(Settings.linkify_wiki_optional_path_prefix || ""), "")
130 def shorten_host(url, hosts, hosts_replacement)
131 %r{^(https?://([^/]*))(.*)$}.match(url) do |m|
132 scheme_host, host, path = m.captures
133 if hosts&.include?(host)
134 path = yield(path) if block_given?
136 "#{hosts_replacement}#{path}"
138 "#{scheme_host}#{path}"
147 linkify(simple_format(self))
155 class Markdown < Base
157 linkify(sanitize(document.to_html), :all)
165 @image_element = first_image_element(document.root) unless defined? @image_element
166 @image_element.attr["src"] if @image_element
170 @image_element = first_image_element(document.root) unless defined? @image_element
171 @image_element.attr["alt"] if @image_element
175 return @description if defined? @description
177 @description = first_truncated_text_content(document.root)
183 @document ||= Kramdown::Document.new(self)
186 def first_image_element(element)
187 return element if image?(element) && element.attr["src"].present?
189 element.children.find do |child|
190 nested_image = first_image_element(child)
191 break nested_image if nested_image
195 def first_truncated_text_content(element)
196 if paragraph?(element)
197 truncated_text_content(element)
199 element.children.find do |child|
200 text = first_truncated_text_content(child)
201 break text unless text.nil?
206 def truncated_text_content(element)
209 append_text = lambda do |child|
210 if child.type == :text
213 child.children.each do |c|
215 break if text.length > MAX_DESCRIPTION_LENGTH
219 append_text.call(element)
221 return nil if text.blank?
223 text.truncate(MAX_DESCRIPTION_LENGTH)
227 element.type == :img || (element.type == :html_element && element.value == "img")
230 def paragraph?(element)
231 element.type == :p || (element.type == :html_element && element.value == "p")
237 linkify(simple_format(ERB::Util.html_escape(self)))