home » zplus/dokk.org.git
ID: fbad831b6a5be8c5008fa187dd7333a058ed133d
320 lines — 9K — View raw


#!/usr/bin/env python3

import bottle
import datetime
import dateutil
import functools
import jinja2
import pathlib
import pyld
import re
import requests

from bottle import jinja2_template as template, request, response
from string import Template

# This only exists for exporting the bottle app object for a WSGI server such as Gunicorn
application = bottle.app()

# Directories to search for HTML templates
bottle.TEMPLATE_PATH = [ './pages' ]

def query(query_string, jsonld_frame=None):
    """
    Send query to Fuseki via HTTP.
    Return the query results.
    """

    http_request = requests.post(
        'http://dokk:7000/dokk?default-graph-uri=urn:x-arq:UnionGraph',
        data = { 'format': 'json', 'query': query_string})

    results = http_request.json()

    if jsonld_frame:
        results = pyld.jsonld.frame(results, jsonld_frame, options={'omitGraph':False})

    return results

def human_size(bytes, B=False):
    """
    Convert a file size in bytes to a human friendly form.
    This is only used in templates when showing file sizes.
    """

    for unit in [ 'B' if B else '', 'K', 'M', 'G', 'T', 'P' ]:
        if bytes < 1024: break
        bytes = bytes / 1024

    return '{}{}'.format(round(bytes), unit).rjust(5)

template = functools.partial(template, template_settings = {
    'filters': {
        'datetime': lambda date: dateutil.parser.parse(date).strftime('%b %-d, %Y - %H:%M%z%Z'),
        'human_size': human_size
    },
    'globals': {
        'now': lambda: datetime.datetime.now(datetime.timezone.utc),
        'query': query,
        'request': request,
        'url': application.get_url,
    },
    'autoescape': True
})

@bottle.error(404)
def error404(error):
    """
    Custom 404 page.

    :param error: bottle.HTTPError given by Bottle when calling abort(404).
    """

    return '[404] {}'.format(error.body)

@bottle.get('/favicon.ico')
def favicon():
    """
    """

    return bottle.static_file('favicon.ico', root='./')

@bottle.get('/static/<filename:path>', name='static')
def static(filename):
    """
    Path for serving static files.
    """

    return bottle.static_file(filename, root='./static/')

@bottle.get('/library/opds.xml', name='library_opds')
def library_opds():
    """
    Serve OPDS Atom RSS feeds
    """

    #response.content_type = 'text/xml; charset=utf-8'
    return template('templates/library/opds/root.tpl')

@bottle.get('/library/opds/books.xml', name='library_opds_books')
def library_opds_books():
    """
    Serve OPDS Atom RSS feeds.
    Items of type "books" only
    """

    #response.content_type = 'text/xml; charset=utf-8'
    return template('templates/library/opds/books.tpl')

@bottle.get('/library/opds/others.xml', name='library_opds_others')
def library_opds_others():
    """
    Serve OPDS Atom RSS feeds.
    Uncategorized items only
    """

    #response.content_type = 'text/xml; charset=utf-8'
    return template('templates/library/opds/others.tpl')

@bottle.get('/library/<item_id>', name='library_item')
def library_item(item_id):
    """
    """

    try:
        with open('../blob.dokk.org/pdf_to_text/' + item_id + '.txt', 'r') as file:
            item_plaintext = file.read()
    except:
        item_plaintext = ''

    return template('templates/library/item.tpl', item_id=item_id, plaintext=item_plaintext)

@bottle.get('/license/<id>', name='license')
def license(id):
    """
    """

    return template('templates/license/license.tpl', license_id=id)

@bottle.get('/manpages/<distribution>/<version>', name='manpages_distribution')
def manpages_distribution(distribution, version):
    """
    List packages by distribution.
    """

    data = query(
    f'''
    PREFIX mp: <dokk:manpages:>

    SELECT DISTINCT ?package
    WHERE {{
        ?id mp:source [
                mp:distribution_name "{distribution}" ;
                mp:distribution_version "{version}" ;
                mp:package ?package ;
            ] .
    }}
    ORDER BY ?package
    ''')

    return template('templates/manpages/distribution.tpl', data=data, distribution=distribution, version=version)

@bottle.get('/manpages/<distribution>/<version>/<package>', name='manpages_package')
def manpages_package(distribution, version, package):
    """
    List manpages by package.
    """

    data = query(
    f'''
    PREFIX mp: <dokk:manpages:>

    SELECT ?id ?filename
    WHERE {{
        ?id mp:source [
            mp:package "{package}" ;
            mp:distribution_version "{version}" ;
            mp:distribution_name "{distribution}" ;
            mp:filename ?filename ;
        ] .
    }}
    ORDER BY ?filename
    ''')

    return template('templates/manpages/package.tpl', data=data, distribution=distribution, version=version, package=package)

@bottle.get('/manpages/<distribution>/<version>/<package>/<name>.<section>.<lang>', name='manpages_page')
def manpages_page(distribution, version, package, name, section, lang):
    """
    Display a single manpage.
    """

    page_id = f'<dokk:manpages:{distribution}/{version}/{package}/{name}.{section}.{lang}>'

    # These characters where url-encoded when creating the RDF data because they
    # make invalid URIs. This means for example that in the graph the URIs will be
    # encoded with %23 instead of #
    page_id = page_id.replace(' ', '_') \
                  .replace('[', '%5B') \
                  .replace(']', '%5D') \
                  .replace('#', '%23')

    data = query(
    f'''
    PREFIX mp: <dokk:manpages:>

    DESCRIBE {page_id}
    ''',
    {
        '@context': {
            'mp': 'dokk:manpages:',
        },
        'mp:source': {},
    })

    # Replace references to other manpages with links.
    # Manpages have a section "See also" at the bottom where they suggest other pages.
    # For example:
    #     SEE ALSO
    #     cdk(3), cdk_screen(3), cdk_display(3), cdk_binding(3), cdk_util(3)
    # We convert these strings to links to other pages.
    # Example: ls(1)  ->  <a href="...">ls(1)</a>
    # regex explanation:
    #    - some manpages use a bold or italic name, so we match an optional <b>
    #      <i> tag
    #    - match valid characters for a manpage, assign name <page>
    #    - match optional closing </em> or </strong> tags
    #    - match '('
    #    - match number, assign name <section>
    #    - match ')'
    html = data['@graph'][0]['mp:html']
    html = re.sub (
        '(?:<b>|<i>)?(?P<page>[\w_.:-]+)(?:</b>|</i>)?\((?P<section>[0-9]\w*)\)',
        lambda match:
            f'''<a href="{application.get_url('manpages_disambiguation',
                          name=match.group('page'), section=match.group('section')).lower()}"
            >{match.group('page')}({match.group('section')})</a>''',
        html)

    data['@graph'][0]['mp:html'] = html

    return template('templates/manpages/manpage.tpl', data=data['@graph'][0], distribution=distribution,
        version=version, package=package, name=name, section=section, lang=lang)

@bottle.get('/manpages/<name>.<section>', name='manpages_disambiguation')
def manpages_disambiguation(name, section):
    """
    Show a list of manpages that match <name> and <section>
    """

    data = query(
    f'''
    PREFIX mp: <dokk:manpages:>

    DESCRIBE ?page
    WHERE {{
        ?page mp:name_lowercase "{name}" ;
              mp:section_lowercase "{section}" .
    }}
    ''',
    {
        '@context': {
            'mp': 'dokk:manpages:',
        },
        'mp:source': {},
    })

    return template('templates/manpages/disambiguation.tpl', name=name, section=section, data=data)

@bottle.get('/mimi_and_eunice/<number>', name='mimi_and_eunice_strip')
def mimi_and_eunice_strip(number):
    """
    """

    iri = '<dokk:mimi_and_eunice:' + number + '>'

    data = query(Template(
    '''
    PREFIX license: <dokk:license:>
    PREFIX mimi_eunice: <dokk:mimi_and_eunice:>

    DESCRIBE $iri ?license
    WHERE {
        $iri mimi_eunice:license ?license
    }
    ''').substitute(iri=iri),
    {
        '@context': {
            'license': 'dokk:license:',
            'mimi_eunice': 'dokk:mimi_and_eunice:',
            'mimi_eunice:tag': { '@container': '@set' },
            'mimi_eunice:transcript': { '@container': '@set' }
        },
        'mimi_eunice:license': {},
        'mimi_eunice:transcript': {
            'mimi_eunice:order': {}
        }
    })

    return template('templates/mimi_and_eunice/strip.tpl', data=data['@graph'][0])

@bottle.get('/articles', name='articles')
def articles():
    """
    """

    pages = [ page.stem for page in sorted(pathlib.Path('./pages').glob('*.html')) ]
    pages.remove('.html')

    return template('templates/articles.tpl', pages=pages)

@bottle.get('/', name='homepage')
@bottle.get('/<page:path>', name='page')
def article(page=''):
    """
    Path for serving a "page".
    """

    page += '.html'

    return template(page)