the footnote extra expects to only parse one document over the Markup's lifetime, and writes the footnotes to the bottom of every page that is rendered (again assuming only one) with links back to the reference having one parser for the entire app, naturally, introduced ever-increasing footnote links and every footnote on the site showing up on every page. this was not intended in some light testing, doing this per-request has a nominal effect on performance
120 lines
4.4 KiB
Python
120 lines
4.4 KiB
Python
"""General page functionality."""
|
|
import datetime
|
|
import logging
|
|
import os
|
|
|
|
from flask import Blueprint, Markup, abort
|
|
from flask import current_app as app
|
|
from flask import make_response, redirect, render_template, request
|
|
from tzlocal import get_localzone
|
|
|
|
from incorporealcms.lib import get_meta_str, init_md
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
bp = Blueprint('pages', __name__, url_prefix='/')
|
|
|
|
|
|
@bp.route('/', defaults={'path': 'index'})
|
|
@bp.route('/<path:path>')
|
|
def display_page(path):
|
|
"""Get the file contents of the requested path and render the file."""
|
|
if is_file_path_actually_dir_path(path):
|
|
return redirect(f'{path}/', code=301)
|
|
|
|
resolved_path = resolve_page_file(path)
|
|
logger.debug("received request for path '%s', resolved to '%s'", path, resolved_path)
|
|
try:
|
|
with app.open_instance_resource(resolved_path, 'r') as entry_file:
|
|
logger.debug("file '%s' found", resolved_path)
|
|
parent_navs = generate_parent_navs(path)
|
|
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone())
|
|
entry = entry_file.read()
|
|
except FileNotFoundError:
|
|
logger.warning("requested path '%s' (resolved path '%s') not found!", path, resolved_path)
|
|
abort(404)
|
|
else:
|
|
md = init_md()
|
|
content = Markup(md.convert(entry))
|
|
logger.debug("file metadata: %s", md.Meta)
|
|
|
|
return render('base.html', title=get_meta_str(md, 'title'), description=get_meta_str(md, 'description'),
|
|
image=get_meta_str(md, 'image'), base_url=request.base_url, content=content, navs=parent_navs,
|
|
mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z'))
|
|
|
|
|
|
def render(template_name_or_list, **context):
|
|
"""Wrap Flask's render_template.
|
|
|
|
* Determine the proper site theme to use in the template and provide it.
|
|
"""
|
|
PAGE_STYLES = {
|
|
'dark': 'css/dark.css',
|
|
'light': 'css/light.css',
|
|
}
|
|
|
|
selected_style = request.args.get('style', None)
|
|
if selected_style:
|
|
user_style = selected_style
|
|
else:
|
|
user_style = request.cookies.get('user-style')
|
|
logger.debug("user style cookie: %s", user_style)
|
|
context['user_style'] = PAGE_STYLES.get(user_style, PAGE_STYLES.get(app.config['DEFAULT_PAGE_STYLE']))
|
|
|
|
resp = make_response(render_template(template_name_or_list, **context))
|
|
if selected_style:
|
|
resp.set_cookie('user-style', selected_style)
|
|
return resp
|
|
|
|
|
|
def resolve_page_file(path):
|
|
"""Manipulate the request path to find appropriate page file.
|
|
|
|
* convert dir requests to index files
|
|
|
|
Worth noting, Flask already does stuff like convert '/foo/../../../bar' to
|
|
'/bar', so we don't need to take care around file access here.
|
|
"""
|
|
if path.endswith('/'):
|
|
path = f'{path}index'
|
|
path = f'pages/{path}.md'
|
|
return path
|
|
|
|
|
|
def is_file_path_actually_dir_path(path):
|
|
"""Check if requested path which looks like a file is actually a directory.
|
|
|
|
If, for example, /foo used to be a file (foo.md) which later became a directory,
|
|
foo/, this returns True. Useful for when I make my structure more complicated
|
|
than it originally was, or if users are just weird.
|
|
"""
|
|
if not path.endswith('/'):
|
|
logger.debug("requested path '%s' looks like a file", path)
|
|
if os.path.isdir(f'{app.instance_path}/pages/{path}'):
|
|
logger.debug("...and it's actually a dir")
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def generate_parent_navs(path):
|
|
"""Create a series of paths/links to navigate up from the given path."""
|
|
# derive additional path/location stuff based on path
|
|
resolved_path = resolve_page_file(path)
|
|
parent_dir = os.path.dirname(resolved_path)
|
|
parent_path = '/'.join(path[:-1].split('/')[:-1]) + '/'
|
|
|
|
logger.debug("path: '%s'; parent path: '%s'; resolved path: '%s'; parent dir: '%s'",
|
|
path, parent_path, resolved_path, parent_dir)
|
|
|
|
if path in ('index', '/'):
|
|
return [(app.config['TITLE_SUFFIX'], '/')]
|
|
else:
|
|
with app.open_instance_resource(resolved_path, 'r') as entry_file:
|
|
entry = entry_file.read()
|
|
# for issues regarding parser reuse (see lib.init_md) we reinitialize the parser here
|
|
md = init_md()
|
|
_ = Markup(md.convert(entry))
|
|
page_name = " ".join(md.Meta.get('title')) if md.Meta.get('title') else f'/{path}'
|
|
return generate_parent_navs(parent_path) + [(page_name, f'/{path}')]
|