##// END OF EJS Templates
Added Redmine::WikiFormatting module and tests for wiki links....
Jean-Philippe Lang -
r688:8a3e713f2f77
parent child
Show More
This diff has been collapsed as it changes many lines, (1130 lines changed) Show them Hide them
@@ -0,0 +1,1130
1 # vim:ts=4:sw=4:
2 # = RedCloth - Textile and Markdown Hybrid for Ruby
3 #
4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
7 # License:: BSD
8 #
9 # (see http://hobix.com/textile/ for a Textile Reference.)
10 #
11 # Based on (and also inspired by) both:
12 #
13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
14 # Textism for PHP: http://www.textism.com/tools/textile/
15 #
16 #
17
18 # = RedCloth
19 #
20 # RedCloth is a Ruby library for converting Textile and/or Markdown
21 # into HTML. You can use either format, intermingled or separately.
22 # You can also extend RedCloth to honor your own custom text stylings.
23 #
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.
26 #
27 # == What is Textile?
28 #
29 # Textile is a simple formatting style for text
30 # documents, loosely based on some HTML conventions.
31 #
32 # == Sample Textile Text
33 #
34 # h2. This is a title
35 #
36 # h3. This is a subhead
37 #
38 # This is a bit of paragraph.
39 #
40 # bq. This is a blockquote.
41 #
42 # = Writing Textile
43 #
44 # A Textile document consists of paragraphs. Paragraphs
45 # can be specially formatted by adding a small instruction
46 # to the beginning of the paragraph.
47 #
48 # h[n]. Header of size [n].
49 # bq. Blockquote.
50 # # Numeric list.
51 # * Bulleted list.
52 #
53 # == Quick Phrase Modifiers
54 #
55 # Quick phrase modifiers are also included, to allow formatting
56 # of small portions of text within a paragraph.
57 #
58 # \_emphasis\_
59 # \_\_italicized\_\_
60 # \*strong\*
61 # \*\*bold\*\*
62 # ??citation??
63 # -deleted text-
64 # +inserted text+
65 # ^superscript^
66 # ~subscript~
67 # @code@
68 # %(classname)span%
69 #
70 # ==notextile== (leave text alone)
71 #
72 # == Links
73 #
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.
76 #
77 # Optional: text in (parentheses) following the link text,
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.
80 #
81 # Example:
82 #
83 # "This is a link (This is a title) ":http://www.textism.com
84 #
85 # Will become:
86 #
87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
88 #
89 # == Images
90 #
91 # To insert an image, put the URL for the image inside exclamation marks.
92 #
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
95 # have descriptive Alt text for the benefit of readers using non-graphical
96 # browsers.
97 #
98 # Optional: place a colon followed by a URL immediately after the
99 # closing ! to make the image into a link.
100 #
101 # Example:
102 #
103 # !http://www.textism.com/common/textist.gif(Textist)!
104 #
105 # Will become:
106 #
107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
108 #
109 # With a link:
110 #
111 # !/common/textist.gif(Textist)!:http://textism.com
112 #
113 # Will become:
114 #
115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
116 #
117 # == Defining Acronyms
118 #
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,
121 # this should be used at least once for each acronym in documents where they appear.
122 #
123 # To quickly define an acronym in Textile, place the full text in (parentheses)
124 # immediately following the acronym.
125 #
126 # Example:
127 #
128 # ACLU(American Civil Liberties Union)
129 #
130 # Will become:
131 #
132 # <acronym title="American Civil Liberties Union">ACLU</acronym>
133 #
134 # == Adding Tables
135 #
136 # In Textile, simple tables can be added by seperating each column by
137 # a pipe.
138 #
139 # |a|simple|table|row|
140 # |And|Another|table|row|
141 #
142 # Attributes are defined by style definitions in parentheses.
143 #
144 # table(border:1px solid black).
145 # (background:#ddd;color:red). |{}| | | |
146 #
147 # == Using RedCloth
148 #
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
151 # RedCloth#to_html method.
152 #
153 # doc = RedCloth.new "
154 #
155 # h2. Test document
156 #
157 # Just a simple test."
158 #
159 # puts doc.to_html
160 #
161 # By default, RedCloth uses both Textile and Markdown formatting, with
162 # Textile formatting taking precedence. If you want to turn off Markdown
163 # formatting, to boost speed and limit the processor:
164 #
165 # class RedCloth::Textile.new( str )
166
167 class RedCloth < String
168
169 VERSION = '3.0.4'
170 DEFAULT_RULES = [:textile, :markdown]
171
172 #
173 # Two accessor for setting security restrictions.
174 #
175 # This is a nice thing if you're using RedCloth for
176 # formatting in public places (e.g. Wikis) where you
177 # don't want users to abuse HTML for bad things.
178 #
179 # If +:filter_html+ is set, HTML which wasn't
180 # created by the Textile processor will be escaped.
181 #
182 # If +:filter_styles+ is set, it will also disable
183 # the style markup specifier. ('{color: red}')
184 #
185 attr_accessor :filter_html, :filter_styles
186
187 #
188 # Accessor for toggling hard breaks.
189 #
190 # If +:hard_breaks+ is set, single newlines will
191 # be converted to HTML break tags. This is the
192 # default behavior for traditional RedCloth.
193 #
194 attr_accessor :hard_breaks
195
196 # Accessor for toggling lite mode.
197 #
198 # In lite mode, block-level rules are ignored. This means
199 # that tables, paragraphs, lists, and such aren't available.
200 # Only the inline markup for bold, italics, entities and so on.
201 #
202 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
203 # r.to_html
204 # #=> "And then? She <strong>fell</strong>!"
205 #
206 attr_accessor :lite_mode
207
208 #
209 # Accessor for toggling span caps.
210 #
211 # Textile places `span' tags around capitalized
212 # words by default, but this wreaks havoc on Wikis.
213 # If +:no_span_caps+ is set, this will be
214 # suppressed.
215 #
216 attr_accessor :no_span_caps
217
218 #
219 # Establishes the markup predence. Available rules include:
220 #
221 # == Textile Rules
222 #
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
225 # the following precedence:
226 #
227 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
228 # block_textile_table:: Textile table block structures
229 # block_textile_lists:: Textile list structures
230 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
231 # inline_textile_image:: Textile inline images
232 # inline_textile_link:: Textile inline links
233 # inline_textile_span:: Textile inline spans
234 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
235 #
236 # == Markdown
237 #
238 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
239 # block_markdown_setext:: Markdown setext headers
240 # block_markdown_atx:: Markdown atx headers
241 # block_markdown_rule:: Markdown horizontal rules
242 # block_markdown_bq:: Markdown blockquotes
243 # block_markdown_lists:: Markdown lists
244 # inline_markdown_link:: Markdown links
245 attr_accessor :rules
246
247 # Returns a new RedCloth object, based on _string_ and
248 # enforcing all the included _restrictions_.
249 #
250 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
251 # r.to_html
252 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
253 #
254 def initialize( string, restrictions = [] )
255 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
256 super( string )
257 end
258
259 #
260 # Generates HTML from the Textile contents.
261 #
262 # r = RedCloth.new( "And then? She *fell*!" )
263 # r.to_html( true )
264 # #=>"And then? She <strong>fell</strong>!"
265 #
266 def to_html( *rules )
267 rules = DEFAULT_RULES if rules.empty?
268 # make our working copy
269 text = self.dup
270
271 @urlrefs = {}
272 @shelf = []
273 textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists,
274 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
275 :inline_textile_code, :inline_textile_span, :glyphs_textile]
276 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
277 :block_markdown_bq, :block_markdown_lists,
278 :inline_markdown_reflink, :inline_markdown_link]
279 @rules = rules.collect do |rule|
280 case rule
281 when :markdown
282 markdown_rules
283 when :textile
284 textile_rules
285 else
286 rule
287 end
288 end.flatten
289
290 # standard clean up
291 incoming_entities text
292 clean_white_space text
293
294 # start processor
295 @pre_list = []
296 rip_offtags text
297 no_textile text
298 hard_break text
299 unless @lite_mode
300 refs text
301 blocks text
302 end
303 inline text
304 smooth_offtags text
305
306 retrieve text
307
308 text.gsub!( /<\/?notextile>/, '' )
309 text.gsub!( /x%x%/, '&#38;' )
310 clean_html text if filter_html
311 text.strip!
312 text
313
314 end
315
316 #######
317 private
318 #######
319 #
320 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
321 # (from PyTextile)
322 #
323 TEXTILE_TAGS =
324
325 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
326 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
327 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
328 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
329 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
330
331 collect! do |a, b|
332 [a.chr, ( b.zero? and "" or "&#{ b };" )]
333 end
334
335 #
336 # Regular expressions to convert to HTML.
337 #
338 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
339 A_VLGN = /[\-^~]/
340 C_CLAS = '(?:\([^)]+\))'
341 C_LNGE = '(?:\[[^\]]+\])'
342 C_STYL = '(?:\{[^}]+\})'
343 S_CSPN = '(?:\\\\\d+)'
344 S_RSPN = '(?:/\d+)'
345 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
346 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
347 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
348 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
349 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
350 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
351 PUNCT_Q = Regexp::quote( '*-_+^~%' )
352 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
353
354 # Text markup tags, don't conflict with block tags
355 SIMPLE_HTML_TAGS = [
356 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
357 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
358 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
359 ]
360
361 QTAGS = [
362 ['**', 'b'],
363 ['*', 'strong'],
364 ['??', 'cite', :limit],
365 ['-', 'del', :limit],
366 ['__', 'i'],
367 ['_', 'em', :limit],
368 ['%', 'span', :limit],
369 ['+', 'ins', :limit],
370 ['^', 'sup'],
371 ['~', 'sub']
372 ]
373 QTAGS.collect! do |rc, ht, rtype|
374 rcq = Regexp::quote rc
375 re =
376 case rtype
377 when :limit
378 /(\W)
379 (#{rcq})
380 (#{C})
381 (?::(\S+?))?
382 (\S.*?\S|\S)
383 #{rcq}
384 (?=\W)/x
385 else
386 /(#{rcq})
387 (#{C})
388 (?::(\S+))?
389 (\S.*?\S|\S)
390 #{rcq}/xm
391 end
392 [rc, ht, re, rtype]
393 end
394
395 # Elements to handle
396 GLYPHS = [
397 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
398 [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
399 [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
400 [ /\'/, '&#8216;' ], # single opening
401 [ /</, '&lt;' ], # less-than
402 [ />/, '&gt;' ], # greater-than
403 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
404 [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
405 [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
406 [ /"/, '&#8220;' ], # double opening
407 [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
408 [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
409 [ /(^|[^"][>\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)?\s?--\s?/, '\1&#8212;' ], # em dash
411 [ /\s->\s/, ' &rarr; ' ], # right arrow
412 [ /\s-\s/, ' &#8211; ' ], # en dash
413 [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
414 [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
415 [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
416 [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
417 ]
418
419 H_ALGN_VALS = {
420 '<' => 'left',
421 '=' => 'center',
422 '>' => 'right',
423 '<>' => 'justify'
424 }
425
426 V_ALGN_VALS = {
427 '^' => 'top',
428 '-' => 'middle',
429 '~' => 'bottom'
430 }
431
432 #
433 # Flexible HTML escaping
434 #
435 def htmlesc( str, mode )
436 str.gsub!( '&', '&amp;' )
437 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
438 str.gsub!( "'", '&#039;' ) if mode == :Quotes
439 str.gsub!( '<', '&lt;')
440 str.gsub!( '>', '&gt;')
441 end
442
443 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
444 def pgl( text )
445 GLYPHS.each do |re, resub, tog|
446 next if tog and method( tog ).call
447 text.gsub! re, resub
448 end
449 end
450
451 # Parses Textile attribute lists and builds an HTML attribute string
452 def pba( text_in, element = "" )
453
454 return '' unless text_in
455
456 style = []
457 text = text_in.dup
458 if element == 'td'
459 colspan = $1 if text =~ /\\(\d+)/
460 rowspan = $1 if text =~ /\/(\d+)/
461 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
462 end
463
464 style << "#{ $1 };" if not filter_styles and
465 text.sub!( /\{([^}]*)\}/, '' )
466
467 lang = $1 if
468 text.sub!( /\[([^)]+?)\]/, '' )
469
470 cls = $1 if
471 text.sub!( /\(([^()]+?)\)/, '' )
472
473 style << "padding-left:#{ $1.length }em;" if
474 text.sub!( /([(]+)/, '' )
475
476 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
477
478 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
479
480 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
481
482 atts = ''
483 atts << " style=\"#{ style.join }\"" unless style.empty?
484 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
485 atts << " lang=\"#{ lang }\"" if lang
486 atts << " id=\"#{ id }\"" if id
487 atts << " colspan=\"#{ colspan }\"" if colspan
488 atts << " rowspan=\"#{ rowspan }\"" if rowspan
489
490 atts
491 end
492
493 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
494
495 # Parses a Textile table block, building HTML from the result.
496 def block_textile_table( text )
497 text.gsub!( TABLE_RE ) do |matches|
498
499 tatts, fullrow = $~[1..2]
500 tatts = pba( tatts, 'table' )
501 tatts = shelve( tatts ) if tatts
502 rows = []
503
504 fullrow.
505 split( /\|$/m ).
506 delete_if { |x| x.empty? }.
507 each do |row|
508
509 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
510
511 cells = []
512 row.split( '|' ).each do |cell|
513 ctyp = 'd'
514 ctyp = 'h' if cell =~ /^_/
515
516 catts = ''
517 catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
518
519 unless cell.strip.empty?
520 catts = shelve( catts ) if catts
521 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
522 end
523 end
524 ratts = shelve( ratts ) if ratts
525 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
526 end
527 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
528 end
529 end
530
531 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
532 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
533
534 # Parses Textile lists and generates HTML
535 def block_textile_lists( text )
536 text.gsub!( LISTS_RE ) do |match|
537 lines = match.split( /\n/ )
538 last_line = -1
539 depth = []
540 lines.each_with_index do |line, line_id|
541 if line =~ LISTS_CONTENT_RE
542 tl,atts,content = $~[1..3]
543 if depth.last
544 if depth.last.length > tl.length
545 (depth.length - 1).downto(0) do |i|
546 break if depth[i].length == tl.length
547 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
548 depth.pop
549 end
550 end
551 if depth.last and depth.last.length == tl.length
552 lines[line_id - 1] << '</li>'
553 end
554 end
555 unless depth.last == tl
556 depth << tl
557 atts = pba( atts )
558 atts = shelve( atts ) if atts
559 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
560 else
561 lines[line_id] = "\t\t<li>#{ content }"
562 end
563 last_line = line_id
564
565 else
566 last_line = line_id
567 end
568 if line_id - last_line > 1 or line_id == lines.length - 1
569 depth.delete_if do |v|
570 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
571 end
572 end
573 end
574 lines.join( "\n" )
575 end
576 end
577
578 CODE_RE = /(\W)
579 @
580 (?:\|(\w+?)\|)?
581 (.+?)
582 @
583 (?=\W)/x
584
585 def inline_textile_code( text )
586 text.gsub!( CODE_RE ) do |m|
587 before,lang,code,after = $~[1..4]
588 lang = " lang=\"#{ lang }\"" if lang
589 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }" )
590 end
591 end
592
593 def lT( text )
594 text =~ /\#$/ ? 'o' : 'u'
595 end
596
597 def hard_break( text )
598 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
599 end
600
601 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
602
603 def blocks( text, deep_code = false )
604 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
605 plain = blk !~ /\A[#*> ]/
606
607 # skip blocks that are complex HTML
608 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
609 blk
610 else
611 # search for indentation levels
612 blk.strip!
613 if blk.empty?
614 blk
615 else
616 code_blk = nil
617 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
618 flush_left iblk
619 blocks iblk, plain
620 iblk.gsub( /^(\S)/, "\t\\1" )
621 if plain
622 code_blk = iblk; ""
623 else
624 iblk
625 end
626 end
627
628 block_applied = 0
629 @rules.each do |rule_name|
630 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
631 end
632 if block_applied.zero?
633 if deep_code
634 blk = "\t<pre><code>#{ blk }</code></pre>"
635 else
636 blk = "\t<p>#{ blk }</p>"
637 end
638 end
639 # hard_break blk
640 blk + "\n#{ code_blk }"
641 end
642 end
643
644 end.join( "\n\n" ) )
645 end
646
647 def textile_bq( tag, atts, cite, content )
648 cite, cite_title = check_refs( cite )
649 cite = " cite=\"#{ cite }\"" if cite
650 atts = shelve( atts ) if atts
651 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
652 end
653
654 def textile_p( tag, atts, cite, content )
655 atts = shelve( atts ) if atts
656 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
657 end
658
659 alias textile_h1 textile_p
660 alias textile_h2 textile_p
661 alias textile_h3 textile_p
662 alias textile_h4 textile_p
663 alias textile_h5 textile_p
664 alias textile_h6 textile_p
665
666 def textile_fn_( tag, num, atts, cite, content )
667 atts << " id=\"fn#{ num }\""
668 content = "<sup>#{ num }</sup> #{ content }"
669 atts = shelve( atts ) if atts
670 "\t<p#{ atts }>#{ content }</p>"
671 end
672
673 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
674
675 def block_textile_prefix( text )
676 if text =~ BLOCK_RE
677 tag,tagpre,num,atts,cite,content = $~[1..6]
678 atts = pba( atts )
679
680 # pass to prefix handler
681 if respond_to? "textile_#{ tag }", true
682 text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) )
683 elsif respond_to? "textile_#{ tagpre }_", true
684 text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) )
685 end
686 end
687 end
688
689 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
690 def block_markdown_setext( text )
691 if text =~ SETEXT_RE
692 tag = if $2 == "="; "h1"; else; "h2"; end
693 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
694 blocks cont
695 text.replace( blk + cont )
696 end
697 end
698
699 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
700 [ ]*
701 (.+?) # $2 = Header text
702 [ ]*
703 \#* # optional closing #'s (not counted)
704 $/x
705 def block_markdown_atx( text )
706 if text =~ ATX_RE
707 tag = "h#{ $1.length }"
708 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
709 blocks cont
710 text.replace( blk + cont )
711 end
712 end
713
714 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
715
716 def block_markdown_bq( text )
717 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
718 blk.gsub!( /^ *> ?/, '' )
719 flush_left blk
720 blocks blk
721 blk.gsub!( /^(\S)/, "\t\\1" )
722 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
723 end
724 end
725
726 MARKDOWN_RULE_RE = /^(#{
727 ['*', '-', '_'].collect { |ch| '( ?' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
728 })$/
729
730 def block_markdown_rule( text )
731 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
732 "<hr />"
733 end
734 end
735
736 # XXX TODO XXX
737 def block_markdown_lists( text )
738 end
739
740 def inline_textile_span( text )
741 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
742 text.gsub!( qtag_re ) do |m|
743
744 case rtype
745 when :limit
746 sta,qtag,atts,cite,content = $~[1..5]
747 else
748 qtag,atts,cite,content = $~[1..4]
749 sta = ''
750 end
751 atts = pba( atts )
752 atts << " cite=\"#{ cite }\"" if cite
753 atts = shelve( atts ) if atts
754
755 "#{ sta }<#{ ht }#{ atts }>#{ content }</#{ ht }>"
756
757 end
758 end
759 end
760
761 LINK_RE = /
762 ([\s\[{(]|[#{PUNCT}])? # $pre
763 " # start
764 (#{C}) # $atts
765 ([^"]+?) # $text
766 \s?
767 (?:\(([^)]+?)\)(?="))? # $title
768 ":
769 (\S+?) # $url
770 (\/)? # $slash
771 ([^\w\/;]*?) # $post
772 (?=<|\s|$)
773 /x
774
775 def inline_textile_link( text )
776 text.gsub!( LINK_RE ) do |m|
777 pre,atts,text,title,url,slash,post = $~[1..7]
778
779 url, url_title = check_refs( url )
780 title ||= url_title
781
782 atts = pba( atts )
783 atts = " href=\"#{ url }#{ slash }\"#{ atts }"
784 atts << " title=\"#{ title }\"" if title
785 atts = shelve( atts ) if atts
786
787 "#{ pre }<a#{ atts }>#{ text }</a>#{ post }"
788 end
789 end
790
791 MARKDOWN_REFLINK_RE = /
792 \[([^\[\]]+)\] # $text
793 [ ]? # opt. space
794 (?:\n[ ]*)? # one optional newline followed by spaces
795 \[(.*?)\] # $id
796 /x
797
798 def inline_markdown_reflink( text )
799 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
800 text, id = $~[1..2]
801
802 if id.empty?
803 url, title = check_refs( text )
804 else
805 url, title = check_refs( id )
806 end
807
808 atts = " href=\"#{ url }\""
809 atts << " title=\"#{ title }\"" if title
810 atts = shelve( atts )
811
812 "<a#{ atts }>#{ text }</a>"
813 end
814 end
815
816 MARKDOWN_LINK_RE = /
817 \[([^\[\]]+)\] # $text
818 \( # open paren
819 [ \t]* # opt space
820 <?(.+?)>? # $href
821 [ \t]* # opt space
822 (?: # whole title
823 (['"]) # $quote
824 (.*?) # $title
825 \3 # matching quote
826 )? # title is optional
827 \)
828 /x
829
830 def inline_markdown_link( text )
831 text.gsub!( MARKDOWN_LINK_RE ) do |m|
832 text, url, quote, title = $~[1..4]
833
834 atts = " href=\"#{ url }\""
835 atts << " title=\"#{ title }\"" if title
836 atts = shelve( atts )
837
838 "<a#{ atts }>#{ text }</a>"
839 end
840 end
841
842 TEXTILE_REFS_RE = /(^ *)\[([^\n]+?)\](#{HYPERLINK})(?=\s|$)/
843 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
844
845 def refs( text )
846 @rules.each do |rule_name|
847 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
848 end
849 end
850
851 def refs_textile( text )
852 text.gsub!( TEXTILE_REFS_RE ) do |m|
853 flag, url = $~[2..3]
854 @urlrefs[flag.downcase] = [url, nil]
855 nil
856 end
857 end
858
859 def refs_markdown( text )
860 text.gsub!( MARKDOWN_REFS_RE ) do |m|
861 flag, url = $~[2..3]
862 title = $~[6]
863 @urlrefs[flag.downcase] = [url, title]
864 nil
865 end
866 end
867
868 def check_refs( text )
869 ret = @urlrefs[text.downcase] if text
870 ret || [text, nil]
871 end
872
873 IMAGE_RE = /
874 (<p>|.|^) # start of line?
875 \! # opening
876 (\<|\=|\>)? # optional alignment atts
877 (#{C}) # optional style,class atts
878 (?:\. )? # optional dot-space
879 ([^\s(!]+?) # presume this is the src
880 \s? # optional space
881 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
882 \! # closing
883 (?::#{ HYPERLINK })? # optional href
884 /x
885
886 def inline_textile_image( text )
887 text.gsub!( IMAGE_RE ) do |m|
888 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
889 atts = pba( atts )
890 atts = " src=\"#{ url }\"#{ atts }"
891 atts << " title=\"#{ title }\"" if title
892 atts << " alt=\"#{ title }\""
893 # size = @getimagesize($url);
894 # if($size) $atts.= " $size[3]";
895
896 href, alt_title = check_refs( href ) if href
897 url, url_title = check_refs( url )
898
899 out = ''
900 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
901 out << "<img#{ shelve( atts ) } />"
902 out << "</a>#{ href_a1 }#{ href_a2 }" if href
903
904 if algn
905 algn = h_align( algn )
906 if stln == "<p>"
907 out = "<p style=\"float:#{ algn }\">#{ out }"
908 else
909 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
910 end
911 else
912 out = stln + out
913 end
914
915 out
916 end
917 end
918
919 def shelve( val )
920 @shelf << val
921 " :redsh##{ @shelf.length }:"
922 end
923
924 def retrieve( text )
925 @shelf.each_with_index do |r, i|
926 text.gsub!( " :redsh##{ i + 1 }:", r )
927 end
928 end
929
930 def incoming_entities( text )
931 ## turn any incoming ampersands into a dummy character for now.
932 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
933 ## implying an incoming html entity, to be skipped
934
935 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
936 end
937
938 def no_textile( text )
939 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
940 '\1<notextile>\2</notextile>\3' )
941 text.gsub!( /^ *==([^=]+.*?)==/m,
942 '\1<notextile>\2</notextile>\3' )
943 end
944
945 def clean_white_space( text )
946 # normalize line breaks
947 text.gsub!( /\r\n/, "\n" )
948 text.gsub!( /\r/, "\n" )
949 text.gsub!( /\t/, ' ' )
950 text.gsub!( /^ +$/, '' )
951 text.gsub!( /\n{3,}/, "\n\n" )
952 text.gsub!( /"$/, "\" " )
953
954 # if entire document is indented, flush
955 # to the left side
956 flush_left text
957 end
958
959 def flush_left( text )
960 indt = 0
961 if text =~ /^ /
962 while text !~ /^ {#{indt}}\S/
963 indt += 1
964 end unless text.empty?
965 if indt.nonzero?
966 text.gsub!( /^ {#{indt}}/, '' )
967 end
968 end
969 end
970
971 def footnote_ref( text )
972 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
973 '<sup><a href="#fn\1">\1</a></sup>\2' )
974 end
975
976 OFFTAGS = /(code|pre|kbd|notextile)/
977 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi
978 OFFTAG_OPEN = /<#{ OFFTAGS }/
979 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
980 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
981 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
982
983 def glyphs_textile( text, level = 0 )
984 if text !~ HASTAG_MATCH
985 pgl text
986 footnote_ref text
987 else
988 codepre = 0
989 text.gsub!( ALLTAG_MATCH ) do |line|
990 ## matches are off if we're between <code>, <pre> etc.
991 if $1
992 if line =~ OFFTAG_OPEN
993 codepre += 1
994 elsif line =~ OFFTAG_CLOSE
995 codepre -= 1
996 codepre = 0 if codepre < 0
997 end
998 elsif codepre.zero?
999 glyphs_textile( line, level + 1 )
1000 else
1001 htmlesc( line, :NoQuotes )
1002 end
1003 # p [level, codepre, line]
1004
1005 line
1006 end
1007 end
1008 end
1009
1010 def rip_offtags( text )
1011 if text =~ /<.*>/
1012 ## strip and encode <pre> content
1013 codepre, used_offtags = 0, {}
1014 text.gsub!( OFFTAG_MATCH ) do |line|
1015 if $3
1016 offtag, aftertag = $4, $5
1017 codepre += 1
1018 used_offtags[offtag] = true
1019 if codepre - used_offtags.length > 0
1020 htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
1021 @pre_list.last << line
1022 line = ""
1023 else
1024 htmlesc( aftertag, :NoQuotes ) if aftertag and not used_offtags['notextile']
1025 line = "<redpre##{ @pre_list.length }>"
1026 @pre_list << "#{ $3 }#{ aftertag }"
1027 end
1028 elsif $1 and codepre > 0
1029 if codepre - used_offtags.length > 0
1030 htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
1031 @pre_list.last << line
1032 line = ""
1033 end
1034 codepre -= 1 unless codepre.zero?
1035 used_offtags = {} if codepre.zero?
1036 end
1037 line
1038 end
1039 end
1040 text
1041 end
1042
1043 def smooth_offtags( text )
1044 unless @pre_list.empty?
1045 ## replace <pre> content
1046 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1047 end
1048 end
1049
1050 def inline( text )
1051 [/^inline_/, /^glyphs_/].each do |meth_re|
1052 @rules.each do |rule_name|
1053 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1054 end
1055 end
1056 end
1057
1058 def h_align( text )
1059 H_ALGN_VALS[text]
1060 end
1061
1062 def v_align( text )
1063 V_ALGN_VALS[text]
1064 end
1065
1066 def textile_popup_help( name, windowW, windowH )
1067 ' <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 />'
1068 end
1069
1070 # HTML cleansing stuff
1071 BASIC_TAGS = {
1072 'a' => ['href', 'title'],
1073 'img' => ['src', 'alt', 'title'],
1074 'br' => [],
1075 'i' => nil,
1076 'u' => nil,
1077 'b' => nil,
1078 'pre' => nil,
1079 'kbd' => nil,
1080 'code' => ['lang'],
1081 'cite' => nil,
1082 'strong' => nil,
1083 'em' => nil,
1084 'ins' => nil,
1085 'sup' => nil,
1086 'sub' => nil,
1087 'del' => nil,
1088 'table' => nil,
1089 'tr' => nil,
1090 'td' => ['colspan', 'rowspan'],
1091 'th' => nil,
1092 'ol' => nil,
1093 'ul' => nil,
1094 'li' => nil,
1095 'p' => nil,
1096 'h1' => nil,
1097 'h2' => nil,
1098 'h3' => nil,
1099 'h4' => nil,
1100 'h5' => nil,
1101 'h6' => nil,
1102 'blockquote' => ['cite']
1103 }
1104
1105 def clean_html( text, tags = BASIC_TAGS )
1106 text.gsub!( /<!\[CDATA\[/, '' )
1107 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1108 raw = $~
1109 tag = raw[2].downcase
1110 if tags.has_key? tag
1111 pcs = [tag]
1112 tags[tag].each do |prop|
1113 ['"', "'", ''].each do |q|
1114 q2 = ( q != '' ? q : '\s' )
1115 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1116 attrv = $1
1117 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1118 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1119 break
1120 end
1121 end
1122 end if tags[tag]
1123 "<#{raw[1]}#{pcs.join " "}>"
1124 else
1125 " "
1126 end
1127 end
1128 end
1129 end
1130
@@ -0,0 +1,79
1 require 'redcloth'
2
3 module Redmine
4 module WikiFormatting
5
6 private
7
8 class TextileFormatter < RedCloth
9 RULES = [:inline_auto_link, :inline_auto_mailto, :textile ]
10
11 def initialize(*args)
12 super
13 self.hard_breaks=true
14 end
15
16 def to_html
17 super(*RULES).to_s
18 end
19
20 private
21
22 # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
23 # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
24 def hard_break( text )
25 text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
26 end
27
28 AUTO_LINK_RE = %r{
29 ( # leading text
30 <\w+.*?>| # leading HTML tag, or
31 [^=<>!:'"/]| # leading punctuation, or
32 ^ # beginning of line
33 )
34 (
35 (?:https?://)| # protocol spec, or
36 (?:www\.) # www.*
37 )
38 (
39 [-\w]+ # subdomain or domain
40 (?:\.[-\w]+)* # remaining subdomains or domain
41 (?::\d+)? # port
42 (?:/(?:(?:[~\w\+%-]|(?:[,.;:][^\s$]))+)?)* # path
43 (?:\?[\w\+%&=.;-]+)? # query string
44 (?:\#[\w\-]*)? # trailing anchor
45 )
46 ([[:punct:]]|\s|<|$) # trailing text
47 }x unless const_defined?(:AUTO_LINK_RE)
48
49 # Turns all urls into clickable links (code from Rails).
50 def inline_auto_link(text)
51 text.gsub!(AUTO_LINK_RE) do
52 all, a, b, c, d = $&, $1, $2, $3, $4
53 if a =~ /<a\s/i || a =~ /![<>=]?/
54 # don't replace URL's that are already linked
55 # and URL's prefixed with ! !> !< != (textile images)
56 all
57 else
58 text = b + c
59 %(#{a}<a href="#{b=="www."?"http://www.":b}#{c}">#{text}</a>#{d})
60 end
61 end
62 end
63
64 # Turns all email addresses into clickable links (code from Rails).
65 def inline_auto_mailto(text)
66 text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
67 text = $1
68 %{<a href="mailto:#{$1}" class="email">#{text}</a>}
69 end
70 end
71 end
72
73 public
74
75 def self.to_html(text, options = {})
76 TextileFormatter.new(text).to_html
77 end
78 end
79 end
@@ -0,0 +1,35
1 # Re-raise errors caught by the controller.
2 class StubController < ApplicationController
3 def rescue_action(e) raise e end;
4 attr_accessor :request, :url
5 end
6
7 class HelperTestCase < Test::Unit::TestCase
8
9 # Add other helpers here if you need them
10 include ActionView::Helpers::ActiveRecordHelper
11 include ActionView::Helpers::TagHelper
12 include ActionView::Helpers::FormTagHelper
13 include ActionView::Helpers::FormOptionsHelper
14 include ActionView::Helpers::FormHelper
15 include ActionView::Helpers::UrlHelper
16 include ActionView::Helpers::AssetTagHelper
17 include ActionView::Helpers::PrototypeHelper
18
19 def setup
20 super
21
22 @request = ActionController::TestRequest.new
23 @controller = StubController.new
24 @controller.request = @request
25
26 # Fake url rewriter so we can test url_for
27 @controller.url = ActionController::UrlRewriter.new @request, {}
28
29 ActionView::Helpers::AssetTagHelper::reset_javascript_include_default
30 end
31
32 def test_dummy
33 # do nothing - required by test/unit
34 end
35 end
@@ -0,0 +1,66
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../../test_helper'
19
20 class ApplicationHelperTest < HelperTestCase
21 include ApplicationHelper
22 fixtures :projects
23
24 def setup
25 super
26 end
27
28 def test_auto_links
29 to_test = {
30 'http://foo.bar' => '<a href="http://foo.bar">http://foo.bar</a>',
31 'www.foo.bar' => '<a href="http://www.foo.bar">www.foo.bar</a>',
32 'http://foo.bar/page?p=1&t=z&s=' => '<a href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
33 'http://foo.bar/page#125' => '<a href="http://foo.bar/page#125">http://foo.bar/page#125</a>'
34 }
35 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
36 end
37
38 def test_auto_mailto
39 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
40 textilizable('test@foo.bar')
41 end
42
43 def test_textile_tags
44 to_test = {
45 # inline images
46 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
47 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
48 # textile links
49 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar">link</a>',
50 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title">link</a>'
51 }
52 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
53 end
54
55 def test_redmine_links
56 issue_link = link_to('#52', {:controller => 'issues', :action => 'show', :id => 52}, :class => 'issue')
57 changeset_link = link_to('r19', {:controller => 'repositories', :action => 'revision', :id => 1, :rev => 19}, :class => 'changeset')
58
59 to_test = {
60 '#52, #52 and #52.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
61 'r19' => changeset_link
62 }
63 @project = Project.find(1)
64 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
65 end
66 end
@@ -1,294 +1,292
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 class RedCloth
19 # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
20 # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
21 def hard_break( text )
22 text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
23 end
24 end
25
26 18 module ApplicationHelper
27 19
28 20 def current_role
29 21 @current_role ||= User.current.role_for_project(@project)
30 22 end
31 23
32 24 # Return true if user is authorized for controller/action, otherwise false
33 25 def authorize_for(controller, action)
34 26 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 27 end
36 28
37 29 # Display a link if user is authorized
38 30 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
39 31 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
40 32 end
41 33
42 34 # Display a link to user's account page
43 35 def link_to_user(user)
44 36 link_to user.name, :controller => 'account', :action => 'show', :id => user
45 37 end
46 38
47 39 def link_to_issue(issue)
48 40 link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue
49 41 end
50 42
51 43 def toggle_link(name, id, options={})
52 44 onclick = "Element.toggle('#{id}'); "
53 45 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
54 46 onclick << "return false;"
55 47 link_to(name, "#", :onclick => onclick)
56 48 end
57 49
58 50 def image_to_function(name, function, html_options = {})
59 51 html_options.symbolize_keys!
60 52 tag(:input, html_options.merge({
61 53 :type => "image", :src => image_path(name),
62 54 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
63 55 }))
64 56 end
65 57
66 58 def prompt_to_remote(name, text, param, url, html_options = {})
67 59 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
68 60 link_to name, {}, html_options
69 61 end
70 62
71 63 def format_date(date)
72 64 return nil unless date
73 65 @date_format_setting ||= Setting.date_format.to_i
74 66 @date_format_setting == 0 ? l_date(date) : date.strftime("%Y-%m-%d")
75 67 end
76 68
77 69 def format_time(time)
78 70 return nil unless time
79 71 @date_format_setting ||= Setting.date_format.to_i
80 72 time = time.to_time if time.is_a?(String)
81 73 @date_format_setting == 0 ? l_datetime(time) : (time.strftime("%Y-%m-%d") + ' ' + l_time(time))
82 74 end
83 75
84 76 def day_name(day)
85 77 l(:general_day_names).split(',')[day-1]
86 78 end
87 79
88 80 def month_name(month)
89 81 l(:actionview_datehelper_select_month_names).split(',')[month-1]
90 82 end
91 83
92 84 def pagination_links_full(paginator, options={}, html_options={})
93 85 page_param = options.delete(:page_param) || :page
94 86
95 87 html = ''
96 88 html << link_to_remote(('&#171; ' + l(:label_previous)),
97 89 {:update => "content", :url => options.merge(page_param => paginator.current.previous)},
98 90 {:href => url_for(:params => options.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
99 91
100 92 html << (pagination_links_each(paginator, options) do |n|
101 93 link_to_remote(n.to_s,
102 94 {:url => {:params => options.merge(page_param => n)}, :update => 'content'},
103 95 {:href => url_for(:params => options.merge(page_param => n))})
104 96 end || '')
105 97
106 98 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
107 99 {:update => "content", :url => options.merge(page_param => paginator.current.next)},
108 100 {:href => url_for(:params => options.merge(page_param => paginator.current.next))}) if paginator.current.next
109 101 html
110 102 end
111 103
112 104 # textilize text according to system settings and RedCloth availability
113 105 def textilizable(text, options = {})
114 106 return "" if text.blank?
107
108 # when using an image link, try to use an attachment, if possible
109 attachments = options[:attachments]
110 if attachments
111 text = text.gsub(/!([<>=]*)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
112 align = $1
113 filename = $2
114 rf = Regexp.new(filename, Regexp::IGNORECASE)
115 # search for the picture in attachments
116 if found = attachments.detect { |att| att.filename =~ rf }
117 image_url = url_for :controller => 'attachments', :action => 'download', :id => found.id
118 "!#{align}#{image_url}!"
119 else
120 "!#{align}#{filename}!"
121 end
122 end
123 end
115 124
125 text = (Setting.text_formatting == 'textile') ?
126 Redmine::WikiFormatting.to_html(text) : simple_format(auto_link(h(text)))
127
116 128 # different methods for formatting wiki links
117 129 case options[:wiki_links]
118 130 when :local
119 131 # used for local links to html files
120 132 format_wiki_link = Proc.new {|project, title| "#{title}.html" }
121 133 when :anchor
122 134 # used for single-file wiki export
123 135 format_wiki_link = Proc.new {|project, title| "##{title}" }
124 136 else
125 137 format_wiki_link = Proc.new {|project, title| url_for :controller => 'wiki', :action => 'index', :id => project, :page => title }
126 138 end
127 139
128 140 project = options[:project] || @project
129 141
130 142 # turn wiki links into html links
131 143 # example:
132 144 # [[mypage]]
133 145 # [[mypage|mytext]]
134 146 # wiki links can refer other project wikis, using project name or identifier:
135 147 # [[project:]] -> wiki starting page
136 148 # [[project:|mytext]]
137 149 # [[project:mypage]]
138 150 # [[project:mypage|mytext]]
139 151 text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) do |m|
140 152 link_project = project
141 153 page = $1
142 154 title = $3
143 155 if page =~ /^([^\:]+)\:(.*)$/
144 156 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
145 157 page = title || $2
146 158 title = $1 if page.blank?
147 159 end
148 160 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)), :class => 'wiki-page')
149 161 end
150 162
151 # turn issue ids into links
163 # turn issue and revision ids into links
152 164 # example:
153 165 # #52 -> <a href="/issues/show/52">#52</a>
154 text = text.gsub(/#(\d+)(?=\b)/) {|m| link_to "##{$1}", {:controller => 'issues', :action => 'show', :id => $1}, :class => 'issue' }
155
156 # turn revision ids into links (@project needed)
157 # example:
158 # r52 -> <a href="/repositories/revision/6?rev=52">r52</a> (@project.id is 6)
159 text = text.gsub(/(?=\b)r(\d+)(?=\b)/) {|m| link_to "r#{$1}", {:controller => 'repositories', :action => 'revision', :id => project.id, :rev => $1}, :class => 'changeset' } if project
160
161 # when using an image link, try to use an attachment, if possible
162 attachments = options[:attachments]
163 if attachments
164 text = text.gsub(/!([<>=]*)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
165 align = $1
166 filename = $2
167 rf = Regexp.new(filename, Regexp::IGNORECASE)
168 # search for the picture in attachments
169 if found = attachments.detect { |att| att.filename =~ rf }
170 image_url = url_for :controller => 'attachments', :action => 'download', :id => found.id
171 "!#{align}#{image_url}!"
172 else
173 "!#{align}#{filename}!"
174 end
166 # r52 -> <a href="/repositories/revision/6?rev=52">r52</a> (project.id is 6)
167 text = text.gsub(%r{([\s,-^])(#|r)(\d+)(?=[[:punct:]]|\s|<|$)}) do |m|
168 leading, otype, oid = $1, $2, $3
169 link = nil
170 if otype == 'r'
171 link = link_to("r#{oid}", {:controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid}, :class => 'changeset') if project
172 else
173 link = link_to("##{oid}", {:controller => 'issues', :action => 'show', :id => oid}, :class => 'issue')
175 174 end
175 leading + (link || "#{otype}#{oid}")
176 176 end
177
178 # finally textilize text
179 @do_textilize ||= (Setting.text_formatting == 'textile') && (ActionView::Helpers::TextHelper.method_defined? "textilize")
180 text = @do_textilize ? auto_link(RedCloth.new(text, [:hard_breaks]).to_html) : simple_format(auto_link(h(text)))
177
178 text
181 179 end
182 180
183 181 # Same as Rails' simple_format helper without using paragraphs
184 182 def simple_format_without_paragraph(text)
185 183 text.to_s.
186 184 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
187 185 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
188 186 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
189 187 end
190 188
191 189 def error_messages_for(object_name, options = {})
192 190 options = options.symbolize_keys
193 191 object = instance_variable_get("@#{object_name}")
194 192 if object && !object.errors.empty?
195 193 # build full_messages here with controller current language
196 194 full_messages = []
197 195 object.errors.each do |attr, msg|
198 196 next if msg.nil?
199 197 msg = msg.first if msg.is_a? Array
200 198 if attr == "base"
201 199 full_messages << l(msg)
202 200 else
203 201 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
204 202 end
205 203 end
206 204 # retrieve custom values error messages
207 205 if object.errors[:custom_values]
208 206 object.custom_values.each do |v|
209 207 v.errors.each do |attr, msg|
210 208 next if msg.nil?
211 209 msg = msg.first if msg.is_a? Array
212 210 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
213 211 end
214 212 end
215 213 end
216 214 content_tag("div",
217 215 content_tag(
218 216 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
219 217 ) +
220 218 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
221 219 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
222 220 )
223 221 else
224 222 ""
225 223 end
226 224 end
227 225
228 226 def lang_options_for_select(blank=true)
229 227 (blank ? [["(auto)", ""]] : []) +
230 228 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.first <=> y.first }
231 229 end
232 230
233 231 def label_tag_for(name, option_tags = nil, options = {})
234 232 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
235 233 content_tag("label", label_text)
236 234 end
237 235
238 236 def labelled_tabular_form_for(name, object, options, &proc)
239 237 options[:html] ||= {}
240 238 options[:html].store :class, "tabular"
241 239 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
242 240 end
243 241
244 242 def check_all_links(form_name)
245 243 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
246 244 " | " +
247 245 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
248 246 end
249 247
250 248 def calendar_for(field_id)
251 249 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
252 250 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
253 251 end
254 252
255 253 def wikitoolbar_for(field_id)
256 254 return '' unless Setting.text_formatting == 'textile'
257 255 javascript_include_tag('jstoolbar') + javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.draw();")
258 256 end
259 257 end
260 258
261 259 class TabularFormBuilder < ActionView::Helpers::FormBuilder
262 260 include GLoc
263 261
264 262 def initialize(object_name, object, template, options, proc)
265 263 set_language_if_valid options.delete(:lang)
266 264 @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
267 265 end
268 266
269 267 (field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector|
270 268 src = <<-END_SRC
271 269 def #{selector}(field, options = {})
272 270 return super if options.delete :no_label
273 271 label_text = l(options[:label]) if options[:label]
274 272 label_text ||= l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym)
275 273 label_text << @template.content_tag("span", " *", :class => "required") if options.delete(:required)
276 274 label = @template.content_tag("label", label_text,
277 275 :class => (@object && @object.errors[field] ? "error" : nil),
278 276 :for => (@object_name.to_s + "_" + field.to_s))
279 277 label + super
280 278 end
281 279 END_SRC
282 280 class_eval src, __FILE__, __LINE__
283 281 end
284 282
285 283 def select(field, choices, options = {}, html_options = {})
286 284 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
287 285 label = @template.content_tag("label", label_text,
288 286 :class => (@object && @object.errors[field] ? "error" : nil),
289 287 :for => (@object_name.to_s + "_" + field.to_s))
290 288 label + super
291 289 end
292 290
293 291 end
294 292
@@ -1,695 +1,695
1 1 /* andreas08 - an open source xhtml/css website layout by Andreas Viklund - http://andreasviklund.com . Free to use in any way and for any purpose as long as the proper credits are given to the original designer. Version: 1.0, November 28, 2005 */
2 2 /* Edited by Jean-Philippe Lang *>
3 3 /**************** Body and tag styles ****************/
4 4
5 5 #header * {margin:0; padding:0;}
6 6 p, ul, ol, li {margin:0; padding:0;}
7 7
8 8 body{
9 9 font:76% Verdana,Tahoma,Arial,sans-serif;
10 10 line-height:1.4em;
11 11 text-align:center;
12 12 color:#303030;
13 13 background:#e8eaec;
14 14 margin:0;
15 15 }
16 16
17 17 a{color:#467aa7;font-weight:bold;text-decoration:none;background-color:inherit;}
18 18 a:hover{color:#2a5a8a; text-decoration:none; background-color:inherit;}
19 19 a img{border:none;}
20 20
21 21 p{margin:0 0 1em 0;}
22 22 p form{margin-top:0; margin-bottom:20px;}
23 23
24 24 img.left,img.center,img.right{padding:4px; border:1px solid #a0a0a0;}
25 25 img.left{float:left; margin:0 12px 5px 0;}
26 26 img.center{display:block; margin:0 auto 5px auto;}
27 27 img.right{float:right; margin:0 0 5px 12px;}
28 28
29 29 /**************** Header and navigation styles ****************/
30 30
31 31 #container{
32 32 width:100%;
33 33 min-width: 800px;
34 34 margin:0;
35 35 padding:0;
36 36 text-align:left;
37 37 background:#ffffff;
38 38 color:#303030;
39 39 }
40 40
41 41 #header{
42 42 height:4.5em;
43 43 margin:0;
44 44 background:#467aa7;
45 45 color:#ffffff;
46 46 margin-bottom:1px;
47 47 }
48 48
49 49 #header h1{
50 50 padding:10px 0 0 20px;
51 51 font-size:2em;
52 52 background-color:inherit;
53 53 color:#fff;
54 54 letter-spacing:-1px;
55 55 font-weight:bold;
56 56 font-family: Trebuchet MS,Georgia,"Times New Roman",serif;
57 57 }
58 58
59 59 #header h2{
60 60 margin:3px 0 0 40px;
61 61 font-size:1.5em;
62 62 background-color:inherit;
63 63 color:#f0f2f4;
64 64 letter-spacing:-1px;
65 65 font-weight:normal;
66 66 font-family: Trebuchet MS,Georgia,"Times New Roman",serif;
67 67 }
68 68
69 69 #header a {color:#fff;}
70 70
71 71 #navigation{
72 72 height:2.2em;
73 73 line-height:2.2em;
74 74 margin:0;
75 75 background:#578bb8;
76 76 color:#ffffff;
77 77 }
78 78
79 79 #navigation li{
80 80 float:left;
81 81 list-style-type:none;
82 82 border-right:1px solid #ffffff;
83 83 white-space:nowrap;
84 84 }
85 85
86 86 #navigation li.right {
87 87 float:right;
88 88 list-style-type:none;
89 89 border-right:0;
90 90 border-left:1px solid #ffffff;
91 91 white-space:nowrap;
92 92 }
93 93
94 94 #navigation li a{
95 95 display:block;
96 96 padding:0px 10px 0px 22px;
97 97 font-size:0.8em;
98 98 font-weight:normal;
99 99 text-decoration:none;
100 100 background-color:inherit;
101 101 color: #ffffff;
102 102 }
103 103
104 104 #navigation li.submenu {background:url(../images/arrow_down.png) 96% 80% no-repeat;}
105 105 #navigation li.submenu a {padding:0px 16px 0px 22px;}
106 106 * html #navigation a {width:1%;}
107 107
108 108 #navigation .selected,#navigation a:hover{
109 109 color:#ffffff;
110 110 text-decoration:none;
111 111 background-color: #80b0da;
112 112 }
113 113
114 114 /**************** Icons *******************/
115 115 .icon {
116 116 background-position: 0% 40%;
117 117 background-repeat: no-repeat;
118 118 padding-left: 20px;
119 119 padding-top: 2px;
120 120 padding-bottom: 3px;
121 121 vertical-align: middle;
122 122 }
123 123
124 124 #navigation .icon {
125 125 background-position: 4px 50%;
126 126 }
127 127
128 128 .icon22 {
129 129 background-position: 0% 40%;
130 130 background-repeat: no-repeat;
131 131 padding-left: 26px;
132 132 line-height: 22px;
133 133 vertical-align: middle;
134 134 }
135 135
136 136 .icon-add { background-image: url(../images/add.png); }
137 137 .icon-edit { background-image: url(../images/edit.png); }
138 138 .icon-del { background-image: url(../images/delete.png); }
139 139 .icon-move { background-image: url(../images/move.png); }
140 140 .icon-save { background-image: url(../images/save.png); }
141 141 .icon-cancel { background-image: url(../images/cancel.png); }
142 142 .icon-pdf { background-image: url(../images/pdf.png); }
143 143 .icon-csv { background-image: url(../images/csv.png); }
144 144 .icon-html { background-image: url(../images/html.png); }
145 145 .icon-image { background-image: url(../images/image.png); }
146 146 .icon-txt { background-image: url(../images/txt.png); }
147 147 .icon-file { background-image: url(../images/file.png); }
148 148 .icon-folder { background-image: url(../images/folder.png); }
149 149 .icon-package { background-image: url(../images/package.png); }
150 150 .icon-home { background-image: url(../images/home.png); }
151 151 .icon-user { background-image: url(../images/user.png); }
152 152 .icon-mypage { background-image: url(../images/user_page.png); }
153 153 .icon-admin { background-image: url(../images/admin.png); }
154 154 .icon-projects { background-image: url(../images/projects.png); }
155 155 .icon-logout { background-image: url(../images/logout.png); }
156 156 .icon-help { background-image: url(../images/help.png); }
157 157 .icon-attachment { background-image: url(../images/attachment.png); }
158 158 .icon-index { background-image: url(../images/index.png); }
159 159 .icon-history { background-image: url(../images/history.png); }
160 160 .icon-feed { background-image: url(../images/feed.png); }
161 161 .icon-time { background-image: url(../images/time.png); }
162 162 .icon-stats { background-image: url(../images/stats.png); }
163 163 .icon-warning { background-image: url(../images/warning.png); }
164 164 .icon-fav { background-image: url(../images/fav.png); }
165 165 .icon-fav-off { background-image: url(../images/fav_off.png); }
166 166 .icon-reload { background-image: url(../images/reload.png); }
167 167 .icon-lock { background-image: url(../images/locked.png); }
168 168 .icon-unlock { background-image: url(../images/unlock.png); }
169 169
170 170 .icon22-projects { background-image: url(../images/22x22/projects.png); }
171 171 .icon22-users { background-image: url(../images/22x22/users.png); }
172 172 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
173 173 .icon22-role { background-image: url(../images/22x22/role.png); }
174 174 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
175 175 .icon22-options { background-image: url(../images/22x22/options.png); }
176 176 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
177 177 .icon22-authent { background-image: url(../images/22x22/authent.png); }
178 178 .icon22-info { background-image: url(../images/22x22/info.png); }
179 179 .icon22-comment { background-image: url(../images/22x22/comment.png); }
180 180 .icon22-package { background-image: url(../images/22x22/package.png); }
181 181 .icon22-settings { background-image: url(../images/22x22/settings.png); }
182 182
183 183 /**************** Content styles ****************/
184 184
185 185 html>body #content {
186 186 height: auto;
187 187 min-height: 500px;
188 188 }
189 189
190 190 #content{
191 191 width: auto;
192 192 height:500px;
193 193 font-size:0.9em;
194 194 padding:20px 10px 10px 20px;
195 195 margin-left: 120px;
196 196 border-left: 1px dashed #c0c0c0;
197 197
198 198 }
199 199
200 200 #content h2, #content div.wiki h1 {
201 201 display:block;
202 202 margin:0 0 16px 0;
203 203 font-size:1.7em;
204 204 font-weight:normal;
205 205 letter-spacing:-1px;
206 206 color:#606060;
207 207 background-color:inherit;
208 208 font-family: Trebuchet MS,Georgia,"Times New Roman",serif;
209 209 }
210 210
211 211 #content h2 a{font-weight:normal;}
212 212 #content h3{margin:0 0 12px 0; font-size:1.4em;color:#707070;font-family: Trebuchet MS,Georgia,"Times New Roman",serif;}
213 213 #content h4{font-size: 1em; margin-bottom: 12px; margin-top: 20px; font-weight: normal; border-bottom: dotted 1px #c0c0c0;}
214 214 #content a:hover,#subcontent a:hover{text-decoration:underline;}
215 215 #content ul,#content ol{margin:0 5px 16px 35px;}
216 216 #content dl{margin:0 5px 10px 25px;}
217 217 #content dt{font-weight:bold; margin-bottom:5px;}
218 218 #content dd{margin:0 0 10px 15px;}
219 219
220 220 #content .tabs{height: 2.6em;}
221 221 #content .tabs ul{margin:0;}
222 222 #content .tabs ul li{
223 223 float:left;
224 224 list-style-type:none;
225 225 white-space:nowrap;
226 226 margin-right:8px;
227 227 background:#fff;
228 228 }
229 229 #content .tabs ul li a{
230 230 display:block;
231 231 font-size: 0.9em;
232 232 text-decoration:none;
233 233 line-height:1em;
234 234 padding:4px;
235 235 border: 1px solid #c0c0c0;
236 236 }
237 237
238 238 #content .tabs ul li a.selected, #content .tabs ul li a:hover{
239 239 background-color: #80b0da;
240 240 border: 1px solid #80b0da;
241 241 color: #fff;
242 242 text-decoration:none;
243 243 }
244 244
245 245 /***********************************************/
246 246
247 247 form {display: inline;}
248 248 blockquote {padding-left: 6px; border-left: 2px solid #ccc;}
249 249 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
250 250
251 251 input.button-small {font-size: 0.8em;}
252 252 textarea.wiki-edit { width: 99.5%; }
253 253 .select-small {font-size: 0.8em;}
254 254 label {font-weight: bold; font-size: 1em; color: #505050;}
255 255 fieldset {border:1px solid #c0c0c0; padding: 6px;}
256 256 legend {color: #505050;}
257 257 .required {color: #bb0000;}
258 258 .odd {background-color:#f6f7f8;}
259 259 .even {background-color: #fff;}
260 260 hr { border:0; border-top: dotted 1px #fff; border-bottom: dotted 1px #c0c0c0; }
261 261 table p {margin:0; padding:0;}
262 262
263 263 .highlight { background-color: #FCFD8D;}
264 264
265 265 div.square {
266 266 border: 1px solid #999;
267 267 float: left;
268 268 margin: .4em .5em 0 0;
269 269 overflow: hidden;
270 270 width: .6em; height: .6em;
271 271 }
272 272
273 273 ul.documents {
274 274 list-style-type: none;
275 275 padding: 0;
276 276 margin: 0;
277 277 }
278 278
279 279 ul.documents li {
280 280 background-image: url(../images/32x32/file.png);
281 281 background-repeat: no-repeat;
282 282 background-position: 0 1px;
283 283 padding-left: 36px;
284 284 margin-bottom: 10px;
285 285 margin-left: -37px;
286 286 }
287 287
288 288 /********** Table used to display lists of things ***********/
289 289
290 290 table.list {
291 291 width:100%;
292 292 border-collapse: collapse;
293 293 border: 1px dotted #d0d0d0;
294 294 margin-bottom: 6px;
295 295 }
296 296
297 297 table.with-cells td {
298 298 border: 1px solid #d7d7d7;
299 299 }
300 300
301 301 table.list td {
302 302 padding:2px;
303 303 }
304 304
305 305 table.list thead th {
306 306 text-align: center;
307 307 background: #eee;
308 308 border: 1px solid #d7d7d7;
309 309 color: #777;
310 310 }
311 311
312 312 table.list tbody th {
313 313 font-weight: bold;
314 314 background: #eed;
315 315 border: 1px solid #d7d7d7;
316 316 color: #777;
317 317 }
318 318
319 319 /*========== Drop down menu ==============*/
320 320 div.menu {
321 321 background-color: #FFFFFF;
322 322 border-style: solid;
323 323 border-width: 1px;
324 324 border-color: #7F9DB9;
325 325 position: absolute;
326 326 top: 0px;
327 327 left: 0px;
328 328 padding: 0;
329 329 visibility: hidden;
330 330 z-index: 101;
331 331 }
332 332
333 333 div.menu a.menuItem {
334 334 font-size: 10px;
335 335 font-weight: normal;
336 336 line-height: 2em;
337 337 color: #000000;
338 338 background-color: #FFFFFF;
339 339 cursor: default;
340 340 display: block;
341 341 padding: 0 1em;
342 342 margin: 0;
343 343 border: 0;
344 344 text-decoration: none;
345 345 white-space: nowrap;
346 346 }
347 347
348 348 div.menu a.menuItem:hover, div.menu a.menuItemHighlight {
349 349 background-color: #80b0da;
350 350 color: #ffffff;
351 351 }
352 352
353 353 div.menu a.menuItem span.menuItemText {}
354 354
355 355 div.menu a.menuItem span.menuItemArrow {
356 356 margin-right: -.75em;
357 357 }
358 358
359 359 /**************** Sidebar styles ****************/
360 360
361 361 #subcontent{
362 362 position: absolute;
363 363 left: 0px;
364 364 width:95px;
365 365 padding:20px 20px 10px 5px;
366 366 overflow: hidden;
367 367 }
368 368
369 369 #subcontent h2{
370 370 display:block;
371 371 margin:0 0 5px 0;
372 372 font-size:1.0em;
373 373 font-weight:bold;
374 374 text-align:left;
375 375 color:#606060;
376 376 background-color:inherit;
377 377 font-family: Trebuchet MS,Georgia,"Times New Roman",serif;
378 378 }
379 379
380 380 #subcontent p{margin:0 0 16px 0; font-size:0.9em;}
381 381
382 382 /**************** Menublock styles ****************/
383 383
384 384 .menublock{margin:0 0 20px 8px; font-size:0.8em;}
385 385 .menublock li{list-style:none; display:block; padding:1px; margin-bottom:0px;}
386 386 .menublock li a{font-weight:bold; text-decoration:none;}
387 387 .menublock li a:hover{text-decoration:none;}
388 388 .menublock li ul{margin:0; font-size:1em; font-weight:normal;}
389 389 .menublock li ul li{margin-bottom:0;}
390 390 .menublock li ul a{font-weight:normal;}
391 391
392 392 /**************** Footer styles ****************/
393 393
394 394 #footer{
395 395 clear:both;
396 396 padding:5px 0;
397 397 margin:0;
398 398 font-size:0.9em;
399 399 color:#f0f0f0;
400 400 background:#467aa7;
401 401 }
402 402
403 403 #footer p{padding:0; margin:0; text-align:center;}
404 404 #footer a{color:#f0f0f0; background-color:inherit; font-weight:bold;}
405 405 #footer a:hover{color:#ffffff; background-color:inherit; text-decoration: underline;}
406 406
407 407 /**************** Misc classes and styles ****************/
408 408
409 409 .splitcontentleft{float:left; width:49%;}
410 410 .splitcontentright{float:right; width:49%;}
411 411 .clear{clear:both;}
412 412 .small{font-size:0.8em;line-height:1.4em;padding:0 0 0 0;}
413 413 .hide{display:none;}
414 414 .textcenter{text-align:center;}
415 415 .textright{text-align:right;}
416 416 .important{color:#f02025; background-color:inherit; font-weight:bold;}
417 417
418 418 .box{
419 419 margin:0 0 20px 0;
420 420 padding:10px;
421 421 border:1px solid #c0c0c0;
422 422 background-color:#fafbfc;
423 423 color:#505050;
424 424 line-height:1.5em;
425 425 }
426 426
427 427 a.close-icon {
428 428 display:block;
429 429 margin-top:3px;
430 430 overflow:hidden;
431 431 width:12px;
432 432 height:12px;
433 433 background-repeat: no-repeat;
434 434 cursor:pointer;
435 435 background-image:url('../images/close.png');
436 436 }
437 437
438 438 a.close-icon:hover {
439 439 background-image:url('../images/close_hl.png');
440 440 }
441 441
442 442 .rightbox{
443 443 background: #fafbfc;
444 444 border: 1px solid #c0c0c0;
445 445 float: right;
446 446 padding: 8px;
447 447 position: relative;
448 448 margin: 0 5px 5px;
449 449 }
450 450
451 451 div.attachments {padding-left: 6px; border-left: 2px solid #ccc; margin-bottom: 8px;}
452 452 div.attachments p {margin-bottom:2px;}
453 453
454 454 .overlay{
455 455 position: absolute;
456 456 margin-left:0;
457 457 z-index: 50;
458 458 }
459 459
460 460 .layout-active {
461 461 background: #ECF3E1;
462 462 }
463 463
464 464 .block-receiver {
465 465 border:1px dashed #c0c0c0;
466 466 margin-bottom: 20px;
467 467 padding: 15px 0 15px 0;
468 468 }
469 469
470 470 .mypage-box {
471 471 margin:0 0 20px 0;
472 472 color:#505050;
473 473 line-height:1.5em;
474 474 }
475 475
476 476 .handle {
477 477 cursor: move;
478 478 }
479 479
480 480 .login {
481 481 width: 50%;
482 482 text-align: left;
483 483 }
484 484
485 485 img.calendar-trigger {
486 486 cursor: pointer;
487 487 vertical-align: middle;
488 488 margin-left: 4px;
489 489 }
490 490
491 491 #history p {
492 492 margin-left: 34px;
493 493 }
494 494
495 495 .progress {
496 496 border: 1px solid #D7D7D7;
497 497 border-collapse: collapse;
498 498 border-spacing: 0pt;
499 499 empty-cells: show;
500 500 padding: 3px;
501 501 width: 40em;
502 502 text-align: center;
503 503 }
504 504
505 505 .progress td { height: 1em; }
506 506 .progress .closed { background: #BAE0BA none repeat scroll 0%; }
507 507 .progress .open { background: #FFF none repeat scroll 0%; }
508 508
509 509 /***** Contextual links div *****/
510 510 .contextual {
511 511 float: right;
512 512 font-size: 0.8em;
513 513 line-height: 16px;
514 514 padding: 2px;
515 515 }
516 516
517 517 .contextual select, .contextual input {
518 518 font-size: 1em;
519 519 }
520 520
521 521 /***** Gantt chart *****/
522 522 .gantt_hdr {
523 523 position:absolute;
524 524 top:0;
525 525 height:16px;
526 526 border-top: 1px solid #c0c0c0;
527 527 border-bottom: 1px solid #c0c0c0;
528 528 border-right: 1px solid #c0c0c0;
529 529 text-align: center;
530 530 overflow: hidden;
531 531 }
532 532
533 533 .task {
534 534 position: absolute;
535 535 height:8px;
536 536 font-size:0.8em;
537 537 color:#888;
538 538 padding:0;
539 539 margin:0;
540 540 line-height:0.8em;
541 541 }
542 542
543 543 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
544 544 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
545 545 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
546 546 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
547 547
548 548 /***** Tooltips ******/
549 549 .tooltip{position:relative;z-index:24;}
550 550 .tooltip:hover{z-index:25;color:#000;}
551 551 .tooltip span.tip{display: none; text-align:left;}
552 552
553 553 div.tooltip:hover span.tip{
554 554 display:block;
555 555 position:absolute;
556 556 top:12px; left:24px; width:270px;
557 557 border:1px solid #555;
558 558 background-color:#fff;
559 559 padding: 4px;
560 560 font-size: 0.8em;
561 561 color:#505050;
562 562 }
563 563
564 564 /***** CSS FORM ******/
565 565 .tabular p{
566 566 margin: 0;
567 567 padding: 5px 0 8px 0;
568 568 padding-left: 180px; /*width of left column containing the label elements*/
569 569 height: 1%;
570 570 clear:both;
571 571 }
572 572
573 573 .tabular label{
574 574 font-weight: bold;
575 575 float: left;
576 576 margin-left: -180px; /*width of left column*/
577 577 margin-bottom: 10px;
578 578 width: 175px; /*width of labels. Should be smaller than left column to create some right
579 579 margin*/
580 580 }
581 581
582 582 .error {
583 583 color: #cc0000;
584 584 }
585 585
586 586 #settings .tabular p{ padding-left: 300px; }
587 587 #settings .tabular label{ margin-left: -300px; width: 295px; }
588 588
589 589 /*.threepxfix class below:
590 590 Targets IE6- ONLY. Adds 3 pixel indent for multi-line form contents.
591 591 to account for 3 pixel bug: http://www.positioniseverything.net/explorer/threepxtest.html
592 592 */
593 593
594 594 * html .threepxfix{
595 595 margin-left: 3px;
596 596 }
597 597
598 598 /***** Wiki sections ****/
599 599 #content div.wiki { font-size: 110%}
600 600
601 601 #content div.wiki h2, div.wiki h3 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; color:#606060; }
602 602 #content div.wiki h2 { font-size: 1.4em;}
603 603 #content div.wiki h3 { font-size: 1.2em;}
604 604
605 605 div.wiki table {
606 606 border: 1px solid #505050;
607 607 border-collapse: collapse;
608 608 }
609 609
610 610 div.wiki table, div.wiki td, div.wiki th {
611 611 border: 1px solid #bbb;
612 612 padding: 4px;
613 613 }
614 614
615 615 div.wiki a {
616 616 background-position: 0% 60%;
617 617 background-repeat: no-repeat;
618 padding-left: 12px;
618 padding-left: 14px;
619 619 background-image: url(../images/external.png);
620 620 }
621 621
622 div.wiki a.wiki-page, div.wiki a.issue, div.wiki a.changeset {
622 div.wiki a.wiki-page, div.wiki a.issue, div.wiki a.changeset, div.wiki a.email {
623 623 padding-left: 0;
624 624 background-image: none;
625 625 }
626 626
627 627 div.wiki code {
628 628 font-size: 1.2em;
629 629 }
630 630
631 631 div.wiki img {
632 632 margin: 6px;
633 633 }
634 634
635 635 .diff_out{
636 636 background: #fcc;
637 637 }
638 638
639 639 .diff_in{
640 640 background: #cfc;
641 641 }
642 642
643 643 #preview .preview { background: #fafbfc url(../images/draft.png); }
644 644
645 645 #ajax-indicator {
646 646 position: absolute; /* fixed not supported by IE */
647 647 background-color:#eee;
648 648 border: 1px solid #bbb;
649 649 top:35%;
650 650 left:40%;
651 651 width:20%;
652 652 font-weight:bold;
653 653 text-align:center;
654 654 padding:0.6em;
655 655 z-index:100;
656 656 filter:alpha(opacity=50);
657 657 -moz-opacity:0.5;
658 658 opacity: 0.5;
659 659 -khtml-opacity: 0.5;
660 660 }
661 661
662 662 html>body #ajax-indicator { position: fixed; }
663 663
664 664 #ajax-indicator span {
665 665 background-position: 0% 40%;
666 666 background-repeat: no-repeat;
667 667 background-image: url(../images/loading.gif);
668 668 padding-left: 26px;
669 669 vertical-align: bottom;
670 670 }
671 671
672 672 /***** Flash & error messages ****/
673 673 #flash div, #errorExplanation {
674 674 padding: 4px 4px 4px 30px;
675 675 margin-bottom: 16px;
676 676 font-size: 1.1em;
677 677 border: 2px solid;
678 678 }
679 679
680 680 #flash div.error, #errorExplanation {
681 681 background: url(../images/false.png) 8px 5px no-repeat;
682 682 background-color: #ffe3e3;
683 683 border-color: #dd0000;
684 684 color: #550000;
685 685 }
686 686
687 687 #flash div.notice {
688 688 background: url(../images/true.png) 8px 5px no-repeat;
689 689 background-color: #dfffdf;
690 690 border-color: #9fcf9f;
691 691 color: #005f00;
692 692 }
693 693
694 694 #errorExplanation ul { margin-bottom: 0px; }
695 695 #errorExplanation ul li { list-style: none; margin-left: -16px;}
@@ -1,72 +1,73
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 ENV["RAILS_ENV"] ||= "test"
19 19 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
20 20 require 'test_help'
21 require File.expand_path(File.dirname(__FILE__) + '/helper_testcase')
21 22
22 23 class Test::Unit::TestCase
23 24 # Transactional fixtures accelerate your tests by wrapping each test method
24 25 # in a transaction that's rolled back on completion. This ensures that the
25 26 # test database remains unchanged so your fixtures don't have to be reloaded
26 27 # between every test method. Fewer database queries means faster tests.
27 28 #
28 29 # Read Mike Clark's excellent walkthrough at
29 30 # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
30 31 #
31 32 # Every Active Record database supports transactions except MyISAM tables
32 33 # in MySQL. Turn off transactional fixtures in this case; however, if you
33 34 # don't care one way or the other, switching from MyISAM to InnoDB tables
34 35 # is recommended.
35 36 self.use_transactional_fixtures = true
36 37
37 38 # Instantiated fixtures are slow, but give you @david where otherwise you
38 39 # would need people(:david). If you don't want to migrate your existing
39 40 # test cases which use the @david style and don't mind the speed hit (each
40 41 # instantiated fixtures translates to a database query per test method),
41 42 # then set this back to true.
42 43 self.use_instantiated_fixtures = false
43 44
44 45 # Add more helper methods to be used by all tests here...
45 46
46 47 def log_user(login, password)
47 48 get "/account/login"
48 49 assert_equal nil, session[:user_id]
49 50 assert_response :success
50 51 assert_template "account/login"
51 52 post "/account/login", :login => login, :password => password
52 53 assert_redirected_to "my/page"
53 54 assert_equal login, User.find(session[:user_id]).login
54 55 end
55 56 end
56 57
57 58
58 59 # ActionController::TestUploadedFile bug
59 60 # see http://dev.rubyonrails.org/ticket/4635
60 61 class String
61 62 def original_filename
62 63 "testfile.txt"
63 64 end
64 65
65 66 def content_type
66 67 "text/plain"
67 68 end
68 69
69 70 def read
70 71 self.to_s
71 72 end
72 73 end No newline at end of file
@@ -1,39 +1,39
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class SettingTest < Test::Unit::TestCase
21 21
22 22 def test_read_default
23 assert_equal "redMine", Setting.app_title
23 assert_equal "Redmine", Setting.app_title
24 24 assert Setting.self_registration?
25 25 assert !Setting.login_required?
26 26 end
27 27
28 28 def test_update
29 29 Setting.app_title = "My title"
30 30 assert_equal "My title", Setting.app_title
31 31 # make sure db has been updated (INSERT)
32 32 assert_equal "My title", Setting.find_by_name('app_title').value
33 33
34 34 Setting.app_title = "My other title"
35 35 assert_equal "My other title", Setting.app_title
36 36 # make sure db has been updated (UPDATE)
37 37 assert_equal "My other title", Setting.find_by_name('app_title').value
38 38 end
39 39 end
General Comments 0
You need to be logged in to leave comments. Login now