home » zplus/dokk.org.git
ID: cf85123b94c7da131de367fb6a4b2b369032ac83
341 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://localhost:3030/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 ?name
    WHERE {{
        [] a mp:Distribution ;
            mp:name "{distribution}" ;
            mp:number {version} ;
            mp:package ?package .

        ?package a mp:Package ;
        mp:name ?name .
    }}
    ORDER BY ?name
    ''')

    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 (?page as ?id) ?filename
    WHERE {{
        [] a mp:Distribution ;
            mp:name "{distribution}" ;
            mp:number {version} ;
            mp:package ?package .

        ?package a mp:Package ;
        mp:name "{package}" ;
        mp:page ?page .

  		?page a mp:Page ;
        mp:filename ?filename
    }}
    ORDER BY ?filename
    ''')

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

@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:>

    SELECT *
    WHERE {{
        ?page a mp:Page ;
        mp:filename ?filename ;
        mp:name_lowercase "{name}" ;
        mp:section_lowercase "{section}" .

        ?package a mp:Package ;
        mp:name ?package_name ;
        mp:page ?page .

        ?distro a mp:Distribution ;
        mp:name ?distro_name ;
        mp:number ?distro_number ;
        mp:package ?package .
    }}
    ORDER BY ?distro_name ?distro_number ?package_name
    ''')['results']['bindings']

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

@bottle.get('/manpages/<distro_name>/<distro_number>/<package>/<page>', name='manpages_page')
def manpages_page(distro_name, distro_number, package, page):
    """
    Display a single manpage.
    """

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

    SELECT *
    WHERE {{
        ?page a mp:Page ;
        mp:filename "{page}" ;
        mp:name ?page_name ;
        mp:section ?page_section ;
        mp:html ?page_html .

        ?package a mp:Package ;
        mp:name "{package}" ;
        mp:page ?page .

        ?distro a mp:Distribution ;
        mp:name "{distro_name}" ;
        mp:number {distro_number} ;
        mp:package ?package .
    }}
    ''')['results']['bindings'][0]

    """
    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['page_html']['value']
    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)

    return template('templates/manpages/manpage.tpl',
        distro = {'name': distro_name, 'number': distro_number},
        package = package,
        page = {
            'filename': page,
            'name': data['page_name']['value'],
            'section': data['page_section']['value'],
            'html': html,
        })

@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)