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