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