ruby-bbcode 2.0.3 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,10 +8,10 @@ module RubyBBCode
8
8
  attr_reader :bbtree, :errors
9
9
 
10
10
  def initialize(text_to_parse, dictionary, escape_html = true)
11
- @text = escape_html ? text_to_parse.gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', "&quot;") : text_to_parse
11
+ @text = escape_html ? text_to_parse.gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;') : text_to_parse
12
12
 
13
13
  @dictionary = dictionary # dictionary containing all allowed/defined tags
14
- @bbtree = BBTree.new({:nodes => TagCollection.new})
14
+ @bbtree = BBTree.new(nodes: TagCollection.new)
15
15
  @ti = nil
16
16
  @errors = []
17
17
  end
@@ -25,34 +25,33 @@ module RubyBBCode
25
25
  # once this tree is built, the to_html method can be invoked where the tree is finally
26
26
  # converted into HTML syntax.
27
27
  def process_text
28
- regex_string = '((\[ (\/)? ( \* | (\w+)) ((=[^\[\]]+) | (\s\w+=\w+)* | ([^\]]*))? \]) | ([^\[]+))'
29
- @text.scan(/#{regex_string}/ix) do |tag_info|
28
+ @text.scan(TagInfo::REGEX) do |tag_info|
30
29
  @ti = TagInfo.new(tag_info, @dictionary)
31
30
 
32
- # if the tag isn't in the @dictionary list, then treat it as text
33
- @ti.handle_tag_as_text if @ti.element_is_tag? and !@ti.tag_in_dictionary?
34
-
35
31
  validate_element
36
32
 
37
33
  case @ti.type
38
34
  when :opening_tag
39
- element = {:is_tag => true, :tag => @ti[:tag], :definition => @ti.definition, :errors => @ti[:errors], :nodes => TagCollection.new }
35
+ element = { is_tag: true, tag: @ti[:tag], definition: @ti.definition, opening_whitespace: @ti.whitespace, errors: @ti[:errors], nodes: TagCollection.new }
40
36
  element[:invalid_quick_param] = true if @ti.invalid_quick_param?
41
37
  element[:params] = get_formatted_element_params
42
38
 
43
- @bbtree.retrogress_bbtree if self_closing_tag_reached_a_closer?
39
+ if self_closing_tag_reached_a_closer?
40
+ transfer_whitespace_to_closing_tag
41
+ @bbtree.retrogress_bbtree
42
+ end
44
43
 
45
44
  @bbtree.build_up_new_tag(element)
46
45
 
47
46
  @bbtree.escalate_bbtree(element)
48
47
  when :text
49
48
  tag_def = @bbtree.current_node.definition
50
- if tag_def and tag_def[:multi_tag] == true
49
+ if tag_def && (tag_def[:multi_tag] == true)
51
50
  set_multi_tag_to_actual_tag
52
51
  tag_def = @bbtree.current_node.definition
53
52
  end
54
53
 
55
- if within_open_tag? and tag_def[:require_between]
54
+ if within_open_tag? && tag_def[:require_between]
56
55
  between = get_formatted_between
57
56
  @bbtree.current_node[:between] = between
58
57
  if use_text_as_parameter?
@@ -63,7 +62,7 @@ module RubyBBCode
63
62
  add_element = true
64
63
 
65
64
  # ...and clear between, as this would result in two 'between' texts
66
- @bbtree.current_node[:between] = ""
65
+ @bbtree.current_node[:between] = ''
67
66
  end
68
67
  else
69
68
  # Between text can be used as (first) parameter
@@ -76,16 +75,20 @@ module RubyBBCode
76
75
 
77
76
  create_text_element
78
77
  when :closing_tag
78
+ @bbtree.current_node[:closing_whitespace] = @ti.whitespace
79
79
  if @ti[:wrong_closing]
80
80
  # Convert into text, so it
81
81
  @ti.handle_tag_as_text
82
82
  create_text_element
83
83
  else
84
- @bbtree.retrogress_bbtree if parent_of_self_closing_tag? and within_open_tag?
84
+ if parent_of_self_closing_tag? && within_open_tag?
85
+ transfer_whitespace_to_closing_tag
86
+ @bbtree.retrogress_bbtree
87
+ end
85
88
  @bbtree.retrogress_bbtree
86
89
  end
87
90
  end
88
- end # end of scan loop
91
+ end
89
92
 
90
93
  validate_all_tags_closed_off
91
94
  validate_stack_level_too_deep_potential
@@ -93,6 +96,25 @@ module RubyBBCode
93
96
 
94
97
  private
95
98
 
99
+ def transfer_whitespace_to_closing_tag
100
+ last_text_node = node_last_text(parent_tag)
101
+ unless last_text_node.nil?
102
+ last_text_node[:text].scan(/(\s+)$/) do |result|
103
+ parent_tag[:closing_whitespace] = result[0]
104
+ last_text_node[:text] = last_text_node[:text][0..-result[0].length - 1]
105
+ end
106
+ end
107
+ end
108
+
109
+ # Return the node that holds the last piece of text for the given node (self or child)
110
+ def node_last_text(node)
111
+ return node if node.type == :text
112
+ return node_last_text(node.children[-1]) unless node.children.empty?
113
+
114
+ # node does not hold text
115
+ nil
116
+ end
117
+
96
118
  def set_multi_tag_to_actual_tag
97
119
  # Try to find the actual tag
98
120
  tag = get_actual_tag
@@ -121,7 +143,7 @@ module RubyBBCode
121
143
  end
122
144
 
123
145
  def create_text_element
124
- element = {:is_tag => false, :text => @ti.text, :errors => @ti[:errors] }
146
+ element = { is_tag: false, text: @ti.text, errors: @ti[:errors] }
125
147
  @bbtree.build_up_new_tag(element)
126
148
  end
127
149
 
@@ -132,26 +154,26 @@ module RubyBBCode
132
154
  # perform special formatting for certain tags
133
155
  params[:url] = match_url_id(params[:url], @ti.definition[:url_matches])
134
156
  end
135
- return params
157
+ params
136
158
  end
137
159
 
138
160
  # Get 'between tag' for tag
139
161
  def get_formatted_between
140
162
  between = @ti[:text]
141
- # perform special formatting for cenrtain tags
163
+ # perform special formatting for certain tags
142
164
  between = match_url_id(between, @bbtree.current_node.definition[:url_matches]) if @bbtree.current_node.definition[:url_matches]
143
- return between
165
+ between
144
166
  end
145
167
 
146
168
  def match_url_id(url, regex_matches)
147
169
  regex_matches.each do |regex|
148
170
  if url =~ regex
149
- id = $1
171
+ id = Regexp.last_match(1)
150
172
  return id
151
173
  end
152
174
  end
153
175
 
154
- return url # if we couldn't find a match, then just return the url, hopefully it's a valid ID...
176
+ url # if we couldn't find a match, then just return the url, hopefully it's a valid ID...
155
177
  end
156
178
 
157
179
  # Validates the element
@@ -162,7 +184,7 @@ module RubyBBCode
162
184
  end
163
185
 
164
186
  def valid_text_or_opening_element?
165
- if @ti.element_is_text? or @ti.element_is_opening_tag?
187
+ if @ti.element_is_text? || @ti.element_is_opening_tag?
166
188
  return false unless valid_opening_tag?
167
189
  return false unless valid_constraints_on_child?
168
190
  end
@@ -171,7 +193,7 @@ module RubyBBCode
171
193
 
172
194
  def valid_opening_tag?
173
195
  if @ti.element_is_opening_tag?
174
- if @ti.only_allowed_in_parent_tags? and (!within_open_tag? or !@ti.allowed_in? parent_tag[:tag]) and !self_closing_tag_reached_a_closer?
196
+ if @ti.only_allowed_in_parent_tags? && (!within_open_tag? || !@ti.allowed_in?(parent_tag[:tag])) && !self_closing_tag_reached_a_closer?
175
197
  # Tag doesn't belong in the last opened tag
176
198
  throw_child_requires_specific_parent_error
177
199
  return false
@@ -183,19 +205,15 @@ module RubyBBCode
183
205
  end
184
206
 
185
207
  # Note that if allow_between_as_param is true, other checks already test the (correctness of the) 'between parameter'
186
- unless @ti.definition[:param_tokens].nil? or @ti.definition[:allow_between_as_param] == true
208
+ unless @ti.definition[:param_tokens].nil? || (@ti.definition[:allow_between_as_param] == true)
187
209
  # Check if all required parameters are added
188
210
  @ti.definition[:param_tokens].each do |token|
189
- if @ti[:params][token[:token]].nil? and token[:optional].nil?
190
- add_tag_error "Tag [#{@ti[:tag]}] must have '#{token[:token]}' parameter"
191
- end
211
+ add_tag_error "Tag [#{@ti[:tag]}] must have '#{token[:token]}' parameter" if @ti[:params][token[:token]].nil? && token[:optional].nil?
192
212
  end
193
213
 
194
214
  # Check if no 'custom parameters' are added
195
215
  @ti[:params].keys.each do |token|
196
- if @ti.definition[:param_tokens].find {|param_token| param_token[:token]==token}.nil?
197
- add_tag_error "Tag [#{@ti[:tag]}] doesn't have a '#{token}' parameter"
198
- end
216
+ add_tag_error "Tag [#{@ti[:tag]}] doesn't have a '#{token}' parameter" if @ti.definition[:param_tokens].find { |param_token| param_token[:token] == token }.nil?
199
217
  end
200
218
  end
201
219
  end
@@ -203,15 +221,15 @@ module RubyBBCode
203
221
  end
204
222
 
205
223
  def self_closing_tag_reached_a_closer?
206
- @ti.definition[:self_closable] and @bbtree.current_node[:tag] == @ti[:tag]
224
+ @ti.definition[:self_closable] && (@bbtree.current_node[:tag] == @ti[:tag])
207
225
  end
208
226
 
209
227
  def valid_constraints_on_child?
210
- if within_open_tag? and parent_has_constraints_on_children?
228
+ if within_open_tag? && parent_has_constraints_on_children?
211
229
  # Check if the found tag is allowed
212
230
  last_tag_def = parent_tag[:definition]
213
231
  allowed_tags = last_tag_def[:only_allow]
214
- if (!@ti[:is_tag] and last_tag_def[:require_between] != true and @ti[:text].lstrip != "") or (@ti[:is_tag] and (allowed_tags.include?(@ti[:tag]) == false)) # TODO: refactor this, it's just too long
232
+ if (!@ti.element_is_tag? && (last_tag_def[:require_between] != true) && (@ti[:text].lstrip != '')) || (@ti.element_is_tag? && (allowed_tags.include?(@ti[:tag]) == false)) # TODO: refactor this, it's just too long
215
233
  # Last opened tag does not allow tag
216
234
  throw_parent_prohibits_this_child_error
217
235
  return false
@@ -221,7 +239,6 @@ module RubyBBCode
221
239
  end
222
240
 
223
241
  def valid_closing_element?
224
-
225
242
  if @ti.element_is_closing_tag?
226
243
 
227
244
  if parent_tag.nil?
@@ -230,9 +247,9 @@ module RubyBBCode
230
247
  return false
231
248
  end
232
249
 
233
- if parent_tag[:tag] != @ti[:tag] and !parent_of_self_closing_tag?
250
+ if (parent_tag[:tag] != @ti[:tag]) && !parent_of_self_closing_tag?
234
251
  # Make an exception for 'supported tags'
235
- if @ti.definition[:supported_tags].nil? or ! @ti.definition[:supported_tags].include? parent_tag[:tag]
252
+ if @ti.definition[:supported_tags].nil? || !@ti.definition[:supported_tags].include?(parent_tag[:tag])
236
253
  add_tag_error "Closing tag [/#{@ti[:tag]}] doesn't match [#{parent_tag[:tag]}]"
237
254
  @ti[:wrong_closing] = true
238
255
  return false
@@ -240,7 +257,7 @@ module RubyBBCode
240
257
  end
241
258
 
242
259
  tag_def = @bbtree.current_node.definition
243
- if tag_def[:require_between] and @bbtree.current_node[:between].nil?
260
+ if tag_def[:require_between] && @bbtree.current_node[:between].nil? && @bbtree.current_node[:nodes].empty?
244
261
  err = "No text between [#{@ti[:tag]}] and [/#{@ti[:tag]}] tags."
245
262
  err = "Cannot determine multi-tag type: #{err}" if tag_def[:multi_tag]
246
263
  add_tag_error err, @bbtree.current_node
@@ -253,14 +270,14 @@ module RubyBBCode
253
270
  def parent_of_self_closing_tag?
254
271
  was_last_tag_self_closable = @bbtree.current_node[:definition][:self_closable] unless @bbtree.current_node[:definition].nil?
255
272
 
256
- was_last_tag_self_closable and last_tag_fit_in_this_tag?
273
+ was_last_tag_self_closable && last_tag_fit_in_this_tag?
257
274
  end
258
275
 
259
276
  def last_tag_fit_in_this_tag?
260
- @ti.definition[:only_allow].each do |tag|
277
+ @ti.definition[:only_allow]&.each do |tag|
261
278
  return true if tag == @bbtree.current_node[:tag]
262
- end unless @ti.definition[:only_allow].nil?
263
- return false
279
+ end
280
+ false
264
281
  end
265
282
 
266
283
  # This validation is for text elements with between text
@@ -270,8 +287,13 @@ module RubyBBCode
270
287
  def valid_param_supplied_as_text?
271
288
  tag_def = @bbtree.current_node.definition
272
289
 
290
+ if within_open_tag? && use_text_as_parameter? && @ti.element_is_tag? && has_no_text_node?
291
+ add_tag_error 'between parameter must be plain text'
292
+ return false
293
+ end
294
+
273
295
  # this conditional ensures whether the validation is apropriate to this tag type
274
- if @ti.element_is_text? and within_open_tag? and tag_def[:require_between] and use_text_as_parameter? and not tag_def[:quick_param_format].nil?
296
+ if @ti.element_is_text? && within_open_tag? && tag_def[:require_between] && use_text_as_parameter? && !tag_def[:quick_param_format].nil?
275
297
 
276
298
  # check if valid
277
299
  if @ti[:text].match(tag_def[:quick_param_format]).nil?
@@ -283,19 +305,17 @@ module RubyBBCode
283
305
  end
284
306
 
285
307
  def validate_all_tags_closed_off
308
+ return unless expecting_a_closing_tag?
309
+
286
310
  # if we're still expecting a closing tag and we've come to the end of the string... throw error(s)
287
- if expecting_a_closing_tag?
288
- @bbtree.tags_list.each do |tag|
289
- add_tag_error "[#{tag[:tag]}] not closed", tag
290
- tag[:closed] = false
291
- end
311
+ @bbtree.tags_list.each do |tag|
312
+ add_tag_error "[#{tag[:tag]}] not closed", tag
313
+ tag[:closed] = false
292
314
  end
293
315
  end
294
316
 
295
317
  def validate_stack_level_too_deep_potential
296
- if @bbtree.nodes.count > 2200
297
- throw_stack_level_will_be_too_deep_error
298
- end
318
+ throw_stack_level_will_be_too_deep_error if @bbtree.nodes.count > 2200
299
319
  end
300
320
 
301
321
  def throw_child_requires_specific_parent_error
@@ -311,20 +331,24 @@ module RubyBBCode
311
331
  def throw_parent_prohibits_this_child_error
312
332
  allowed_tags = parent_tag[:definition][:only_allow]
313
333
  err = "[#{parent_tag[:tag]}] can only contain [#{allowed_tags.to_sentence(to_sentence_bbcode_tags)}] tags, so "
314
- err += "[#{@ti[:tag]}]" if @ti[:is_tag]
315
- err += "\"#{@ti[:text]}\"" unless @ti[:is_tag]
334
+ err += "[#{@ti[:tag]}]" if @ti.element_is_tag?
335
+ err += "\"#{@ti[:text]}\"" unless @ti.element_is_tag?
316
336
  err += ' is not allowed'
317
337
  add_tag_error err
318
338
  end
319
339
 
320
340
  def throw_stack_level_will_be_too_deep_error
321
- @errors << "Stack level would go too deep. You must be trying to process a text containing thousands of BBTree nodes at once. (limit around 2300 tags containing 2,300 strings). Check RubyBBCode::TagCollection#to_html to see why this validation is needed."
341
+ @errors << 'Stack level would go too deep. You must be trying to process a text containing thousands of BBTree nodes at once. (limit around 2300 tags containing 2,300 strings). Check RubyBBCode::TagCollection#to_html to see why this validation is needed.'
322
342
  end
323
343
 
324
344
  def to_sentence_bbcode_tags
325
- {:words_connector => "], [",
326
- :two_words_connector => "] and [",
327
- :last_word_connector => "] and ["}
345
+ { words_connector: '], [',
346
+ two_words_connector: '] and [',
347
+ last_word_connector: '] and [' }
348
+ end
349
+
350
+ def has_no_text_node?
351
+ @bbtree.current_node[:nodes].blank? || @bbtree.current_node[:nodes][0][:text].nil?
328
352
  end
329
353
 
330
354
  def expecting_a_closing_tag?
@@ -337,7 +361,7 @@ module RubyBBCode
337
361
 
338
362
  def use_text_as_parameter?
339
363
  tag = @bbtree.current_node
340
- tag.definition[:allow_between_as_param] and tag.params_not_set? and !tag.invalid_quick_param?
364
+ tag.definition[:allow_between_as_param] && tag.params_not_set? && !tag.invalid_quick_param?
341
365
  end
342
366
 
343
367
  def parent_tag
@@ -14,16 +14,15 @@ module RubyBBCode::Templates
14
14
  def initialize(node)
15
15
  @node = node
16
16
  @tag_definition = node.definition # tag_definition
17
- @opening_part = "[#{node[:tag]}#{node.allow_params? ? '%param%' : ''}]"
18
- unless @node[:errors].empty?
19
- @opening_part = "<span class='bbcode_error' #{BBCodeErrorsTemplate.error_attribute(@node[:errors])}>#{@opening_part}</span>"
20
- end
21
- @closing_part = "[/#{node[:tag]}]"
17
+ @opening_part = "[#{node[:tag]}#{node.allow_params? ? '%param%' : ''}]" + add_whitespace(node[:opening_whitespace])
18
+ @opening_part = "<span class='bbcode_error' #{BBCodeErrorsTemplate.error_attribute(@node[:errors])}>#{@opening_part}</span>" unless @node[:errors].empty?
19
+ @closing_part = "[/#{node[:tag]}]" + add_whitespace(node[:closing_whitespace])
22
20
  end
23
21
 
24
- def self.convert_text(node)
22
+ def self.convert_text(node, _parent_node)
25
23
  # Keep the text as it was
26
24
  return "<span class='bbcode_error' #{error_attribute(node[:errors])}>#{node[:text]}</span>" unless node[:errors].empty?
25
+
27
26
  node[:text]
28
27
  end
29
28
 
@@ -41,8 +40,7 @@ module RubyBBCode::Templates
41
40
  end
42
41
  end
43
42
 
44
- def inlay_closing_part!
45
- end
43
+ def inlay_closing_part!; end
46
44
 
47
45
  def remove_unused_tokens!
48
46
  @opening_part.gsub!('%param%', '')
@@ -54,15 +52,20 @@ module RubyBBCode::Templates
54
52
 
55
53
  private
56
54
 
57
- def get_between
58
- return @node[:between] if @tag_definition[:require_between] and @node[:between]
59
- ''
60
- end
55
+ def add_whitespace(whitespace)
56
+ whitespace || ''
57
+ end
61
58
 
62
- def self.error_attribute(errors)
63
- # Escape (double) quotes so the JSON can be generated properly (and parsed properly by JavaScript)
64
- escapedErrors = errors.map { |error| error.gsub("\"","&quot;").gsub("'", "&#39;")}
65
- "data-bbcode-errors='#{JSON.fast_generate(escapedErrors)}'"
66
- end
59
+ def get_between
60
+ return @node[:between] if @tag_definition[:require_between] && @node[:between]
61
+
62
+ ''
63
+ end
64
+
65
+ def self.error_attribute(errors)
66
+ # Escape (double) quotes so the JSON can be generated properly (and parsed properly by JavaScript)
67
+ escapedErrors = errors.map { |error| error.gsub('"', '&quot;').gsub("'", '&#39;') }
68
+ "data-bbcode-errors='#{JSON.fast_generate(escapedErrors)}'"
69
+ end
67
70
  end
68
71
  end
@@ -10,20 +10,30 @@ module RubyBBCode::Templates
10
10
 
11
11
  def initialize(node)
12
12
  @node = node
13
- @tag_definition = node.definition # tag_definition
14
- @opening_part = node.definition[:html_open].dup
15
- @closing_part = node.definition[:html_close].dup
13
+ @tag_definition = node.definition
14
+ @opening_part = node.definition[:html_open] + add_whitespace(:opening_whitespace)
15
+ @closing_part = node.definition[:html_close] + add_whitespace(:closing_whitespace)
16
16
  end
17
17
 
18
18
  # Newlines are converted to html <br /> syntax before being returned.
19
- def self.convert_text(node)
19
+ def self.convert_text(node, parent_node)
20
20
  return '' if node[:text].nil?
21
- # convert_newlines_to_br
22
- node[:text].gsub("\r\n", "\n").gsub("\n", "<br />\n")
21
+
22
+ text = node[:text]
23
+ whitespace = ''
24
+
25
+ if !parent_node.nil? && parent_node.definition[:block_tag]
26
+ # Strip EOL whitespace, so it does not get converted
27
+ text.scan(/(\s+)$/) do |result|
28
+ whitespace = result[0]
29
+ text = text[0..-result[0].length - 1]
30
+ end
31
+ end
32
+ convert_newlines(text) + whitespace
23
33
  end
24
34
 
25
35
  def inlay_between_text!
26
- @opening_part.gsub!('%between%', @node[:between]) if between_text_as_param?
36
+ @opening_part.gsub!('%between%', format_between) if between_text_as_param?
27
37
  end
28
38
 
29
39
  def inlay_params!
@@ -31,12 +41,12 @@ module RubyBBCode::Templates
31
41
  @tag_definition[:param_tokens].each do |token|
32
42
  param_value = @node[:params][token[:token]] || token[:default]
33
43
  param_value = CGI.escape(param_value) if token[:uri_escape]
34
- @opening_part.gsub!("%#{token[:token].to_s}%", "#{token[:prefix]}#{param_value}#{token[:postfix]}") unless param_value.nil?
44
+ @opening_part.gsub!("%#{token[:token]}%", "#{token[:prefix]}#{param_value}#{token[:postfix]}") unless param_value.nil?
35
45
  end
36
46
  end
37
47
 
38
48
  def inlay_closing_part!
39
- @closing_part.gsub!('%between%', @node[:between]) if between_text_as_param?
49
+ @closing_part.gsub!('%between%', format_between) if between_text_as_param?
40
50
  end
41
51
 
42
52
  def remove_unused_tokens!
@@ -45,11 +55,27 @@ module RubyBBCode::Templates
45
55
  end
46
56
  end
47
57
 
58
+ def self.convert_newlines(text)
59
+ text.gsub("\r\n", "\n").gsub("\n", "<br />\n")
60
+ end
61
+
48
62
  private
49
63
 
64
+ def add_whitespace(key)
65
+ whitespace = @node[key]
66
+ return '' if whitespace.nil?
67
+
68
+ whitespace = HtmlTemplate.convert_newlines(whitespace) unless @tag_definition[:block_tag]
69
+ whitespace
70
+ end
71
+
50
72
  # Return true if the between text is needed as param
51
73
  def between_text_as_param?
52
74
  @tag_definition[:require_between]
53
75
  end
76
+
77
+ def format_between
78
+ @node[:between] || ''
79
+ end
54
80
  end
55
- end
81
+ end