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 |
} |
|