1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-05 17:02:47 +00:00

Optimize wiki module (#5213)

* optimize code

* optimize code

* python load wiki file_content

* optimize style

* improve code

* optimize code

* update code

* optimize wiki style

* format <h> label

* add scroll interactive

* optimize code

* repair code bug

* format datetime

Co-authored-by: 王健辉 <40563566+mrwangjianhui@users.noreply.github.com>
This commit is contained in:
杨顺强
2022-07-27 16:26:55 +08:00
committed by GitHub
parent 8ff1b0ba72
commit 27399d8970
8 changed files with 382 additions and 31 deletions

View File

@@ -105,7 +105,7 @@ class MainPanel extends Component {
{this.props.permission == 'rw' && ( {this.props.permission == 'rw' && (
Utils.isDesktop() ? Utils.isDesktop() ?
<button className="btn btn-secondary operation-item" title={gettext('Edit')} onClick={this.onEditClick}>{gettext('Edit')}</button> : <button className="btn btn-secondary operation-item" title={gettext('Edit')} onClick={this.onEditClick}>{gettext('Edit')}</button> :
<span className="fa fa-pencil-alt mobile-toolbar-icon" title={gettext('Edit')} onClick={this.onEditClick} style={{'font-size': '1.1rem'}}></span> <span className="fa fa-pencil-alt mobile-toolbar-icon" title={gettext('Edit')} onClick={this.onEditClick} style={{'fontSize': '1.1rem'}}></span>
)} )}
</div> </div>
<div className="common-toolbar"> <div className="common-toolbar">

View File

@@ -27,7 +27,7 @@ class Wiki extends Component {
pathExist: true, pathExist: true,
closeSideBar: false, closeSideBar: false,
isViewFile: true, isViewFile: true,
isDataLoading: true, isDataLoading: false,
direntList: [], direntList: [],
content: '', content: '',
permission: '', permission: '',
@@ -43,6 +43,7 @@ class Wiki extends Component {
window.onpopstate = this.onpopstate; window.onpopstate = this.onpopstate;
this.indexPath = '/index.md'; this.indexPath = '/index.md';
this.homePath = '/home.md'; this.homePath = '/home.md';
this.pythonWrapper = null;
} }
componentWillMount() { componentWillMount() {
@@ -52,39 +53,54 @@ class Wiki extends Component {
} }
componentDidMount() { componentDidMount() {
this.loadWikiData(initialPath);
}
loadWikiData = (initialPath) => {
this.loadSidePanel(initialPath); this.loadSidePanel(initialPath);
this.loadWikiData(initialPath);
if (isDir === 'None') { this.links = document.querySelectorAll(`#wiki-file-content a`);
if (initialPath === '/home.md') { this.links.forEach(link => link.addEventListener('click', this.onConentLinkClick));
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) => { loadSidePanel = (initialPath) => {
if (hasIndex) { if (hasIndex) {
this.loadIndexNode(); this.loadIndexNode();
} else { return;
if (isDir === 'None') {
initialPath = '/';
this.loadNodeAndParentsByPath('/');
} else {
this.loadNodeAndParentsByPath(initialPath);
}
} }
// 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 = () => { loadIndexNode = () => {
@@ -120,6 +136,7 @@ class Wiki extends Component {
path: filePath, path: filePath,
}); });
this.removePythonWrapper();
seafileAPI.getWikiFileContent(slug, filePath).then(res => { seafileAPI.getWikiFileContent(slug, filePath).then(res => {
let data = res.data; let data = res.data;
this.setState({ 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) => { onLinkClick = (link) => {
const url = link; const url = link;
if (Utils.isWikiInternalMarkdownLink(url, slug)) { if (Utils.isWikiInternalMarkdownLink(url, slug)) {

View File

@@ -1220,3 +1220,124 @@ a.table-sort-op:hover {
box-shadow: 0 0 6px #ccc; box-shadow: 0 0 6px #ccc;
text-align: center; 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;
}

View File

@@ -22,3 +22,4 @@ python-cas
djangosaml2==0.20.0 djangosaml2==0.20.0
pysaml2==6.5.1 pysaml2==6.5.1
cffi==1.14.0 cffi==1.14.0
Markdown

View File

@@ -304,6 +304,8 @@ def translate_seahub_time(value, autoescape=None):
return mark_safe(time_with_tag) return mark_safe(time_with_tag)
@register.filter(name='translate_seahub_time_str')
def translate_seahub_time_str(val): def translate_seahub_time_str(val):
"""Convert python datetime to human friendly format.""" """Convert python datetime to human friendly format."""

View File

@@ -28,6 +28,7 @@
<body> <body>
<div id="wrapper" class="{{ LANGUAGE_CODE }}"></div> <div id="wrapper" class="{{ LANGUAGE_CODE }}"></div>
<div id="modal-wrapper" class="{{ LANGUAGE_CODE }}"></div> <div id="modal-wrapper" class="{{ LANGUAGE_CODE }}"></div>
{%block extra_content %}{% endblock %}
<script type="text/javascript"> <script type="text/javascript">
window.app = { window.app = {

View File

@@ -1,4 +1,5 @@
{% extends "base_for_react.html" %} {% extends "base_for_react.html" %}
{% load i18n %}
{% load render_bundle from webpack_loader %} {% load render_bundle from webpack_loader %}
{% load seahub_tags %} {% load seahub_tags %}
{% block extra_ogp_tags %} {% block extra_ogp_tags %}
@@ -16,6 +17,24 @@
{% block sub_title %} {{filename}} - {% endblock %} {% block sub_title %} {{filename}} - {% endblock %}
{% block extra_content %}
{% if not is_dir %}
<div id="wiki-file-content" class="{{ LANGUAGE_CODE }}">
<div class="article">
{{ file_content }}
<p id="wiki-page-last-modified">{% translate "Last modified by" %} {{modifier|email2nickname}}, <span>{{modify_time|translate_seahub_time_str}}</span></p>
</div>
<div class="seafile-markdown-outline">
<div id="seafile-editor-outline" class="seafile-editor-outline">
{% for outline in outlines %}
{{ outline }}
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block extra_script %} {% block extra_script %}
<script type="text/javascript"> <script type="text/javascript">
window.wiki = { window.wiki = {
@@ -32,6 +51,71 @@
} }
}; };
</script> </script>
<script type="text/javascript">
// titles info
let titlesInfo = [];
let headingList = document.querySelectorAll('h2[id^="user-content"], h3[id^="user-content"]');
for (let i = 0; i < headingList.length; i++) {
titlesInfo.push(headingList[i].offsetTop);
}
// outline infos
const outlineInfos = document.querySelectorAll('.outline-h2, .outline-h3');
const addActiveClass = (item) => {
const className = item.className;
if (className.indexOf('active') > -1) return;
item.className += ' active';
};
const removeActiveClass = (item) => {
const className = item.className;
if (className.indexOf('active') === -1) return;
item.className = className.replace(/(?:^|\s)active(?!\S)/g, '');
};
const updateOutline = (activeIndex) => {
for (let i = 0; i < outlineInfos.length; i++) {
const item = outlineInfos[i];
if (activeIndex !== i) {
removeActiveClass(item);
continue;
}
addActiveClass(item);
}
};
const outlineContainer = document.getElementById('seafile-editor-outline');
outlineContainer && outlineContainer.addEventListener('click', (event) => {
const text = event.target.innerText;
let url = new URL(window.location.href);
url.hash = '#user-content-' + text;
window.location.href = url.toString();
});
// scroll event handle
const container = document.getElementById('wiki-file-content');
container && container.addEventListener('scroll', () => {
const titlesLength = titlesInfo.length;
const contentScrollTop = container.scrollTop + 180;
let activeTitleIndex;
if (contentScrollTop <= titlesInfo[0]) {
activeTitleIndex = 0;
} else if (contentScrollTop > titlesInfo[titlesLength - 1]) {
activeTitleIndex = titlesInfo.length - 1;
} else {
for (let i = 0; i < titlesLength; i++) {
if (contentScrollTop > titlesInfo[i]) {
continue;
} else {
activeTitleIndex = i - 1;
break;
}
}
}
updateOutline(activeTitleIndex);
});
// set first outline to active
updateOutline(0);
</script>
{% render_bundle 'wiki' 'js' %} {% render_bundle 'wiki' 'js' %}
{% endblock %} {% endblock %}

View File

@@ -1,22 +1,28 @@
# Copyright (c) 2012-2016 Seafile Ltd. # Copyright (c) 2012-2016 Seafile Ltd.
import os import os
import re
import logging import logging
import urllib.request, urllib.error, urllib.parse import urllib.request
import posixpath import posixpath
from datetime import datetime
import seaserv import markdown
import lxml.html
from seaserv import seafile_api from seaserv import seafile_api
from django.urls import reverse 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.shortcuts import render, get_object_or_404, redirect
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from seahub.auth.decorators import login_required
from seahub.share.models import FileShare from seahub.share.models import FileShare
from seahub.wiki.models import Wiki from seahub.wiki.models import Wiki
from seahub.views import check_folder_permission 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.utils.file_types import *
from seahub.settings import SERVICE_URL
# Get an instance of a logger # Get an instance of a logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -88,6 +94,98 @@ def slug(request, slug, file_path="home.md"):
repo = seafile_api.get_repo(wiki.repo_id) 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 <img> 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 <a> 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 <h> label
for p in html_content.split('\n'):
if p.startswith('<h1>') and p.endswith('</h1>'):
head = p.replace('<h1>', '<h1 id="user-content-%s">' % p.strip('<h1></h1>'), 1)
html_content = html_content.replace(p, head)
elif p.startswith('<h2>') and p.endswith('</h2>'):
head = p.replace('<h2>', '<h2 id="user-content-%s">' % p.strip('<h2></h2>'), 1)
html_content = html_content.replace(p, head)
outline = '<div class="outline-h2">' + p.strip('<h2></h2>') + '</div>'
outlines.append(mark_safe(outline))
elif p.startswith('<h3>') and p.endswith('</h3>'):
head = p.replace('<h3>', '<h3 id="user-content-%s">' % p.strip('<h3></h3>'), 1)
html_content = html_content.replace(p, head)
outline = '<div class="outline-h3">' + p.strip('<h3></h3>') + '</div>'
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", { return render(request, "wiki/wiki.html", {
"wiki": wiki, "wiki": wiki,
"repo_name": repo.name if repo else '', "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, "user_can_write": user_can_write,
"file_path": file_path, "file_path": file_path,
"filename": os.path.splitext(os.path.basename(file_path))[0], "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, "repo_id": wiki.repo_id,
"search_repo_id": wiki.repo_id, "search_repo_id": wiki.repo_id,
"search_wiki": True, "search_wiki": True,