]> git.openstreetmap.org Git - rails.git/blob - lib/rich_text.rb
Make api changeset index path resourceful
[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     include ActionView::Helpers::TagHelper
29
30     def spam_score
31       link_count = 0
32       link_size = 0
33
34       doc = Nokogiri::HTML(to_html)
35
36       if doc.content.empty?
37         link_proportion = 0
38       else
39         doc.xpath("//a").each do |link|
40           link_count += 1
41           link_size += link.content.length
42         end
43
44         link_proportion = link_size.to_f / doc.content.length
45       end
46
47       spammy_phrases = SPAMMY_PHRASES.count do |phrase|
48         doc.content.include?(phrase)
49       end
50
51       ([link_proportion - 0.2, 0.0].max * 200) +
52         (link_count * 40) +
53         (spammy_phrases * 40)
54     end
55
56     def image
57       nil
58     end
59
60     def image_alt
61       nil
62     end
63
64     def description
65       nil
66     end
67
68     protected
69
70     def simple_format(text)
71       SimpleFormat.new.simple_format(text)
72     end
73
74     def sanitize(text)
75       Sanitize.clean(text, Sanitize::Config::OSM).html_safe
76     end
77
78     def linkify(text, mode = :urls)
79       if text.html_safe?
80         Rinku.auto_link(text, mode, tag_builder.tag_options(:rel => "nofollow noopener noreferrer")).html_safe
81       else
82         Rinku.auto_link(text, mode, tag_builder.tag_options(:rel => "nofollow noopener noreferrer"))
83       end
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