This commit is contained in:
gd 2021-03-23 23:11:57 +03:00
commit 73f23a591c
18 changed files with 912 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__/
*~
*.swp

30
CHANGELOG.md Normal file
View File

@ -0,0 +1,30 @@
# Changelog
## v1.0 2021.03.23
### Added
- Bootstrap 5. Brand new frontend.
- Bootstrap Icons.
- `Flask.session` based authentication. Can be enabled in `config.py`. Password encrypted by `bcrypt`.
- `pass.py` to set password.
- Normal 404 error page.
- `CONTENTS.md` parser. You can navigate between articles.
- Article title parser. The title is now displayed in the title of the page.
- New shitcode. It will be refactored in next versions.
### Changed
- `contents.md` and `home.md` renamed to `CONTENTS.md` and `HOME.md`.
- `native` Pygments theme by default.
- File search algorithm changed. Now the viewing of files nested in folders works.
- The main application code has been moved to the `owl.py` module. The launch point of the application is now also the `owl.py`, and not the `wsgi.py`. It may not be the best architectural solution, but it seems to be the most concise now.
### Removed
- Old shitcode removed. See Changed.
- Old frontend and templates.
## v0.1 2020.08.15
First version released.

24
LICENSE Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

35
README.md Normal file
View File

@ -0,0 +1,35 @@
# owl
![](https://img.shields.io/badge/owl-v1.0-%2300f)
**owl** — is the minimalistic kn**owl**edge base web app written in Python.
See full docs and demo here: [https://owl.gch.icu/docs/](https://owl.gch.icu/docs/).
Run **owl** in five lines:
```bash
python3 -m venv env
source env/bin/activate
git clone https://github.com/gechandesu/owl.git && cd owl
pip install -r requirements.txt
python owl.py
```
App is now available at [http://localhost:5000/](http://localhost:5000/).
**owl** doesn't use a database, all files are stored in plain text.
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.
See [CHANGELOG.md](CHANGELOG.md)
# License
This software is licensed under The Unlicense. See [LICENSE](LICENSE).

16
config.py Normal file
View File

@ -0,0 +1,16 @@
class Config(object):
DEBUG = False
SECRET_KEY = 'top_secret'
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'
]

3
docs/CONTENTS.md Normal file
View File

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

6
docs/HOME.md Normal file
View File

@ -0,0 +1,6 @@
# @v@ owl took off!
Read the [Docs](https://owl.gch.icu/docs/) to get started.
Also there is project's [git repository](https://gitea.gch.icu/gd/owl) ([mirror](https://github.com/gechandesu/owl)).

157
owl.py Normal file
View File

@ -0,0 +1,157 @@
import os
import re
from functools import wraps
from datetime import timedelta
import pygments
from markdown2 import Markdown
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)
def render_html(filepath: str) -> str:
markdown = Markdown(extras=app.config['MARKDOWN2_EXTRAS'])
return markdown.convert(
read_file(
app.config['MARKDOWN_ROOT'] + 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)
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)
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 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:
if os.path.exists('.pw'):
pw_hash = read_file('.pw')
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):
if os.path.exists(app.config['MARKDOWN_ROOT'] + path + '.md'):
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):
if os.path.exists(app.config['MARKDOWN_ROOT'] + path + '.md') \
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()

26
pass.py Normal file
View File

@ -0,0 +1,26 @@
from getpass import getpass
from owl import app
from flask_bcrypt import Bcrypt
bcrypt = Bcrypt(app)
def generate_pw_hash(password, file):
pw_hash = bcrypt.generate_password_hash(password).decode('utf-8')
with open(file, 'w') as pwfile:
pwfile.write(pw_hash)
if __name__ == '__main__':
with app.app_context():
file = input('Enter password file name (default: .pw): ')
if not file:
file = '.pw'
password = getpass('Enter new password: ')
confirm = getpass('Confirm password: ')
if password != confirm:
print('Abort! Password mismatch.')
exit()
generate_pw_hash(password, file)
print('Success! New password file created: {}'.format(file))
if file != '.pw':
print('Don\'t forgot change password file name in `config.py`.')

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
flask>=1.1
flask-bcrypt>=0.7
markdown2>=2.3
pygments>=2.6

82
static/css/codehilite.css Normal file
View File

@ -0,0 +1,82 @@
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.hll { background-color: #404040 }
.c { color: #999999; font-style: italic } /* Comment */
.err { color: #a61717; background-color: #e3d2d2 } /* Error */
.esc { color: #d0d0d0 } /* Escape */
.g { color: #d0d0d0 } /* Generic */
.k { color: #6ab825; font-weight: bold } /* Keyword */
.l { color: #d0d0d0 } /* Literal */
.n { color: #d0d0d0 } /* Name */
.o { color: #d0d0d0 } /* Operator */
.x { color: #d0d0d0 } /* Other */
.p { color: #d0d0d0 } /* Punctuation */
.ch { color: #999999; font-style: italic } /* Comment.Hashbang */
.cm { color: #999999; font-style: italic } /* Comment.Multiline */
.cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
.cpf { color: #999999; font-style: italic } /* Comment.PreprocFile */
.c1 { color: #999999; font-style: italic } /* Comment.Single */
.cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
.gd { color: #d22323 } /* Generic.Deleted */
.ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
.gr { color: #d22323 } /* Generic.Error */
.gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
.gi { color: #589819 } /* Generic.Inserted */
.go { color: #cccccc } /* Generic.Output */
.gp { color: #aaaaaa } /* Generic.Prompt */
.gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
.gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
.gt { color: #d22323 } /* Generic.Traceback */
.kc { color: #6ab825; font-weight: bold } /* Keyword.Constant */
.kd { color: #6ab825; font-weight: bold } /* Keyword.Declaration */
.kn { color: #6ab825; font-weight: bold } /* Keyword.Namespace */
.kp { color: #6ab825 } /* Keyword.Pseudo */
.kr { color: #6ab825; font-weight: bold } /* Keyword.Reserved */
.kt { color: #6ab825; font-weight: bold } /* Keyword.Type */
.ld { color: #d0d0d0 } /* Literal.Date */
.m { color: #3677a9 } /* Literal.Number */
.s { color: #ed9d13 } /* Literal.String */
.na { color: #bbbbbb } /* Name.Attribute */
.nb { color: #24909d } /* Name.Builtin */
.nc { color: #447fcf; text-decoration: underline } /* Name.Class */
.no { color: #40ffff } /* Name.Constant */
.nd { color: #ffa500 } /* Name.Decorator */
.ni { color: #d0d0d0 } /* Name.Entity */
.ne { color: #bbbbbb } /* Name.Exception */
.nf { color: #447fcf } /* Name.Function */
.nl { color: #d0d0d0 } /* Name.Label */
.nn { color: #447fcf; text-decoration: underline } /* Name.Namespace */
.nx { color: #d0d0d0 } /* Name.Other */
.py { color: #d0d0d0 } /* Name.Property */
.nt { color: #6ab825; font-weight: bold } /* Name.Tag */
.nv { color: #40ffff } /* Name.Variable */
.ow { color: #6ab825; font-weight: bold } /* Operator.Word */
.w { color: #666666 } /* Text.Whitespace */
.mb { color: #3677a9 } /* Literal.Number.Bin */
.mf { color: #3677a9 } /* Literal.Number.Float */
.mh { color: #3677a9 } /* Literal.Number.Hex */
.mi { color: #3677a9 } /* Literal.Number.Integer */
.mo { color: #3677a9 } /* Literal.Number.Oct */
.sa { color: #ed9d13 } /* Literal.String.Affix */
.sb { color: #ed9d13 } /* Literal.String.Backtick */
.sc { color: #ed9d13 } /* Literal.String.Char */
.dl { color: #ed9d13 } /* Literal.String.Delimiter */
.sd { color: #ed9d13 } /* Literal.String.Doc */
.s2 { color: #ed9d13 } /* Literal.String.Double */
.se { color: #ed9d13 } /* Literal.String.Escape */
.sh { color: #ed9d13 } /* Literal.String.Heredoc */
.si { color: #ed9d13 } /* Literal.String.Interpol */
.sx { color: #ffa500 } /* Literal.String.Other */
.sr { color: #ed9d13 } /* Literal.String.Regex */
.s1 { color: #ed9d13 } /* Literal.String.Single */
.ss { color: #ed9d13 } /* Literal.String.Symbol */
.bp { color: #24909d } /* Name.Builtin.Pseudo */
.fm { color: #447fcf } /* Name.Function.Magic */
.vc { color: #40ffff } /* Name.Variable.Class */
.vg { color: #40ffff } /* Name.Variable.Global */
.vi { color: #40ffff } /* Name.Variable.Instance */
.vm { color: #40ffff } /* Name.Variable.Magic */
.il { color: #3677a9 } /* Literal.Number.Integer.Long */

310
static/css/style.css Normal file
View File

@ -0,0 +1,310 @@
body {
font-family: 'Ubuntu', sans-serif;
font-size: 19px;
}
h1,
h2,
h3,
h4 { margin: 1rem 0; }
.header-link {
position: absolute;
margin-left: .2em;
opacity: 0;
}
h1:hover .header-link,
h2:hover .header-link,
h3:hover .header-link,
h4:hover .header-link {
opacity: 100;
}
.sidebar h1 .header-link,
.sidebar h2 .header-link,
.sidebar h3 .header-link,
.sidebar h4 .header-link {
display: none;
}
a { color: #212529; }
a:hover { color: #707275; }
blockquote {
border-left: 4px solid #212529;
margin: 1rem 0;
padding: 0.2rem 1rem;
color: #212529;
}
blockquote p { margin: 0; }
/* Details and summary */
details, summary {
display: block;
margin: 1rem 0;
transition: 200ms linear;
}
summary {
cursor: pointer;
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;
}
/* Code styling */
code,
pre {
font-family: 'Ubuntu Mono', monospace;
font-size: 19px;
color: #d0d0d0;
}
pre code { background: unset; padding: unset; }
pre {
background: #1c1d21;
border-radius: 6px;
padding: 1rem;
}
code {
color: #1c1d21;
background: #ffeff0;
padding: 4px;
border-radius: 6px;
}
.raw-pre {
color: unset;
background: unset;
}
/* Large headings */
.large-h { font-size: 42px; }
.title-h { font-size: 72px; line-height: 1.1; }
/* Blank spaces */
.blank-1 { display: block; height: 1rem; }
.blank-2 { display: block; height: 2rem; }
.blank-5 { display: block; height: 5rem; }
/* Sign in form */
.form-signin {
position: absolute;
left: 50%;
top: 45%;
transform: translate(-50%,-50%);
min-width: 360px;
}
#inputPassword { margin-bottom: 8px; }
/* Sign out button */
.signout-btn {
z-index: 1001;
position: fixed;
top: 16px;
right: 1rem;
height: 46px;
width: 46px;
text-align: center;
border-radius: 3px;
cursor: pointer;
}
.signout-btn i {
font-size: 30px;
line-height: 46px;
}
.signout-btn a { color: #212529; }
/* 404 page */
.page_not_found {
position: absolute;
left: 50%;
top: 45%;
transform: translate(-50%,-50%);
text-align: center;
}
/* Header bar */
.header {
display: block;
position: fixed;
height: 5rem;
background: unset;
}
/* Sidebar */
.sidebar-toggle-btn {
z-index: 1001;
position: fixed;
top: 1rem;
left: 316px;
height: 46px;
width: 46px;
text-align: center;
border-radius: 3px;
cursor: pointer;
transition: left 0.4s ease;
}
.sidebar-toggle-btn.click { left: 1rem; }
.sidebar-toggle-btn i {
font-size: 26px;
line-height: 46px;
}
.sidebar {
z-index: 1000;
position: fixed;
width: 300px;
height: 100%;
left: 0px;
padding: 1rem;
overflow: auto;
transition: left 0.4s ease;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
background: #ffffff;
}
.sidebar.hide { left: -300px; }
/* Side menu */
.sidebar a {
display: block;
width: auto;
padding: 2px 0;
text-decoration: none;
transition: 0.2s linear;
color: #212529;
}
.sidebar a:hover { text-decoration: underline; }
.sidebar ul,
.sidebar ol,
.sidebar li {
list-style-type: none;
list-style-position: inside;
position: relative;
padding: 3px 0 3px 10px;
margin: 0;
color: #6c757d;
}
.mark {
display: inline;
left: -10px;
bottom: 1px;
width: 100%;
padding: 4px;
border-radius: 6px;
position: absolute;
color: #ffffff;
background: unset;
}
.mark:hover { color: #212529; }
.mark::before { content: '•'; }
/* Content container toggle */
.content {
margin-left: 300px;
transition: margin-left 0.5s;
}
.content.wide { margin-left: 0px; }
/* Back to top button */
.to-top-btn {
z-index: 1001;
display: none;
position: fixed;
height: 100%;
width: 3rem;
top: 5rem;
left: 315px;
cursor: pointer;
text-align: center;
transition: left 0.4s ease;
}
.to-top-btn i {
font-size: 26px;
line-height: 46px;
}
.to-top-btn.wide { left: 15px; }
.to-top-btn.show { display: block; }
/* Content block */
article {
display: block;
margin: auto;
padding: 1rem;
max-width: 840px;
}
article.wide { max-width: 980px; }
/* Responsivity */
@media (max-width: 1200px) {
.header { background: #ffffff; }
.sidebar { left: -300px; }
.sidebar-toggle-btn { left: 1rem; }
.content { margin-left: 0px; }
.sidebar.hide { left: 0px; }
.sidebar-toggle-btn.click { left: 316px; }
.to-top-btn.show { display: none; }
article.wide { max-width: 840px; }
}

BIN
static/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

63
static/js/script.js Normal file
View File

@ -0,0 +1,63 @@
// Here vanilla JavaScript is mixed with JQuery (3.6.0 slim).
// This is very bad. But works. TODO: delete jQuery.
// Toggle sidebar and change elements width.
$('.sidebar-toggle-btn').click(function(){
$(this).toggleClass("click");
$('.sidebar').toggleClass("hide");
$('#to-top-btn').toggleClass("wide");
$('article').toggleClass("wide");
if (screen.width > 1200 ) {
$('.content').toggleClass("wide");
}
});
// Add styling for tables.
$('table').toggleClass("table table-bordered")
// Back to top button.
var btn = $('#to-top-btn');
$(window).scroll(function() {
if ($(window).scrollTop() > 300) {
btn.addClass('show');
} else {
btn.removeClass('show');
}
});
btn.on('click', function(e) {
e.preventDefault();
window.scrollTo({ top: 0, behavior: "smooth"});
});
// Add marker to sidebar links.
$('.sidebar a').append('<div class="mark"></div>');
// Highlight current page link in sidebar -
// toggle marker on current page link.
var pathname = window.location.pathname;
var links = document.getElementsByTagName("a");
for (var element of links) {
var ref = element.getAttribute('href');
if (ref.substr(-1) !== "/") {
ref = ref + '/';
}
if (ref == pathname) {
$(element).children('.mark').css('color', '#212529');
}
}
// Add paragraph button aside of headings
$(function() {
return $("h1, h2, h3, h4").each(function(i, el) {
var $el, icon, id;
$el = $(el);
id = $el.attr('id');
icon = '<i class="bi bi-paragraph"></i>';
if (id) {
return $el.append($("<a />").addClass("header-link").attr("href", "#" + id).html(icon));
}
});
});

15
templates/404.j2 Normal file
View File

@ -0,0 +1,15 @@
{% extends 'base.j2' %}
{% block title %}
Page not found
{% endblock %}
{% block content %}
<main class="page_not_found">
<h1 class="large-h">Page not found</h1>
<div class="blank-2"></div>
<a href="/"><button type="button" class="btn btn-primary btn-lg btn-dark">Go back</button></a>
</main>
{% endblock %}

47
templates/base.j2 Normal file
View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>
{% block title %}
owl
{% endblock %}
</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='images/favicon.ico')}}" type="image/x-icon">
<!-- Bottrstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<!-- Bottstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.0/font/bootstrap-icons.css">
<!-- Ubuntu Mono from Google Fonts -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;500&display=swap" rel="stylesheet">
<!-- Custom CSS -->
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/style.css')}}">
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/codehilite.css')}}">
</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 %}
<div class="to-top-btn" id="to-top-btn">
<i class="bi bi-arrow-up-square"></i>
</div>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js" integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous"></script>
<!-- Minified JQuery 3.6.0 slim -->
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js" integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI=" crossorigin="anonymous"></script>
<!-- Custom JS -->
<script src="{{ url_for('static', filename='js/script.js')}}"></script>
</body>
</html>

69
templates/index.j2 Normal file
View File

@ -0,0 +1,69 @@
{% extends 'base.j2' %}
{% block title %}
{{ title }}
{% endblock %}
{% block content %}
<!-- Header bar -->
<div class="headerbar">
<!-- Sidebar toggle button -->
<div class="sidebar-toggle-btn">
<i class="bi bi-layout-sidebar-inset"></i>
</div>
</div>
<nav class="sidebar">
<!-- Sidebar content -->
{{ contents | safe }}
</nav>
<main class="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 -->
{% 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;">
{% 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">
<small class="mb-1">Previous</small>
{% if links[links.index(link) - 1] == '/' %}
{# 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>
{% endif %}
</a>
</div>
{% endif %}
</div>
<div class="col-sm-6" style="float: right; padding: 0 0 0 15px;">
{% 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>
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% endfor %}
</article>
<div class="blank-5"></div>
</main>
{% endblock %}

22
templates/signin.j2 Normal file
View File

@ -0,0 +1,22 @@
{% extends 'base.j2' %}
{% block title %}
Sign in
{% endblock %}
{% block content %}
<main class="form-signin">
<form action="/signin/" method="POST">
<center><h1 class="title-h">@v@</h1></center>
<div class="blank-1"></div>
{% if wrong_pw %}
<p><center style="color: #ff0000;">Wrong password.</center></p>
{% endif %}
<label for="inputPassword" class="visually-hidden">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
<button class="w-100 btn btn-lg btn-primary btn-dark" type="submit">Sign in</button>
</form>
</main>
{% endblock %}