##// END OF EJS Templates
markdown: enable full githubFlavoredMarkdown
marcink -
r317:2f438a3b default
parent child Browse files
Show More
@@ -1,105 +1,106 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import re
22
22
23 import markdown
23 import markdown
24
24
25 from mdx_gfm import GithubFlavoredMarkdownExtension # noqa
26
25
27
26 class FlavoredCheckboxExtension(markdown.Extension):
28 class FlavoredCheckboxExtension(markdown.Extension):
27
29
28 def extendMarkdown(self, md, md_globals):
30 def extendMarkdown(self, md, md_globals):
29 md.preprocessors.add('checklist',
31 md.preprocessors.add('checklist',
30 FlavoredCheckboxPreprocessor(md), '<reference')
32 FlavoredCheckboxPreprocessor(md), '<reference')
31 md.postprocessors.add('checklist',
33 md.postprocessors.add('checklist',
32 FlavoredCheckboxPostprocessor(md), '>unescape')
34 FlavoredCheckboxPostprocessor(md), '>unescape')
33
35
34
36
35 class FlavoredCheckboxPreprocessor(markdown.preprocessors.Preprocessor):
37 class FlavoredCheckboxPreprocessor(markdown.preprocessors.Preprocessor):
36 """
38 """
37 Replaces occurrences of [ ] or [x] to checkbox input
39 Replaces occurrences of [ ] or [x] to checkbox input
38 """
40 """
39
41
40 pattern = re.compile(r'^([*-]) \[([ x])\]')
42 pattern = re.compile(r'^([*-]) \[([ x])\]')
41
43
42 def run(self, lines):
44 def run(self, lines):
43 return [self._transform_line(line) for line in lines]
45 return [self._transform_line(line) for line in lines]
44
46
45 def _transform_line(self, line):
47 def _transform_line(self, line):
46 return self.pattern.sub(self._replacer, line)
48 return self.pattern.sub(self._replacer, line)
47
49
48 def _replacer(self, match):
50 def _replacer(self, match):
49 list_prefix, state = match.groups()
51 list_prefix, state = match.groups()
50 checked = '' if state == ' ' else ' checked="checked"'
52 checked = '' if state == ' ' else ' checked="checked"'
51 return '%s <input type="checkbox" disabled="disabled"%s>' % (list_prefix,
53 return '%s <input type="checkbox" disabled="disabled"%s>' % (list_prefix,
52 checked)
54 checked)
53
55
54
56
55 class FlavoredCheckboxPostprocessor(markdown.postprocessors.Postprocessor):
57 class FlavoredCheckboxPostprocessor(markdown.postprocessors.Postprocessor):
56 """
58 """
57 Adds `flavored_checkbox_list` class to list of checkboxes
59 Adds `flavored_checkbox_list` class to list of checkboxes
58 """
60 """
59
61
60 pattern = re.compile(r'^([*-]) \[([ x])\]')
62 pattern = re.compile(r'^([*-]) \[([ x])\]')
61
63
62 def run(self, html):
64 def run(self, html):
63 before = '<ul>\n<li><input type="checkbox"'
65 before = '<ul>\n<li><input type="checkbox"'
64 after = '<ul class="flavored_checkbox_list">\n<li><input type="checkbox"'
66 after = '<ul class="flavored_checkbox_list">\n<li><input type="checkbox"'
65 return html.replace(before, after)
67 return html.replace(before, after)
66
68
67
69
68
69
70 # Global Vars
70 # Global Vars
71 URLIZE_RE = '(%s)' % '|'.join([
71 URLIZE_RE = '(%s)' % '|'.join([
72 r'<(?:f|ht)tps?://[^>]*>',
72 r'<(?:f|ht)tps?://[^>]*>',
73 r'\b(?:f|ht)tps?://[^)<>\s]+[^.,)<>\s]',
73 r'\b(?:f|ht)tps?://[^)<>\s]+[^.,)<>\s]',
74 r'\bwww\.[^)<>\s]+[^.,)<>\s]',
74 r'\bwww\.[^)<>\s]+[^.,)<>\s]',
75 r'[^(<\s]+\.(?:com|net|org)\b',
75 r'[^(<\s]+\.(?:com|net|org)\b',
76 ])
76 ])
77
77
78
78 class UrlizePattern(markdown.inlinepatterns.Pattern):
79 class UrlizePattern(markdown.inlinepatterns.Pattern):
79 """ Return a link Element given an autolink (`http://example/com`). """
80 """ Return a link Element given an autolink (`http://example/com`). """
80 def handleMatch(self, m):
81 def handleMatch(self, m):
81 url = m.group(2)
82 url = m.group(2)
82
83
83 if url.startswith('<'):
84 if url.startswith('<'):
84 url = url[1:-1]
85 url = url[1:-1]
85
86
86 text = url
87 text = url
87
88
88 if not url.split('://')[0] in ('http','https','ftp'):
89 if not url.split('://')[0] in ('http','https','ftp'):
89 if '@' in url and not '/' in url:
90 if '@' in url and not '/' in url:
90 url = 'mailto:' + url
91 url = 'mailto:' + url
91 else:
92 else:
92 url = 'http://' + url
93 url = 'http://' + url
93
94
94 el = markdown.util.etree.Element("a")
95 el = markdown.util.etree.Element("a")
95 el.set('href', url)
96 el.set('href', url)
96 el.text = markdown.util.AtomicString(text)
97 el.text = markdown.util.AtomicString(text)
97 return el
98 return el
98
99
99
100
100 class UrlizeExtension(markdown.Extension):
101 class UrlizeExtension(markdown.Extension):
101 """ Urlize Extension for Python-Markdown. """
102 """ Urlize Extension for Python-Markdown. """
102
103
103 def extendMarkdown(self, md, md_globals):
104 def extendMarkdown(self, md, md_globals):
104 """ Replace autolink with UrlizePattern """
105 """ Replace autolink with UrlizePattern """
105 md.inlinePatterns['autolink'] = UrlizePattern(URLIZE_RE, md)
106 md.inlinePatterns['autolink'] = UrlizePattern(URLIZE_RE, md)
@@ -1,231 +1,230 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Renderer for markup languages with ability to parse using rst or markdown
23 Renderer for markup languages with ability to parse using rst or markdown
24 """
24 """
25
25
26
26
27 import re
27 import re
28 import os
28 import os
29 import logging
29 import logging
30 from mako.lookup import TemplateLookup
30 from mako.lookup import TemplateLookup
31
31
32 from docutils.core import publish_parts
32 from docutils.core import publish_parts
33 from docutils.parsers.rst import directives
33 from docutils.parsers.rst import directives
34 import markdown
34 import markdown
35
35
36 from rhodecode.lib.markdown_ext import FlavoredCheckboxExtension, UrlizeExtension
36 from rhodecode.lib.markdown_ext import (
37 UrlizeExtension, GithubFlavoredMarkdownExtension)
37 from rhodecode.lib.utils2 import safe_unicode, md5_safe, MENTIONS_REGEX
38 from rhodecode.lib.utils2 import safe_unicode, md5_safe, MENTIONS_REGEX
38
39
39 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
40
41
41 # default renderer used to generate automated comments
42 # default renderer used to generate automated comments
42 DEFAULT_COMMENTS_RENDERER = 'rst'
43 DEFAULT_COMMENTS_RENDERER = 'rst'
43
44
44
45
45 class MarkupRenderer(object):
46 class MarkupRenderer(object):
46 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
47 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
47
48
48 MARKDOWN_PAT = re.compile(r'\.(md|mkdn?|mdown|markdown)$', re.IGNORECASE)
49 MARKDOWN_PAT = re.compile(r'\.(md|mkdn?|mdown|markdown)$', re.IGNORECASE)
49 RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE)
50 RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE)
50 PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE)
51 PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE)
51
52
52 def _detect_renderer(self, source, filename=None):
53 def _detect_renderer(self, source, filename=None):
53 """
54 """
54 runs detection of what renderer should be used for generating html
55 runs detection of what renderer should be used for generating html
55 from a markup language
56 from a markup language
56
57
57 filename can be also explicitly a renderer name
58 filename can be also explicitly a renderer name
58
59
59 :param source:
60 :param source:
60 :param filename:
61 :param filename:
61 """
62 """
62
63
63 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
64 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
64 detected_renderer = 'markdown'
65 detected_renderer = 'markdown'
65 elif MarkupRenderer.RST_PAT.findall(filename):
66 elif MarkupRenderer.RST_PAT.findall(filename):
66 detected_renderer = 'rst'
67 detected_renderer = 'rst'
67 elif MarkupRenderer.PLAIN_PAT.findall(filename):
68 elif MarkupRenderer.PLAIN_PAT.findall(filename):
68 detected_renderer = 'rst'
69 detected_renderer = 'rst'
69 else:
70 else:
70 detected_renderer = 'plain'
71 detected_renderer = 'plain'
71
72
72 return getattr(MarkupRenderer, detected_renderer)
73 return getattr(MarkupRenderer, detected_renderer)
73
74
74 def render(self, source, filename=None):
75 def render(self, source, filename=None):
75 """
76 """
76 Renders a given filename using detected renderer
77 Renders a given filename using detected renderer
77 it detects renderers based on file extension or mimetype.
78 it detects renderers based on file extension or mimetype.
78 At last it will just do a simple html replacing new lines with <br/>
79 At last it will just do a simple html replacing new lines with <br/>
79
80
80 :param file_name:
81 :param file_name:
81 :param source:
82 :param source:
82 """
83 """
83
84
84 renderer = self._detect_renderer(source, filename)
85 renderer = self._detect_renderer(source, filename)
85 readme_data = renderer(source)
86 readme_data = renderer(source)
86 return readme_data
87 return readme_data
87
88
88 @classmethod
89 @classmethod
89 def _flavored_markdown(cls, text):
90 def _flavored_markdown(cls, text):
90 """
91 """
91 Github style flavored markdown
92 Github style flavored markdown
92
93
93 :param text:
94 :param text:
94 """
95 """
95
96
96 # Extract pre blocks.
97 # Extract pre blocks.
97 extractions = {}
98 extractions = {}
98
99
99 def pre_extraction_callback(matchobj):
100 def pre_extraction_callback(matchobj):
100 digest = md5_safe(matchobj.group(0))
101 digest = md5_safe(matchobj.group(0))
101 extractions[digest] = matchobj.group(0)
102 extractions[digest] = matchobj.group(0)
102 return "{gfm-extraction-%s}" % digest
103 return "{gfm-extraction-%s}" % digest
103 pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
104 pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
104 text = re.sub(pattern, pre_extraction_callback, text)
105 text = re.sub(pattern, pre_extraction_callback, text)
105
106
106 # Prevent foo_bar_baz from ending up with an italic word in the middle.
107 # Prevent foo_bar_baz from ending up with an italic word in the middle.
107 def italic_callback(matchobj):
108 def italic_callback(matchobj):
108 s = matchobj.group(0)
109 s = matchobj.group(0)
109 if list(s).count('_') >= 2:
110 if list(s).count('_') >= 2:
110 return s.replace('_', r'\_')
111 return s.replace('_', r'\_')
111 return s
112 return s
112 text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text)
113 text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text)
113
114
114 # Insert pre block extractions.
115 # Insert pre block extractions.
115 def pre_insert_callback(matchobj):
116 def pre_insert_callback(matchobj):
116 return '\n\n' + extractions[matchobj.group(1)]
117 return '\n\n' + extractions[matchobj.group(1)]
117 text = re.sub(r'\{gfm-extraction-([0-9a-f]{32})\}',
118 text = re.sub(r'\{gfm-extraction-([0-9a-f]{32})\}',
118 pre_insert_callback, text)
119 pre_insert_callback, text)
119
120
120 return text
121 return text
121
122
122 @classmethod
123 @classmethod
123 def urlify_text(cls, text):
124 def urlify_text(cls, text):
124 url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
125 url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
125 r'|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
126 r'|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
126
127
127 def url_func(match_obj):
128 def url_func(match_obj):
128 url_full = match_obj.groups()[0]
129 url_full = match_obj.groups()[0]
129 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
130 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
130
131
131 return url_pat.sub(url_func, text)
132 return url_pat.sub(url_func, text)
132
133
133 @classmethod
134 @classmethod
134 def plain(cls, source, universal_newline=True):
135 def plain(cls, source, universal_newline=True):
135 source = safe_unicode(source)
136 source = safe_unicode(source)
136 if universal_newline:
137 if universal_newline:
137 newline = '\n'
138 newline = '\n'
138 source = newline.join(source.splitlines())
139 source = newline.join(source.splitlines())
139
140
140 source = cls.urlify_text(source)
141 source = cls.urlify_text(source)
141 return '<br />' + source.replace("\n", '<br />')
142 return '<br />' + source.replace("\n", '<br />')
142
143
143 @classmethod
144 @classmethod
144 def markdown(cls, source, safe=True, flavored=False, mentions=False):
145 def markdown(cls, source, safe=True, flavored=False, mentions=False):
145 # It does not allow to insert inline HTML. In presence of HTML tags, it
146 # It does not allow to insert inline HTML. In presence of HTML tags, it
146 # will replace them instead with [HTML_REMOVED]. This is controlled by
147 # will replace them instead with [HTML_REMOVED]. This is controlled by
147 # the safe_mode=True parameter of the markdown method.
148 # the safe_mode=True parameter of the markdown method.
148 extensions = ['codehilite', 'extra', 'def_list', 'sane_lists']
149 extensions = ['codehilite', 'extra', 'def_list', 'sane_lists']
149 if flavored:
150 if flavored:
150 extensions.append('nl2br')
151 extensions.append(GithubFlavoredMarkdownExtension())
151 extensions.append(FlavoredCheckboxExtension())
152 extensions.append(UrlizeExtension())
153
152
154 if mentions:
153 if mentions:
155 mention_pat = re.compile(MENTIONS_REGEX)
154 mention_pat = re.compile(MENTIONS_REGEX)
156
155
157 def wrapp(match_obj):
156 def wrapp(match_obj):
158 uname = match_obj.groups()[0]
157 uname = match_obj.groups()[0]
159 return ' **@%(uname)s** ' % {'uname': uname}
158 return ' **@%(uname)s** ' % {'uname': uname}
160 mention_hl = mention_pat.sub(wrapp, source).strip()
159 mention_hl = mention_pat.sub(wrapp, source).strip()
161 # we extracted mentions render with this using Mentions false
160 # we extracted mentions render with this using Mentions false
162 return cls.markdown(mention_hl, safe=safe, flavored=flavored,
161 return cls.markdown(mention_hl, safe=safe, flavored=flavored,
163 mentions=False)
162 mentions=False)
164
163
165 source = safe_unicode(source)
164 source = safe_unicode(source)
166 try:
165 try:
167 if flavored:
166 if flavored:
168 source = cls._flavored_markdown(source)
167 source = cls._flavored_markdown(source)
169 return markdown.markdown(
168 return markdown.markdown(
170 source, extensions, safe_mode=True, enable_attributes=False)
169 source, extensions, safe_mode=True, enable_attributes=False)
171 except Exception:
170 except Exception:
172 log.exception('Error when rendering Markdown')
171 log.exception('Error when rendering Markdown')
173 if safe:
172 if safe:
174 log.debug('Fallbacking to render in plain mode')
173 log.debug('Fallbacking to render in plain mode')
175 return cls.plain(source)
174 return cls.plain(source)
176 else:
175 else:
177 raise
176 raise
178
177
179 @classmethod
178 @classmethod
180 def rst(cls, source, safe=True, mentions=False):
179 def rst(cls, source, safe=True, mentions=False):
181 if mentions:
180 if mentions:
182 mention_pat = re.compile(MENTIONS_REGEX)
181 mention_pat = re.compile(MENTIONS_REGEX)
183
182
184 def wrapp(match_obj):
183 def wrapp(match_obj):
185 uname = match_obj.groups()[0]
184 uname = match_obj.groups()[0]
186 return ' **@%(uname)s** ' % {'uname': uname}
185 return ' **@%(uname)s** ' % {'uname': uname}
187 mention_hl = mention_pat.sub(wrapp, source).strip()
186 mention_hl = mention_pat.sub(wrapp, source).strip()
188 # we extracted mentions render with this using Mentions false
187 # we extracted mentions render with this using Mentions false
189 return cls.rst(mention_hl, safe=safe, mentions=False)
188 return cls.rst(mention_hl, safe=safe, mentions=False)
190
189
191 source = safe_unicode(source)
190 source = safe_unicode(source)
192 try:
191 try:
193 docutils_settings = dict([(alias, None) for alias in
192 docutils_settings = dict([(alias, None) for alias in
194 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
193 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
195
194
196 docutils_settings.update({'input_encoding': 'unicode',
195 docutils_settings.update({'input_encoding': 'unicode',
197 'report_level': 4})
196 'report_level': 4})
198
197
199 for k, v in docutils_settings.iteritems():
198 for k, v in docutils_settings.iteritems():
200 directives.register_directive(k, v)
199 directives.register_directive(k, v)
201
200
202 parts = publish_parts(source=source,
201 parts = publish_parts(source=source,
203 writer_name="html4css1",
202 writer_name="html4css1",
204 settings_overrides=docutils_settings)
203 settings_overrides=docutils_settings)
205
204
206 return parts['html_title'] + parts["fragment"]
205 return parts['html_title'] + parts["fragment"]
207 except Exception:
206 except Exception:
208 log.exception('Error when rendering RST')
207 log.exception('Error when rendering RST')
209 if safe:
208 if safe:
210 log.debug('Fallbacking to render in plain mode')
209 log.debug('Fallbacking to render in plain mode')
211 return cls.plain(source)
210 return cls.plain(source)
212 else:
211 else:
213 raise
212 raise
214
213
215
214
216 class RstTemplateRenderer(object):
215 class RstTemplateRenderer(object):
217
216
218 def __init__(self):
217 def __init__(self):
219 base = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
218 base = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
220 rst_template_dirs = [os.path.join(base, 'templates', 'rst_templates')]
219 rst_template_dirs = [os.path.join(base, 'templates', 'rst_templates')]
221 self.template_store = TemplateLookup(
220 self.template_store = TemplateLookup(
222 directories=rst_template_dirs,
221 directories=rst_template_dirs,
223 input_encoding='utf-8',
222 input_encoding='utf-8',
224 imports=['from rhodecode.lib import helpers as h'])
223 imports=['from rhodecode.lib import helpers as h'])
225
224
226 def _get_template(self, templatename):
225 def _get_template(self, templatename):
227 return self.template_store.get_template(templatename)
226 return self.template_store.get_template(templatename)
228
227
229 def render(self, template_name, **kwargs):
228 def render(self, template_name, **kwargs):
230 template = self._get_template(template_name)
229 template = self._get_template(template_name)
231 return template.render(**kwargs)
230 return template.render(**kwargs)
General Comments 0
You need to be logged in to leave comments. Login now