aboutsummaryrefslogtreecommitdiff
# SPDX-License-Identifier: CC0-1.0

# koszko.org website logic.

# Copyright (C) 2021, 2022 Wojtek Kosior

import gettext
import re
import dataclasses as dc
import typing as t

from pathlib import Path, PurePosixPath

import flask
import werkzeug
import jinja2
import jinja2.exceptions


here = Path(__file__).resolve().parent


pages = {
    'index.html'
}


langs_short_2_long = {
    'en': 'en_US',
    'pl': 'pl_PL'
}

langs_long_2_short = dict((v, k) for k, v in langs_short_2_long.items())

default_locale = 'pl_PL'


def raise_exception(msg: str) -> t.NoReturn:
    raise Exception(msg)


post_titles_pl = {
    'gospel-despite-no-enthusiasm': 'Dobra Nowina pomimo zniechęcenia',
    'how-i-fought...-another-view': 'Jak walczyłem... inne spojrzenie'
}

post_titles_en = {
    'gospel-despite-no-enthusiasm': 'Gospel despite lack of enthusiasm',
    'how-i-fought...-another-view': 'How I fought... another view'
}

post_titles = {'en': post_titles_en, 'pl': post_titles_pl}

@dc.dataclass(frozen=True)
class PostData:
    date:      str
    page_path: str
    title:     str

    def __lt__(self, other: 'PostData') -> bool:
        return (other.date, self.page_path) < (self.date, other.page_path)

post_filename_re = re.compile(
    r'''
    ^
    (?P<date>20[0-9][0-9]-[0-1][0-9]-[0-3][0-9])
    -
    (?P<post_identifier>.*)
    \.html\.jinja
    $
    ''',
    re.VERBOSE
)

def get_posts(lang: str) -> t.Sequence[PostData]:
    lang_dir = here / 'templates' / lang
    posts_data = []

    for template_path in (lang_dir / 'posts').glob('20*'):
        filename_match = post_filename_re.match(template_path.name)
        assert filename_match is not None

        date = filename_match.group('date')
        post_identifier = filename_match.group('post_identifier')
        page_path = f'posts/{date}-{post_identifier}.html'
        title = post_titles[lang][post_identifier]

        posts_data.append(PostData(date=date, page_path=page_path, title=title))

    return sorted(posts_data)


@dc.dataclass(init=False)
class Website(flask.Flask):
    def __init__(self) -> None:
        super().__init__(__name__)

        self.jinja_options = {
            **self.jinja_options,
            'loader':        jinja2.PackageLoader(__package__),
            'autoescape':    jinja2.select_autoescape(['.jinja']),
            'lstrip_blocks': True,
            'extensions': [
                *self.jinja_options.get('extensions', []),
                'jinja2.ext.i18n',
                'jinja2.ext.do'
            ]
        }

        self.jinja_env.globals['raise'] = raise_exception
        self.jinja_env.globals['get_posts'] = get_posts

    def koszko_install_translations(self, locale: str) -> None:
        translations = gettext.translation(
            'messages',
            localedir = here / 'locales',
            languages = [locale]
        )
        self.jinja_env.install_gettext_translations(translations)


website_app = Website()


def show_page(lang_short: str, page_path: str) -> str:
    for segment in PurePosixPath(page_path).parts:
        if segment.startswith('__'):
            flask.abort(404)

    effective_page_path = '__index.html' if page_path == '' else page_path

    app = t.cast(Website, flask.current_app)
    app.koszko_install_translations(langs_short_2_long[lang_short])

    for prefix in [f'{lang_short}', '']:
        pure_path = PurePosixPath(prefix) / f'{effective_page_path}.jinja'
        footer_path = pure_path.parent / f'__footer_for_{pure_path.name}'
        try:
            html = flask.render_template(
                str(pure_path),
                lang_short            = lang_short,
                page_path             = page_path,
                dedicated_footer_path = str(footer_path)
            )
            break
        except jinja2.exceptions.TemplateNotFound:
            if prefix == '':
                raise

    return html

for lang in langs_short_2_long.keys():
    def __show_page_in_lang(page_path: str, lang_short: str = lang) -> str:
        return show_page(lang_short, page_path)

    website_app.add_url_rule(
        f'/{lang}/<path:page_path>',
        f'show_page_in_{lang}',
        __show_page_in_lang,
        methods=['GET']
    )

    def __show_main_page_in_lang(lang_short: str = lang) -> str:
        return show_page(lang_short, '')

    website_app.add_url_rule(
        f'/{lang}/',
        f'show_main_page_in_{lang}',
        __show_main_page_in_lang,
        methods=['GET']
    )

@website_app.route('/<path:page_path>', methods=['GET'])
def redirect_to_resource(page_path) -> werkzeug.Response:
    if page_path != '':
        page_path_posix = PurePosixPath(page_path)

        if page_path_posix.parts[0] == 'sideload':
            return flask.Response(
                "Incomplete server configuration - requests for '/sideload/' not routed as they should be.",
                500
            )

        # Route to other stuff that was on this domain before and has been moved
        # under '/sideload/'.
        for name in ['fraktal', 'mm', 'articles/etherwall_private_chain.html']:
            try:
                page_path_posix.relative_to(PurePosixPath(name))
            except ValueError:
                pass
            else:
                return flask.redirect(f'/sideload/{name}')

        # Make all resources from '/static' aliased under '/'.
        if not page_path.endswith('.html'):
            return flask.redirect(f'/static/{page_path}')

    # Redirect to the most suitable language version of a page.
    chosen_locale = flask.request.accept_languages.best_match(
        langs_short_2_long.values(),
        default = default_locale
    )
    if chosen_locale is None:
        chosen_locale = default_locale

    lang_short = langs_long_2_short[chosen_locale]

    url = flask.url_for(
        f'.show_page_in_{lang_short}',
        page_path  = page_path
    )
    return flask.redirect(url)

@website_app.route('/', methods=['GET'])
def redirect_to_lang_main_page() -> werkzeug.Response:
    return redirect_to_resource('')