jarrett-rbbcode 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2004-2008 David Heinemeier Hansson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README ADDED
@@ -0,0 +1,13 @@
1
+ RbbCode is a customizable Ruby library for parsing BB Code.
2
+
3
+ RbbCode validates and cleans input. It supports customizable schemas so you can set rules about what tags are allowed where. The default rules are designed to ensure valid HTML output.
4
+
5
+ Example usage:
6
+
7
+ require 'rubygems'
8
+ require 'rbbcode'
9
+
10
+ bb_code = 'This is [b]bold[/b] text'
11
+ parser = RbbCode::Parser.new
12
+ html = parser.parse(bb_code)
13
+ # => 'This is <strong>bold</strong> text'
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,93 @@
1
+ # TODO: Lists must be surrounded by </p> and <p>
2
+
3
+ require 'cgi'
4
+
5
+ module RbbCode
6
+ DEFAULT_TAG_MAPPINGS = {
7
+ 'p' => 'p',
8
+ 'br' => 'br',
9
+ 'b' => 'strong',
10
+ 'i' => 'em',
11
+ 'u' => 'u',
12
+ 'code' => 'code',
13
+ 'quote' => 'blockquote',
14
+ 'list' => 'ul',
15
+ '*' => 'li'
16
+ }
17
+
18
+ class HtmlMaker
19
+ def make_html(node)
20
+ output = ''
21
+ case node.class.to_s
22
+ when 'RbbCode::RootNode'
23
+ node.children.each do |child|
24
+ output << make_html(child)
25
+ end
26
+ when 'RbbCode::TagNode'
27
+ custom_tag_method = "html_from_#{node.tag_name}_tag"
28
+ if respond_to?(custom_tag_method)
29
+ output << send(custom_tag_method, node)
30
+ else
31
+ inner_html = ''
32
+ node.children.each do |child|
33
+ inner_html << make_html(child)
34
+ end
35
+ output << content_tag(map_tag_name(node.tag_name), inner_html)
36
+ end
37
+ when 'RbbCode::TextNode'
38
+ output << node.text
39
+ else
40
+ raise "Don't know how to make HTML from #{node.class}"
41
+ end
42
+ output
43
+ end
44
+
45
+ protected
46
+
47
+ def content_tag(tag_name, contents, attributes = {})
48
+ output = "<#{tag_name}"
49
+ attributes.each do |attr, value|
50
+ output << " #{attr}=\"#{value}\""
51
+ end
52
+ if contents.nil? or contents.empty?
53
+ output << '/>'
54
+ else
55
+ output << ">#{contents}</#{tag_name}>"
56
+ end
57
+ end
58
+
59
+ def html_from_img_tag(node)
60
+ src = sanitize_url(node.inner_bb_code)
61
+ content_tag('img', nil, {'src' => src, 'alt' => ''})
62
+ end
63
+
64
+ def html_from_url_tag(node)
65
+ inner_bb_code = node.inner_bb_code
66
+ if node.value.nil?
67
+ url = inner_bb_code
68
+ else
69
+ url = node.value
70
+ end
71
+ url = sanitize_url(url)
72
+ content_tag('a', inner_bb_code, {'href' => url})
73
+ end
74
+
75
+ def map_tag_name(tag_name)
76
+ unless DEFAULT_TAG_MAPPINGS.has_key?(tag_name)
77
+ raise "No tag mapping for '#{tag_name}'"
78
+ end
79
+ DEFAULT_TAG_MAPPINGS[tag_name]
80
+ end
81
+
82
+ def sanitize_url(url)
83
+ # Prepend a protocol if there isn't one
84
+ unless url.match(/^[a-zA-Z]+:\/\//)
85
+ url = 'http://' + url
86
+ end
87
+ # Replace all functional permutations of "javascript:" with a hex-encoded version of the same
88
+ url.gsub(/(\s*j\s*\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*):/i) do |match_str|
89
+ CGI::escape($1) + '%3A'
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,25 @@
1
+ module RbbCode
2
+ class Parser
3
+ def initialize(config = {})
4
+ @config = config
5
+ end
6
+
7
+ def parse(str)
8
+ str = escape_html_tags(str)
9
+
10
+ schema = @config[:schema] || RbbCode::Schema.new
11
+
12
+ tree_maker = @config[:tree_maker] || RbbCode::TreeMaker.new(schema)
13
+ tree = tree_maker.make_tree(str)
14
+
15
+ html_maker = @config[:html_maker] || RbbCode::HtmlMaker.new
16
+ html_maker.make_html(tree)
17
+ end
18
+
19
+ protected
20
+
21
+ def escape_html_tags(str)
22
+ str.gsub('<', '&lt;').gsub('>', '&gt;')
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,235 @@
1
+ module RbbCode
2
+ DEFAULT_ALLOWED_TAGS = [
3
+ 'p',
4
+ 'br',
5
+ 'b',
6
+ 'i',
7
+ 'u',
8
+ 'url',
9
+ 'img',
10
+ 'code',
11
+ 'quote',
12
+ 'list',
13
+ '*'
14
+ ]
15
+
16
+ DEFAULT_BLOCK_LEVEL_ELEMENTS = [
17
+ 'quote',
18
+ 'list',
19
+ '*'
20
+ ]
21
+
22
+ class SchemaNode
23
+ def initialize(schema)
24
+ @schema = schema
25
+ end
26
+
27
+ protected
28
+
29
+ def normalize_ancestors(ancestors)
30
+ if ancestors.length == 1 and ancestors[0].is_a?(Array)
31
+ ancestors = ancestors[0]
32
+ end
33
+ ancestors
34
+ end
35
+ end
36
+
37
+ class SchemaTag < SchemaNode
38
+ def initialize(schema, name)
39
+ @schema = schema
40
+ @name = name
41
+ end
42
+
43
+ def may_be_nested
44
+ @schema.allow_descent(@name, @name)
45
+ self
46
+ end
47
+
48
+ def may_contain_text
49
+ @schema.allow_text(@name)
50
+ self
51
+ end
52
+
53
+ def may_not_be_nested
54
+ @schema.forbid_descent(@name, @name)
55
+ self
56
+ end
57
+
58
+ def may_descend_from(tag_name)
59
+ @schema.allow_descent(tag_name, @name)
60
+ self
61
+ end
62
+
63
+ def may_only_be_parent_of(*tag_names)
64
+ @schema.forbid_children_except(@name, *tag_names)
65
+ self
66
+ end
67
+
68
+ def may_not_contain_text
69
+ @schema.forbid_text(@name)
70
+ self
71
+ end
72
+
73
+ def may_not_descend_from(tag_name)
74
+ @schema.forbid_descent(tag_name, @name)
75
+ self
76
+ end
77
+
78
+ def must_be_child_of(*tag_names)
79
+ @schema.require_parents(tag_names, @name)
80
+ self
81
+ end
82
+
83
+ def must_be_empty
84
+ @schema.forbid_children_except(@name, [])
85
+ may_not_contain_text
86
+ self
87
+ end
88
+
89
+ def need_not_be_child_of(tag_name)
90
+ @schema.unrequire_parent(tag_name, @name)
91
+ self
92
+ end
93
+
94
+ # Returns true if tag_name is valid in the context defined by its list of ancestors.
95
+ # ancestors should be ordered from most recent ancestor to most distant.
96
+ def valid_in_context?(*ancestors)
97
+ @schema.tag_valid_in_context?(@name, normalize_ancestors(ancestors))
98
+ end
99
+ end
100
+
101
+ class SchemaText < SchemaNode
102
+ def valid_in_context?(*ancestors)
103
+ @schema.text_valid_in_context?(normalize_ancestors(ancestors))
104
+ end
105
+ end
106
+
107
+ class Schema
108
+ def allow_descent(ancestor, descendant) #:nodoc:
109
+ if @forbidden_descent.has_key?(descendant.to_s) and @forbidden_descent[descendant.to_s].include?(ancestor.to_s)
110
+ @forbidden_descent[descendant.to_s].delete(ancestor.to_s)
111
+ end
112
+ end
113
+
114
+ def allow_tag(*tag_names)
115
+ tag_names.each do |tag_name|
116
+ unless @allowed_tags.include?(tag_name.to_s)
117
+ @allowed_tags << tag_name.to_s
118
+ end
119
+ end
120
+ end
121
+
122
+ def allow_text(tag_name)
123
+ @no_text.delete(tag_name.to_s)
124
+ end
125
+
126
+ def block_level?(tag_name)
127
+ DEFAULT_BLOCK_LEVEL_ELEMENTS.include?(tag_name.to_s)
128
+ end
129
+
130
+ alias_method :allow_tags, :allow_tag
131
+
132
+ def clear
133
+ @allowed_tags = []
134
+ @forbidden_descent = {}
135
+ @required_parents = {}
136
+ @no_text = []
137
+ end
138
+
139
+ def forbid_children_except(parent, children)
140
+ @child_requirements[parent.to_s] = children.collect { |c| c.to_s }
141
+ end
142
+
143
+ def forbid_descent(ancestor, descendant) #:nodoc:
144
+ @forbidden_descent[descendant.to_s] ||= []
145
+ unless @forbidden_descent[descendant.to_s].include?(ancestor.to_s)
146
+ @forbidden_descent[descendant.to_s] << ancestor.to_s
147
+ end
148
+ end
149
+
150
+ def forbid_tag(name)
151
+ @allowed_tags.delete(name.to_s)
152
+ end
153
+
154
+ def forbid_text(tag_name)
155
+ @no_text << tag_name.to_s unless @no_text.include?(tag_name.to_s)
156
+ end
157
+
158
+ def initialize
159
+ @allowed_tags = DEFAULT_ALLOWED_TAGS.dup
160
+ @forbidden_descent = {}
161
+ @required_parents = {}
162
+ @child_requirements = {}
163
+ @no_text = []
164
+ use_defaults
165
+ end
166
+
167
+ def line_break_tag_name
168
+ 'br'
169
+ end
170
+
171
+ def paragraph_tag_name
172
+ 'p'
173
+ end
174
+
175
+ def require_parents(parents, child) #:nodoc:
176
+ @required_parents[child.to_s] = parents.collect { |p| p.to_s }
177
+ parents.each do |parent|
178
+ if @forbidden_descent.has_key?(child.to_s)
179
+ @forbidden_descent[child.to_s].delete(parent)
180
+ end
181
+ end
182
+ end
183
+
184
+ def tag(name)
185
+ SchemaTag.new(self, name)
186
+ end
187
+
188
+ def tag_valid_in_context?(tag_name, ancestors)
189
+ return false unless @allowed_tags.include?(tag_name.to_s)
190
+ if @required_parents.has_key?(tag_name.to_s) and !@required_parents[tag_name.to_s].include?(ancestors[0].to_s)
191
+ return false
192
+ end
193
+ if @child_requirements.has_key?(ancestors[0].to_s) and !@child_requirements[ancestors[0].to_s].include?(tag_name.to_s)
194
+ return false
195
+ end
196
+ if @forbidden_descent.has_key?(tag_name.to_s)
197
+ @forbidden_descent[tag_name.to_s].each do |forbidden_ancestor|
198
+ return false if ancestors.include?(forbidden_ancestor)
199
+ end
200
+ end
201
+ return true
202
+ end
203
+
204
+ def text
205
+ SchemaText.new(self)
206
+ end
207
+
208
+ def text_valid_in_context?(*ancestors)
209
+ if @no_text.include?(ancestors[0].to_s)
210
+ return false
211
+ end
212
+ return true
213
+ end
214
+
215
+ def unrequire_parent(parent, child)
216
+ @required_parents.delete(child.to_s)
217
+ end
218
+
219
+ def use_defaults
220
+ tag('br').must_be_empty
221
+ tag('p').may_not_be_nested
222
+ tag('b').may_not_be_nested
223
+ tag('i').may_not_be_nested
224
+ tag('u').may_not_be_nested
225
+ tag('url').may_not_be_nested
226
+ tag('img').may_not_be_nested
227
+ tag('code').may_not_be_nested
228
+ tag('p').may_not_be_nested
229
+ tag('*').must_be_child_of('list')
230
+ tag('list').may_not_descend_from('p')
231
+ tag('list').may_only_be_parent_of('*')
232
+ tag('list').may_not_contain_text
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,321 @@
1
+ require 'pp'
2
+
3
+ module RbbCode
4
+ module CharCodes
5
+ CR_CODE = 13
6
+ LF_CODE = 10
7
+
8
+ L_BRACK_CODE = 91
9
+ R_BRACK_CODE = 93
10
+ SLASH_CODE = 47
11
+
12
+ LOWER_A_CODE = 97
13
+ LOWER_Z_CODE = 122
14
+
15
+ UPPER_A_CODE = 65
16
+ UPPER_Z_CODE = 90
17
+ end
18
+
19
+ class Node
20
+ def << (child)
21
+ @children << child
22
+ end
23
+
24
+ attr_accessor :children
25
+
26
+ def initialize(parent)
27
+ @parent = parent
28
+ @children = []
29
+ end
30
+
31
+ attr_accessor :parent
32
+ end
33
+
34
+ class RootNode < Node
35
+ def initialize
36
+ @children = []
37
+ end
38
+ end
39
+
40
+ class TextNode < Node
41
+
42
+ undef_method '<<'.to_sym
43
+ undef_method :children
44
+
45
+ def initialize(parent, text)
46
+ @parent = parent
47
+ @text = text
48
+ end
49
+
50
+ attr_accessor :text
51
+
52
+ def to_bb_code
53
+ @text
54
+ end
55
+ end
56
+
57
+ class TagNode < Node
58
+ def self.from_opening_bb_code(parent, bb_code)
59
+ if equal_index = bb_code.index('=')
60
+ tag_name = bb_code[1, equal_index - 1]
61
+ value = bb_code[(equal_index + 1)..-2]
62
+ else
63
+ tag_name = bb_code[1..-2]
64
+ value = nil
65
+ end
66
+ new(parent, tag_name, value)
67
+ end
68
+
69
+ def initialize(parent, tag_name, value = nil)
70
+ super(parent)
71
+ @tag_name = tag_name
72
+ @value = value
73
+ end
74
+
75
+ def inner_bb_code
76
+ @children.inject('') do |output, child|
77
+ output << child.to_bb_code
78
+ end
79
+ end
80
+
81
+ def to_bb_code
82
+ if @value.nil?
83
+ output = "[#{@tag_name}]"
84
+ else
85
+ output = "[#{@tag_name}=#{@value}]"
86
+ end
87
+ output << inner_bb_code << "[/#{@tag_name}]"
88
+ end
89
+
90
+ attr_reader :tag_name
91
+
92
+ attr_reader :value
93
+ end
94
+
95
+ class TreeMaker
96
+ include CharCodes
97
+
98
+ def initialize(schema)
99
+ @schema = schema
100
+ end
101
+
102
+ def make_tree(str)
103
+ delete_empty_paragraphs(parse_str(str))
104
+ end
105
+
106
+ protected
107
+
108
+ def ancestor_list(parent)
109
+ ancestors = []
110
+ while parent.is_a?(TagNode)
111
+ ancestors << parent.tag_name
112
+ parent = parent.parent
113
+ end
114
+ ancestors
115
+ end
116
+
117
+ def break_type(break_str)
118
+ if break_str.length > 2
119
+ :paragraph
120
+ elsif break_str.length == 1
121
+ :line_break
122
+ elsif break_str == "\r\n"
123
+ :line_break
124
+ else
125
+ :paragraph
126
+ end
127
+ end
128
+
129
+ def delete_empty_paragraphs(node)
130
+ node.children.reject! do |child|
131
+ if child.is_a?(TagNode)
132
+ if !child.children.empty?
133
+ delete_empty_paragraphs(child)
134
+ false
135
+ elsif child.tag_name == @schema.paragraph_tag_name
136
+ # It's an empty paragraph tag, so the reject! block should return true
137
+ true
138
+ else
139
+ false
140
+ end
141
+ else
142
+ false
143
+ end
144
+ end
145
+ node
146
+ end
147
+
148
+ def parse_str(str)
149
+ tree = RootNode.new
150
+ # Initially, we open a paragraph tag. If it turns out that the first thing we encounter
151
+ # is a block-level element, no problem: we'll be calling promote_block_level_elements
152
+ # later anyway.
153
+ current_parent = TagNode.new(tree, @schema.paragraph_tag_name)
154
+ tree << current_parent
155
+ current_token = ''
156
+ current_token_type = :unknown
157
+ str.each_byte do |char_code|
158
+ char = char_code.chr
159
+ case current_token_type
160
+ when :unknown
161
+ case char
162
+ when '['
163
+ current_token_type = :possible_tag
164
+ current_token << char
165
+ when "\r", "\n"
166
+ current_token_type = :break
167
+ current_token << char
168
+ else
169
+ if current_parent.is_a?(RootNode)
170
+ new_paragraph_tag = TagNode.new(current_parent, @schema.paragraph_tag_name)
171
+ current_parent << new_paragraph_tag
172
+ current_parent = new_paragraph_tag
173
+ end
174
+ current_token_type = :text
175
+ current_token << char
176
+ end
177
+ when :text
178
+ case char
179
+ when "["
180
+ if @schema.text_valid_in_context?(*ancestor_list(current_parent))
181
+ current_parent << TextNode.new(current_parent, current_token)
182
+ end
183
+ current_token = '['
184
+ current_token_type = :possible_tag
185
+ when "\r", "\n"
186
+ if @schema.text_valid_in_context?(*ancestor_list(current_parent))
187
+ current_parent << TextNode.new(current_parent, current_token)
188
+ end
189
+ current_token = char
190
+ current_token_type = :break
191
+ else
192
+ current_token << char
193
+ end
194
+ when :break
195
+ if char == CR_CODE or char_code == LF_CODE
196
+ current_token << char
197
+ else
198
+ if break_type(current_token) == :paragraph
199
+ while current_parent.is_a?(TagNode) and !@schema.block_level?(current_parent.tag_name) and current_parent.tag_name != @schema.paragraph_tag_name
200
+ current_parent = current_parent.parent
201
+ end
202
+ # The current parent might be a paragraph tag, in which case we should move up one more level.
203
+ # Otherwise, it might be a block-level element or a root node, in which case we should not move up.
204
+ if current_parent.is_a?(TagNode) and current_parent.tag_name == @schema.paragraph_tag_name
205
+ current_parent = current_parent.parent
206
+ end
207
+ # Regardless of whether the current parent is a block-level element, we need to open a new paragraph.
208
+ new_paragraph_node = TagNode.new(current_parent, @schema.paragraph_tag_name)
209
+ current_parent << new_paragraph_node
210
+ current_parent = new_paragraph_node
211
+ else # line break
212
+ prev_sibling = current_parent.children.last
213
+ if prev_sibling.is_a?(TagNode) and @schema.block_level?(prev_sibling.tag_name)
214
+ # Although the input only contains a single newline, we should
215
+ # interpret is as the start of a new paragraph, because the last
216
+ # thing we encountered was a block-level element.
217
+ new_paragraph_node = TagNode.new(current_parent, @schema.paragraph_tag_name)
218
+ current_parent << new_paragraph_node
219
+ current_parent = new_paragraph_node
220
+ elsif @schema.tag(@schema.line_break_tag_name).valid_in_context?(*ancestor_list(current_parent))
221
+ current_parent << TagNode.new(current_parent, @schema.line_break_tag_name)
222
+ end
223
+ end
224
+ if char == '['
225
+ current_token = '['
226
+ current_token_type = :possible_tag
227
+ else
228
+ current_token = char
229
+ current_token_type = :text
230
+ end
231
+ end
232
+ when :possible_tag
233
+ case char
234
+ when '['
235
+ current_parent << TextNode.new(current_parent, '[')
236
+ # No need to reset current_token or current_token_type
237
+ when '/'
238
+ current_token_type = :closing_tag
239
+ current_token << '/'
240
+ else
241
+ if tag_name_char?(char_code)
242
+ current_token_type = :opening_tag
243
+ current_token << char
244
+ elsif tag_name
245
+ current_token_type = :text
246
+ current_token << char
247
+ end
248
+ end
249
+ when :opening_tag
250
+ if tag_name_char?(char_code) or char == '='
251
+ current_token << char
252
+ elsif char == ']'
253
+ current_token << ']'
254
+ tag_node = TagNode.from_opening_bb_code(current_parent, current_token)
255
+ if @schema.block_level?(tag_node.tag_name) and current_parent.tag_name == @schema.paragraph_tag_name
256
+ # If there is a line break before this, it's superfluous and should be deleted
257
+ prev_sibling = current_parent.children.last
258
+ if prev_sibling.is_a?(TagNode) and prev_sibling.tag_name == @schema.line_break_tag_name
259
+ current_parent.children.pop
260
+ end
261
+ # Promote a block-level element
262
+ current_parent = current_parent.parent
263
+ tag_node.parent = current_parent
264
+ current_parent << tag_node
265
+ current_parent = tag_node
266
+ # If all of this results in empty paragraph tags, no worries: they will be deleted later.
267
+ elsif @schema.tag(tag_node.tag_name).valid_in_context?(*ancestor_list(current_parent))
268
+ current_parent << tag_node
269
+ current_parent = tag_node
270
+ end # else, don't do anything--the tag is invalid and will be ignored
271
+ current_token_type = :unknown
272
+ current_token = ''
273
+ elsif char == "\r" or char == "\n"
274
+ current_parent << TextNode.new(current_parent, current_token)
275
+ current_token = char
276
+ current_token_type = :break
277
+ elsif current_token.include?('=')
278
+ current_token << char
279
+ else
280
+ current_token_type = :text
281
+ current_token << char
282
+ end
283
+ when :closing_tag
284
+ if tag_name_char?(char_code)
285
+ current_token << char
286
+ elsif char == ']'
287
+ original_parent = current_parent
288
+ while current_parent.is_a?(TagNode) and current_parent.tag_name != current_token[2..-1]
289
+ current_parent = current_parent.parent
290
+ end
291
+ if current_parent.is_a?(TagNode)
292
+ current_parent = current_parent.parent
293
+ else # current_parent is a RootNode
294
+ # we made it to the top of the tree, and never found the tag to close
295
+ # so we'll just ignore the closing tag altogether
296
+ current_parent = original_parent
297
+ end
298
+ current_token_type = :unknown
299
+ current_token = ''
300
+ elsif char == "\r" or char == "\n"
301
+ current_parent << TextNode.new(current_parent, current_token)
302
+ current_token = char
303
+ current_token_type = :break
304
+ else
305
+ current_token_type = :text
306
+ current_token << char
307
+ end
308
+ end
309
+ end
310
+ # Handle whatever's left in the current token
311
+ if current_token_type != :break and !current_token.empty?
312
+ current_parent << TextNode.new(current_parent, current_token)
313
+ end
314
+ tree
315
+ end
316
+
317
+ def tag_name_char?(char_code)
318
+ (char_code >= LOWER_A_CODE and char_code <= LOWER_Z_CODE) or (char_code >= UPPER_A_CODE and char_code <= UPPER_Z_CODE) or char_code.chr == '*'
319
+ end
320
+ end
321
+ end
data/lib/rbbcode.rb ADDED
@@ -0,0 +1,6 @@
1
+ $: << File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'rbbcode/parser'
4
+ require 'rbbcode/schema'
5
+ require 'rbbcode/tree_maker'
6
+ require 'rbbcode/html_maker'
data/rbbcode.gemspec ADDED
@@ -0,0 +1,55 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{rbbcode}
5
+ s.version = "0.1.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Jarrett Colby"]
9
+ s.date = %q{2009-05-31}
10
+ s.description = %q{RbbCode is a customizable Ruby library for parsing BB Code. RbbCode validates and cleans input. It supports customizable schemas so you can set rules about what tags are allowed where. The default rules are designed to ensure valid HTML output.}
11
+ s.email = %q{jarrett@jarrettcolby.com}
12
+ s.extra_rdoc_files = [
13
+ "README"
14
+ ]
15
+ s.files = [
16
+ "MIT-LICENSE",
17
+ "README",
18
+ "VERSION",
19
+ "lib/rbbcode.rb",
20
+ "lib/rbbcode/html_maker.rb",
21
+ "lib/rbbcode/parser.rb",
22
+ "lib/rbbcode/schema.rb",
23
+ "lib/rbbcode/tree_maker.rb",
24
+ "rbbcode.gemspec",
25
+ "spec/html_maker_spec.rb",
26
+ "spec/node_spec_helper.rb",
27
+ "spec/parser_spec.rb",
28
+ "spec/schema_spec.rb",
29
+ "spec/spec_helper.rb",
30
+ "spec/tree_maker_spec.rb"
31
+ ]
32
+ s.homepage = %q{http://github.com/jarrett/rbbcode}
33
+ s.rdoc_options = ["--charset=UTF-8"]
34
+ s.require_paths = ["lib"]
35
+ s.rubygems_version = %q{1.3.3}
36
+ s.summary = %q{Ruby BB Code parser}
37
+ s.test_files = [
38
+ "spec/html_maker_spec.rb",
39
+ "spec/node_spec_helper.rb",
40
+ "spec/parser_spec.rb",
41
+ "spec/schema_spec.rb",
42
+ "spec/spec_helper.rb",
43
+ "spec/tree_maker_spec.rb"
44
+ ]
45
+
46
+ if s.respond_to? :specification_version then
47
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
48
+ s.specification_version = 3
49
+
50
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
51
+ else
52
+ end
53
+ else
54
+ end
55
+ end
@@ -0,0 +1,70 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/node_spec_helper')
3
+
4
+ describe RbbCode::HtmlMaker do
5
+ context '#make_html' do
6
+ def expect_html(expected_html, &block)
7
+ @html_maker.make_html(NodeBuilder.build(&block)).should == expected_html
8
+ end
9
+
10
+ before :each do
11
+ @html_maker = RbbCode::HtmlMaker.new
12
+ end
13
+
14
+ it 'should replace simple BB code tags with HTML tags' do
15
+ expect_html('<p>This is <strong>bold</strong> text</p>') do
16
+ tag('p') do
17
+ text 'This is '
18
+ tag('b') { text 'bold' }
19
+ text ' text'
20
+ end
21
+ end
22
+ end
23
+
24
+ it 'should work for nested tags' do
25
+ expect_html('<p>This is <strong>bold and <u>underlined</u></strong> text</p>') do
26
+ tag('p') do
27
+ text 'This is '
28
+ tag('b') do
29
+ text 'bold and '
30
+ tag('u') { text 'underlined' }
31
+ end
32
+ text ' text'
33
+ end
34
+ end
35
+ end
36
+
37
+ it 'should not allow JavaScript in URLs' do
38
+ urls = {
39
+ 'javascript:alert("foo");' => 'http://javascript%3Aalert("foo");',
40
+ 'j a v a script:alert("foo");' => 'http://j+a+v+a+script%3Aalert("foo");',
41
+ ' javascript:alert("foo");' => 'http://+javascript%3Aalert("foo");',
42
+ 'JavaScript:alert("foo");' => 'http://JavaScript%3Aalert("foo");' ,
43
+ "java\nscript:alert(\"foo\");" => 'http://java%0Ascript%3Aalert("foo");',
44
+ "java\rscript:alert(\"foo\");" => 'http://java%0Dscript%3Aalert("foo");'
45
+ }
46
+
47
+ # url tag
48
+ urls.each do |evil_url, clean_url|
49
+ expect_html("<p><a href=\"#{clean_url}\">foo</a></p>") do
50
+ tag('p') do
51
+ tag('url', evil_url) do
52
+ text 'foo'
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # img tag
59
+ urls.each do |evil_url, clean_url|
60
+ expect_html("<p><img src=\"#{clean_url}\" alt=\"\"/></p>") do
61
+ tag('p') do
62
+ tag('img') do
63
+ text evil_url
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,114 @@
1
+ module RbbCode
2
+ class RootNode
3
+ def == (other_node)
4
+ self.class == other_node.class and self.children == other_node.children
5
+ end
6
+
7
+ def print_tree(indent = 0)
8
+ output = ''
9
+ indent.times { output << " " }
10
+ output << 'ROOT'
11
+ children.each do |child|
12
+ output << "\n" << child.print_tree(indent + 1)
13
+ end
14
+ output << "\n/ROOT"
15
+ output
16
+ end
17
+ end
18
+
19
+ class TagNode
20
+ def == (other_node)
21
+ self.class == other_node.class and self.tag_name == other_node.tag_name and self.value == other_node.value and self.children == other_node.children
22
+ end
23
+
24
+ def print_tree(indent = 0)
25
+ output = ''
26
+ indent.times { output << " " }
27
+ if value.nil?
28
+ output << "[#{tag_name}]"
29
+ else
30
+ output << "[#{tag_name}=#{value}]"
31
+ end
32
+ children.each do |child|
33
+ output << "\n" << child.print_tree(indent + 1)
34
+ end
35
+ output << "\n"
36
+ indent.times { output << " " }
37
+ output << "[/#{tag_name}]"
38
+ output
39
+ end
40
+ end
41
+
42
+ class TextNode
43
+ def == (other_node)
44
+ self.class == other_node.class and self.text == other_node.text
45
+ end
46
+
47
+ def print_tree(indent = 0)
48
+ output = ''
49
+ indent.times { output << " " }
50
+ output << '"' << text << '"'
51
+ end
52
+ end
53
+ end
54
+
55
+ class NodeBuilder
56
+ include RbbCode
57
+
58
+ def self.build(&block)
59
+ builder = new
60
+ builder.instance_eval(&block)
61
+ builder.root
62
+ end
63
+
64
+ attr_reader :root
65
+
66
+ protected
67
+
68
+ def << (node)
69
+ @current_parent.children << node
70
+ end
71
+
72
+ def initialize
73
+ @root = RootNode.new
74
+ @current_parent = @root
75
+ end
76
+
77
+ def text(contents, &block)
78
+ self << TextNode.new(@current_parent, contents)
79
+ end
80
+
81
+ def tag(tag_name, value = nil, &block)
82
+ tag_node = TagNode.new(@current_parent, tag_name, value)
83
+ self << tag_node
84
+ original_parent = @current_parent
85
+ @current_parent = tag_node
86
+ instance_eval(&block)
87
+ @current_parent = original_parent
88
+ end
89
+ end
90
+
91
+ module NodeMatchers
92
+ class MatchNode
93
+ def initialize(expected_tree)
94
+ @expected_tree = expected_tree
95
+ end
96
+
97
+ def matches?(target)
98
+ @target = target
99
+ @target == @expected_tree
100
+ end
101
+
102
+ def failure_message
103
+ "Expected:\n\n#{@expected_tree.print_tree}\n\nbut got:\n\n#{@target.print_tree}"
104
+ end
105
+
106
+ def negative_failure_message
107
+ "Expected anything other than:\n\n#{@expected_tree.print_tree}"
108
+ end
109
+ end
110
+
111
+ def match_node(expected_node)
112
+ MatchNode.new(expected_node)
113
+ end
114
+ end
@@ -0,0 +1,75 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe RbbCode::Parser do
4
+ context '#parse_bb_code' do
5
+ before :each do
6
+ @parser = RbbCode::Parser.new
7
+ end
8
+
9
+ it 'should create paragraphs and line breaks' do
10
+ bb_code = "This is one paragraph.\n\nThis is another paragraph."
11
+ @parser.parse(bb_code).should == '<p>This is one paragraph.</p><p>This is another paragraph.</p>'
12
+ bb_code = "This is one line.\nThis is another line."
13
+ @parser.parse(bb_code).should == '<p>This is one line.<br/>This is another line.</p>'
14
+ end
15
+
16
+ it 'should turn [b] to <strong>' do
17
+ @parser.parse('This is [b]bold[/b] text').should == '<p>This is <strong>bold</strong> text</p>'
18
+ end
19
+
20
+ it 'should turn [i] to <em> by default' do
21
+ @parser.parse('This is [i]italic[/i] text').should == '<p>This is <em>italic</em> text</p>'
22
+ end
23
+
24
+ it 'should turn [u] to <u>' do
25
+ @parser.parse('This is [u]underlined[/u] text').should == '<p>This is <u>underlined</u> text</p>'
26
+ end
27
+
28
+ it 'should turn [url]http://google.com[/url] to a link' do
29
+ @parser.parse('Visit [url]http://google.com[/url] now').should == '<p>Visit <a href="http://google.com">http://google.com</a> now</p>'
30
+ end
31
+
32
+ it 'should turn [url=http://google.com]Google[/url] to a link' do
33
+ @parser.parse('Visit [url=http://google.com]Google[/url] now').should == '<p>Visit <a href="http://google.com">Google</a> now</p>'
34
+ end
35
+
36
+ it 'should turn [img] to <img>' do
37
+ @parser.parse('[img]http://example.com/image.jpg[/img]').should == '<p><img src="http://example.com/image.jpg" alt=""/></p>'
38
+ end
39
+
40
+ it 'should turn [code] to <code>' do
41
+ @parser.parse('Too bad [code]method_missing[/code] is rarely useful').should == '<p>Too bad <code>method_missing</code> is rarely useful</p>'
42
+ end
43
+
44
+ it 'should parse nested tags' do
45
+ @parser.parse('[b][i]This is bold-italic[/i][/b]').should == '<p><strong><em>This is bold-italic</em></strong></p>'
46
+ end
47
+
48
+ it 'should not put <p> tags around <ul> tags' do
49
+ @parser.parse("Text.\n\n[list]\n[*]Foo[/*]\n[*]Bar[/*]\n[/list]\n\nMore text.").should == '<p>Text.</p><ul><li>Foo</li><li>Bar</li></ul><p>More text.</p>'
50
+ end
51
+
52
+ it 'should ignore forbidden or unrecognized tags' do
53
+ @parser.parse('There is [foo]no such thing[/foo] as a foo tag').should == '<p>There is no such thing as a foo tag</p>'
54
+ end
55
+
56
+ it 'should recover gracefully from malformed or improperly matched tags' do
57
+ @parser.parse('This [i/]tag[/i] is malformed').should == '<p>This [i/]tag is malformed</p>'
58
+ @parser.parse('This [i]]tag[/i] is malformed').should == '<p>This <em>]tag</em> is malformed</p>'
59
+ @parser.parse('This [i]tag[[/i] is malformed').should == '<p>This <em>tag[</em> is malformed</p>'
60
+ @parser.parse('This [i]tag[//i] is malformed').should == '<p>This <em>tag[//i] is malformed</em></p>'
61
+ @parser.parse('This [[i]tag[/i] is malformed').should == '<p>This [<em>tag</em> is malformed</p>'
62
+ @parser.parse('This [i]tag[/i]] is malformed').should == '<p>This <em>tag</em>] is malformed</p>'
63
+ @parser.parse('This [i]i tag[i] is not properly matched').should == '<p>This <em>i tag is not properly matched</em></p>'
64
+ @parser.parse('This i tag[/i] is not properly matched').should == '<p>This i tag is not properly matched</p>'
65
+ end
66
+
67
+ it 'should escape < and >' do
68
+ @parser.parse('This is [i]italic[/i], but this it not <i>italic</i>.').should == '<p>This is <em>italic</em>, but this it not &lt;i&gt;italic&lt;/i&gt;.</p>'
69
+ end
70
+
71
+ it 'should work when the string begins with a tag' do
72
+ @parser.parse('[b]This is bold[/b]').should == '<p><strong>This is bold</strong></p>'
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,98 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe RbbCode::Schema do
4
+ before :each do
5
+ @schema = RbbCode::Schema.new
6
+ @schema.clear
7
+ @schema.allow_tags(*RbbCode::DEFAULT_ALLOWED_TAGS)
8
+ end
9
+
10
+ it 'should allow the default tags at the top level' do
11
+ schema = RbbCode::Schema.new
12
+ [
13
+ 'b',
14
+ 'i',
15
+ 'u',
16
+ 'url',
17
+ 'img',
18
+ 'code',
19
+ 'quote',
20
+ 'list'
21
+ ].each do |tag|
22
+ schema.tag(tag).valid_in_context?().should == true
23
+ end
24
+ end
25
+
26
+ it 'should not allow unknown tags' do
27
+ @schema.tag('foo').valid_in_context?().should == false
28
+ end
29
+
30
+ it 'should return a new SchemaTag object when tag is called' do
31
+ @schema.tag('b').should be_a(RbbCode::SchemaTag)
32
+ end
33
+
34
+ it 'should not allow nesting a tag when may_not_be_nested is called on it' do
35
+ @schema.tag('b').may_not_be_nested
36
+ @schema.tag('b').valid_in_context?('b').should == false
37
+ end
38
+
39
+ it 'should allow nesting a tag when may_be_nested is called on it' do
40
+ @schema.tag('b').may_not_be_nested
41
+ @schema.tag('b').may_be_nested
42
+ @schema.tag('b').valid_in_context?('b').should == true
43
+ end
44
+
45
+ it 'should not allow a tag to descend from another when forbidden by may_not_descend_from' do
46
+ @schema.tag('b').may_not_descend_from('u')
47
+ @schema.tag('b').valid_in_context?('u').should == false
48
+ end
49
+
50
+ it 'should allow a tag to descend from another when permitted by may_descend_from' do
51
+ @schema.tag('b').may_not_descend_from('u')
52
+ @schema.tag('b').may_descend_from('u')
53
+ @schema.tag('b').valid_in_context?('u').should == true
54
+ end
55
+
56
+ it 'should not allow a tag to descend from anything other than the tags specified in must_be_child_of' do
57
+ @schema.tag('b').must_be_child_of('u', 'quote')
58
+ @schema.tag('b').valid_in_context?('i').should == false
59
+ @schema.tag('b').valid_in_context?('u').should == true
60
+ @schema.tag('b').valid_in_context?('quote').should == true
61
+ end
62
+
63
+ it 'should allow a tag to descend from the one specified in must_be_child_of' do
64
+ @schema.tag('b').may_not_descend_from('u')
65
+ @schema.tag('b').must_be_child_of('u')
66
+ @schema.tag('b').valid_in_context?('u').should == true
67
+ end
68
+
69
+ it 'should not require a tag to be a child of another when need_not_be_child_of is called' do
70
+ @schema.tag('b').must_be_child_of('u')
71
+ @schema.tag('b').need_not_be_child_of('u')
72
+ @schema.tag('b').valid_in_context?('i').should == true
73
+ end
74
+
75
+ it 'should allow only the specified tag as a child when may_only_be_parent_of is called' do
76
+ @schema.tag('list').may_only_be_parent_of('*')
77
+ @schema.tag('*').valid_in_context?('list').should == true
78
+ @schema.tag('u').valid_in_context?('list').should == false
79
+ @schema.tag('u').valid_in_context?('*', 'list').should == true
80
+ end
81
+
82
+ it 'should not allow text inside a tag when may_not_contain_text is called' do
83
+ @schema.tag('list').may_not_contain_text
84
+ @schema.text.valid_in_context?('list').should == false
85
+ end
86
+
87
+ it 'should allow text inside a tag when may_contain_text is called' do
88
+ @schema.tag('list').may_not_contain_text
89
+ @schema.tag('list').may_contain_text
90
+ @schema.text.valid_in_context?('list').should == true
91
+ end
92
+
93
+ it 'should not allow text or children when must_be_empty is called' do
94
+ @schema.tag('br').must_be_empty
95
+ @schema.text.valid_in_context?('br').should == false
96
+ @schema.tag('b').valid_in_context?('br').should == false
97
+ end
98
+ end
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'spec'
4
+
5
+ def puts(foo)
6
+ raise 'puts called'
7
+ end
8
+
9
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/rbbcode')
@@ -0,0 +1,107 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/node_spec_helper')
3
+ require 'pp'
4
+
5
+ describe RbbCode::TreeMaker do
6
+ include NodeMatchers
7
+
8
+ context '#make_tree' do
9
+ def expect_tree(str, &block)
10
+ expected = NodeBuilder.build(&block)
11
+ @tree_maker.make_tree(str).should match_node(expected)
12
+ end
13
+
14
+ before :each do
15
+ @schema = RbbCode::Schema.new
16
+ @tree_maker = RbbCode::TreeMaker.new(@schema)
17
+ end
18
+
19
+ it 'should make a tree from a string with one tag' do
20
+ str = 'This is [b]bold[/b] text'
21
+
22
+ expect_tree(str) do
23
+ tag('p') do
24
+ text 'This is '
25
+ tag('b') { text 'bold' }
26
+ text ' text'
27
+ end
28
+ end
29
+ end
30
+
31
+ it 'should ignore tags that are invalid in their context' do
32
+ @schema.tag('u').may_not_descend_from('b')
33
+
34
+ str = 'This is [b]bold and [u]underlined[/u][/b] text'
35
+
36
+ expect_tree(str) do
37
+ tag('p') do
38
+ text 'This is '
39
+ tag('b') do
40
+ text 'bold and '
41
+ text 'underlined'
42
+ end
43
+ text ' text'
44
+ end
45
+ end
46
+ end
47
+
48
+ it 'should create paragraph tags' do
49
+ str = "This is a paragraph.\n\nThis is another."
50
+
51
+ expect_tree(str) do
52
+ tag('p') do
53
+ text 'This is a paragraph.'
54
+ end
55
+ tag('p') do
56
+ text 'This is another.'
57
+ end
58
+ end
59
+ end
60
+
61
+ it 'should not put block-level elements inside paragraph tags' do
62
+ str = "This is a list:\n\n[list]\n\n[*]Foo[/i]\n\n[/list]\n\nwith some text after it"
63
+
64
+ expect_tree(str) do
65
+ tag('p') do
66
+ text 'This is a list:'
67
+ end
68
+ tag('list') do
69
+ tag('*') { text 'Foo' }
70
+ end
71
+ tag('p') do
72
+ text 'with some text after it'
73
+ end
74
+ end
75
+ end
76
+
77
+ it 'should not insert br tags in the midst of block-level elements' do
78
+ str = "List:\n[list]\n[*]Foo[/*]\n[*]Bar[/*]\n[/list]\nText after list"
79
+
80
+ expect_tree(str) do
81
+ tag('p') do
82
+ text 'List:'
83
+ end
84
+ tag('list') do
85
+ tag('*') { text 'Foo' }
86
+ tag('*') { text 'Bar' }
87
+ end
88
+ tag('p') do
89
+ text 'Text after list'
90
+ end
91
+ end
92
+ end
93
+
94
+ it 'should store tag values' do
95
+ str = 'This is a [url=http://google.com]link[/url]'
96
+
97
+ expect_tree(str) do
98
+ tag('p') do
99
+ text 'This is a '
100
+ tag('url', 'http://google.com') do
101
+ text 'link'
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jarrett-rbbcode
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jarrett Colby
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-31 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: RbbCode is a customizable Ruby library for parsing BB Code. RbbCode validates and cleans input. It supports customizable schemas so you can set rules about what tags are allowed where. The default rules are designed to ensure valid HTML output.
17
+ email: jarrett@jarrettcolby.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ files:
25
+ - MIT-LICENSE
26
+ - README
27
+ - VERSION
28
+ - lib/rbbcode.rb
29
+ - lib/rbbcode/html_maker.rb
30
+ - lib/rbbcode/parser.rb
31
+ - lib/rbbcode/schema.rb
32
+ - lib/rbbcode/tree_maker.rb
33
+ - rbbcode.gemspec
34
+ - spec/html_maker_spec.rb
35
+ - spec/node_spec_helper.rb
36
+ - spec/parser_spec.rb
37
+ - spec/schema_spec.rb
38
+ - spec/spec_helper.rb
39
+ - spec/tree_maker_spec.rb
40
+ has_rdoc: false
41
+ homepage: http://github.com/jarrett/rbbcode
42
+ post_install_message:
43
+ rdoc_options:
44
+ - --charset=UTF-8
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.2.0
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: Ruby BB Code parser
66
+ test_files:
67
+ - spec/html_maker_spec.rb
68
+ - spec/node_spec_helper.rb
69
+ - spec/parser_spec.rb
70
+ - spec/schema_spec.rb
71
+ - spec/spec_helper.rb
72
+ - spec/tree_maker_spec.rb