v1.1
This commit is contained in:
parent
73f23a591c
commit
c024615796
19
CHANGELOG.md
19
CHANGELOG.md
@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## v1.1 2021.03.28
|
||||
|
||||
### Added
|
||||
|
||||
- **owl** switched to `markdown` module.
|
||||
- `markdown_alerts` extension. Short admonition syntax.
|
||||
- `markdown_del_ins` extension. Deleted and inserted text support.
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed `PASSWORD_FILE` detecting.
|
||||
- Fixed path resolving. Now uses `os.path.join()`.
|
||||
|
||||
### Removed
|
||||
|
||||
- `markdown2`. Replaced by `markdown` module.
|
||||
|
||||
## v1.0 2021.03.23
|
||||
|
||||
### Added
|
||||
@ -27,4 +44,4 @@
|
||||
|
||||
## v0.1 2020.08.15
|
||||
|
||||
First version released.
|
||||
First version released.
|
12
README.md
12
README.md
@ -1,6 +1,6 @@
|
||||
# owl
|
||||
|
||||
![](https://img.shields.io/badge/owl-v1.0-%2300f)
|
||||
![](https://img.shields.io/badge/owl-v1.1-blueviolet)
|
||||
|
||||
**owl** — is the minimalistic kn**owl**edge base web app written in Python.
|
||||
|
||||
@ -22,13 +22,11 @@ App is now available at [http://localhost:5000/](http://localhost:5000/).
|
||||
|
||||
This solution is suitable for creating documentation or maintaining a personal knowledge base.
|
||||
|
||||
New in `v1.0`:
|
||||
- This is brand new owl!
|
||||
- New frontend and refactored backend.
|
||||
- Bootstrap 5
|
||||
- Optional authentication.
|
||||
New in `v1.1`:
|
||||
- Switched to [Python-Markdown](https://github.com/Python-Markdown/markdown).
|
||||
- Added some new Markdown extensions.
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md)
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
# License
|
||||
|
||||
|
36
config.py
36
config.py
@ -1,16 +1,32 @@
|
||||
class Config(object):
|
||||
DEBUG = False
|
||||
SECRET_KEY = 'top_secret'
|
||||
SECRET_KEY = None
|
||||
PASSWORD_FILE = '.pw'
|
||||
SIGN_IN = False # Enable or disable authentication
|
||||
MARKDOWN_ROOT = 'docs/' # Path to your .md files
|
||||
MARKDOWN_DOWLOADS = True
|
||||
# See https://github.com/trentm/python-markdown2/wiki/Extras
|
||||
MARKDOWN2_EXTRAS = [
|
||||
'fenced-code-blocks',
|
||||
'markdown-in-html',
|
||||
'code-friendly',
|
||||
'header-ids',
|
||||
'strike',
|
||||
'tables'
|
||||
]
|
||||
MARKDOWN_EXTRAS = [
|
||||
'admonition',
|
||||
'attr_list',
|
||||
'codehilite',
|
||||
'def_list',
|
||||
'fenced_code',
|
||||
'md_in_html',
|
||||
'markdown_alerts',
|
||||
'markdown_del_ins',
|
||||
'tables',
|
||||
'toc'
|
||||
]
|
||||
MARKDOWN_EXTRAS_CONFIGS = {
|
||||
'markdown_alerts': {
|
||||
'info': 'alert alert-info',
|
||||
'note': 'alert alert-primary',
|
||||
'tip': 'alert alert-success',
|
||||
'success': 'alert alert-success',
|
||||
'warning': 'alert alert-warning',
|
||||
'danger': 'alert alert-danger'
|
||||
},
|
||||
'toc': {
|
||||
'toc_depth': '2-5'
|
||||
}
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
### Contents
|
||||
# @v@
|
||||
|
||||
- [Home](/)
|
||||
- [Home](/)
|
16
docs/HOME.md
16
docs/HOME.md
@ -1,6 +1,16 @@
|
||||
# @v@ owl took off!
|
||||
# owl took off!
|
||||
|
||||
Read the [Docs](https://owl.gch.icu/docs/) to get started.
|
||||
<pre class='raw'>
|
||||
, ,
|
||||
/\ /\
|
||||
((@v@))
|
||||
((;;; (\
|
||||
\ ;;; \'
|
||||
,V.V `
|
||||
`` ``
|
||||
</pre>
|
||||
|
||||
Also there is project's [git repository](https://gitea.gch.icu/gd/owl) ([mirror](https://github.com/gechandesu/owl)).
|
||||
Read the [Docs](https://owl.gch.icu/docs/){target="_blank"} to get started.
|
||||
|
||||
Also there is project's [git repository](https://gitea.gch.icu/gd/owl) ([mirror on GitHub](https://github.com/gechandesu/owl)).
|
||||
|
||||
|
34
owl.py
34
owl.py
@ -1,10 +1,12 @@
|
||||
__version__ = '1.1'
|
||||
|
||||
import os
|
||||
import re
|
||||
from functools import wraps
|
||||
from datetime import timedelta
|
||||
|
||||
import pygments
|
||||
from markdown2 import Markdown
|
||||
from markdown import Markdown
|
||||
|
||||
from flask import Flask
|
||||
from flask import request
|
||||
@ -30,31 +32,35 @@ def read_file(filepath: str) -> str:
|
||||
except IOError:
|
||||
return 'Error: Cannot read file: {}'.format(filepath)
|
||||
|
||||
def get_path(path: str) -> str:
|
||||
return os.path.join(app.config['MARKDOWN_ROOT'], path)
|
||||
|
||||
def render_html(filepath: str) -> str:
|
||||
markdown = Markdown(extras=app.config['MARKDOWN2_EXTRAS'])
|
||||
return markdown.convert(
|
||||
read_file(
|
||||
app.config['MARKDOWN_ROOT'] + filepath
|
||||
)
|
||||
)
|
||||
html = Markdown(
|
||||
extensions = app.config['MARKDOWN_EXTRAS'],
|
||||
extension_configs = app.config['MARKDOWN_EXTRAS_CONFIGS']
|
||||
)
|
||||
return html.convert(
|
||||
read_file(get_path(filepath))
|
||||
)
|
||||
|
||||
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.
|
||||
article = read_file(app.config['MARKDOWN_ROOT'] + filepath)
|
||||
article = read_file(get_path(filepath))
|
||||
pattern = re.compile(r'^\s*#\s.*')
|
||||
if pattern.search(article):
|
||||
return pattern.search(article).group().strip()[2:]
|
||||
else:
|
||||
return 'Error: Cannot parse title from file:'.format(filepath)
|
||||
return 'Error: Cannot parse title from file: {}'.format(filepath)
|
||||
|
||||
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 = []
|
||||
for tpl in r.findall(read_file(app.config['MARKDOWN_ROOT'] + filepath)):
|
||||
for tpl in r.findall(read_file(get_path(filepath))):
|
||||
for item in tpl:
|
||||
if re.match(r'\(.*\)', item):
|
||||
if item == '(/)':
|
||||
@ -67,8 +73,8 @@ def parse_content_links(filepath: str) -> list:
|
||||
return links
|
||||
|
||||
def check_password(password: str) -> bool:
|
||||
if os.path.exists('.pw'):
|
||||
pw_hash = read_file('.pw')
|
||||
if os.path.exists(app.config['PASSWORD_FILE']):
|
||||
pw_hash = read_file(app.config['PASSWORD_FILE'])
|
||||
return bcrypt.check_password_hash(pw_hash, password)
|
||||
else:
|
||||
return False
|
||||
@ -129,7 +135,7 @@ def index():
|
||||
@app.route('/<path:path>/')
|
||||
@login_required
|
||||
def get_article(path):
|
||||
if os.path.exists(app.config['MARKDOWN_ROOT'] + path + '.md'):
|
||||
if os.path.exists(get_path(path) + '.md'):
|
||||
return render_template(
|
||||
'index.j2',
|
||||
title = parse_title_from_markdown(path + '.md'),
|
||||
@ -144,7 +150,7 @@ def get_article(path):
|
||||
@app.route('/<path:path>.md')
|
||||
@login_required
|
||||
def download_article(path):
|
||||
if os.path.exists(app.config['MARKDOWN_ROOT'] + path + '.md') \
|
||||
if os.path.exists(get_path(path) + '.md') \
|
||||
and app.config['MARKDOWN_DOWLOADS']:
|
||||
return send_from_directory(
|
||||
app.config['MARKDOWN_ROOT'],
|
||||
|
@ -1,4 +1,6 @@
|
||||
flask>=1.1
|
||||
flask-bcrypt>=0.7
|
||||
markdown2>=2.3
|
||||
pygments>=2.6
|
||||
pygments>=2.6
|
||||
markdown>=3.3.4
|
||||
markdown-alerts>=0.1
|
||||
markdown-del-ins>=1.0.0
|
||||
|
@ -1,6 +1,6 @@
|
||||
body {
|
||||
font-family: 'Ubuntu', sans-serif;
|
||||
font-size: 19px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h1,
|
||||
@ -41,10 +41,28 @@ blockquote {
|
||||
|
||||
blockquote p { margin: 0; }
|
||||
|
||||
dl dt {
|
||||
padding:0;
|
||||
margin-top:16px;
|
||||
font-size:1em;
|
||||
font-style:italic;
|
||||
font-weight:bold
|
||||
}
|
||||
|
||||
dl dd {
|
||||
padding:0 2rem;
|
||||
margin-bottom:16px
|
||||
}
|
||||
|
||||
/* Delete margin for admonitions */
|
||||
.alert p:last-child {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Details and summary */
|
||||
|
||||
details, summary {
|
||||
display: block;
|
||||
margin: 1rem 0;
|
||||
transition: 200ms linear;
|
||||
}
|
||||
@ -54,37 +72,13 @@ summary {
|
||||
transition: .3s;
|
||||
}
|
||||
|
||||
/* Hide defaul marker */
|
||||
details > summary { list-style: none; }
|
||||
details summary::-webkit-details-marker { display: none; }
|
||||
|
||||
details[open] summary ~ * {
|
||||
animation: open 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes open {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
details summary:before {
|
||||
content: '+';
|
||||
font-family: 'Ubuntu Mono';
|
||||
font-size: 20px;
|
||||
display: inline-flex;
|
||||
padding: 0 0.3rem;
|
||||
}
|
||||
|
||||
details[open] summary:before {
|
||||
content: '-';
|
||||
font-family: 'Ubuntu Mono';
|
||||
font-size: 20px;
|
||||
display: inline-flex;
|
||||
padding: 0 0.3rem;
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Code styling */
|
||||
@ -92,7 +86,7 @@ details[open] summary:before {
|
||||
code,
|
||||
pre {
|
||||
font-family: 'Ubuntu Mono', monospace;
|
||||
font-size: 19px;
|
||||
font-size: 18px;
|
||||
color: #d0d0d0;
|
||||
}
|
||||
|
||||
@ -111,7 +105,7 @@ code {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.raw-pre {
|
||||
.raw {
|
||||
color: unset;
|
||||
background: unset;
|
||||
}
|
||||
@ -168,15 +162,6 @@ code {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Header bar */
|
||||
|
||||
.header {
|
||||
display: block;
|
||||
position: fixed;
|
||||
height: 5rem;
|
||||
background: unset;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
|
||||
.sidebar-toggle-btn {
|
||||
@ -227,6 +212,13 @@ code {
|
||||
|
||||
.sidebar a:hover { text-decoration: underline; }
|
||||
|
||||
.sidebar h1,
|
||||
.sidebar h2,
|
||||
.sidebar h3,
|
||||
.sidebar h4 {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.sidebar ul,
|
||||
.sidebar ol,
|
||||
.sidebar li {
|
||||
@ -301,7 +293,8 @@ article.wide { max-width: 980px; }
|
||||
@media (max-width: 1200px) {
|
||||
.header { background: #ffffff; }
|
||||
.sidebar { left: -300px; }
|
||||
.sidebar-toggle-btn { left: 1rem; }
|
||||
.sidebar-toggle-btn { left: 0.5rem; }
|
||||
.signout-btn { right: 0.5rem; }
|
||||
.content { margin-left: 0px; }
|
||||
.sidebar.hide { left: 0px; }
|
||||
.sidebar-toggle-btn.click { left: 316px; }
|
||||
|
@ -23,12 +23,6 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% if session['logged_in'] %}
|
||||
<div class="signout-btn">
|
||||
<a href="/signout/" title="Sign out"><i class="bi bi-box-arrow-right"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block content %}
|
||||
No content here
|
||||
{% endblock %}
|
||||
|
@ -6,34 +6,38 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- Header bar -->
|
||||
<div class="headerbar">
|
||||
<!-- Sidebar toggle button -->
|
||||
{# Sidebar toggle button #}
|
||||
<div class="sidebar-toggle-btn">
|
||||
<i class="bi bi-layout-sidebar-inset"></i>
|
||||
</div>
|
||||
{# Sign out button #}
|
||||
{% if session['logged_in'] %}
|
||||
<div class="signout-btn">
|
||||
<a href="/signout/" title="Sign out"><i class="bi bi-box-arrow-right"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<nav class="sidebar">
|
||||
<!-- Sidebar content -->
|
||||
{# Sidebar content #}
|
||||
{{ contents | safe }}
|
||||
</nav>
|
||||
|
||||
<main class="content">
|
||||
<!-- Page main content -->
|
||||
{# Page main content #}
|
||||
<div class="blank-1"></div>
|
||||
<div class="blank-2"></div>
|
||||
{# CURRENT PATH {{ current_path }} #}
|
||||
<article>
|
||||
{{ article | safe }}
|
||||
<div class="blank-2"></div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{# Pagination #}
|
||||
{% for link in links %}
|
||||
{% if link == current_path %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-6" style="float: left; padding: 0 15px 0 0;">
|
||||
<div class="col-sm-6 float-sm-start" style="margin-top: 8px;">
|
||||
{% if (links.index(link) - 1) > -1 %}
|
||||
<div class="list-group">
|
||||
<a href="{{ links[links.index(link) - 1] }}" class="list-group-item list-group-item-action">
|
||||
@ -42,18 +46,18 @@
|
||||
{# Unique handler for root URL #}
|
||||
<h5 class="mb-1">« {{ get_title('HOME/') }}</h5>
|
||||
{% else %}
|
||||
<h5 class="mb-1">« {{ get_title(links[links.index(link) - 1]) }}</h5>
|
||||
<h5 class="mb-1">« {{ get_title(links[links.index(link) - 1][1:]) }}</h5>
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-6" style="float: right; padding: 0 0 0 15px;">
|
||||
<div class="col-sm-6 float-sm-end" style="margin-top: 8px;">
|
||||
{% if (links.index(link) + 1) <= (get_len(links) - 1) %}
|
||||
<div class="list-group">
|
||||
<a href="{{ links[links.index(link) + 1] }}" class="list-group-item list-group-item-action" style="text-align: right;">
|
||||
<small class="mb-1">Next</small>
|
||||
<h5 class="mb-1">{{ get_title(links[links.index(link) + 1]) }} »</h5>
|
||||
<h5 class="mb-1">{{ get_title(links[links.index(link) + 1][1:]) }} »</h5>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
Loading…
Reference in New Issue
Block a user