@@ -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):"(.+?)"/) 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 | 5 | gem "coderay", "~> 1.1.0" |
|
6 | 6 | gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby] |
|
7 | 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 | 11 | # Optional gem for LDAP authentication |
|
10 | 12 | group :ldap do |
@@ -267,6 +267,8 end | |||
|
267 | 267 | |
|
268 | 268 | Redmine::WikiFormatting.map do |format| |
|
269 | 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 | 272 | end |
|
271 | 273 | |
|
272 | 274 | ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler |
General Comments 0
You need to be logged in to leave comments.
Login now