git-svn-id: http://svn.redmine.org/redmine/trunk@12452 e93f8b46-1217-0410-a6f0-8f06a7374b81tags/2.5.0
@@ -5,6 +5,8 @@ gem "jquery-rails", "~> 2.0.2" | |||
gem "coderay", "~> 1.1.0" | |||
gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby] | |||
gem "builder", "3.0.0" | |||
# TODO: upgrade to redcarpet 3.x when ruby1.8 support is dropped | |||
gem "redcarpet", "~> 2.3.0" | |||
# Optional gem for LDAP authentication | |||
group :ldap do |
@@ -267,6 +267,8 @@ end | |||
Redmine::WikiFormatting.map do |format| | |||
format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper | |||
format.register :markdown, Redmine::WikiFormatting::Markdown::Formatter, Redmine::WikiFormatting::Markdown::Helper, | |||
:label => 'Markdown (experimental)' | |||
end | |||
ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler |
@@ -0,0 +1,136 @@ | |||
# Redmine - project management software | |||
# Copyright (C) 2006-2013 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 'cgi' | |||
module Redmine | |||
module WikiFormatting | |||
module Markdown | |||
class HTML < Redcarpet::Render::HTML | |||
include ActionView::Helpers::TagHelper | |||
def link(link, title, content) | |||
css = nil | |||
unless link && link.starts_with?('/') | |||
css = 'external' | |||
end | |||
content_tag('a', content.html_safe, :href => link, :title => title, :class => css) | |||
end | |||
def block_code(code, language) | |||
if language.present? | |||
"<pre><code class=\"#{CGI.escapeHTML language} syntaxhl\">" + | |||
Redmine::SyntaxHighlighting.highlight_by_language(code, language) + | |||
"</code></pre>" | |||
else | |||
"<pre>" + CGI.escapeHTML(code) + "</pre>" | |||
end | |||
end | |||
end | |||
class Formatter | |||
def initialize(text) | |||
@text = text | |||
end | |||
def to_html(*args) | |||
html = formatter.render(@text) | |||
# restore wiki links eg. [[Foo]] | |||
html.gsub!(%r{\[<a href="(.*?)">(.*?)</a>\]}) do | |||
"[[#{$2}]]" | |||
end | |||
# restore Redmine links with double-quotes, eg. version:"1.0" | |||
html.gsub!(/(\w):"(.+?)"/) do | |||
"#{$1}:\"#{$2}\"" | |||
end | |||
html | |||
end | |||
def get_section(index) | |||
section = extract_sections(index)[1] | |||
hash = Digest::MD5.hexdigest(section) | |||
return section, hash | |||
end | |||
def update_section(index, update, hash=nil) | |||
t = extract_sections(index) | |||
if hash.present? && hash != Digest::MD5.hexdigest(t[1]) | |||
raise Redmine::WikiFormatting::StaleSectionError | |||
end | |||
t[1] = update unless t[1].blank? | |||
t.reject(&:blank?).join "\n\n" | |||
end | |||
def extract_sections(index) | |||
sections = ['', '', ''] | |||
offset = 0 | |||
i = 0 | |||
l = 1 | |||
inside_pre = false | |||
@text.split(/(^(?:.+\r?\n\r?(?:\=+|\-+)|#+.+|~~~.*)\s*$)/).each do |part| | |||
level = nil | |||
if part =~ /\A~{3,}(\S+)?\s*$/ | |||
if $1 | |||
if !inside_pre | |||
inside_pre = true | |||
end | |||
else | |||
inside_pre = !inside_pre | |||
end | |||
elsif inside_pre | |||
# nop | |||
elsif part =~ /\A(#+).+/ | |||
level = $1.size | |||
elsif part =~ /\A.+\r?\n\r?(\=+|\-+)\s*$/ | |||
level = $1.include?('=') ? 1 : 2 | |||
end | |||
if level | |||
i += 1 | |||
if offset == 0 && i == index | |||
# entering the requested section | |||
offset = 1 | |||
l = level | |||
elsif offset == 1 && i > index && level <= l | |||
# leaving the requested section | |||
offset = 2 | |||
end | |||
end | |||
sections[offset] << part | |||
end | |||
sections.map(&:strip) | |||
end | |||
private | |||
def formatter | |||
@@formatter ||= Redcarpet::Markdown.new( | |||
Redmine::WikiFormatting::Markdown::HTML.new( | |||
:filter_html => true, | |||
:hard_wrap => true | |||
), | |||
:autolink => true, | |||
:fenced_code_blocks => true, | |||
:space_after_headers => true, | |||
:tables => true, | |||
:strikethrough => true, | |||
:superscript => true | |||
) | |||
end | |||
end | |||
end | |||
end | |||
end |
@@ -0,0 +1,45 @@ | |||
# Redmine - project management software | |||
# Copyright (C) 2006-2013 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 Markdown | |||
module Helper | |||
def wikitoolbar_for(field_id) | |||
heads_for_wiki_formatter | |||
javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.draw();") | |||
end | |||
def initial_page_content(page) | |||
"# #{@page.pretty_title}" | |||
end | |||
def heads_for_wiki_formatter | |||
unless @heads_for_wiki_formatter_included | |||
content_for :header_tags do | |||
javascript_include_tag('jstoolbar/jstoolbar') + | |||
javascript_include_tag('jstoolbar/markdown') + | |||
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language.to_s.downcase}") + | |||
stylesheet_link_tag('jstoolbar') | |||
end | |||
@heads_for_wiki_formatter_included = true | |||
end | |||
end | |||
end | |||
end | |||
end | |||
end |
@@ -0,0 +1,194 @@ | |||
/* ***** BEGIN LICENSE BLOCK ***** | |||
* This file is part of DotClear. | |||
* Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All | |||
* rights reserved. | |||
* | |||
* DotClear 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. | |||
* | |||
* DotClear 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 DotClear; if not, write to the Free Software | |||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |||
* | |||
* ***** END LICENSE BLOCK ***** | |||
*/ | |||
/* Modified by JP LANG for markdown formatting */ | |||
// 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("*") } | |||
} | |||
} | |||
// 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('# ', '',function(str) { | |||
str = str.replace(/^#+\s+/, '') | |||
return str; | |||
}); | |||
} | |||
} | |||
} | |||
jsToolBar.prototype.elements.h2 = { | |||
type: 'button', | |||
title: 'Heading 2', | |||
fn: { | |||
wiki: function() { | |||
this.encloseLineSelection('## ', '',function(str) { | |||
str = str.replace(/^#+\s+/, '') | |||
return str; | |||
}); | |||
} | |||
} | |||
} | |||
jsToolBar.prototype.elements.h3 = { | |||
type: 'button', | |||
title: 'Heading 3', | |||
fn: { | |||
wiki: function() { | |||
this.encloseLineSelection('### ', '',function(str) { | |||
str = str.replace(/^#+\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* "); | |||
}); | |||
} | |||
} | |||
} | |||
// ol | |||
jsToolBar.prototype.elements.ol = { | |||
type: 'button', | |||
title: 'Ordered list', | |||
fn: { | |||
wiki: function() { | |||
this.encloseLineSelection('','',function(str) { | |||
str = str.replace(/\r/g,''); | |||
return str.replace(/(\n|^)[*-]?\s*/g,"$11. "); | |||
}); | |||
} | |||
} | |||
} | |||
// spacer | |||
jsToolBar.prototype.elements.space3 = {type: 'space'} | |||
// bq | |||
jsToolBar.prototype.elements.bq = { | |||
type: 'button', | |||
title: 'Quote', | |||
fn: { | |||
wiki: function() { | |||
this.encloseLineSelection('','',function(str) { | |||
str = str.replace(/\r/g,''); | |||
return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2"); | |||
}); | |||
} | |||
} | |||
} | |||
// unbq | |||
jsToolBar.prototype.elements.unbq = { | |||
type: 'button', | |||
title: 'Unquote', | |||
fn: { | |||
wiki: function() { | |||
this.encloseLineSelection('','',function(str) { | |||
str = str.replace(/\r/g,''); | |||
return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2"); | |||
}); | |||
} | |||
} | |||
} | |||
// pre | |||
jsToolBar.prototype.elements.pre = { | |||
type: 'button', | |||
title: 'Preformatted text', | |||
fn: { | |||
wiki: function() { this.encloseLineSelection('~~~\n', '\n~~~') } | |||
} | |||
} | |||
// spacer | |||
jsToolBar.prototype.elements.space4 = {type: 'space'} | |||
// wiki page | |||
jsToolBar.prototype.elements.link = { | |||
type: 'button', | |||
title: 'Wiki link', | |||
fn: { | |||
wiki: function() { this.encloseSelection("[[", "]]") } | |||
} | |||
} | |||
// image | |||
jsToolBar.prototype.elements.img = { | |||
type: 'button', | |||
title: 'Image', | |||
fn: { | |||
wiki: function() { this.encloseSelection("![](", ")") } | |||
} | |||
} | |||
// spacer | |||
jsToolBar.prototype.elements.space5 = {type: 'space'} |
@@ -0,0 +1,62 @@ | |||
# Redmine - project management software | |||
# Copyright (C) 2006-2013 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 File.expand_path('../../../../../test_helper', __FILE__) | |||
class Redmine::WikiFormatting::MarkdownFormatterTest < ActionView::TestCase | |||
include ApplicationHelper | |||
def setup | |||
@formatter = Redmine::WikiFormatting::Markdown::Formatter | |||
end | |||
def test_inline_style | |||
assert_equal "<p><strong>foo</strong></p>", @formatter.new("**foo**").to_html.strip | |||
end | |||
def test_wiki_links_should_be_preserved | |||
text = 'This is a wiki link: [[Foo]]' | |||
assert_include '[[Foo]]', @formatter.new(text).to_html | |||
end | |||
def test_redmine_links_with_double_quotes_should_be_preserved | |||
text = 'This is a redmine link: version:"1.0"' | |||
assert_include 'version:"1.0"', @formatter.new(text).to_html | |||
end | |||
def test_should_support_syntax_highligth | |||
text = <<-STR | |||
~~~ruby | |||
def foo | |||
end | |||
~~~ | |||
STR | |||
assert_select_in @formatter.new(text).to_html, 'pre code.ruby.syntaxhl' do | |||
assert_select 'span.keyword', :text => 'def' | |||
end | |||
end | |||
def test_external_links_should_have_external_css_class | |||
text = 'This is a [link](http://example.net/)' | |||
assert_equal '<p>This is a <a class="external" href="http://example.net/">link</a></p>', @formatter.new(text).to_html.strip | |||
end | |||
def test_locals_links_should_not_have_external_css_class | |||
text = 'This is a [link](/issues)' | |||
assert_equal '<p>This is a <a href="/issues">link</a></p>', @formatter.new(text).to_html.strip | |||
end | |||
end |