##// END OF EJS Templates
Escape textile titles and styles (#2377)....
Jean-Philippe Lang -
r2168:7776b5b6659e
parent child
Show More
@@ -1,1172 +1,1175
1 # vim:ts=4:sw=4:
1 # vim:ts=4:sw=4:
2 # = RedCloth - Textile and Markdown Hybrid for Ruby
2 # = RedCloth - Textile and Markdown Hybrid for Ruby
3 #
3 #
4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
7 # License:: BSD
7 # License:: BSD
8 #
8 #
9 # (see http://hobix.com/textile/ for a Textile Reference.)
9 # (see http://hobix.com/textile/ for a Textile Reference.)
10 #
10 #
11 # Based on (and also inspired by) both:
11 # Based on (and also inspired by) both:
12 #
12 #
13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
14 # Textism for PHP: http://www.textism.com/tools/textile/
14 # Textism for PHP: http://www.textism.com/tools/textile/
15 #
15 #
16 #
16 #
17
17
18 # = RedCloth
18 # = RedCloth
19 #
19 #
20 # RedCloth is a Ruby library for converting Textile and/or Markdown
20 # RedCloth is a Ruby library for converting Textile and/or Markdown
21 # into HTML. You can use either format, intermingled or separately.
21 # into HTML. You can use either format, intermingled or separately.
22 # You can also extend RedCloth to honor your own custom text stylings.
22 # You can also extend RedCloth to honor your own custom text stylings.
23 #
23 #
24 # RedCloth users are encouraged to use Textile if they are generating
24 # RedCloth users are encouraged to use Textile if they are generating
25 # HTML and to use Markdown if others will be viewing the plain text.
25 # HTML and to use Markdown if others will be viewing the plain text.
26 #
26 #
27 # == What is Textile?
27 # == What is Textile?
28 #
28 #
29 # Textile is a simple formatting style for text
29 # Textile is a simple formatting style for text
30 # documents, loosely based on some HTML conventions.
30 # documents, loosely based on some HTML conventions.
31 #
31 #
32 # == Sample Textile Text
32 # == Sample Textile Text
33 #
33 #
34 # h2. This is a title
34 # h2. This is a title
35 #
35 #
36 # h3. This is a subhead
36 # h3. This is a subhead
37 #
37 #
38 # This is a bit of paragraph.
38 # This is a bit of paragraph.
39 #
39 #
40 # bq. This is a blockquote.
40 # bq. This is a blockquote.
41 #
41 #
42 # = Writing Textile
42 # = Writing Textile
43 #
43 #
44 # A Textile document consists of paragraphs. Paragraphs
44 # A Textile document consists of paragraphs. Paragraphs
45 # can be specially formatted by adding a small instruction
45 # can be specially formatted by adding a small instruction
46 # to the beginning of the paragraph.
46 # to the beginning of the paragraph.
47 #
47 #
48 # h[n]. Header of size [n].
48 # h[n]. Header of size [n].
49 # bq. Blockquote.
49 # bq. Blockquote.
50 # # Numeric list.
50 # # Numeric list.
51 # * Bulleted list.
51 # * Bulleted list.
52 #
52 #
53 # == Quick Phrase Modifiers
53 # == Quick Phrase Modifiers
54 #
54 #
55 # Quick phrase modifiers are also included, to allow formatting
55 # Quick phrase modifiers are also included, to allow formatting
56 # of small portions of text within a paragraph.
56 # of small portions of text within a paragraph.
57 #
57 #
58 # \_emphasis\_
58 # \_emphasis\_
59 # \_\_italicized\_\_
59 # \_\_italicized\_\_
60 # \*strong\*
60 # \*strong\*
61 # \*\*bold\*\*
61 # \*\*bold\*\*
62 # ??citation??
62 # ??citation??
63 # -deleted text-
63 # -deleted text-
64 # +inserted text+
64 # +inserted text+
65 # ^superscript^
65 # ^superscript^
66 # ~subscript~
66 # ~subscript~
67 # @code@
67 # @code@
68 # %(classname)span%
68 # %(classname)span%
69 #
69 #
70 # ==notextile== (leave text alone)
70 # ==notextile== (leave text alone)
71 #
71 #
72 # == Links
72 # == Links
73 #
73 #
74 # To make a hypertext link, put the link text in "quotation
74 # To make a hypertext link, put the link text in "quotation
75 # marks" followed immediately by a colon and the URL of the link.
75 # marks" followed immediately by a colon and the URL of the link.
76 #
76 #
77 # Optional: text in (parentheses) following the link text,
77 # Optional: text in (parentheses) following the link text,
78 # but before the closing quotation mark, will become a Title
78 # but before the closing quotation mark, will become a Title
79 # attribute for the link, visible as a tool tip when a cursor is above it.
79 # attribute for the link, visible as a tool tip when a cursor is above it.
80 #
80 #
81 # Example:
81 # Example:
82 #
82 #
83 # "This is a link (This is a title) ":http://www.textism.com
83 # "This is a link (This is a title) ":http://www.textism.com
84 #
84 #
85 # Will become:
85 # Will become:
86 #
86 #
87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
88 #
88 #
89 # == Images
89 # == Images
90 #
90 #
91 # To insert an image, put the URL for the image inside exclamation marks.
91 # To insert an image, put the URL for the image inside exclamation marks.
92 #
92 #
93 # Optional: text that immediately follows the URL in (parentheses) will
93 # Optional: text that immediately follows the URL in (parentheses) will
94 # be used as the Alt text for the image. Images on the web should always
94 # be used as the Alt text for the image. Images on the web should always
95 # have descriptive Alt text for the benefit of readers using non-graphical
95 # have descriptive Alt text for the benefit of readers using non-graphical
96 # browsers.
96 # browsers.
97 #
97 #
98 # Optional: place a colon followed by a URL immediately after the
98 # Optional: place a colon followed by a URL immediately after the
99 # closing ! to make the image into a link.
99 # closing ! to make the image into a link.
100 #
100 #
101 # Example:
101 # Example:
102 #
102 #
103 # !http://www.textism.com/common/textist.gif(Textist)!
103 # !http://www.textism.com/common/textist.gif(Textist)!
104 #
104 #
105 # Will become:
105 # Will become:
106 #
106 #
107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
108 #
108 #
109 # With a link:
109 # With a link:
110 #
110 #
111 # !/common/textist.gif(Textist)!:http://textism.com
111 # !/common/textist.gif(Textist)!:http://textism.com
112 #
112 #
113 # Will become:
113 # Will become:
114 #
114 #
115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
116 #
116 #
117 # == Defining Acronyms
117 # == Defining Acronyms
118 #
118 #
119 # HTML allows authors to define acronyms via the tag. The definition appears as a
119 # HTML allows authors to define acronyms via the tag. The definition appears as a
120 # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
120 # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
121 # this should be used at least once for each acronym in documents where they appear.
121 # this should be used at least once for each acronym in documents where they appear.
122 #
122 #
123 # To quickly define an acronym in Textile, place the full text in (parentheses)
123 # To quickly define an acronym in Textile, place the full text in (parentheses)
124 # immediately following the acronym.
124 # immediately following the acronym.
125 #
125 #
126 # Example:
126 # Example:
127 #
127 #
128 # ACLU(American Civil Liberties Union)
128 # ACLU(American Civil Liberties Union)
129 #
129 #
130 # Will become:
130 # Will become:
131 #
131 #
132 # <acronym title="American Civil Liberties Union">ACLU</acronym>
132 # <acronym title="American Civil Liberties Union">ACLU</acronym>
133 #
133 #
134 # == Adding Tables
134 # == Adding Tables
135 #
135 #
136 # In Textile, simple tables can be added by seperating each column by
136 # In Textile, simple tables can be added by seperating each column by
137 # a pipe.
137 # a pipe.
138 #
138 #
139 # |a|simple|table|row|
139 # |a|simple|table|row|
140 # |And|Another|table|row|
140 # |And|Another|table|row|
141 #
141 #
142 # Attributes are defined by style definitions in parentheses.
142 # Attributes are defined by style definitions in parentheses.
143 #
143 #
144 # table(border:1px solid black).
144 # table(border:1px solid black).
145 # (background:#ddd;color:red). |{}| | | |
145 # (background:#ddd;color:red). |{}| | | |
146 #
146 #
147 # == Using RedCloth
147 # == Using RedCloth
148 #
148 #
149 # RedCloth is simply an extension of the String class, which can handle
149 # RedCloth is simply an extension of the String class, which can handle
150 # Textile formatting. Use it like a String and output HTML with its
150 # Textile formatting. Use it like a String and output HTML with its
151 # RedCloth#to_html method.
151 # RedCloth#to_html method.
152 #
152 #
153 # doc = RedCloth.new "
153 # doc = RedCloth.new "
154 #
154 #
155 # h2. Test document
155 # h2. Test document
156 #
156 #
157 # Just a simple test."
157 # Just a simple test."
158 #
158 #
159 # puts doc.to_html
159 # puts doc.to_html
160 #
160 #
161 # By default, RedCloth uses both Textile and Markdown formatting, with
161 # By default, RedCloth uses both Textile and Markdown formatting, with
162 # Textile formatting taking precedence. If you want to turn off Markdown
162 # Textile formatting taking precedence. If you want to turn off Markdown
163 # formatting, to boost speed and limit the processor:
163 # formatting, to boost speed and limit the processor:
164 #
164 #
165 # class RedCloth::Textile.new( str )
165 # class RedCloth::Textile.new( str )
166
166
167 class RedCloth3 < String
167 class RedCloth3 < String
168
168
169 VERSION = '3.0.4'
169 VERSION = '3.0.4'
170 DEFAULT_RULES = [:textile, :markdown]
170 DEFAULT_RULES = [:textile, :markdown]
171
171
172 #
172 #
173 # Two accessor for setting security restrictions.
173 # Two accessor for setting security restrictions.
174 #
174 #
175 # This is a nice thing if you're using RedCloth for
175 # This is a nice thing if you're using RedCloth for
176 # formatting in public places (e.g. Wikis) where you
176 # formatting in public places (e.g. Wikis) where you
177 # don't want users to abuse HTML for bad things.
177 # don't want users to abuse HTML for bad things.
178 #
178 #
179 # If +:filter_html+ is set, HTML which wasn't
179 # If +:filter_html+ is set, HTML which wasn't
180 # created by the Textile processor will be escaped.
180 # created by the Textile processor will be escaped.
181 #
181 #
182 # If +:filter_styles+ is set, it will also disable
182 # If +:filter_styles+ is set, it will also disable
183 # the style markup specifier. ('{color: red}')
183 # the style markup specifier. ('{color: red}')
184 #
184 #
185 attr_accessor :filter_html, :filter_styles
185 attr_accessor :filter_html, :filter_styles
186
186
187 #
187 #
188 # Accessor for toggling hard breaks.
188 # Accessor for toggling hard breaks.
189 #
189 #
190 # If +:hard_breaks+ is set, single newlines will
190 # If +:hard_breaks+ is set, single newlines will
191 # be converted to HTML break tags. This is the
191 # be converted to HTML break tags. This is the
192 # default behavior for traditional RedCloth.
192 # default behavior for traditional RedCloth.
193 #
193 #
194 attr_accessor :hard_breaks
194 attr_accessor :hard_breaks
195
195
196 # Accessor for toggling lite mode.
196 # Accessor for toggling lite mode.
197 #
197 #
198 # In lite mode, block-level rules are ignored. This means
198 # In lite mode, block-level rules are ignored. This means
199 # that tables, paragraphs, lists, and such aren't available.
199 # that tables, paragraphs, lists, and such aren't available.
200 # Only the inline markup for bold, italics, entities and so on.
200 # Only the inline markup for bold, italics, entities and so on.
201 #
201 #
202 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
202 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
203 # r.to_html
203 # r.to_html
204 # #=> "And then? She <strong>fell</strong>!"
204 # #=> "And then? She <strong>fell</strong>!"
205 #
205 #
206 attr_accessor :lite_mode
206 attr_accessor :lite_mode
207
207
208 #
208 #
209 # Accessor for toggling span caps.
209 # Accessor for toggling span caps.
210 #
210 #
211 # Textile places `span' tags around capitalized
211 # Textile places `span' tags around capitalized
212 # words by default, but this wreaks havoc on Wikis.
212 # words by default, but this wreaks havoc on Wikis.
213 # If +:no_span_caps+ is set, this will be
213 # If +:no_span_caps+ is set, this will be
214 # suppressed.
214 # suppressed.
215 #
215 #
216 attr_accessor :no_span_caps
216 attr_accessor :no_span_caps
217
217
218 #
218 #
219 # Establishes the markup predence. Available rules include:
219 # Establishes the markup predence. Available rules include:
220 #
220 #
221 # == Textile Rules
221 # == Textile Rules
222 #
222 #
223 # The following textile rules can be set individually. Or add the complete
223 # The following textile rules can be set individually. Or add the complete
224 # set of rules with the single :textile rule, which supplies the rule set in
224 # set of rules with the single :textile rule, which supplies the rule set in
225 # the following precedence:
225 # the following precedence:
226 #
226 #
227 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
227 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
228 # block_textile_table:: Textile table block structures
228 # block_textile_table:: Textile table block structures
229 # block_textile_lists:: Textile list structures
229 # block_textile_lists:: Textile list structures
230 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
230 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
231 # inline_textile_image:: Textile inline images
231 # inline_textile_image:: Textile inline images
232 # inline_textile_link:: Textile inline links
232 # inline_textile_link:: Textile inline links
233 # inline_textile_span:: Textile inline spans
233 # inline_textile_span:: Textile inline spans
234 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
234 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
235 #
235 #
236 # == Markdown
236 # == Markdown
237 #
237 #
238 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
238 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
239 # block_markdown_setext:: Markdown setext headers
239 # block_markdown_setext:: Markdown setext headers
240 # block_markdown_atx:: Markdown atx headers
240 # block_markdown_atx:: Markdown atx headers
241 # block_markdown_rule:: Markdown horizontal rules
241 # block_markdown_rule:: Markdown horizontal rules
242 # block_markdown_bq:: Markdown blockquotes
242 # block_markdown_bq:: Markdown blockquotes
243 # block_markdown_lists:: Markdown lists
243 # block_markdown_lists:: Markdown lists
244 # inline_markdown_link:: Markdown links
244 # inline_markdown_link:: Markdown links
245 attr_accessor :rules
245 attr_accessor :rules
246
246
247 # Returns a new RedCloth object, based on _string_ and
247 # Returns a new RedCloth object, based on _string_ and
248 # enforcing all the included _restrictions_.
248 # enforcing all the included _restrictions_.
249 #
249 #
250 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
250 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
251 # r.to_html
251 # r.to_html
252 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
252 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
253 #
253 #
254 def initialize( string, restrictions = [] )
254 def initialize( string, restrictions = [] )
255 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
255 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
256 super( string )
256 super( string )
257 end
257 end
258
258
259 #
259 #
260 # Generates HTML from the Textile contents.
260 # Generates HTML from the Textile contents.
261 #
261 #
262 # r = RedCloth.new( "And then? She *fell*!" )
262 # r = RedCloth.new( "And then? She *fell*!" )
263 # r.to_html( true )
263 # r.to_html( true )
264 # #=>"And then? She <strong>fell</strong>!"
264 # #=>"And then? She <strong>fell</strong>!"
265 #
265 #
266 def to_html( *rules )
266 def to_html( *rules )
267 rules = DEFAULT_RULES if rules.empty?
267 rules = DEFAULT_RULES if rules.empty?
268 # make our working copy
268 # make our working copy
269 text = self.dup
269 text = self.dup
270
270
271 @urlrefs = {}
271 @urlrefs = {}
272 @shelf = []
272 @shelf = []
273 textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists,
273 textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists,
274 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
274 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
275 :inline_textile_code, :inline_textile_span, :glyphs_textile]
275 :inline_textile_code, :inline_textile_span, :glyphs_textile]
276 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
276 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
277 :block_markdown_bq, :block_markdown_lists,
277 :block_markdown_bq, :block_markdown_lists,
278 :inline_markdown_reflink, :inline_markdown_link]
278 :inline_markdown_reflink, :inline_markdown_link]
279 @rules = rules.collect do |rule|
279 @rules = rules.collect do |rule|
280 case rule
280 case rule
281 when :markdown
281 when :markdown
282 markdown_rules
282 markdown_rules
283 when :textile
283 when :textile
284 textile_rules
284 textile_rules
285 else
285 else
286 rule
286 rule
287 end
287 end
288 end.flatten
288 end.flatten
289
289
290 # standard clean up
290 # standard clean up
291 incoming_entities text
291 incoming_entities text
292 clean_white_space text
292 clean_white_space text
293
293
294 # start processor
294 # start processor
295 @pre_list = []
295 @pre_list = []
296 rip_offtags text
296 rip_offtags text
297 no_textile text
297 no_textile text
298 escape_html_tags text
298 escape_html_tags text
299 hard_break text
299 hard_break text
300 unless @lite_mode
300 unless @lite_mode
301 refs text
301 refs text
302 # need to do this before text is split by #blocks
302 # need to do this before text is split by #blocks
303 block_textile_quotes text
303 block_textile_quotes text
304 blocks text
304 blocks text
305 end
305 end
306 inline text
306 inline text
307 smooth_offtags text
307 smooth_offtags text
308
308
309 retrieve text
309 retrieve text
310
310
311 text.gsub!( /<\/?notextile>/, '' )
311 text.gsub!( /<\/?notextile>/, '' )
312 text.gsub!( /x%x%/, '&#38;' )
312 text.gsub!( /x%x%/, '&#38;' )
313 clean_html text if filter_html
313 clean_html text if filter_html
314 text.strip!
314 text.strip!
315 text
315 text
316
316
317 end
317 end
318
318
319 #######
319 #######
320 private
320 private
321 #######
321 #######
322 #
322 #
323 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
323 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
324 # (from PyTextile)
324 # (from PyTextile)
325 #
325 #
326 TEXTILE_TAGS =
326 TEXTILE_TAGS =
327
327
328 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
328 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
329 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
329 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
330 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
330 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
331 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
331 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
332 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
332 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
333
333
334 collect! do |a, b|
334 collect! do |a, b|
335 [a.chr, ( b.zero? and "" or "&#{ b };" )]
335 [a.chr, ( b.zero? and "" or "&#{ b };" )]
336 end
336 end
337
337
338 #
338 #
339 # Regular expressions to convert to HTML.
339 # Regular expressions to convert to HTML.
340 #
340 #
341 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
341 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
342 A_VLGN = /[\-^~]/
342 A_VLGN = /[\-^~]/
343 C_CLAS = '(?:\([^)]+\))'
343 C_CLAS = '(?:\([^)]+\))'
344 C_LNGE = '(?:\[[^\[\]]+\])'
344 C_LNGE = '(?:\[[^\[\]]+\])'
345 C_STYL = '(?:\{[^}]+\})'
345 C_STYL = '(?:\{[^}]+\})'
346 S_CSPN = '(?:\\\\\d+)'
346 S_CSPN = '(?:\\\\\d+)'
347 S_RSPN = '(?:/\d+)'
347 S_RSPN = '(?:/\d+)'
348 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
348 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
349 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
349 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
350 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
350 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
351 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
351 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
352 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
352 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
353 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
353 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
354 PUNCT_Q = Regexp::quote( '*-_+^~%' )
354 PUNCT_Q = Regexp::quote( '*-_+^~%' )
355 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
355 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
356
356
357 # Text markup tags, don't conflict with block tags
357 # Text markup tags, don't conflict with block tags
358 SIMPLE_HTML_TAGS = [
358 SIMPLE_HTML_TAGS = [
359 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
359 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
360 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
360 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
361 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
361 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
362 ]
362 ]
363
363
364 QTAGS = [
364 QTAGS = [
365 ['**', 'b', :limit],
365 ['**', 'b', :limit],
366 ['*', 'strong', :limit],
366 ['*', 'strong', :limit],
367 ['??', 'cite', :limit],
367 ['??', 'cite', :limit],
368 ['-', 'del', :limit],
368 ['-', 'del', :limit],
369 ['__', 'i', :limit],
369 ['__', 'i', :limit],
370 ['_', 'em', :limit],
370 ['_', 'em', :limit],
371 ['%', 'span', :limit],
371 ['%', 'span', :limit],
372 ['+', 'ins', :limit],
372 ['+', 'ins', :limit],
373 ['^', 'sup', :limit],
373 ['^', 'sup', :limit],
374 ['~', 'sub', :limit]
374 ['~', 'sub', :limit]
375 ]
375 ]
376 QTAGS.collect! do |rc, ht, rtype|
376 QTAGS.collect! do |rc, ht, rtype|
377 rcq = Regexp::quote rc
377 rcq = Regexp::quote rc
378 re =
378 re =
379 case rtype
379 case rtype
380 when :limit
380 when :limit
381 /(^|[>\s\(])
381 /(^|[>\s\(])
382 (#{rcq})
382 (#{rcq})
383 (#{C})
383 (#{C})
384 (?::(\S+?))?
384 (?::(\S+?))?
385 ([^\s\-].*?[^\s\-]|\w)
385 ([^\s\-].*?[^\s\-]|\w)
386 #{rcq}
386 #{rcq}
387 (?=[[:punct:]]|\s|\)|$)/x
387 (?=[[:punct:]]|\s|\)|$)/x
388 else
388 else
389 /(#{rcq})
389 /(#{rcq})
390 (#{C})
390 (#{C})
391 (?::(\S+))?
391 (?::(\S+))?
392 ([^\s\-].*?[^\s\-]|\w)
392 ([^\s\-].*?[^\s\-]|\w)
393 #{rcq}/xm
393 #{rcq}/xm
394 end
394 end
395 [rc, ht, re, rtype]
395 [rc, ht, re, rtype]
396 end
396 end
397
397
398 # Elements to handle
398 # Elements to handle
399 GLYPHS = [
399 GLYPHS = [
400 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
400 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
401 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
401 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
402 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
402 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
403 # [ /\'/, '&#8216;' ], # single opening
403 # [ /\'/, '&#8216;' ], # single opening
404 # [ /</, '&lt;' ], # less-than
404 # [ /</, '&lt;' ], # less-than
405 # [ />/, '&gt;' ], # greater-than
405 # [ />/, '&gt;' ], # greater-than
406 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
406 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
407 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
407 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
408 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
408 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
409 # [ /"/, '&#8220;' ], # double opening
409 # [ /"/, '&#8220;' ], # double opening
410 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
410 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
411 [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
411 # [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
412 # [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
412 # [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
413 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
413 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
414 # [ /\s->\s/, ' &rarr; ' ], # right arrow
414 # [ /\s->\s/, ' &rarr; ' ], # right arrow
415 # [ /\s-\s/, ' &#8211; ' ], # en dash
415 # [ /\s-\s/, ' &#8211; ' ], # en dash
416 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
416 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
417 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
417 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
418 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
418 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
419 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
419 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
420 ]
420 ]
421
421
422 H_ALGN_VALS = {
422 H_ALGN_VALS = {
423 '<' => 'left',
423 '<' => 'left',
424 '=' => 'center',
424 '=' => 'center',
425 '>' => 'right',
425 '>' => 'right',
426 '<>' => 'justify'
426 '<>' => 'justify'
427 }
427 }
428
428
429 V_ALGN_VALS = {
429 V_ALGN_VALS = {
430 '^' => 'top',
430 '^' => 'top',
431 '-' => 'middle',
431 '-' => 'middle',
432 '~' => 'bottom'
432 '~' => 'bottom'
433 }
433 }
434
434
435 #
435 #
436 # Flexible HTML escaping
436 # Flexible HTML escaping
437 #
437 #
438 def htmlesc( str, mode=:Quotes )
438 def htmlesc( str, mode=:Quotes )
439 if str
439 if str
440 str.gsub!( '&', '&amp;' )
440 str.gsub!( '&', '&amp;' )
441 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
441 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
442 str.gsub!( "'", '&#039;' ) if mode == :Quotes
442 str.gsub!( "'", '&#039;' ) if mode == :Quotes
443 str.gsub!( '<', '&lt;')
443 str.gsub!( '<', '&lt;')
444 str.gsub!( '>', '&gt;')
444 str.gsub!( '>', '&gt;')
445 end
445 end
446 str
446 str
447 end
447 end
448
448
449 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
449 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
450 def pgl( text )
450 def pgl( text )
451 GLYPHS.each do |re, resub, tog|
451 #GLYPHS.each do |re, resub, tog|
452 next if tog and method( tog ).call
452 # next if tog and method( tog ).call
453 text.gsub! re, resub
453 # text.gsub! re, resub
454 #end
455 text.gsub!(/\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/) do |m|
456 "<acronym title=\"#{htmlesc $2}\">#{$1}</acronym>"
454 end
457 end
455 end
458 end
456
459
457 # Parses Textile attribute lists and builds an HTML attribute string
460 # Parses Textile attribute lists and builds an HTML attribute string
458 def pba( text_in, element = "" )
461 def pba( text_in, element = "" )
459
462
460 return '' unless text_in
463 return '' unless text_in
461
464
462 style = []
465 style = []
463 text = text_in.dup
466 text = text_in.dup
464 if element == 'td'
467 if element == 'td'
465 colspan = $1 if text =~ /\\(\d+)/
468 colspan = $1 if text =~ /\\(\d+)/
466 rowspan = $1 if text =~ /\/(\d+)/
469 rowspan = $1 if text =~ /\/(\d+)/
467 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
470 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
468 end
471 end
469
472
470 style << "#{ $1 };" if not filter_styles and
473 style << "#{ htmlesc $1 };" if not filter_styles and
471 text.sub!( /\{([^}]*)\}/, '' )
474 text.sub!( /\{([^}]*)\}/, '' )
472
475
473 lang = $1 if
476 lang = $1 if
474 text.sub!( /\[([^)]+?)\]/, '' )
477 text.sub!( /\[([^)]+?)\]/, '' )
475
478
476 cls = $1 if
479 cls = $1 if
477 text.sub!( /\(([^()]+?)\)/, '' )
480 text.sub!( /\(([^()]+?)\)/, '' )
478
481
479 style << "padding-left:#{ $1.length }em;" if
482 style << "padding-left:#{ $1.length }em;" if
480 text.sub!( /([(]+)/, '' )
483 text.sub!( /([(]+)/, '' )
481
484
482 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
485 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
483
486
484 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
487 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
485
488
486 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
489 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
487
490
488 atts = ''
491 atts = ''
489 atts << " style=\"#{ style.join }\"" unless style.empty?
492 atts << " style=\"#{ style.join }\"" unless style.empty?
490 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
493 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
491 atts << " lang=\"#{ lang }\"" if lang
494 atts << " lang=\"#{ lang }\"" if lang
492 atts << " id=\"#{ id }\"" if id
495 atts << " id=\"#{ id }\"" if id
493 atts << " colspan=\"#{ colspan }\"" if colspan
496 atts << " colspan=\"#{ colspan }\"" if colspan
494 atts << " rowspan=\"#{ rowspan }\"" if rowspan
497 atts << " rowspan=\"#{ rowspan }\"" if rowspan
495
498
496 atts
499 atts
497 end
500 end
498
501
499 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
502 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
500
503
501 # Parses a Textile table block, building HTML from the result.
504 # Parses a Textile table block, building HTML from the result.
502 def block_textile_table( text )
505 def block_textile_table( text )
503 text.gsub!( TABLE_RE ) do |matches|
506 text.gsub!( TABLE_RE ) do |matches|
504
507
505 tatts, fullrow = $~[1..2]
508 tatts, fullrow = $~[1..2]
506 tatts = pba( tatts, 'table' )
509 tatts = pba( tatts, 'table' )
507 tatts = shelve( tatts ) if tatts
510 tatts = shelve( tatts ) if tatts
508 rows = []
511 rows = []
509
512
510 fullrow.each_line do |row|
513 fullrow.each_line do |row|
511 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
514 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
512 cells = []
515 cells = []
513 row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
516 row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
514 next if cell == '|'
517 next if cell == '|'
515 ctyp = 'd'
518 ctyp = 'd'
516 ctyp = 'h' if cell =~ /^_/
519 ctyp = 'h' if cell =~ /^_/
517
520
518 catts = ''
521 catts = ''
519 catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
522 catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
520
523
521 catts = shelve( catts ) if catts
524 catts = shelve( catts ) if catts
522 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
525 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
523 end
526 end
524 ratts = shelve( ratts ) if ratts
527 ratts = shelve( ratts ) if ratts
525 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
528 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
526 end
529 end
527 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
530 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
528 end
531 end
529 end
532 end
530
533
531 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
534 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
532 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
535 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
533
536
534 # Parses Textile lists and generates HTML
537 # Parses Textile lists and generates HTML
535 def block_textile_lists( text )
538 def block_textile_lists( text )
536 text.gsub!( LISTS_RE ) do |match|
539 text.gsub!( LISTS_RE ) do |match|
537 lines = match.split( /\n/ )
540 lines = match.split( /\n/ )
538 last_line = -1
541 last_line = -1
539 depth = []
542 depth = []
540 lines.each_with_index do |line, line_id|
543 lines.each_with_index do |line, line_id|
541 if line =~ LISTS_CONTENT_RE
544 if line =~ LISTS_CONTENT_RE
542 tl,atts,content = $~[1..3]
545 tl,atts,content = $~[1..3]
543 if depth.last
546 if depth.last
544 if depth.last.length > tl.length
547 if depth.last.length > tl.length
545 (depth.length - 1).downto(0) do |i|
548 (depth.length - 1).downto(0) do |i|
546 break if depth[i].length == tl.length
549 break if depth[i].length == tl.length
547 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
550 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
548 depth.pop
551 depth.pop
549 end
552 end
550 end
553 end
551 if depth.last and depth.last.length == tl.length
554 if depth.last and depth.last.length == tl.length
552 lines[line_id - 1] << '</li>'
555 lines[line_id - 1] << '</li>'
553 end
556 end
554 end
557 end
555 unless depth.last == tl
558 unless depth.last == tl
556 depth << tl
559 depth << tl
557 atts = pba( atts )
560 atts = pba( atts )
558 atts = shelve( atts ) if atts
561 atts = shelve( atts ) if atts
559 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
562 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
560 else
563 else
561 lines[line_id] = "\t\t<li>#{ content }"
564 lines[line_id] = "\t\t<li>#{ content }"
562 end
565 end
563 last_line = line_id
566 last_line = line_id
564
567
565 else
568 else
566 last_line = line_id
569 last_line = line_id
567 end
570 end
568 if line_id - last_line > 1 or line_id == lines.length - 1
571 if line_id - last_line > 1 or line_id == lines.length - 1
569 depth.delete_if do |v|
572 depth.delete_if do |v|
570 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
573 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
571 end
574 end
572 end
575 end
573 end
576 end
574 lines.join( "\n" )
577 lines.join( "\n" )
575 end
578 end
576 end
579 end
577
580
578 QUOTES_RE = /(^>+([^\n]*?)\n?)+/m
581 QUOTES_RE = /(^>+([^\n]*?)\n?)+/m
579 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
582 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
580
583
581 def block_textile_quotes( text )
584 def block_textile_quotes( text )
582 text.gsub!( QUOTES_RE ) do |match|
585 text.gsub!( QUOTES_RE ) do |match|
583 lines = match.split( /\n/ )
586 lines = match.split( /\n/ )
584 quotes = ''
587 quotes = ''
585 indent = 0
588 indent = 0
586 lines.each do |line|
589 lines.each do |line|
587 line =~ QUOTES_CONTENT_RE
590 line =~ QUOTES_CONTENT_RE
588 bq,content = $1, $2
591 bq,content = $1, $2
589 l = bq.count('>')
592 l = bq.count('>')
590 if l != indent
593 if l != indent
591 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
594 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
592 indent = l
595 indent = l
593 end
596 end
594 quotes << (content + "\n")
597 quotes << (content + "\n")
595 end
598 end
596 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
599 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
597 quotes
600 quotes
598 end
601 end
599 end
602 end
600
603
601 CODE_RE = /(\W)
604 CODE_RE = /(\W)
602 @
605 @
603 (?:\|(\w+?)\|)?
606 (?:\|(\w+?)\|)?
604 (.+?)
607 (.+?)
605 @
608 @
606 (?=\W)/x
609 (?=\W)/x
607
610
608 def inline_textile_code( text )
611 def inline_textile_code( text )
609 text.gsub!( CODE_RE ) do |m|
612 text.gsub!( CODE_RE ) do |m|
610 before,lang,code,after = $~[1..4]
613 before,lang,code,after = $~[1..4]
611 lang = " lang=\"#{ lang }\"" if lang
614 lang = " lang=\"#{ lang }\"" if lang
612 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }" )
615 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }" )
613 end
616 end
614 end
617 end
615
618
616 def lT( text )
619 def lT( text )
617 text =~ /\#$/ ? 'o' : 'u'
620 text =~ /\#$/ ? 'o' : 'u'
618 end
621 end
619
622
620 def hard_break( text )
623 def hard_break( text )
621 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
624 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
622 end
625 end
623
626
624 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
627 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
625
628
626 def blocks( text, deep_code = false )
629 def blocks( text, deep_code = false )
627 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
630 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
628 plain = blk !~ /\A[#*> ]/
631 plain = blk !~ /\A[#*> ]/
629
632
630 # skip blocks that are complex HTML
633 # skip blocks that are complex HTML
631 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
634 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
632 blk
635 blk
633 else
636 else
634 # search for indentation levels
637 # search for indentation levels
635 blk.strip!
638 blk.strip!
636 if blk.empty?
639 if blk.empty?
637 blk
640 blk
638 else
641 else
639 code_blk = nil
642 code_blk = nil
640 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
643 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
641 flush_left iblk
644 flush_left iblk
642 blocks iblk, plain
645 blocks iblk, plain
643 iblk.gsub( /^(\S)/, "\t\\1" )
646 iblk.gsub( /^(\S)/, "\t\\1" )
644 if plain
647 if plain
645 code_blk = iblk; ""
648 code_blk = iblk; ""
646 else
649 else
647 iblk
650 iblk
648 end
651 end
649 end
652 end
650
653
651 block_applied = 0
654 block_applied = 0
652 @rules.each do |rule_name|
655 @rules.each do |rule_name|
653 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
656 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
654 end
657 end
655 if block_applied.zero?
658 if block_applied.zero?
656 if deep_code
659 if deep_code
657 blk = "\t<pre><code>#{ blk }</code></pre>"
660 blk = "\t<pre><code>#{ blk }</code></pre>"
658 else
661 else
659 blk = "\t<p>#{ blk }</p>"
662 blk = "\t<p>#{ blk }</p>"
660 end
663 end
661 end
664 end
662 # hard_break blk
665 # hard_break blk
663 blk + "\n#{ code_blk }"
666 blk + "\n#{ code_blk }"
664 end
667 end
665 end
668 end
666
669
667 end.join( "\n\n" ) )
670 end.join( "\n\n" ) )
668 end
671 end
669
672
670 def textile_bq( tag, atts, cite, content )
673 def textile_bq( tag, atts, cite, content )
671 cite, cite_title = check_refs( cite )
674 cite, cite_title = check_refs( cite )
672 cite = " cite=\"#{ cite }\"" if cite
675 cite = " cite=\"#{ cite }\"" if cite
673 atts = shelve( atts ) if atts
676 atts = shelve( atts ) if atts
674 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
677 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
675 end
678 end
676
679
677 def textile_p( tag, atts, cite, content )
680 def textile_p( tag, atts, cite, content )
678 atts = shelve( atts ) if atts
681 atts = shelve( atts ) if atts
679 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
682 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
680 end
683 end
681
684
682 alias textile_h1 textile_p
685 alias textile_h1 textile_p
683 alias textile_h2 textile_p
686 alias textile_h2 textile_p
684 alias textile_h3 textile_p
687 alias textile_h3 textile_p
685 alias textile_h4 textile_p
688 alias textile_h4 textile_p
686 alias textile_h5 textile_p
689 alias textile_h5 textile_p
687 alias textile_h6 textile_p
690 alias textile_h6 textile_p
688
691
689 def textile_fn_( tag, num, atts, cite, content )
692 def textile_fn_( tag, num, atts, cite, content )
690 atts << " id=\"fn#{ num }\" class=\"footnote\""
693 atts << " id=\"fn#{ num }\" class=\"footnote\""
691 content = "<sup>#{ num }</sup> #{ content }"
694 content = "<sup>#{ num }</sup> #{ content }"
692 atts = shelve( atts ) if atts
695 atts = shelve( atts ) if atts
693 "\t<p#{ atts }>#{ content }</p>"
696 "\t<p#{ atts }>#{ content }</p>"
694 end
697 end
695
698
696 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
699 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
697
700
698 def block_textile_prefix( text )
701 def block_textile_prefix( text )
699 if text =~ BLOCK_RE
702 if text =~ BLOCK_RE
700 tag,tagpre,num,atts,cite,content = $~[1..6]
703 tag,tagpre,num,atts,cite,content = $~[1..6]
701 atts = pba( atts )
704 atts = pba( atts )
702
705
703 # pass to prefix handler
706 # pass to prefix handler
704 if respond_to? "textile_#{ tag }", true
707 if respond_to? "textile_#{ tag }", true
705 text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) )
708 text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) )
706 elsif respond_to? "textile_#{ tagpre }_", true
709 elsif respond_to? "textile_#{ tagpre }_", true
707 text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) )
710 text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) )
708 end
711 end
709 end
712 end
710 end
713 end
711
714
712 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
715 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
713 def block_markdown_setext( text )
716 def block_markdown_setext( text )
714 if text =~ SETEXT_RE
717 if text =~ SETEXT_RE
715 tag = if $2 == "="; "h1"; else; "h2"; end
718 tag = if $2 == "="; "h1"; else; "h2"; end
716 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
719 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
717 blocks cont
720 blocks cont
718 text.replace( blk + cont )
721 text.replace( blk + cont )
719 end
722 end
720 end
723 end
721
724
722 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
725 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
723 [ ]*
726 [ ]*
724 (.+?) # $2 = Header text
727 (.+?) # $2 = Header text
725 [ ]*
728 [ ]*
726 \#* # optional closing #'s (not counted)
729 \#* # optional closing #'s (not counted)
727 $/x
730 $/x
728 def block_markdown_atx( text )
731 def block_markdown_atx( text )
729 if text =~ ATX_RE
732 if text =~ ATX_RE
730 tag = "h#{ $1.length }"
733 tag = "h#{ $1.length }"
731 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
734 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
732 blocks cont
735 blocks cont
733 text.replace( blk + cont )
736 text.replace( blk + cont )
734 end
737 end
735 end
738 end
736
739
737 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
740 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
738
741
739 def block_markdown_bq( text )
742 def block_markdown_bq( text )
740 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
743 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
741 blk.gsub!( /^ *> ?/, '' )
744 blk.gsub!( /^ *> ?/, '' )
742 flush_left blk
745 flush_left blk
743 blocks blk
746 blocks blk
744 blk.gsub!( /^(\S)/, "\t\\1" )
747 blk.gsub!( /^(\S)/, "\t\\1" )
745 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
748 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
746 end
749 end
747 end
750 end
748
751
749 MARKDOWN_RULE_RE = /^(#{
752 MARKDOWN_RULE_RE = /^(#{
750 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
753 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
751 })$/
754 })$/
752
755
753 def block_markdown_rule( text )
756 def block_markdown_rule( text )
754 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
757 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
755 "<hr />"
758 "<hr />"
756 end
759 end
757 end
760 end
758
761
759 # XXX TODO XXX
762 # XXX TODO XXX
760 def block_markdown_lists( text )
763 def block_markdown_lists( text )
761 end
764 end
762
765
763 def inline_textile_span( text )
766 def inline_textile_span( text )
764 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
767 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
765 text.gsub!( qtag_re ) do |m|
768 text.gsub!( qtag_re ) do |m|
766
769
767 case rtype
770 case rtype
768 when :limit
771 when :limit
769 sta,qtag,atts,cite,content = $~[1..5]
772 sta,qtag,atts,cite,content = $~[1..5]
770 else
773 else
771 qtag,atts,cite,content = $~[1..4]
774 qtag,atts,cite,content = $~[1..4]
772 sta = ''
775 sta = ''
773 end
776 end
774 atts = pba( atts )
777 atts = pba( atts )
775 atts << " cite=\"#{ cite }\"" if cite
778 atts << " cite=\"#{ cite }\"" if cite
776 atts = shelve( atts ) if atts
779 atts = shelve( atts ) if atts
777
780
778 "#{ sta }<#{ ht }#{ atts }>#{ content }</#{ ht }>"
781 "#{ sta }<#{ ht }#{ atts }>#{ content }</#{ ht }>"
779
782
780 end
783 end
781 end
784 end
782 end
785 end
783
786
784 LINK_RE = /
787 LINK_RE = /
785 ([\s\[{(]|[#{PUNCT}])? # $pre
788 ([\s\[{(]|[#{PUNCT}])? # $pre
786 " # start
789 " # start
787 (#{C}) # $atts
790 (#{C}) # $atts
788 ([^"\n]+?) # $text
791 ([^"\n]+?) # $text
789 \s?
792 \s?
790 (?:\(([^)]+?)\)(?="))? # $title
793 (?:\(([^)]+?)\)(?="))? # $title
791 ":
794 ":
792 ([\w\/]\S+?) # $url
795 ([\w\/]\S+?) # $url
793 (\/)? # $slash
796 (\/)? # $slash
794 ([^\w\=\/;\(\)]*?) # $post
797 ([^\w\=\/;\(\)]*?) # $post
795 (?=<|\s|$)
798 (?=<|\s|$)
796 /x
799 /x
797 #"
800 #"
798 def inline_textile_link( text )
801 def inline_textile_link( text )
799 text.gsub!( LINK_RE ) do |m|
802 text.gsub!( LINK_RE ) do |m|
800 pre,atts,text,title,url,slash,post = $~[1..7]
803 pre,atts,text,title,url,slash,post = $~[1..7]
801
804
802 url, url_title = check_refs( url )
805 url, url_title = check_refs( url )
803 title ||= url_title
806 title ||= url_title
804
807
805 # Idea below : an URL with unbalanced parethesis and
808 # Idea below : an URL with unbalanced parethesis and
806 # ending by ')' is put into external parenthesis
809 # ending by ')' is put into external parenthesis
807 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
810 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
808 url=url[0..-2] # discard closing parenth from url
811 url=url[0..-2] # discard closing parenth from url
809 post = ")"+post # add closing parenth to post
812 post = ")"+post # add closing parenth to post
810 end
813 end
811 atts = pba( atts )
814 atts = pba( atts )
812 atts = " href=\"#{ url }#{ slash }\"#{ atts }"
815 atts = " href=\"#{ url }#{ slash }\"#{ atts }"
813 atts << " title=\"#{ title }\"" if title
816 atts << " title=\"#{ htmlesc title }\"" if title
814 atts = shelve( atts ) if atts
817 atts = shelve( atts ) if atts
815
818
816 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
819 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
817
820
818 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
821 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
819 end
822 end
820 end
823 end
821
824
822 MARKDOWN_REFLINK_RE = /
825 MARKDOWN_REFLINK_RE = /
823 \[([^\[\]]+)\] # $text
826 \[([^\[\]]+)\] # $text
824 [ ]? # opt. space
827 [ ]? # opt. space
825 (?:\n[ ]*)? # one optional newline followed by spaces
828 (?:\n[ ]*)? # one optional newline followed by spaces
826 \[(.*?)\] # $id
829 \[(.*?)\] # $id
827 /x
830 /x
828
831
829 def inline_markdown_reflink( text )
832 def inline_markdown_reflink( text )
830 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
833 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
831 text, id = $~[1..2]
834 text, id = $~[1..2]
832
835
833 if id.empty?
836 if id.empty?
834 url, title = check_refs( text )
837 url, title = check_refs( text )
835 else
838 else
836 url, title = check_refs( id )
839 url, title = check_refs( id )
837 end
840 end
838
841
839 atts = " href=\"#{ url }\""
842 atts = " href=\"#{ url }\""
840 atts << " title=\"#{ title }\"" if title
843 atts << " title=\"#{ title }\"" if title
841 atts = shelve( atts )
844 atts = shelve( atts )
842
845
843 "<a#{ atts }>#{ text }</a>"
846 "<a#{ atts }>#{ text }</a>"
844 end
847 end
845 end
848 end
846
849
847 MARKDOWN_LINK_RE = /
850 MARKDOWN_LINK_RE = /
848 \[([^\[\]]+)\] # $text
851 \[([^\[\]]+)\] # $text
849 \( # open paren
852 \( # open paren
850 [ \t]* # opt space
853 [ \t]* # opt space
851 <?(.+?)>? # $href
854 <?(.+?)>? # $href
852 [ \t]* # opt space
855 [ \t]* # opt space
853 (?: # whole title
856 (?: # whole title
854 (['"]) # $quote
857 (['"]) # $quote
855 (.*?) # $title
858 (.*?) # $title
856 \3 # matching quote
859 \3 # matching quote
857 )? # title is optional
860 )? # title is optional
858 \)
861 \)
859 /x
862 /x
860
863
861 def inline_markdown_link( text )
864 def inline_markdown_link( text )
862 text.gsub!( MARKDOWN_LINK_RE ) do |m|
865 text.gsub!( MARKDOWN_LINK_RE ) do |m|
863 text, url, quote, title = $~[1..4]
866 text, url, quote, title = $~[1..4]
864
867
865 atts = " href=\"#{ url }\""
868 atts = " href=\"#{ url }\""
866 atts << " title=\"#{ title }\"" if title
869 atts << " title=\"#{ title }\"" if title
867 atts = shelve( atts )
870 atts = shelve( atts )
868
871
869 "<a#{ atts }>#{ text }</a>"
872 "<a#{ atts }>#{ text }</a>"
870 end
873 end
871 end
874 end
872
875
873 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
876 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
874 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
877 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
875
878
876 def refs( text )
879 def refs( text )
877 @rules.each do |rule_name|
880 @rules.each do |rule_name|
878 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
881 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
879 end
882 end
880 end
883 end
881
884
882 def refs_textile( text )
885 def refs_textile( text )
883 text.gsub!( TEXTILE_REFS_RE ) do |m|
886 text.gsub!( TEXTILE_REFS_RE ) do |m|
884 flag, url = $~[2..3]
887 flag, url = $~[2..3]
885 @urlrefs[flag.downcase] = [url, nil]
888 @urlrefs[flag.downcase] = [url, nil]
886 nil
889 nil
887 end
890 end
888 end
891 end
889
892
890 def refs_markdown( text )
893 def refs_markdown( text )
891 text.gsub!( MARKDOWN_REFS_RE ) do |m|
894 text.gsub!( MARKDOWN_REFS_RE ) do |m|
892 flag, url = $~[2..3]
895 flag, url = $~[2..3]
893 title = $~[6]
896 title = $~[6]
894 @urlrefs[flag.downcase] = [url, title]
897 @urlrefs[flag.downcase] = [url, title]
895 nil
898 nil
896 end
899 end
897 end
900 end
898
901
899 def check_refs( text )
902 def check_refs( text )
900 ret = @urlrefs[text.downcase] if text
903 ret = @urlrefs[text.downcase] if text
901 ret || [text, nil]
904 ret || [text, nil]
902 end
905 end
903
906
904 IMAGE_RE = /
907 IMAGE_RE = /
905 (<p>|.|^) # start of line?
908 (<p>|.|^) # start of line?
906 \! # opening
909 \! # opening
907 (\<|\=|\>)? # optional alignment atts
910 (\<|\=|\>)? # optional alignment atts
908 (#{C}) # optional style,class atts
911 (#{C}) # optional style,class atts
909 (?:\. )? # optional dot-space
912 (?:\. )? # optional dot-space
910 ([^\s(!]+?) # presume this is the src
913 ([^\s(!]+?) # presume this is the src
911 \s? # optional space
914 \s? # optional space
912 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
915 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
913 \! # closing
916 \! # closing
914 (?::#{ HYPERLINK })? # optional href
917 (?::#{ HYPERLINK })? # optional href
915 /x
918 /x
916
919
917 def inline_textile_image( text )
920 def inline_textile_image( text )
918 text.gsub!( IMAGE_RE ) do |m|
921 text.gsub!( IMAGE_RE ) do |m|
919 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
922 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
920 htmlesc title
923 htmlesc title
921 atts = pba( atts )
924 atts = pba( atts )
922 atts = " src=\"#{ url }\"#{ atts }"
925 atts = " src=\"#{ url }\"#{ atts }"
923 atts << " title=\"#{ title }\"" if title
926 atts << " title=\"#{ title }\"" if title
924 atts << " alt=\"#{ title }\""
927 atts << " alt=\"#{ title }\""
925 # size = @getimagesize($url);
928 # size = @getimagesize($url);
926 # if($size) $atts.= " $size[3]";
929 # if($size) $atts.= " $size[3]";
927
930
928 href, alt_title = check_refs( href ) if href
931 href, alt_title = check_refs( href ) if href
929 url, url_title = check_refs( url )
932 url, url_title = check_refs( url )
930
933
931 out = ''
934 out = ''
932 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
935 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
933 out << "<img#{ shelve( atts ) } />"
936 out << "<img#{ shelve( atts ) } />"
934 out << "</a>#{ href_a1 }#{ href_a2 }" if href
937 out << "</a>#{ href_a1 }#{ href_a2 }" if href
935
938
936 if algn
939 if algn
937 algn = h_align( algn )
940 algn = h_align( algn )
938 if stln == "<p>"
941 if stln == "<p>"
939 out = "<p style=\"float:#{ algn }\">#{ out }"
942 out = "<p style=\"float:#{ algn }\">#{ out }"
940 else
943 else
941 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
944 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
942 end
945 end
943 else
946 else
944 out = stln + out
947 out = stln + out
945 end
948 end
946
949
947 out
950 out
948 end
951 end
949 end
952 end
950
953
951 def shelve( val )
954 def shelve( val )
952 @shelf << val
955 @shelf << val
953 " :redsh##{ @shelf.length }:"
956 " :redsh##{ @shelf.length }:"
954 end
957 end
955
958
956 def retrieve( text )
959 def retrieve( text )
957 @shelf.each_with_index do |r, i|
960 @shelf.each_with_index do |r, i|
958 text.gsub!( " :redsh##{ i + 1 }:", r )
961 text.gsub!( " :redsh##{ i + 1 }:", r )
959 end
962 end
960 end
963 end
961
964
962 def incoming_entities( text )
965 def incoming_entities( text )
963 ## turn any incoming ampersands into a dummy character for now.
966 ## turn any incoming ampersands into a dummy character for now.
964 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
967 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
965 ## implying an incoming html entity, to be skipped
968 ## implying an incoming html entity, to be skipped
966
969
967 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
970 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
968 end
971 end
969
972
970 def no_textile( text )
973 def no_textile( text )
971 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
974 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
972 '\1<notextile>\2</notextile>\3' )
975 '\1<notextile>\2</notextile>\3' )
973 text.gsub!( /^ *==([^=]+.*?)==/m,
976 text.gsub!( /^ *==([^=]+.*?)==/m,
974 '\1<notextile>\2</notextile>\3' )
977 '\1<notextile>\2</notextile>\3' )
975 end
978 end
976
979
977 def clean_white_space( text )
980 def clean_white_space( text )
978 # normalize line breaks
981 # normalize line breaks
979 text.gsub!( /\r\n/, "\n" )
982 text.gsub!( /\r\n/, "\n" )
980 text.gsub!( /\r/, "\n" )
983 text.gsub!( /\r/, "\n" )
981 text.gsub!( /\t/, ' ' )
984 text.gsub!( /\t/, ' ' )
982 text.gsub!( /^ +$/, '' )
985 text.gsub!( /^ +$/, '' )
983 text.gsub!( /\n{3,}/, "\n\n" )
986 text.gsub!( /\n{3,}/, "\n\n" )
984 text.gsub!( /"$/, "\" " )
987 text.gsub!( /"$/, "\" " )
985
988
986 # if entire document is indented, flush
989 # if entire document is indented, flush
987 # to the left side
990 # to the left side
988 flush_left text
991 flush_left text
989 end
992 end
990
993
991 def flush_left( text )
994 def flush_left( text )
992 indt = 0
995 indt = 0
993 if text =~ /^ /
996 if text =~ /^ /
994 while text !~ /^ {#{indt}}\S/
997 while text !~ /^ {#{indt}}\S/
995 indt += 1
998 indt += 1
996 end unless text.empty?
999 end unless text.empty?
997 if indt.nonzero?
1000 if indt.nonzero?
998 text.gsub!( /^ {#{indt}}/, '' )
1001 text.gsub!( /^ {#{indt}}/, '' )
999 end
1002 end
1000 end
1003 end
1001 end
1004 end
1002
1005
1003 def footnote_ref( text )
1006 def footnote_ref( text )
1004 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1007 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1005 '<sup><a href="#fn\1">\1</a></sup>\2' )
1008 '<sup><a href="#fn\1">\1</a></sup>\2' )
1006 end
1009 end
1007
1010
1008 OFFTAGS = /(code|pre|kbd|notextile)/
1011 OFFTAGS = /(code|pre|kbd|notextile)/
1009 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi
1012 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi
1010 OFFTAG_OPEN = /<#{ OFFTAGS }/
1013 OFFTAG_OPEN = /<#{ OFFTAGS }/
1011 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1014 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1012 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1015 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1013 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1016 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1014
1017
1015 def glyphs_textile( text, level = 0 )
1018 def glyphs_textile( text, level = 0 )
1016 if text !~ HASTAG_MATCH
1019 if text !~ HASTAG_MATCH
1017 pgl text
1020 pgl text
1018 footnote_ref text
1021 footnote_ref text
1019 else
1022 else
1020 codepre = 0
1023 codepre = 0
1021 text.gsub!( ALLTAG_MATCH ) do |line|
1024 text.gsub!( ALLTAG_MATCH ) do |line|
1022 ## matches are off if we're between <code>, <pre> etc.
1025 ## matches are off if we're between <code>, <pre> etc.
1023 if $1
1026 if $1
1024 if line =~ OFFTAG_OPEN
1027 if line =~ OFFTAG_OPEN
1025 codepre += 1
1028 codepre += 1
1026 elsif line =~ OFFTAG_CLOSE
1029 elsif line =~ OFFTAG_CLOSE
1027 codepre -= 1
1030 codepre -= 1
1028 codepre = 0 if codepre < 0
1031 codepre = 0 if codepre < 0
1029 end
1032 end
1030 elsif codepre.zero?
1033 elsif codepre.zero?
1031 glyphs_textile( line, level + 1 )
1034 glyphs_textile( line, level + 1 )
1032 else
1035 else
1033 htmlesc( line, :NoQuotes )
1036 htmlesc( line, :NoQuotes )
1034 end
1037 end
1035 # p [level, codepre, line]
1038 # p [level, codepre, line]
1036
1039
1037 line
1040 line
1038 end
1041 end
1039 end
1042 end
1040 end
1043 end
1041
1044
1042 def rip_offtags( text )
1045 def rip_offtags( text )
1043 if text =~ /<.*>/
1046 if text =~ /<.*>/
1044 ## strip and encode <pre> content
1047 ## strip and encode <pre> content
1045 codepre, used_offtags = 0, {}
1048 codepre, used_offtags = 0, {}
1046 text.gsub!( OFFTAG_MATCH ) do |line|
1049 text.gsub!( OFFTAG_MATCH ) do |line|
1047 if $3
1050 if $3
1048 offtag, aftertag = $4, $5
1051 offtag, aftertag = $4, $5
1049 codepre += 1
1052 codepre += 1
1050 used_offtags[offtag] = true
1053 used_offtags[offtag] = true
1051 if codepre - used_offtags.length > 0
1054 if codepre - used_offtags.length > 0
1052 htmlesc( line, :NoQuotes )
1055 htmlesc( line, :NoQuotes )
1053 @pre_list.last << line
1056 @pre_list.last << line
1054 line = ""
1057 line = ""
1055 else
1058 else
1056 htmlesc( aftertag, :NoQuotes ) if aftertag
1059 htmlesc( aftertag, :NoQuotes ) if aftertag
1057 line = "<redpre##{ @pre_list.length }>"
1060 line = "<redpre##{ @pre_list.length }>"
1058 $3.match(/<#{ OFFTAGS }([^>]*)>/)
1061 $3.match(/<#{ OFFTAGS }([^>]*)>/)
1059 tag = $1
1062 tag = $1
1060 $2.to_s.match(/(class\=\S+)/i)
1063 $2.to_s.match(/(class\=\S+)/i)
1061 tag << " #{$1}" if $1
1064 tag << " #{$1}" if $1
1062 @pre_list << "<#{ tag }>#{ aftertag }"
1065 @pre_list << "<#{ tag }>#{ aftertag }"
1063 end
1066 end
1064 elsif $1 and codepre > 0
1067 elsif $1 and codepre > 0
1065 if codepre - used_offtags.length > 0
1068 if codepre - used_offtags.length > 0
1066 htmlesc( line, :NoQuotes )
1069 htmlesc( line, :NoQuotes )
1067 @pre_list.last << line
1070 @pre_list.last << line
1068 line = ""
1071 line = ""
1069 end
1072 end
1070 codepre -= 1 unless codepre.zero?
1073 codepre -= 1 unless codepre.zero?
1071 used_offtags = {} if codepre.zero?
1074 used_offtags = {} if codepre.zero?
1072 end
1075 end
1073 line
1076 line
1074 end
1077 end
1075 end
1078 end
1076 text
1079 text
1077 end
1080 end
1078
1081
1079 def smooth_offtags( text )
1082 def smooth_offtags( text )
1080 unless @pre_list.empty?
1083 unless @pre_list.empty?
1081 ## replace <pre> content
1084 ## replace <pre> content
1082 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1085 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1083 end
1086 end
1084 end
1087 end
1085
1088
1086 def inline( text )
1089 def inline( text )
1087 [/^inline_/, /^glyphs_/].each do |meth_re|
1090 [/^inline_/, /^glyphs_/].each do |meth_re|
1088 @rules.each do |rule_name|
1091 @rules.each do |rule_name|
1089 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1092 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1090 end
1093 end
1091 end
1094 end
1092 end
1095 end
1093
1096
1094 def h_align( text )
1097 def h_align( text )
1095 H_ALGN_VALS[text]
1098 H_ALGN_VALS[text]
1096 end
1099 end
1097
1100
1098 def v_align( text )
1101 def v_align( text )
1099 V_ALGN_VALS[text]
1102 V_ALGN_VALS[text]
1100 end
1103 end
1101
1104
1102 def textile_popup_help( name, windowW, windowH )
1105 def textile_popup_help( name, windowW, windowH )
1103 ' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
1106 ' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
1104 end
1107 end
1105
1108
1106 # HTML cleansing stuff
1109 # HTML cleansing stuff
1107 BASIC_TAGS = {
1110 BASIC_TAGS = {
1108 'a' => ['href', 'title'],
1111 'a' => ['href', 'title'],
1109 'img' => ['src', 'alt', 'title'],
1112 'img' => ['src', 'alt', 'title'],
1110 'br' => [],
1113 'br' => [],
1111 'i' => nil,
1114 'i' => nil,
1112 'u' => nil,
1115 'u' => nil,
1113 'b' => nil,
1116 'b' => nil,
1114 'pre' => nil,
1117 'pre' => nil,
1115 'kbd' => nil,
1118 'kbd' => nil,
1116 'code' => ['lang'],
1119 'code' => ['lang'],
1117 'cite' => nil,
1120 'cite' => nil,
1118 'strong' => nil,
1121 'strong' => nil,
1119 'em' => nil,
1122 'em' => nil,
1120 'ins' => nil,
1123 'ins' => nil,
1121 'sup' => nil,
1124 'sup' => nil,
1122 'sub' => nil,
1125 'sub' => nil,
1123 'del' => nil,
1126 'del' => nil,
1124 'table' => nil,
1127 'table' => nil,
1125 'tr' => nil,
1128 'tr' => nil,
1126 'td' => ['colspan', 'rowspan'],
1129 'td' => ['colspan', 'rowspan'],
1127 'th' => nil,
1130 'th' => nil,
1128 'ol' => nil,
1131 'ol' => nil,
1129 'ul' => nil,
1132 'ul' => nil,
1130 'li' => nil,
1133 'li' => nil,
1131 'p' => nil,
1134 'p' => nil,
1132 'h1' => nil,
1135 'h1' => nil,
1133 'h2' => nil,
1136 'h2' => nil,
1134 'h3' => nil,
1137 'h3' => nil,
1135 'h4' => nil,
1138 'h4' => nil,
1136 'h5' => nil,
1139 'h5' => nil,
1137 'h6' => nil,
1140 'h6' => nil,
1138 'blockquote' => ['cite']
1141 'blockquote' => ['cite']
1139 }
1142 }
1140
1143
1141 def clean_html( text, tags = BASIC_TAGS )
1144 def clean_html( text, tags = BASIC_TAGS )
1142 text.gsub!( /<!\[CDATA\[/, '' )
1145 text.gsub!( /<!\[CDATA\[/, '' )
1143 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1146 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1144 raw = $~
1147 raw = $~
1145 tag = raw[2].downcase
1148 tag = raw[2].downcase
1146 if tags.has_key? tag
1149 if tags.has_key? tag
1147 pcs = [tag]
1150 pcs = [tag]
1148 tags[tag].each do |prop|
1151 tags[tag].each do |prop|
1149 ['"', "'", ''].each do |q|
1152 ['"', "'", ''].each do |q|
1150 q2 = ( q != '' ? q : '\s' )
1153 q2 = ( q != '' ? q : '\s' )
1151 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1154 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1152 attrv = $1
1155 attrv = $1
1153 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1156 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1154 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1157 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1155 break
1158 break
1156 end
1159 end
1157 end
1160 end
1158 end if tags[tag]
1161 end if tags[tag]
1159 "<#{raw[1]}#{pcs.join " "}>"
1162 "<#{raw[1]}#{pcs.join " "}>"
1160 else
1163 else
1161 " "
1164 " "
1162 end
1165 end
1163 end
1166 end
1164 end
1167 end
1165
1168
1166 ALLOWED_TAGS = %w(redpre pre code notextile)
1169 ALLOWED_TAGS = %w(redpre pre code notextile)
1167
1170
1168 def escape_html_tags(text)
1171 def escape_html_tags(text)
1169 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1172 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1170 end
1173 end
1171 end
1174 end
1172
1175
@@ -1,433 +1,443
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../../test_helper'
18 require File.dirname(__FILE__) + '/../../test_helper'
19
19
20 class ApplicationHelperTest < HelperTestCase
20 class ApplicationHelperTest < HelperTestCase
21 include ApplicationHelper
21 include ApplicationHelper
22 include ActionView::Helpers::TextHelper
22 include ActionView::Helpers::TextHelper
23 fixtures :projects, :roles, :enabled_modules, :users,
23 fixtures :projects, :roles, :enabled_modules, :users,
24 :repositories, :changesets,
24 :repositories, :changesets,
25 :trackers, :issue_statuses, :issues, :versions, :documents,
25 :trackers, :issue_statuses, :issues, :versions, :documents,
26 :wikis, :wiki_pages, :wiki_contents,
26 :wikis, :wiki_pages, :wiki_contents,
27 :boards, :messages,
27 :boards, :messages,
28 :attachments
28 :attachments
29
29
30 def setup
30 def setup
31 super
31 super
32 end
32 end
33
33
34 def test_auto_links
34 def test_auto_links
35 to_test = {
35 to_test = {
36 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
36 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
37 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
37 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
38 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
38 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
39 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
39 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
40 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
40 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
41 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
41 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
42 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
42 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
43 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
43 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
44 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
44 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
45 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
45 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
46 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
46 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
47 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
47 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
48 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
48 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
49 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
49 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
50 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
50 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
51 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
51 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
52 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
52 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
53 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
53 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
54 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
54 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
55 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
55 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
56 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
56 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
57 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
57 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
58 }
58 }
59 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
59 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
60 end
60 end
61
61
62 def test_auto_mailto
62 def test_auto_mailto
63 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
63 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
64 textilizable('test@foo.bar')
64 textilizable('test@foo.bar')
65 end
65 end
66
66
67 def test_inline_images
67 def test_inline_images
68 to_test = {
68 to_test = {
69 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
69 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
70 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
70 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
71 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
71 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
72 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height100px;" alt="" />',
72 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height100px;" alt="" />',
73 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
73 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
74 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
74 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
75 }
75 }
76 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
76 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
77 end
77 end
78
78
79 def test_acronyms
80 to_test = {
81 'this is an acronym: GPL(General Public License)' => 'this is an acronym: <acronym title="General Public License">GPL</acronym>',
82 'GPL(This is a double-quoted "title")' => '<acronym title="This is a double-quoted &quot;title&quot;">GPL</acronym>',
83 }
84 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
85
86 end
87
79 def test_attached_images
88 def test_attached_images
80 to_test = {
89 to_test = {
81 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
90 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
82 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />'
91 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />'
83 }
92 }
84 attachments = Attachment.find(:all)
93 attachments = Attachment.find(:all)
85 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
94 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
86 end
95 end
87
96
88 def test_textile_external_links
97 def test_textile_external_links
89 to_test = {
98 to_test = {
90 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
99 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
91 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
100 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
92 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
101 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
102 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
93 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
103 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
94 # no multiline link text
104 # no multiline link text
95 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />\nand another on a second line\":test"
105 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />\nand another on a second line\":test"
96 }
106 }
97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
107 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
98 end
108 end
99
109
100 def test_redmine_links
110 def test_redmine_links
101 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
111 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
102 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
112 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
103
113
104 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
114 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
105 :class => 'changeset', :title => 'My very first commit')
115 :class => 'changeset', :title => 'My very first commit')
106
116
107 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
117 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
108 :class => 'document')
118 :class => 'document')
109
119
110 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
120 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
111 :class => 'version')
121 :class => 'version')
112
122
113 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
123 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
114
124
115 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
125 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
116 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
126 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
117
127
118 to_test = {
128 to_test = {
119 # tickets
129 # tickets
120 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
130 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
121 # changesets
131 # changesets
122 'r1' => changeset_link,
132 'r1' => changeset_link,
123 # documents
133 # documents
124 'document#1' => document_link,
134 'document#1' => document_link,
125 'document:"Test document"' => document_link,
135 'document:"Test document"' => document_link,
126 # versions
136 # versions
127 'version#2' => version_link,
137 'version#2' => version_link,
128 'version:1.0' => version_link,
138 'version:1.0' => version_link,
129 'version:"1.0"' => version_link,
139 'version:"1.0"' => version_link,
130 # source
140 # source
131 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
141 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
132 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
142 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
133 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
143 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
134 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
144 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
135 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
145 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
136 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
146 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
137 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
147 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
138 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
148 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
139 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
149 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
140 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
150 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
141 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
151 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
142 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
152 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
143 # message
153 # message
144 'message#4' => link_to('Post 2', message_url, :class => 'message'),
154 'message#4' => link_to('Post 2', message_url, :class => 'message'),
145 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
155 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
146 # escaping
156 # escaping
147 '!#3.' => '#3.',
157 '!#3.' => '#3.',
148 '!r1' => 'r1',
158 '!r1' => 'r1',
149 '!document#1' => 'document#1',
159 '!document#1' => 'document#1',
150 '!document:"Test document"' => 'document:"Test document"',
160 '!document:"Test document"' => 'document:"Test document"',
151 '!version#2' => 'version#2',
161 '!version#2' => 'version#2',
152 '!version:1.0' => 'version:1.0',
162 '!version:1.0' => 'version:1.0',
153 '!version:"1.0"' => 'version:"1.0"',
163 '!version:"1.0"' => 'version:"1.0"',
154 '!source:/some/file' => 'source:/some/file',
164 '!source:/some/file' => 'source:/some/file',
155 # invalid expressions
165 # invalid expressions
156 'source:' => 'source:',
166 'source:' => 'source:',
157 # url hash
167 # url hash
158 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
168 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
159 }
169 }
160 @project = Project.find(1)
170 @project = Project.find(1)
161 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
171 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
162 end
172 end
163
173
164 def test_wiki_links
174 def test_wiki_links
165 to_test = {
175 to_test = {
166 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
176 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
167 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
177 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
168 # link with anchor
178 # link with anchor
169 '[[CookBook documentation#One-section]]' => '<a href="/wiki/ecookbook/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
179 '[[CookBook documentation#One-section]]' => '<a href="/wiki/ecookbook/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
170 '[[Another page#anchor|Page]]' => '<a href="/wiki/ecookbook/Another_page#anchor" class="wiki-page">Page</a>',
180 '[[Another page#anchor|Page]]' => '<a href="/wiki/ecookbook/Another_page#anchor" class="wiki-page">Page</a>',
171 # page that doesn't exist
181 # page that doesn't exist
172 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
182 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
173 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
183 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
174 # link to another project wiki
184 # link to another project wiki
175 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
185 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
176 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
186 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
177 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
187 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
178 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
188 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
179 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
189 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
180 # striked through link
190 # striked through link
181 '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>',
191 '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>',
182 '-[[Another page|Page]] link-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a> link</del>',
192 '-[[Another page|Page]] link-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a> link</del>',
183 # escaping
193 # escaping
184 '![[Another page|Page]]' => '[[Another page|Page]]',
194 '![[Another page|Page]]' => '[[Another page|Page]]',
185 }
195 }
186 @project = Project.find(1)
196 @project = Project.find(1)
187 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
197 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
188 end
198 end
189
199
190 def test_html_tags
200 def test_html_tags
191 to_test = {
201 to_test = {
192 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
202 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
193 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
203 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
194 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
204 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
195 # do not escape pre/code tags
205 # do not escape pre/code tags
196 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
206 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
197 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
207 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
198 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
208 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
199 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
209 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
200 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
210 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
201 # remove attributes except class
211 # remove attributes except class
202 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
212 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
203 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
213 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
204 }
214 }
205 to_test.each { |text, result| assert_equal result, textilizable(text) }
215 to_test.each { |text, result| assert_equal result, textilizable(text) }
206 end
216 end
207
217
208 def test_allowed_html_tags
218 def test_allowed_html_tags
209 to_test = {
219 to_test = {
210 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
220 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
211 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
221 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
212 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
222 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
213 }
223 }
214 to_test.each { |text, result| assert_equal result, textilizable(text) }
224 to_test.each { |text, result| assert_equal result, textilizable(text) }
215 end
225 end
216
226
217 def syntax_highlight
227 def syntax_highlight
218 raw = <<-RAW
228 raw = <<-RAW
219 <pre><code class="ruby">
229 <pre><code class="ruby">
220 # Some ruby code here
230 # Some ruby code here
221 </pre></code>
231 </pre></code>
222 RAW
232 RAW
223
233
224 expected = <<-EXPECTED
234 expected = <<-EXPECTED
225 <pre><code class="ruby CodeRay"><span class="no">1</span> <span class="c"># Some ruby code here</span>
235 <pre><code class="ruby CodeRay"><span class="no">1</span> <span class="c"># Some ruby code here</span>
226 </pre></code>
236 </pre></code>
227 EXPECTED
237 EXPECTED
228
238
229 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
239 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
230 end
240 end
231
241
232 def test_wiki_links_in_tables
242 def test_wiki_links_in_tables
233 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
243 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
234 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
244 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
235 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
245 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
236 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
246 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
237 }
247 }
238 @project = Project.find(1)
248 @project = Project.find(1)
239 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
249 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
240 end
250 end
241
251
242 def test_text_formatting
252 def test_text_formatting
243 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
253 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
244 '(_text within parentheses_)' => '(<em>text within parentheses</em>)'
254 '(_text within parentheses_)' => '(<em>text within parentheses</em>)'
245 }
255 }
246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
256 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
247 end
257 end
248
258
249 def test_wiki_horizontal_rule
259 def test_wiki_horizontal_rule
250 assert_equal '<hr />', textilizable('---')
260 assert_equal '<hr />', textilizable('---')
251 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
261 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
252 end
262 end
253
263
254 def test_acronym
264 def test_acronym
255 assert_equal '<p>This is an acronym: <acronym title="American Civil Liberties Union">ACLU</acronym>.</p>',
265 assert_equal '<p>This is an acronym: <acronym title="American Civil Liberties Union">ACLU</acronym>.</p>',
256 textilizable('This is an acronym: ACLU(American Civil Liberties Union).')
266 textilizable('This is an acronym: ACLU(American Civil Liberties Union).')
257 end
267 end
258
268
259 def test_footnotes
269 def test_footnotes
260 raw = <<-RAW
270 raw = <<-RAW
261 This is some text[1].
271 This is some text[1].
262
272
263 fn1. This is the foot note
273 fn1. This is the foot note
264 RAW
274 RAW
265
275
266 expected = <<-EXPECTED
276 expected = <<-EXPECTED
267 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
277 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
268 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
278 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
269 EXPECTED
279 EXPECTED
270
280
271 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
281 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
272 end
282 end
273
283
274 def test_table_of_content
284 def test_table_of_content
275 raw = <<-RAW
285 raw = <<-RAW
276 {{toc}}
286 {{toc}}
277
287
278 h1. Title
288 h1. Title
279
289
280 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
290 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
281
291
282 h2. Subtitle
292 h2. Subtitle
283
293
284 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
294 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
285
295
286 h2. Subtitle with %{color:red}red text%
296 h2. Subtitle with %{color:red}red text%
287
297
288 h1. Another title
298 h1. Another title
289
299
290 RAW
300 RAW
291
301
292 expected = '<ul class="toc">' +
302 expected = '<ul class="toc">' +
293 '<li class="heading1"><a href="#Title">Title</a></li>' +
303 '<li class="heading1"><a href="#Title">Title</a></li>' +
294 '<li class="heading2"><a href="#Subtitle">Subtitle</a></li>' +
304 '<li class="heading2"><a href="#Subtitle">Subtitle</a></li>' +
295 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
305 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
296 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
306 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
297 '</ul>'
307 '</ul>'
298
308
299 assert textilizable(raw).gsub("\n", "").include?(expected)
309 assert textilizable(raw).gsub("\n", "").include?(expected)
300 end
310 end
301
311
302 def test_blockquote
312 def test_blockquote
303 # orig raw text
313 # orig raw text
304 raw = <<-RAW
314 raw = <<-RAW
305 John said:
315 John said:
306 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
316 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
307 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
317 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
308 > * Donec odio lorem,
318 > * Donec odio lorem,
309 > * sagittis ac,
319 > * sagittis ac,
310 > * malesuada in,
320 > * malesuada in,
311 > * adipiscing eu, dolor.
321 > * adipiscing eu, dolor.
312 >
322 >
313 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
323 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
314 > Proin a tellus. Nam vel neque.
324 > Proin a tellus. Nam vel neque.
315
325
316 He's right.
326 He's right.
317 RAW
327 RAW
318
328
319 # expected html
329 # expected html
320 expected = <<-EXPECTED
330 expected = <<-EXPECTED
321 <p>John said:</p>
331 <p>John said:</p>
322 <blockquote>
332 <blockquote>
323 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
333 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
324 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
334 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
325 <ul>
335 <ul>
326 <li>Donec odio lorem,</li>
336 <li>Donec odio lorem,</li>
327 <li>sagittis ac,</li>
337 <li>sagittis ac,</li>
328 <li>malesuada in,</li>
338 <li>malesuada in,</li>
329 <li>adipiscing eu, dolor.</li>
339 <li>adipiscing eu, dolor.</li>
330 </ul>
340 </ul>
331 <blockquote>
341 <blockquote>
332 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
342 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
333 </blockquote>
343 </blockquote>
334 <p>Proin a tellus. Nam vel neque.</p>
344 <p>Proin a tellus. Nam vel neque.</p>
335 </blockquote>
345 </blockquote>
336 <p>He's right.</p>
346 <p>He's right.</p>
337 EXPECTED
347 EXPECTED
338
348
339 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
349 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
340 end
350 end
341
351
342 def test_table
352 def test_table
343 raw = <<-RAW
353 raw = <<-RAW
344 This is a table with empty cells:
354 This is a table with empty cells:
345
355
346 |cell11|cell12||
356 |cell11|cell12||
347 |cell21||cell23|
357 |cell21||cell23|
348 |cell31|cell32|cell33|
358 |cell31|cell32|cell33|
349 RAW
359 RAW
350
360
351 expected = <<-EXPECTED
361 expected = <<-EXPECTED
352 <p>This is a table with empty cells:</p>
362 <p>This is a table with empty cells:</p>
353
363
354 <table>
364 <table>
355 <tr><td>cell11</td><td>cell12</td><td></td></tr>
365 <tr><td>cell11</td><td>cell12</td><td></td></tr>
356 <tr><td>cell21</td><td></td><td>cell23</td></tr>
366 <tr><td>cell21</td><td></td><td>cell23</td></tr>
357 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
367 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
358 </table>
368 </table>
359 EXPECTED
369 EXPECTED
360
370
361 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
371 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
362 end
372 end
363
373
364 def test_default_formatter
374 def test_default_formatter
365 Setting.text_formatting = 'unknown'
375 Setting.text_formatting = 'unknown'
366 text = 'a *link*: http://www.example.net/'
376 text = 'a *link*: http://www.example.net/'
367 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
377 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
368 Setting.text_formatting = 'textile'
378 Setting.text_formatting = 'textile'
369 end
379 end
370
380
371 def test_date_format_default
381 def test_date_format_default
372 today = Date.today
382 today = Date.today
373 Setting.date_format = ''
383 Setting.date_format = ''
374 assert_equal l_date(today), format_date(today)
384 assert_equal l_date(today), format_date(today)
375 end
385 end
376
386
377 def test_date_format
387 def test_date_format
378 today = Date.today
388 today = Date.today
379 Setting.date_format = '%d %m %Y'
389 Setting.date_format = '%d %m %Y'
380 assert_equal today.strftime('%d %m %Y'), format_date(today)
390 assert_equal today.strftime('%d %m %Y'), format_date(today)
381 end
391 end
382
392
383 def test_time_format_default
393 def test_time_format_default
384 now = Time.now
394 now = Time.now
385 Setting.date_format = ''
395 Setting.date_format = ''
386 Setting.time_format = ''
396 Setting.time_format = ''
387 assert_equal l_datetime(now), format_time(now)
397 assert_equal l_datetime(now), format_time(now)
388 assert_equal l_time(now), format_time(now, false)
398 assert_equal l_time(now), format_time(now, false)
389 end
399 end
390
400
391 def test_time_format
401 def test_time_format
392 now = Time.now
402 now = Time.now
393 Setting.date_format = '%d %m %Y'
403 Setting.date_format = '%d %m %Y'
394 Setting.time_format = '%H %M'
404 Setting.time_format = '%H %M'
395 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
405 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
396 assert_equal now.strftime('%H %M'), format_time(now, false)
406 assert_equal now.strftime('%H %M'), format_time(now, false)
397 end
407 end
398
408
399 def test_utc_time_format
409 def test_utc_time_format
400 now = Time.now.utc
410 now = Time.now.utc
401 Setting.date_format = '%d %m %Y'
411 Setting.date_format = '%d %m %Y'
402 Setting.time_format = '%H %M'
412 Setting.time_format = '%H %M'
403 assert_equal Time.now.strftime('%d %m %Y %H %M'), format_time(now)
413 assert_equal Time.now.strftime('%d %m %Y %H %M'), format_time(now)
404 assert_equal Time.now.strftime('%H %M'), format_time(now, false)
414 assert_equal Time.now.strftime('%H %M'), format_time(now, false)
405 end
415 end
406
416
407 def test_due_date_distance_in_words
417 def test_due_date_distance_in_words
408 to_test = { Date.today => 'Due in 0 days',
418 to_test = { Date.today => 'Due in 0 days',
409 Date.today + 1 => 'Due in 1 day',
419 Date.today + 1 => 'Due in 1 day',
410 Date.today + 100 => 'Due in 100 days',
420 Date.today + 100 => 'Due in 100 days',
411 Date.today + 20000 => 'Due in 20000 days',
421 Date.today + 20000 => 'Due in 20000 days',
412 Date.today - 1 => '1 day late',
422 Date.today - 1 => '1 day late',
413 Date.today - 100 => '100 days late',
423 Date.today - 100 => '100 days late',
414 Date.today - 20000 => '20000 days late',
424 Date.today - 20000 => '20000 days late',
415 }
425 }
416 to_test.each do |date, expected|
426 to_test.each do |date, expected|
417 assert_equal expected, due_date_distance_in_words(date)
427 assert_equal expected, due_date_distance_in_words(date)
418 end
428 end
419 end
429 end
420
430
421 def test_avatar
431 def test_avatar
422 # turn on avatars
432 # turn on avatars
423 Setting.gravatar_enabled = '1'
433 Setting.gravatar_enabled = '1'
424 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
434 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
425 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
435 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
426 assert_nil avatar('jsmith')
436 assert_nil avatar('jsmith')
427 assert_nil avatar(nil)
437 assert_nil avatar(nil)
428
438
429 # turn off avatars
439 # turn off avatars
430 Setting.gravatar_enabled = '0'
440 Setting.gravatar_enabled = '0'
431 assert_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
441 assert_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
432 end
442 end
433 end
443 end
General Comments 0
You need to be logged in to leave comments. Login now