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