]> git.openstreetmap.org Git - rails.git/blob - lib/rich_text.rb
Remove TagHelper include from RichText module
[rails.git] / lib / rich_text.rb
1 # frozen_string_literal: true
2
3 module RichText
4   SPAMMY_PHRASES = [
5     "Business Description:", "Additional Keywords:"
6   ].freeze
7
8   MAX_DESCRIPTION_LENGTH = 500
9
10   def self.new(format, text)
11     case format
12     when "html" then HTML.new(text || "")
13     when "markdown" then Markdown.new(text || "")
14     when "text" then Text.new(text || "")
15     end
16   end
17
18   class SimpleFormat
19     include ActionView::Helpers::TextHelper
20     include ActionView::Helpers::OutputSafetyHelper
21
22     def sanitize(text, _options = {})
23       Sanitize.clean(text, Sanitize::Config::OSM).html_safe
24     end
25   end
26
27   class Base < String
28     def spam_score
29       link_count = 0
30       link_size = 0
31
32       doc = Nokogiri::HTML(to_html)
33
34       if doc.content.empty?
35         link_proportion = 0
36       else
37         doc.xpath("//a").each do |link|
38           link_count += 1
39           link_size += link.content.length
40         end
41
42         link_proportion = link_size.to_f / doc.content.length
43       end
44
45       spammy_phrases = SPAMMY_PHRASES.count do |phrase|
46         doc.content.include?(phrase)
47       end
48
49       ([link_proportion - 0.2, 0.0].max * 200) +
50         (link_count * 40) +
51         (spammy_phrases * 40)
52     end
53
54     def image
55       nil
56     end
57
58     def image_alt
59       nil
60     end
61
62     def description
63       nil
64     end
65
66     protected
67
68     def simple_format(text)
69       SimpleFormat.new.simple_format(text, :dir => "auto")
70     end
71
72     def sanitize(text)
73       Sanitize.clean(text, Sanitize::Config::OSM).html_safe
74     end
75
76     def linkify(text, mode = :urls)
77       link_attr = 'rel="nofollow noopener noreferrer"'
78       Rinku.auto_link(ERB::Util.html_escape(text), mode, link_attr) do |url|
79         %r{^https?://([^/]*)(.*)$}.match(url) do |m|
80           "#{Settings.linkify_hosts_replacement}#{m[2]}" if Settings.linkify_hosts_replacement &&
81                                                             Settings.linkify_hosts&.include?(m[1])
82         end || url
83       end.html_safe
84     end
85   end
86
87   class HTML < Base
88     def to_html
89       linkify(sanitize(simple_format(self)))
90     end
91
92     def to_text
93       to_s
94     end
95   end
96
97   class Markdown < Base
98     def to_html
99       linkify(sanitize(document.to_html), :all)
100     end
101
102     def to_text
103       to_s
104     end
105
106     def image
107       @image_element = first_image_element(document.root) unless defined? @image_element
108       @image_element.attr["src"] if @image_element
109     end
110
111     def image_alt
112       @image_element = first_image_element(document.root) unless defined? @image_element
113       @image_element.attr["alt"] if @image_element
114     end
115
116     def description
117       return @description if defined? @description
118
119       @description = first_truncated_text_content(document.root)
120     end
121
122     private
123
124     def document
125       @document ||= Kramdown::Document.new(self)
126     end
127
128     def first_image_element(element)
129       return element if image?(element) && element.attr["src"].present?
130
131       element.children.find do |child|
132         nested_image = first_image_element(child)
133         break nested_image if nested_image
134       end
135     end
136
137     def first_truncated_text_content(element)
138       if paragraph?(element)
139         truncated_text_content(element)
140       else
141         element.children.find do |child|
142           text = first_truncated_text_content(child)
143           break text unless text.nil?
144         end
145       end
146     end
147
148     def truncated_text_content(element)
149       text = +""
150
151       append_text = lambda do |child|
152         if child.type == :text
153           text << child.value
154         else
155           child.children.each do |c|
156             append_text.call(c)
157             break if text.length > MAX_DESCRIPTION_LENGTH
158           end
159         end
160       end
161       append_text.call(element)
162
163       return nil if text.blank?
164
165       text.truncate(MAX_DESCRIPTION_LENGTH)
166     end
167
168     def image?(element)
169       element.type == :img || (element.type == :html_element && element.value == "img")
170     end
171
172     def paragraph?(element)
173       element.type == :p || (element.type == :html_element && element.value == "p")
174     end
175   end
176
177   class Text < Base
178     def to_html
179       linkify(simple_format(ERB::Util.html_escape(self)))
180     end
181
182     def to_text
183       to_s
184     end
185   end
186 end