##// END OF EJS Templates
Adds experimental support for Markdown formatting with redcarpet (#15520)....
Jean-Philippe Lang -
r12177:471e01ca5059
parent child
Show More
@@ -0,0 +1,136
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 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 'cgi'
19
20 module Redmine
21 module WikiFormatting
22 module Markdown
23 class HTML < Redcarpet::Render::HTML
24 include ActionView::Helpers::TagHelper
25
26 def link(link, title, content)
27 css = nil
28 unless link && link.starts_with?('/')
29 css = 'external'
30 end
31 content_tag('a', content.html_safe, :href => link, :title => title, :class => css)
32 end
33
34 def block_code(code, language)
35 if language.present?
36 "<pre><code class=\"#{CGI.escapeHTML language} syntaxhl\">" +
37 Redmine::SyntaxHighlighting.highlight_by_language(code, language) +
38 "</code></pre>"
39 else
40 "<pre>" + CGI.escapeHTML(code) + "</pre>"
41 end
42 end
43 end
44
45 class Formatter
46 def initialize(text)
47 @text = text
48 end
49
50 def to_html(*args)
51 html = formatter.render(@text)
52 # restore wiki links eg. [[Foo]]
53 html.gsub!(%r{\[<a href="(.*?)">(.*?)</a>\]}) do
54 "[[#{$2}]]"
55 end
56 # restore Redmine links with double-quotes, eg. version:"1.0"
57 html.gsub!(/(\w):&quot;(.+?)&quot;/) do
58 "#{$1}:\"#{$2}\""
59 end
60 html
61 end
62
63 def get_section(index)
64 section = extract_sections(index)[1]
65 hash = Digest::MD5.hexdigest(section)
66 return section, hash
67 end
68
69 def update_section(index, update, hash=nil)
70 t = extract_sections(index)
71 if hash.present? && hash != Digest::MD5.hexdigest(t[1])
72 raise Redmine::WikiFormatting::StaleSectionError
73 end
74 t[1] = update unless t[1].blank?
75 t.reject(&:blank?).join "\n\n"
76 end
77
78 def extract_sections(index)
79 sections = ['', '', '']
80 offset = 0
81 i = 0
82 l = 1
83 inside_pre = false
84 @text.split(/(^(?:.+\r?\n\r?(?:\=+|\-+)|#+.+|~~~.*)\s*$)/).each do |part|
85 level = nil
86 if part =~ /\A~{3,}(\S+)?\s*$/
87 if $1
88 if !inside_pre
89 inside_pre = true
90 end
91 else
92 inside_pre = !inside_pre
93 end
94 elsif inside_pre
95 # nop
96 elsif part =~ /\A(#+).+/
97 level = $1.size
98 elsif part =~ /\A.+\r?\n\r?(\=+|\-+)\s*$/
99 level = $1.include?('=') ? 1 : 2
100 end
101 if level
102 i += 1
103 if offset == 0 && i == index
104 # entering the requested section
105 offset = 1
106 l = level
107 elsif offset == 1 && i > index && level <= l
108 # leaving the requested section
109 offset = 2
110 end
111 end
112 sections[offset] << part
113 end
114 sections.map(&:strip)
115 end
116
117 private
118
119 def formatter
120 @@formatter ||= Redcarpet::Markdown.new(
121 Redmine::WikiFormatting::Markdown::HTML.new(
122 :filter_html => true,
123 :hard_wrap => true
124 ),
125 :autolink => true,
126 :fenced_code_blocks => true,
127 :space_after_headers => true,
128 :tables => true,
129 :strikethrough => true,
130 :superscript => true
131 )
132 end
133 end
134 end
135 end
136 end
@@ -0,0 +1,45
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 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 Markdown
21 module Helper
22 def wikitoolbar_for(field_id)
23 heads_for_wiki_formatter
24 javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.draw();")
25 end
26
27 def initial_page_content(page)
28 "# #{@page.pretty_title}"
29 end
30
31 def heads_for_wiki_formatter
32 unless @heads_for_wiki_formatter_included
33 content_for :header_tags do
34 javascript_include_tag('jstoolbar/jstoolbar') +
35 javascript_include_tag('jstoolbar/markdown') +
36 javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language.to_s.downcase}") +
37 stylesheet_link_tag('jstoolbar')
38 end
39 @heads_for_wiki_formatter_included = true
40 end
41 end
42 end
43 end
44 end
45 end
@@ -0,0 +1,194
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 markdown 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 // del
44 jsToolBar.prototype.elements.del = {
45 type: 'button',
46 title: 'Deleted',
47 fn: {
48 wiki: function() { this.singleTag('~~') }
49 }
50 }
51
52 // code
53 jsToolBar.prototype.elements.code = {
54 type: 'button',
55 title: 'Code',
56 fn: {
57 wiki: function() { this.singleTag('`') }
58 }
59 }
60
61 // spacer
62 jsToolBar.prototype.elements.space1 = {type: 'space'}
63
64 // headings
65 jsToolBar.prototype.elements.h1 = {
66 type: 'button',
67 title: 'Heading 1',
68 fn: {
69 wiki: function() {
70 this.encloseLineSelection('# ', '',function(str) {
71 str = str.replace(/^#+\s+/, '')
72 return str;
73 });
74 }
75 }
76 }
77 jsToolBar.prototype.elements.h2 = {
78 type: 'button',
79 title: 'Heading 2',
80 fn: {
81 wiki: function() {
82 this.encloseLineSelection('## ', '',function(str) {
83 str = str.replace(/^#+\s+/, '')
84 return str;
85 });
86 }
87 }
88 }
89 jsToolBar.prototype.elements.h3 = {
90 type: 'button',
91 title: 'Heading 3',
92 fn: {
93 wiki: function() {
94 this.encloseLineSelection('### ', '',function(str) {
95 str = str.replace(/^#+\s+/, '')
96 return str;
97 });
98 }
99 }
100 }
101
102 // spacer
103 jsToolBar.prototype.elements.space2 = {type: 'space'}
104
105 // ul
106 jsToolBar.prototype.elements.ul = {
107 type: 'button',
108 title: 'Unordered list',
109 fn: {
110 wiki: function() {
111 this.encloseLineSelection('','',function(str) {
112 str = str.replace(/\r/g,'');
113 return str.replace(/(\n|^)[#-]?\s*/g,"$1* ");
114 });
115 }
116 }
117 }
118
119 // ol
120 jsToolBar.prototype.elements.ol = {
121 type: 'button',
122 title: 'Ordered list',
123 fn: {
124 wiki: function() {
125 this.encloseLineSelection('','',function(str) {
126 str = str.replace(/\r/g,'');
127 return str.replace(/(\n|^)[*-]?\s*/g,"$11. ");
128 });
129 }
130 }
131 }
132
133 // spacer
134 jsToolBar.prototype.elements.space3 = {type: 'space'}
135
136 // bq
137 jsToolBar.prototype.elements.bq = {
138 type: 'button',
139 title: 'Quote',
140 fn: {
141 wiki: function() {
142 this.encloseLineSelection('','',function(str) {
143 str = str.replace(/\r/g,'');
144 return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2");
145 });
146 }
147 }
148 }
149
150 // unbq
151 jsToolBar.prototype.elements.unbq = {
152 type: 'button',
153 title: 'Unquote',
154 fn: {
155 wiki: function() {
156 this.encloseLineSelection('','',function(str) {
157 str = str.replace(/\r/g,'');
158 return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2");
159 });
160 }
161 }
162 }
163
164 // pre
165 jsToolBar.prototype.elements.pre = {
166 type: 'button',
167 title: 'Preformatted text',
168 fn: {
169 wiki: function() { this.encloseLineSelection('~~~\n', '\n~~~') }
170 }
171 }
172
173 // spacer
174 jsToolBar.prototype.elements.space4 = {type: 'space'}
175
176 // wiki page
177 jsToolBar.prototype.elements.link = {
178 type: 'button',
179 title: 'Wiki link',
180 fn: {
181 wiki: function() { this.encloseSelection("[[", "]]") }
182 }
183 }
184 // image
185 jsToolBar.prototype.elements.img = {
186 type: 'button',
187 title: 'Image',
188 fn: {
189 wiki: function() { this.encloseSelection("![](", ")") }
190 }
191 }
192
193 // spacer
194 jsToolBar.prototype.elements.space5 = {type: 'space'}
@@ -0,0 +1,62
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 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 File.expand_path('../../../../../test_helper', __FILE__)
19
20 class Redmine::WikiFormatting::MarkdownFormatterTest < ActionView::TestCase
21 include ApplicationHelper
22
23 def setup
24 @formatter = Redmine::WikiFormatting::Markdown::Formatter
25 end
26
27 def test_inline_style
28 assert_equal "<p><strong>foo</strong></p>", @formatter.new("**foo**").to_html.strip
29 end
30
31 def test_wiki_links_should_be_preserved
32 text = 'This is a wiki link: [[Foo]]'
33 assert_include '[[Foo]]', @formatter.new(text).to_html
34 end
35
36 def test_redmine_links_with_double_quotes_should_be_preserved
37 text = 'This is a redmine link: version:"1.0"'
38 assert_include 'version:"1.0"', @formatter.new(text).to_html
39 end
40
41 def test_should_support_syntax_highligth
42 text = <<-STR
43 ~~~ruby
44 def foo
45 end
46 ~~~
47 STR
48 assert_select_in @formatter.new(text).to_html, 'pre code.ruby.syntaxhl' do
49 assert_select 'span.keyword', :text => 'def'
50 end
51 end
52
53 def test_external_links_should_have_external_css_class
54 text = 'This is a [link](http://example.net/)'
55 assert_equal '<p>This is a <a class="external" href="http://example.net/">link</a></p>', @formatter.new(text).to_html.strip
56 end
57
58 def test_locals_links_should_not_have_external_css_class
59 text = 'This is a [link](/issues)'
60 assert_equal '<p>This is a <a href="/issues">link</a></p>', @formatter.new(text).to_html.strip
61 end
62 end
@@ -5,6 +5,8 gem "jquery-rails", "~> 2.0.2"
5 gem "coderay", "~> 1.1.0"
5 gem "coderay", "~> 1.1.0"
6 gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
6 gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
7 gem "builder", "3.0.0"
7 gem "builder", "3.0.0"
8 # TODO: upgrade to redcarpet 3.x when ruby1.8 support is dropped
9 gem "redcarpet", "~> 2.3.0"
8
10
9 # Optional gem for LDAP authentication
11 # Optional gem for LDAP authentication
10 group :ldap do
12 group :ldap do
@@ -267,6 +267,8 end
267
267
268 Redmine::WikiFormatting.map do |format|
268 Redmine::WikiFormatting.map do |format|
269 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
269 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
270 format.register :markdown, Redmine::WikiFormatting::Markdown::Formatter, Redmine::WikiFormatting::Markdown::Helper,
271 :label => 'Markdown (experimental)'
270 end
272 end
271
273
272 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
274 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
General Comments 0
You need to be logged in to leave comments. Login now