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