2021-03-28 13:21:31 +03:00
|
|
|
__version__ = '1.1'
|
|
|
|
|
2021-03-23 23:11:57 +03:00
|
|
|
import os
|
|
|
|
import re
|
|
|
|
from functools import wraps
|
|
|
|
from datetime import timedelta
|
|
|
|
|
|
|
|
import pygments
|
2021-03-28 13:21:31 +03:00
|
|
|
from markdown import Markdown
|
2021-03-23 23:11:57 +03:00
|
|
|
|
|
|
|
from flask import Flask
|
|
|
|
from flask import request
|
|
|
|
from flask import session
|
|
|
|
from flask import redirect
|
|
|
|
from flask import render_template
|
|
|
|
from flask import send_from_directory
|
|
|
|
from flask_bcrypt import Bcrypt
|
|
|
|
|
|
|
|
from config import Config
|
|
|
|
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
app.config.from_object(Config)
|
|
|
|
app.permanent_session_lifetime = timedelta(hours=24)
|
|
|
|
|
|
|
|
bcrypt = Bcrypt(app)
|
|
|
|
|
|
|
|
def read_file(filepath: str) -> str:
|
|
|
|
try:
|
|
|
|
with open(filepath, 'r') as file:
|
|
|
|
return file.read()
|
|
|
|
except IOError:
|
|
|
|
return 'Error: Cannot read file: {}'.format(filepath)
|
|
|
|
|
2021-03-28 13:21:31 +03:00
|
|
|
def get_path(path: str) -> str:
|
|
|
|
return os.path.join(app.config['MARKDOWN_ROOT'], path)
|
|
|
|
|
2021-03-23 23:11:57 +03:00
|
|
|
def render_html(filepath: str) -> str:
|
2021-03-28 13:21:31 +03:00
|
|
|
html = Markdown(
|
|
|
|
extensions = app.config['MARKDOWN_EXTRAS'],
|
|
|
|
extension_configs = app.config['MARKDOWN_EXTRAS_CONFIGS']
|
|
|
|
)
|
|
|
|
return html.convert(
|
|
|
|
read_file(get_path(filepath))
|
|
|
|
)
|
2021-03-23 23:11:57 +03:00
|
|
|
|
|
|
|
def parse_title_from_markdown(filepath: str) -> str:
|
|
|
|
# This function parses article title from first level heading.
|
|
|
|
# It returns the occurrence of the first heading, and there
|
|
|
|
# can be nothing before it except empty lines and spaces.
|
2021-03-28 13:21:31 +03:00
|
|
|
article = read_file(get_path(filepath))
|
2021-03-23 23:11:57 +03:00
|
|
|
pattern = re.compile(r'^\s*#\s.*')
|
|
|
|
if pattern.search(article):
|
|
|
|
return pattern.search(article).group().strip()[2:]
|
|
|
|
else:
|
2021-03-28 13:21:31 +03:00
|
|
|
return 'Error: Cannot parse title from file: {}'.format(filepath)
|
2021-03-23 23:11:57 +03:00
|
|
|
|
|
|
|
def parse_content_links(filepath: str) -> list:
|
|
|
|
# This function returns a list of links from a Markdown file.
|
|
|
|
# Only links contained in the list (ul ol li) are parsed.
|
|
|
|
r = re.compile(r'(.*(-|\+|\*|\d).?\[.*\])(\(.*\))', re.MULTILINE)
|
|
|
|
links = []
|
2021-03-28 13:21:31 +03:00
|
|
|
for tpl in r.findall(read_file(get_path(filepath))):
|
2021-03-23 23:11:57 +03:00
|
|
|
for item in tpl:
|
|
|
|
if re.match(r'\(.*\)', item):
|
|
|
|
if item == '(/)':
|
|
|
|
item = '/' # This is a crutch for fixing the root url
|
|
|
|
# which for some reason continues to contain
|
|
|
|
# parentheses after re.match(r'').
|
|
|
|
if not item[1:-1].endswith('/'):
|
|
|
|
item = item[1:-1] + '/'
|
|
|
|
links.append(item)
|
|
|
|
return links
|
|
|
|
|
|
|
|
def check_password(password: str) -> bool:
|
2021-03-28 13:21:31 +03:00
|
|
|
if os.path.exists(app.config['PASSWORD_FILE']):
|
|
|
|
pw_hash = read_file(app.config['PASSWORD_FILE'])
|
2021-03-23 23:11:57 +03:00
|
|
|
return bcrypt.check_password_hash(pw_hash, password)
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
@app.errorhandler(404)
|
|
|
|
def page_not_found(e):
|
|
|
|
return render_template('404.j2'), 404
|
|
|
|
|
|
|
|
@app.context_processor
|
|
|
|
def utility_processor():
|
|
|
|
def get_len(list: list) -> int:
|
|
|
|
return len(list)
|
|
|
|
def get_title(path: str) -> str:
|
|
|
|
return parse_title_from_markdown(path[:-1] + '.md')
|
|
|
|
return dict(get_title = get_title, get_len = get_len)
|
|
|
|
|
|
|
|
def login_required(func):
|
|
|
|
@wraps(func)
|
|
|
|
def wrap(*args, **kwargs):
|
|
|
|
if app.config['SIGN_IN']:
|
|
|
|
if 'logged_in' in session:
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
else:
|
|
|
|
return redirect('/signin/')
|
|
|
|
else:
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
return wrap
|
|
|
|
|
|
|
|
@app.route('/signin/', methods = ['GET', 'POST'])
|
|
|
|
def signin():
|
|
|
|
if request.method == 'POST':
|
|
|
|
if check_password(request.form['password']):
|
|
|
|
session['logged_in'] = True
|
|
|
|
return redirect('/', 302)
|
|
|
|
else:
|
|
|
|
return render_template('signin.j2', wrong_pw = True)
|
|
|
|
else:
|
|
|
|
return render_template('signin.j2')
|
|
|
|
|
|
|
|
@app.route('/signout/')
|
|
|
|
@login_required
|
|
|
|
def signout():
|
|
|
|
session.pop('logged_in', None)
|
|
|
|
return redirect('/signin/')
|
|
|
|
|
|
|
|
@app.route('/')
|
|
|
|
@login_required
|
|
|
|
def index():
|
|
|
|
return render_template(
|
|
|
|
'index.j2',
|
|
|
|
title = parse_title_from_markdown('HOME.md'),
|
|
|
|
article = render_html('HOME.md'),
|
|
|
|
contents = render_html('CONTENTS.md'),
|
|
|
|
current_path = '/',
|
|
|
|
links = parse_content_links('CONTENTS.md')
|
|
|
|
)
|
|
|
|
|
|
|
|
@app.route('/<path:path>/')
|
|
|
|
@login_required
|
|
|
|
def get_article(path):
|
2021-03-28 13:21:31 +03:00
|
|
|
if os.path.exists(get_path(path) + '.md'):
|
2021-03-23 23:11:57 +03:00
|
|
|
return render_template(
|
|
|
|
'index.j2',
|
|
|
|
title = parse_title_from_markdown(path + '.md'),
|
|
|
|
article = render_html(path + '.md'),
|
|
|
|
contents = render_html('CONTENTS.md'),
|
|
|
|
current_path = request.path,
|
|
|
|
links = parse_content_links('CONTENTS.md')
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
return page_not_found(404)
|
|
|
|
|
|
|
|
@app.route('/<path:path>.md')
|
|
|
|
@login_required
|
|
|
|
def download_article(path):
|
2021-03-28 13:21:31 +03:00
|
|
|
if os.path.exists(get_path(path) + '.md') \
|
2021-03-23 23:11:57 +03:00
|
|
|
and app.config['MARKDOWN_DOWLOADS']:
|
|
|
|
return send_from_directory(
|
|
|
|
app.config['MARKDOWN_ROOT'],
|
|
|
|
path + '.md'
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
return page_not_found(404)
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
app.run()
|