ruby-bbcode 0.0.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ module RubyBBCode
2
+ # TagInfo is basically what the regex scan get's converted into
3
+ # during the tag_sifter#process_text method.
4
+ # This class was made mostly just to keep track of all of the confusing
5
+ # the logic conditions that are checked.
6
+ #
7
+ class TagInfo
8
+ def initialize(tag_info, dictionary)
9
+ @tag_data = find_tag_info(tag_info)
10
+ @dictionary = dictionary
11
+ @definition = @dictionary[@tag_data[:tag].to_sym] unless @tag_data[:tag].nil?
12
+ end
13
+
14
+ def [](key)
15
+ @tag_data[key]
16
+ end
17
+
18
+ def []=(key, value)
19
+ @tag_data[key] = value
20
+ end
21
+
22
+ def tag_data
23
+ @tag_data
24
+ end
25
+
26
+ def definition
27
+ @definition
28
+ end
29
+
30
+ def definition=(val)
31
+ @definition = val
32
+ end
33
+
34
+ def dictionary # need this for reasigning multi_tag elements
35
+ @dictionary
36
+ end
37
+
38
+ # This represents the text value of the element (if it's not a tag element)
39
+ # Newlines are converted to html <br /> syntax before being returned.
40
+ def text
41
+ text = @tag_data[:text]
42
+ # convert_newlines_to_br
43
+ text.gsub!("\r\n", "\n")
44
+ text.gsub!("\n", "<br />\n")
45
+ text
46
+ end
47
+
48
+ # allows for a very snazy case/ when conditional
49
+ def type
50
+ return :opening_tag if element_is_opening_tag?
51
+ return :text if element_is_text?
52
+ return :closing_tag if element_is_closing_tag?
53
+ end
54
+
55
+ def handle_unregistered_tags_as_text
56
+ if element_is_tag? and tag_missing_from_tag_dictionary?
57
+ # Handle as text from now on!
58
+ self[:is_tag] = false
59
+ self[:closing_tag] = false
60
+ self[:text] = self[:complete_match]
61
+ end
62
+ end
63
+
64
+ def element_is_tag?
65
+ self[:is_tag]
66
+ end
67
+
68
+ def element_is_opening_tag?
69
+ self[:is_tag] and !self[:closing_tag]
70
+ end
71
+
72
+ def element_is_closing_tag?
73
+ self[:closing_tag]
74
+ end
75
+
76
+ def element_is_text?
77
+ !self[:text].nil?
78
+ end
79
+
80
+ def has_params?
81
+ self[:params][:tag_param] != nil
82
+ end
83
+
84
+ def tag_missing_from_tag_dictionary?
85
+ !@dictionary.include?(self[:tag].to_sym)
86
+ end
87
+
88
+ def allowed_outside_parent_tags?
89
+ @definition[:only_in].nil?
90
+ end
91
+
92
+ def constrained_to_within_parent_tags?
93
+ !@definition[:only_in].nil?
94
+ end
95
+
96
+ def allowed_in(tag_symbol)
97
+ @definition[:only_in].include?(tag_symbol)
98
+ end
99
+
100
+ def can_have_params?
101
+ @definition[:allow_tag_param]
102
+ end
103
+
104
+ # Checks if the tag param matches the regex pattern defined in tags.rb
105
+ def invalid_param?
106
+ self[:params][:tag_param].match(@definition[:tag_param]).nil?
107
+ end
108
+
109
+ protected
110
+
111
+ def find_tag_info(tag_info)
112
+ ti = {}
113
+ ti[:complete_match] = tag_info[0]
114
+ ti[:is_tag] = (tag_info[0].start_with? '[')
115
+ if ti[:is_tag]
116
+ ti[:closing_tag] = (tag_info[2] == '/')
117
+ ti[:tag] = tag_info[3]
118
+ ti[:params] = {}
119
+ if tag_info[5][0] == ?=
120
+ ti[:params][:tag_param] = tag_info[5][1..-1]
121
+ elsif tag_info[5][0] == ?\s
122
+ #FIXME: Find params... Delete this or write a test to cover this and implement it
123
+ end
124
+ else
125
+ # Plain text
126
+ ti[:text] = tag_info[9]
127
+ end
128
+ ti
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,71 @@
1
+ module RubyBBCode
2
+ # A TagNode specifies either an opening tag element or a (plain) text elements
3
+ #
4
+ # TagInfo elements are essentially converted into these nodes which are
5
+ # later converted into html output in the bbtree_to_html method
6
+ class TagNode
7
+ # Tag or text element that is stored in this node
8
+ attr_accessor :element
9
+
10
+ # ==== Attributes
11
+ #
12
+ # * +element+ - contains the information of TagInfo#tag_data.
13
+ # A text element has the form of
14
+ # { :is_tag=>false, :text=>"ITALIC" }
15
+ # and a tag element has the form of
16
+ # { :is_tag=>true, :tag=>:i, :nodes => [] }
17
+ # * +nodes+
18
+ def initialize(element, nodes = [])
19
+ @element = element
20
+ end
21
+
22
+ def [](key)
23
+ @element[key]
24
+ end
25
+
26
+ def []=(key, value)
27
+ @element[key] = value
28
+ end
29
+
30
+ # Debugging/ visualization purposes
31
+ def type
32
+ return :tag if @element[:is_tag]
33
+ return :text if !@element[:is_tag]
34
+ end
35
+
36
+ # Checks to see if the parameter for the TagNode has been set.
37
+ def param_not_set?
38
+ (@element[:params].nil? or @element[:params][:tag_param].nil?)
39
+ end
40
+
41
+ # check if the parameter for the TagNode is set
42
+ def param_set?
43
+ !param_not_set?
44
+ end
45
+
46
+ def has_children?
47
+ return false if type == :text or children.length == 0 # text nodes return false too
48
+ return true if children.length > 0
49
+ end
50
+
51
+ def allow_tag_param?
52
+ definition[:allow_tag_param]
53
+ end
54
+
55
+ # shows the tag definition for this TagNode as defined in tags.rb
56
+ def definition
57
+ @element[:definition]
58
+ end
59
+
60
+ def children
61
+ @element[:nodes]
62
+ end
63
+
64
+ # Easy way to set the tag_param value of the hash, which represents
65
+ # the parameter supplied
66
+ def tag_param=(param)
67
+ @element[:params] = {:tag_param => param}
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,329 @@
1
+ require 'active_support/core_ext/array/conversions'
2
+
3
+ module RubyBBCode
4
+ # TagSifter is in charge of building up the BBTree with nodes as it parses through the
5
+ # supplied text such as
6
+ # "[b]I'm bold and the next word is [i]ITALIC[/i][b]"
7
+ class TagSifter
8
+ attr_reader :bbtree, :errors
9
+
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
12
+
13
+ @dictionary = dictionary # the dictionary for all the defined tags in tags.rb
14
+ @bbtree = BBTree.new({:nodes => TagCollection.new}, dictionary)
15
+ @ti = nil
16
+ @errors = false
17
+ end
18
+
19
+ def invalid?
20
+ @errors != false
21
+ end
22
+
23
+ # BBTree#process_text is responsible for parsing the actual BBCode text and converting it
24
+ # into a 'syntax tree' of nodes, each node represeting either a tag type or content for a tag
25
+ # once this tree is built, the to_html method can be invoked where the tree is finally
26
+ # converted into HTML syntax.
27
+ def process_text
28
+ regex_string = '((\[ (\/)? ( \* | (\w+)) ((=[^\[\]]+) | (\s\w+=\w+)* | ([^\]]*))? \]) | ([^\[]+))'
29
+ @text.scan(/#{regex_string}/ix) do |tag_info|
30
+ @ti = TagInfo.new(tag_info, @dictionary)
31
+
32
+ @ti.handle_unregistered_tags_as_text # if the tag isn't in the @dictionary list, then treat it as text
33
+ handle_closing_tags_that_are_multi_as_text_if_it_doesnt_match_the_latest_opener_tag_on_the_stack
34
+
35
+ return if !valid_element?
36
+
37
+ case @ti.type # Validation of tag succeeded, add to @bbtree.tags_list and/or bbtree
38
+ when :opening_tag
39
+ element = {:is_tag => true, :tag => @ti[:tag].to_sym, :definition => @ti.definition, :nodes => TagCollection.new }
40
+ element[:params] = {:tag_param => get_formatted_element_params} if @ti.can_have_params? and @ti.has_params?
41
+
42
+ @bbtree.retrogress_bbtree if self_closing_tag_reached_a_closer?
43
+
44
+ @bbtree.build_up_new_tag(element)
45
+
46
+ @bbtree.escalate_bbtree(element)
47
+ when :text
48
+ set_parent_tag_from_multi_tag_to_concrete! if @bbtree.current_node.definition && @bbtree.current_node.definition[:multi_tag] == true
49
+ element = {:is_tag => false, :text => @ti.text }
50
+ if within_open_tag?
51
+ tag = @bbtree.current_node.definition
52
+ if tag[:require_between]
53
+ @bbtree.current_node[:between] = get_formatted_element_params
54
+ if candidate_for_using_between_as_param?
55
+ use_between_as_tag_param # Did not specify tag_param, so use between text.
56
+ end
57
+ next # don't add this node to @bbtree.current_node.children if we're within an open tag that requires_between (to be a param), and the between couldn't be used as a param... Yet it passed validation so the param must have been specified within the opening tag???
58
+ end
59
+ end
60
+ @bbtree.build_up_new_tag(element)
61
+ when :closing_tag
62
+ @bbtree.retrogress_bbtree if parent_of_self_closing_tag? and @bbtree.within_open_tag?
63
+ @bbtree.retrogress_bbtree
64
+ end
65
+
66
+ end # end of scan loop
67
+
68
+ validate_all_tags_closed_off # TODO: consider automatically closing off all the tags... I think that's how the HTML 5 parser works too
69
+ validate_stack_level_too_deep_potential
70
+ end
71
+
72
+ def set_parent_tag_from_multi_tag_to_concrete!
73
+ # if the proper tag can't be matched, we need to treat the parent tag as text instead! Or throw an error message....
74
+
75
+ proper_tag = get_proper_tag
76
+ if proper_tag == :tag_not_found
77
+ @bbtree.redefine_parent_tag_as_text
78
+
79
+ @bbtree.nodes << TagNode.new(@ti.tag_data) # escalate the bbtree with this element as though it's regular text data...
80
+ return
81
+ end
82
+ @bbtree.current_node[:definition] = @dictionary[proper_tag]
83
+ @bbtree.current_node[:tag] = proper_tag
84
+ end
85
+
86
+ def get_proper_tag
87
+ supported_tags = @bbtree.current_node[:definition][:supported_tags]
88
+
89
+ supported_tags.each do |tag|
90
+ regex_list = @dictionary[tag][:url_matches]
91
+
92
+ regex_list.each do |regex|
93
+ return tag if regex =~ @ti.tag_data[:text]
94
+ end
95
+ end
96
+ :tag_not_found
97
+ end
98
+
99
+ def handle_closing_tags_that_are_multi_as_text_if_it_doesnt_match_the_latest_opener_tag_on_the_stack
100
+ if @ti.element_is_closing_tag?
101
+ return if @bbtree.current_node[:definition].nil?
102
+ if parent_tag != @ti[:tag].to_sym and @bbtree.current_node[:definition][:multi_tag] # if opening tag doesn't match this closing tag... and if the opener was a multi_tag...
103
+ @ti[:is_tag] = false
104
+ @ti[:closing_tag] = false
105
+ @ti[:text] = @ti.tag_data[:complete_match]
106
+ end
107
+ end
108
+
109
+ end
110
+
111
+
112
+ private
113
+
114
+ # This method allows us to format params if needed...
115
+ # TODO: Maybe this kind of thing *could* be handled in the bbtree_to_html where the %between% is
116
+ # sorted out and the html is generated, but... That code has yet to be refactored and we can.
117
+ # refactor this code easily to happen over there if necessary... Yes, I think it's more logical
118
+ # to be put over there, but that method needs to be cleaned up before we introduce the formatting overthere... and knowing the parent node is helpful!
119
+ def get_formatted_element_params
120
+ if @ti[:is_tag]
121
+ param = @ti[:params][:tag_param]
122
+ if @ti.can_have_params? and @ti.has_params?
123
+ # perform special formatting for cenrtain tags
124
+ param = conduct_special_formatting(param) if @ti[:tag].to_sym == :youtube # note: this line isn't ever used because @@tags don't allow it... I think if we have tags without the same kind of :require_between restriction, we'll need to pay close attention to this case
125
+ end
126
+ return param
127
+ else # must be text... @ti[:is_tag] == false
128
+ param = @ti[:text]
129
+ # perform special formatting for cenrtain tags
130
+ param = conduct_special_formatting(param) if @bbtree.current_node.definition[:url_matches]
131
+ return param
132
+ end
133
+ end
134
+
135
+ def conduct_special_formatting(url, regex_matches = nil)
136
+ regex_matches = @bbtree.current_node.definition[:url_matches] if regex_matches.nil? # for testing purposes we can force in regex_matches
137
+
138
+ regex_matches.each do |regex|
139
+ if url =~ regex
140
+ id = $1
141
+ return id
142
+ end
143
+ end
144
+
145
+ return url # if we couldn't find a match, then just return the url, hopefully it's a valid youtube ID...
146
+ end
147
+
148
+
149
+ # Validates the element
150
+ def valid_element?
151
+ return false if !valid_text_or_opening_element?
152
+ return false if !valid_closing_element?
153
+ return false if !valid_param_supplied_as_text?
154
+ true
155
+ end
156
+
157
+ def valid_text_or_opening_element?
158
+ if @ti.element_is_text? or @ti.element_is_opening_tag?
159
+ return false if validate_opening_tag == false
160
+ return false if validate_constraints_on_child == false
161
+ end
162
+ true
163
+ end
164
+
165
+ def validate_opening_tag
166
+ if @ti.element_is_opening_tag?
167
+ unless @ti.allowed_outside_parent_tags? or (within_open_tag? and @ti.allowed_in(parent_tag.to_sym)) or self_closing_tag_reached_a_closer?
168
+ # Tag doesn't belong in the last opened tag
169
+ throw_child_requires_specific_parent_error; return false
170
+ end
171
+
172
+ # Originally: tag[:allow_tag_param] and ti[:params][:tag_param] != nil
173
+ if @ti.can_have_params? and @ti.has_params?
174
+ # Test if matches
175
+ if @ti.invalid_param?
176
+ throw_invalid_param_error; return false
177
+ end
178
+ end
179
+ end
180
+ true
181
+ end
182
+
183
+ def self_closing_tag_reached_a_closer?
184
+ @ti.definition[:self_closable] and @bbtree.current_node[:tag] == @ti.tag_data[:tag].to_sym
185
+ end
186
+
187
+ def validate_constraints_on_child
188
+ # TODO: Rename this if statement to #validate_constraints_on_child
189
+ if within_open_tag? and parent_has_constraints_on_children?
190
+ # Check if the found tag is allowed
191
+ last_tag = @dictionary[parent_tag]
192
+ allowed_tags = last_tag[:only_allow]
193
+ if (!@ti[:is_tag] and last_tag[:require_between] != true and @ti[:text].lstrip != "") or (@ti[:is_tag] and (allowed_tags.include?(@ti[:tag].to_sym) == false)) # TODO: refactor this, it's just too long
194
+ # Last opened tag does not allow tag
195
+ throw_parent_prohibits_this_child_error; return false
196
+ end
197
+ end
198
+ true
199
+ end
200
+
201
+ def valid_closing_element?
202
+ tag = @ti.definition
203
+
204
+ if @ti.element_is_closing_tag?
205
+ if parent_tag != @ti[:tag].to_sym and !parent_of_self_closing_tag?
206
+ @errors = ["Closing tag [/#{@ti[:tag]}] doesn't match [#{parent_tag}]"]
207
+ return false
208
+ end
209
+
210
+ if tag[:require_between] == true and @bbtree.current_node[:between].nil?
211
+ @errors = ["No text between [#{@ti[:tag]}] and [/#{@ti[:tag]}] tags."]
212
+ return false
213
+ end
214
+ end
215
+ true
216
+ end
217
+
218
+ def parent_of_self_closing_tag?
219
+ tag_being_parsed = @ti.definition
220
+ was_last_tag_self_closable = @bbtree.current_node[:definition][:self_closable] unless @bbtree.current_node[:definition].nil?
221
+
222
+ was_last_tag_self_closable and last_tag_fit_in_this_tag?
223
+ end
224
+
225
+ def last_tag_fit_in_this_tag?
226
+ @ti.definition[:only_allow].each do |tag|
227
+ return true if tag == @bbtree.current_node[:tag]
228
+ end unless @ti.definition[:only_allow].nil?
229
+ return false
230
+ end
231
+
232
+ # This validation is for text elements with between text
233
+ # that might be construed as a param.
234
+ # The validation code checks if the params match constraints
235
+ # imposed by the node/tag/parent.
236
+ def valid_param_supplied_as_text?
237
+ tag = @bbtree.current_node.definition
238
+
239
+ # this conditional ensures whether the validation is apropriate to this tag type
240
+ if @ti.element_is_text? and within_open_tag? and tag[:require_between] and candidate_for_using_between_as_param?
241
+
242
+ # check if valid
243
+ if @ti[:text].match(tag[:tag_param]).nil?
244
+ @errors = [tag[:tag_param_description].gsub('%param%', @ti[:text])]
245
+ return false
246
+ end
247
+ end
248
+ true
249
+ end
250
+
251
+ def validate_all_tags_closed_off
252
+ # if we're still expecting a closing tag and we've come to the end of the string... throw error
253
+ throw_unexpected_end_of_string_error if expecting_a_closing_tag?
254
+ end
255
+
256
+ def validate_stack_level_too_deep_potential
257
+ if @bbtree.nodes.count > 2200
258
+ throw_stack_level_will_be_too_deep_error
259
+ end
260
+ end
261
+
262
+ def throw_child_requires_specific_parent_error
263
+ err = "[#{@ti[:tag]}] can only be used in [#{@ti.definition[:only_in].to_sentence(to_sentence_bbcode_tags)}]"
264
+ err += ", so using it in a [#{parent_tag}] tag is not allowed" if expecting_a_closing_tag?
265
+ @errors = [err]
266
+ end
267
+
268
+ def throw_invalid_param_error
269
+ @errors = [@ti.definition[:tag_param_description].gsub('%param%', @ti[:params][:tag_param])]
270
+ end
271
+
272
+ def throw_parent_prohibits_this_child_error
273
+ allowed_tags = @dictionary[parent_tag][:only_allow]
274
+ err = "[#{parent_tag}] can only contain [#{allowed_tags.to_sentence(to_sentence_bbcode_tags)}] tags, so "
275
+ err += "[#{@ti[:tag]}]" if @ti[:is_tag]
276
+ err += "\"#{@ti[:text]}\"" unless @ti[:is_tag]
277
+ err += ' is not allowed'
278
+ @errors = [err]
279
+ end
280
+
281
+ def throw_unexpected_end_of_string_error
282
+ @errors = ["[#{@bbtree.tags_list.to_sentence(to_sentence_bbcode_tags)}] not closed"]
283
+ end
284
+
285
+ def throw_stack_level_will_be_too_deep_error
286
+ @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."]
287
+ end
288
+
289
+
290
+ def to_sentence_bbcode_tags
291
+ {:words_connector => "], [",
292
+ :two_words_connector => "] and [",
293
+ :last_word_connector => "] and ["}
294
+ end
295
+
296
+
297
+ def expecting_a_closing_tag?
298
+ @bbtree.expecting_a_closing_tag?
299
+ end
300
+
301
+ def within_open_tag?
302
+ @bbtree.within_open_tag?
303
+ end
304
+
305
+ def use_between_as_tag_param
306
+ param = get_formatted_element_params
307
+ @bbtree.current_node.tag_param = param # @bbtree.current_node[:params] = {:tag_param => @ti[:text]}
308
+ end
309
+
310
+ def candidate_for_using_between_as_param?
311
+ # TODO: the bool values...
312
+ # are unclear and should be worked on. Additional tag might be tag[:requires_param] such that
313
+ # [img] would have that as true... and [url] would have that as well...
314
+ # as it is now, if a tag (say youtube) has tag[:require_between] == true and tag[:allow_tag_param].nil?
315
+ # then the :between is assumed to be the param... that is, a tag that should respond 'true' to tag.requires_param?
316
+ tag = @bbtree.current_node.definition
317
+ tag[:allow_tag_param_between] and @bbtree.current_node.param_not_set?
318
+ end
319
+
320
+ def parent_tag
321
+ @bbtree.parent_tag
322
+ end
323
+
324
+ def parent_has_constraints_on_children?
325
+ @bbtree.parent_has_constraints_on_children?
326
+ end
327
+
328
+ end
329
+ end