Chris@0
|
1 # Redmine - project management software
|
Chris@1494
|
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
|
Chris@0
|
3 #
|
Chris@0
|
4 # This program is free software; you can redistribute it and/or
|
Chris@0
|
5 # modify it under the terms of the GNU General Public License
|
Chris@0
|
6 # as published by the Free Software Foundation; either version 2
|
Chris@0
|
7 # of the License, or (at your option) any later version.
|
Chris@909
|
8 #
|
Chris@0
|
9 # This program is distributed in the hope that it will be useful,
|
Chris@0
|
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
Chris@0
|
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
Chris@0
|
12 # GNU General Public License for more details.
|
Chris@909
|
13 #
|
Chris@0
|
14 # You should have received a copy of the GNU General Public License
|
Chris@0
|
15 # along with this program; if not, write to the Free Software
|
Chris@0
|
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
Chris@0
|
17
|
Chris@1115
|
18 require 'digest/md5'
|
Chris@1115
|
19
|
Chris@0
|
20 module Redmine
|
Chris@0
|
21 module WikiFormatting
|
Chris@909
|
22 class StaleSectionError < Exception; end
|
Chris@909
|
23
|
Chris@0
|
24 @@formatters = {}
|
Chris@0
|
25
|
Chris@0
|
26 class << self
|
Chris@0
|
27 def map
|
Chris@0
|
28 yield self
|
Chris@0
|
29 end
|
Chris@909
|
30
|
Chris@0
|
31 def register(name, formatter, helper)
|
Chris@0
|
32 raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name.to_s]
|
Chris@0
|
33 @@formatters[name.to_s] = {:formatter => formatter, :helper => helper}
|
Chris@0
|
34 end
|
Chris@909
|
35
|
Chris@909
|
36 def formatter
|
Chris@909
|
37 formatter_for(Setting.text_formatting)
|
Chris@909
|
38 end
|
Chris@909
|
39
|
Chris@0
|
40 def formatter_for(name)
|
Chris@0
|
41 entry = @@formatters[name.to_s]
|
Chris@0
|
42 (entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter
|
Chris@0
|
43 end
|
Chris@909
|
44
|
Chris@0
|
45 def helper_for(name)
|
Chris@0
|
46 entry = @@formatters[name.to_s]
|
Chris@0
|
47 (entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper
|
Chris@0
|
48 end
|
Chris@909
|
49
|
Chris@0
|
50 def format_names
|
Chris@0
|
51 @@formatters.keys.map
|
Chris@0
|
52 end
|
Chris@909
|
53
|
Chris@909
|
54 def to_html(format, text, options = {})
|
Chris@1115
|
55 text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, text, options[:object], options[:attribute])
|
Chris@0
|
56 # Text retrieved from the cache store may be frozen
|
Chris@0
|
57 # We need to dup it so we can do in-place substitutions with gsub!
|
Chris@0
|
58 cache_store.fetch cache_key do
|
Chris@0
|
59 formatter_for(format).new(text).to_html
|
Chris@0
|
60 end.dup
|
Chris@0
|
61 else
|
Chris@0
|
62 formatter_for(format).new(text).to_html
|
Chris@0
|
63 end
|
Chris@0
|
64 text
|
Chris@0
|
65 end
|
Chris@0
|
66
|
Chris@909
|
67 # Returns true if the text formatter supports single section edit
|
Chris@909
|
68 def supports_section_edit?
|
Chris@909
|
69 (formatter.instance_methods & ['update_section', :update_section]).any?
|
Chris@909
|
70 end
|
Chris@909
|
71
|
Chris@1115
|
72 # Returns a cache key for the given text +format+, +text+, +object+ and +attribute+ or nil if no caching should be done
|
Chris@1115
|
73 def cache_key_for(format, text, object, attribute)
|
Chris@1115
|
74 if object && attribute && !object.new_record? && format.present?
|
Chris@1115
|
75 "formatted_text/#{format}/#{object.class.model_name.cache_key}/#{object.id}-#{attribute}-#{Digest::MD5.hexdigest text}"
|
Chris@0
|
76 end
|
Chris@0
|
77 end
|
Chris@909
|
78
|
Chris@0
|
79 # Returns the cache store used to cache HTML output
|
Chris@0
|
80 def cache_store
|
Chris@0
|
81 ActionController::Base.cache_store
|
Chris@0
|
82 end
|
Chris@0
|
83 end
|
Chris@909
|
84
|
Chris@1115
|
85 module LinksHelper
|
Chris@1115
|
86 AUTO_LINK_RE = %r{
|
Chris@1115
|
87 ( # leading text
|
Chris@1115
|
88 <\w+.*?>| # leading HTML tag, or
|
Chris@1464
|
89 [\s\(\[,;]| # leading punctuation, or
|
Chris@1115
|
90 ^ # beginning of line
|
Chris@1115
|
91 )
|
Chris@1115
|
92 (
|
Chris@1115
|
93 (?:https?://)| # protocol spec, or
|
Chris@1115
|
94 (?:s?ftps?://)|
|
Chris@1115
|
95 (?:www\.) # www.*
|
Chris@1115
|
96 )
|
Chris@1115
|
97 (
|
Chris@1464
|
98 ([^<]\S*?) # url
|
Chris@1115
|
99 (\/)? # slash
|
Chris@1115
|
100 )
|
Chris@1115
|
101 ((?:>)?|[^[:alnum:]_\=\/;\(\)]*?) # post
|
Chris@1115
|
102 (?=<|\s|$)
|
Chris@1115
|
103 }x unless const_defined?(:AUTO_LINK_RE)
|
Chris@1115
|
104
|
Chris@1115
|
105 # Destructively remplaces urls into clickable links
|
Chris@1115
|
106 def auto_link!(text)
|
Chris@1115
|
107 text.gsub!(AUTO_LINK_RE) do
|
Chris@1115
|
108 all, leading, proto, url, post = $&, $1, $2, $3, $6
|
Chris@1115
|
109 if leading =~ /<a\s/i || leading =~ /![<>=]?/
|
Chris@1115
|
110 # don't replace URL's that are already linked
|
Chris@1115
|
111 # and URL's prefixed with ! !> !< != (textile images)
|
Chris@1115
|
112 all
|
Chris@1115
|
113 else
|
Chris@1115
|
114 # Idea below : an URL with unbalanced parethesis and
|
Chris@1115
|
115 # ending by ')' is put into external parenthesis
|
Chris@1115
|
116 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
|
Chris@1115
|
117 url=url[0..-2] # discard closing parenth from url
|
Chris@1115
|
118 post = ")"+post # add closing parenth to post
|
Chris@1115
|
119 end
|
Chris@1115
|
120 content = proto + url
|
Chris@1115
|
121 href = "#{proto=="www."?"http://www.":proto}#{url}"
|
Chris@1115
|
122 %(#{leading}<a class="external" href="#{ERB::Util.html_escape href}">#{ERB::Util.html_escape content}</a>#{post}).html_safe
|
Chris@1115
|
123 end
|
Chris@1115
|
124 end
|
Chris@1115
|
125 end
|
Chris@1115
|
126
|
Chris@1115
|
127 # Destructively remplaces email addresses into clickable links
|
Chris@1115
|
128 def auto_mailto!(text)
|
Chris@1115
|
129 text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
|
Chris@1115
|
130 mail = $1
|
Chris@1115
|
131 if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
|
Chris@1115
|
132 mail
|
Chris@1115
|
133 else
|
Chris@1115
|
134 %(<a class="email" href="mailto:#{ERB::Util.html_escape mail}">#{ERB::Util.html_escape mail}</a>).html_safe
|
Chris@1115
|
135 end
|
Chris@1115
|
136 end
|
Chris@1115
|
137 end
|
Chris@1115
|
138 end
|
Chris@1115
|
139
|
Chris@0
|
140 # Default formatter module
|
Chris@0
|
141 module NullFormatter
|
Chris@0
|
142 class Formatter
|
Chris@0
|
143 include ActionView::Helpers::TagHelper
|
Chris@0
|
144 include ActionView::Helpers::TextHelper
|
Chris@0
|
145 include ActionView::Helpers::UrlHelper
|
Chris@1115
|
146 include Redmine::WikiFormatting::LinksHelper
|
Chris@909
|
147
|
Chris@0
|
148 def initialize(text)
|
Chris@0
|
149 @text = text
|
Chris@0
|
150 end
|
Chris@909
|
151
|
Chris@0
|
152 def to_html(*args)
|
Chris@1115
|
153 t = CGI::escapeHTML(@text)
|
Chris@1115
|
154 auto_link!(t)
|
Chris@1115
|
155 auto_mailto!(t)
|
Chris@1115
|
156 simple_format(t, {}, :sanitize => false)
|
Chris@0
|
157 end
|
Chris@0
|
158 end
|
Chris@909
|
159
|
Chris@0
|
160 module Helper
|
Chris@0
|
161 def wikitoolbar_for(field_id)
|
Chris@0
|
162 end
|
Chris@909
|
163
|
Chris@0
|
164 def heads_for_wiki_formatter
|
Chris@0
|
165 end
|
Chris@909
|
166
|
Chris@0
|
167 def initial_page_content(page)
|
Chris@0
|
168 page.pretty_title.to_s
|
Chris@0
|
169 end
|
Chris@0
|
170 end
|
Chris@0
|
171 end
|
Chris@0
|
172 end
|
Chris@0
|
173 end
|