Skip to content

Commit

Permalink
Read me and other updates
Browse files Browse the repository at this point in the history
  • Loading branch information
idling-mind committed Aug 27, 2023
1 parent f9ef11e commit 6b2a9bb
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 34 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/python-pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Python package

on:
push:
branches: [ "master" ]
branches: [ "main" ]
pull_request:
branches: [ "master" ]
branches: [ "main" ]

jobs:
build:
Expand Down
138 changes: 137 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,139 @@
# html2dash

Convert an HTML layout to an equivalent dash layout
Write your dash layout in html/xml form.

## Why does this package exist?

Dash is a great framework for building web apps using only python (no html/css/
javascript). If you have used dash long enough, you must have noticed some of the
following.

- For larger layouts, the python code becomes very long and hard to read.
- Cannot copy paste html code from examples on the web.
- Python's 4 space indentation makes the layout code shift a lot to the right
and look ugly.

html2dash solves these problems by allowing you to write your dash layout in
html/xml form. It converts the html/xml code to equivalent dash layout code.

## Examples

Here is a simple example:

```python
from dash import Dash
from html2dash import html2dash

app = Dash(__name__)

layout = """
<div>
<h1>Hello World</h1>
<p>This is a paragraph</p>
<div>
<h2>Subheading</h2>
<p>Another paragraph</p>
</div>
</div>
"""

app.layout = html2dash(layout)
```

You can define attributes like `id`, `class`, `style` etc. These
will be converted to equivalent dash attributes. For example:

```python
layout = """
<div id="my-div" class="my-class" style="color: red;">
<h1>Hello World</h1>
<p>This is a paragraph</p>
<div>
<h2>Subheading</h2>
<p>Another paragraph</p>
</div>
</div>
"""
```

This is equivalent to:

```python
layout = html.Div(
id="my-div",
className="my-class",
style={"color": "red"},
children=[
html.H1("Hello World"),
html.P("This is a paragraph"),
html.Div(
children=[
html.H2("Subheading"),
html.P("Another paragraph"),
]
)
]
)
```

You can use any html tag that appears in `dash.html` module. If `html2dash` does
not find the tag in `dash.html`, it will search in the `dash.dcc` module.

```python
from html2dash import html2dash

layout = html2dash("""
<div>
<h1>Hello World</h1>
<p>This is a paragraph</p>
<Input id="my-input" value="Hello World" />
</div>
""")
```

Here, `Input` is not found in `dash.html` module. So, it will search in `dash.dcc`
module and find `dcc.Input` and convert it to `dcc.Input(id="my-input", value="Hello World")`.

The order in which `html2dash` searches for tags is:

1. `dash.html`
2. `dash.dcc`

You can add additional component libraries to the module list as follows.

```python
from html2dash import html2dash, settings
import dash_mantine_components as dmc

# settings["modules"] is a list of modules to search for tags.
# Default value is [html, dcc]
settings["modules"].append(dmc)

layout = html2dash("""
<div>
<h1>Hello World</h1>
<p>This is a paragraph</p>
<div>
<Badge>Default</Badge>
<Badge variant="outline">Outline</Badge>
</div>
</div>
""")
```

You can also map html tags to dash components. For example, if you dont want to
use `<icon>` tag, you can map it to `DashIconify` as follows.

```python
from html2dash import html2dash, settings
from dash_iconify import DashIconify

settings["element-map"]["icon"] = DashIconify

layout = html2dash("""
<div>
<h1>Icon example</h1>
<icon icon="mdi:home"/>
</div>
""")
```
54 changes: 37 additions & 17 deletions html2dash/html2dash.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
"""html2dash
Converts HTML to Dash components.
Usage:
from html2dash import html2dash, settings
settings["modules"] = [html, dcc] + settings["modules"]
app.layout = html2dash(Path("layout.html").read_text())
"""
from bs4 import BeautifulSoup, element, Comment
from dash import html, dcc
import re
Expand All @@ -15,30 +25,47 @@
"autocomplete": "autoComplete",
"autofocus": "autoFocus",
"class": "className",
"colspan": "colSpan",
"for": "htmlFor",
"maxlength": "maxLength",
"minlength": "minLength",
"novalidate": "noValidate",
"readonly": "readOnly",
"rowspan": "rowSpan",
"tabindex": "tabIndex",
}


def html2dash(html_str: str) -> html.Div:
soup = BeautifulSoup(html_str, "xml")
if soup.body is not None:
soup = soup.body
children = [parse_element(child) for child in soup.children]
return html.Div(children=children)


def parse_element(tag: element.Tag):
if tag is None:
return str(tag)
elif isinstance(tag, Comment):
if tag is None or isinstance(tag, Comment):
return None
elif isinstance(tag, element.NavigableString):
text = str(tag)
if text.strip():
return text
return None
dash_element = None
for module in settings["modules"]:
mapped_element = settings["element-map"].get(tag.name)
if mapped_element is not None:
dash_element = mapped_element
elif hasattr(module, tag.name):
dash_element = getattr(module, tag.name)
elif hasattr(module, tag.name.title()):
dash_element = getattr(module, tag.name.title())
if not dash_element:
logger.warning(
f"Could not find the element '{tag.name}'" f" in any of the modules."
)
return None
attrs = {k: v for k, v in tag.attrs.items()}
attrs = fix_attrs(attrs)
children = []
Expand All @@ -48,17 +75,7 @@ def parse_element(tag: element.Tag):
children.append(child_object)
if children:
attrs["children"] = children
for module in settings["modules"]:
mapped_element = settings["element-map"].get(tag.name)
if mapped_element is not None:
return mapped_element(**attrs)
elif hasattr(module, tag.name):
return getattr(module, tag.name)(**attrs)
elif hasattr(module, tag.name.title()):
return getattr(module, tag.name.title())(**attrs)
logger.warning(
f"Could not find the element '{tag.name}'" f" in any of the modules."
)
return dash_element(**attrs)


def fix_attrs(attrs: dict) -> dict:
Expand All @@ -75,9 +92,12 @@ def fix_attrs(attrs: dict) -> dict:
elif isinstance(v, list):
return_attrs[k] = " ".join(v)
else:
try:
return_attrs[fix_hyphenated_attr(k)] = json.loads(v)
except Exception:
if isinstance(v, str) and any([s in v for s in ["{", "["]]):
try:
return_attrs[fix_hyphenated_attr(k)] = json.loads(v)
except Exception:
return_attrs[fix_hyphenated_attr(k)] = v
else:
return_attrs[fix_hyphenated_attr(k)] = v
return return_attrs

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ readme = "README.md"
requires-python = ">=3.7"
license = { file = "LICENSE" }
keywords = ["dash", "plotly", "html"]
dependencies = ["dash"]
dependencies = ["dash", "beautifulsoup4", "lxml"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
Expand Down
37 changes: 27 additions & 10 deletions testapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,44 @@
settings["modules"].append(dmc)
settings["element-map"]["icon"] = DashIconify
settings["element-map"]["rprogress"] = dmc.RingProgress
settings["element-map"]["lprogress"] = dmc.Progress

app = Dash(
__name__,
external_scripts=[
"https://cdn.jsdelivr.net/npm/@tabler/[email protected]/dist/js/tabler.min.js"
],
external_stylesheets=[
"https://cdn.jsdelivr.net/npm/@tabler/[email protected]/dist/css/tabler.min.css"
"https://cdn.jsdelivr.net/npm/@tabler/[email protected]/dist/css/tabler.min.css",
"https://rsms.me/inter/inter.css",
]
)

app.layout = html2dash(Path("layout.html").read_text())
app.layout = html2dash(Path("tabler.html").read_text())

@callback(
Output("checkbox_output", "children"),
Input("checkbox", "checked"),
)
def checkbox_output(checked):
if checked:
return f"Checkbox is {checked}"
return f"Checkbox is {checked}"
# @callback(
# Output("checkbox_output", "children"),
# Input("checkbox", "checked"),
# )
# def checkbox_output(checked):
# if checked:
# return f"Checkbox is {checked}"
# return f"Checkbox is {checked}"

# @callback(
# Output("lprogress", "sections"),
# Input("button", "n_clicks"),
# )
# def lprogress(n_clicks):
# if not n_clicks:
# return [
# {"value": 10, "color": "blue", "tooltip": "10 blue"},
# ]
# return [
# {"value": 10, "color": "blue", "tooltip": "10 blue"},
# {"value": 10, "color": "green", "tooltip": "10 green"},
# {"value": 20, "color": "yellow", "tooltip": "20 yellow"},
# ]

if __name__ == "__main__":
app.run_server(debug=True)
7 changes: 4 additions & 3 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# some basic tests

from pathlib import Path
from dash import html
from html2dash import html2dash


def test_html2dash_empty():
assert html2dash("").to_plotly_json() == html.Div([]).to_plotly_json()

0 comments on commit 6b2a9bb

Please sign in to comment.