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
## 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

View File

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

View File

@ -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'
}
}

View File

@ -1,3 +1,3 @@
### Contents
# @v@
- [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)).

30
owl.py
View File

@ -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'],

View File

@ -1,4 +1,6 @@
flask>=1.1
flask-bcrypt>=0.7
markdown2>=2.3
pygments>=2.6
markdown>=3.3.4
markdown-alerts>=0.1
markdown-del-ins>=1.0.0

View File

@ -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; }

View File

@ -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 %}

View File

@ -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 %}