dtext_rb 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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