Skip to content

Commit 6b2a9bb

Browse files
committed
Read me and other updates
1 parent f9ef11e commit 6b2a9bb

File tree

6 files changed

+208
-34
lines changed

6 files changed

+208
-34
lines changed

.github/workflows/python-pytest.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ name: Python package
55

66
on:
77
push:
8-
branches: [ "master" ]
8+
branches: [ "main" ]
99
pull_request:
10-
branches: [ "master" ]
10+
branches: [ "main" ]
1111

1212
jobs:
1313
build:

README.md

+137-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,139 @@
11
# html2dash
22

3-
Convert an HTML layout to an equivalent dash layout
3+
Write your dash layout in html/xml form.
4+
5+
## Why does this package exist?
6+
7+
Dash is a great framework for building web apps using only python (no html/css/
8+
javascript). If you have used dash long enough, you must have noticed some of the
9+
following.
10+
11+
- For larger layouts, the python code becomes very long and hard to read.
12+
- Cannot copy paste html code from examples on the web.
13+
- Python's 4 space indentation makes the layout code shift a lot to the right
14+
and look ugly.
15+
16+
html2dash solves these problems by allowing you to write your dash layout in
17+
html/xml form. It converts the html/xml code to equivalent dash layout code.
18+
19+
## Examples
20+
21+
Here is a simple example:
22+
23+
```python
24+
from dash import Dash
25+
from html2dash import html2dash
26+
27+
app = Dash(__name__)
28+
29+
layout = """
30+
<div>
31+
<h1>Hello World</h1>
32+
<p>This is a paragraph</p>
33+
<div>
34+
<h2>Subheading</h2>
35+
<p>Another paragraph</p>
36+
</div>
37+
</div>
38+
"""
39+
40+
app.layout = html2dash(layout)
41+
```
42+
43+
You can define attributes like `id`, `class`, `style` etc. These
44+
will be converted to equivalent dash attributes. For example:
45+
46+
```python
47+
layout = """
48+
<div id="my-div" class="my-class" style="color: red;">
49+
<h1>Hello World</h1>
50+
<p>This is a paragraph</p>
51+
<div>
52+
<h2>Subheading</h2>
53+
<p>Another paragraph</p>
54+
</div>
55+
</div>
56+
"""
57+
```
58+
59+
This is equivalent to:
60+
61+
```python
62+
layout = html.Div(
63+
id="my-div",
64+
className="my-class",
65+
style={"color": "red"},
66+
children=[
67+
html.H1("Hello World"),
68+
html.P("This is a paragraph"),
69+
html.Div(
70+
children=[
71+
html.H2("Subheading"),
72+
html.P("Another paragraph"),
73+
]
74+
)
75+
]
76+
)
77+
```
78+
79+
You can use any html tag that appears in `dash.html` module. If `html2dash` does
80+
not find the tag in `dash.html`, it will search in the `dash.dcc` module.
81+
82+
```python
83+
from html2dash import html2dash
84+
85+
layout = html2dash("""
86+
<div>
87+
<h1>Hello World</h1>
88+
<p>This is a paragraph</p>
89+
<Input id="my-input" value="Hello World" />
90+
</div>
91+
""")
92+
```
93+
94+
Here, `Input` is not found in `dash.html` module. So, it will search in `dash.dcc`
95+
module and find `dcc.Input` and convert it to `dcc.Input(id="my-input", value="Hello World")`.
96+
97+
The order in which `html2dash` searches for tags is:
98+
99+
1. `dash.html`
100+
2. `dash.dcc`
101+
102+
You can add additional component libraries to the module list as follows.
103+
104+
```python
105+
from html2dash import html2dash, settings
106+
import dash_mantine_components as dmc
107+
108+
# settings["modules"] is a list of modules to search for tags.
109+
# Default value is [html, dcc]
110+
settings["modules"].append(dmc)
111+
112+
layout = html2dash("""
113+
<div>
114+
<h1>Hello World</h1>
115+
<p>This is a paragraph</p>
116+
<div>
117+
<Badge>Default</Badge>
118+
<Badge variant="outline">Outline</Badge>
119+
</div>
120+
</div>
121+
""")
122+
```
123+
124+
You can also map html tags to dash components. For example, if you dont want to
125+
use `<icon>` tag, you can map it to `DashIconify` as follows.
126+
127+
```python
128+
from html2dash import html2dash, settings
129+
from dash_iconify import DashIconify
130+
131+
settings["element-map"]["icon"] = DashIconify
132+
133+
layout = html2dash("""
134+
<div>
135+
<h1>Icon example</h1>
136+
<icon icon="mdi:home"/>
137+
</div>
138+
""")
139+
```

html2dash/html2dash.py

+37-17
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
"""html2dash
2+
3+
Converts HTML to Dash components.
4+
5+
Usage:
6+
from html2dash import html2dash, settings
7+
settings["modules"] = [html, dcc] + settings["modules"]
8+
app.layout = html2dash(Path("layout.html").read_text())
9+
10+
"""
111
from bs4 import BeautifulSoup, element, Comment
212
from dash import html, dcc
313
import re
@@ -15,30 +25,47 @@
1525
"autocomplete": "autoComplete",
1626
"autofocus": "autoFocus",
1727
"class": "className",
28+
"colspan": "colSpan",
1829
"for": "htmlFor",
1930
"maxlength": "maxLength",
2031
"minlength": "minLength",
2132
"novalidate": "noValidate",
33+
"readonly": "readOnly",
34+
"rowspan": "rowSpan",
2235
"tabindex": "tabIndex",
2336
}
2437

2538

2639
def html2dash(html_str: str) -> html.Div:
2740
soup = BeautifulSoup(html_str, "xml")
41+
if soup.body is not None:
42+
soup = soup.body
2843
children = [parse_element(child) for child in soup.children]
2944
return html.Div(children=children)
3045

3146

3247
def parse_element(tag: element.Tag):
33-
if tag is None:
34-
return str(tag)
35-
elif isinstance(tag, Comment):
48+
if tag is None or isinstance(tag, Comment):
3649
return None
3750
elif isinstance(tag, element.NavigableString):
3851
text = str(tag)
3952
if text.strip():
4053
return text
4154
return None
55+
dash_element = None
56+
for module in settings["modules"]:
57+
mapped_element = settings["element-map"].get(tag.name)
58+
if mapped_element is not None:
59+
dash_element = mapped_element
60+
elif hasattr(module, tag.name):
61+
dash_element = getattr(module, tag.name)
62+
elif hasattr(module, tag.name.title()):
63+
dash_element = getattr(module, tag.name.title())
64+
if not dash_element:
65+
logger.warning(
66+
f"Could not find the element '{tag.name}'" f" in any of the modules."
67+
)
68+
return None
4269
attrs = {k: v for k, v in tag.attrs.items()}
4370
attrs = fix_attrs(attrs)
4471
children = []
@@ -48,17 +75,7 @@ def parse_element(tag: element.Tag):
4875
children.append(child_object)
4976
if children:
5077
attrs["children"] = children
51-
for module in settings["modules"]:
52-
mapped_element = settings["element-map"].get(tag.name)
53-
if mapped_element is not None:
54-
return mapped_element(**attrs)
55-
elif hasattr(module, tag.name):
56-
return getattr(module, tag.name)(**attrs)
57-
elif hasattr(module, tag.name.title()):
58-
return getattr(module, tag.name.title())(**attrs)
59-
logger.warning(
60-
f"Could not find the element '{tag.name}'" f" in any of the modules."
61-
)
78+
return dash_element(**attrs)
6279

6380

6481
def fix_attrs(attrs: dict) -> dict:
@@ -75,9 +92,12 @@ def fix_attrs(attrs: dict) -> dict:
7592
elif isinstance(v, list):
7693
return_attrs[k] = " ".join(v)
7794
else:
78-
try:
79-
return_attrs[fix_hyphenated_attr(k)] = json.loads(v)
80-
except Exception:
95+
if isinstance(v, str) and any([s in v for s in ["{", "["]]):
96+
try:
97+
return_attrs[fix_hyphenated_attr(k)] = json.loads(v)
98+
except Exception:
99+
return_attrs[fix_hyphenated_attr(k)] = v
100+
else:
81101
return_attrs[fix_hyphenated_attr(k)] = v
82102
return return_attrs
83103

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ readme = "README.md"
1111
requires-python = ">=3.7"
1212
license = { file = "LICENSE" }
1313
keywords = ["dash", "plotly", "html"]
14-
dependencies = ["dash"]
14+
dependencies = ["dash", "beautifulsoup4", "lxml"]
1515
classifiers = [
1616
"Programming Language :: Python :: 3",
1717
"License :: OSI Approved :: MIT License",

testapp.py

+27-10
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,44 @@
77
settings["modules"].append(dmc)
88
settings["element-map"]["icon"] = DashIconify
99
settings["element-map"]["rprogress"] = dmc.RingProgress
10+
settings["element-map"]["lprogress"] = dmc.Progress
1011

1112
app = Dash(
1213
__name__,
1314
external_scripts=[
1415
"https://cdn.jsdelivr.net/npm/@tabler/[email protected]/dist/js/tabler.min.js"
1516
],
1617
external_stylesheets=[
17-
"https://cdn.jsdelivr.net/npm/@tabler/[email protected]/dist/css/tabler.min.css"
18+
"https://cdn.jsdelivr.net/npm/@tabler/[email protected]/dist/css/tabler.min.css",
19+
"https://rsms.me/inter/inter.css",
1820
]
1921
)
2022

21-
app.layout = html2dash(Path("layout.html").read_text())
23+
app.layout = html2dash(Path("tabler.html").read_text())
2224

23-
@callback(
24-
Output("checkbox_output", "children"),
25-
Input("checkbox", "checked"),
26-
)
27-
def checkbox_output(checked):
28-
if checked:
29-
return f"Checkbox is {checked}"
30-
return f"Checkbox is {checked}"
25+
# @callback(
26+
# Output("checkbox_output", "children"),
27+
# Input("checkbox", "checked"),
28+
# )
29+
# def checkbox_output(checked):
30+
# if checked:
31+
# return f"Checkbox is {checked}"
32+
# return f"Checkbox is {checked}"
33+
34+
# @callback(
35+
# Output("lprogress", "sections"),
36+
# Input("button", "n_clicks"),
37+
# )
38+
# def lprogress(n_clicks):
39+
# if not n_clicks:
40+
# return [
41+
# {"value": 10, "color": "blue", "tooltip": "10 blue"},
42+
# ]
43+
# return [
44+
# {"value": 10, "color": "blue", "tooltip": "10 blue"},
45+
# {"value": 10, "color": "green", "tooltip": "10 green"},
46+
# {"value": 20, "color": "yellow", "tooltip": "20 yellow"},
47+
# ]
3148

3249
if __name__ == "__main__":
3350
app.run_server(debug=True)

tests/test_basic.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
# some basic tests
2-
3-
from pathlib import Path
1+
from dash import html
42
from html2dash import html2dash
53

4+
5+
def test_html2dash_empty():
6+
assert html2dash("").to_plotly_json() == html.Div([]).to_plotly_json()

0 commit comments

Comments
 (0)