##// END OF EJS Templates
Makes wiki text formatter pluggable....
Jean-Philippe Lang -
r1953:a3b9a5aa5fe3
parent child
Show More
@@ -0,0 +1,183
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 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 module Textile
24 class Formatter < RedCloth3
25
26 # auto_link rule after textile rules so that it doesn't break !image_url! tags
27 RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros]
28
29 def initialize(*args)
30 super
31 self.hard_breaks=true
32 self.no_span_caps=true
33 end
34
35 def to_html(*rules, &block)
36 @toc = []
37 @macros_runner = block
38 super(*RULES).to_s
39 end
40
41 private
42
43 # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
44 # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
45 def hard_break( text )
46 text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1<br />\n" ) if hard_breaks
47 end
48
49 # Patch to add code highlighting support to RedCloth
50 def smooth_offtags( text )
51 unless @pre_list.empty?
52 ## replace <pre> content
53 text.gsub!(/<redpre#(\d+)>/) do
54 content = @pre_list[$1.to_i]
55 if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
56 content = "<code class=\"#{$1} CodeRay\">" +
57 CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline)
58 end
59 content
60 end
61 end
62 end
63
64 # Patch to add 'table of content' support to RedCloth
65 def textile_p_withtoc(tag, atts, cite, content)
66 # removes wiki links from the item
67 toc_item = content.gsub(/(\[\[|\]\])/, '')
68 # removes styles
69 # eg. %{color:red}Triggers% => Triggers
70 toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1'
71
72 # replaces non word caracters by dashes
73 anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
74
75 unless anchor.blank?
76 if tag =~ /^h(\d)$/
77 @toc << [$1.to_i, anchor, toc_item]
78 end
79 atts << " id=\"#{anchor}\""
80 content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a>"
81 end
82 textile_p(tag, atts, cite, content)
83 end
84
85 alias :textile_h1 :textile_p_withtoc
86 alias :textile_h2 :textile_p_withtoc
87 alias :textile_h3 :textile_p_withtoc
88
89 def inline_toc(text)
90 text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do
91 div_class = 'toc'
92 div_class << ' right' if $1 == '>'
93 div_class << ' left' if $1 == '<'
94 out = "<ul class=\"#{div_class}\">"
95 @toc.each do |heading|
96 level, anchor, toc_item = heading
97 out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n"
98 end
99 out << '</ul>'
100 out
101 end
102 end
103
104 MACROS_RE = /
105 (!)? # escaping
106 (
107 \{\{ # opening tag
108 ([\w]+) # macro name
109 (\(([^\}]*)\))? # optional arguments
110 \}\} # closing tag
111 )
112 /x unless const_defined?(:MACROS_RE)
113
114 def inline_macros(text)
115 text.gsub!(MACROS_RE) do
116 esc, all, macro = $1, $2, $3.downcase
117 args = ($5 || '').split(',').each(&:strip)
118 if esc.nil?
119 begin
120 @macros_runner.call(macro, args)
121 rescue => e
122 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
123 end || all
124 else
125 all
126 end
127 end
128 end
129
130 AUTO_LINK_RE = %r{
131 ( # leading text
132 <\w+.*?>| # leading HTML tag, or
133 [^=<>!:'"/]| # leading punctuation, or
134 ^ # beginning of line
135 )
136 (
137 (?:https?://)| # protocol spec, or
138 (?:ftp://)|
139 (?:www\.) # www.*
140 )
141 (
142 (\S+?) # url
143 (\/)? # slash
144 )
145 ([^\w\=\/;\(\)]*?) # post
146 (?=<|\s|$)
147 }x unless const_defined?(:AUTO_LINK_RE)
148
149 # Turns all urls into clickable links (code from Rails).
150 def inline_auto_link(text)
151 text.gsub!(AUTO_LINK_RE) do
152 all, leading, proto, url, post = $&, $1, $2, $3, $6
153 if leading =~ /<a\s/i || leading =~ /![<>=]?/
154 # don't replace URL's that are already linked
155 # and URL's prefixed with ! !> !< != (textile images)
156 all
157 else
158 # Idea below : an URL with unbalanced parethesis and
159 # ending by ')' is put into external parenthesis
160 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
161 url=url[0..-2] # discard closing parenth from url
162 post = ")"+post # add closing parenth to post
163 end
164 %(#{leading}<a class="external" href="#{proto=="www."?"http://www.":proto}#{url}">#{proto + url}</a>#{post})
165 end
166 end
167 end
168
169 # Turns all email addresses into clickable links (code from Rails).
170 def inline_auto_mailto(text)
171 text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
172 mail = $1
173 if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
174 mail
175 else
176 %{<a href="mailto:#{mail}" class="email">#{mail}</a>}
177 end
178 end
179 end
180 end
181 end
182 end
183 end
@@ -0,0 +1,43
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 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 module Redmine
19 module WikiFormatting
20 module Textile
21 module Helper
22 def wikitoolbar_for(field_id)
23 help_link = l(:setting_text_formatting) + ': ' +
24 link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
25 :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;")
26
27 javascript_include_tag('jstoolbar/jstoolbar') +
28 javascript_include_tag('jstoolbar/textile') +
29 javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
30 javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
31 end
32
33 def initial_page_content(page)
34 "h1. #{@page.pretty_title}"
35 end
36
37 def heads_for_wiki_formatter
38 stylesheet_link_tag 'jstoolbar'
39 end
40 end
41 end
42 end
43 end
@@ -0,0 +1,200
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 /* Modified by JP LANG for textile formatting */
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 }
@@ -63,7 +63,7 class WikiController < ApplicationController
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,4 +208,11 private
208 208 def editable?(page = @page)
209 209 page.editable_by?(User.current)
210 210 end
211
212 # Returns the default content of a new wiki page
213 def initial_page_content(page)
214 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
215 extend helper unless self.instance_of?(helper)
216 helper.instance_method(:initial_page_content).bind(self).call(page)
217 end
211 218 end
@@ -17,10 +17,14
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
@@ -259,9 +263,7 module ApplicationHelper
259 263 end
260 264 end
261 265
262 text = (Setting.text_formatting == 'textile') ?
263 Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
264 simple_format(auto_link(h(text)))
266 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
265 267
266 268 # different methods for formatting wiki links
267 269 case options[:wiki_links]
@@ -549,18 +551,6 module ApplicationHelper
549 551 end
550 552 end
551 553
552 def wikitoolbar_for(field_id)
553 return '' unless Setting.text_formatting == 'textile'
554
555 help_link = l(:setting_text_formatting) + ': ' +
556 link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
557 :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;")
558
559 javascript_include_tag('jstoolbar/jstoolbar') +
560 javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
561 javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
562 end
563
564 554 def content_for(name, content = nil, &block)
565 555 @has_content ||= {}
566 556 @has_content[name] = true
@@ -570,4 +560,12 module ApplicationHelper
570 560 def has_content?(name)
571 561 (@has_content && @has_content[name]) || false
572 562 end
563
564 private
565
566 def wiki_helper
567 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
568 extend helper
569 return self
570 end
573 571 end
@@ -7,7 +7,7
7 7 <meta name="keywords" content="issue,bug,tracker" />
8 8 <%= stylesheet_link_tag 'application', :media => 'all' %>
9 9 <%= javascript_include_tag :defaults %>
10 <%= stylesheet_link_tag 'jstoolbar' %>
10 <%= heads_for_wiki_formatter %>
11 11 <!--[if IE]>
12 12 <style type="text/css">
13 13 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
@@ -32,6 +32,6 hr {
32 32 <body>
33 33 <%= yield %>
34 34 <hr />
35 <span class="footer"><%= Redmine::WikiFormatting.to_html(Setting.emails_footer) %></span>
35 <span class="footer"><%= Redmine::WikiFormatting.to_html(Setting.text_formatting, Setting.emails_footer) %></span>
36 36 </body>
37 37 </html>
@@ -39,7 +39,7
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>
@@ -6,6 +6,7 require 'redmine/core_ext'
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)
@@ -150,3 +151,7 Redmine::Activity.map do |activity|
150 151 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
151 152 activity.register :messages, :default => false
152 153 end
154
155 Redmine::WikiFormatting.map do |format|
156 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
157 end
@@ -149,6 +149,16 module Redmine #:nodoc:
149 149 Redmine::Activity.register(*args)
150 150 end
151 151
152 # Registers a wiki formatter.
153 #
154 # Parameters:
155 # * +name+ - human-readable name
156 # * +formatter+ - formatter class, which should have an instance method +to_html+
157 # * +helper+ - helper module, which will be included by wiki pages
158 def wiki_format_provider(name, formatter, helper)
159 Redmine::WikiFormatting.register(name, formatter, helper)
160 end
161
152 162 # Returns +true+ if the plugin can be configured.
153 163 def configurable?
154 164 settings && settings.is_a?(Hash) && !settings[:partial].blank?
@@ -1,5 +1,5
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 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
@@ -15,176 +15,65
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 require 'redcloth3'
19 require 'coderay'
20
21 18 module Redmine
22 19 module WikiFormatting
20 @@formatters = {}
23 21
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
22 class << self
23 def map
24 yield self
35 25 end
36 26
37 def to_html(*rules, &block)
38 @toc = []
39 @macros_runner = block
40 super(*RULES).to_s
27 def register(name, formatter, helper)
28 raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name]
29 @@formatters[name.to_sym] = {:formatter => formatter, :helper => helper}
41 30 end
42 31
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
32 def formatter_for(name)
33 entry = @@formatters[name.to_sym]
34 (entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter
49 35 end
50 36
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
37 def helper_for(name)
38 entry = @@formatters[name.to_sym]
39 (entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper
62 40 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 41
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\">&para;</a>"
83 end
84 textile_p(tag, atts, cite, content)
42 def format_names
43 @@formatters.keys.map
85 44 end
86 45
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
46 def to_html(format, text, options = {}, &block)
47 formatter_for(format).new(text).to_html(&block)
103 48 end
104 49 end
105 50
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)
51 # Default formatter module
52 module NullFormatter
53 class Formatter
54 include ActionView::Helpers::TagHelper
55 include ActionView::Helpers::TextHelper
115 56
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
57 def initialize(text)
58 @text = text
129 59 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)
150 60
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
61 def to_html(*args)
62 simple_format(auto_link(CGI::escapeHTML(@text)))
168 63 end
169 64 end
170 65
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 mail
177 else
178 %{<a href="mailto:#{mail}" class="email">#{mail}</a>}
66 module Helper
67 def wikitoolbar_for(field_id)
179 68 end
69
70 def heads_for_wiki_formatter
180 71 end
72
73 def initial_page_content(page)
74 page.pretty_title.to_s
181 75 end
182 76 end
183
184 public
185
186 def self.to_html(text, options = {}, &block)
187 TextileFormatter.new(text).to_html(&block)
188 77 end
189 78 end
190 79 end
@@ -378,182 +378,3 jsToolBar.prototype.resizeDragStop = function(event) {
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 }
@@ -350,6 +350,13 EXPECTED
350 350 assert textilizable(text).match(/Unknow project/)
351 351 end
352 352
353 def test_default_formatter
354 Setting.text_formatting = 'unknown'
355 text = 'a *link*: http://www.example.net/'
356 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
357 Setting.text_formatting = 'textile'
358 end
359
353 360 def test_date_format_default
354 361 today = Date.today
355 362 Setting.date_format = ''
General Comments 0
You need to be logged in to leave comments. Login now