191 lines
4.9 KiB
Python
191 lines
4.9 KiB
Python
'''functions for web template management and rendering'''
|
|
import codecs, traceback, os, json, aiohttp, xml
|
|
|
|
from os import listdir, makedirs
|
|
from os.path import isfile, isdir, getmtime, abspath
|
|
|
|
from jinja2 import Environment, FileSystemLoader, ChoiceLoader, select_autoescape, Markup
|
|
from sanic import response as Response
|
|
from hamlpy.hamlpy import Compiler
|
|
from markdown import markdown
|
|
from watchdog.observers import Observer
|
|
from watchdog.events import FileSystemEventHandler
|
|
|
|
from . import logging
|
|
from .color import *
|
|
|
|
|
|
class Template(Environment):
|
|
def __init__(self, build={}, search=[], global_vars={}, autoescape=None):
|
|
self.autoescape = ['html', 'css'] if not autoescape else autoescape
|
|
self.search = []
|
|
self.build = {}
|
|
|
|
for source, dest in build.items():
|
|
self.__addBuildPath(source, dest)
|
|
|
|
for path in search:
|
|
self.__addSearchPath(path)
|
|
|
|
self.var = {
|
|
'markdown': markdown,
|
|
'markup': Markup,
|
|
'cleanhtml': remove_tags,
|
|
'lighten': lighten,
|
|
'darken': darken,
|
|
'saturate': saturate,
|
|
'desaturate': desaturate,
|
|
'rgba': rgba
|
|
}
|
|
|
|
self.var.update(global_vars)
|
|
|
|
super().__init__(
|
|
loader=ChoiceLoader([FileSystemLoader(path) for path in self.search]),
|
|
autoescape=select_autoescape(self.autoescape),
|
|
lstrip_blocks=True,
|
|
trim_blocks=True
|
|
)
|
|
|
|
|
|
def __addSearchPath(self, path):
|
|
tplPath = abspath(str(path))
|
|
|
|
if tplPath not in self.search:
|
|
self.search.append(tplPath)
|
|
|
|
|
|
def __addBuildPath(self, source, destination):
|
|
src = abspath(str(source))
|
|
dest = abspath(str(destination))
|
|
|
|
if not isdir(src):
|
|
raise FileNotFoundError('Source path doesn\'t exist: {src}')
|
|
|
|
self.build[src] = dest
|
|
self.__addSearchPath(dest)
|
|
|
|
|
|
def addEnv(self, k, v):
|
|
self.var[k] = v
|
|
|
|
|
|
def delEnv(self, var):
|
|
if not self.var.get(var):
|
|
raise ValueError(f'"{var}" not in global variables')
|
|
|
|
del self.var[var]
|
|
|
|
|
|
def render(self, tplfile, context, request=None, headers={}, cookies={}, **kwargs):
|
|
if not isinstance(context, dict):
|
|
raise TypeError(f'context for {tplfile} not a dict')
|
|
|
|
data = global_variables.copy()
|
|
data['request'] = request if request else {'headers': headers, 'cookies': cookies}
|
|
data.update(context)
|
|
|
|
return env.get_template(tplfile).render(data)
|
|
|
|
|
|
def response(self, *args, ctype='text/html', status=200, headers={}, **kwargs):
|
|
html = self.render(*args, **kwargs)
|
|
return Response.HTTPResponse(body=html, status=status, content_type=ctype, headers=headers)
|
|
|
|
|
|
def buildTemplates(self, src=None):
|
|
paths = {src: self.search.get(src)} if src else self.search
|
|
|
|
for src, dest in paths.items():
|
|
timefile = f'{dest}/times.json'
|
|
updated = False
|
|
|
|
if not isdir(f'{dest}'):
|
|
makedirs(f'{dest}')
|
|
|
|
if isfile(timefile):
|
|
try:
|
|
times = json.load(open(timefile))
|
|
|
|
except:
|
|
times = {}
|
|
|
|
else:
|
|
times = {}
|
|
|
|
for filename in listdir(src):
|
|
fullPath = f'{src}/{filename}'
|
|
modtime = getmtime(fullPath)
|
|
base, ext = filename.split('.', 1)
|
|
|
|
if ext != 'haml':
|
|
pass
|
|
|
|
elif base not in times or times.get(base) != modtime:
|
|
updated = True
|
|
logging.verbose(f"Template '{filename}' was changed. Building...")
|
|
|
|
try:
|
|
destination = f'{dest}/{base}.html'
|
|
haml_lines = codecs.open(fullPath, 'r', encoding='utf-8').read().splitlines()
|
|
|
|
compiler = Compiler()
|
|
output = compiler.process_lines(haml_lines)
|
|
outfile = codecs.open(destination, 'w', encoding='utf-8')
|
|
outfile.write(output)
|
|
|
|
logging.info(f"Template '{filename}' has been built")
|
|
|
|
except Exception as e:
|
|
'''I'm actually not sure what sort of errors can happen here, so generic catch-all for now'''
|
|
traceback.print_exc()
|
|
logging.error(f'Failed to build {filename}: {e}')
|
|
|
|
times[base] = modtime
|
|
|
|
if updated:
|
|
with open(timefile, 'w') as filename:
|
|
filename.write(json.dumps(times))
|
|
|
|
|
|
def remove_tags(self, text):
|
|
return ''.join(xml.etree.ElementTree.fromstring(text).itertext())
|
|
|
|
|
|
def setupWatcher(self):
|
|
watchPaths = [path['source'] for k, path in build_path_pairs.items()]
|
|
logging.info('Starting template watcher')
|
|
observer = Observer()
|
|
|
|
for tplpath in watchPaths:
|
|
logging.debug(f'Watching template dir for changes: {tplpath}')
|
|
observer.schedule(templateWatchHandler(), tplpath, recursive=True)
|
|
|
|
self.watcher = observer
|
|
|
|
|
|
def startWatcher(self):
|
|
if not self.watcher:
|
|
self.setupWatcher()
|
|
|
|
self.watcher.start()
|
|
|
|
|
|
def stopWatcher(self, destroy=False):
|
|
self.watcher.stop()
|
|
|
|
if destroy:
|
|
self.watcher = None
|
|
|
|
|
|
class TemplateWatchHandler(FileSystemEventHandler):
|
|
def on_any_event(self, event):
|
|
filename, ext = os.path.splitext(os.path.relpath(event.src_path))
|
|
|
|
if event.event_type in ['modified', 'created'] and ext[1:] == 'haml':
|
|
logging.info('Rebuilding templates')
|
|
buildTemplates()
|
|
|
|
|
|
__all__ = ['addSearchPath', 'delSearchPath', 'addBuildPath', 'delSearchPath', 'addEnv', 'delEnv', 'setup', 'renderTemplate', 'aiohttp', 'buildTemplates', 'templateWatcher']
|