init
This commit is contained in:
		
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
**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 %}
 | 
			
		||||
		Reference in New Issue
	
	Block a user