Commit 1d9c5188 authored by jplang's avatar jplang

Makes wiki text formatter pluggable.

Original patch #2025 by Yuki Sonoda slightly edited.

git-svn-id: https://svn.redmine.org/redmine/trunk@1955 e93f8b46-1217-0410-a6f0-8f06a7374b81
parent 9b84c044
......@@ -63,7 +63,7 @@ class WikiController < ApplicationController
@page.content = WikiContent.new(:page => @page) if @page.new_record?
@content = @page.content_for_version(params[:version])
@content.text = "h1. #{@page.pretty_title}" if @content.text.blank?
@content.text = initial_page_content(@page) if @content.text.blank?
# don't keep previous comment
@content.comments = nil
if request.get?
......@@ -208,4 +208,11 @@ private
def editable?(page = @page)
page.editable_by?(User.current)
end
# Returns the default content of a new wiki page
def initial_page_content(page)
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
extend helper unless self.instance_of?(helper)
helper.instance_method(:initial_page_content).bind(self).call(page)
end
end
......@@ -17,10 +17,14 @@
require 'coderay'
require 'coderay/helpers/file_type'
require 'forwardable'
module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions
extend Forwardable
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
def current_role
@current_role ||= User.current.role_for_project(@project)
end
......@@ -259,9 +263,7 @@ module ApplicationHelper
end
end
text = (Setting.text_formatting == 'textile') ?
Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
simple_format(auto_link(h(text)))
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
# different methods for formatting wiki links
case options[:wiki_links]
......@@ -549,18 +551,6 @@ module ApplicationHelper
end
end
def wikitoolbar_for(field_id)
return '' unless Setting.text_formatting == 'textile'
help_link = l(:setting_text_formatting) + ': ' +
link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
: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;")
javascript_include_tag('jstoolbar/jstoolbar') +
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
end
def content_for(name, content = nil, &block)
@has_content ||= {}
@has_content[name] = true
......@@ -570,4 +560,12 @@ module ApplicationHelper
def has_content?(name)
(@has_content && @has_content[name]) || false
end
private
def wiki_helper
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
extend helper
return self
end
end
......@@ -7,7 +7,7 @@
<meta name="keywords" content="issue,bug,tracker" />
<%= stylesheet_link_tag 'application', :media => 'all' %>
<%= javascript_include_tag :defaults %>
<%= stylesheet_link_tag 'jstoolbar' %>
<%= heads_for_wiki_formatter %>
<!--[if IE]>
<style type="text/css">
* html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
......
......@@ -32,6 +32,6 @@ hr {
<body>
<%= yield %>
<hr />
<span class="footer"><%= Redmine::WikiFormatting.to_html(Setting.emails_footer) %></span>
<span class="footer"><%= Redmine::WikiFormatting.to_html(Setting.text_formatting, Setting.emails_footer) %></span>
</body>
</html>
......@@ -39,7 +39,7 @@
<%= select_tag 'settings[protocol]', options_for_select(['http', 'https'], Setting.protocol) %></p>
<p><label><%= l(:setting_text_formatting) %></label>
<%= select_tag 'settings[text_formatting]', options_for_select([[l(:label_none), "0"], ["textile", "textile"]], Setting.text_formatting) %></p>
<%= 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>
<p><label><%= l(:setting_wiki_compression) %></label>
<%= 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'
require 'redmine/themes'
require 'redmine/hook'
require 'redmine/plugin'
require 'redmine/wiki_formatting'
begin
require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
......@@ -150,3 +151,7 @@ Redmine::Activity.map do |activity|
activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
activity.register :messages, :default => false
end
Redmine::WikiFormatting.map do |format|
format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
end
......@@ -148,6 +148,16 @@ module Redmine #:nodoc:
def activity_provider(*args)
Redmine::Activity.register(*args)
end
# Registers a wiki formatter.
#
# Parameters:
# * +name+ - human-readable name
# * +formatter+ - formatter class, which should have an instance method +to_html+
# * +helper+ - helper module, which will be included by wiki pages
def wiki_format_provider(name, formatter, helper)
Redmine::WikiFormatting.register(name, formatter, helper)
end
# Returns +true+ if the plugin can be configured.
def configurable?
......
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
......@@ -15,176 +15,65 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'redcloth3'
require 'coderay'
module Redmine
module WikiFormatting
private
class TextileFormatter < RedCloth3
# auto_link rule after textile rules so that it doesn't break !image_url! tags
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros]
@@formatters = {}
class << self
def map
yield self
end
def initialize(*args)
super
self.hard_breaks=true
self.no_span_caps=true
def register(name, formatter, helper)
raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name]
@@formatters[name.to_sym] = {:formatter => formatter, :helper => helper}
end
def to_html(*rules, &block)
@toc = []
@macros_runner = block
super(*RULES).to_s
def formatter_for(name)
entry = @@formatters[name.to_sym]
(entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter
end
private
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
def hard_break( text )
text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1<br />\n" ) if hard_breaks
def helper_for(name)
entry = @@formatters[name.to_sym]
(entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper
end
# Patch to add code highlighting support to RedCloth
def smooth_offtags( text )
unless @pre_list.empty?
## replace <pre> content
text.gsub!(/<redpre#(\d+)>/) do
content = @pre_list[$1.to_i]
if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
content = "<code class=\"#{$1} CodeRay\">" +
CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline)
end
content
end
end
def format_names
@@formatters.keys.map
end
# Patch to add 'table of content' support to RedCloth
def textile_p_withtoc(tag, atts, cite, content)
# removes wiki links from the item
toc_item = content.gsub(/(\[\[|\]\])/, '')
# removes styles
# eg. %{color:red}Triggers% => Triggers
toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1'
def to_html(format, text, options = {}, &block)
formatter_for(format).new(text).to_html(&block)
end
end
# Default formatter module
module NullFormatter
class Formatter
include ActionView::Helpers::TagHelper
include ActionView::Helpers::TextHelper
# replaces non word caracters by dashes
anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
unless anchor.blank?
if tag =~ /^h(\d)$/
@toc << [$1.to_i, anchor, toc_item]
end
atts << " id=\"#{anchor}\""
content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a>"
def initialize(text)
@text = text
end
textile_p(tag, atts, cite, content)
end
alias :textile_h1 :textile_p_withtoc
alias :textile_h2 :textile_p_withtoc
alias :textile_h3 :textile_p_withtoc
def inline_toc(text)
text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do
div_class = 'toc'
div_class << ' right' if $1 == '>'
div_class << ' left' if $1 == '<'
out = "<ul class=\"#{div_class}\">"
@toc.each do |heading|
level, anchor, toc_item = heading
out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n"
end
out << '</ul>'
out
def to_html(*args)
simple_format(auto_link(CGI::escapeHTML(@text)))
end
end
MACROS_RE = /
(!)? # escaping
(
\{\{ # opening tag
([\w]+) # macro name
(\(([^\}]*)\))? # optional arguments
\}\} # closing tag
)
/x unless const_defined?(:MACROS_RE)
def inline_macros(text)
text.gsub!(MACROS_RE) do
esc, all, macro = $1, $2, $3.downcase
args = ($5 || '').split(',').each(&:strip)
if esc.nil?
begin
@macros_runner.call(macro, args)
rescue => e
"<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
end || all
else
all
end
module Helper
def wikitoolbar_for(field_id)
end
end
AUTO_LINK_RE = %r{
( # leading text
<\w+.*?>| # leading HTML tag, or
[^=<>!:'"/]| # leading punctuation, or
^ # beginning of line
)
(
(?:https?://)| # protocol spec, or
(?:ftp://)|
(?:www\.) # www.*
)
(
(\S+?) # url
(\/)? # slash
)
([^\w\=\/;\(\)]*?) # post
(?=<|\s|$)
}x unless const_defined?(:AUTO_LINK_RE)
# Turns all urls into clickable links (code from Rails).
def inline_auto_link(text)
text.gsub!(AUTO_LINK_RE) do
all, leading, proto, url, post = $&, $1, $2, $3, $6
if leading =~ /<a\s/i || leading =~ /![<>=]?/
# don't replace URL's that are already linked
# and URL's prefixed with ! !> !< != (textile images)
all
else
# Idea below : an URL with unbalanced parethesis and
# ending by ')' is put into external parenthesis
if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
url=url[0..-2] # discard closing parenth from url
post = ")"+post # add closing parenth to post
end
%(#{leading}<a class="external" href="#{proto=="www."?"http://www.":proto}#{url}">#{proto + url}</a>#{post})
end
def heads_for_wiki_formatter
end
end
# Turns all email addresses into clickable links (code from Rails).
def inline_auto_mailto(text)
text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
mail = $1
if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
mail
else
%{<a href="mailto:#{mail}" class="email">#{mail}</a>}
end
def initial_page_content(page)
page.pretty_title.to_s
end
end
end
public
def self.to_html(text, options = {}, &block)
TextileFormatter.new(text).to_html(&block)
end
end
end
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'redcloth3'
require 'coderay'
module Redmine
module WikiFormatting
module Textile
class Formatter < RedCloth3
# auto_link rule after textile rules so that it doesn't break !image_url! tags
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros]
def initialize(*args)
super
self.hard_breaks=true
self.no_span_caps=true
end
def to_html(*rules, &block)
@toc = []
@macros_runner = block
super(*RULES).to_s
end
private
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
def hard_break( text )
text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1<br />\n" ) if hard_breaks
end
# Patch to add code highlighting support to RedCloth
def smooth_offtags( text )
unless @pre_list.empty?
## replace <pre> content
text.gsub!(/<redpre#(\d+)>/) do
content = @pre_list[$1.to_i]
if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
content = "<code class=\"#{$1} CodeRay\">" +
CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline)
end
content
end
end
end
# Patch to add 'table of content' support to RedCloth
def textile_p_withtoc(tag, atts, cite, content)
# removes wiki links from the item
toc_item = content.gsub(/(\[\[|\]\])/, '')
# removes styles
# eg. %{color:red}Triggers% => Triggers
toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1'
# replaces non word caracters by dashes
anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
unless anchor.blank?
if tag =~ /^h(\d)$/
@toc << [$1.to_i, anchor, toc_item]
end
atts << " id=\"#{anchor}\""
content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a>"
end
textile_p(tag, atts, cite, content)
end
alias :textile_h1 :textile_p_withtoc
alias :textile_h2 :textile_p_withtoc
alias :textile_h3 :textile_p_withtoc
def inline_toc(text)
text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do
div_class = 'toc'
div_class << ' right' if $1 == '>'
div_class << ' left' if $1 == '<'
out = "<ul class=\"#{div_class}\">"
@toc.each do |heading|
level, anchor, toc_item = heading
out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n"
end
out << '</ul>'
out
end
end
MACROS_RE = /
(!)? # escaping
(
\{\{ # opening tag
([\w]+) # macro name
(\(([^\}]*)\))? # optional arguments
\}\} # closing tag
)
/x unless const_defined?(:MACROS_RE)
def inline_macros(text)
text.gsub!(MACROS_RE) do
esc, all, macro = $1, $2, $3.downcase
args = ($5 || '').split(',').each(&:strip)
if esc.nil?
begin
@macros_runner.call(macro, args)
rescue => e
"<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
end || all
else
all
end
end
end
AUTO_LINK_RE = %r{
( # leading text
<\w+.*?>| # leading HTML tag, or
[^=<>!:'"/]| # leading punctuation, or
^ # beginning of line
)
(
(?:https?://)| # protocol spec, or
(?:ftp://)|
(?:www\.) # www.*
)
(
(\S+?) # url
(\/)? # slash
)
([^\w\=\/;\(\)]*?) # post
(?=<|\s|$)
}x unless const_defined?(:AUTO_LINK_RE)
# Turns all urls into clickable links (code from Rails).
def inline_auto_link(text)
text.gsub!(AUTO_LINK_RE) do
all, leading, proto, url, post = $&, $1, $2, $3, $6
if leading =~ /<a\s/i || leading =~ /![<>=]?/
# don't replace URL's that are already linked
# and URL's prefixed with ! !> !< != (textile images)
all
else
# Idea below : an URL with unbalanced parethesis and
# ending by ')' is put into external parenthesis
if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
url=url[0..-2] # discard closing parenth from url
post = ")"+post # add closing parenth to post
end
%(#{leading}<a class="external" href="#{proto=="www."?"http://www.":proto}#{url}">#{proto + url}</a>#{post})
end
end
end
# Turns all email addresses into clickable links (code from Rails).
def inline_auto_mailto(text)
text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
mail = $1
if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
mail
else
%{<a href="mailto:#{mail}" class="email">#{mail}</a>}
end
end
end
end
end
end
end
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module Redmine
module WikiFormatting
module Textile
module Helper
def wikitoolbar_for(field_id)
help_link = l(:setting_text_formatting) + ': ' +
link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
: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;")
javascript_include_tag('jstoolbar/jstoolbar') +
javascript_include_tag('jstoolbar/textile') +
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
end
def initial_page_content(page)
"h1. #{@page.pretty_title}"
end
def heads_for_wiki_formatter
stylesheet_link_tag 'jstoolbar'
end
end
end
end
end
......@@ -378,182 +378,3 @@ jsToolBar.prototype.resizeDragStop = function(event) {
document.removeEventListener('mousemove', this.dragMoveHdlr, false);
document.removeEventListener('mouseup', this.dragStopHdlr, false);
};
// Elements definition ------------------------------------
// strong
jsToolBar.prototype.elements.strong = {
type: 'button',
title: 'Strong',
fn: {
wiki: function() { this.singleTag('*') }
}
}
// em
jsToolBar.prototype.elements.em = {
type: 'button',
title: 'Italic',
fn: {
wiki: function() { this.singleTag("_") }
}
}
// ins
jsToolBar.prototype.elements.ins = {
type: 'button',
title: 'Underline',
fn: {
wiki: function() { this.singleTag('+') }
}
}
// del
jsToolBar.prototype.elements.del = {
type: 'button',
title: 'Deleted',
fn: {
wiki: function() { this.singleTag('-') }
}
}
// code
jsToolBar.prototype.elements.code = {
type: 'button',
title: 'Code',
fn: {
wiki: function() { this.singleTag('@') }
}
}
// spacer
jsToolBar.prototype.elements.space1 = {type: 'space'}
// headings
jsToolBar.prototype.elements.h1 = {
type: 'button',
title: 'Heading 1',
fn: {
wiki: function() {
this.encloseLineSelection('h1. ', '',function(str) {
str = str.replace(/^h\d+\.\s+/, '')
return str;
});
}
}
}
jsToolBar.prototype.elements.h2 = {
type: 'button',
title: 'Heading 2',
fn: {
wiki: function() {
this.encloseLineSelection('h2. ', '',function(str) {
str = str.replace(/^h\d+\.\s+/, '')
return str;
});
}
}
}
jsToolBar.prototype.elements.h3 = {
type: 'button',
title: 'Heading 3',
fn: {
wiki: function() {
this.encloseLineSelection('h3. ', '',function(str) {
str = str.replace(/^h\d+\.\s+/, '')
return str;
});
}
}
}
// spacer
jsToolBar.prototype.elements.space2 = {type: 'space'}
// ul
jsToolBar.prototype.elements.ul = {
type: 'button',
title: 'Unordered list',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^)[#-]?\s*/g,"$1* ");
});
}
}
}