diff --git a/rhodecode/controllers/summary.py b/rhodecode/controllers/summary.py
--- a/rhodecode/controllers/summary.py
+++ b/rhodecode/controllers/summary.py
@@ -38,7 +38,7 @@ from rhodecode.lib.utils2 import safe_st
from rhodecode.lib.auth import (
LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, XHRRequired)
from rhodecode.lib.base import BaseRepoController, render
-from rhodecode.lib.markup_renderer import MarkupRenderer
+from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
from rhodecode.lib.ext_json import json
from rhodecode.lib.vcs.backends.base import EmptyCommit
from rhodecode.lib.vcs.exceptions import (
@@ -70,7 +70,12 @@ class SummaryController(BaseRepoControll
log.debug("Searching for a README file.")
readme_node = ReadmeFinder(default_renderer).search(commit)
if readme_node:
- readme_data = self._render_readme_or_none(commit, readme_node)
+ relative_url = h.url('files_raw_home',
+ repo_name=repo_name,
+ revision=commit.raw_id,
+ f_path=readme_node.path)
+ readme_data = self._render_readme_or_none(
+ commit, readme_node, relative_url)
readme_filename = readme_node.path
return readme_data, readme_filename
@@ -95,13 +100,16 @@ class SummaryController(BaseRepoControll
log.exception(
"Problem getting commit when trying to render the README.")
- def _render_readme_or_none(self, commit, readme_node):
+ def _render_readme_or_none(self, commit, readme_node, relative_url):
log.debug(
'Found README file `%s` rendering...', readme_node.path)
renderer = MarkupRenderer()
try:
- return renderer.render(
+ html_source = renderer.render(
readme_node.content, filename=readme_node.path)
+ if relative_url:
+ return relative_links(html_source, relative_url)
+ return html_source
except Exception:
log.exception(
"Exception while trying to render the README")
diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py
--- a/rhodecode/lib/helpers.py
+++ b/rhodecode/lib/helpers.py
@@ -75,7 +75,7 @@ from rhodecode.lib.utils import repo_nam
from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
AttributeDict, safe_int, md5, md5_safe
-from rhodecode.lib.markup_renderer import MarkupRenderer
+from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
@@ -1828,19 +1828,29 @@ def renderer_from_filename(filename, exc
return None
-def render(source, renderer='rst', mentions=False):
+def render(source, renderer='rst', mentions=False, relative_url=None):
+
+ def maybe_convert_relative_links(html_source):
+ if relative_url:
+ return relative_links(html_source, relative_url)
+ return html_source
+
if renderer == 'rst':
return literal(
'
%s
' %
- MarkupRenderer.rst(source, mentions=mentions))
+ maybe_convert_relative_links(
+ MarkupRenderer.rst(source, mentions=mentions)))
elif renderer == 'markdown':
return literal(
'%s
' %
- MarkupRenderer.markdown(source, flavored=True, mentions=mentions))
+ maybe_convert_relative_links(
+ MarkupRenderer.markdown(source, flavored=True,
+ mentions=mentions)))
elif renderer == 'jupyter':
return literal(
'%s
' %
- MarkupRenderer.jupyter(source))
+ maybe_convert_relative_links(
+ MarkupRenderer.jupyter(source)))
# None means just show the file-source
return None
diff --git a/rhodecode/lib/markup_renderer.py b/rhodecode/lib/markup_renderer.py
--- a/rhodecode/lib/markup_renderer.py
+++ b/rhodecode/lib/markup_renderer.py
@@ -25,8 +25,10 @@ Renderer for markup languages with abili
import re
import os
+import lxml
import logging
-import itertools
+import urlparse
+import urllib
from mako.lookup import TemplateLookup
from mako.template import Template as MakoTemplate
@@ -35,9 +37,9 @@ from docutils.core import publish_parts
from docutils.parsers.rst import directives
import markdown
-from rhodecode.lib.markdown_ext import (
- UrlizeExtension, GithubFlavoredMarkdownExtension)
-from rhodecode.lib.utils2 import safe_unicode, md5_safe, MENTIONS_REGEX
+from rhodecode.lib.markdown_ext import GithubFlavoredMarkdownExtension
+from rhodecode.lib.utils2 import (
+ safe_str, safe_unicode, md5_safe, MENTIONS_REGEX)
log = logging.getLogger(__name__)
@@ -45,6 +47,84 @@ log = logging.getLogger(__name__)
DEFAULT_COMMENTS_RENDERER = 'rst'
+def relative_links(html_source, server_path):
+ doc = lxml.html.fromstring(html_source)
+ for el in doc.cssselect('img, video'):
+ src = el.attrib['src']
+ if src:
+ el.attrib['src'] = relative_path(src, server_path)
+
+ for el in doc.cssselect('a:not(.gfm)'):
+ src = el.attrib['href']
+ if src:
+ el.attrib['href'] = relative_path(src, server_path)
+
+ return lxml.html.tostring(doc)
+
+
+def relative_path(path, request_path, is_repo_file=None):
+ """
+ relative link support, path is a rel path, and request_path is current
+ server path (not absolute)
+
+ e.g.
+
+ path = '../logo.png'
+ request_path= '/repo/files/path/file.md'
+ produces: '/repo/files/logo.png'
+ """
+ # TODO(marcink): unicode/str support ?
+ # maybe=> safe_unicode(urllib.quote(safe_str(final_path), '/:'))
+
+ def dummy_check(p):
+ return True # assume default is a valid file path
+
+ is_repo_file = is_repo_file or dummy_check
+ if not path:
+ return request_path
+
+ path = safe_unicode(path)
+ request_path = safe_unicode(request_path)
+
+ if path.startswith((u'data:', u'#', u':')):
+ # skip data, anchor, invalid links
+ return path
+
+ is_absolute = bool(urlparse.urlparse(path).netloc)
+ if is_absolute:
+ return path
+
+ if not request_path:
+ return path
+
+ if path.startswith(u'/'):
+ path = path[1:]
+
+ if path.startswith(u'./'):
+ path = path[2:]
+
+ parts = request_path.split('/')
+ # compute how deep we need to traverse the request_path
+ depth = 0
+
+ if is_repo_file(request_path):
+ # if request path is a VALID file, we use a relative path with
+ # one level up
+ depth += 1
+
+ while path.startswith(u'../'):
+ depth += 1
+ path = path[3:]
+
+ if depth > 0:
+ parts = parts[:-depth]
+
+ parts.append(path)
+ final_path = u'/'.join(parts).lstrip(u'/')
+
+ return u'/' + final_path
+
+
class MarkupRenderer(object):
RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
diff --git a/rhodecode/templates/files/files_source.mako b/rhodecode/templates/files/files_source.mako
--- a/rhodecode/templates/files/files_source.mako
+++ b/rhodecode/templates/files/files_source.mako
@@ -53,7 +53,7 @@
%else:
% if c.file.size < c.cut_off_limit:
%if c.renderer and not c.annotate:
- ${h.render(c.file.content, renderer=c.renderer)}
+ ${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))}
%else:
%if c.annotate:
diff --git a/rhodecode/tests/lib/test_markup_renderer.py b/rhodecode/tests/lib/test_markup_renderer.py
--- a/rhodecode/tests/lib/test_markup_renderer.py
+++ b/rhodecode/tests/lib/test_markup_renderer.py
@@ -20,7 +20,8 @@
import pytest
-from rhodecode.lib.markup_renderer import MarkupRenderer, RstTemplateRenderer
+from rhodecode.lib.markup_renderer import (
+ MarkupRenderer, RstTemplateRenderer, relative_path, relative_links)
@pytest.mark.parametrize(
@@ -177,3 +178,78 @@ Auto status change to |new_status|
renderer = RstTemplateRenderer()
rendered = renderer.render('auto_status_change.mako', **params)
assert expected == rendered
+
+
+@pytest.mark.parametrize(
+ "src_path, server_path, is_path, expected",
+ [
+ ('source.png', '/repo/files/path', lambda p: False,
+ '/repo/files/path/source.png'),
+
+ ('source.png', 'mk/git/blob/master/README.md', lambda p: True,
+ '/mk/git/blob/master/source.png'),
+
+ ('./source.png', 'mk/git/blob/master/README.md', lambda p: True,
+ '/mk/git/blob/master/source.png'),
+
+ ('/source.png', 'mk/git/blob/master/README.md', lambda p: True,
+ '/mk/git/blob/master/source.png'),
+
+ ('./source.png', 'repo/files/path/source.md', lambda p: True,
+ '/repo/files/path/source.png'),
+
+ ('./source.png', '/repo/files/path/file.md', lambda p: True,
+ '/repo/files/path/source.png'),
+
+ ('../source.png', '/repo/files/path/file.md', lambda p: True,
+ '/repo/files/source.png'),
+
+ ('./../source.png', '/repo/files/path/file.md', lambda p: True,
+ '/repo/files/source.png'),
+
+ ('./source.png', '/repo/files/path/file.md', lambda p: True,
+ '/repo/files/path/source.png'),
+
+ ('../../../source.png', 'path/file.md', lambda p: True,
+ '/source.png'),
+
+ ('../../../../../source.png', '/path/file.md', None,
+ '/source.png'),
+
+ ('../../../../../source.png', 'files/path/file.md', None,
+ '/source.png'),
+
+ ('../../../../../https://google.com/image.png', 'files/path/file.md', None,
+ '/https://google.com/image.png'),
+
+ ('https://google.com/image.png', 'files/path/file.md', None,
+ 'https://google.com/image.png'),
+
+ ('://foo', '/files/path/file.md', None,
+ '://foo'),
+
+ (u'한글.png', '/files/path/file.md', None,
+ u'/files/path/한글.png'),
+
+ ('my custom image.png', '/files/path/file.md', None,
+ '/files/path/my custom image.png'),
+ ])
+def test_relative_path(src_path, server_path, is_path, expected):
+ path = relative_path(src_path, server_path, is_path)
+ assert path == expected
+
+
+@pytest.mark.parametrize(
+ "src_html, expected_html",
+ [
+ ('', ''),
+ ('', ''),
+ ('', ''),
+ ('', ''),
+ ('', ''),
+ ('', ''),
+ ('', ''),
+
+ ])
+def test_relative_links(src_html, expected_html):
+ assert relative_links(src_html, '/path/raw/file.md') == expected_html