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