ruby-bbcode 0.0.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ Version 1.0.0 - 11-Oct-2014
2
+ ---------------------------
3
+
4
+ * Added 'self closing tags' option (enabled for [*])
5
+ * Added [list], [*] and [code] tags
6
+ * Removed deprecated method
7
+ * Renamed check_bbcode_validity to bbcode_check_validity
data/README.textile CHANGED
@@ -1,4 +1,6 @@
1
- h1. Ruby-BBcode
1
+ h1. Ruby-BBCode
2
+
3
+ !https://badge.fury.io/rb/ruby-bbcode.svg!:http://badge.fury.io/rb/ruby-bbcode
2
4
 
3
5
  This gem adds support for "BBCode":http:/www.bbcode.org/ to Ruby. The BBCode is parsed by a parser before converted to HTML, allowing to convert nested BBCode tags in strings to their correct HTML equivalent. The parser also checks whether the BBCode is valid and gives errors for incorrect BBCode texts.
4
6
 
@@ -11,21 +13,25 @@ h2. Example
11
13
 
12
14
  h2. Installing
13
15
 
14
- Add the folling line to the Gemfile in your Rails application:
16
+ Add the folling line to the Gemfile of your application:
15
17
  <pre><code>gem 'ruby-bbcode'</code></pre>
16
- Or grab it directly from the repository:
18
+
19
+ Or to use the source code from the repository:
17
20
  <pre><code>gem 'ruby-bbcode', :git => 'git://github.com/veger/ruby-bbcode.git'</code></pre>
21
+
18
22
  Run
19
23
  <pre><code>bundle install</code></pre>
20
24
 
21
- And the gem is available in you application
25
+ And Ruby-BBCode is available in your application.
22
26
 
23
27
  _Note_: Do not forget to restart your server!
24
28
 
25
29
  h2. Acknowledgements
26
30
 
27
- Some of the ideas and the tests came from "bb-ruby":http://github.com/cpjolicoeur/bb-ruby of Craig P Jolicoeur
31
+ A big thanks to "TheNotary":https://github.com/TheNotary for all contributions he made to this project!
32
+
33
+ Some of the ideas and the tests came from "bb-ruby":http://github.com/cpjolicoeur/bb-ruby of Craig P Jolicoeur.
28
34
 
29
35
  h2. License
30
36
 
31
- MIT License. See the included MIT-LICENCE file.
37
+ MIT License. See the included "MIT-LICENCE":https://github.com/veger/ruby-bbcode/blob/master/MIT-LICENSE file.
data/Rakefile CHANGED
@@ -9,14 +9,16 @@ begin
9
9
  rescue LoadError
10
10
  require 'rdoc/rdoc'
11
11
  require 'rake/rdoctask'
12
+ require 'rdoc/task'
12
13
  RDoc::Task = Rake::RDocTask
13
14
  end
14
15
 
15
16
  RDoc::Task.new(:rdoc) do |rdoc|
16
17
  rdoc.rdoc_dir = 'rdoc'
17
- rdoc.title = 'RubyBbcode'
18
+ rdoc.title = 'Ruby BBCode'
18
19
  rdoc.options << '--line-numbers'
19
- rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('README.textile')
21
+ rdoc.rdoc_files.include('CHANGELOG.md')
20
22
  rdoc.rdoc_files.include('lib/**/*.rb')
21
23
  end
22
24
 
@@ -33,5 +35,11 @@ Rake::TestTask.new(:test) do |t|
33
35
  t.verbose = true
34
36
  end
35
37
 
38
+ Rake::TestTask.new(:current) do |t|
39
+ t.libs << 'lib'
40
+ t.libs << 'test'
41
+ t.pattern = 'test/**/current_test.rb'
42
+ t.verbose = true
43
+ end
36
44
 
37
45
  task :default => :test
data/lib/ruby-bbcode.rb CHANGED
@@ -1,200 +1,77 @@
1
1
  require 'tags/tags'
2
-
2
+ require 'ruby-bbcode/tag_info'
3
+ require 'ruby-bbcode/tag_sifter'
4
+ require 'ruby-bbcode/tag_node'
5
+ require 'ruby-bbcode/tag_collection'
6
+ require 'ruby-bbcode/bbtree'
7
+
8
+ # RubyBBCode adds support for BBCode to Ruby.
9
+ # The BBCode is parsed by a parser before converted to HTML, allowing to convert nested BBCode tags in strings to their correct HTML equivalent.
10
+ # THe used parser also checks whether the BBCode is valid and gives errors for incorrect BBCode texts.
3
11
  module RubyBBCode
4
- include BBCode::Tags
5
-
6
- @@to_sentence_bbcode_tags = {:words_connector => "], [",
7
- :two_words_connector => "] and [",
8
- :last_word_connector => "] and ["}
12
+ include ::RubyBBCode::Tags
9
13
 
14
+ # This method converts the given text (with BBCode tags) into a HTML representation
15
+ # The escape_html parameter (default: true) escapes HTML tags that were present in the given text and therefore blocking (mallicious) HTML in the original text
16
+ # The additional_tags parameter is used to add additional BBCode tags that should be accepted
17
+ # The method paramter determines whether the tags parameter needs to be used to blacklist (when set to :disable) or whitelist (when not set to :disable) the list of BBCode tags
10
18
  def self.to_html(text, escape_html = true, additional_tags = {}, method = :disable, *tags)
11
- # We cannot convert to HTML if the BBCode is not valid!
12
19
  text = text.clone
13
- use_tags = @@tags.merge(additional_tags)
14
20
 
15
- if method == :disable then
16
- tags.each { |t| use_tags.delete(t) }
17
- else
18
- new_use_tags = {}
19
- tags.each { |t| new_use_tags[t] = use_tags[t] if use_tags.key?(t) }
20
- use_tags = new_use_tags
21
- end
21
+ use_tags = determine_applicable_tags(additional_tags, method, *tags)
22
22
 
23
- if escape_html
24
- text.gsub!('<', '&lt;')
25
- text.gsub!('>', '&gt;')
26
- end
23
+ @tag_sifter = TagSifter.new(text, use_tags, escape_html)
27
24
 
28
- valid = parse(text, use_tags)
29
- raise valid.join(', ') if valid != true
25
+ @tag_sifter.process_text
30
26
 
31
- bbtree_to_html(@bbtree[:nodes], use_tags)
32
- end
27
+ if @tag_sifter.invalid?
28
+ raise @tag_sifter.errors.join(', ') # We cannot convert to HTML if the BBCode is not valid!
29
+ else
30
+ @tag_sifter.bbtree.to_html(use_tags)
31
+ end
33
32
 
34
- def self.is_valid?(text, additional_tags = {})
35
- parse(text, @@tags.merge(additional_tags));
36
33
  end
37
34
 
38
- def self.tag_list
39
- @@tags
35
+ # Returns true when valid, else returns array with error(s)
36
+ def self.validity_check(text, additional_tags = {})
37
+ @tag_sifter = TagSifter.new(text, @@tags.merge(additional_tags))
38
+
39
+ @tag_sifter.process_text
40
+ return @tag_sifter.errors if @tag_sifter.invalid?
41
+ true
40
42
  end
41
43
 
44
+
42
45
  protected
43
- def self.parse(text, tags = {})
44
- tags = @@tags if tags == {}
45
- tags_list = []
46
- @bbtree = {:nodes => []}
47
- bbtree_depth = 0
48
- bbtree_current_node = @bbtree
49
- text.scan(/((\[ (\/)? (\w+) ((=[^\[\]]+) | (\s\w+=\w+)* | ([^\]]*))? \]) | ([^\[]+))/ix) do |tag_info|
50
- ti = find_tag_info(tag_info)
51
-
52
- if ti[:is_tag] and !tags.include?(ti[:tag].to_sym)
53
- # Handle as text from now on!
54
- ti[:is_tag] = false
55
- ti[:text] = ti[:complete_match]
56
- end
57
-
58
- if !ti[:is_tag] or !ti[:closing_tag]
59
- if ti[:is_tag]
60
- tag = tags[ti[:tag].to_sym]
61
- unless tag[:only_in].nil? or (tags_list.length > 0 and tag[:only_in].include?(tags_list.last.to_sym))
62
- # Tag does to be put in the last opened tag
63
- err = "[#{ti[:tag]}] can only be used in [#{tag[:only_in].to_sentence(@@to_sentence_bbcode_tags)}]"
64
- err += ", so using it in a [#{tags_list.last}] tag is not allowed" if tags_list.length > 0
65
- return [err]
66
- end
67
-
68
- if tag[:allow_tag_param] and ti[:params][:tag_param] != nil
69
- # Test if matches
70
- return [tag[:tag_param_description].gsub('%param%', ti[:params][:tag_param])] if ti[:params][:tag_param].match(tag[:tag_param]).nil?
71
- end
72
- end
73
-
74
- if tags_list.length > 0 and tags[tags_list.last.to_sym][:only_allow] != nil
75
- # Check if the found tag is allowed
76
- last_tag = tags[tags_list.last.to_sym]
77
- allowed_tags = last_tag[:only_allow]
78
- 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))
79
- # Last opened tag does not allow tag
80
- err = "[#{tags_list.last}] can only contain [#{allowed_tags.to_sentence(@@to_sentence_bbcode_tags)}] tags, so "
81
- err += "[#{ti[:tag]}]" if ti[:is_tag]
82
- err += "\"#{ti[:text]}\"" unless ti[:is_tag]
83
- err += ' is not allowed'
84
- return [err]
85
- end
86
- end
87
-
88
- # Validation of tag succeeded, add to tags_list and/or bbtree
89
- if ti[:is_tag]
90
- tag = tags[ti[:tag].to_sym]
91
- tags_list.push ti[:tag]
92
- element = {:is_tag => true, :tag => ti[:tag].to_sym, :nodes => [] }
93
- element[:params] = {:tag_param => ti[:params][:tag_param]} if tag[:allow_tag_param] and ti[:params][:tag_param] != nil
94
- else
95
- text = ti[:text]
96
- text.gsub!("\r\n", "\n")
97
- text.gsub!("\n", "<br />\n")
98
- element = {:is_tag => false, :text => text }
99
- if bbtree_depth > 0
100
- tag = tags[bbtree_current_node[:tag]]
101
- if tag[:require_between] == true
102
- bbtree_current_node[:between] = ti[:text]
103
- if tag[:allow_tag_param] and tag[:allow_tag_param_between] and (bbtree_current_node[:params] == nil or bbtree_current_node[:params][:tag_param] == nil)
104
- # Did not specify tag_param, so use between.
105
- # Check if valid
106
- return [tag[:tag_param_description].gsub('%param%', ti[:text])] if ti[:text].match(tag[:tag_param]).nil?
107
- # Store as tag_param
108
- bbtree_current_node[:params] = {:tag_param => ti[:text]}
109
- end
110
- element = nil
111
- end
112
- end
113
- end
114
- bbtree_current_node[:nodes] << element unless element == nil
115
- if ti[:is_tag]
116
- # Advance to next level (the node we just added)
117
- bbtree_current_node = element
118
- bbtree_depth += 1
119
- end
120
- end
121
-
122
- if ti[:is_tag] and ti[:closing_tag]
123
- if ti[:is_tag]
124
- tag = tags[ti[:tag].to_sym]
125
- return ["Closing tag [/#{ti[:tag]}] does match [#{tags_list.last}]"] if tags_list.last != ti[:tag]
126
- return ["No text between [#{ti[:tag]}] and [/#{ti[:tag]}] tags."] if tag[:require_between] == true and bbtree_current_node[:between].blank?
127
- tags_list.pop
128
-
129
- # Find parent node (kinda hard since no link to parent node is available...)
130
- bbtree_depth -= 1
131
- bbtree_current_node = @bbtree
132
- bbtree_depth.times { bbtree_current_node = bbtree_current_node[:nodes].last }
133
- end
134
- end
135
- end
136
- return ["[#{tags_list.to_sentence((@@to_sentence_bbcode_tags))}] not closed"] if tags_list.length > 0
137
46
 
138
- true
47
+ # This method provides the final set of bbcode tags, it merges the default tags with the given additional_tags
48
+ # and blacklists(method = :disable) or whitelists the list of tags with the given tags parameter.
49
+ def self.determine_applicable_tags(additional_tags, method, *tags)
50
+ use_tags = @@tags.merge(additional_tags)
51
+ if method == :disable then # if method is set to :disable
52
+ tags.each { |t| use_tags.delete(t) } # blacklist (remove) the supplied tags
53
+ else # method is not :disable, but has any other value
54
+ # Only use the supplied tags (whitelist)
55
+ new_use_tags = {}
56
+ tags.each { |t| new_use_tags[t] = use_tags[t] if use_tags.key?(t) }
57
+ use_tags = new_use_tags
58
+ end
59
+ use_tags
139
60
  end
140
61
 
141
- def self.find_tag_info(tag_info)
142
- ti = {}
143
- ti[:complete_match] = tag_info[0]
144
- ti[:is_tag] = (tag_info[0].start_with? '[')
145
- if ti[:is_tag]
146
- ti[:closing_tag] = (tag_info[2] == '/')
147
- ti[:tag] = tag_info[3]
148
- ti[:params] = {}
149
- if tag_info[4][0] == ?=
150
- ti[:params][:tag_param] = tag_info[4][1..-1]
151
- elsif tag_info[4][0] == ?\s
152
- #TODO: Find params
153
- end
62
+ def self.parse(text, tags)
63
+ @tag_sifter = TagSifter.new(text, tags)
64
+
65
+ @tag_sifter.process_text
66
+
67
+ if @tag_sifter.invalid?
68
+ @tag_sifter.errors
154
69
  else
155
- # Plain text
156
- ti[:text] = tag_info[8]
70
+ true
157
71
  end
158
- ti
159
- end
160
72
 
161
- def self.bbtree_to_html(node_list, tags = {})
162
- tags = @@tags if tags == {}
163
- text = ""
164
- node_list.each do |node|
165
- if node[:is_tag]
166
- tag = tags[node[:tag]]
167
- t = tag[:html_open].dup
168
- t.gsub!('%between%', node[:between]) if tag[:require_between]
169
- if tag[:allow_tag_param]
170
- if node[:params] and !node[:params][:tag_param].blank?
171
- match_array = node[:params][:tag_param].scan(tag[:tag_param])[0]
172
- index = 0
173
- match_array.each do |match|
174
- if index < tag[:tag_param_tokens].length
175
- t.gsub!("%#{tag[:tag_param_tokens][index][:token].to_s}%", tag[:tag_param_tokens][index][:prefix].to_s+match+tag[:tag_param_tokens][index][:postfix].to_s)
176
- index += 1
177
- end
178
- end
179
- else
180
- # Remove unused tokens
181
- tag[:tag_param_tokens].each do |token|
182
- t.gsub!("%#{token[:token]}%", '')
183
- end
184
- end
185
- end
186
-
187
- text += t
188
- text += bbtree_to_html(node[:nodes], tags) if node[:nodes].length > 0
189
- t = tag[:html_close]
190
- t.gsub!('%between%', node[:between]) if tag[:require_between]
191
- text += t
192
- else
193
- text += node[:text] unless node[:text].nil?
194
- end
195
- end
196
- text
197
73
  end
74
+
198
75
  end
199
76
 
200
77
  String.class_eval do
@@ -202,14 +79,14 @@ String.class_eval do
202
79
  def bbcode_to_html(escape_html = true, additional_tags = {}, method = :disable, *tags)
203
80
  RubyBBCode.to_html(self, escape_html, additional_tags, method, *tags)
204
81
  end
205
-
82
+
206
83
  # Replace the BBCode content of a string with its corresponding HTML markup
207
84
  def bbcode_to_html!(escape_html = true, additional_tags = {}, method = :disable, *tags)
208
85
  self.replace(RubyBBCode.to_html(self, escape_html, additional_tags, method, *tags))
209
86
  end
210
87
 
211
88
  # Check if string contains valid BBCode. Returns true when valid, else returns array with error(s)
212
- def is_valid_bbcode?
213
- RubyBBCode.is_valid?(self)
89
+ def bbcode_check_validity
90
+ RubyBBCode.validity_check(self)
214
91
  end
215
92
  end
@@ -0,0 +1,94 @@
1
+ module RubyBBCode
2
+ # Tree of nodes containing the parsed BBCode information and the plain texts
3
+ #
4
+ # As you parse a string of text, say:
5
+ # "[b]I'm bold and the next word is [i]ITALIC[/i][b]"
6
+ # ...you build up a tree of nodes (@bbtree). The above string converts to 4 nodes when the parse has completed.
7
+ # * Node 1) An opening tag node representing "[b]"
8
+ # * Node 2) A text node representing "I'm bold and the next word is "
9
+ # * Node 3) An opening tag node representing "[i]"
10
+ # * Node 4) A text node representing "ITALIC"
11
+ #
12
+ # The closing of the nodes seems to be implied which is fine by me --less to keep track of.
13
+ #
14
+ class BBTree
15
+ attr_accessor :current_node, :tags_list
16
+
17
+ def initialize(hash = { :nodes => TagCollection.new }, dictionary)
18
+ @bbtree = hash
19
+ @current_node = TagNode.new(@bbtree)
20
+ @tags_list = []
21
+ @dictionary = dictionary
22
+ end
23
+
24
+ def [](key)
25
+ @bbtree[key]
26
+ end
27
+
28
+ def []=(key, value)
29
+ @bbtree[key] = value
30
+ end
31
+
32
+ def nodes
33
+ @bbtree[:nodes]
34
+ end
35
+ alias :children :nodes # needed due to the similarities between BBTree[:nodes] and TagNode[:nodes]... they're walked through in debugging.rb right now
36
+
37
+ def type
38
+ :bbtree
39
+ end
40
+
41
+ def within_open_tag?
42
+ @tags_list.length > 0
43
+ end
44
+ alias :expecting_a_closing_tag? :within_open_tag? # just giving this method multiple names for semantical purposes
45
+
46
+ def parent_tag
47
+ return nil if !within_open_tag?
48
+ @tags_list.last.to_sym
49
+ end
50
+
51
+ def parent_has_constraints_on_children?
52
+ @dictionary[parent_tag][:only_allow] != nil
53
+ end
54
+
55
+ # Advance to next level (the node we just added)
56
+ def escalate_bbtree(element)
57
+ @tags_list.push element[:tag]
58
+ @current_node = TagNode.new(element)
59
+ end
60
+
61
+ # Step down the bbtree a notch because we've reached a closing tag
62
+ def retrogress_bbtree
63
+ @tags_list.pop # remove latest tag in tags_list since it's closed now...
64
+ # The parsed data manifests in @bbtree.current_node.children << TagNode.new(element) which I think is more confusing than needed
65
+
66
+ if within_open_tag?
67
+ # Set the current node to be the node we've just parsed over which is infact within another node??...
68
+ @current_node = TagNode.new(self.nodes.last)
69
+ else # If we're still at the root of the BBTree or have returned back to the root via encountring closing tags...
70
+ @current_node = TagNode.new({:nodes => self.nodes}) # Note: just passing in self works too...
71
+ end
72
+
73
+ # OKOKOK!
74
+ # Since @bbtree = @current_node, if we ever set @current_node to something, we're actually changing @bbtree...
75
+ # therefore... my brain is now numb
76
+ end
77
+
78
+ def redefine_parent_tag_as_text
79
+ @tags_list.pop
80
+ @current_node[:is_tag] = false
81
+ @current_node[:closing_tag] = false
82
+ @current_node.element[:text] = "[#{@current_node[:tag].to_s}]"
83
+ end
84
+
85
+ def build_up_new_tag(element)
86
+ @current_node.children << TagNode.new(element)
87
+ end
88
+
89
+ def to_html(tags = {})
90
+ self.nodes.to_html(tags)
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,99 @@
1
+ module RubyBBCode
2
+ # This class holds TagNode instances and helps build them into html when the time comes.
3
+ #
4
+ # It is really just a simple array, with the addition of the #to_html method
5
+ class TagCollection < Array
6
+
7
+ # This method is vulnerable to stack-level-too-deep scenarios where >=1,200 tags are being parsed.
8
+ # But that scenario can be mitigated by splitting up the tags. bbtree = { :nodes => [900tags, 1000tags] }, the work
9
+ # for that bbtree can be split up into two passes, do the each node one at a time. I'm not coding that though, it's pointless, just a thought though
10
+ def to_html(tags)
11
+ html_string = ""
12
+ self.each do |node|
13
+ if node.type == :tag
14
+ t = HtmlTemplate.new node
15
+
16
+ t.inlay_between_text!
17
+
18
+ if node.allow_tag_param? and node.param_set?
19
+ t.inlay_inline_params!
20
+ elsif node.allow_tag_param? and node.param_not_set?
21
+ t.remove_unused_tokens!
22
+ end
23
+
24
+ html_string << t.opening_html
25
+
26
+ # invoke "recursive" call if this node contains child nodes
27
+ html_string << node.children.to_html(tags) if node.has_children? # FIXME: Don't use recursion, it can lead to stack-level-too-deep errors for large volumes?
28
+
29
+ t.inlay_closing_html!
30
+
31
+ html_string << t.closing_html
32
+ elsif node.type == :text
33
+ html_string << node[:text] unless node[:text].nil?
34
+ end
35
+ end
36
+
37
+ html_string
38
+ end
39
+
40
+
41
+
42
+ # This class is designed to help us build up the HTML data. It starts out as a template such as...
43
+ # @opening_html = '<a href="%url%">%between%'
44
+ # @closing_html = '</a>'
45
+ # and then slowly turns into...
46
+ # @opening_html = '<a href="http://www.blah.com">cool beans'
47
+ # @closing_html = '</a>'
48
+ # TODO: Think about creating a separate file for this or something... maybe look into folder structures cause this project
49
+ # got huge when I showed up.
50
+ class HtmlTemplate
51
+ attr_accessor :opening_html, :closing_html
52
+
53
+ def initialize(node)
54
+ @node = node
55
+ @tag_definition = node.definition # tag_definition
56
+ @opening_html = node.definition[:html_open].dup
57
+ @closing_html = node.definition[:html_close].dup
58
+ end
59
+
60
+ def inlay_between_text!
61
+ @opening_html.gsub!('%between%',@node[:between]) if between_text_goes_into_html_output_as_param? # set the between text to where it goes if required to do so...
62
+ end
63
+
64
+ def inlay_inline_params!
65
+ # Get list of paramaters to feed
66
+ match_array = @node[:params][:tag_param].scan(@tag_definition[:tag_param])[0]
67
+
68
+ # for each parameter to feed
69
+ match_array.each.with_index do |match, i|
70
+ if i < @tag_definition[:tag_param_tokens].length
71
+
72
+ # Substitute the %param% keyword for the appropriate data specified
73
+ @opening_html.gsub!("%#{@tag_definition[:tag_param_tokens][i][:token].to_s}%",
74
+ @tag_definition[:tag_param_tokens][i][:prefix].to_s +
75
+ match +
76
+ @tag_definition[:tag_param_tokens][i][:postfix].to_s)
77
+ end
78
+ end
79
+ end
80
+
81
+ def inlay_closing_html!
82
+ @closing_html.gsub!('%between%',@node[:between]) if @tag_definition[:require_between]
83
+ end
84
+
85
+ def remove_unused_tokens!
86
+ @tag_definition[:tag_param_tokens].each do |token|
87
+ @opening_html.gsub!("%#{token[:token]}%", '')
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def between_text_goes_into_html_output_as_param?
94
+ @tag_definition[:require_between]
95
+ end
96
+ end
97
+
98
+ end
99
+ end