Show More
@@ -38,7 +38,7 b' from rhodecode.lib.utils2 import safe_st' | |||||
38 | from rhodecode.lib.auth import ( |
|
38 | from rhodecode.lib.auth import ( | |
39 | LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, XHRRequired) |
|
39 | LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, XHRRequired) | |
40 | from rhodecode.lib.base import BaseRepoController, render |
|
40 | from rhodecode.lib.base import BaseRepoController, render | |
41 | from rhodecode.lib.markup_renderer import MarkupRenderer |
|
41 | from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links | |
42 | from rhodecode.lib.ext_json import json |
|
42 | from rhodecode.lib.ext_json import json | |
43 | from rhodecode.lib.vcs.backends.base import EmptyCommit |
|
43 | from rhodecode.lib.vcs.backends.base import EmptyCommit | |
44 | from rhodecode.lib.vcs.exceptions import ( |
|
44 | from rhodecode.lib.vcs.exceptions import ( | |
@@ -70,7 +70,12 b' class SummaryController(BaseRepoControll' | |||||
70 | log.debug("Searching for a README file.") |
|
70 | log.debug("Searching for a README file.") | |
71 | readme_node = ReadmeFinder(default_renderer).search(commit) |
|
71 | readme_node = ReadmeFinder(default_renderer).search(commit) | |
72 | if readme_node: |
|
72 | if readme_node: | |
73 | readme_data = self._render_readme_or_none(commit, readme_node) |
|
73 | relative_url = h.url('files_raw_home', | |
|
74 | repo_name=repo_name, | |||
|
75 | revision=commit.raw_id, | |||
|
76 | f_path=readme_node.path) | |||
|
77 | readme_data = self._render_readme_or_none( | |||
|
78 | commit, readme_node, relative_url) | |||
74 | readme_filename = readme_node.path |
|
79 | readme_filename = readme_node.path | |
75 | return readme_data, readme_filename |
|
80 | return readme_data, readme_filename | |
76 |
|
81 | |||
@@ -95,13 +100,16 b' class SummaryController(BaseRepoControll' | |||||
95 | log.exception( |
|
100 | log.exception( | |
96 | "Problem getting commit when trying to render the README.") |
|
101 | "Problem getting commit when trying to render the README.") | |
97 |
|
102 | |||
98 | def _render_readme_or_none(self, commit, readme_node): |
|
103 | def _render_readme_or_none(self, commit, readme_node, relative_url): | |
99 | log.debug( |
|
104 | log.debug( | |
100 | 'Found README file `%s` rendering...', readme_node.path) |
|
105 | 'Found README file `%s` rendering...', readme_node.path) | |
101 | renderer = MarkupRenderer() |
|
106 | renderer = MarkupRenderer() | |
102 | try: |
|
107 | try: | |
103 |
|
|
108 | html_source = renderer.render( | |
104 | readme_node.content, filename=readme_node.path) |
|
109 | readme_node.content, filename=readme_node.path) | |
|
110 | if relative_url: | |||
|
111 | return relative_links(html_source, relative_url) | |||
|
112 | return html_source | |||
105 | except Exception: |
|
113 | except Exception: | |
106 | log.exception( |
|
114 | log.exception( | |
107 | "Exception while trying to render the README") |
|
115 | "Exception while trying to render the README") |
@@ -75,7 +75,7 b' from rhodecode.lib.utils import repo_nam' | |||||
75 | from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \ |
|
75 | from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \ | |
76 | get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \ |
|
76 | get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \ | |
77 | AttributeDict, safe_int, md5, md5_safe |
|
77 | AttributeDict, safe_int, md5, md5_safe | |
78 | from rhodecode.lib.markup_renderer import MarkupRenderer |
|
78 | from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links | |
79 | from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError |
|
79 | from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError | |
80 | from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit |
|
80 | from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit | |
81 | from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT |
|
81 | from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT | |
@@ -1828,19 +1828,29 b' def renderer_from_filename(filename, exc' | |||||
1828 | return None |
|
1828 | return None | |
1829 |
|
1829 | |||
1830 |
|
1830 | |||
1831 | def render(source, renderer='rst', mentions=False): |
|
1831 | def render(source, renderer='rst', mentions=False, relative_url=None): | |
|
1832 | ||||
|
1833 | def maybe_convert_relative_links(html_source): | |||
|
1834 | if relative_url: | |||
|
1835 | return relative_links(html_source, relative_url) | |||
|
1836 | return html_source | |||
|
1837 | ||||
1832 | if renderer == 'rst': |
|
1838 | if renderer == 'rst': | |
1833 | return literal( |
|
1839 | return literal( | |
1834 | '<div class="rst-block">%s</div>' % |
|
1840 | '<div class="rst-block">%s</div>' % | |
1835 | MarkupRenderer.rst(source, mentions=mentions)) |
|
1841 | maybe_convert_relative_links( | |
|
1842 | MarkupRenderer.rst(source, mentions=mentions))) | |||
1836 | elif renderer == 'markdown': |
|
1843 | elif renderer == 'markdown': | |
1837 | return literal( |
|
1844 | return literal( | |
1838 | '<div class="markdown-block">%s</div>' % |
|
1845 | '<div class="markdown-block">%s</div>' % | |
1839 | MarkupRenderer.markdown(source, flavored=True, mentions=mentions)) |
|
1846 | maybe_convert_relative_links( | |
|
1847 | MarkupRenderer.markdown(source, flavored=True, | |||
|
1848 | mentions=mentions))) | |||
1840 | elif renderer == 'jupyter': |
|
1849 | elif renderer == 'jupyter': | |
1841 | return literal( |
|
1850 | return literal( | |
1842 | '<div class="ipynb">%s</div>' % |
|
1851 | '<div class="ipynb">%s</div>' % | |
1843 | MarkupRenderer.jupyter(source)) |
|
1852 | maybe_convert_relative_links( | |
|
1853 | MarkupRenderer.jupyter(source))) | |||
1844 |
|
1854 | |||
1845 | # None means just show the file-source |
|
1855 | # None means just show the file-source | |
1846 | return None |
|
1856 | return None |
@@ -25,8 +25,10 b' Renderer for markup languages with abili' | |||||
25 |
|
25 | |||
26 | import re |
|
26 | import re | |
27 | import os |
|
27 | import os | |
|
28 | import lxml | |||
28 | import logging |
|
29 | import logging | |
29 | import itertools |
|
30 | import urlparse | |
|
31 | import urllib | |||
30 |
|
32 | |||
31 | from mako.lookup import TemplateLookup |
|
33 | from mako.lookup import TemplateLookup | |
32 | from mako.template import Template as MakoTemplate |
|
34 | from mako.template import Template as MakoTemplate | |
@@ -35,9 +37,9 b' from docutils.core import publish_parts' | |||||
35 | from docutils.parsers.rst import directives |
|
37 | from docutils.parsers.rst import directives | |
36 | import markdown |
|
38 | import markdown | |
37 |
|
39 | |||
38 |
from rhodecode.lib.markdown_ext import |
|
40 | from rhodecode.lib.markdown_ext import GithubFlavoredMarkdownExtension | |
39 | UrlizeExtension, GithubFlavoredMarkdownExtension) |
|
41 | from rhodecode.lib.utils2 import ( | |
40 |
|
|
42 | safe_str, safe_unicode, md5_safe, MENTIONS_REGEX) | |
41 |
|
43 | |||
42 | log = logging.getLogger(__name__) |
|
44 | log = logging.getLogger(__name__) | |
43 |
|
45 | |||
@@ -45,6 +47,84 b' log = logging.getLogger(__name__)' | |||||
45 | DEFAULT_COMMENTS_RENDERER = 'rst' |
|
47 | DEFAULT_COMMENTS_RENDERER = 'rst' | |
46 |
|
48 | |||
47 |
|
49 | |||
|
50 | def relative_links(html_source, server_path): | |||
|
51 | doc = lxml.html.fromstring(html_source) | |||
|
52 | for el in doc.cssselect('img, video'): | |||
|
53 | src = el.attrib['src'] | |||
|
54 | if src: | |||
|
55 | el.attrib['src'] = relative_path(src, server_path) | |||
|
56 | ||||
|
57 | for el in doc.cssselect('a:not(.gfm)'): | |||
|
58 | src = el.attrib['href'] | |||
|
59 | if src: | |||
|
60 | el.attrib['href'] = relative_path(src, server_path) | |||
|
61 | ||||
|
62 | return lxml.html.tostring(doc) | |||
|
63 | ||||
|
64 | ||||
|
65 | def relative_path(path, request_path, is_repo_file=None): | |||
|
66 | """ | |||
|
67 | relative link support, path is a rel path, and request_path is current | |||
|
68 | server path (not absolute) | |||
|
69 | ||||
|
70 | e.g. | |||
|
71 | ||||
|
72 | path = '../logo.png' | |||
|
73 | request_path= '/repo/files/path/file.md' | |||
|
74 | produces: '/repo/files/logo.png' | |||
|
75 | """ | |||
|
76 | # TODO(marcink): unicode/str support ? | |||
|
77 | # maybe=> safe_unicode(urllib.quote(safe_str(final_path), '/:')) | |||
|
78 | ||||
|
79 | def dummy_check(p): | |||
|
80 | return True # assume default is a valid file path | |||
|
81 | ||||
|
82 | is_repo_file = is_repo_file or dummy_check | |||
|
83 | if not path: | |||
|
84 | return request_path | |||
|
85 | ||||
|
86 | path = safe_unicode(path) | |||
|
87 | request_path = safe_unicode(request_path) | |||
|
88 | ||||
|
89 | if path.startswith((u'data:', u'#', u':')): | |||
|
90 | # skip data, anchor, invalid links | |||
|
91 | return path | |||
|
92 | ||||
|
93 | is_absolute = bool(urlparse.urlparse(path).netloc) | |||
|
94 | if is_absolute: | |||
|
95 | return path | |||
|
96 | ||||
|
97 | if not request_path: | |||
|
98 | return path | |||
|
99 | ||||
|
100 | if path.startswith(u'/'): | |||
|
101 | path = path[1:] | |||
|
102 | ||||
|
103 | if path.startswith(u'./'): | |||
|
104 | path = path[2:] | |||
|
105 | ||||
|
106 | parts = request_path.split('/') | |||
|
107 | # compute how deep we need to traverse the request_path | |||
|
108 | depth = 0 | |||
|
109 | ||||
|
110 | if is_repo_file(request_path): | |||
|
111 | # if request path is a VALID file, we use a relative path with | |||
|
112 | # one level up | |||
|
113 | depth += 1 | |||
|
114 | ||||
|
115 | while path.startswith(u'../'): | |||
|
116 | depth += 1 | |||
|
117 | path = path[3:] | |||
|
118 | ||||
|
119 | if depth > 0: | |||
|
120 | parts = parts[:-depth] | |||
|
121 | ||||
|
122 | parts.append(path) | |||
|
123 | final_path = u'/'.join(parts).lstrip(u'/') | |||
|
124 | ||||
|
125 | return u'/' + final_path | |||
|
126 | ||||
|
127 | ||||
48 | class MarkupRenderer(object): |
|
128 | class MarkupRenderer(object): | |
49 | RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw'] |
|
129 | RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw'] | |
50 |
|
130 |
@@ -53,7 +53,7 b'' | |||||
53 | %else: |
|
53 | %else: | |
54 | % if c.file.size < c.cut_off_limit: |
|
54 | % if c.file.size < c.cut_off_limit: | |
55 | %if c.renderer and not c.annotate: |
|
55 | %if c.renderer and not c.annotate: | |
56 | ${h.render(c.file.content, renderer=c.renderer)} |
|
56 | ${h.render(c.file.content, renderer=c.renderer, relative_url=h.url('files_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path))} | |
57 | %else: |
|
57 | %else: | |
58 | <table class="cb codehilite"> |
|
58 | <table class="cb codehilite"> | |
59 | %if c.annotate: |
|
59 | %if c.annotate: |
@@ -20,7 +20,8 b'' | |||||
20 |
|
20 | |||
21 | import pytest |
|
21 | import pytest | |
22 |
|
22 | |||
23 |
from rhodecode.lib.markup_renderer import |
|
23 | from rhodecode.lib.markup_renderer import ( | |
|
24 | MarkupRenderer, RstTemplateRenderer, relative_path, relative_links) | |||
24 |
|
25 | |||
25 |
|
26 | |||
26 | @pytest.mark.parametrize( |
|
27 | @pytest.mark.parametrize( | |
@@ -177,3 +178,78 b' Auto status change to |new_status|' | |||||
177 | renderer = RstTemplateRenderer() |
|
178 | renderer = RstTemplateRenderer() | |
178 | rendered = renderer.render('auto_status_change.mako', **params) |
|
179 | rendered = renderer.render('auto_status_change.mako', **params) | |
179 | assert expected == rendered |
|
180 | assert expected == rendered | |
|
181 | ||||
|
182 | ||||
|
183 | @pytest.mark.parametrize( | |||
|
184 | "src_path, server_path, is_path, expected", | |||
|
185 | [ | |||
|
186 | ('source.png', '/repo/files/path', lambda p: False, | |||
|
187 | '/repo/files/path/source.png'), | |||
|
188 | ||||
|
189 | ('source.png', 'mk/git/blob/master/README.md', lambda p: True, | |||
|
190 | '/mk/git/blob/master/source.png'), | |||
|
191 | ||||
|
192 | ('./source.png', 'mk/git/blob/master/README.md', lambda p: True, | |||
|
193 | '/mk/git/blob/master/source.png'), | |||
|
194 | ||||
|
195 | ('/source.png', 'mk/git/blob/master/README.md', lambda p: True, | |||
|
196 | '/mk/git/blob/master/source.png'), | |||
|
197 | ||||
|
198 | ('./source.png', 'repo/files/path/source.md', lambda p: True, | |||
|
199 | '/repo/files/path/source.png'), | |||
|
200 | ||||
|
201 | ('./source.png', '/repo/files/path/file.md', lambda p: True, | |||
|
202 | '/repo/files/path/source.png'), | |||
|
203 | ||||
|
204 | ('../source.png', '/repo/files/path/file.md', lambda p: True, | |||
|
205 | '/repo/files/source.png'), | |||
|
206 | ||||
|
207 | ('./../source.png', '/repo/files/path/file.md', lambda p: True, | |||
|
208 | '/repo/files/source.png'), | |||
|
209 | ||||
|
210 | ('./source.png', '/repo/files/path/file.md', lambda p: True, | |||
|
211 | '/repo/files/path/source.png'), | |||
|
212 | ||||
|
213 | ('../../../source.png', 'path/file.md', lambda p: True, | |||
|
214 | '/source.png'), | |||
|
215 | ||||
|
216 | ('../../../../../source.png', '/path/file.md', None, | |||
|
217 | '/source.png'), | |||
|
218 | ||||
|
219 | ('../../../../../source.png', 'files/path/file.md', None, | |||
|
220 | '/source.png'), | |||
|
221 | ||||
|
222 | ('../../../../../https://google.com/image.png', 'files/path/file.md', None, | |||
|
223 | '/https://google.com/image.png'), | |||
|
224 | ||||
|
225 | ('https://google.com/image.png', 'files/path/file.md', None, | |||
|
226 | 'https://google.com/image.png'), | |||
|
227 | ||||
|
228 | ('://foo', '/files/path/file.md', None, | |||
|
229 | '://foo'), | |||
|
230 | ||||
|
231 | (u'한글.png', '/files/path/file.md', None, | |||
|
232 | u'/files/path/한글.png'), | |||
|
233 | ||||
|
234 | ('my custom image.png', '/files/path/file.md', None, | |||
|
235 | '/files/path/my custom image.png'), | |||
|
236 | ]) | |||
|
237 | def test_relative_path(src_path, server_path, is_path, expected): | |||
|
238 | path = relative_path(src_path, server_path, is_path) | |||
|
239 | assert path == expected | |||
|
240 | ||||
|
241 | ||||
|
242 | @pytest.mark.parametrize( | |||
|
243 | "src_html, expected_html", | |||
|
244 | [ | |||
|
245 | ('<div></div>', '<div></div>'), | |||
|
246 | ('<img src="/file.png"></img>', '<img src="/path/raw/file.png">'), | |||
|
247 | ('<img src="data:abcd"/>', '<img src="data:abcd">'), | |||
|
248 | ('<a href="/file.png"></a>', '<a href="/path/raw/file.png"></a>'), | |||
|
249 | ('<a href="#anchor"></a>', '<a href="#anchor"></a>'), | |||
|
250 | ('<a href="./README.md"></a>', '<a href="/path/raw/README.md"></a>'), | |||
|
251 | ('<a href="../README.md"></a>', '<a href="/path/README.md"></a>'), | |||
|
252 | ||||
|
253 | ]) | |||
|
254 | def test_relative_links(src_html, expected_html): | |||
|
255 | assert relative_links(src_html, '/path/raw/file.md') == expected_html |
General Comments 0
You need to be logged in to leave comments.
Login now