This commit is contained in:
gd 2021-03-28 13:21:31 +03:00
parent 73f23a591c
commit c024615796
10 changed files with 135 additions and 95 deletions

View File

@ -1,5 +1,22 @@
# Changelog # 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 ## v1.0 2021.03.23
### Added ### Added
@ -27,4 +44,4 @@
## v0.1 2020.08.15 ## v0.1 2020.08.15
First version released. First version released.

View File

@ -1,6 +1,6 @@
# owl # 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. **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. This solution is suitable for creating documentation or maintaining a personal knowledge base.
New in `v1.0`: New in `v1.1`:
- This is brand new owl! - Switched to [Python-Markdown](https://github.com/Python-Markdown/markdown).
- New frontend and refactored backend. - Added some new Markdown extensions.
- Bootstrap 5
- Optional authentication.
See [CHANGELOG.md](CHANGELOG.md) See [CHANGELOG.md](CHANGELOG.md).
# License # License

View File

@ -1,16 +1,32 @@
class Config(object): class Config(object):
DEBUG = False DEBUG = False
SECRET_KEY = 'top_secret' SECRET_KEY = None
PASSWORD_FILE = '.pw' PASSWORD_FILE = '.pw'
SIGN_IN = False # Enable or disable authentication SIGN_IN = False # Enable or disable authentication
MARKDOWN_ROOT = 'docs/' # Path to your .md files MARKDOWN_ROOT = 'docs/' # Path to your .md files
MARKDOWN_DOWLOADS = True MARKDOWN_DOWLOADS = True
# See https://github.com/trentm/python-markdown2/wiki/Extras MARKDOWN_EXTRAS = [
MARKDOWN2_EXTRAS = [ 'admonition',
'fenced-code-blocks', 'attr_list',
'markdown-in-html', 'codehilite',
'code-friendly', 'def_list',
'header-ids', 'fenced_code',
'strike', 'md_in_html',
'tables' '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'
}
}

View File

@ -1,3 +1,3 @@
### Contents # @v@
- [Home](/) - [Home](/)

View File

@ -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
View File

@ -1,10 +1,12 @@
__version__ = '1.1'
import os import os
import re import re
from functools import wraps from functools import wraps
from datetime import timedelta from datetime import timedelta
import pygments import pygments
from markdown2 import Markdown from markdown import Markdown
from flask import Flask from flask import Flask
from flask import request from flask import request
@ -30,31 +32,35 @@ def read_file(filepath: str) -> str:
except IOError: except IOError:
return 'Error: Cannot read file: {}'.format(filepath) 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: def render_html(filepath: str) -> str:
markdown = Markdown(extras=app.config['MARKDOWN2_EXTRAS']) html = Markdown(
return markdown.convert( extensions = app.config['MARKDOWN_EXTRAS'],
read_file( extension_configs = app.config['MARKDOWN_EXTRAS_CONFIGS']
app.config['MARKDOWN_ROOT'] + filepath )
) return html.convert(
) read_file(get_path(filepath))
)
def parse_title_from_markdown(filepath: str) -> str: def parse_title_from_markdown(filepath: str) -> str:
# This function parses article title from first level heading. # This function parses article title from first level heading.
# It returns the occurrence of the first heading, and there # It returns the occurrence of the first heading, and there
# can be nothing before it except empty lines and spaces. # 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.*') pattern = re.compile(r'^\s*#\s.*')
if pattern.search(article): if pattern.search(article):
return pattern.search(article).group().strip()[2:] return pattern.search(article).group().strip()[2:]
else: 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: def parse_content_links(filepath: str) -> list:
# This function returns a list of links from a Markdown file. # This function returns a list of links from a Markdown file.
# Only links contained in the list (ul ol li) are parsed. # Only links contained in the list (ul ol li) are parsed.
r = re.compile(r'(.*(-|\+|\*|\d).?\[.*\])(\(.*\))', re.MULTILINE) r = re.compile(r'(.*(-|\+|\*|\d).?\[.*\])(\(.*\))', re.MULTILINE)
links = [] 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: for item in tpl:
if re.match(r'\(.*\)', item): if re.match(r'\(.*\)', item):
if item == '(/)': if item == '(/)':
@ -67,8 +73,8 @@ def parse_content_links(filepath: str) -> list:
return links return links
def check_password(password: str) -> bool: def check_password(password: str) -> bool:
if os.path.exists('.pw'): if os.path.exists(app.config['PASSWORD_FILE']):
pw_hash = read_file('.pw') pw_hash = read_file(app.config['PASSWORD_FILE'])
return bcrypt.check_password_hash(pw_hash, password) return bcrypt.check_password_hash(pw_hash, password)
else: else:
return False return False
@ -129,7 +135,7 @@ def index():
@app.route('/<path:path>/') @app.route('/<path:path>/')
@login_required @login_required
def get_article(path): 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( return render_template(
'index.j2', 'index.j2',
title = parse_title_from_markdown(path + '.md'), title = parse_title_from_markdown(path + '.md'),
@ -144,7 +150,7 @@ def get_article(path):
@app.route('/<path:path>.md') @app.route('/<path:path>.md')
@login_required @login_required
def download_article(path): 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']: and app.config['MARKDOWN_DOWLOADS']:
return send_from_directory( return send_from_directory(
app.config['MARKDOWN_ROOT'], app.config['MARKDOWN_ROOT'],

View File

@ -1,4 +1,6 @@
flask>=1.1 flask>=1.1
flask-bcrypt>=0.7 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

View File

@ -1,6 +1,6 @@
body { body {
font-family: 'Ubuntu', sans-serif; font-family: 'Ubuntu', sans-serif;
font-size: 19px; font-size: 18px;
} }
h1, h1,
@ -41,10 +41,28 @@ blockquote {
blockquote p { margin: 0; } 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 and summary */
details, summary { details, summary {
display: block;
margin: 1rem 0; margin: 1rem 0;
transition: 200ms linear; transition: 200ms linear;
} }
@ -54,37 +72,13 @@ summary {
transition: .3s; transition: .3s;
} }
/* Hide defaul marker */
details > summary { list-style: none; }
details summary::-webkit-details-marker { display: none; }
details[open] summary ~ * { details[open] summary ~ * {
animation: open 0.3s ease-in-out; animation: open 0.3s ease-in-out;
} }
@keyframes open { @keyframes open {
0% { 0% { opacity: 0; }
opacity: 0; 100% { opacity: 1; }
}
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;
} }
/* Code styling */ /* Code styling */
@ -92,7 +86,7 @@ details[open] summary:before {
code, code,
pre { pre {
font-family: 'Ubuntu Mono', monospace; font-family: 'Ubuntu Mono', monospace;
font-size: 19px; font-size: 18px;
color: #d0d0d0; color: #d0d0d0;
} }
@ -111,7 +105,7 @@ code {
border-radius: 6px; border-radius: 6px;
} }
.raw-pre { .raw {
color: unset; color: unset;
background: unset; background: unset;
} }
@ -168,15 +162,6 @@ code {
text-align: center; text-align: center;
} }
/* Header bar */
.header {
display: block;
position: fixed;
height: 5rem;
background: unset;
}
/* Sidebar */ /* Sidebar */
.sidebar-toggle-btn { .sidebar-toggle-btn {
@ -227,6 +212,13 @@ code {
.sidebar a:hover { text-decoration: underline; } .sidebar a:hover { text-decoration: underline; }
.sidebar h1,
.sidebar h2,
.sidebar h3,
.sidebar h4 {
margin-left: 1rem;
}
.sidebar ul, .sidebar ul,
.sidebar ol, .sidebar ol,
.sidebar li { .sidebar li {
@ -301,7 +293,8 @@ article.wide { max-width: 980px; }
@media (max-width: 1200px) { @media (max-width: 1200px) {
.header { background: #ffffff; } .header { background: #ffffff; }
.sidebar { left: -300px; } .sidebar { left: -300px; }
.sidebar-toggle-btn { left: 1rem; } .sidebar-toggle-btn { left: 0.5rem; }
.signout-btn { right: 0.5rem; }
.content { margin-left: 0px; } .content { margin-left: 0px; }
.sidebar.hide { left: 0px; } .sidebar.hide { left: 0px; }
.sidebar-toggle-btn.click { left: 316px; } .sidebar-toggle-btn.click { left: 316px; }

View File

@ -23,12 +23,6 @@
</head> </head>
<body> <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 %} {% block content %}
No content here No content here
{% endblock %} {% endblock %}

View File

@ -6,34 +6,38 @@
{% block content %} {% block content %}
<!-- Header bar -->
<div class="headerbar"> <div class="headerbar">
<!-- Sidebar toggle button --> {# Sidebar toggle button #}
<div class="sidebar-toggle-btn"> <div class="sidebar-toggle-btn">
<i class="bi bi-layout-sidebar-inset"></i> <i class="bi bi-layout-sidebar-inset"></i>
</div> </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> </div>
<nav class="sidebar"> <nav class="sidebar">
<!-- Sidebar content --> {# Sidebar content #}
{{ contents | safe }} {{ contents | safe }}
</nav> </nav>
<main class="content"> <main class="content">
<!-- Page main content --> {# Page main content #}
<div class="blank-1"></div> <div class="blank-1"></div>
<div class="blank-2"></div> <div class="blank-2"></div>
{# CURRENT PATH {{ current_path }} #}
<article> <article>
{{ article | safe }} {{ article | safe }}
<div class="blank-2"></div> <div class="blank-2"></div>
<!-- Pagination --> {# Pagination #}
{% for link in links %} {% for link in links %}
{% if link == current_path %} {% if link == current_path %}
<div class="container"> <div class="container">
<div class="row"> <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 %} {% if (links.index(link) - 1) > -1 %}
<div class="list-group"> <div class="list-group">
<a href="{{ links[links.index(link) - 1] }}" class="list-group-item list-group-item-action"> <a href="{{ links[links.index(link) - 1] }}" class="list-group-item list-group-item-action">
@ -42,18 +46,18 @@
{# Unique handler for root URL #} {# Unique handler for root URL #}
<h5 class="mb-1">« {{ get_title('HOME/') }}</h5> <h5 class="mb-1">« {{ get_title('HOME/') }}</h5>
{% else %} {% 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 %} {% endif %}
</a> </a>
</div> </div>
{% endif %} {% endif %}
</div> </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) %} {% if (links.index(link) + 1) <= (get_len(links) - 1) %}
<div class="list-group"> <div class="list-group">
<a href="{{ links[links.index(link) + 1] }}" class="list-group-item list-group-item-action" style="text-align: right;"> <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> <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> </a>
</div> </div>
{% endif %} {% endif %}