diff --git a/frontend/src/pages/wiki/main-panel.js b/frontend/src/pages/wiki/main-panel.js index 48d4ad1125..c49b226e03 100644 --- a/frontend/src/pages/wiki/main-panel.js +++ b/frontend/src/pages/wiki/main-panel.js @@ -105,7 +105,7 @@ class MainPanel extends Component { {this.props.permission == 'rw' && ( Utils.isDesktop() ? : - + )}
diff --git a/frontend/src/wiki.js b/frontend/src/wiki.js index 205450a24b..f842253cab 100644 --- a/frontend/src/wiki.js +++ b/frontend/src/wiki.js @@ -27,7 +27,7 @@ class Wiki extends Component { pathExist: true, closeSideBar: false, isViewFile: true, - isDataLoading: true, + isDataLoading: false, direntList: [], content: '', permission: '', @@ -43,6 +43,7 @@ class Wiki extends Component { window.onpopstate = this.onpopstate; this.indexPath = '/index.md'; this.homePath = '/home.md'; + this.pythonWrapper = null; } componentWillMount() { @@ -52,39 +53,54 @@ class Wiki extends Component { } componentDidMount() { + this.loadSidePanel(initialPath); this.loadWikiData(initialPath); + + this.links = document.querySelectorAll(`#wiki-file-content a`); + this.links.forEach(link => link.addEventListener('click', this.onConentLinkClick)); } - loadWikiData = (initialPath) => { - this.loadSidePanel(initialPath); - - if (isDir === 'None') { - if (initialPath === '/home.md') { - this.showDir('/'); - } else { - this.setState({pathExist: false}); - let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(initialPath); - window.history.pushState({url: fileUrl, path: initialPath}, initialPath, fileUrl); - } - } else if (isDir === 'True') { - this.showDir(initialPath); - } else if (isDir === 'False') { - this.showFile(initialPath); - } + componentWillUnmount() { + this.links.forEach(link => link.removeEventListener('click', this.onConentLinkClick)); } loadSidePanel = (initialPath) => { if (hasIndex) { this.loadIndexNode(); - } else { - if (isDir === 'None') { - initialPath = '/'; - this.loadNodeAndParentsByPath('/'); - } else { - this.loadNodeAndParentsByPath(initialPath); - } + return; } + // load dir list + initialPath = isDir === 'None' ? '/' : initialPath; + this.loadNodeAndParentsByPath(initialPath); + } + + loadWikiData = (initialPath) => { + this.pythonWrapper = document.getElementById('wiki-file-content'); + if (isDir === 'False') { + // this.showFile(initialPath); + this.setState({path: initialPath}); + return; + } + + // if it is a file list, remove the template content provided by python + this.removePythonWrapper(); + + if (isDir === 'True') { + this.showDir(initialPath); + return; + } + + if (isDir === 'None' && initialPath === '/home.md') { + this.showDir('/'); + return; + } + + if (isDir === 'None') { + this.setState({pathExist: false}); + let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(initialPath); + window.history.pushState({url: fileUrl, path: initialPath}, initialPath, fileUrl); + } } loadIndexNode = () => { @@ -119,7 +135,8 @@ class Wiki extends Component { isViewFile: true, path: filePath, }); - + + this.removePythonWrapper(); seafileAPI.getWikiFileContent(slug, filePath).then(res => { let data = res.data; this.setState({ @@ -221,6 +238,29 @@ class Wiki extends Component { }); } + removePythonWrapper = () => { + if (this.pythonWrapper) { + document.body.removeChild(this.pythonWrapper); + this.pythonWrapper = null; + } + } + + onConentLinkClick = (event) => { + event.preventDefault(); + event.stopPropagation(); + let link = ''; + if (event.target.tagName !== 'A') { + let target = event.target.parentNode; + while (target.tagName !== 'A') { + target = target.parentNode; + } + link = target.href; + } else { + link = event.target.href; + } + this.onLinkClick(link); + } + onLinkClick = (link) => { const url = link; if (Utils.isWikiInternalMarkdownLink(url, slug)) { diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index 4414ab2952..6d4a63e852 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -1220,3 +1220,124 @@ a.table-sort-op:hover { box-shadow: 0 0 6px #ccc; text-align: center; } + +#wiki-file-content { + position: absolute; + right: 0; + bottom: 0; + left: 20%; + top: 90px; + z-index: 2; + background: #fff; + overflow: auto; + display: flex; +} + +#wiki-file-content .article { + margin-right: 200px; + padding: 10px 30px 20px; +} + +#wiki-file-content .seafile-markdown-outline { + position: fixed; + top: 97px; + right: 0; + width: 200px; + overflow: auto; + height: 80%; +} + +@media (max-width: 767px) { + #wiki-file-content { + left: 0; + } + + #wiki-file-content .article { + margin-right: 0; + width: 100%; + } + + #wiki-file-content .seafile-markdown-outline { + display: none; + } +} + +.seafile-md-viewer-content .article { + padding: 0; +} +.seafile-md-viewer-content { + background: #fff; + padding: 70px 75px; + border:1px solid #e6e6dd; + min-height: calc(100% - 60px); +} +.seafile-md-viewer-outline-heading2, +.seafile-md-viewer-outline-heading3 { + margin-left: .75rem; + line-height: 2.5; + color:#666; + white-space: nowrap; + overflow:hidden; + text-overflow:ellipsis; + cursor:pointer; +} +.seafile-md-viewer-outline-heading3 { + margin-left: 2rem; +} +.seafile-md-viewer-outline-heading2:hover, +.seafile-md-viewer-outline-heading3:hover { + color: #eb8205; +} +.seafile-markdown-outline { + position: fixed; + padding-right: 1rem; + top: 97px; + right: 0; + width: 200px; + overflow: auto; + height: 80%; +} +.seafile-editor-outline { + border-left: 1px solid #ddd; +} +.seafile-markdown-outline .active { + color: #eb8205; + border-left: 1px solid #eb8205; +} +.seafile-markdown-outline .outline-h2, +.seafile-markdown-outline .outline-h3 { + height: 30px; + margin-left: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 14px; +} +.seafile-markdown-outline .outline-h2 { + padding-left: 20px; +} +.seafile-markdown-outline .outline-h3 { + padding-left: 40px; +} + +#wiki-file-content .seafile-markdown-outline .outline-h2, +#wiki-file-content .seafile-markdown-outline .outline-h3 { + height: 24px; + font-size: 12px; + color: #4d5156; +} + +#wiki-file-content .seafile-markdown-outline .outline-h2.active, +#wiki-file-content .seafile-markdown-outline .outline-h3.active { + color: #eb8205; +} + +#wiki-file-content .seafile-markdown-outline .seafile-markdown-outline { + overflow-y: hidden; + margin-right: 10px; +} + +#wiki-file-content .seafile-markdown-outline .seafile-markdown-outline:hover { + overflow-y: auto; +} + diff --git a/requirements.txt b/requirements.txt index a40a2e9228..11e19d9365 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,4 @@ python-cas djangosaml2==0.20.0 pysaml2==6.5.1 cffi==1.14.0 +Markdown diff --git a/seahub/base/templatetags/seahub_tags.py b/seahub/base/templatetags/seahub_tags.py index 79c0d7803f..a9de49bac9 100644 --- a/seahub/base/templatetags/seahub_tags.py +++ b/seahub/base/templatetags/seahub_tags.py @@ -304,6 +304,8 @@ def translate_seahub_time(value, autoescape=None): return mark_safe(time_with_tag) + +@register.filter(name='translate_seahub_time_str') def translate_seahub_time_str(val): """Convert python datetime to human friendly format.""" diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index ed63a5d7ea..ef8a8452ca 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -28,6 +28,7 @@
+ {%block extra_content %}{% endblock %} + {% render_bundle 'wiki' 'js' %} {% endblock %} diff --git a/seahub/wiki/views.py b/seahub/wiki/views.py index 2deb7e58c1..025ed4e3bb 100644 --- a/seahub/wiki/views.py +++ b/seahub/wiki/views.py @@ -1,22 +1,28 @@ # Copyright (c) 2012-2016 Seafile Ltd. import os +import re import logging -import urllib.request, urllib.error, urllib.parse +import urllib.request import posixpath +from datetime import datetime -import seaserv +import markdown +import lxml.html from seaserv import seafile_api from django.urls import reverse -from django.http import Http404, HttpResponseRedirect +from django.http import HttpResponseRedirect +from django.utils.safestring import mark_safe from django.shortcuts import render, get_object_or_404, redirect from django.utils.translation import ugettext as _ -from seahub.auth.decorators import login_required from seahub.share.models import FileShare from seahub.wiki.models import Wiki from seahub.views import check_folder_permission -from seahub.utils import get_service_url, get_file_type_and_ext, render_permission_error +from seahub.utils import get_file_type_and_ext, render_permission_error, \ + gen_inner_file_get_url, render_error +from seahub.views.file import send_file_access_msg from seahub.utils.file_types import * +from seahub.settings import SERVICE_URL # Get an instance of a logger logger = logging.getLogger(__name__) @@ -88,6 +94,98 @@ def slug(request, slug, file_path="home.md"): repo = seafile_api.get_repo(wiki.repo_id) + file_content, outlines, latest_contributor, last_modified = '', [], '', 0 + if is_dir is False: + send_file_access_msg(request, repo, file_path, 'web') + + file_name = os.path.basename(file_path) + token = seafile_api.get_fileserver_access_token( + repo.repo_id, file_id, 'download', request.user.username, 'False') + if not token: + return render_error(request, _('Internal Server Error')) + + url = gen_inner_file_get_url(token, file_name) + try: + file_response = urllib.request.urlopen(url).read().decode() + except Exception as e: + logger.error(e) + return render_error(request, _('Internal Server Error')) + + if file_type == MARKDOWN: + # Convert a markdown string to HTML + try: + html_content = markdown.markdown(file_response) + except Exception as e: + logger.error(e) + return render_error(request, _('Internal Server Error')) + + # Parse the html and replace image url to wiki mode + html_doc = lxml.html.fromstring(html_content) + img_elements = html_doc.xpath('//img') # Get the elements + img_url_re = re.compile(r'^%s/lib/%s/file/.*raw=1$' % (SERVICE_URL.strip('/'), repo.id)) + for img in img_elements: + img_url = img.attrib.get('src', '') + if img_url_re.match(img_url) is not None: + img_path = img_url[img_url.find('/file/')+5:img_url.find('?')] + new_img_url = '%s/view-image-via-public-wiki/?slug=%s&path=%s' \ + % (SERVICE_URL.strip('/'), slug, img_path) + html_content = html_content.replace(img_url, new_img_url) + elif re.compile(r'^\.\./*|^\./').match(img_url): + if img_url.startswith('../'): + img_path = os.path.join(os.path.dirname(os.path.dirname(file_path)), img_url[3:]) + else: + img_path = os.path.join(os.path.dirname(file_path), img_url[2:]) + new_img_url = '%s/view-image-via-public-wiki/?slug=%s&path=%s' \ + % (SERVICE_URL.strip('/'), slug, img_path) + html_content = html_content.replace(img_url, new_img_url) + + # Replace link url to wiki mode + link_elements = html_doc.xpath('//a') # Get the elements + file_link_re = re.compile(r'^%s/lib/%s/file/.*' % (SERVICE_URL.strip('/'), repo.id)) + md_link_re = re.compile(r'^%s/lib/%s/file/.*\.md$' % (SERVICE_URL.strip('/'), repo.id)) + dir_link_re = re.compile(r'^%s/library/%s/(.*)' % (SERVICE_URL.strip('/'), repo.id)) + for link in link_elements: + link_url = link.attrib.get('href', '') + if file_link_re.match(link_url) is not None: + link_path = link_url[link_url.find('/file/') + 5:].strip('/') + if md_link_re.match(link_url) is not None: + new_md_url = '%s/published/%s/%s' % (SERVICE_URL.strip('/'), slug, link_path) + html_content = html_content.replace(link_url, new_md_url) + else: + new_file_url = '%s/d/%s/files/?p=%s&dl=1' % (SERVICE_URL.strip('/'), fs.token, link_path) + html_content = html_content.replace(link_url, new_file_url) + elif dir_link_re.match(link_url) is not None: + link_path = dir_link_re.match(link_url).groups()[0].strip('/') + dir_path = link_path[link_path.find('/'):].strip('/') + new_dir_url = '%s/published/%s/%s' % (SERVICE_URL.strip('/'), slug, dir_path) + html_content = html_content.replace(link_url, new_dir_url) + + # Get markdown outlines and format label + for p in html_content.split('\n'): + if p.startswith('

') and p.endswith('

'): + head = p.replace('

', '

' % p.strip('

'), 1) + html_content = html_content.replace(p, head) + elif p.startswith('

') and p.endswith('

'): + head = p.replace('

', '

' % p.strip('

'), 1) + html_content = html_content.replace(p, head) + outline = '
' + p.strip('

') + '
' + outlines.append(mark_safe(outline)) + elif p.startswith('

') and p.endswith('

'): + head = p.replace('

', '

' % p.strip('

'), 1) + html_content = html_content.replace(p, head) + outline = '
' + p.strip('

') + '
' + outlines.append(mark_safe(outline)) + + file_content = mark_safe(html_content) + + try: + dirent = seafile_api.get_dirent_by_path(wiki.repo_id, file_path) + if dirent: + latest_contributor, last_modified = dirent.modifier, dirent.mtime + except Exception as e: + logger.warning(e) + last_modified = datetime.fromtimestamp(last_modified) + return render(request, "wiki/wiki.html", { "wiki": wiki, "repo_name": repo.name if repo else '', @@ -97,6 +195,10 @@ def slug(request, slug, file_path="home.md"): "user_can_write": user_can_write, "file_path": file_path, "filename": os.path.splitext(os.path.basename(file_path))[0], + "file_content": file_content, + "outlines": outlines, + "modifier": latest_contributor, + "modify_time": last_modified, "repo_id": wiki.repo_id, "search_repo_id": wiki.repo_id, "search_wiki": True,