cuddly/cuddly/__init__.py

154 lines
4.3 KiB
Python

import sys
import pathlib
from urllib.parse import urlparse
import kdl
import click
from bs4 import BeautifulSoup
from bs4.element import Tag
from jinja2 import Environment
class CuddlyMaybeList(object):
"""
If the given list is only one item behave like a proxy to that item.
"""
def __init__(self, inner):
self.inner = inner
def __getattr__(self, name):
if len(self.inner) == 1:
return getattr(self.inner[0], name)
else:
return getattr(self.inner, name)
def __iter__(self):
return iter(self.inner)
def __len__(self):
return len(self.inner)
def __str__(self):
if len(self.inner) == 1:
return str(self.inner[0])
else:
return str(self.inner)
def __getitem__(self, key):
return self.inner[key]
class CuddlyNode(object):
"""
Common interface to Document and Node.
Provide helper method to render to HTMl.
Allow access to children nodes like object properties.
"""
def __init__(self, inner):
self.inner = inner
self.children = {}
for node in self.inner.nodes:
self.children.setdefault(node.name, [])
self.children[node.name].append(CuddlyNode(node))
self.children = {name: CuddlyMaybeList(nodes) for name, nodes in self.children.items()}
@property
def value(self):
if args := getattr(self.inner, 'args', None):
return args[0]
else:
return None
@property
def values(self):
return getattr(self.inner, 'args', [])
@property
def attributes(self):
return getattr(self.inner, 'props', {})
@property
def name(self):
return getattr(self.inner, 'name', None)
def __getattr__(self, name):
children = self.children[name]
return children
def __str__(self):
return str(self.value)
def render(self, template):
variables = {
'values': self.values,
'value': self.value,
'children': self.children,
'attributes': self.attributes,
}
variables.update(self.children)
return template.render(variables)
class RenderNode(object):
def __init__(self, components, env):
self.components = components
self.env = env
def __call__(self, node):
if isinstance(node, CuddlyMaybeList) and len(node) > 1:
raise Exception(f'Render filter must called be on single node, not list, got {type(node)}')
if node.name in self.components:
template = self.env.from_string(self.components[node.name])
return node.render(template)
else:
raise Exception(f'No template to render node type `{node.name}`')
def stripscheme(url):
parsed_url = urlparse(url)
scheme_len = len(parsed_url.scheme) + 1
if parsed_url.netloc:
scheme_len += 2
return url[scheme_len:]
@click.command()
@click.option('--template', help="Path to a template file")
@click.option('--input', help="Path to a KDL file")
@click.option('--output', help="Output file")
def main(template, input, output):
"""
Template a file using a KDL input
"""
template_path = template
with open(template_path) as template_file:
template_html = BeautifulSoup(template_file.read(), features="html.parser")
components = template_html.components.extract()
components = [element for element in components.children if isinstance(element, Tag)]
template_string = template_html.decode()
components = {
component.name: component.decode_contents().strip()
for component in components
}
with open(input) as input_file:
input_node = CuddlyNode(kdl.parse(input_file.read()))
env = Environment()
env.filters['render'] = RenderNode(components, env)
env.filters['stripscheme'] = stripscheme
template = env.from_string(template_string)
output_html = input_node.render(template)
if output.endswith('.pdf'):
from weasyprint import HTML, default_url_fetcher
base_dir = pathlib.Path(template_path).absolute().parent
HTML(string=output_html, base_url=f'file://{base_dir}/').write_pdf(output, pdf_variant='pdf/ua-1')
else:
with open(output, 'w') as output_file:
output_file.write(output_html)