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@1517
|
31 def register(name, formatter, helper, options={})
|
Chris@1517
|
32 name = name.to_s
|
Chris@1517
|
33 raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name]
|
Chris@1517
|
34 @@formatters[name] = {
|
Chris@1517
|
35 :formatter => formatter,
|
Chris@1517
|
36 :helper => helper,
|
Chris@1517
|
37 :label => options[:label] || name.humanize
|
Chris@1517
|
38 }
|
Chris@0
|
39 end
|
Chris@909
|
40
|
Chris@909
|
41 def formatter
|
Chris@909
|
42 formatter_for(Setting.text_formatting)
|
Chris@909
|
43 end
|
Chris@909
|
44
|
Chris@0
|
45 def formatter_for(name)
|
Chris@0
|
46 entry = @@formatters[name.to_s]
|
Chris@0
|
47 (entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter
|
Chris@0
|
48 end
|
Chris@909
|
49
|
Chris@0
|
50 def helper_for(name)
|
Chris@0
|
51 entry = @@formatters[name.to_s]
|
Chris@0
|
52 (entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper
|
Chris@0
|
53 end
|
Chris@909
|
54
|
Chris@0
|
55 def format_names
|
Chris@0
|
56 @@formatters.keys.map
|
Chris@0
|
57 end
|
Chris@909
|
58
|
Chris@1517
|
59 def formats_for_select
|
Chris@1517
|
60 @@formatters.map {|name, options| [options[:label], name]}
|
Chris@1517
|
61 end
|
Chris@1517
|
62
|
Chris@909
|
63 def to_html(format, text, options = {})
|
Chris@1115
|
64 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
|
65 # Text retrieved from the cache store may be frozen
|
Chris@0
|
66 # We need to dup it so we can do in-place substitutions with gsub!
|
Chris@0
|
67 cache_store.fetch cache_key do
|
Chris@0
|
68 formatter_for(format).new(text).to_html
|
Chris@0
|
69 end.dup
|
Chris@0
|
70 else
|
Chris@0
|
71 formatter_for(format).new(text).to_html
|
Chris@0
|
72 end
|
Chris@0
|
73 text
|
Chris@0
|
74 end
|
Chris@0
|
75
|
Chris@909
|
76 # Returns true if the text formatter supports single section edit
|
Chris@909
|
77 def supports_section_edit?
|
Chris@909
|
78 (formatter.instance_methods & ['update_section', :update_section]).any?
|
Chris@909
|
79 end
|
Chris@909
|
80
|
Chris@1115
|
81 # Returns a cache key for the given text +format+, +text+, +object+ and +attribute+ or nil if no caching should be done
|
Chris@1115
|
82 def cache_key_for(format, text, object, attribute)
|
Chris@1115
|
83 if object && attribute && !object.new_record? && format.present?
|
Chris@1115
|
84 "formatted_text/#{format}/#{object.class.model_name.cache_key}/#{object.id}-#{attribute}-#{Digest::MD5.hexdigest text}"
|
Chris@0
|
85 end
|
Chris@0
|
86 end
|
Chris@909
|
87
|
Chris@0
|
88 # Returns the cache store used to cache HTML output
|
Chris@0
|
89 def cache_store
|
Chris@0
|
90 ActionController::Base.cache_store
|
Chris@0
|
91 end
|
Chris@0
|
92 end
|
Chris@909
|
93
|
Chris@1115
|
94 module LinksHelper
|
Chris@1115
|
95 AUTO_LINK_RE = %r{
|
Chris@1115
|
96 ( # leading text
|
Chris@1115
|
97 <\w+.*?>| # leading HTML tag, or
|
Chris@1464
|
98 [\s\(\[,;]| # leading punctuation, or
|
Chris@1115
|
99 ^ # beginning of line
|
Chris@1115
|
100 )
|
Chris@1115
|
101 (
|
Chris@1115
|
102 (?:https?://)| # protocol spec, or
|
Chris@1115
|
103 (?:s?ftps?://)|
|
Chris@1115
|
104 (?:www\.) # www.*
|
Chris@1115
|
105 )
|
Chris@1115
|
106 (
|
Chris@1464
|
107 ([^<]\S*?) # url
|
Chris@1115
|
108 (\/)? # slash
|
Chris@1115
|
109 )
|
Chris@1115
|
110 ((?:>)?|[^[:alnum:]_\=\/;\(\)]*?) # post
|
Chris@1115
|
111 (?=<|\s|$)
|
Chris@1115
|
112 }x unless const_defined?(:AUTO_LINK_RE)
|
Chris@1115
|
113
|
Chris@1115
|
114 # Destructively remplaces urls into clickable links
|
Chris@1115
|
115 def auto_link!(text)
|
Chris@1115
|
116 text.gsub!(AUTO_LINK_RE) do
|
Chris@1115
|
117 all, leading, proto, url, post = $&, $1, $2, $3, $6
|
Chris@1115
|
118 if leading =~ /<a\s/i || leading =~ /![<>=]?/
|
Chris@1115
|
119 # don't replace URL's that are already linked
|
Chris@1115
|
120 # and URL's prefixed with ! !> !< != (textile images)
|
Chris@1115
|
121 all
|
Chris@1115
|
122 else
|
Chris@1115
|
123 # Idea below : an URL with unbalanced parethesis and
|
Chris@1115
|
124 # ending by ')' is put into external parenthesis
|
Chris@1115
|
125 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
|
Chris@1115
|
126 url=url[0..-2] # discard closing parenth from url
|
Chris@1115
|
127 post = ")"+post # add closing parenth to post
|
Chris@1115
|
128 end
|
Chris@1115
|
129 content = proto + url
|
Chris@1115
|
130 href = "#{proto=="www."?"http://www.":proto}#{url}"
|
Chris@1115
|
131 %(#{leading}<a class="external" href="#{ERB::Util.html_escape href}">#{ERB::Util.html_escape content}</a>#{post}).html_safe
|
Chris@1115
|
132 end
|
Chris@1115
|
133 end
|
Chris@1115
|
134 end
|
Chris@1115
|
135
|
Chris@1115
|
136 # Destructively remplaces email addresses into clickable links
|
Chris@1115
|
137 def auto_mailto!(text)
|
Chris@1115
|
138 text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
|
Chris@1115
|
139 mail = $1
|
Chris@1115
|
140 if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
|
Chris@1115
|
141 mail
|
Chris@1115
|
142 else
|
Chris@1115
|
143 %(<a class="email" href="mailto:#{ERB::Util.html_escape mail}">#{ERB::Util.html_escape mail}</a>).html_safe
|
Chris@1115
|
144 end
|
Chris@1115
|
145 end
|
Chris@1115
|
146 end
|
Chris@1115
|
147 end
|
Chris@1115
|
148
|
Chris@0
|
149 # Default formatter module
|
Chris@0
|
150 module NullFormatter
|
Chris@0
|
151 class Formatter
|
Chris@0
|
152 include ActionView::Helpers::TagHelper
|
Chris@0
|
153 include ActionView::Helpers::TextHelper
|
Chris@0
|
154 include ActionView::Helpers::UrlHelper
|
Chris@1115
|
155 include Redmine::WikiFormatting::LinksHelper
|
Chris@909
|
156
|
Chris@0
|
157 def initialize(text)
|
Chris@0
|
158 @text = text
|
Chris@0
|
159 end
|
Chris@909
|
160
|
Chris@0
|
161 def to_html(*args)
|
Chris@1115
|
162 t = CGI::escapeHTML(@text)
|
Chris@1115
|
163 auto_link!(t)
|
Chris@1115
|
164 auto_mailto!(t)
|
Chris@1115
|
165 simple_format(t, {}, :sanitize => false)
|
Chris@0
|
166 end
|
Chris@0
|
167 end
|
Chris@909
|
168
|
Chris@0
|
169 module Helper
|
Chris@0
|
170 def wikitoolbar_for(field_id)
|
Chris@0
|
171 end
|
Chris@909
|
172
|
Chris@0
|
173 def heads_for_wiki_formatter
|
Chris@0
|
174 end
|
Chris@909
|
175
|
Chris@0
|
176 def initial_page_content(page)
|
Chris@0
|
177 page.pretty_title.to_s
|
Chris@0
|
178 end
|
Chris@0
|
179 end
|
Chris@0
|
180 end
|
Chris@0
|
181 end
|
Chris@0
|
182 end
|