init
This commit is contained in:
commit
73f23a591c
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*~
|
||||
*.swp
|
30
CHANGELOG.md
Normal file
30
CHANGELOG.md
Normal 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
24
LICENSE
Normal 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
35
README.md
Normal 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
16
config.py
Normal 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
3
docs/CONTENTS.md
Normal file
@ -0,0 +1,3 @@
|
||||
### Contents
|
||||
|
||||
- [Home](/)
|
6
docs/HOME.md
Normal file
6
docs/HOME.md
Normal 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
157
owl.py
Normal 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
26
pass.py
Normal 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
4
requirements.txt
Normal 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
82
static/css/codehilite.css
Normal 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
310
static/css/style.css
Normal 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
BIN
static/images/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
63
static/js/script.js
Normal file
63
static/js/script.js
Normal 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
15
templates/404.j2
Normal 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
47
templates/base.j2
Normal 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
69
templates/index.j2
Normal 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
22
templates/signin.j2
Normal 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 %}
|
Loading…
Reference in New Issue
Block a user