Created
December 2, 2017 20:14
-
-
Save un-def/f7f083e37b57dca6c3d231a3153ccafd to your computer and use it in GitHub Desktop.
Ugly Python-based XML/HTML DSL
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from abc import ABC, ABCMeta, abstractmethod | |
from types import FunctionType | |
from collections import OrderedDict | |
# flake8: noqa | |
class UnresolvedFreeVarsError(Exception): | |
def __init__(self, freevars): | |
if isinstance(freevars, FreeVarWrapper): | |
freevars = (freevars,) | |
self.freevars = freevars | |
def __str__(self): | |
names = ', '.join(fv.name for fv in self.freevars) | |
return "Unresolved freevar(s): {names}".format(names=names) | |
class BaseNode(ABC): | |
def __str__(self): | |
return self.render() | |
@abstractmethod | |
def render(self, indent=0): | |
pass | |
def _resolve_slice_object(self, slice_obj): | |
name = slice_obj.start | |
value = slice_obj.stop | |
if isinstance(value, FreeVarWrapper): | |
raise UnresolvedFreeVarError(value) | |
if isinstance(name, FreeVarWrapper): | |
name = name.name | |
return str(name), value | |
class TextNode(BaseNode): | |
def __init__(self, value): | |
self.value = str(value) | |
def render(self, indent=0): | |
return ' ' * indent + self.value | |
class ElementNode(BaseNode): | |
def __init__(self, name): | |
self.name = name | |
self.children = [] | |
self.attrs = OrderedDict() | |
def __getitem__(self, items): | |
if not isinstance(items, tuple): | |
items = (items,) | |
for item in items: | |
if isinstance(item, slice): | |
self._set_attr(item) | |
else: | |
if isinstance(item, FreeVarWrapper): | |
item = item.to_element_node() | |
elif not isinstance(item, (ElementNode, ComponentNode)): | |
item = TextNode(item) | |
self.children.append(item) | |
return self | |
def _set_attr(self, slice_obj): | |
name, value = self._resolve_slice_object(slice_obj) | |
if isinstance(value, (list, tuple)): | |
unresolved_freevars = list( | |
filter(lambda e: isinstance(e, FreeVarWrapper), value) | |
) | |
if unresolved_freevars: | |
raise UnresolvedFreeVarsError(unresolved_freevars) | |
value = ' '.join(str(e) for e in value) | |
self.attrs[name] = value | |
def render(self, indent=0): | |
if self.attrs: | |
attrs = ' ' + ' '.join('{}="{}"'.format(k, v) | |
for k, v in self.attrs.items()) | |
else: | |
attrs = '' | |
indentation = ' ' * indent | |
if self.children: | |
strings = [] | |
strings.append('{}<{}{}>'.format(indentation, self.name, attrs)) | |
strings.extend(c.render(indent=indent+2) for c in self.children) | |
strings.append('{}</{}>'.format(indentation, self.name)) | |
return '\n'.join(strings) | |
else: | |
return '{}<{}{}/>'.format(indentation, self.name, attrs) | |
class ComponentNode(BaseNode): | |
def __init__(self, component_class): | |
self.component_class = component_class | |
self.component_params = {} | |
def __getitem__(self, items): | |
if not isinstance(items, tuple): | |
items = (items,) | |
for item in items: | |
if not isinstance(item, slice): | |
raise ValueError('Component should be used with ' | |
'parameter:value syntax') | |
name, value = self._resolve_slice_object(item) | |
self.component_params[name] = value | |
return self | |
def render(self, indent=0): | |
component = self.component_class(**self.component_params) | |
return component.render().render(indent=indent) | |
class FreeVarWrapper: | |
element_node_class = ElementNode | |
def __init__(self, name): | |
name = name.strip('_') | |
self.name = name | |
def __str__(self): | |
return '<FreeVar: {name}>'.format(self.name) | |
def __getitem__(self, items): | |
return self.to_element_node()[items] | |
def __sub__(self, other): | |
return self.__class__('{}-{}'.format(self.name, other.name)) | |
def to_element_node(self): | |
return self.element_node_class(self.name) | |
class RenderEnv(dict): | |
def __init__(self, render_globals): | |
self.available_components = {} | |
for name, obj in render_globals.items(): | |
if isinstance(obj, BaseComponentMeta) and obj is not BaseComponent: | |
self.available_components[name] = obj | |
self.render_globals = render_globals | |
def __missing__(self, key): | |
if key in self.available_components: | |
return ComponentNode(self.available_components[key]) | |
return FreeVarWrapper(key) | |
class BaseComponentMeta(ABCMeta): | |
def __new__(meta, name, bases, dct): | |
if bases and 'render' in dct: | |
render = dct['render'] | |
dct['render'] = FunctionType(render.__code__, | |
RenderEnv(render.__globals__)) | |
return super().__new__(meta, name, bases, dct) | |
class BaseComponent(metaclass=BaseComponentMeta): | |
def __str__(self): | |
return str(self.render()) | |
@abstractmethod | |
def render(self): | |
pass | |
class Alert(BaseComponent): | |
def __init__(self, style, text): | |
self.style = style | |
self.text = text | |
def render(self): | |
return ( | |
div[class_:('alert', 'alert-'+self.style), | |
self.text | |
] | |
) | |
class Content(BaseComponent): | |
def __init__(self, image): | |
self.image = image | |
def get_link(self, location): | |
return '/link/to/' + location | |
def render(self): | |
danger = 'This is a danger alert' | |
info = 'This is a info alert' | |
return ( | |
div[class_:'toolbar', id:'#id123', data-initial-value:'foo', | |
a['class':'link', href:self.get_link('nowhere'), | |
'Click me!', | |
img[src:self.image], | |
'Please!' | |
], | |
div[id:'alert-wrapper', | |
Alert[style:'danger', text:danger] | |
], | |
eval_[data-value:14/88], | |
Alert[style:'info', text:info] | |
] | |
) | |
class PageComponent(BaseComponent): | |
def __init__(self, title): | |
self.title = title | |
def render(self): | |
return ( | |
html[ | |
head[ | |
title[self.title] | |
], | |
body[ | |
Content[image:'http://deaddrop.ftp.sh/QvystY0EkXu6.jpg'] | |
], | |
] | |
) | |
component = PageComponent(title='It works!') | |
print(component) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<title> | |
It works! | |
</title> | |
</head> | |
<body> | |
<div class="toolbar" id="#id123" data-initial-value="foo"> | |
<a class="link" href="/link/to/nowhere"> | |
Click me! | |
<img src="http://deaddrop.ftp.sh/QvystY0EkXu6.jpg"/> | |
Please! | |
</a> | |
<div id="alert-wrapper"> | |
<div class="alert alert-danger"> | |
This is a danger alert | |
</div> | |
</div> | |
<eval data-value="0.1590909090909091"/> | |
<div class="alert alert-info"> | |
This is a info alert | |
</div> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment