]> git.openstreetmap.org Git - rails.git/blob - lib/rich_text.rb
Merge branch 'pull/5187'
[rails.git] / lib / rich_text.rb
1 module RichText
2   SPAMMY_PHRASES = [
3     "Business Description:", "Additional Keywords:"
4   ].freeze
5
6   MAX_DESCRIPTION_LENGTH = 500
7
8   def self.new(format, text)
9     case format
10     when "html" then HTML.new(text || "")
11     when "markdown" then Markdown.new(text || "")
12     when "text" then Text.new(text || "")
13     end
14   end
15
16   class SimpleFormat
17     include ActionView::Helpers::TextHelper
18     include ActionView::Helpers::OutputSafetyHelper
19
20     def sanitize(text, _options = {})
21       Sanitize.clean(text, Sanitize::Config::OSM).html_safe
22     end
23   end
24
25   class Base < String
26     include ActionView::Helpers::TagHelper
27
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)
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       if text.html_safe?
78         Rinku.auto_link(text, mode, tag_builder.tag_options(:rel => "nofollow noopener noreferrer")).html_safe
79       else
80         Rinku.auto_link(text, mode, tag_builder.tag_options(:rel => "nofollow noopener noreferrer"))
81       end
82     end
83   end
84
85   class HTML < Base
86     def to_html
87       linkify(sanitize(simple_format(self)))
88     end
89
90     def to_text
91       to_s
92     end
93   end
94
95   class Markdown < Base
96     def to_html
97       linkify(sanitize(document.to_html), :all)
98     end
99
100     def to_text
101       to_s
102     end
103
104     def image
105       @image_element = first_image_element(document.root) unless defined? @image_element
106       @image_element.attr["src"] if @image_element
107     end
108
109     def image_alt
110       @image_element = first_image_element(document.root) unless defined? @image_element
111       @image_element.attr["alt"] if @image_element
112     end
113
114     def description
115       return @description if defined? @description
116
117       @description = first_truncated_text_content(document.root)
118     end
119
120     private
121
122     def document
123       @document ||= Kramdown::Document.new(self)
124     end
125
126     def first_image_element(element)
127       return element if image?(element) && element.attr["src"].present?
128
129       element.children.find do |child|
130         nested_image = first_image_element(child)
131         break nested_image if nested_image
132       end
133     end
134
135     def first_truncated_text_content(element)
136       if paragraph?(element)
137         truncated_text_content(element)
138       else
139         element.children.find do |child|
140           text = first_truncated_text_content(child)
141           break text unless text.nil?
142         end
143       end
144     end
145
146     def truncated_text_content(element)
147       text = ""
148
149       append_text = lambda do |child|
150         if child.type == :text
151           text << child.value
152         else
153           child.children.each do |c|
154             append_text.call(c)
155             break if text.length > MAX_DESCRIPTION_LENGTH
156           end
157         end
158       end
159       append_text.call(element)
160
161       return nil if text.blank?
162
163       text.truncate(MAX_DESCRIPTION_LENGTH)
164     end
165
166     def image?(element)
167       element.type == :img || (element.type == :html_element && element.value == "img")
168     end
169
170     def paragraph?(element)
171       element.type == :p || (element.type == :html_element && element.value == "p")
172     end
173   end
174
175   class Text < Base
176     def to_html
177       linkify(simple_format(ERB::Util.html_escape(self)))
178     end
179
180     def to_text
181       to_s
182     end
183   end
184 end