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 +21 -0
- data/README +13 -0
- data/VERSION +1 -0
- data/lib/rbbcode/html_maker.rb +93 -0
- data/lib/rbbcode/parser.rb +25 -0
- data/lib/rbbcode/schema.rb +235 -0
- data/lib/rbbcode/tree_maker.rb +321 -0
- data/lib/rbbcode.rb +6 -0
- data/rbbcode.gemspec +55 -0
- data/spec/html_maker_spec.rb +70 -0
- data/spec/node_spec_helper.rb +114 -0
- data/spec/parser_spec.rb +75 -0
- data/spec/schema_spec.rb +98 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/tree_maker_spec.rb +107 -0
- metadata +72 -0
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('<', '<').gsub('>', '>')
|
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
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
|
data/spec/parser_spec.rb
ADDED
@@ -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 <i>italic</i>.</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
|
data/spec/schema_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|