#!/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)