diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..54681b3 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python +python: + - "2.6" + - "2.7" + - "3.3" + - "3.4" + - "3.5" + - "pypy" + +install: "pip install -e .[test]" +script: py.test diff --git a/examples/app1.py b/examples/app1.py index 74afeac..f61e14f 100644 --- a/examples/app1.py +++ b/examples/app1.py @@ -7,7 +7,7 @@ from werkzeug.wrappers import Request, Response -from katla.renderer import HTML5Renderer +from katla.renderer import HTMLRenderer from app1_root import Root @@ -18,7 +18,7 @@ def __init__(self): self.root = Root() def dispatch_request(self, request): - res = HTML5Renderer().render(self.root) + res = HTMLRenderer().render(self.root) return Response(res.to_html_string(), mimetype='text/html') def wsgi_app(self, environ, start_response): diff --git a/katla/renderer.py b/katla/renderer.py index a3f8c81..eab8fe4 100644 --- a/katla/renderer.py +++ b/katla/renderer.py @@ -7,7 +7,7 @@ from lxml import etree -from . import attributes +from . import transforms from .tag import tag @@ -25,8 +25,8 @@ def to_html_string(self): doctype=None) -base_attributes = {'style': attributes.style, - 'className' : attributes.className} +base_transforms = {'style': transforms.style, + 'className' : transforms.className} class BaseRenderer(object): @@ -37,8 +37,140 @@ def render(self, comp): return RenderedTree(tree) -class HTML5Renderer(BaseRenderer): +class HTMLRenderer(BaseRenderer): - # div = partial(etree.Element, 'div') - span = tag('span', base_attributes) - div = tag('div', base_attributes) + # From list @ https://developer.mozilla.org/fr/docs/Web/HTML/Element + + html = tag('html') + + # Meta + base = tag('base', base_transforms) + head = tag('head', base_transforms) + link = tag('link', base_transforms) + meta = tag('meta', base_transforms) + style = tag('style', base_transforms) + title = tag('title', base_transforms) + + # Content + address = tag('address', base_transforms) + article = tag('article', base_transforms) + body = tag('body', base_transforms) + footer = tag('footer', base_transforms) + header = tag('header', base_transforms) + h1 = tag('h1', base_transforms) + h2 = tag('h2', base_transforms) + h3 = tag('h3', base_transforms) + h4 = tag('h4', base_transforms) + h5 = tag('h5', base_transforms) + h6 = tag('h6', base_transforms) + hgroup = tag('hgroup', base_transforms) + nav = tag('nav', base_transforms) + section = tag('section', base_transforms) + + # Text content + blockquote = tag('blockquote', base_transforms) + dd = tag('dd', base_transforms) + div = tag('div', base_transforms) + dl = tag('dl', base_transforms) + dt = tag('dt', base_transforms) + figcaption = tag('figcaption', base_transforms) + figure = tag('figure', base_transforms) + hr = tag('hr', base_transforms) + li = tag('li', base_transforms) + main = tag('main', base_transforms) + ol = tag('ol', base_transforms) + p = tag('p', base_transforms) + pre = tag('pre', base_transforms) + ul = tag('ul', base_transforms) + + # Text semantics + a = tag('a', base_transforms) + abbr = tag('abbr', base_transforms) + b = tag('b', base_transforms) + bdi = tag('bdi', base_transforms) + bdo = tag('bdo', base_transforms) + br = tag('br', base_transforms) + cite = tag('cite', base_transforms) + code = tag('code', base_transforms) + data = tag('data', base_transforms) + dfn = tag('dfn', base_transforms) + em = tag('em', base_transforms) + i = tag('i', base_transforms) + kbd = tag('kbd', base_transforms) + mark = tag('mark', base_transforms) + q = tag('q', base_transforms) + rp = tag('rp', base_transforms) + rt = tag('rt', base_transforms) + ruby = tag('ruby', base_transforms) + s = tag('s', base_transforms) + samp = tag('samp', base_transforms) + small = tag('small', base_transforms) + span = tag('span', base_transforms) + strong = tag('strong', base_transforms) + sub = tag('sub', base_transforms) + sup = tag('sup', base_transforms) + time = tag('time', base_transforms) + u = tag('u', base_transforms) + var = tag('var', base_transforms) + wbr = tag('wbr', base_transforms) + + # Image and multimedia + area = tag('area', base_transforms) + audio = tag('audio', base_transforms) + img = tag('img', base_transforms) + map_ = tag('map', base_transforms) + track = tag('track', base_transforms) + video = tag('video', base_transforms) + + # Embedded content + embed = tag('embed', base_transforms) + iframe = tag('iframe', base_transforms) + object = tag('object', base_transforms) + param = tag('param', base_transforms) + source = tag('source', base_transforms) + + # Scripts + canvas = tag('canvas', base_transforms) + script = tag('script', base_transforms) + noscript = tag('noscript', base_transforms) + + # Edition + del_ = tag('del', base_transforms) + ins = tag('ins', base_transforms) + + # Tables + caption = tag('caption', base_transforms) + col = tag('col', base_transforms) + colgroup = tag('colgroup', base_transforms) + table = tag('table', base_transforms) + tbody = tag('tbody', base_transforms) + td = tag('td', base_transforms) + tfoot = tag('tfoot', base_transforms) + th = tag('th', base_transforms) + thead = tag('thead', base_transforms) + tr = tag('tr', base_transforms) + + # Forms + button = tag('button', base_transforms) + datalist = tag('datalist', base_transforms) + colgroup = tag('colgroup', base_transforms) + fieldset = tag('fieldset', base_transforms) + form = tag('form', base_transforms) + input_ = tag('input', base_transforms) + keygen = tag('keygen', base_transforms) + label = tag('label', base_transforms) + legend = tag('legend', base_transforms) + meter = tag('meter', base_transforms) + optgroup = tag('optgroup', base_transforms) + option = tag('option', base_transforms) + output = tag('output', base_transforms) + progress = tag('progress', base_transforms) + select = tag('select', base_transforms) + textarea = tag('textarea', base_transforms) + + # Interactive + details = tag('details', base_transforms) + dialog = tag('dialog', base_transforms) + menu = tag('menu', base_transforms) + menuitem = tag('menuitem', base_transforms) + summary = tag('summary', base_transforms) diff --git a/katla/styles.py b/katla/styles.py new file mode 100644 index 0000000..1e6ef8a --- /dev/null +++ b/katla/styles.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +""" + katla + :copyright: (c) 2016 by Yann Gravrand. + :license: BSD, see LICENSE for more details. +""" + + +def from_string(s): + """Returns a dictionary of styles from a styles string + :param s: Input string, eg. "display:none; text-align: right" + :return: A dictionary of styles {declaration: value} + """ + declarations = [decl.strip().split(':') for decl in s.split(';')] + return {name.strip(): value.strip() for (name, value) in declarations} + + +def to_string(d): + """Returns a string from a dictionary of styles + :param s: Styles dictionary, eg. {"display": "none", "text-align": "right"} + :return: A styles string + """ + return '; '.join(['%s: %s' % + (name, value) for name, value in d.items()]) diff --git a/katla/tag.py b/katla/tag.py index e414840..709b5bb 100644 --- a/katla/tag.py +++ b/katla/tag.py @@ -4,15 +4,35 @@ :copyright: (c) 2016 by Yann Gravrand. :license: BSD, see LICENSE for more details. """ - from lxml import etree -from component import Component +from .component import Component + + +# Python 3 handling +try: + unicode = unicode +except NameError: + str, unicode, bytes, basestring = str, str, bytes, (str, bytes) +else: + str, unicode, bytes, basestring = str, unicode, str, basestring class Tag(object): + """``Tag`` is the base class to render HTML tags""" def __init__(self, name, attrs_transforms): + """Init a tag. + :param name: Name of the tag to use in output + :param attrs_transforms: either: + - a dictionary of {``attribute_name``:``attribute_transform``}, + each ``attribute_transform`` being a function: + :param name: Original attribute name + :param value: Original attribute value + :return: A (``name``, ``value``) tuple with the new attribute name and value + - a list of allowed attribute names + - None to apply no transform and disable restriction on attribute names + """ self._name = name self._attrs_transforms = None if attrs_transforms is None: @@ -44,7 +64,6 @@ def _extend(self, tree, children): self._set_text(tree, child) # TODO comment case elif isinstance(child, Component): - print 'COMP CASE' res = child.render(self._renderer) if res is not None: tree.append(res) @@ -66,4 +85,5 @@ def _handle_attrs(self, attrs): def tag(name, attrs_transforms=None): + """Creates a ``Tag``, see ``Tag:__init__`` for documentation""" return Tag(name, attrs_transforms) diff --git a/katla/attributes.py b/katla/transforms.py similarity index 59% rename from katla/attributes.py rename to katla/transforms.py index a8afbfe..9f038ae 100644 --- a/katla/attributes.py +++ b/katla/transforms.py @@ -3,15 +3,18 @@ katla :copyright: (c) 2016 by Yann Gravrand. :license: BSD, see LICENSE for more details. + + The ``transforms`` module defines usual transforms on attributes, + e.g. "className" --> "class" """ +from . import styles + def className(name, val): return 'class', val def style(name, val): if isinstance(val, dict): - return (name, '; '.join(['%s: %s' % - (attr_name, attr_value) for \ - attr_name, attr_value in val.items()])) + return name, styles.to_string(val) return name, val diff --git a/setup.py b/setup.py index 9f05303..76ae2d9 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,7 @@ packages=find_packages(), zip_safe=True, install_requires=( - 'werkzeug', 'lxml', ), - extras_require={'test': ('pytest',)} + extras_require={'test': ('pytest', 'werkzeug')} ) diff --git a/tests/test_component.py b/tests/test_component.py index 1f5a16e..0784a2e 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -7,9 +7,9 @@ import unittest -from katla import attributes +from katla import transforms from katla.component import Component -from katla.renderer import BaseRenderer, base_attributes +from katla.renderer import BaseRenderer, base_transforms from katla.tag import tag from .utils import render_tag @@ -29,7 +29,7 @@ def render(self, r): class Renderer(BaseRenderer): - div = tag('div', base_attributes) + div = tag('div', base_transforms) class TestComponent(unittest.TestCase): @@ -38,12 +38,12 @@ def test_simple(self): a = A() r = Renderer() self.assertEquals(render_tag(a.render(r)), - '
Hello World
', + b'
Hello World
', 'Simple component rendering') def test_composition(self): b = B() r = Renderer() self.assertEquals(render_tag(b.render(r)), - '
Wrapped:
Hello World
Hello World
', + b'
Wrapped:
Hello World
Hello World
', 'Simple component rendering') diff --git a/tests/test_tag.py b/tests/test_tag.py index 3d1b38c..b387854 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -7,7 +7,7 @@ import unittest -from katla import attributes +from katla import styles, transforms from katla.tag import tag from .utils import render_tag @@ -17,54 +17,55 @@ class TestTag(unittest.TestCase): div = tag('div') span = tag('span', ('alt',)) - a = tag('a', {'className': attributes.className, - 'style': attributes.style}) + a = tag('a', {'className': transforms.className, + 'style': transforms.style}) def test_tags_and_text(self): d = self.div() - self.assertEquals(render_tag(d), '
', + self.assertEquals(render_tag(d), b'
', 'Empty tag') d = self.div('Hello') - self.assertEquals(render_tag(d), '
Hello
', + self.assertEquals(render_tag(d), b'
Hello
', 'Text only tag') d = self.div('Hello', ' ', 'World') - self.assertEquals(render_tag(d), '
Hello World
', + self.assertEquals(render_tag(d), b'
Hello World
', 'Tag with multiple concatenated texts') d = self.div('Hello', self.div('World')) - self.assertEquals(render_tag(d), '
Hello
World
', + self.assertEquals(render_tag(d), b'
Hello
World
', 'Child tag') d = self.div('Hello', self.span(' World'), ' ') - self.assertEquals(render_tag(d), '
Hello World
', + self.assertEquals(render_tag(d), b'
Hello World
', 'Child tag and texts') def test_tag_attributes(self): d = self.div('Hello', ' ', 'World', title='Hello', alt='Hello') self.assertEquals(render_tag(d), - '
Hello World
', + b'
Hello World
', 'Multiple attributes') d = self.div('Hello', self.span('World', alt='World'), ' ', title="Hello") self.assertEquals(render_tag(d), - '
HelloWorld
', + b'
HelloWorld
', 'Child with attributes') d = self.span('Hi', alt='World', title='World') - self.assertEquals(render_tag(d), 'Hi', + self.assertEquals(render_tag(d), b'Hi', 'Forbidden attributes are cleared') d = self.a('Link', className='link') self.assertEquals(render_tag(d), - 'Link', + b'Link', 'Attribute transform: className example') - d = self.a('Link', style={'display': 'none', - 'text-decoration': 'underline'}) + st = {'display': 'none', + 'text-decoration': 'underline'} + d = self.a('Link', style=st) self.assertEquals(render_tag(d), - 'Link', + b'Link' % styles.to_string(st), 'Attribute transform: style example') diff --git a/tests/utils.py b/tests/utils.py index 66a3ec9..c3896e4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -15,5 +15,5 @@ def render_tag(tag): pretty_print=False, xml_declaration=False, doctype=None) - print res + print(res) return res