Patch #2025 » make-wiki-formatters-pluggable.patch
app/controllers/wiki_controller.rb | ||
---|---|---|
63 | 63 |
@page.content = WikiContent.new(:page => @page) if @page.new_record? |
64 | 64 |
|
65 | 65 |
@content = @page.content_for_version(params[:version]) |
66 |
@content.text = "h1. #{@page.pretty_title}" if @content.text.blank?
|
|
66 |
@content.text = initial_page_content(@page) if @content.text.blank?
|
|
67 | 67 |
# don't keep previous comment |
68 | 68 |
@content.comments = nil |
69 | 69 |
if request.get? |
... | ... | |
208 | 208 |
def editable?(page = @page) |
209 | 209 |
page.editable_by?(User.current) |
210 | 210 |
end |
211 | ||
212 |
def initial_page_content(page) |
|
213 |
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) |
|
214 |
helper ||= ApplicationHelper::NullFormattingHelper |
|
215 |
extend helper unless self.instance_of?(helper) |
|
216 |
helper.instance_method(:initial_page_content).bind(self).call(page) |
|
217 |
end |
|
211 | 218 |
end |
app/helpers/application_helper.rb | ||
---|---|---|
17 | 17 | |
18 | 18 |
require 'coderay' |
19 | 19 |
require 'coderay/helpers/file_type' |
20 |
require 'forwardable' |
|
20 | 21 | |
21 | 22 |
module ApplicationHelper |
22 | 23 |
include Redmine::WikiFormatting::Macros::Definitions |
23 | 24 | |
25 |
extend Forwardable |
|
26 |
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter |
|
27 | ||
24 | 28 |
def current_role |
25 | 29 |
@current_role ||= User.current.role_for_project(@project) |
26 | 30 |
end |
... | ... | |
257 | 261 |
end |
258 | 262 |
end |
259 | 263 |
|
260 |
text = (Setting.text_formatting == 'textile') ?
|
|
261 |
Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
|
|
262 |
simple_format(auto_link(h(text)))
|
|
264 |
text = (Setting.text_formatting == '0') ?
|
|
265 |
simple_format(auto_link(h(text))) :
|
|
266 |
Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
|
|
263 | 267 | |
264 | 268 |
# different methods for formatting wiki links |
265 | 269 |
case options[:wiki_links] |
... | ... | |
547 | 551 |
end |
548 | 552 |
end |
549 | 553 |
|
550 |
def wikitoolbar_for(field_id) |
|
551 |
return '' unless Setting.text_formatting == 'textile' |
|
552 |
|
|
553 |
help_link = l(:setting_text_formatting) + ': ' + |
|
554 |
link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'), |
|
555 |
:onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;") |
|
556 | ||
557 |
javascript_include_tag('jstoolbar/jstoolbar') + |
|
558 |
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") + |
|
559 |
javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();") |
|
560 |
end |
|
561 |
|
|
562 | 554 |
def content_for(name, content = nil, &block) |
563 | 555 |
@has_content ||= {} |
564 | 556 |
@has_content[name] = true |
... | ... | |
568 | 560 |
def has_content?(name) |
569 | 561 |
(@has_content && @has_content[name]) || false |
570 | 562 |
end |
563 | ||
564 |
private |
|
565 |
def wiki_helper |
|
566 |
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) |
|
567 |
helper ||= NullFormattingHelper |
|
568 |
extend helper |
|
569 |
return self |
|
570 |
end |
|
571 | ||
572 |
module NullFormattingHelper |
|
573 |
def wikitoolbar_for(field_id); '' end |
|
574 |
def heads_for_wiki_formatter; '' end |
|
575 |
def initial_page_content(page); @page.pretty_title end |
|
576 |
end |
|
571 | 577 |
end |
app/helpers/textile_helper.rb | ||
---|---|---|
1 |
module TextileHelper |
|
2 |
def wikitoolbar_for(field_id) |
|
3 |
help_link = l(:setting_text_formatting) + ': ' + |
|
4 |
link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'), |
|
5 |
:onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;") |
|
6 | ||
7 |
javascript_include_tag('jstoolbar/jstoolbar') + |
|
8 |
javascript_include_tag('jstoolbar/textile') + |
|
9 |
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") + |
|
10 |
javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();") |
|
11 |
end |
|
12 | ||
13 |
def initial_page_content(page) |
|
14 |
"h1. #{@page.pretty_title}" |
|
15 |
end |
|
16 | ||
17 |
def heads_for_wiki_formatter; '' end |
|
18 |
end |
app/views/layouts/base.rhtml | ||
---|---|---|
8 | 8 |
<%= stylesheet_link_tag 'application', :media => 'all' %> |
9 | 9 |
<%= javascript_include_tag :defaults %> |
10 | 10 |
<%= stylesheet_link_tag 'jstoolbar' %> |
11 |
<%= heads_for_wiki_formatter %> |
|
11 | 12 |
<!--[if IE]> |
12 | 13 |
<style type="text/css"> |
13 | 14 |
* html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); } |
app/views/settings/_general.rhtml | ||
---|---|---|
39 | 39 |
<%= select_tag 'settings[protocol]', options_for_select(['http', 'https'], Setting.protocol) %></p> |
40 | 40 | |
41 | 41 |
<p><label><%= l(:setting_text_formatting) %></label> |
42 |
<%= select_tag 'settings[text_formatting]', options_for_select([[l(:label_none), "0"], ["textile", "textile"]], Setting.text_formatting) %></p>
|
|
42 |
<%= select_tag 'settings[text_formatting]', options_for_select([[l(:label_none), "0"], *Redmine::WikiFormatting.format_names.collect{|name| [name, name]} ], Setting.text_formatting.to_sym) %></p>
|
|
43 | 43 | |
44 | 44 |
<p><label><%= l(:setting_wiki_compression) %></label> |
45 | 45 |
<%= select_tag 'settings[wiki_compression]', options_for_select( [[l(:label_none), 0], ["gzip", "gzip"]], Setting.wiki_compression) %></p> |
lib/redmine.rb | ||
---|---|---|
6 | 6 |
require 'redmine/themes' |
7 | 7 |
require 'redmine/hook' |
8 | 8 |
require 'redmine/plugin' |
9 |
require 'redmine/wiki_formatting' |
|
9 | 10 | |
10 | 11 |
begin |
11 | 12 |
require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick) |
... | ... | |
149 | 150 |
activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false |
150 | 151 |
activity.register :messages, :default => false |
151 | 152 |
end |
153 | ||
154 |
Redmine::WikiFormatting.map do |format| |
|
155 |
format.register :textile, Redmine::WikiFormatting::TextileFormatter, TextileHelper |
|
156 |
end |
lib/redmine/plugin.rb | ||
---|---|---|
143 | 143 |
Redmine::Activity.register(*args) |
144 | 144 |
end |
145 | 145 | |
146 |
# Registers a wiki formatter. |
|
147 |
# |
|
148 |
# Parameters: |
|
149 |
# [+name+] human-readable name |
|
150 |
# [+formatter+] formatter class, which should have instance_method as to_html(text) |
|
151 |
# [+helper+] helper module, which will be included by wiki pages. |
|
152 |
def wiki_format_provider(name, formatter, helper) |
|
153 |
Redmine::WikiFormatting.register(name, formatter, helper) |
|
154 |
end |
|
155 | ||
146 | 156 |
# Returns +true+ if the plugin can be configured. |
147 | 157 |
def configurable? |
148 | 158 |
settings && settings.is_a?(Hash) && !settings[:partial].blank? |
lib/redmine/wiki_formatting.rb | ||
---|---|---|
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 'redcloth3' |
|
19 |
require 'coderay' |
|
20 | ||
21 | 1 |
module Redmine |
22 | 2 |
module WikiFormatting |
23 |
|
|
24 |
private |
|
25 |
|
|
26 |
class TextileFormatter < RedCloth3 |
|
27 |
|
|
28 |
# auto_link rule after textile rules so that it doesn't break !image_url! tags |
|
29 |
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros] |
|
30 |
|
|
31 |
def initialize(*args) |
|
32 |
super |
|
33 |
self.hard_breaks=true |
|
34 |
self.no_span_caps=true |
|
35 |
end |
|
36 |
|
|
37 |
def to_html(*rules, &block) |
|
38 |
@toc = [] |
|
39 |
@macros_runner = block |
|
40 |
super(*RULES).to_s |
|
41 |
end |
|
42 | ||
43 |
private |
|
44 | ||
45 |
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. |
|
46 |
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a> |
|
47 |
def hard_break( text ) |
|
48 |
text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1<br />\n" ) if hard_breaks |
|
49 |
end |
|
50 |
|
|
51 |
# Patch to add code highlighting support to RedCloth |
|
52 |
def smooth_offtags( text ) |
|
53 |
unless @pre_list.empty? |
|
54 |
## replace <pre> content |
|
55 |
text.gsub!(/<redpre#(\d+)>/) do |
|
56 |
content = @pre_list[$1.to_i] |
|
57 |
if content.match(/<code\s+class="(\w+)">\s?(.+)/m) |
|
58 |
content = "<code class=\"#{$1} CodeRay\">" + |
|
59 |
CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline) |
|
60 |
end |
|
61 |
content |
|
62 |
end |
|
63 |
end |
|
64 |
end |
|
65 |
|
|
66 |
# Patch to add 'table of content' support to RedCloth |
|
67 |
def textile_p_withtoc(tag, atts, cite, content) |
|
68 |
# removes wiki links from the item |
|
69 |
toc_item = content.gsub(/(\[\[|\]\])/, '') |
|
70 |
# removes styles |
|
71 |
# eg. %{color:red}Triggers% => Triggers |
|
72 |
toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1' |
|
73 |
|
|
74 |
# replaces non word caracters by dashes |
|
75 |
anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') |
|
76 | ||
77 |
unless anchor.blank? |
|
78 |
if tag =~ /^h(\d)$/ |
|
79 |
@toc << [$1.to_i, anchor, toc_item] |
|
80 |
end |
|
81 |
atts << " id=\"#{anchor}\"" |
|
82 |
content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a>" |
|
83 |
end |
|
84 |
textile_p(tag, atts, cite, content) |
|
85 |
end |
|
86 | ||
87 |
alias :textile_h1 :textile_p_withtoc |
|
88 |
alias :textile_h2 :textile_p_withtoc |
|
89 |
alias :textile_h3 :textile_p_withtoc |
|
90 |
|
|
91 |
def inline_toc(text) |
|
92 |
text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do |
|
93 |
div_class = 'toc' |
|
94 |
div_class << ' right' if $1 == '>' |
|
95 |
div_class << ' left' if $1 == '<' |
|
96 |
out = "<ul class=\"#{div_class}\">" |
|
97 |
@toc.each do |heading| |
|
98 |
level, anchor, toc_item = heading |
|
99 |
out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n" |
|
100 |
end |
|
101 |
out << '</ul>' |
|
102 |
out |
|
103 |
end |
|
104 |
end |
|
105 |
|
|
106 |
MACROS_RE = / |
|
107 |
(!)? # escaping |
|
108 |
( |
|
109 |
\{\{ # opening tag |
|
110 |
([\w]+) # macro name |
|
111 |
(\(([^\}]*)\))? # optional arguments |
|
112 |
\}\} # closing tag |
|
113 |
) |
|
114 |
/x unless const_defined?(:MACROS_RE) |
|
115 |
|
|
116 |
def inline_macros(text) |
|
117 |
text.gsub!(MACROS_RE) do |
|
118 |
esc, all, macro = $1, $2, $3.downcase |
|
119 |
args = ($5 || '').split(',').each(&:strip) |
|
120 |
if esc.nil? |
|
121 |
begin |
|
122 |
@macros_runner.call(macro, args) |
|
123 |
rescue => e |
|
124 |
"<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>" |
|
125 |
end || all |
|
126 |
else |
|
127 |
all |
|
128 |
end |
|
129 |
end |
|
130 |
end |
|
131 |
|
|
132 |
AUTO_LINK_RE = %r{ |
|
133 |
( # leading text |
|
134 |
<\w+.*?>| # leading HTML tag, or |
|
135 |
[^=<>!:'"/]| # leading punctuation, or |
|
136 |
^ # beginning of line |
|
137 |
) |
|
138 |
( |
|
139 |
(?:https?://)| # protocol spec, or |
|
140 |
(?:ftp://)| |
|
141 |
(?:www\.) # www.* |
|
142 |
) |
|
143 |
( |
|
144 |
(\S+?) # url |
|
145 |
(\/)? # slash |
|
146 |
) |
|
147 |
([^\w\=\/;\(\)]*?) # post |
|
148 |
(?=<|\s|$) |
|
149 |
}x unless const_defined?(:AUTO_LINK_RE) |
|
3 |
@@formatters = {} |
|
150 | 4 | |
151 |
# Turns all urls into clickable links (code from Rails). |
|
152 |
def inline_auto_link(text) |
|
153 |
text.gsub!(AUTO_LINK_RE) do |
|
154 |
all, leading, proto, url, post = $&, $1, $2, $3, $6 |
|
155 |
if leading =~ /<a\s/i || leading =~ /![<>=]?/ |
|
156 |
# don't replace URL's that are already linked |
|
157 |
# and URL's prefixed with ! !> !< != (textile images) |
|
158 |
all |
|
159 |
else |
|
160 |
# Idea below : an URL with unbalanced parethesis and |
|
161 |
# ending by ')' is put into external parenthesis |
|
162 |
if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) ) |
|
163 |
url=url[0..-2] # discard closing parenth from url |
|
164 |
post = ")"+post # add closing parenth to post |
|
165 |
end |
|
166 |
%(#{leading}<a class="external" href="#{proto=="www."?"http://www.":proto}#{url}">#{proto + url}</a>#{post}) |
|
167 |
end |
|
168 |
end |
|
169 |
end |
|
170 | ||
171 |
# Turns all email addresses into clickable links (code from Rails). |
|
172 |
def inline_auto_mailto(text) |
|
173 |
text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do |
|
174 |
mail = $1 |
|
175 |
if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/) |
|
176 |
|
|
177 |
else |
|
178 |
%{<a href="mailto:#{mail}" class="email">#{mail}</a>} |
|
179 |
end |
|
180 |
end |
|
181 |
end |
|
5 |
def self.map |
|
6 |
yield self |
|
7 |
end |
|
8 |
def self.register(name, formatter, helper) |
|
9 |
raise ArgumentError, "format name `#{name}' is already taken" if @@formatters[name] |
|
10 |
@@formatters[name.to_sym] = {:formatter => formatter, :helper => helper} |
|
11 |
end |
|
12 |
def self.formatter_for(name) |
|
13 |
entry = @@formatters[name.to_sym] |
|
14 |
return entry && entry[:formatter] |
|
15 |
end |
|
16 |
def self.helper_for(name) |
|
17 |
entry = @@formatters[name.to_sym] |
|
18 |
return entry && entry[:helper] |
|
19 |
end |
|
20 |
def self.format_names |
|
21 |
@@formatters.keys.map |
|
182 | 22 |
end |
183 |
|
|
184 |
public |
|
185 |
|
|
186 |
def self.to_html(text, options = {}, &block) |
|
187 |
TextileFormatter.new(text).to_html(&block) |
|
23 |
def self.to_html(format, text, options = {}, &block) |
|
24 |
(formatter_for(format) || TextileFormatter).new(text).to_html(&block) |
|
188 | 25 |
end |
189 | 26 |
end |
190 | 27 |
end |
lib/redmine/wiki_formatting/textile_formatter.rb | ||
---|---|---|
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 'redcloth3' |
|
19 |
require 'coderay' |
|
20 | ||
21 |
module Redmine |
|
22 |
module WikiFormatting |
|
23 |
class TextileFormatter < RedCloth3 |
|
24 |
|
|
25 |
# auto_link rule after textile rules so that it doesn't break !image_url! tags |
|
26 |
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros] |
|
27 |
|
|
28 |
def initialize(*args) |
|
29 |
super |
|
30 |
self.hard_breaks=true |
|
31 |
self.no_span_caps=true |
|
32 |
end |
|
33 |
|
|
34 |
def to_html(*rules, &block) |
|
35 |
@toc = [] |
|
36 |
@macros_runner = block |
|
37 |
super(*RULES).to_s |
|
38 |
end |
|
39 | ||
40 |
private |
|
41 | ||
42 |
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. |
|
43 |
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a> |
|
44 |
def hard_break( text ) |
|
45 |
text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1<br />\n" ) if hard_breaks |
|
46 |
end |
|
47 |
|
|
48 |
# Patch to add code highlighting support to RedCloth |
|
49 |
def smooth_offtags( text ) |
|
50 |
unless @pre_list.empty? |
|
51 |
## replace <pre> content |
|
52 |
text.gsub!(/<redpre#(\d+)>/) do |
|
53 |
content = @pre_list[$1.to_i] |
|
54 |
if content.match(/<code\s+class="(\w+)">\s?(.+)/m) |
|
55 |
content = "<code class=\"#{$1} CodeRay\">" + |
|
56 |
CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline) |
|
57 |
end |
|
58 |
content |
|
59 |
end |
|
60 |
end |
|
61 |
end |
|
62 |
|
|
63 |
# Patch to add 'table of content' support to RedCloth |
|
64 |
def textile_p_withtoc(tag, atts, cite, content) |
|
65 |
# removes wiki links from the item |
|
66 |
toc_item = content.gsub(/(\[\[|\]\])/, '') |
|
67 |
# removes styles |
|
68 |
# eg. %{color:red}Triggers% => Triggers |
|
69 |
toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1' |
|
70 |
|
|
71 |
# replaces non word caracters by dashes |
|
72 |
anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') |
|
73 | ||
74 |
unless anchor.blank? |
|
75 |
if tag =~ /^h(\d)$/ |
|
76 |
@toc << [$1.to_i, anchor, toc_item] |
|
77 |
end |
|
78 |
atts << " id=\"#{anchor}\"" |
|
79 |
content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a>" |
|
80 |
end |
|
81 |
textile_p(tag, atts, cite, content) |
|
82 |
end |
|
83 | ||
84 |
alias :textile_h1 :textile_p_withtoc |
|
85 |
alias :textile_h2 :textile_p_withtoc |
|
86 |
alias :textile_h3 :textile_p_withtoc |
|
87 |
|
|
88 |
def inline_toc(text) |
|
89 |
text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do |
|
90 |
div_class = 'toc' |
|
91 |
div_class << ' right' if $1 == '>' |
|
92 |
div_class << ' left' if $1 == '<' |
|
93 |
out = "<ul class=\"#{div_class}\">" |
|
94 |
@toc.each do |heading| |
|
95 |
level, anchor, toc_item = heading |
|
96 |
out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n" |
|
97 |
end |
|
98 |
out << '</ul>' |
|
99 |
out |
|
100 |
end |
|
101 |
end |
|
102 |
|
|
103 |
MACROS_RE = / |
|
104 |
(!)? # escaping |
|
105 |
( |
|
106 |
\{\{ # opening tag |
|
107 |
([\w]+) # macro name |
|
108 |
(\(([^\}]*)\))? # optional arguments |
|
109 |
\}\} # closing tag |
|
110 |
) |
|
111 |
/x unless const_defined?(:MACROS_RE) |
|
112 |
|
|
113 |
def inline_macros(text) |
|
114 |
text.gsub!(MACROS_RE) do |
|
115 |
esc, all, macro = $1, $2, $3.downcase |
|
116 |
args = ($5 || '').split(',').each(&:strip) |
|
117 |
if esc.nil? |
|
118 |
begin |
|
119 |
@macros_runner.call(macro, args) |
|
120 |
rescue => e |
|
121 |
"<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>" |
|
122 |
end || all |
|
123 |
else |
|
124 |
all |
|
125 |
end |
|
126 |
end |
|
127 |
end |
|
128 |
|
|
129 |
AUTO_LINK_RE = %r{ |
|
130 |
( # leading text |
|
131 |
<\w+.*?>| # leading HTML tag, or |
|
132 |
[^=<>!:'"/]| # leading punctuation, or |
|
133 |
^ # beginning of line |
|
134 |
) |
|
135 |
( |
|
136 |
(?:https?://)| # protocol spec, or |
|
137 |
(?:ftp://)| |
|
138 |
(?:www\.) # www.* |
|
139 |
) |
|
140 |
( |
|
141 |
(\S+?) # url |
|
142 |
(\/)? # slash |
|
143 |
) |
|
144 |
([^\w\=\/;\(\)]*?) # post |
|
145 |
(?=<|\s|$) |
|
146 |
}x unless const_defined?(:AUTO_LINK_RE) |
|
147 | ||
148 |
# Turns all urls into clickable links (code from Rails). |
|
149 |
def inline_auto_link(text) |
|
150 |
text.gsub!(AUTO_LINK_RE) do |
|
151 |
all, leading, proto, url, post = $&, $1, $2, $3, $6 |
|
152 |
if leading =~ /<a\s/i || leading =~ /![<>=]?/ |
|
153 |
# don't replace URL's that are already linked |
|
154 |
# and URL's prefixed with ! !> !< != (textile images) |
|
155 |
all |
|
156 |
else |
|
157 |
# Idea below : an URL with unbalanced parethesis and |
|
158 |
# ending by ')' is put into external parenthesis |
|
159 |
if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) ) |
|
160 |
url=url[0..-2] # discard closing parenth from url |
|
161 |
post = ")"+post # add closing parenth to post |
|
162 |
end |
|
163 |
%(#{leading}<a class="external" href="#{proto=="www."?"http://www.":proto}#{url}">#{proto + url}</a>#{post}) |
|
164 |
end |
|
165 |
end |
|
166 |
end |
|
167 | ||
168 |
# Turns all email addresses into clickable links (code from Rails). |
|
169 |
def inline_auto_mailto(text) |
|
170 |
text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do |
|
171 |
mail = $1 |
|
172 |
if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/) |
|
173 |
|
|
174 |
else |
|
175 |
%{<a href="mailto:#{mail}" class="email">#{mail}</a>} |
|
176 |
end |
|
177 |
end |
|
178 |
end |
|
179 |
end |
|
180 |
end |
|
181 |
end |
public/javascripts/jstoolbar/jstoolbar.js | ||
---|---|---|
378 | 378 |
document.removeEventListener('mousemove', this.dragMoveHdlr, false); |
379 | 379 |
document.removeEventListener('mouseup', this.dragStopHdlr, false); |
380 | 380 |
}; |
381 | ||
382 |
// Elements definition ------------------------------------ |
|
383 | ||
384 |
// strong |
|
385 |
jsToolBar.prototype.elements.strong = { |
|
386 |
type: 'button', |
|
387 |
title: 'Strong', |
|
388 |
fn: { |
|
389 |
wiki: function() { this.singleTag('*') } |
|
390 |
} |
|
391 |
} |
|
392 | ||
393 |
// em |
|
394 |
jsToolBar.prototype.elements.em = { |
|
395 |
type: 'button', |
|
396 |
title: 'Italic', |
|
397 |
fn: { |
|
398 |
wiki: function() { this.singleTag("_") } |
|
399 |
} |
|
400 |
} |
|
401 | ||
402 |
// ins |
|
403 |
jsToolBar.prototype.elements.ins = { |
|
404 |
type: 'button', |
|
405 |
title: 'Underline', |
|
406 |
fn: { |
|
407 |
wiki: function() { this.singleTag('+') } |
|
408 |
} |
|
409 |
} |
|
410 | ||
411 |
// del |
|
412 |
jsToolBar.prototype.elements.del = { |
|
413 |
type: 'button', |
|
414 |
title: 'Deleted', |
|
415 |
fn: { |
|
416 |
wiki: function() { this.singleTag('-') } |
|
417 |
} |
|
418 |
} |
|
419 | ||
420 |
// code |
|
421 |
jsToolBar.prototype.elements.code = { |
|
422 |
type: 'button', |
|
423 |
title: 'Code', |
|
424 |
fn: { |
|
425 |
wiki: function() { this.singleTag('@') } |
|
426 |
} |
|
427 |
} |
|
428 | ||
429 |
// spacer |
|
430 |
jsToolBar.prototype.elements.space1 = {type: 'space'} |
|
431 | ||
432 |
// headings |
|
433 |
jsToolBar.prototype.elements.h1 = { |
|
434 |
type: 'button', |
|
435 |
title: 'Heading 1', |
|
436 |
fn: { |
|
437 |
wiki: function() { |
|
438 |
this.encloseLineSelection('h1. ', '',function(str) { |
|
439 |
str = str.replace(/^h\d+\.\s+/, '') |
|
440 |
return str; |
|
441 |
}); |
|
442 |
} |
|
443 |
} |
|
444 |
} |
|
445 |
jsToolBar.prototype.elements.h2 = { |
|
446 |
type: 'button', |
|
447 |
title: 'Heading 2', |
|
448 |
fn: { |
|
449 |
wiki: function() { |
|
450 |
this.encloseLineSelection('h2. ', '',function(str) { |
|
451 |
str = str.replace(/^h\d+\.\s+/, '') |
|
452 |
return str; |
|
453 |
}); |
|
454 |
} |
|
455 |
} |
|
456 |
} |
|
457 |
jsToolBar.prototype.elements.h3 = { |
|
458 |
type: 'button', |
|
459 |
title: 'Heading 3', |
|
460 |
fn: { |
|
461 |
wiki: function() { |
|
462 |
this.encloseLineSelection('h3. ', '',function(str) { |
|
463 |
str = str.replace(/^h\d+\.\s+/, '') |
|
464 |
return str; |
|
465 |
}); |
|
466 |
} |
|
467 |
} |
|
468 |
} |
|
469 | ||
470 |
// spacer |
|
471 |
jsToolBar.prototype.elements.space2 = {type: 'space'} |
|
472 | ||
473 |
// ul |
|
474 |
jsToolBar.prototype.elements.ul = { |
|
475 |
type: 'button', |
|
476 |
title: 'Unordered list', |
|
477 |
fn: { |
|
478 |
wiki: function() { |
|
479 |
this.encloseLineSelection('','',function(str) { |
|
480 |
str = str.replace(/\r/g,''); |
|
481 |
return str.replace(/(\n|^)[#-]?\s*/g,"$1* "); |
|
482 |
}); |
|
483 |
} |
|
484 |
} |
|
485 |
} |
|
486 | ||
487 |
// ol |
|
488 |
jsToolBar.prototype.elements.ol = { |
|
489 |
type: 'button', |
|
490 |
title: 'Ordered list', |
|
491 |
fn: { |
|
492 |
wiki: function() { |
|
493 |
this.encloseLineSelection('','',function(str) { |
|
494 |
str = str.replace(/\r/g,''); |
|
495 |
return str.replace(/(\n|^)[*-]?\s*/g,"$1# "); |
|
496 |
}); |
|
497 |
} |
|
498 |
} |
|
499 |
} |
|
500 | ||
501 |
// spacer |
|
502 |
jsToolBar.prototype.elements.space3 = {type: 'space'} |
|
503 | ||
504 |
// bq |
|
505 |
jsToolBar.prototype.elements.bq = { |
|
506 |
type: 'button', |
|
507 |
title: 'Quote', |
|
508 |
fn: { |
|
509 |
wiki: function() { |
|
510 |
this.encloseLineSelection('','',function(str) { |
|
511 |
str = str.replace(/\r/g,''); |
|
512 |
return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2"); |
|
513 |
}); |
|
514 |
} |
|
515 |
} |
|
516 |
} |
|
517 | ||
518 |
// unbq |
|
519 |
jsToolBar.prototype.elements.unbq = { |
|
520 |
type: 'button', |
|
521 |
title: 'Unquote', |
|
522 |
fn: { |
|
523 |
wiki: function() { |
|
524 |
this.encloseLineSelection('','',function(str) { |
|
525 |
str = str.replace(/\r/g,''); |
|
526 |
return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2"); |
|
527 |
}); |
|
528 |
} |
|
529 |
} |
|
530 |
} |
|
531 | ||
532 |
// pre |
|
533 |
jsToolBar.prototype.elements.pre = { |
|
534 |
type: 'button', |
|
535 |
title: 'Preformatted text', |
|
536 |
fn: { |
|
537 |
wiki: function() { this.encloseLineSelection('<pre>\n', '\n</pre>') } |
|
538 |
} |
|
539 |
} |
|
540 | ||
541 |
// spacer |
|
542 |
jsToolBar.prototype.elements.space4 = {type: 'space'} |
|
543 | ||
544 |
// wiki page |
|
545 |
jsToolBar.prototype.elements.link = { |
|
546 |
type: 'button', |
|
547 |
title: 'Wiki link', |
|
548 |
fn: { |
|
549 |
wiki: function() { this.encloseSelection("[[", "]]") } |
|
550 |
} |
|
551 |
} |
|
552 |
// image |
|
553 |
jsToolBar.prototype.elements.img = { |
|
554 |
type: 'button', |
|
555 |
title: 'Image', |
|
556 |
fn: { |
|
557 |
wiki: function() { this.encloseSelection("!", "!") } |
|
558 |
} |
|
559 |
} |
public/javascripts/jstoolbar/textile.js | ||
---|---|---|
1 |
/* ***** BEGIN LICENSE BLOCK ***** |
|
2 |
* This file is part of DotClear. |
|
3 |
* Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All |
|
4 |
* rights reserved. |
|
5 |
* |
|
6 |
* DotClear is free software; you can redistribute it and/or modify |
|
7 |
* it under the terms of the GNU General Public License as published by |
|
8 |
* the Free Software Foundation; either version 2 of the License, or |
|
9 |
* (at your option) any later version. |
|
10 |
* |
|
11 |
* DotClear is distributed in the hope that it will be useful, |
|
12 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 |
* GNU General Public License for more details. |
|
15 |
* |
|
16 |
* You should have received a copy of the GNU General Public License |
|
17 |
* along with DotClear; if not, write to the Free Software |
|
18 |
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
|
19 |
* |
|
20 |
* ***** END LICENSE BLOCK ***** |
|
21 |
*/ |
|
22 | ||
23 |
// Elements definition ------------------------------------ |
|
24 | ||
25 |
// strong |
|
26 |
jsToolBar.prototype.elements.strong = { |
|
27 |
type: 'button', |
|
28 |
title: 'Strong', |
|
29 |
fn: { |
|
30 |
wiki: function() { this.singleTag('*') } |
|
31 |
} |
|
32 |
} |
|
33 | ||
34 |
// em |
|
35 |
jsToolBar.prototype.elements.em = { |
|
36 |
type: 'button', |
|
37 |
title: 'Italic', |
|
38 |
fn: { |
|
39 |
wiki: function() { this.singleTag("_") } |
|
40 |
} |
|
41 |
} |
|
42 | ||
43 |
// ins |
|
44 |
jsToolBar.prototype.elements.ins = { |
|
45 |
type: 'button', |
|
46 |
title: 'Underline', |
|
47 |
fn: { |
|
48 |
wiki: function() { this.singleTag('+') } |
|
49 |
} |
|
50 |
} |
|
51 | ||
52 |
// del |
|
53 |
jsToolBar.prototype.elements.del = { |
|
54 |
type: 'button', |
|
55 |
title: 'Deleted', |
|
56 |
fn: { |
|
57 |
wiki: function() { this.singleTag('-') } |
|
58 |
} |
|
59 |
} |
|
60 | ||
61 |
// code |
|
62 |
jsToolBar.prototype.elements.code = { |
|
63 |
type: 'button', |
|
64 |
title: 'Code', |
|
65 |
fn: { |
|
66 |
wiki: function() { this.singleTag('@') } |
|
67 |
} |
|
68 |
} |
|
69 | ||
70 |
// spacer |
|
71 |
jsToolBar.prototype.elements.space1 = {type: 'space'} |
|
72 | ||
73 |
// headings |
|
74 |
jsToolBar.prototype.elements.h1 = { |
|
75 |
type: 'button', |
|
76 |
title: 'Heading 1', |
|
77 |
fn: { |
|
78 |
wiki: function() { |
|
79 |
this.encloseLineSelection('h1. ', '',function(str) { |
|
80 |
str = str.replace(/^h\d+\.\s+/, '') |
|
81 |
return str; |
|
82 |
}); |
|
83 |
} |
|
84 |
} |
|
85 |
} |
|
86 |
jsToolBar.prototype.elements.h2 = { |
|
87 |
type: 'button', |
|
88 |
title: 'Heading 2', |
|
89 |
fn: { |
|
90 |
wiki: function() { |
|
91 |
this.encloseLineSelection('h2. ', '',function(str) { |
|
92 |
str = str.replace(/^h\d+\.\s+/, '') |
|
93 |
return str; |
|
94 |
}); |
|
95 |
} |
|
96 |
} |
|
97 |
} |
|
98 |
jsToolBar.prototype.elements.h3 = { |
|
99 |
type: 'button', |
|
100 |
title: 'Heading 3', |
|
101 |
fn: { |
|
102 |
wiki: function() { |
|
103 |
this.encloseLineSelection('h3. ', '',function(str) { |
|
104 |
str = str.replace(/^h\d+\.\s+/, '') |
|
105 |
return str; |
|
106 |
}); |
|
107 |
} |
|
108 |
} |
|
109 |
} |
|
110 | ||
111 |
// spacer |
|
112 |
jsToolBar.prototype.elements.space2 = {type: 'space'} |
|
113 | ||
114 |
// ul |
|
115 |
jsToolBar.prototype.elements.ul = { |
|
116 |
type: 'button', |
|
117 |
title: 'Unordered list', |
|
118 |
fn: { |
|
119 |
wiki: function() { |
|
120 |
this.encloseLineSelection('','',function(str) { |
|
121 |
str = str.replace(/\r/g,''); |
|
122 |
return str.replace(/(\n|^)[#-]?\s*/g,"$1* "); |
|
123 |
}); |
|
124 |
} |
|
125 |
} |
|
126 |
} |
|
127 | ||
128 |
// ol |
|
129 |
jsToolBar.prototype.elements.ol = { |
|
130 |
type: 'button', |
|
131 |
title: 'Ordered list', |
|
132 |
fn: { |
|
133 |
wiki: function() { |
|
134 |
this.encloseLineSelection('','',function(str) { |
|
135 |
str = str.replace(/\r/g,''); |
|
136 |
return str.replace(/(\n|^)[*-]?\s*/g,"$1# "); |
|
137 |
}); |
|
138 |
} |
|
139 |
} |
|
140 |
} |
|
141 | ||
142 |
// spacer |
|
143 |
jsToolBar.prototype.elements.space3 = {type: 'space'} |
|
144 | ||
145 |
// bq |
|
146 |
jsToolBar.prototype.elements.bq = { |
|
147 |
type: 'button', |
|
148 |
title: 'Quote', |
|
149 |
fn: { |
|
150 |
wiki: function() { |
|
151 |
this.encloseLineSelection('','',function(str) { |
|
152 |
str = str.replace(/\r/g,''); |
|
153 |
return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2"); |
|
154 |
}); |
|
155 |
} |
|
156 |
} |
|
157 |
} |
|
158 | ||
159 |
// unbq |
|
160 |
jsToolBar.prototype.elements.unbq = { |
|
161 |
type: 'button', |
|
162 |
title: 'Unquote', |
|
163 |
fn: { |
|
164 |
wiki: function() { |
|
165 |
this.encloseLineSelection('','',function(str) { |
|
166 |
str = str.replace(/\r/g,''); |
|
167 |
return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2"); |
|
168 |
}); |
|
169 |
} |
|
170 |
} |
|
171 |
} |
|
172 | ||
173 |
// pre |
|
174 |
jsToolBar.prototype.elements.pre = { |
|
175 |
type: 'button', |
|
176 |
title: 'Preformatted text', |
|
177 |
fn: { |
|
178 |
wiki: function() { this.encloseLineSelection('<pre>\n', '\n</pre>') } |
|
179 |
} |
|
180 |
} |
|
181 | ||
182 |
// spacer |
|
183 |
jsToolBar.prototype.elements.space4 = {type: 'space'} |
|
184 | ||
185 |
// wiki page |
|
186 |
jsToolBar.prototype.elements.link = { |
|
187 |
type: 'button', |
|
188 |
title: 'Wiki link', |
|
189 |
fn: { |
|
190 |
wiki: function() { this.encloseSelection("[[", "]]") } |
|
191 |
} |
|
192 |
} |
|
193 |
// image |
|
194 |
jsToolBar.prototype.elements.img = { |
|
195 |
type: 'button', |
|
196 |
title: 'Image', |
|
197 |
fn: { |
|
198 |
wiki: function() { this.encloseSelection("!", "!") } |
|
199 |
} |
|
200 |
} |