##// END OF EJS Templates
Fixed that ordered/unordered lists inside table cell are mangled (#14038)....
Jean-Philippe Lang -
r11615:b3d80d50a3a1
parent child
Show More
@@ -1,1208 +1,1209
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 = [:block_textile_table, :block_textile_lists,
273 textile_rules = [: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 # need to do this before #hard_break and #blocks
299 # need to do this before #hard_break and #blocks
300 block_textile_quotes text unless @lite_mode
300 block_textile_quotes text unless @lite_mode
301 hard_break text
301 hard_break text
302 unless @lite_mode
302 unless @lite_mode
303 refs text
303 refs 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_JOIN = QTAGS.map {|rc, ht, rtype| Regexp::quote rc}.join('|')
376 QTAGS_JOIN = QTAGS.map {|rc, ht, rtype| Regexp::quote rc}.join('|')
377
377
378 QTAGS.collect! do |rc, ht, rtype|
378 QTAGS.collect! do |rc, ht, rtype|
379 rcq = Regexp::quote rc
379 rcq = Regexp::quote rc
380 re =
380 re =
381 case rtype
381 case rtype
382 when :limit
382 when :limit
383 /(^|[>\s\(]) # sta
383 /(^|[>\s\(]) # sta
384 (?!\-\-)
384 (?!\-\-)
385 (#{QTAGS_JOIN}|) # oqs
385 (#{QTAGS_JOIN}|) # oqs
386 (#{rcq}) # qtag
386 (#{rcq}) # qtag
387 (\w|[^\s].*?[^\s]) # content
387 (\w|[^\s].*?[^\s]) # content
388 (?!\-\-)
388 (?!\-\-)
389 #{rcq}
389 #{rcq}
390 (#{QTAGS_JOIN}|) # oqa
390 (#{QTAGS_JOIN}|) # oqa
391 (?=[[:punct:]]|<|\s|\)|$)/x
391 (?=[[:punct:]]|<|\s|\)|$)/x
392 else
392 else
393 /(#{rcq})
393 /(#{rcq})
394 (#{C})
394 (#{C})
395 (?::(\S+))?
395 (?::(\S+))?
396 (\w|[^\s\-].*?[^\s\-])
396 (\w|[^\s\-].*?[^\s\-])
397 #{rcq}/xm
397 #{rcq}/xm
398 end
398 end
399 [rc, ht, re, rtype]
399 [rc, ht, re, rtype]
400 end
400 end
401
401
402 # Elements to handle
402 # Elements to handle
403 GLYPHS = [
403 GLYPHS = [
404 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
404 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
405 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
405 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
406 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
406 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
407 # [ /\'/, '&#8216;' ], # single opening
407 # [ /\'/, '&#8216;' ], # single opening
408 # [ /</, '&lt;' ], # less-than
408 # [ /</, '&lt;' ], # less-than
409 # [ />/, '&gt;' ], # greater-than
409 # [ />/, '&gt;' ], # greater-than
410 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
410 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
411 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
411 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
412 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
412 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
413 # [ /"/, '&#8220;' ], # double opening
413 # [ /"/, '&#8220;' ], # double opening
414 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
414 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
415 # [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
415 # [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
416 # [ /(^|[^"][>\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
416 # [ /(^|[^"][>\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
417 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
417 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
418 # [ /\s->\s/, ' &rarr; ' ], # right arrow
418 # [ /\s->\s/, ' &rarr; ' ], # right arrow
419 # [ /\s-\s/, ' &#8211; ' ], # en dash
419 # [ /\s-\s/, ' &#8211; ' ], # en dash
420 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
420 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
421 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
421 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
422 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
422 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
423 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
423 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
424 ]
424 ]
425
425
426 H_ALGN_VALS = {
426 H_ALGN_VALS = {
427 '<' => 'left',
427 '<' => 'left',
428 '=' => 'center',
428 '=' => 'center',
429 '>' => 'right',
429 '>' => 'right',
430 '<>' => 'justify'
430 '<>' => 'justify'
431 }
431 }
432
432
433 V_ALGN_VALS = {
433 V_ALGN_VALS = {
434 '^' => 'top',
434 '^' => 'top',
435 '-' => 'middle',
435 '-' => 'middle',
436 '~' => 'bottom'
436 '~' => 'bottom'
437 }
437 }
438
438
439 #
439 #
440 # Flexible HTML escaping
440 # Flexible HTML escaping
441 #
441 #
442 def htmlesc( str, mode=:Quotes )
442 def htmlesc( str, mode=:Quotes )
443 if str
443 if str
444 str.gsub!( '&', '&amp;' )
444 str.gsub!( '&', '&amp;' )
445 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
445 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
446 str.gsub!( "'", '&#039;' ) if mode == :Quotes
446 str.gsub!( "'", '&#039;' ) if mode == :Quotes
447 str.gsub!( '<', '&lt;')
447 str.gsub!( '<', '&lt;')
448 str.gsub!( '>', '&gt;')
448 str.gsub!( '>', '&gt;')
449 end
449 end
450 str
450 str
451 end
451 end
452
452
453 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
453 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
454 def pgl( text )
454 def pgl( text )
455 #GLYPHS.each do |re, resub, tog|
455 #GLYPHS.each do |re, resub, tog|
456 # next if tog and method( tog ).call
456 # next if tog and method( tog ).call
457 # text.gsub! re, resub
457 # text.gsub! re, resub
458 #end
458 #end
459 text.gsub!(/\b([A-Z][A-Z0-9]{1,})\b(?:[(]([^)]*)[)])/) do |m|
459 text.gsub!(/\b([A-Z][A-Z0-9]{1,})\b(?:[(]([^)]*)[)])/) do |m|
460 "<acronym title=\"#{htmlesc $2}\">#{$1}</acronym>"
460 "<acronym title=\"#{htmlesc $2}\">#{$1}</acronym>"
461 end
461 end
462 end
462 end
463
463
464 # Parses Textile attribute lists and builds an HTML attribute string
464 # Parses Textile attribute lists and builds an HTML attribute string
465 def pba( text_in, element = "" )
465 def pba( text_in, element = "" )
466
466
467 return '' unless text_in
467 return '' unless text_in
468
468
469 style = []
469 style = []
470 text = text_in.dup
470 text = text_in.dup
471 if element == 'td'
471 if element == 'td'
472 colspan = $1 if text =~ /\\(\d+)/
472 colspan = $1 if text =~ /\\(\d+)/
473 rowspan = $1 if text =~ /\/(\d+)/
473 rowspan = $1 if text =~ /\/(\d+)/
474 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
474 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
475 end
475 end
476
476
477 if text.sub!( /\{([^"}]*)\}/, '' ) && !filter_styles
477 if text.sub!( /\{([^"}]*)\}/, '' ) && !filter_styles
478 sanitized = sanitize_styles($1)
478 sanitized = sanitize_styles($1)
479 style << "#{ sanitized };" unless sanitized.blank?
479 style << "#{ sanitized };" unless sanitized.blank?
480 end
480 end
481
481
482 lang = $1 if
482 lang = $1 if
483 text.sub!( /\[([^)]+?)\]/, '' )
483 text.sub!( /\[([^)]+?)\]/, '' )
484
484
485 cls = $1 if
485 cls = $1 if
486 text.sub!( /\(([^()]+?)\)/, '' )
486 text.sub!( /\(([^()]+?)\)/, '' )
487
487
488 style << "padding-left:#{ $1.length }em;" if
488 style << "padding-left:#{ $1.length }em;" if
489 text.sub!( /([(]+)/, '' )
489 text.sub!( /([(]+)/, '' )
490
490
491 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
491 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
492
492
493 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
493 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
494
494
495 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
495 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
496
496
497 atts = ''
497 atts = ''
498 atts << " style=\"#{ style.join }\"" unless style.empty?
498 atts << " style=\"#{ style.join }\"" unless style.empty?
499 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
499 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
500 atts << " lang=\"#{ lang }\"" if lang
500 atts << " lang=\"#{ lang }\"" if lang
501 atts << " id=\"#{ id }\"" if id
501 atts << " id=\"#{ id }\"" if id
502 atts << " colspan=\"#{ colspan }\"" if colspan
502 atts << " colspan=\"#{ colspan }\"" if colspan
503 atts << " rowspan=\"#{ rowspan }\"" if rowspan
503 atts << " rowspan=\"#{ rowspan }\"" if rowspan
504
504
505 atts
505 atts
506 end
506 end
507
507
508 STYLES_RE = /^(color|width|height|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
508 STYLES_RE = /^(color|width|height|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
509
509
510 def sanitize_styles(str)
510 def sanitize_styles(str)
511 styles = str.split(";").map(&:strip)
511 styles = str.split(";").map(&:strip)
512 styles.reject! do |style|
512 styles.reject! do |style|
513 !style.match(STYLES_RE)
513 !style.match(STYLES_RE)
514 end
514 end
515 styles.join(";")
515 styles.join(";")
516 end
516 end
517
517
518 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
518 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
519
519
520 # Parses a Textile table block, building HTML from the result.
520 # Parses a Textile table block, building HTML from the result.
521 def block_textile_table( text )
521 def block_textile_table( text )
522 text.gsub!( TABLE_RE ) do |matches|
522 text.gsub!( TABLE_RE ) do |matches|
523
523
524 tatts, fullrow = $~[1..2]
524 tatts, fullrow = $~[1..2]
525 tatts = pba( tatts, 'table' )
525 tatts = pba( tatts, 'table' )
526 tatts = shelve( tatts ) if tatts
526 tatts = shelve( tatts ) if tatts
527 rows = []
527 rows = []
528
528 fullrow.gsub!(/([^|])\n/, "\\1<br />")
529 fullrow.each_line do |row|
529 fullrow.each_line do |row|
530 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
530 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
531 cells = []
531 cells = []
532 row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
532 row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
533 Rails.logger.debug "cell: #{cell}"
533 next if cell == '|'
534 next if cell == '|'
534 ctyp = 'd'
535 ctyp = 'd'
535 ctyp = 'h' if cell =~ /^_/
536 ctyp = 'h' if cell =~ /^_/
536
537
537 catts = ''
538 catts = ''
538 catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
539 catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
539
540
540 catts = shelve( catts ) if catts
541 catts = shelve( catts ) if catts
541 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
542 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
542 end
543 end
543 ratts = shelve( ratts ) if ratts
544 ratts = shelve( ratts ) if ratts
544 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
545 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
545 end
546 end
546 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
547 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
547 end
548 end
548 end
549 end
549
550
550 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
551 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
551 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
552 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
552
553
553 # Parses Textile lists and generates HTML
554 # Parses Textile lists and generates HTML
554 def block_textile_lists( text )
555 def block_textile_lists( text )
555 text.gsub!( LISTS_RE ) do |match|
556 text.gsub!( LISTS_RE ) do |match|
556 lines = match.split( /\n/ )
557 lines = match.split( /\n/ )
557 last_line = -1
558 last_line = -1
558 depth = []
559 depth = []
559 lines.each_with_index do |line, line_id|
560 lines.each_with_index do |line, line_id|
560 if line =~ LISTS_CONTENT_RE
561 if line =~ LISTS_CONTENT_RE
561 tl,atts,content = $~[1..3]
562 tl,atts,content = $~[1..3]
562 if depth.last
563 if depth.last
563 if depth.last.length > tl.length
564 if depth.last.length > tl.length
564 (depth.length - 1).downto(0) do |i|
565 (depth.length - 1).downto(0) do |i|
565 break if depth[i].length == tl.length
566 break if depth[i].length == tl.length
566 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
567 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
567 depth.pop
568 depth.pop
568 end
569 end
569 end
570 end
570 if depth.last and depth.last.length == tl.length
571 if depth.last and depth.last.length == tl.length
571 lines[line_id - 1] << '</li>'
572 lines[line_id - 1] << '</li>'
572 end
573 end
573 end
574 end
574 unless depth.last == tl
575 unless depth.last == tl
575 depth << tl
576 depth << tl
576 atts = pba( atts )
577 atts = pba( atts )
577 atts = shelve( atts ) if atts
578 atts = shelve( atts ) if atts
578 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
579 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
579 else
580 else
580 lines[line_id] = "\t\t<li>#{ content }"
581 lines[line_id] = "\t\t<li>#{ content }"
581 end
582 end
582 last_line = line_id
583 last_line = line_id
583
584
584 else
585 else
585 last_line = line_id
586 last_line = line_id
586 end
587 end
587 if line_id - last_line > 1 or line_id == lines.length - 1
588 if line_id - last_line > 1 or line_id == lines.length - 1
588 while v = depth.pop
589 while v = depth.pop
589 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
590 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
590 end
591 end
591 end
592 end
592 end
593 end
593 lines.join( "\n" )
594 lines.join( "\n" )
594 end
595 end
595 end
596 end
596
597
597 QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m
598 QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m
598 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
599 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
599
600
600 def block_textile_quotes( text )
601 def block_textile_quotes( text )
601 text.gsub!( QUOTES_RE ) do |match|
602 text.gsub!( QUOTES_RE ) do |match|
602 lines = match.split( /\n/ )
603 lines = match.split( /\n/ )
603 quotes = ''
604 quotes = ''
604 indent = 0
605 indent = 0
605 lines.each do |line|
606 lines.each do |line|
606 line =~ QUOTES_CONTENT_RE
607 line =~ QUOTES_CONTENT_RE
607 bq,content = $1, $2
608 bq,content = $1, $2
608 l = bq.count('>')
609 l = bq.count('>')
609 if l != indent
610 if l != indent
610 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
611 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
611 indent = l
612 indent = l
612 end
613 end
613 quotes << (content + "\n")
614 quotes << (content + "\n")
614 end
615 end
615 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
616 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
616 quotes
617 quotes
617 end
618 end
618 end
619 end
619
620
620 CODE_RE = /(\W)
621 CODE_RE = /(\W)
621 @
622 @
622 (?:\|(\w+?)\|)?
623 (?:\|(\w+?)\|)?
623 (.+?)
624 (.+?)
624 @
625 @
625 (?=\W)/x
626 (?=\W)/x
626
627
627 def inline_textile_code( text )
628 def inline_textile_code( text )
628 text.gsub!( CODE_RE ) do |m|
629 text.gsub!( CODE_RE ) do |m|
629 before,lang,code,after = $~[1..4]
630 before,lang,code,after = $~[1..4]
630 lang = " lang=\"#{ lang }\"" if lang
631 lang = " lang=\"#{ lang }\"" if lang
631 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }", false )
632 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }", false )
632 end
633 end
633 end
634 end
634
635
635 def lT( text )
636 def lT( text )
636 text =~ /\#$/ ? 'o' : 'u'
637 text =~ /\#$/ ? 'o' : 'u'
637 end
638 end
638
639
639 def hard_break( text )
640 def hard_break( text )
640 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
641 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
641 end
642 end
642
643
643 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
644 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
644
645
645 def blocks( text, deep_code = false )
646 def blocks( text, deep_code = false )
646 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
647 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
647 plain = blk !~ /\A[#*> ]/
648 plain = blk !~ /\A[#*> ]/
648
649
649 # skip blocks that are complex HTML
650 # skip blocks that are complex HTML
650 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
651 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
651 blk
652 blk
652 else
653 else
653 # search for indentation levels
654 # search for indentation levels
654 blk.strip!
655 blk.strip!
655 if blk.empty?
656 if blk.empty?
656 blk
657 blk
657 else
658 else
658 code_blk = nil
659 code_blk = nil
659 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
660 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
660 flush_left iblk
661 flush_left iblk
661 blocks iblk, plain
662 blocks iblk, plain
662 iblk.gsub( /^(\S)/, "\t\\1" )
663 iblk.gsub( /^(\S)/, "\t\\1" )
663 if plain
664 if plain
664 code_blk = iblk; ""
665 code_blk = iblk; ""
665 else
666 else
666 iblk
667 iblk
667 end
668 end
668 end
669 end
669
670
670 block_applied = 0
671 block_applied = 0
671 @rules.each do |rule_name|
672 @rules.each do |rule_name|
672 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
673 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
673 end
674 end
674 if block_applied.zero?
675 if block_applied.zero?
675 if deep_code
676 if deep_code
676 blk = "\t<pre><code>#{ blk }</code></pre>"
677 blk = "\t<pre><code>#{ blk }</code></pre>"
677 else
678 else
678 blk = "\t<p>#{ blk }</p>"
679 blk = "\t<p>#{ blk }</p>"
679 end
680 end
680 end
681 end
681 # hard_break blk
682 # hard_break blk
682 blk + "\n#{ code_blk }"
683 blk + "\n#{ code_blk }"
683 end
684 end
684 end
685 end
685
686
686 end.join( "\n\n" ) )
687 end.join( "\n\n" ) )
687 end
688 end
688
689
689 def textile_bq( tag, atts, cite, content )
690 def textile_bq( tag, atts, cite, content )
690 cite, cite_title = check_refs( cite )
691 cite, cite_title = check_refs( cite )
691 cite = " cite=\"#{ cite }\"" if cite
692 cite = " cite=\"#{ cite }\"" if cite
692 atts = shelve( atts ) if atts
693 atts = shelve( atts ) if atts
693 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
694 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
694 end
695 end
695
696
696 def textile_p( tag, atts, cite, content )
697 def textile_p( tag, atts, cite, content )
697 atts = shelve( atts ) if atts
698 atts = shelve( atts ) if atts
698 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
699 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
699 end
700 end
700
701
701 alias textile_h1 textile_p
702 alias textile_h1 textile_p
702 alias textile_h2 textile_p
703 alias textile_h2 textile_p
703 alias textile_h3 textile_p
704 alias textile_h3 textile_p
704 alias textile_h4 textile_p
705 alias textile_h4 textile_p
705 alias textile_h5 textile_p
706 alias textile_h5 textile_p
706 alias textile_h6 textile_p
707 alias textile_h6 textile_p
707
708
708 def textile_fn_( tag, num, atts, cite, content )
709 def textile_fn_( tag, num, atts, cite, content )
709 atts << " id=\"fn#{ num }\" class=\"footnote\""
710 atts << " id=\"fn#{ num }\" class=\"footnote\""
710 content = "<sup>#{ num }</sup> #{ content }"
711 content = "<sup>#{ num }</sup> #{ content }"
711 atts = shelve( atts ) if atts
712 atts = shelve( atts ) if atts
712 "\t<p#{ atts }>#{ content }</p>"
713 "\t<p#{ atts }>#{ content }</p>"
713 end
714 end
714
715
715 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
716 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
716
717
717 def block_textile_prefix( text )
718 def block_textile_prefix( text )
718 if text =~ BLOCK_RE
719 if text =~ BLOCK_RE
719 tag,tagpre,num,atts,cite,content = $~[1..6]
720 tag,tagpre,num,atts,cite,content = $~[1..6]
720 atts = pba( atts )
721 atts = pba( atts )
721
722
722 # pass to prefix handler
723 # pass to prefix handler
723 replacement = nil
724 replacement = nil
724 if respond_to? "textile_#{ tag }", true
725 if respond_to? "textile_#{ tag }", true
725 replacement = method( "textile_#{ tag }" ).call( tag, atts, cite, content )
726 replacement = method( "textile_#{ tag }" ).call( tag, atts, cite, content )
726 elsif respond_to? "textile_#{ tagpre }_", true
727 elsif respond_to? "textile_#{ tagpre }_", true
727 replacement = method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content )
728 replacement = method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content )
728 end
729 end
729 text.gsub!( $& ) { replacement } if replacement
730 text.gsub!( $& ) { replacement } if replacement
730 end
731 end
731 end
732 end
732
733
733 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
734 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
734 def block_markdown_setext( text )
735 def block_markdown_setext( text )
735 if text =~ SETEXT_RE
736 if text =~ SETEXT_RE
736 tag = if $2 == "="; "h1"; else; "h2"; end
737 tag = if $2 == "="; "h1"; else; "h2"; end
737 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
738 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
738 blocks cont
739 blocks cont
739 text.replace( blk + cont )
740 text.replace( blk + cont )
740 end
741 end
741 end
742 end
742
743
743 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
744 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
744 [ ]*
745 [ ]*
745 (.+?) # $2 = Header text
746 (.+?) # $2 = Header text
746 [ ]*
747 [ ]*
747 \#* # optional closing #'s (not counted)
748 \#* # optional closing #'s (not counted)
748 $/x
749 $/x
749 def block_markdown_atx( text )
750 def block_markdown_atx( text )
750 if text =~ ATX_RE
751 if text =~ ATX_RE
751 tag = "h#{ $1.length }"
752 tag = "h#{ $1.length }"
752 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
753 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
753 blocks cont
754 blocks cont
754 text.replace( blk + cont )
755 text.replace( blk + cont )
755 end
756 end
756 end
757 end
757
758
758 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
759 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
759
760
760 def block_markdown_bq( text )
761 def block_markdown_bq( text )
761 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
762 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
762 blk.gsub!( /^ *> ?/, '' )
763 blk.gsub!( /^ *> ?/, '' )
763 flush_left blk
764 flush_left blk
764 blocks blk
765 blocks blk
765 blk.gsub!( /^(\S)/, "\t\\1" )
766 blk.gsub!( /^(\S)/, "\t\\1" )
766 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
767 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
767 end
768 end
768 end
769 end
769
770
770 MARKDOWN_RULE_RE = /^(#{
771 MARKDOWN_RULE_RE = /^(#{
771 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
772 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
772 })$/
773 })$/
773
774
774 def block_markdown_rule( text )
775 def block_markdown_rule( text )
775 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
776 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
776 "<hr />"
777 "<hr />"
777 end
778 end
778 end
779 end
779
780
780 # XXX TODO XXX
781 # XXX TODO XXX
781 def block_markdown_lists( text )
782 def block_markdown_lists( text )
782 end
783 end
783
784
784 def inline_textile_span( text )
785 def inline_textile_span( text )
785 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
786 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
786 text.gsub!( qtag_re ) do |m|
787 text.gsub!( qtag_re ) do |m|
787
788
788 case rtype
789 case rtype
789 when :limit
790 when :limit
790 sta,oqs,qtag,content,oqa = $~[1..6]
791 sta,oqs,qtag,content,oqa = $~[1..6]
791 atts = nil
792 atts = nil
792 if content =~ /^(#{C})(.+)$/
793 if content =~ /^(#{C})(.+)$/
793 atts, content = $~[1..2]
794 atts, content = $~[1..2]
794 end
795 end
795 else
796 else
796 qtag,atts,cite,content = $~[1..4]
797 qtag,atts,cite,content = $~[1..4]
797 sta = ''
798 sta = ''
798 end
799 end
799 atts = pba( atts )
800 atts = pba( atts )
800 atts = shelve( atts ) if atts
801 atts = shelve( atts ) if atts
801
802
802 "#{ sta }#{ oqs }<#{ ht }#{ atts }>#{ content }</#{ ht }>#{ oqa }"
803 "#{ sta }#{ oqs }<#{ ht }#{ atts }>#{ content }</#{ ht }>#{ oqa }"
803
804
804 end
805 end
805 end
806 end
806 end
807 end
807
808
808 LINK_RE = /
809 LINK_RE = /
809 (
810 (
810 ([\s\[{(]|[#{PUNCT}])? # $pre
811 ([\s\[{(]|[#{PUNCT}])? # $pre
811 " # start
812 " # start
812 (#{C}) # $atts
813 (#{C}) # $atts
813 ([^"\n]+?) # $text
814 ([^"\n]+?) # $text
814 \s?
815 \s?
815 (?:\(([^)]+?)\)(?="))? # $title
816 (?:\(([^)]+?)\)(?="))? # $title
816 ":
817 ":
817 ( # $url
818 ( # $url
818 (\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
819 (\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
819 [[:alnum:]_\/]\S+?
820 [[:alnum:]_\/]\S+?
820 )
821 )
821 (\/)? # $slash
822 (\/)? # $slash
822 ([^[:alnum:]_\=\/;\(\)]*?) # $post
823 ([^[:alnum:]_\=\/;\(\)]*?) # $post
823 )
824 )
824 (?=<|\s|$)
825 (?=<|\s|$)
825 /x
826 /x
826 #"
827 #"
827 def inline_textile_link( text )
828 def inline_textile_link( text )
828 text.gsub!( LINK_RE ) do |m|
829 text.gsub!( LINK_RE ) do |m|
829 all,pre,atts,text,title,url,proto,slash,post = $~[1..9]
830 all,pre,atts,text,title,url,proto,slash,post = $~[1..9]
830 if text.include?('<br />')
831 if text.include?('<br />')
831 all
832 all
832 else
833 else
833 url, url_title = check_refs( url )
834 url, url_title = check_refs( url )
834 title ||= url_title
835 title ||= url_title
835
836
836 # Idea below : an URL with unbalanced parethesis and
837 # Idea below : an URL with unbalanced parethesis and
837 # ending by ')' is put into external parenthesis
838 # ending by ')' is put into external parenthesis
838 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
839 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
839 url=url[0..-2] # discard closing parenth from url
840 url=url[0..-2] # discard closing parenth from url
840 post = ")"+post # add closing parenth to post
841 post = ")"+post # add closing parenth to post
841 end
842 end
842 atts = pba( atts )
843 atts = pba( atts )
843 atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }"
844 atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }"
844 atts << " title=\"#{ htmlesc title }\"" if title
845 atts << " title=\"#{ htmlesc title }\"" if title
845 atts = shelve( atts ) if atts
846 atts = shelve( atts ) if atts
846
847
847 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
848 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
848
849
849 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
850 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
850 end
851 end
851 end
852 end
852 end
853 end
853
854
854 MARKDOWN_REFLINK_RE = /
855 MARKDOWN_REFLINK_RE = /
855 \[([^\[\]]+)\] # $text
856 \[([^\[\]]+)\] # $text
856 [ ]? # opt. space
857 [ ]? # opt. space
857 (?:\n[ ]*)? # one optional newline followed by spaces
858 (?:\n[ ]*)? # one optional newline followed by spaces
858 \[(.*?)\] # $id
859 \[(.*?)\] # $id
859 /x
860 /x
860
861
861 def inline_markdown_reflink( text )
862 def inline_markdown_reflink( text )
862 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
863 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
863 text, id = $~[1..2]
864 text, id = $~[1..2]
864
865
865 if id.empty?
866 if id.empty?
866 url, title = check_refs( text )
867 url, title = check_refs( text )
867 else
868 else
868 url, title = check_refs( id )
869 url, title = check_refs( id )
869 end
870 end
870
871
871 atts = " href=\"#{ url }\""
872 atts = " href=\"#{ url }\""
872 atts << " title=\"#{ title }\"" if title
873 atts << " title=\"#{ title }\"" if title
873 atts = shelve( atts )
874 atts = shelve( atts )
874
875
875 "<a#{ atts }>#{ text }</a>"
876 "<a#{ atts }>#{ text }</a>"
876 end
877 end
877 end
878 end
878
879
879 MARKDOWN_LINK_RE = /
880 MARKDOWN_LINK_RE = /
880 \[([^\[\]]+)\] # $text
881 \[([^\[\]]+)\] # $text
881 \( # open paren
882 \( # open paren
882 [ \t]* # opt space
883 [ \t]* # opt space
883 <?(.+?)>? # $href
884 <?(.+?)>? # $href
884 [ \t]* # opt space
885 [ \t]* # opt space
885 (?: # whole title
886 (?: # whole title
886 (['"]) # $quote
887 (['"]) # $quote
887 (.*?) # $title
888 (.*?) # $title
888 \3 # matching quote
889 \3 # matching quote
889 )? # title is optional
890 )? # title is optional
890 \)
891 \)
891 /x
892 /x
892
893
893 def inline_markdown_link( text )
894 def inline_markdown_link( text )
894 text.gsub!( MARKDOWN_LINK_RE ) do |m|
895 text.gsub!( MARKDOWN_LINK_RE ) do |m|
895 text, url, quote, title = $~[1..4]
896 text, url, quote, title = $~[1..4]
896
897
897 atts = " href=\"#{ url }\""
898 atts = " href=\"#{ url }\""
898 atts << " title=\"#{ title }\"" if title
899 atts << " title=\"#{ title }\"" if title
899 atts = shelve( atts )
900 atts = shelve( atts )
900
901
901 "<a#{ atts }>#{ text }</a>"
902 "<a#{ atts }>#{ text }</a>"
902 end
903 end
903 end
904 end
904
905
905 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
906 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
906 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
907 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
907
908
908 def refs( text )
909 def refs( text )
909 @rules.each do |rule_name|
910 @rules.each do |rule_name|
910 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
911 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
911 end
912 end
912 end
913 end
913
914
914 def refs_textile( text )
915 def refs_textile( text )
915 text.gsub!( TEXTILE_REFS_RE ) do |m|
916 text.gsub!( TEXTILE_REFS_RE ) do |m|
916 flag, url = $~[2..3]
917 flag, url = $~[2..3]
917 @urlrefs[flag.downcase] = [url, nil]
918 @urlrefs[flag.downcase] = [url, nil]
918 nil
919 nil
919 end
920 end
920 end
921 end
921
922
922 def refs_markdown( text )
923 def refs_markdown( text )
923 text.gsub!( MARKDOWN_REFS_RE ) do |m|
924 text.gsub!( MARKDOWN_REFS_RE ) do |m|
924 flag, url = $~[2..3]
925 flag, url = $~[2..3]
925 title = $~[6]
926 title = $~[6]
926 @urlrefs[flag.downcase] = [url, title]
927 @urlrefs[flag.downcase] = [url, title]
927 nil
928 nil
928 end
929 end
929 end
930 end
930
931
931 def check_refs( text )
932 def check_refs( text )
932 ret = @urlrefs[text.downcase] if text
933 ret = @urlrefs[text.downcase] if text
933 ret || [text, nil]
934 ret || [text, nil]
934 end
935 end
935
936
936 IMAGE_RE = /
937 IMAGE_RE = /
937 (>|\s|^) # start of line?
938 (>|\s|^) # start of line?
938 \! # opening
939 \! # opening
939 (\<|\=|\>)? # optional alignment atts
940 (\<|\=|\>)? # optional alignment atts
940 (#{C}) # optional style,class atts
941 (#{C}) # optional style,class atts
941 (?:\. )? # optional dot-space
942 (?:\. )? # optional dot-space
942 ([^\s(!]+?) # presume this is the src
943 ([^\s(!]+?) # presume this is the src
943 \s? # optional space
944 \s? # optional space
944 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
945 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
945 \! # closing
946 \! # closing
946 (?::#{ HYPERLINK })? # optional href
947 (?::#{ HYPERLINK })? # optional href
947 /x
948 /x
948
949
949 def inline_textile_image( text )
950 def inline_textile_image( text )
950 text.gsub!( IMAGE_RE ) do |m|
951 text.gsub!( IMAGE_RE ) do |m|
951 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
952 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
952 htmlesc title
953 htmlesc title
953 atts = pba( atts )
954 atts = pba( atts )
954 atts = " src=\"#{ htmlesc url.dup }\"#{ atts }"
955 atts = " src=\"#{ htmlesc url.dup }\"#{ atts }"
955 atts << " title=\"#{ title }\"" if title
956 atts << " title=\"#{ title }\"" if title
956 atts << " alt=\"#{ title }\""
957 atts << " alt=\"#{ title }\""
957 # size = @getimagesize($url);
958 # size = @getimagesize($url);
958 # if($size) $atts.= " $size[3]";
959 # if($size) $atts.= " $size[3]";
959
960
960 href, alt_title = check_refs( href ) if href
961 href, alt_title = check_refs( href ) if href
961 url, url_title = check_refs( url )
962 url, url_title = check_refs( url )
962
963
963 out = ''
964 out = ''
964 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
965 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
965 out << "<img#{ shelve( atts ) } />"
966 out << "<img#{ shelve( atts ) } />"
966 out << "</a>#{ href_a1 }#{ href_a2 }" if href
967 out << "</a>#{ href_a1 }#{ href_a2 }" if href
967
968
968 if algn
969 if algn
969 algn = h_align( algn )
970 algn = h_align( algn )
970 if stln == "<p>"
971 if stln == "<p>"
971 out = "<p style=\"float:#{ algn }\">#{ out }"
972 out = "<p style=\"float:#{ algn }\">#{ out }"
972 else
973 else
973 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
974 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
974 end
975 end
975 else
976 else
976 out = stln + out
977 out = stln + out
977 end
978 end
978
979
979 out
980 out
980 end
981 end
981 end
982 end
982
983
983 def shelve( val )
984 def shelve( val )
984 @shelf << val
985 @shelf << val
985 " :redsh##{ @shelf.length }:"
986 " :redsh##{ @shelf.length }:"
986 end
987 end
987
988
988 def retrieve( text )
989 def retrieve( text )
989 @shelf.each_with_index do |r, i|
990 @shelf.each_with_index do |r, i|
990 text.gsub!( " :redsh##{ i + 1 }:", r )
991 text.gsub!( " :redsh##{ i + 1 }:", r )
991 end
992 end
992 end
993 end
993
994
994 def incoming_entities( text )
995 def incoming_entities( text )
995 ## turn any incoming ampersands into a dummy character for now.
996 ## turn any incoming ampersands into a dummy character for now.
996 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
997 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
997 ## implying an incoming html entity, to be skipped
998 ## implying an incoming html entity, to be skipped
998
999
999 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
1000 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
1000 end
1001 end
1001
1002
1002 def no_textile( text )
1003 def no_textile( text )
1003 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
1004 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
1004 '\1<notextile>\2</notextile>\3' )
1005 '\1<notextile>\2</notextile>\3' )
1005 text.gsub!( /^ *==([^=]+.*?)==/m,
1006 text.gsub!( /^ *==([^=]+.*?)==/m,
1006 '\1<notextile>\2</notextile>\3' )
1007 '\1<notextile>\2</notextile>\3' )
1007 end
1008 end
1008
1009
1009 def clean_white_space( text )
1010 def clean_white_space( text )
1010 # normalize line breaks
1011 # normalize line breaks
1011 text.gsub!( /\r\n/, "\n" )
1012 text.gsub!( /\r\n/, "\n" )
1012 text.gsub!( /\r/, "\n" )
1013 text.gsub!( /\r/, "\n" )
1013 text.gsub!( /\t/, ' ' )
1014 text.gsub!( /\t/, ' ' )
1014 text.gsub!( /^ +$/, '' )
1015 text.gsub!( /^ +$/, '' )
1015 text.gsub!( /\n{3,}/, "\n\n" )
1016 text.gsub!( /\n{3,}/, "\n\n" )
1016 text.gsub!( /"$/, "\" " )
1017 text.gsub!( /"$/, "\" " )
1017
1018
1018 # if entire document is indented, flush
1019 # if entire document is indented, flush
1019 # to the left side
1020 # to the left side
1020 flush_left text
1021 flush_left text
1021 end
1022 end
1022
1023
1023 def flush_left( text )
1024 def flush_left( text )
1024 indt = 0
1025 indt = 0
1025 if text =~ /^ /
1026 if text =~ /^ /
1026 while text !~ /^ {#{indt}}\S/
1027 while text !~ /^ {#{indt}}\S/
1027 indt += 1
1028 indt += 1
1028 end unless text.empty?
1029 end unless text.empty?
1029 if indt.nonzero?
1030 if indt.nonzero?
1030 text.gsub!( /^ {#{indt}}/, '' )
1031 text.gsub!( /^ {#{indt}}/, '' )
1031 end
1032 end
1032 end
1033 end
1033 end
1034 end
1034
1035
1035 def footnote_ref( text )
1036 def footnote_ref( text )
1036 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1037 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1037 '<sup><a href="#fn\1">\1</a></sup>\2' )
1038 '<sup><a href="#fn\1">\1</a></sup>\2' )
1038 end
1039 end
1039
1040
1040 OFFTAGS = /(code|pre|kbd|notextile)/
1041 OFFTAGS = /(code|pre|kbd|notextile)/
1041 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\W|\Z)/mi
1042 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\W|\Z)/mi
1042 OFFTAG_OPEN = /<#{ OFFTAGS }/
1043 OFFTAG_OPEN = /<#{ OFFTAGS }/
1043 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1044 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1044 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1045 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1045 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1046 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1046
1047
1047 def glyphs_textile( text, level = 0 )
1048 def glyphs_textile( text, level = 0 )
1048 if text !~ HASTAG_MATCH
1049 if text !~ HASTAG_MATCH
1049 pgl text
1050 pgl text
1050 footnote_ref text
1051 footnote_ref text
1051 else
1052 else
1052 codepre = 0
1053 codepre = 0
1053 text.gsub!( ALLTAG_MATCH ) do |line|
1054 text.gsub!( ALLTAG_MATCH ) do |line|
1054 ## matches are off if we're between <code>, <pre> etc.
1055 ## matches are off if we're between <code>, <pre> etc.
1055 if $1
1056 if $1
1056 if line =~ OFFTAG_OPEN
1057 if line =~ OFFTAG_OPEN
1057 codepre += 1
1058 codepre += 1
1058 elsif line =~ OFFTAG_CLOSE
1059 elsif line =~ OFFTAG_CLOSE
1059 codepre -= 1
1060 codepre -= 1
1060 codepre = 0 if codepre < 0
1061 codepre = 0 if codepre < 0
1061 end
1062 end
1062 elsif codepre.zero?
1063 elsif codepre.zero?
1063 glyphs_textile( line, level + 1 )
1064 glyphs_textile( line, level + 1 )
1064 else
1065 else
1065 htmlesc( line, :NoQuotes )
1066 htmlesc( line, :NoQuotes )
1066 end
1067 end
1067 # p [level, codepre, line]
1068 # p [level, codepre, line]
1068
1069
1069 line
1070 line
1070 end
1071 end
1071 end
1072 end
1072 end
1073 end
1073
1074
1074 def rip_offtags( text, escape_aftertag=true, escape_line=true )
1075 def rip_offtags( text, escape_aftertag=true, escape_line=true )
1075 if text =~ /<.*>/
1076 if text =~ /<.*>/
1076 ## strip and encode <pre> content
1077 ## strip and encode <pre> content
1077 codepre, used_offtags = 0, {}
1078 codepre, used_offtags = 0, {}
1078 text.gsub!( OFFTAG_MATCH ) do |line|
1079 text.gsub!( OFFTAG_MATCH ) do |line|
1079 if $3
1080 if $3
1080 first, offtag, aftertag = $3, $4, $5
1081 first, offtag, aftertag = $3, $4, $5
1081 codepre += 1
1082 codepre += 1
1082 used_offtags[offtag] = true
1083 used_offtags[offtag] = true
1083 if codepre - used_offtags.length > 0
1084 if codepre - used_offtags.length > 0
1084 htmlesc( line, :NoQuotes ) if escape_line
1085 htmlesc( line, :NoQuotes ) if escape_line
1085 @pre_list.last << line
1086 @pre_list.last << line
1086 line = ""
1087 line = ""
1087 else
1088 else
1088 ### htmlesc is disabled between CODE tags which will be parsed with highlighter
1089 ### htmlesc is disabled between CODE tags which will be parsed with highlighter
1089 ### Regexp in formatter.rb is : /<code\s+class="(\w+)">\s?(.+)/m
1090 ### Regexp in formatter.rb is : /<code\s+class="(\w+)">\s?(.+)/m
1090 ### NB: some changes were made not to use $N variables, because we use "match"
1091 ### NB: some changes were made not to use $N variables, because we use "match"
1091 ### and it breaks following lines
1092 ### and it breaks following lines
1092 htmlesc( aftertag, :NoQuotes ) if aftertag && escape_aftertag && !first.match(/<code\s+class="(\w+)">/)
1093 htmlesc( aftertag, :NoQuotes ) if aftertag && escape_aftertag && !first.match(/<code\s+class="(\w+)">/)
1093 line = "<redpre##{ @pre_list.length }>"
1094 line = "<redpre##{ @pre_list.length }>"
1094 first.match(/<#{ OFFTAGS }([^>]*)>/)
1095 first.match(/<#{ OFFTAGS }([^>]*)>/)
1095 tag = $1
1096 tag = $1
1096 $2.to_s.match(/(class\=("[^"]+"|'[^']+'))/i)
1097 $2.to_s.match(/(class\=("[^"]+"|'[^']+'))/i)
1097 tag << " #{$1}" if $1
1098 tag << " #{$1}" if $1
1098 @pre_list << "<#{ tag }>#{ aftertag }"
1099 @pre_list << "<#{ tag }>#{ aftertag }"
1099 end
1100 end
1100 elsif $1 and codepre > 0
1101 elsif $1 and codepre > 0
1101 if codepre - used_offtags.length > 0
1102 if codepre - used_offtags.length > 0
1102 htmlesc( line, :NoQuotes ) if escape_line
1103 htmlesc( line, :NoQuotes ) if escape_line
1103 @pre_list.last << line
1104 @pre_list.last << line
1104 line = ""
1105 line = ""
1105 end
1106 end
1106 codepre -= 1 unless codepre.zero?
1107 codepre -= 1 unless codepre.zero?
1107 used_offtags = {} if codepre.zero?
1108 used_offtags = {} if codepre.zero?
1108 end
1109 end
1109 line
1110 line
1110 end
1111 end
1111 end
1112 end
1112 text
1113 text
1113 end
1114 end
1114
1115
1115 def smooth_offtags( text )
1116 def smooth_offtags( text )
1116 unless @pre_list.empty?
1117 unless @pre_list.empty?
1117 ## replace <pre> content
1118 ## replace <pre> content
1118 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1119 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1119 end
1120 end
1120 end
1121 end
1121
1122
1122 def inline( text )
1123 def inline( text )
1123 [/^inline_/, /^glyphs_/].each do |meth_re|
1124 [/^inline_/, /^glyphs_/].each do |meth_re|
1124 @rules.each do |rule_name|
1125 @rules.each do |rule_name|
1125 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1126 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1126 end
1127 end
1127 end
1128 end
1128 end
1129 end
1129
1130
1130 def h_align( text )
1131 def h_align( text )
1131 H_ALGN_VALS[text]
1132 H_ALGN_VALS[text]
1132 end
1133 end
1133
1134
1134 def v_align( text )
1135 def v_align( text )
1135 V_ALGN_VALS[text]
1136 V_ALGN_VALS[text]
1136 end
1137 end
1137
1138
1138 def textile_popup_help( name, windowW, windowH )
1139 def textile_popup_help( name, windowW, windowH )
1139 ' <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 />'
1140 ' <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 />'
1140 end
1141 end
1141
1142
1142 # HTML cleansing stuff
1143 # HTML cleansing stuff
1143 BASIC_TAGS = {
1144 BASIC_TAGS = {
1144 'a' => ['href', 'title'],
1145 'a' => ['href', 'title'],
1145 'img' => ['src', 'alt', 'title'],
1146 'img' => ['src', 'alt', 'title'],
1146 'br' => [],
1147 'br' => [],
1147 'i' => nil,
1148 'i' => nil,
1148 'u' => nil,
1149 'u' => nil,
1149 'b' => nil,
1150 'b' => nil,
1150 'pre' => nil,
1151 'pre' => nil,
1151 'kbd' => nil,
1152 'kbd' => nil,
1152 'code' => ['lang'],
1153 'code' => ['lang'],
1153 'cite' => nil,
1154 'cite' => nil,
1154 'strong' => nil,
1155 'strong' => nil,
1155 'em' => nil,
1156 'em' => nil,
1156 'ins' => nil,
1157 'ins' => nil,
1157 'sup' => nil,
1158 'sup' => nil,
1158 'sub' => nil,
1159 'sub' => nil,
1159 'del' => nil,
1160 'del' => nil,
1160 'table' => nil,
1161 'table' => nil,
1161 'tr' => nil,
1162 'tr' => nil,
1162 'td' => ['colspan', 'rowspan'],
1163 'td' => ['colspan', 'rowspan'],
1163 'th' => nil,
1164 'th' => nil,
1164 'ol' => nil,
1165 'ol' => nil,
1165 'ul' => nil,
1166 'ul' => nil,
1166 'li' => nil,
1167 'li' => nil,
1167 'p' => nil,
1168 'p' => nil,
1168 'h1' => nil,
1169 'h1' => nil,
1169 'h2' => nil,
1170 'h2' => nil,
1170 'h3' => nil,
1171 'h3' => nil,
1171 'h4' => nil,
1172 'h4' => nil,
1172 'h5' => nil,
1173 'h5' => nil,
1173 'h6' => nil,
1174 'h6' => nil,
1174 'blockquote' => ['cite']
1175 'blockquote' => ['cite']
1175 }
1176 }
1176
1177
1177 def clean_html( text, tags = BASIC_TAGS )
1178 def clean_html( text, tags = BASIC_TAGS )
1178 text.gsub!( /<!\[CDATA\[/, '' )
1179 text.gsub!( /<!\[CDATA\[/, '' )
1179 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1180 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1180 raw = $~
1181 raw = $~
1181 tag = raw[2].downcase
1182 tag = raw[2].downcase
1182 if tags.has_key? tag
1183 if tags.has_key? tag
1183 pcs = [tag]
1184 pcs = [tag]
1184 tags[tag].each do |prop|
1185 tags[tag].each do |prop|
1185 ['"', "'", ''].each do |q|
1186 ['"', "'", ''].each do |q|
1186 q2 = ( q != '' ? q : '\s' )
1187 q2 = ( q != '' ? q : '\s' )
1187 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1188 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1188 attrv = $1
1189 attrv = $1
1189 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1190 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1190 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1191 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1191 break
1192 break
1192 end
1193 end
1193 end
1194 end
1194 end if tags[tag]
1195 end if tags[tag]
1195 "<#{raw[1]}#{pcs.join " "}>"
1196 "<#{raw[1]}#{pcs.join " "}>"
1196 else
1197 else
1197 " "
1198 " "
1198 end
1199 end
1199 end
1200 end
1200 end
1201 end
1201
1202
1202 ALLOWED_TAGS = %w(redpre pre code notextile)
1203 ALLOWED_TAGS = %w(redpre pre code notextile)
1203
1204
1204 def escape_html_tags(text)
1205 def escape_html_tags(text)
1205 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1206 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1206 end
1207 end
1207 end
1208 end
1208
1209
@@ -1,456 +1,492
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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.expand_path('../../../../../test_helper', __FILE__)
18 require File.expand_path('../../../../../test_helper', __FILE__)
19 require 'digest/md5'
19 require 'digest/md5'
20
20
21 class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
21 class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
22
22
23 def setup
23 def setup
24 @formatter = Redmine::WikiFormatting::Textile::Formatter
24 @formatter = Redmine::WikiFormatting::Textile::Formatter
25 end
25 end
26
26
27 MODIFIERS = {
27 MODIFIERS = {
28 "*" => 'strong', # bold
28 "*" => 'strong', # bold
29 "_" => 'em', # italic
29 "_" => 'em', # italic
30 "+" => 'ins', # underline
30 "+" => 'ins', # underline
31 "-" => 'del', # deleted
31 "-" => 'del', # deleted
32 "^" => 'sup', # superscript
32 "^" => 'sup', # superscript
33 "~" => 'sub' # subscript
33 "~" => 'sub' # subscript
34 }
34 }
35
35
36 def test_modifiers
36 def test_modifiers
37 assert_html_output(
37 assert_html_output(
38 '*bold*' => '<strong>bold</strong>',
38 '*bold*' => '<strong>bold</strong>',
39 'before *bold*' => 'before <strong>bold</strong>',
39 'before *bold*' => 'before <strong>bold</strong>',
40 '*bold* after' => '<strong>bold</strong> after',
40 '*bold* after' => '<strong>bold</strong> after',
41 '*two words*' => '<strong>two words</strong>',
41 '*two words*' => '<strong>two words</strong>',
42 '*two*words*' => '<strong>two*words</strong>',
42 '*two*words*' => '<strong>two*words</strong>',
43 '*two * words*' => '<strong>two * words</strong>',
43 '*two * words*' => '<strong>two * words</strong>',
44 '*two* *words*' => '<strong>two</strong> <strong>words</strong>',
44 '*two* *words*' => '<strong>two</strong> <strong>words</strong>',
45 '*(two)* *(words)*' => '<strong>(two)</strong> <strong>(words)</strong>',
45 '*(two)* *(words)*' => '<strong>(two)</strong> <strong>(words)</strong>',
46 # with class
46 # with class
47 '*(foo)two words*' => '<strong class="foo">two words</strong>'
47 '*(foo)two words*' => '<strong class="foo">two words</strong>'
48 )
48 )
49 end
49 end
50
50
51 def test_modifiers_combination
51 def test_modifiers_combination
52 MODIFIERS.each do |m1, tag1|
52 MODIFIERS.each do |m1, tag1|
53 MODIFIERS.each do |m2, tag2|
53 MODIFIERS.each do |m2, tag2|
54 next if m1 == m2
54 next if m1 == m2
55 text = "#{m2}#{m1}Phrase modifiers#{m1}#{m2}"
55 text = "#{m2}#{m1}Phrase modifiers#{m1}#{m2}"
56 html = "<#{tag2}><#{tag1}>Phrase modifiers</#{tag1}></#{tag2}>"
56 html = "<#{tag2}><#{tag1}>Phrase modifiers</#{tag1}></#{tag2}>"
57 assert_html_output text => html
57 assert_html_output text => html
58 end
58 end
59 end
59 end
60 end
60 end
61
61
62 def test_styles
62 def test_styles
63 # single style
63 # single style
64 assert_html_output({
64 assert_html_output({
65 'p{color:red}. text' => '<p style="color:red;">text</p>',
65 'p{color:red}. text' => '<p style="color:red;">text</p>',
66 'p{color:red;}. text' => '<p style="color:red;">text</p>',
66 'p{color:red;}. text' => '<p style="color:red;">text</p>',
67 'p{color: red}. text' => '<p style="color: red;">text</p>',
67 'p{color: red}. text' => '<p style="color: red;">text</p>',
68 'p{color:#f00}. text' => '<p style="color:#f00;">text</p>',
68 'p{color:#f00}. text' => '<p style="color:#f00;">text</p>',
69 'p{color:#ff0000}. text' => '<p style="color:#ff0000;">text</p>',
69 'p{color:#ff0000}. text' => '<p style="color:#ff0000;">text</p>',
70 'p{border:10px}. text' => '<p style="border:10px;">text</p>',
70 'p{border:10px}. text' => '<p style="border:10px;">text</p>',
71 'p{border:10}. text' => '<p style="border:10;">text</p>',
71 'p{border:10}. text' => '<p style="border:10;">text</p>',
72 'p{border:10%}. text' => '<p style="border:10%;">text</p>',
72 'p{border:10%}. text' => '<p style="border:10%;">text</p>',
73 'p{border:10em}. text' => '<p style="border:10em;">text</p>',
73 'p{border:10em}. text' => '<p style="border:10em;">text</p>',
74 'p{border:1.5em}. text' => '<p style="border:1.5em;">text</p>',
74 'p{border:1.5em}. text' => '<p style="border:1.5em;">text</p>',
75 'p{border-left:1px}. text' => '<p style="border-left:1px;">text</p>',
75 'p{border-left:1px}. text' => '<p style="border-left:1px;">text</p>',
76 'p{border-right:1px}. text' => '<p style="border-right:1px;">text</p>',
76 'p{border-right:1px}. text' => '<p style="border-right:1px;">text</p>',
77 'p{border-top:1px}. text' => '<p style="border-top:1px;">text</p>',
77 'p{border-top:1px}. text' => '<p style="border-top:1px;">text</p>',
78 'p{border-bottom:1px}. text' => '<p style="border-bottom:1px;">text</p>',
78 'p{border-bottom:1px}. text' => '<p style="border-bottom:1px;">text</p>',
79 }, false)
79 }, false)
80
80
81 # multiple styles
81 # multiple styles
82 assert_html_output({
82 assert_html_output({
83 'p{color:red; border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
83 'p{color:red; border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
84 'p{color:red ; border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
84 'p{color:red ; border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
85 'p{color:red;border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
85 'p{color:red;border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
86 }, false)
86 }, false)
87
87
88 # styles with multiple values
88 # styles with multiple values
89 assert_html_output({
89 assert_html_output({
90 'p{border:1px solid red;}. text' => '<p style="border:1px solid red;">text</p>',
90 'p{border:1px solid red;}. text' => '<p style="border:1px solid red;">text</p>',
91 'p{border-top-left-radius: 10px 5px;}. text' => '<p style="border-top-left-radius: 10px 5px;">text</p>',
91 'p{border-top-left-radius: 10px 5px;}. text' => '<p style="border-top-left-radius: 10px 5px;">text</p>',
92 }, false)
92 }, false)
93 end
93 end
94
94
95 def test_invalid_styles_should_be_filtered
95 def test_invalid_styles_should_be_filtered
96 assert_html_output({
96 assert_html_output({
97 'p{invalid}. text' => '<p>text</p>',
97 'p{invalid}. text' => '<p>text</p>',
98 'p{invalid:red}. text' => '<p>text</p>',
98 'p{invalid:red}. text' => '<p>text</p>',
99 'p{color:(red)}. text' => '<p>text</p>',
99 'p{color:(red)}. text' => '<p>text</p>',
100 'p{color:red;invalid:blue}. text' => '<p style="color:red;">text</p>',
100 'p{color:red;invalid:blue}. text' => '<p style="color:red;">text</p>',
101 'p{invalid:blue;color:red}. text' => '<p style="color:red;">text</p>',
101 'p{invalid:blue;color:red}. text' => '<p style="color:red;">text</p>',
102 'p{color:"}. text' => '<p>p{color:"}. text</p>',
102 'p{color:"}. text' => '<p>p{color:"}. text</p>',
103 }, false)
103 }, false)
104 end
104 end
105
105
106 def test_inline_code
106 def test_inline_code
107 assert_html_output(
107 assert_html_output(
108 'this is @some code@' => 'this is <code>some code</code>',
108 'this is @some code@' => 'this is <code>some code</code>',
109 '@<Location /redmine>@' => '<code>&lt;Location /redmine&gt;</code>'
109 '@<Location /redmine>@' => '<code>&lt;Location /redmine&gt;</code>'
110 )
110 )
111 end
111 end
112
112
113 def test_nested_lists
113 def test_nested_lists
114 raw = <<-RAW
114 raw = <<-RAW
115 # Item 1
115 # Item 1
116 # Item 2
116 # Item 2
117 ** Item 2a
117 ** Item 2a
118 ** Item 2b
118 ** Item 2b
119 # Item 3
119 # Item 3
120 ** Item 3a
120 ** Item 3a
121 RAW
121 RAW
122
122
123 expected = <<-EXPECTED
123 expected = <<-EXPECTED
124 <ol>
124 <ol>
125 <li>Item 1</li>
125 <li>Item 1</li>
126 <li>Item 2
126 <li>Item 2
127 <ul>
127 <ul>
128 <li>Item 2a</li>
128 <li>Item 2a</li>
129 <li>Item 2b</li>
129 <li>Item 2b</li>
130 </ul>
130 </ul>
131 </li>
131 </li>
132 <li>Item 3
132 <li>Item 3
133 <ul>
133 <ul>
134 <li>Item 3a</li>
134 <li>Item 3a</li>
135 </ul>
135 </ul>
136 </li>
136 </li>
137 </ol>
137 </ol>
138 EXPECTED
138 EXPECTED
139
139
140 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
140 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
141 end
141 end
142
142
143 def test_escaping
143 def test_escaping
144 assert_html_output(
144 assert_html_output(
145 'this is a <script>' => 'this is a &lt;script&gt;'
145 'this is a <script>' => 'this is a &lt;script&gt;'
146 )
146 )
147 end
147 end
148
148
149 def test_use_of_backslashes_followed_by_numbers_in_headers
149 def test_use_of_backslashes_followed_by_numbers_in_headers
150 assert_html_output({
150 assert_html_output({
151 'h1. 2009\02\09' => '<h1>2009\02\09</h1>'
151 'h1. 2009\02\09' => '<h1>2009\02\09</h1>'
152 }, false)
152 }, false)
153 end
153 end
154
154
155 def test_double_dashes_should_not_strikethrough
155 def test_double_dashes_should_not_strikethrough
156 assert_html_output(
156 assert_html_output(
157 'double -- dashes -- test' => 'double -- dashes -- test',
157 'double -- dashes -- test' => 'double -- dashes -- test',
158 'double -- *dashes* -- test' => 'double -- <strong>dashes</strong> -- test'
158 'double -- *dashes* -- test' => 'double -- <strong>dashes</strong> -- test'
159 )
159 )
160 end
160 end
161
161
162 def test_acronyms
162 def test_acronyms
163 assert_html_output(
163 assert_html_output(
164 'this is an acronym: GPL(General Public License)' => 'this is an acronym: <acronym title="General Public License">GPL</acronym>',
164 'this is an acronym: GPL(General Public License)' => 'this is an acronym: <acronym title="General Public License">GPL</acronym>',
165 '2 letters JP(Jean-Philippe) acronym' => '2 letters <acronym title="Jean-Philippe">JP</acronym> acronym',
165 '2 letters JP(Jean-Philippe) acronym' => '2 letters <acronym title="Jean-Philippe">JP</acronym> acronym',
166 'GPL(This is a double-quoted "title")' => '<acronym title="This is a double-quoted &quot;title&quot;">GPL</acronym>'
166 'GPL(This is a double-quoted "title")' => '<acronym title="This is a double-quoted &quot;title&quot;">GPL</acronym>'
167 )
167 )
168 end
168 end
169
169
170 def test_blockquote
170 def test_blockquote
171 # orig raw text
171 # orig raw text
172 raw = <<-RAW
172 raw = <<-RAW
173 John said:
173 John said:
174 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
174 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
175 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
175 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
176 > * Donec odio lorem,
176 > * Donec odio lorem,
177 > * sagittis ac,
177 > * sagittis ac,
178 > * malesuada in,
178 > * malesuada in,
179 > * adipiscing eu, dolor.
179 > * adipiscing eu, dolor.
180 >
180 >
181 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
181 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
182 > Proin a tellus. Nam vel neque.
182 > Proin a tellus. Nam vel neque.
183
183
184 He's right.
184 He's right.
185 RAW
185 RAW
186
186
187 # expected html
187 # expected html
188 expected = <<-EXPECTED
188 expected = <<-EXPECTED
189 <p>John said:</p>
189 <p>John said:</p>
190 <blockquote>
190 <blockquote>
191 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.<br />
191 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.<br />
192 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
192 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
193 <ul>
193 <ul>
194 <li>Donec odio lorem,</li>
194 <li>Donec odio lorem,</li>
195 <li>sagittis ac,</li>
195 <li>sagittis ac,</li>
196 <li>malesuada in,</li>
196 <li>malesuada in,</li>
197 <li>adipiscing eu, dolor.</li>
197 <li>adipiscing eu, dolor.</li>
198 </ul>
198 </ul>
199 <blockquote>
199 <blockquote>
200 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
200 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
201 </blockquote>
201 </blockquote>
202 <p>Proin a tellus. Nam vel neque.</p>
202 <p>Proin a tellus. Nam vel neque.</p>
203 </blockquote>
203 </blockquote>
204 <p>He's right.</p>
204 <p>He's right.</p>
205 EXPECTED
205 EXPECTED
206
206
207 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
207 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
208 end
208 end
209
209
210 def test_table
210 def test_table
211 raw = <<-RAW
211 raw = <<-RAW
212 This is a table with empty cells:
212 This is a table with empty cells:
213
213
214 |cell11|cell12||
214 |cell11|cell12||
215 |cell21||cell23|
215 |cell21||cell23|
216 |cell31|cell32|cell33|
216 |cell31|cell32|cell33|
217 RAW
217 RAW
218
218
219 expected = <<-EXPECTED
219 expected = <<-EXPECTED
220 <p>This is a table with empty cells:</p>
220 <p>This is a table with empty cells:</p>
221
221
222 <table>
222 <table>
223 <tr><td>cell11</td><td>cell12</td><td></td></tr>
223 <tr><td>cell11</td><td>cell12</td><td></td></tr>
224 <tr><td>cell21</td><td></td><td>cell23</td></tr>
224 <tr><td>cell21</td><td></td><td>cell23</td></tr>
225 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
225 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
226 </table>
226 </table>
227 EXPECTED
227 EXPECTED
228
228
229 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
229 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
230 end
230 end
231
231
232 def test_table_with_line_breaks
232 def test_table_with_line_breaks
233 raw = <<-RAW
233 raw = <<-RAW
234 This is a table with line breaks:
234 This is a table with line breaks:
235
235
236 |cell11
236 |cell11
237 continued|cell12||
237 continued|cell12||
238 |-cell21-||cell23
238 |-cell21-||cell23
239 cell23 line2
239 cell23 line2
240 cell23 *line3*|
240 cell23 *line3*|
241 |cell31|cell32
241 |cell31|cell32
242 cell32 line2|cell33|
242 cell32 line2|cell33|
243
243
244 RAW
244 RAW
245
245
246 expected = <<-EXPECTED
246 expected = <<-EXPECTED
247 <p>This is a table with line breaks:</p>
247 <p>This is a table with line breaks:</p>
248
248
249 <table>
249 <table>
250 <tr>
250 <tr>
251 <td>cell11<br />continued</td>
251 <td>cell11<br />continued</td>
252 <td>cell12</td>
252 <td>cell12</td>
253 <td></td>
253 <td></td>
254 </tr>
254 </tr>
255 <tr>
255 <tr>
256 <td><del>cell21</del></td>
256 <td><del>cell21</del></td>
257 <td></td>
257 <td></td>
258 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
258 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
259 </tr>
259 </tr>
260 <tr>
260 <tr>
261 <td>cell31</td>
261 <td>cell31</td>
262 <td>cell32<br/>cell32 line2</td>
262 <td>cell32<br/>cell32 line2</td>
263 <td>cell33</td>
263 <td>cell33</td>
264 </tr>
264 </tr>
265 </table>
265 </table>
266 EXPECTED
266 EXPECTED
267
267
268 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
268 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
269 end
269 end
270
270
271 def test_tables_with_lists
272 raw = <<-RAW
273 This is a table with lists:
274
275 |cell11|cell12|
276 |cell21|ordered list
277 # item
278 # item 2|
279 |cell31|unordered list
280 * item
281 * item 2|
282
283 RAW
284
285 expected = <<-EXPECTED
286 <p>This is a table with lists:</p>
287
288 <table>
289 <tr>
290 <td>cell11</td>
291 <td>cell12</td>
292 </tr>
293 <tr>
294 <td>cell21</td>
295 <td>ordered list<br /># item<br /># item 2</td>
296 </tr>
297 <tr>
298 <td>cell31</td>
299 <td>unordered list<br />* item<br />* item 2</td>
300 </tr>
301 </table>
302 EXPECTED
303
304 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
305 end
306
271 def test_textile_should_not_mangle_brackets
307 def test_textile_should_not_mangle_brackets
272 assert_equal '<p>[msg1][msg2]</p>', to_html('[msg1][msg2]')
308 assert_equal '<p>[msg1][msg2]</p>', to_html('[msg1][msg2]')
273 end
309 end
274
310
275 def test_textile_should_escape_image_urls
311 def test_textile_should_escape_image_urls
276 # this is onclick="alert('XSS');" in encoded form
312 # this is onclick="alert('XSS');" in encoded form
277 raw = '!/images/comment.png"onclick=&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;!'
313 raw = '!/images/comment.png"onclick=&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;!'
278 expected = '<p><img src="/images/comment.png&quot;onclick=&amp;#x61;&amp;#x6c;&amp;#x65;&amp;#x72;&amp;#x74;&amp;#x28;&amp;#x27;&amp;#x58;&amp;#x53;&amp;#x53;&amp;#x27;&amp;#x29;;&amp;#x22;" alt="" /></p>'
314 expected = '<p><img src="/images/comment.png&quot;onclick=&amp;#x61;&amp;#x6c;&amp;#x65;&amp;#x72;&amp;#x74;&amp;#x28;&amp;#x27;&amp;#x58;&amp;#x53;&amp;#x53;&amp;#x27;&amp;#x29;;&amp;#x22;" alt="" /></p>'
279 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
315 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
280 end
316 end
281
317
282
318
283 STR_WITHOUT_PRE = [
319 STR_WITHOUT_PRE = [
284 # 0
320 # 0
285 "h1. Title
321 "h1. Title
286
322
287 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
323 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
288 # 1
324 # 1
289 "h2. Heading 2
325 "h2. Heading 2
290
326
291 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
327 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
292
328
293 Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc.",
329 Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc.",
294 # 2
330 # 2
295 "h2. Heading 2
331 "h2. Heading 2
296
332
297 Morbi facilisis accumsan orci non pharetra.
333 Morbi facilisis accumsan orci non pharetra.
298
334
299 h3. Heading 3
335 h3. Heading 3
300
336
301 Nulla nunc nisi, egestas in ornare vel, posuere ac libero.",
337 Nulla nunc nisi, egestas in ornare vel, posuere ac libero.",
302 # 3
338 # 3
303 "h3. Heading 3
339 "h3. Heading 3
304
340
305 Praesent eget turpis nibh, a lacinia nulla.",
341 Praesent eget turpis nibh, a lacinia nulla.",
306 # 4
342 # 4
307 "h2. Heading 2
343 "h2. Heading 2
308
344
309 Ut rhoncus elementum adipiscing."]
345 Ut rhoncus elementum adipiscing."]
310
346
311 TEXT_WITHOUT_PRE = STR_WITHOUT_PRE.join("\n\n").freeze
347 TEXT_WITHOUT_PRE = STR_WITHOUT_PRE.join("\n\n").freeze
312
348
313 def test_get_section_should_return_the_requested_section_and_its_hash
349 def test_get_section_should_return_the_requested_section_and_its_hash
314 assert_section_with_hash STR_WITHOUT_PRE[1], TEXT_WITHOUT_PRE, 2
350 assert_section_with_hash STR_WITHOUT_PRE[1], TEXT_WITHOUT_PRE, 2
315 assert_section_with_hash STR_WITHOUT_PRE[2..3].join("\n\n"), TEXT_WITHOUT_PRE, 3
351 assert_section_with_hash STR_WITHOUT_PRE[2..3].join("\n\n"), TEXT_WITHOUT_PRE, 3
316 assert_section_with_hash STR_WITHOUT_PRE[3], TEXT_WITHOUT_PRE, 5
352 assert_section_with_hash STR_WITHOUT_PRE[3], TEXT_WITHOUT_PRE, 5
317 assert_section_with_hash STR_WITHOUT_PRE[4], TEXT_WITHOUT_PRE, 6
353 assert_section_with_hash STR_WITHOUT_PRE[4], TEXT_WITHOUT_PRE, 6
318
354
319 assert_section_with_hash '', TEXT_WITHOUT_PRE, 0
355 assert_section_with_hash '', TEXT_WITHOUT_PRE, 0
320 assert_section_with_hash '', TEXT_WITHOUT_PRE, 10
356 assert_section_with_hash '', TEXT_WITHOUT_PRE, 10
321 end
357 end
322
358
323 def test_update_section_should_update_the_requested_section
359 def test_update_section_should_update_the_requested_section
324 replacement = "New text"
360 replacement = "New text"
325
361
326 assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement)
362 assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement)
327 assert_equal [STR_WITHOUT_PRE[0..1], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(3, replacement)
363 assert_equal [STR_WITHOUT_PRE[0..1], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(3, replacement)
328 assert_equal [STR_WITHOUT_PRE[0..2], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(5, replacement)
364 assert_equal [STR_WITHOUT_PRE[0..2], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(5, replacement)
329 assert_equal [STR_WITHOUT_PRE[0..3], replacement].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(6, replacement)
365 assert_equal [STR_WITHOUT_PRE[0..3], replacement].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(6, replacement)
330
366
331 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(0, replacement)
367 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(0, replacement)
332 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(10, replacement)
368 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(10, replacement)
333 end
369 end
334
370
335 def test_update_section_with_hash_should_update_the_requested_section
371 def test_update_section_with_hash_should_update_the_requested_section
336 replacement = "New text"
372 replacement = "New text"
337
373
338 assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"),
374 assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"),
339 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement, Digest::MD5.hexdigest(STR_WITHOUT_PRE[1]))
375 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement, Digest::MD5.hexdigest(STR_WITHOUT_PRE[1]))
340 end
376 end
341
377
342 def test_update_section_with_wrong_hash_should_raise_an_error
378 def test_update_section_with_wrong_hash_should_raise_an_error
343 assert_raise Redmine::WikiFormatting::StaleSectionError do
379 assert_raise Redmine::WikiFormatting::StaleSectionError do
344 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, "New text", Digest::MD5.hexdigest("Old text"))
380 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, "New text", Digest::MD5.hexdigest("Old text"))
345 end
381 end
346 end
382 end
347
383
348 STR_WITH_PRE = [
384 STR_WITH_PRE = [
349 # 0
385 # 0
350 "h1. Title
386 "h1. Title
351
387
352 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
388 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
353 # 1
389 # 1
354 "h2. Heading 2
390 "h2. Heading 2
355
391
356 <pre><code class=\"ruby\">
392 <pre><code class=\"ruby\">
357 def foo
393 def foo
358 end
394 end
359 </code></pre>
395 </code></pre>
360
396
361 <pre><code><pre><code class=\"ruby\">
397 <pre><code><pre><code class=\"ruby\">
362 Place your code here.
398 Place your code here.
363 </code></pre>
399 </code></pre>
364 </code></pre>
400 </code></pre>
365
401
366 Morbi facilisis accumsan orci non pharetra.
402 Morbi facilisis accumsan orci non pharetra.
367
403
368 <pre>
404 <pre>
369 Pre Content:
405 Pre Content:
370
406
371 h2. Inside pre
407 h2. Inside pre
372
408
373 <tag> inside pre block
409 <tag> inside pre block
374
410
375 Morbi facilisis accumsan orci non pharetra.
411 Morbi facilisis accumsan orci non pharetra.
376 </pre>",
412 </pre>",
377 # 2
413 # 2
378 "h3. Heading 3
414 "h3. Heading 3
379
415
380 Nulla nunc nisi, egestas in ornare vel, posuere ac libero."]
416 Nulla nunc nisi, egestas in ornare vel, posuere ac libero."]
381
417
382 def test_get_section_should_ignore_pre_content
418 def test_get_section_should_ignore_pre_content
383 text = STR_WITH_PRE.join("\n\n")
419 text = STR_WITH_PRE.join("\n\n")
384
420
385 assert_section_with_hash STR_WITH_PRE[1..2].join("\n\n"), text, 2
421 assert_section_with_hash STR_WITH_PRE[1..2].join("\n\n"), text, 2
386 assert_section_with_hash STR_WITH_PRE[2], text, 3
422 assert_section_with_hash STR_WITH_PRE[2], text, 3
387 end
423 end
388
424
389 def test_update_section_should_not_escape_pre_content_outside_section
425 def test_update_section_should_not_escape_pre_content_outside_section
390 text = STR_WITH_PRE.join("\n\n")
426 text = STR_WITH_PRE.join("\n\n")
391 replacement = "New text"
427 replacement = "New text"
392
428
393 assert_equal [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"),
429 assert_equal [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"),
394 @formatter.new(text).update_section(3, replacement)
430 @formatter.new(text).update_section(3, replacement)
395 end
431 end
396
432
397 def test_get_section_should_support_lines_with_spaces_before_heading
433 def test_get_section_should_support_lines_with_spaces_before_heading
398 # the lines after Content 2 and Heading 4 contain a space
434 # the lines after Content 2 and Heading 4 contain a space
399 text = <<-STR
435 text = <<-STR
400 h1. Heading 1
436 h1. Heading 1
401
437
402 Content 1
438 Content 1
403
439
404 h1. Heading 2
440 h1. Heading 2
405
441
406 Content 2
442 Content 2
407
443
408 h1. Heading 3
444 h1. Heading 3
409
445
410 Content 3
446 Content 3
411
447
412 h1. Heading 4
448 h1. Heading 4
413
449
414 Content 4
450 Content 4
415 STR
451 STR
416
452
417 [1, 2, 3, 4].each do |index|
453 [1, 2, 3, 4].each do |index|
418 assert_match /\Ah1. Heading #{index}.+Content #{index}/m, @formatter.new(text).get_section(index).first
454 assert_match /\Ah1. Heading #{index}.+Content #{index}/m, @formatter.new(text).get_section(index).first
419 end
455 end
420 end
456 end
421
457
422 def test_get_section_should_support_headings_starting_with_a_tab
458 def test_get_section_should_support_headings_starting_with_a_tab
423 text = <<-STR
459 text = <<-STR
424 h1.\tHeading 1
460 h1.\tHeading 1
425
461
426 Content 1
462 Content 1
427
463
428 h1. Heading 2
464 h1. Heading 2
429
465
430 Content 2
466 Content 2
431 STR
467 STR
432
468
433 assert_match /\Ah1.\tHeading 1\s+Content 1\z/, @formatter.new(text).get_section(1).first
469 assert_match /\Ah1.\tHeading 1\s+Content 1\z/, @formatter.new(text).get_section(1).first
434 end
470 end
435
471
436 private
472 private
437
473
438 def assert_html_output(to_test, expect_paragraph = true)
474 def assert_html_output(to_test, expect_paragraph = true)
439 to_test.each do |text, expected|
475 to_test.each do |text, expected|
440 assert_equal(( expect_paragraph ? "<p>#{expected}</p>" : expected ), @formatter.new(text).to_html, "Formatting the following text failed:\n===\n#{text}\n===\n")
476 assert_equal(( expect_paragraph ? "<p>#{expected}</p>" : expected ), @formatter.new(text).to_html, "Formatting the following text failed:\n===\n#{text}\n===\n")
441 end
477 end
442 end
478 end
443
479
444 def to_html(text)
480 def to_html(text)
445 @formatter.new(text).to_html
481 @formatter.new(text).to_html
446 end
482 end
447
483
448 def assert_section_with_hash(expected, text, index)
484 def assert_section_with_hash(expected, text, index)
449 result = @formatter.new(text).get_section(index)
485 result = @formatter.new(text).get_section(index)
450
486
451 assert_kind_of Array, result
487 assert_kind_of Array, result
452 assert_equal 2, result.size
488 assert_equal 2, result.size
453 assert_equal expected, result.first, "section content did not match"
489 assert_equal expected, result.first, "section content did not match"
454 assert_equal Digest::MD5.hexdigest(expected), result.last, "section hash did not match"
490 assert_equal Digest::MD5.hexdigest(expected), result.last, "section hash did not match"
455 end
491 end
456 end
492 end
General Comments 0
You need to be logged in to leave comments. Login now