From 3c889ee516b60cbf9ad31a48fee3ae9923e1336b Mon Sep 17 00:00:00 2001 From: chriddyp Date: Thu, 30 Nov 2017 15:35:15 -0500 Subject: [PATCH 1/3] add basic HTML serializer for header and footer --- dash_html_components/__init__.py | 2 ++ dash_html_components/_to_html5.py | 43 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 dash_html_components/_to_html5.py diff --git a/dash_html_components/__init__.py b/dash_html_components/__init__.py index ccbe49b8..4a39d2d3 100644 --- a/dash_html_components/__init__.py +++ b/dash_html_components/__init__.py @@ -2,6 +2,7 @@ import dash as _dash import sys as _sys from .version import __version__ +from ._to_html5 import _to_html5 _current_path = _os.path.dirname(_os.path.abspath(__file__)) @@ -24,3 +25,4 @@ for component in _components: setattr(_this_module, component.__name__, component) setattr(component, '_js_dist', _js_dist) + setattr(component, 'to_html5', _to_html5) diff --git a/dash_html_components/_to_html5.py b/dash_html_components/_to_html5.py new file mode 100644 index 00000000..2676b1e4 --- /dev/null +++ b/dash_html_components/_to_html5.py @@ -0,0 +1,43 @@ +import collections +from dash.development.base_component import Component + +_VOID_ELEMENTS = [ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', + 'link', 'meta', 'param', 'source', 'track', 'wbr' +] + + +def _to_html5(self): + def __to_html5(component): + if not isinstance(component, Component): + return str(component) + component_type = component._type.lower() + + children = '' + if component_type not in _VOID_ELEMENTS: + children = getattr(component, 'children', '') + if isinstance(children, Component): + children = __to_html5(children) + elif isinstance(children, collections.MutableSequence): + children = '\n'.join([__to_html5(child) for child in children]) + else: + children = str(children) + if children != '': + children = '\n' + children + + closing = '' + if component_type not in _VOID_ELEMENTS: + closing = '\n'.format(component_type) + + return '<{type}{properties}>{children}{closing}'.format( + type=component_type, + properties=''.join([ + ' {name}="{value}"'.format( + name=p, value=getattr(component, p)) + for p in component._prop_names + if (hasattr(component, p) and p is not 'children') + ]), + children=children, + closing=closing) + + return __to_html5(self) From 722d5a87554edb544b4621b34c88bf1e35c3a9a6 Mon Sep 17 00:00:00 2001 From: chriddyp Date: Thu, 30 Nov 2017 19:53:19 -0500 Subject: [PATCH 2/3] handle dict to inline css and `className` to `class` --- dash_html_components/_to_html5.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/dash_html_components/_to_html5.py b/dash_html_components/_to_html5.py index 2676b1e4..e1ea883e 100644 --- a/dash_html_components/_to_html5.py +++ b/dash_html_components/_to_html5.py @@ -1,11 +1,33 @@ import collections from dash.development.base_component import Component +import re _VOID_ELEMENTS = [ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' ] +_CAMELCASE_REGEX = re.compile('([a-z0-9])([A-Z])') + + +def _camel_case_to_css_case(name): + return _CAMELCASE_REGEX.sub(r'\1-\2', name).lower() + + +def _attribute(name, value): + if name == 'className': + return ('class', value,) + elif name == 'style': + # Dash CSS is camel-cased but CSS is hyphenated + # convert e.g. fontColor to font-color + inline_style = '; '.join([ + '='.join([_camel_case_to_css_case(k), v]) + for (k, v) in value.iteritems() + ]) + return (name, inline_style,) + else: + return (name, value,) + def _to_html5(self): def __to_html5(component): @@ -32,8 +54,8 @@ def __to_html5(component): return '<{type}{properties}>{children}{closing}'.format( type=component_type, properties=''.join([ - ' {name}="{value}"'.format( - name=p, value=getattr(component, p)) + ' {}="{}"'.format( + _attribute(p, getattr(component, p))) for p in component._prop_names if (hasattr(component, p) and p is not 'children') ]), From a4901febb54379ade577ed08b15aefd3740a5c76 Mon Sep 17 00:00:00 2001 From: chriddyp Date: Thu, 30 Nov 2017 19:53:31 -0500 Subject: [PATCH 3/3] tests --- tests/test_dash_html_components.py | 74 ++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/test_dash_html_components.py b/tests/test_dash_html_components.py index b5d2f44f..6345af9f 100644 --- a/tests/test_dash_html_components.py +++ b/tests/test_dash_html_components.py @@ -39,3 +39,77 @@ def test_sample_items(self): self.assertEqual( layout._namespace, 'dash_html_components' ) + + def test_to_html5(self): + test_cases = [ + { + 'name': 'None Children', + 'input': html.Div(), + 'output': '
' + }, + { + 'name': 'Text Children', + 'input': html.Script('alert'), + 'output': '' + }, + { + 'name': 'Numerical Children', + 'input': html.Div(3), + 'output': '
3
' + }, + { + 'name': 'Single Attribute', + 'input': html.Script('alert', type='text/javascript'), + 'output': '' + }, + { + 'name': 'Multiple Attributes', + 'input': html.A(href='codepen', target='_blank'), + 'output': '' + }, + { + 'name': 'Void Elements', + 'input': html.Link(rel='stylesheet', type='text/css'), + 'output': '' + }, + { + 'name': 'Style', + 'input': html.Div(style={'fontSize': '15px', 'color': 'blue'}), + 'output': '
' + }, + { + 'name': 'className', + 'input': html.Div(className='container'), + 'output': '
' + }, + { + 'name': 'Nested Children', + 'input': html.Div([ + 3, + html.H1('header'), + html.Div(), + html.Div( + html.P([ + html.Img(src='imgur', className='full-bleed') + ]) + ) + ], className='parent'), + 'output': '\n'.join([ + '
', + '3', + '

header

', + '
', + '
', + '

', + '' + '

', + '
', + '
' + ]) + } + ] + for test_case in test_cases: + self.assertEqual( + test_case['input'].to_html5(), + test_case['output'] + )