commit 73f23a591ce64d4b64ebee7a70b4edd576079f23
Author: gd
Date: Tue Mar 23 23:11:57 2021 +0300
init
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4f04a84
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+__pycache__/
+*~
+*.swp
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..696ed1d
--- /dev/null
+++ b/CHANGELOG.md
@@ -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.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..68a49da
--- /dev/null
+++ b/LICENSE
@@ -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
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..def1b19
--- /dev/null
+++ b/README.md
@@ -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).
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..e564299
--- /dev/null
+++ b/config.py
@@ -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'
+ ]
\ No newline at end of file
diff --git a/docs/CONTENTS.md b/docs/CONTENTS.md
new file mode 100644
index 0000000..5587640
--- /dev/null
+++ b/docs/CONTENTS.md
@@ -0,0 +1,3 @@
+### Contents
+
+- [Home](/)
diff --git a/docs/HOME.md b/docs/HOME.md
new file mode 100644
index 0000000..9de7ccf
--- /dev/null
+++ b/docs/HOME.md
@@ -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)).
+
diff --git a/owl.py b/owl.py
new file mode 100644
index 0000000..2c244df
--- /dev/null
+++ b/owl.py
@@ -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('//')
+@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('/.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()
\ No newline at end of file
diff --git a/pass.py b/pass.py
new file mode 100644
index 0000000..95ea04e
--- /dev/null
+++ b/pass.py
@@ -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`.')
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..c8afa2f
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+flask>=1.1
+flask-bcrypt>=0.7
+markdown2>=2.3
+pygments>=2.6
\ No newline at end of file
diff --git a/static/css/codehilite.css b/static/css/codehilite.css
new file mode 100644
index 0000000..01ab791
--- /dev/null
+++ b/static/css/codehilite.css
@@ -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 */
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644
index 0000000..a09f927
--- /dev/null
+++ b/static/css/style.css
@@ -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; }
+}
\ No newline at end of file
diff --git a/static/images/favicon.ico b/static/images/favicon.ico
new file mode 100644
index 0000000..88fec17
Binary files /dev/null and b/static/images/favicon.ico differ
diff --git a/static/js/script.js b/static/js/script.js
new file mode 100644
index 0000000..2b5fe11
--- /dev/null
+++ b/static/js/script.js
@@ -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('');
+
+// 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 = '';
+ if (id) {
+ return $el.append($("").addClass("header-link").attr("href", "#" + id).html(icon));
+ }
+ });
+});
\ No newline at end of file
diff --git a/templates/404.j2 b/templates/404.j2
new file mode 100644
index 0000000..83c25e1
--- /dev/null
+++ b/templates/404.j2
@@ -0,0 +1,15 @@
+{% extends 'base.j2' %}
+
+{% block title %}
+ Page not found
+{% endblock %}
+
+{% block content %}
+
+
+ Page not found
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/base.j2 b/templates/base.j2
new file mode 100644
index 0000000..612b559
--- /dev/null
+++ b/templates/base.j2
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+ {% block title %}
+ owl
+ {% endblock %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if session['logged_in'] %}
+
+ {% endif %}
+
+ {% block content %}
+ No content here
+ {% endblock %}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/index.j2 b/templates/index.j2
new file mode 100644
index 0000000..50f830e
--- /dev/null
+++ b/templates/index.j2
@@ -0,0 +1,69 @@
+{% extends 'base.j2' %}
+
+{% block title %}
+ {{ title }}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+ {# CURRENT PATH {{ current_path }} #}
+
+ {{ article | safe }}
+
+
+
+ {% for link in links %}
+ {% if link == current_path %}
+
+
+
+ {% if (links.index(link) - 1) > -1 %}
+
+ {% endif %}
+
+
+ {% if (links.index(link) + 1) <= (get_len(links) - 1) %}
+
+ {% endif %}
+
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/signin.j2 b/templates/signin.j2
new file mode 100644
index 0000000..4660e99
--- /dev/null
+++ b/templates/signin.j2
@@ -0,0 +1,22 @@
+{% extends 'base.j2' %}
+
+{% block title %}
+ Sign in
+{% endblock %}
+
+{% block content %}
+
+
+
+ {% endif %}
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file