dtext_rb 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ require "mkmf"
2
+
3
+ pkg_config "glib-2.0"
4
+
5
+ have_library "glib-2.0"
6
+ have_header "glib.h"
7
+ have_header "dtext.h"
8
+ create_makefile "dtext/dtext"
data/lib/dtext.rb ADDED
@@ -0,0 +1 @@
1
+ require "dtext/dtext"
data/lib/dtext_ruby.rb ADDED
@@ -0,0 +1,326 @@
1
+ require 'cgi'
2
+ require 'uri'
3
+
4
+ class DTextRuby
5
+ MENTION_REGEXP = /(?<=^| )@\S+/
6
+
7
+ def self.u(string)
8
+ CGI.escape(string)
9
+ end
10
+
11
+ def self.h(string)
12
+ CGI.escapeHTML(string)
13
+ end
14
+
15
+ def self.strip_blocks(string, tag)
16
+ blocks = string.scan(/\[\/?#{tag}\]|.+?(?=\[\/?#{tag}\]|$)/m)
17
+ n = 0
18
+ stripped = ""
19
+ blocks.each do |block|
20
+ case block
21
+ when "[#{tag}]"
22
+ n += 1
23
+
24
+ when "[/#{tag}]"
25
+ n -= 1
26
+
27
+ else
28
+ if n == 0
29
+ stripped += block
30
+ end
31
+ end
32
+ end
33
+
34
+ stripped.strip
35
+ end
36
+
37
+ def self.parse_inline(str, options = {})
38
+ str.gsub!(/&/, "&amp;")
39
+ str.gsub!(/</, "&lt;")
40
+ str.gsub!(/>/, "&gt;")
41
+ str.gsub!(/\n/m, "<br>") unless options[:ignore_newlines]
42
+ str.gsub!(/\[b\](.+?)\[\/b\]/i, '<strong>\1</strong>')
43
+ str.gsub!(/\[i\](.+?)\[\/i\]/i, '<em>\1</em>')
44
+ str.gsub!(/\[s\](.+?)\[\/s\]/i, '<s>\1</s>')
45
+ str.gsub!(/\[u\](.+?)\[\/u\]/i, '<u>\1</u>')
46
+ str.gsub!(/\[tn\](.+?)\[\/tn\]/i, '<p class="tn">\1</p>')
47
+
48
+ str = parse_mentions(str)
49
+ str = parse_links(str)
50
+ str = parse_aliased_wiki_links(str)
51
+ str = parse_wiki_links(str)
52
+ str = parse_post_links(str)
53
+ str = parse_id_links(str)
54
+ str
55
+ end
56
+
57
+ def self.parse_mentions(str)
58
+ str.gsub!(MENTION_REGEXP) do |name|
59
+ next name unless name =~ /[a-z0-9]/i
60
+
61
+ if name =~ /([:;,.!?\)\]<>])$/
62
+ name.chop!
63
+ ch = $1
64
+ else
65
+ ch = ""
66
+ end
67
+
68
+ '<a href="/users?name=' + u(CGI.unescapeHTML(name[1..-1])) + '">' + name + '</a>' + ch
69
+ end
70
+ str
71
+ end
72
+
73
+ def self.parse_table_elements(str)
74
+ str = parse_inline(str, :ignore_newlines => true)
75
+ str.gsub!(/\[(\/?(?:tr|td|th|thead|tbody))\]/, '<\1>')
76
+ str
77
+ end
78
+
79
+ def self.parse_links(str)
80
+ str.gsub(/("[^"]+":(https?:\/\/|\/)[^\s\r\n<>]+|https?:\/\/[^\s\r\n<>]+|"[^"]+":\[(https?:\/\/|\/)[^\s\r\n<>\]]+\])+/) do |url|
81
+ ch = ""
82
+
83
+ if url =~ /^"([^"]+)":\[(.+)\]$/
84
+ text = $1
85
+ url = $2
86
+ else
87
+ if url =~ /^"([^"]+)":(.+)$/
88
+ text = $1
89
+ url = $2
90
+ else
91
+ text = url
92
+ end
93
+
94
+ if url =~ /([;,.!?\)\]<>])$/
95
+ url.chop!
96
+ ch = $1
97
+ end
98
+ end
99
+
100
+ '<a href="' + url + '">' + text + '</a>' + ch
101
+ end
102
+ end
103
+
104
+ def self.parse_aliased_wiki_links(str)
105
+ str.gsub(/\[\[([^\|\]]+)\|([^\]]+)\]\]/m) do
106
+ text = CGI.unescapeHTML($2)
107
+ title = CGI.unescapeHTML($1).tr(" ", "_").downcase
108
+ %{<a href="/wiki_pages/show_or_new?title=#{u(title)}">#{h(text)}</a>}
109
+ end
110
+ end
111
+
112
+ def self.parse_wiki_links(str)
113
+ str.gsub(/\[\[([^\]]+)\]\]/) do
114
+ text = CGI.unescapeHTML($1)
115
+ title = text.tr(" ", "_").downcase
116
+ %{<a href="/wiki_pages/show_or_new?title=#{u(title)}">#{h(text)}</a>}
117
+ end
118
+ end
119
+
120
+ def self.parse_post_links(str)
121
+ str.gsub(/\{\{([^\}]+)\}\}/) do
122
+ tags = CGI.unescapeHTML($1)
123
+ %{<a rel="nofollow" href="/posts?tags=#{u(tags)}">#{h(tags)}</a>}
124
+ end
125
+ end
126
+
127
+ def self.parse_id_links(str)
128
+ str = str.gsub(/\bpost #(\d+)/i, %{<a href="/posts/\\1">post #\\1</a>})
129
+ str = str.gsub(/\bforum #(\d+)/i, %{<a href="/forum_posts/\\1">forum #\\1</a>})
130
+ str = str.gsub(/\btopic #(\d+)(?!\/p\d|\d)/i, %{<a href="/forum_topics/\\1">topic #\\1</a>})
131
+ str = str.gsub(/\btopic #(\d+)\/p(\d+)/i, %{<a href="/forum_topics/\\1?page=\\2">topic #\\1/p\\2</a>})
132
+ str = str.gsub(/\bcomment #(\d+)/i, %{<a href="/comments/\\1">comment #\\1</a>})
133
+ str = str.gsub(/\bpool #(\d+)/i, %{<a href="/pools/\\1">pool #\\1</a>})
134
+ str = str.gsub(/\buser #(\d+)/i, %{<a href="/users/\\1">user #\\1</a>})
135
+ str = str.gsub(/\bartist #(\d+)/i, %{<a href="/artists/\\1">artist #\\1</a>})
136
+ str = str.gsub(/\bissue #(\d+)/i, %{<a href="https://github.com/r888888888/danbooru/issues/\\1">issue #\\1</a>})
137
+ str = str.gsub(/\bpixiv #(\d+)(?!\/p\d|\d)/i, %{<a href="http://www.pixiv.net/member_illust.php?mode=medium&illust_id=\\1">pixiv #\\1</a>})
138
+ str = str.gsub(/\bpixiv #(\d+)\/p(\d+)/i, %{<a href="http://www.pixiv.net/member_illust.php?mode=manga_big&illust_id=\\1&page=\\2">pixiv #\\1/p\\2</a>})
139
+ end
140
+
141
+ def self.parse_list(str, options = {})
142
+ html = ""
143
+ current_item = ""
144
+ layout = []
145
+ nest = 0
146
+
147
+ str.split(/\n/).each do |line|
148
+ if line =~ /^\s*(\*+) (.+)/
149
+ if nest > 0
150
+ html += "<li>#{current_item}</li>"
151
+ elsif not current_item.strip.empty?
152
+ html += "<p>#{current_item}</p>"
153
+ end
154
+
155
+ nest = $1.size
156
+ current_item = parse_inline($2)
157
+ else
158
+ current_item += parse_inline(line)
159
+ end
160
+
161
+ if nest > layout.size
162
+ html += "<ul>"
163
+ layout << "ul"
164
+ end
165
+
166
+ while nest < layout.size
167
+ elist = layout.pop
168
+ if elist
169
+ html += "</#{elist}>"
170
+ end
171
+ end
172
+ end
173
+
174
+ html += "<li>#{current_item}</li>"
175
+
176
+ while layout.any?
177
+ elist = layout.pop
178
+ html += "</#{elist}>"
179
+ end
180
+
181
+ html
182
+ end
183
+
184
+ def self.parse(str, options = {})
185
+ return "" if str.nil?
186
+
187
+ # Make sure quote tags are surrounded by newlines
188
+
189
+ unless options[:inline]
190
+ str.gsub!(/\s*\[quote\](?!\])\s*/m, "\n\n[quote]\n\n")
191
+ str.gsub!(/\s*\[\/quote\]\s*/m, "\n\n[/quote]\n\n")
192
+ str.gsub!(/\s*\[code\](?!\])/m, "\n\n[code]\n\n")
193
+ str.gsub!(/\[\/code\]\s*/m, "\n\n[/code]\n\n")
194
+ str.gsub!(/\s*\[spoilers?\](?!\])\s*/m, "\n\n[spoiler]\n\n")
195
+ str.gsub!(/\s*\[\/spoilers?\]\s*/m, "\n\n[/spoiler]\n\n")
196
+ str.gsub!(/^(h[1-6]\.\s*.+)$/, "\n\n\\1\n\n")
197
+ str.gsub!(/\s*\[expand(\=[^\]]*)?\](?!\])\s*/m, "\n\n[expand\\1]\n\n")
198
+ str.gsub!(/\s*\[\/expand\]\s*/m, "\n\n[/expand]\n\n")
199
+ str.gsub!(/\s*\[table\](?!\])\s*/m, "\n\n[table]\n\n")
200
+ str.gsub!(/\s*\[\/table\]\s*/m, "\n\n[/table]\n\n")
201
+ end
202
+
203
+ str.gsub!(/(?:\r?\n){3,}/, "\n\n")
204
+ str.strip!
205
+ blocks = str.split(/(?:\r?\n){2}/)
206
+ stack = []
207
+ flags = {}
208
+
209
+ html = blocks.map do |block|
210
+ case block
211
+ when /\A(h[1-6])\.\s*(.+)\Z/
212
+ tag = $1
213
+ content = $2
214
+
215
+ if options[:inline]
216
+ "<h6>" + parse_inline(content, options) + "</h6>"
217
+ else
218
+ "<#{tag}>" + parse_inline(content, options) + "</#{tag}>"
219
+ end
220
+
221
+ when /^\s*\*+ /
222
+ parse_list(block, options)
223
+
224
+ when "[quote]"
225
+ if options[:inline]
226
+ ""
227
+ else
228
+ stack << "blockquote"
229
+ "<blockquote>"
230
+ end
231
+
232
+ when "[/quote]"
233
+ if options[:inline]
234
+ ""
235
+ elsif stack.last == "blockquote"
236
+ stack.pop
237
+ '</blockquote>'
238
+ else
239
+ ""
240
+ end
241
+
242
+ when "[spoiler]"
243
+ stack << "spoiler"
244
+ '<div class="spoiler">'
245
+
246
+ when "[/spoiler]"
247
+ if stack.last == "spoiler"
248
+ stack.pop
249
+ "</div>"
250
+ else
251
+ ""
252
+ end
253
+
254
+ when "[table]"
255
+ stack << "table"
256
+ flags[:table] = true
257
+ '<table class="striped">'
258
+
259
+ when "[/table]"
260
+ if stack.last == "table"
261
+ stack.pop
262
+ flags[:table] = false
263
+ "</table>"
264
+ else
265
+ ""
266
+ end
267
+
268
+ when /\[code\](?!\])/
269
+ flags[:code] = true
270
+ stack << "pre"
271
+ '<pre>'
272
+
273
+ when /\[\/code\](?!\])/
274
+ flags[:code] = false
275
+ if stack.last == "pre"
276
+ stack.pop
277
+ "</pre>"
278
+ else
279
+ ""
280
+ end
281
+
282
+ when /\[expand(?:\=([^\]]*))?\](?!\])/
283
+ stack << "expandable"
284
+ expand_html = '<div class="expandable"><div class="expandable-header">'
285
+ expand_html << "<span>#{h($1)}</span>" if $1.present?
286
+ expand_html << '<input type="button" value="Show" class="expandable-button"/></div>'
287
+ expand_html << '<div class="expandable-content">'
288
+ expand_html
289
+
290
+ when /\[\/expand\](?!\])/
291
+ if stack.last == "expandable"
292
+ stack.pop
293
+ '</div></div>'
294
+ end
295
+
296
+ else
297
+ if flags[:code]
298
+ CGI.escape_html(block) + "\n\n"
299
+ elsif flags[:table]
300
+ parse_table_elements(block)
301
+ else
302
+ '<p>' + parse_inline(block) + '</p>'
303
+ end
304
+ end
305
+ end
306
+
307
+ stack.reverse.each do |tag|
308
+ if tag == "blockquote"
309
+ html << "</blockquote>"
310
+ elsif tag == "div"
311
+ html << "</div>"
312
+ elsif tag == "pre"
313
+ html << "</pre>"
314
+ elsif tag == "spoiler"
315
+ html << "</div>"
316
+ elsif tag == "expandable"
317
+ html << "</div></div>"
318
+ elsif tag == "table"
319
+ html << "</table>"
320
+ end
321
+ end
322
+
323
+ html.join("")
324
+ end
325
+ end
326
+
@@ -0,0 +1,211 @@
1
+ require 'minitest/autorun'
2
+ require 'dtext/dtext'
3
+
4
+ class DTextTest < Minitest::Test
5
+ def assert_parse(expected, input)
6
+ assert_equal(expected, DTextRagel.parse(input))
7
+ end
8
+
9
+ def test_mentions
10
+ assert_parse('<p><a rel="nofollow" href="/users?name=bob">@bob</a></p>', "@bob")
11
+ assert_parse('<p>hi <a rel="nofollow" href="/users?name=bob">@bob</a></p>', "hi @bob")
12
+ assert_parse('<p>this is not @.@ @_@ <a rel="nofollow" href="/users?name=bob">@bob</a></p>', "this is not @.@ @_@ @bob")
13
+ assert_parse('<p>multiple <a rel="nofollow" href="/users?name=bob">@bob</a> <a rel="nofollow" href="/users?name=anna">@anna</a></p>', "multiple @bob @anna")
14
+ end
15
+
16
+ def test_sanitize_heart
17
+ assert_parse('<p>&lt;3</p>', "<3")
18
+ end
19
+
20
+ def test_sanitize_less_than
21
+ assert_parse('<p>&lt;</p>', "<")
22
+ end
23
+
24
+ def test_sanitize_greater_than
25
+ assert_parse('<p>&gt;</p>', ">")
26
+ end
27
+
28
+ def test_sanitize_ampersand
29
+ assert_parse('<p>&amp;</p>', "&")
30
+ end
31
+
32
+ def test_wiki_links
33
+ assert_parse("<p>a <a href=\"/wiki_pages/show_or_new?title=b\">b</a> c</p>", "a [[b]] c")
34
+ end
35
+
36
+ def test_wiki_links_spoiler
37
+ assert_parse("<p>a <a href=\"/wiki_pages/show_or_new?title=spoiler\">spoiler</a> c</p>", "a [[spoiler]] c")
38
+ end
39
+
40
+ def test_spoilers_inline
41
+ assert_parse("<p>this is <span class=\"spoiler\">an inline spoiler</span>.</p>", "this is [spoiler]an inline spoiler[/spoiler].")
42
+ end
43
+
44
+ def test_spoilers_block
45
+ assert_parse("<p>this is</p><div class=\"spoiler\"><p>a block spoiler</p></div><p>.</p>", "this is\n\n[spoiler]\na block spoiler\n[/spoiler].")
46
+ end
47
+
48
+ def test_spoilers_with_no_closing_tag_1
49
+ assert_parse("<div class=\"spoiler\"><p>this is a spoiler with no closing tag</p><p>new text</p></div>", "[spoiler]this is a spoiler with no closing tag\n\nnew text")
50
+ end
51
+
52
+ def test_spoilers_with_no_closing_tag_2
53
+ assert_parse("<div class=\"spoiler\"><p>this is a spoiler with no closing tag<br>new text</p></div>", "[spoiler]this is a spoiler with no closing tag\nnew text")
54
+ end
55
+
56
+ def test_spoilers_with_no_closing_tag_block
57
+ assert_parse("<div class=\"spoiler\"><p>this is a block spoiler with no closing tag</p></div>", "[spoiler]\nthis is a block spoiler with no closing tag")
58
+ end
59
+
60
+ def test_spoilers_nested
61
+ assert_parse("<div class=\"spoiler\"><p>this is <span class=\"spoiler\">a nested</span> spoiler</p></div>", "[spoiler]this is [spoiler]a nested[/spoiler] spoiler[/spoiler]")
62
+ end
63
+
64
+ def test_paragraphs
65
+ assert_parse("<p>abc</p>", "abc")
66
+ end
67
+
68
+ def test_paragraphs_with_newlines_1
69
+ assert_parse("<p>a<br>b<br>c</p>", "a\nb\nc")
70
+ end
71
+
72
+ def test_paragraphs_with_newlines_2
73
+ assert_parse("<p>a</p><p>b</p>", "a\n\nb")
74
+ end
75
+
76
+ def test_headers
77
+ assert_parse("<h1>header</h1>", "h1. header")
78
+ end
79
+
80
+ def test_quote_blocks
81
+ assert_parse('<blockquote><p>test</p></blockquote>', "[quote]\ntest\n[/quote]")
82
+ end
83
+
84
+ def test_quote_blocks_nested
85
+ assert_parse("<blockquote><p>a</p><blockquote><p>b</p></blockquote><p>c</p></blockquote>", "[quote]\na\n[quote]\nb\n[/quote]\nc\n[/quote]")
86
+ end
87
+
88
+ def test_quote_blocks_nested_spoiler
89
+ assert_parse("<blockquote><p>a<br><span class=\"spoiler\">blah</span><br>c</p></blockquote>", "[quote]\na\n[spoiler]blah[/spoiler]\nc[/quote]")
90
+ assert_parse("<blockquote><p>a</p><div class=\"spoiler\"><p>blah</p></div><p>c</p></blockquote>", "[quote]\na\n\n[spoiler]blah[/spoiler]\n\nc[/quote]")
91
+ end
92
+
93
+ def test_quote_blocks_nested_expand
94
+ assert_parse("<blockquote><p>a</p><div class=\"expandable\"><div class=\"expandable-header\"><input type=\"button\" value=\"Show\" class=\"expandable-button\"/></div><div class=\"expandable-content\"><p>b</p></div></div><p>c</p></blockquote>", "[quote]\na\n[expand]\nb\n[/expand]\nc\n[/quote]")
95
+ end
96
+
97
+ def test_code
98
+ assert_parse("<pre>for (i=0; i&lt;5; ++i) {\n printf(1);\n}\n\nexit(1);</pre>", "[code]for (i=0; i<5; ++i) {\n printf(1);\n}\n\nexit(1);")
99
+ end
100
+
101
+ def test_urls
102
+ assert_parse('<p>a <a href="http://test.com">http://test.com</a> b</p>', p('a http://test.com b'))
103
+ end
104
+
105
+ def test_urls_with_newline
106
+ assert_parse('<p><a href="http://test.com">http://test.com</a><br>b</p>', "http://test.com\nb")
107
+ end
108
+
109
+ def test_urls_with_paths
110
+ assert_parse('<p>a <a href="http://test.com/~bob/image.jpg">http://test.com/~bob/image.jpg</a> b</p>', p('a http://test.com/~bob/image.jpg b'))
111
+ end
112
+
113
+ def test_urls_with_fragment
114
+ assert_parse('<p>a <a href="http://test.com/home.html#toc">http://test.com/home.html#toc</a> b</p>', p('a http://test.com/home.html#toc b'))
115
+ end
116
+
117
+ def test_auto_urls
118
+ assert_parse('<p>a <a href="http://test.com">http://test.com</a>. b</p>', p('a http://test.com. b'))
119
+ end
120
+
121
+ def test_auto_urls_in_parentheses
122
+ assert_parse('<p>a (<a href="http://test.com">http://test.com</a>) b</p>', p('a (http://test.com) b'))
123
+ end
124
+
125
+ def test_old_style_links
126
+ assert_parse('<p><a href="http://test.com">test</a></p>', p('"test":http://test.com'))
127
+ end
128
+
129
+ def test_old_style_links_with_special_entities
130
+ assert_parse('<p>"1" <a href="http://three.com">2 &amp; 3</a></p>', p('"1" "2 & 3":http://three.com'))
131
+ end
132
+
133
+ def test_new_style_links
134
+ assert_parse('<p><a href="http://test.com">test</a></p>', p('"test":[http://test.com]'))
135
+ end
136
+
137
+ def test_new_style_links_with_parentheses
138
+ assert_parse('<p><a href="http://test.com/(parentheses)">test</a></p>', p('"test":[http://test.com/(parentheses)]'))
139
+ assert_parse('<p>(<a href="http://test.com/(parentheses)">test</a>)</p>', p('("test":[http://test.com/(parentheses)])'))
140
+ assert_parse('<p>[<a href="http://test.com/(parentheses)">test</a>]</p>', p('["test":[http://test.com/(parentheses)]]'))
141
+ end
142
+
143
+ def test_lists_1
144
+ assert_parse('<ul><li>a</li></ul>', p('* a'))
145
+ end
146
+
147
+ def test_lists_2
148
+ assert_parse('<ul><li>a</li><li>b</li></ul>', "* a\n* b")
149
+ end
150
+
151
+ def test_lists_nested
152
+ assert_parse('<ul><li>a</li><ul><li>b</li></ul></ul>', "* a\n** b")
153
+ end
154
+
155
+ def test_lists_inline
156
+ assert_parse('<ul><li><a href="/posts/1">post #1</a></li></ul>', "* post #1")
157
+ end
158
+
159
+ def test_lists_not_preceded_by_newline
160
+ assert_parse('<p>a<br>b</p><ul><li>c</li><li>d</li></ul>', "a\nb\n* c\n* d")
161
+ end
162
+
163
+ def test_lists_with_multiline_items
164
+ assert_parse('<p>a</p><ul><li>b<br>c</li><li>d<br>e</li></ul><p>another one</p>', "a\n* b\nc\n* d\ne\n\nanother one")
165
+ assert_parse('<p>a</p><ul><li>b<br>c</li><ul><li>d<br>e</li></ul></ul><p>another one</p>', "a\n* b\nc\n** d\ne\n\nanother one")
166
+ end
167
+
168
+ def test_inline_tags
169
+ assert_parse('<p><a rel="nofollow" href="/posts?tags=tag">tag</a></p>', "{{tag}}")
170
+ end
171
+
172
+ def test_inline_tags_conjunction
173
+ assert_parse('<p><a rel="nofollow" href="/posts?tags=tag1%20tag2">tag1 tag2</a></p>', "{{tag1 tag2}}")
174
+ end
175
+
176
+ def test_inline_tags_special_entities
177
+ assert_parse('<p><a rel="nofollow" href="/posts?tags=%3C3">&lt;3</a></p>', "{{<3}}")
178
+ end
179
+
180
+ def test_extra_newlines
181
+ assert_parse('<p>a</p><p>b</p>', "a\n\n\n\n\n\n\nb\n\n\n\n")
182
+ end
183
+
184
+ def test_complex_links_1
185
+ assert_parse("<p><a href=\"/wiki_pages/show_or_new?title=1\">2 3</a> | <a href=\"/wiki_pages/show_or_new?title=4\">5 6</a></p>", "[[1|2 3]] | [[4|5 6]]")
186
+ end
187
+
188
+ def test_complex_links_2
189
+ assert_parse("<p>Tags <strong>(<a href=\"/wiki_pages/show_or_new?title=howto:tag\">Tagging Guidelines</a> | <a href=\"/wiki_pages/show_or_new?title=howto:tag_checklist\">Tag Checklist</a> | <a href=\"/wiki_pages/show_or_new?title=tag_groups\">Tag Groups</a>)</strong></p>", "Tags [b]([[howto:tag|Tagging Guidelines]] | [[howto:tag_checklist|Tag Checklist]] | [[Tag Groups]])[/b]")
190
+ end
191
+
192
+ def test_table
193
+ assert_parse("<table class=\"striped\"><thead><tr><th>header</th></tr></thead><tbody><tr><td><a href=\"/posts/100\">post #100</a></td></tr></tbody></table>", "[table][thead][tr][th]header[/th][/tr][/thead][tbody][tr][td]post #100[/td][/tr][/tbody][/table]")
194
+ end
195
+
196
+ def test_table_with_newlines
197
+ assert_parse("<table class=\"striped\"><thead><tr><th>header</th></tr></thead><tbody><tr><td><a href=\"/posts/100\">post #100</a></td></tr></tbody></table>", "[table]\n[thead]\n[tr]\n[th]header[/th][/tr][/thead][tbody][tr][td]post #100[/td][/tr][/tbody][/table]")
198
+ end
199
+
200
+ def test_forum_links
201
+ assert_parse('<p><a href="/forum_topics/1234?page=4">topic #1234/p4</a></p>', "topic #1234/p4")
202
+ end
203
+
204
+ def test_boundary_exploit
205
+ assert_parse('<p><a rel="nofollow" href="/users?name=mack">@mack</a>&lt;</p>', "@mack<")
206
+ end
207
+
208
+ def test_inline_mode
209
+ assert_equal("hello", DTextRagel.parse("hello", :inline => true).strip)
210
+ end
211
+ end