├── tests ├── __init__.py ├── test_basic.py ├── utils.py └── test_functions.py ├── setup.py ├── docs ├── requirements.txt └── _src │ ├── index.rst │ ├── api.rst │ ├── _templates │ ├── custom-class-template.rst │ └── custom-module-template.rst │ └── conf.py ├── html2dash ├── __init__.py └── html2dash.py ├── .vscode └── settings.json ├── examples ├── pandas_table.py ├── tabler.py ├── markdown_to_dash.py └── tabler.html ├── .github └── workflows │ ├── python-publish.yml │ └── python-pytest.yml ├── pyproject.toml ├── LICENSE ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | html2dash 2 | sphinx-click==4.4.0 3 | furo==2023.3.27 -------------------------------------------------------------------------------- /html2dash/__init__.py: -------------------------------------------------------------------------------- 1 | from .html2dash import html2dash 2 | 3 | __version__ = "0.2.4" 4 | -------------------------------------------------------------------------------- /docs/_src/index.rst: -------------------------------------------------------------------------------- 1 | .. html2dash documentation master file 2 | 3 | html2dash 4 | ========= 5 | -------------------------------------------------------------------------------- /docs/_src/api.rst: -------------------------------------------------------------------------------- 1 | html2dash API 2 | ================ 3 | 4 | .. autosummary:: 5 | :toctree: _autosummary 6 | :template: custom-module-template.rst 7 | 8 | html2dash -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "tests" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true 7 | } -------------------------------------------------------------------------------- /tests/test_basic.py: -------------------------------------------------------------------------------- 1 | from dash import html 2 | from html2dash import html2dash 3 | 4 | 5 | def test_html2dash_empty(): 6 | assert html2dash("").to_plotly_json() == html.Div([]).to_plotly_json() 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/pandas_table.py: -------------------------------------------------------------------------------- 1 | from dash import Dash 2 | from html2dash import html2dash 3 | import dash_mantine_components as dmc 4 | import pandas as pd 5 | 6 | element_map = { 7 | "table": dmc.Table, 8 | } 9 | 10 | df = pd.read_csv("https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv") 11 | app = Dash() 12 | 13 | pandas_table = html2dash(df.head(50).to_html(index=False), element_map=element_map) 14 | pandas_table.children[0].striped = True 15 | pandas_table.children[0].withBorder = True 16 | app.layout = dmc.Container(pandas_table) 17 | 18 | if __name__ == "__main__": 19 | app.run_server(debug=True) 20 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from dash.development.base_component import Component 2 | 3 | 4 | def dash_to_dict(dash_component: Component) -> dict: 5 | """function to recursively convert dash components to dicts""" 6 | if isinstance(dash_component, Component): 7 | return_dict = {} 8 | for k, v in dash_component.to_plotly_json().items(): 9 | if k == "props": 10 | return_dict[k] = {k: dash_to_dict(v) for k, v in v.items()} 11 | elif k == "children": 12 | return_dict[k] = [dash_to_dict(child) for child in v] 13 | else: 14 | return_dict[k] = v 15 | return return_dict 16 | elif isinstance(dash_component, list): 17 | return [dash_to_dict(child) for child in dash_component] 18 | return dash_component 19 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | pypi-publish: 12 | name: upload release to PyPI 13 | runs-on: ubuntu-latest 14 | permissions: 15 | id-token: write 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python 19 | uses: actions/setup-python@v3 20 | with: 21 | python-version: '3.x' 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install build 26 | - name: Build package 27 | run: python -m build 28 | 29 | - name: Publish package distributions to PyPI 30 | uses: pypa/gh-action-pypi-publish@release/v1 -------------------------------------------------------------------------------- /docs/_src/_templates/custom-class-template.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | :members: 7 | :show-inheritance: 8 | :inherited-members: 9 | :special-members: __call__, __add__, __mul__ 10 | 11 | {% block methods %} 12 | {% if methods %} 13 | .. rubric:: {{ _('Methods') }} 14 | 15 | .. autosummary:: 16 | :nosignatures: 17 | {% for item in methods %} 18 | {%- if not item.startswith('_') %} 19 | ~{{ name }}.{{ item }} 20 | {%- endif -%} 21 | {%- endfor %} 22 | {% endif %} 23 | {% endblock %} 24 | 25 | {% block attributes %} 26 | {% if attributes %} 27 | .. rubric:: {{ _('Attributes') }} 28 | 29 | .. autosummary:: 30 | {% for item in attributes %} 31 | ~{{ name }}.{{ item }} 32 | {%- endfor %} 33 | {% endif %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /examples/tabler.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from dash import Dash, html, dcc 3 | from html2dash import html2dash 4 | import dash_mantine_components as dmc 5 | from dash_iconify import DashIconify 6 | 7 | modules = [html, dcc, dmc] 8 | element_map = {} 9 | element_map["icon"] = DashIconify 10 | element_map["rprogress"] = dmc.RingProgress 11 | element_map["lprogress"] = dmc.Progress 12 | 13 | app = Dash( 14 | __name__, 15 | external_scripts=[ 16 | "https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/js/tabler.min.js" 17 | ], 18 | external_stylesheets=[ 19 | "https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler.min.css", 20 | "https://rsms.me/inter/inter.css", 21 | ], 22 | ) 23 | 24 | app.layout = html2dash( 25 | Path("tabler.html").read_text(), module_list=modules, element_map=element_map 26 | ) 27 | 28 | if __name__ == "__main__": 29 | app.run_server(debug=True) 30 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "html2dash" 7 | dynamic = ["version"] 8 | description = "Convert an HTML layout to a plotly dash layout" 9 | authors = [{ name = "Najeem Muhammed", email = "najeem@gmail.com" }] 10 | readme = "README.md" 11 | requires-python = ">=3.7" 12 | license = { file = "LICENSE" } 13 | keywords = ["dash", "plotly", "html"] 14 | dependencies = ["dash", "beautifulsoup4", "lxml"] 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | ] 20 | 21 | [project.optional-dependencies] 22 | test = ["pytest", "coverage"] 23 | 24 | [project.urls] 25 | "Homepage" = "https://github.com/idling-mind/html2dash" 26 | "Bug Tracker" = "https://github.com/idling-mind/html2dash/issues" 27 | 28 | [tool.setuptools.dynamic] 29 | version = { attr = "html2dash.__version__" } 30 | -------------------------------------------------------------------------------- /tests/test_functions.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from dash import html 3 | from dash.development.base_component import Component 4 | from html2dash import ( 5 | html2dash, 6 | ) 7 | from html2dash.html2dash import ( 8 | parse_element, 9 | fix_hyphenated_attr, 10 | ) 11 | from .utils import dash_to_dict 12 | 13 | 14 | def test_is_dash_component(): 15 | assert isinstance(html.Div(), Component) 16 | 17 | 18 | def test_fix_hyphenated_attr(): 19 | assert fix_hyphenated_attr("foo-bar") == "fooBar" 20 | assert fix_hyphenated_attr("baz-qux-quux") == "bazQuxQuux" 21 | 22 | 23 | def test_parse_element_none(): 24 | assert parse_element(None) is None 25 | 26 | 27 | def test_parse_element_comment(): 28 | assert ( 29 | parse_element(BeautifulSoup("", "xml").comment) is None 30 | ) 31 | 32 | 33 | def test_html2dash_empty(): 34 | assert html2dash("").to_plotly_json() == html.Div([]).to_plotly_json() 35 | 36 | 37 | def test_html2dash_simple(): 38 | a = dash_to_dict(html2dash("
hi
")) 39 | b = dash_to_dict(html.Div([html.Div(["hi"])])) 40 | assert a == b 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023, Najeem Muhammed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/python-pytest.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.8", "3.9", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest 31 | pip install . 32 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 33 | - name: Lint with flake8 34 | run: | 35 | # stop the build if there are Python syntax errors or undefined names 36 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 37 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 38 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 39 | - name: Test with pytest 40 | run: | 41 | pytest 42 | -------------------------------------------------------------------------------- /docs/_src/_templates/custom-module-template.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. automodule:: {{ fullname }} 4 | 5 | {% block attributes %} 6 | {% if attributes %} 7 | .. rubric:: Module attributes 8 | 9 | .. autosummary:: 10 | :toctree: 11 | {% for item in attributes %} 12 | {{ item }} 13 | {%- endfor %} 14 | {% endif %} 15 | {% endblock %} 16 | 17 | {% block functions %} 18 | {% if functions %} 19 | .. rubric:: {{ _('Functions') }} 20 | 21 | .. autosummary:: 22 | :toctree: 23 | :nosignatures: 24 | {% for item in functions %} 25 | {{ item }} 26 | {%- endfor %} 27 | {% endif %} 28 | {% endblock %} 29 | 30 | {% block classes %} 31 | {% if classes %} 32 | .. rubric:: {{ _('Classes') }} 33 | 34 | .. autosummary:: 35 | :toctree: 36 | :template: custom-class-template.rst 37 | :nosignatures: 38 | {% for item in classes %} 39 | {{ item }} 40 | {%- endfor %} 41 | {% endif %} 42 | {% endblock %} 43 | 44 | {% block exceptions %} 45 | {% if exceptions %} 46 | .. rubric:: {{ _('Exceptions') }} 47 | 48 | .. autosummary:: 49 | :toctree: 50 | {% for item in exceptions %} 51 | {{ item }} 52 | {%- endfor %} 53 | {% endif %} 54 | {% endblock %} 55 | 56 | {% block modules %} 57 | {% if modules %} 58 | .. autosummary:: 59 | :toctree: 60 | :template: custom-module-template.rst 61 | :recursive: 62 | {% for item in modules %} 63 | {{ item }} 64 | {%- endfor %} 65 | {% endif %} 66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # HTML Files 2 | *.htm* 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /examples/markdown_to_dash.py: -------------------------------------------------------------------------------- 1 | # You'll have to install markdownit to run this example 2 | # pip install markdown-it-py[plugins] 3 | from dash import Dash, html, dcc 4 | from html2dash import html2dash 5 | import dash_mantine_components as dmc 6 | from markdown_it import MarkdownIt 7 | from mdit_py_plugins.attrs.index import attrs_block_plugin, attrs_plugin 8 | from mdit_py_plugins.container.index import container_plugin 9 | from mdit_py_plugins.admon.index import admon_plugin 10 | from functools import partial 11 | from dash_iconify import DashIconify 12 | 13 | md = ( 14 | MarkdownIt() 15 | .enable("table") 16 | .use(attrs_block_plugin) 17 | .use(attrs_plugin) 18 | .use(container_plugin, "container") 19 | .use(container_plugin, "stack") 20 | .use(admon_plugin) 21 | ) 22 | h = md.render( 23 | """ 24 | # Hello World 25 | This is a paragraph. 26 | 27 | {#subtitle} 28 | ## This is a subtitle with an ID 29 | **Bold text** and *italic text*. 30 | 31 | 32 | ## This is a table 33 | 34 | {striped=True withBorder=True} 35 | | Name | Age | 36 | | ---- | --- | 37 | | John | 30 | 38 | | Jane | 28 | 39 | 40 | {#my-list} 41 | ## This is a list 42 | - Item 1 43 | - Item 2 44 | - Item 3 45 | 46 | ## This is a code block 47 | 48 | {language=python withLineNumbers=True} 49 | ```python 50 | if __name__ == "__main__": 51 | print("Hello World") 52 | ``` 53 | 54 | ## This is a link 55 | [Click here](https://google.com) 56 | 57 | ## This is a blockquote 58 | > This is a blockquote 59 | 60 | ## This is a horizontal rule 61 | --- 62 | 63 | {p=xl fluid=True} 64 | :::: container 65 | This is a container 66 | 67 | {align=center} 68 | ::: stack 69 | Hello there 70 | 71 | Another paragraph 72 | ::: 73 | :::: 74 | """ 75 | ) 76 | 77 | def custom_div(children, **kwargs): 78 | if "className" in kwargs and kwargs["className"] == "container": 79 | return dmc.Container(children, **kwargs) 80 | elif "className" in kwargs and kwargs["className"] == "stack": 81 | return dmc.Stack(children, **kwargs) 82 | return html.Div(children, **kwargs) 83 | 84 | modules = [dmc, html, dcc] 85 | 86 | element_map = { 87 | "h1": dmc.Title, 88 | "h2": partial(dmc.Title, order=2), 89 | "p": partial(dmc.Text, weight=500), 90 | "img": dmc.Image, 91 | "ul": partial(dmc.List, icon=dmc.ThemeIcon(DashIconify(icon="mdi:check"))), 92 | "li": dmc.ListItem, 93 | "code": dmc.Prism, 94 | "div": custom_div, 95 | } 96 | 97 | app = Dash(__name__) 98 | 99 | 100 | app.layout = html2dash(h, module_list=modules, element_map=element_map) 101 | 102 | if __name__ == "__main__": 103 | app.run_server(debug=True) 104 | -------------------------------------------------------------------------------- /docs/_src/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from pathlib import Path 4 | import sys 5 | sys.path.append(str(Path(__file__).parent.parent.parent)) 6 | from html2dash import __version__ 7 | 8 | __title__ = "html2dash" 9 | __author__ = "Najeem Muhammed" 10 | __description__ = "Convert an html layout to a dash layout" 11 | 12 | from datetime import datetime 13 | 14 | tls_verify = False 15 | extensions = [ 16 | "sphinx.ext.autodoc", 17 | "sphinx.ext.autosummary", 18 | "sphinx.ext.napoleon", 19 | "sphinx.ext.doctest", 20 | "sphinx.ext.intersphinx", 21 | "sphinx.ext.todo", 22 | "sphinx.ext.coverage", 23 | "sphinx.ext.mathjax", 24 | "sphinx.ext.viewcode", 25 | "sphinx_click", 26 | ] 27 | 28 | autosummary_generate = True 29 | autoclass_content = "both" 30 | add_module_names = False 31 | autosummary_imported_members = True 32 | autodoc_default_options = { 33 | "members": True, 34 | } 35 | 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ["_templates"] 39 | 40 | # The suffix(es) of source filenames. 41 | # You can specify multiple suffix as a list of string: 42 | # 43 | # source_suffix = ['.rst', '.md'] 44 | source_suffix = ".rst" 45 | 46 | # The master toctree document. 47 | master_doc = "index" 48 | 49 | # General information about the project. 50 | project = __title__ 51 | copyright = f"{datetime.now().year}, {__author__}" 52 | author = __author__ 53 | 54 | version = __version__ 55 | # The full version, including alpha/beta/rc tags. 56 | release = __version__ 57 | 58 | exclude_patterns = ["_build", "**.ipynb_checkpoints"] 59 | 60 | pygments_style = "sphinx" 61 | 62 | todo_include_todos = True 63 | 64 | 65 | html_theme = "furo" 66 | html_title = f"{__title__} v{__version__}" 67 | html_logo = "" 68 | html_theme_options = { 69 | "navigation_with_keys": True, 70 | } 71 | html_static_path = ["_static"] 72 | htmlhelp_basename = f"{__title__}doc" 73 | latex_elements = { 74 | } 75 | latex_documents = [ 76 | ( 77 | master_doc, 78 | f"{__title__}.tex", 79 | f"{__title__} Documentation", 80 | __author__, 81 | "manual", 82 | ), 83 | ] 84 | man_pages = [ 85 | (master_doc, __title__, f"{__title__} Documentation", [author], 1)] 86 | texinfo_documents = [ 87 | ( 88 | master_doc, 89 | __title__, 90 | f"{__title__} Documentation", 91 | author, 92 | __title__, 93 | __description__, 94 | "Miscellaneous", 95 | ), 96 | ] 97 | intersphinx_mapping = { 98 | "python": ("https://docs.python.org/3/", None), 99 | } 100 | 101 | 102 | # Napoleon settings 103 | napoleon_google_docstring = True 104 | napoleon_numpy_docstring = True 105 | napoleon_include_init_with_doc = False 106 | napoleon_include_private_with_doc = False 107 | napoleon_include_special_with_doc = True 108 | napoleon_use_admonition_for_examples = False 109 | napoleon_use_admonition_for_notes = False 110 | napoleon_use_admonition_for_references = False 111 | napoleon_use_ivar = False 112 | napoleon_use_param = True 113 | napoleon_use_rtype = True 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # html2dash 2 | 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 | - Sometimes I get the html form of a class (like pandas dataframe), but I 13 | cannot easily display that in dash. 14 | - Use html from markdown parsers 15 | - Cannot copy paste html code from examples on the web. 16 | - Cannot use tools like emmet to generate html code. 17 | 18 | html2dash solves these problems by allowing you to write your dash layout in 19 | html/xml form. It converts the html/xml code to equivalent dash layout code. 20 | 21 | ## Installation 22 | 23 | ```bash 24 | pip install html2dash 25 | ``` 26 | 27 | ## Examples 28 | 29 | Here is a simple example: 30 | 31 | ```python 32 | from dash import Dash 33 | from html2dash import html2dash 34 | 35 | app = Dash(__name__) 36 | 37 | layout = """ 38 |
39 |

Hello World

40 |

This is a paragraph

41 |
42 |

Subheading

43 |

Another paragraph

44 |
45 |
46 | """ 47 | 48 | app.layout = html2dash(layout) 49 | ``` 50 | 51 | You can define attributes like `id`, `class`, `style` etc. These 52 | will be converted to equivalent dash attributes. For example: 53 | 54 | ```python 55 | layout = """ 56 |
57 |

Hello World

58 |

This is a paragraph

59 |
60 |

Subheading

61 |

Another paragraph

62 |
63 |
64 | """ 65 | ``` 66 | 67 | This is equivalent to: 68 | 69 | ```python 70 | layout = html.Div( 71 | id="my-div", 72 | className="my-class", 73 | style={"color": "red"}, 74 | children=[ 75 | html.H1("Hello World"), 76 | html.P("This is a paragraph"), 77 | html.Div( 78 | children=[ 79 | html.H2("Subheading"), 80 | html.P("Another paragraph"), 81 | ] 82 | ) 83 | ] 84 | ) 85 | ``` 86 | 87 | You can use any html tag that appears in `dash.html` module. If `html2dash` does 88 | not find the tag in `dash.html`, it will search in the `dash.dcc` module. 89 | 90 | ```python 91 | from html2dash import html2dash 92 | 93 | layout = html2dash(""" 94 |
95 |

Hello World

96 |

This is a paragraph

97 | 98 |
99 | """) 100 | ``` 101 | 102 | Here, `Input` is not found in `dash.html` module. So, it will search in `dash.dcc` 103 | module and find `dcc.Input` and convert it to `dcc.Input(id="my-input", value="Hello World")`. 104 | 105 | The order in which `html2dash` searches for tags is: 106 | 107 | 1. `dash.html` 108 | 2. `dash.dcc` 109 | 110 | You can change the list of modules that `html2dash` searches for tags by 111 | passing in `module_list` argument. 112 | 113 | ```python 114 | from dash import Dash, html, dcc 115 | from html2dash import html2dash 116 | import dash_mantine_components as dmc 117 | 118 | modules = [html, dcc, dmc] 119 | 120 | layout = html2dash(""" 121 |
122 |

Hello World

123 |

This is a paragraph

124 |
125 | Default 126 | Outline 127 |
128 |
129 | """, module_list=modules) 130 | ``` 131 | 132 | You can also map html tags to dash components. For example, if you dont want to 133 | use `` tag, you can map it to `DashIconify` as follows. 134 | 135 | ```python 136 | from html2dash import html2dash 137 | from dash_iconify import DashIconify 138 | 139 | element_map = {"icon": DashIconify} 140 | 141 | layout = html2dash(""" 142 |
143 |

Icon example

144 | 145 |
146 | """, element_map=element_map) 147 | ``` 148 | 149 | The `element_map` is a dictionary that maps html tags to dash components. 150 | The `element_map` will be searched first before searching in the `module_list`. 151 | 152 | The mapped component does not have to be a dash component. It can be any 153 | function that takes `children` and `**kwargs` as arguments and returns a dash 154 | component. 155 | 156 | ```python 157 | from html2dash import html2dash 158 | 159 | def my_component(children, **kwargs): 160 | return html.Div(children=children, **kwargs) 161 | 162 | element_map = {"my-component": my_component} 163 | 164 | layout = html2dash(""" 165 |
166 |

My component

167 | 168 |

My component

169 |

This is my component

170 |
171 | 172 | 173 |

My component 2

174 |

This is my component 2

175 |
176 |
177 | """, element_map=element_map) 178 | ``` 179 | 180 | ## Example usecase: Display a pandas dataframe in dash 181 | 182 | Since pandas dataframes come with a `to_html` method, you can easily display 183 | them in dash using `html2dash`. 184 | 185 | ```python 186 | import pandas as pd 187 | from html2dash import html2dash 188 | 189 | df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) 190 | layout = html2dash(df.to_html()) 191 | ``` 192 | 193 | If you want to use `dash_mantine_components` to display the dataframe, you can 194 | do the following. 195 | 196 | ```python 197 | import pandas as pd 198 | from html2dash import html2dash 199 | import dash_mantine_components as dmc 200 | 201 | # would have been mapped to dash.html.Table 202 | # But, we want to use dmc.Table instead. 203 | element_map = {"table": dmc.Table} 204 | 205 | df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) 206 | layout = html2dash(df.to_html(), element_map=element_map) 207 | ``` 208 | 209 | `html2dash` can handle multi-index dataframes as well. 210 | 211 | ```python 212 | import pandas as pd 213 | from html2dash import html2dash, settings 214 | import dash_mantine_components as dmc 215 | 216 | df = pd.DataFrame( 217 | { 218 | ("a", "b"): [1, 2, 3], 219 | ("a", "c"): [4, 5, 6], 220 | ("d", "e"): [7, 8, 9], 221 | } 222 | ) 223 | 224 | element_map = {"table": dmc.Table} 225 | 226 | layout = html2dash(df.to_html(), element_map=element_map) 227 | ``` 228 | 229 | ## Case sensitivity of html tags 230 | 231 | html tags are case insensitive. So, `
` and `
` are equivalent. But, 232 | html2dash is partly case sensitive. For any tag, it first tries to find the tag 233 | with the given case. If it does not find the tag, it tries to find the tag with 234 | the first letter capitalized. 235 | 236 | For example, if you have the following layout: 237 | 238 | ```python 239 | layout = html2dash(""" 240 |
241 |

Hello World

242 |

This is a paragraph

243 | 244 |
245 | """) 246 | ``` 247 | 248 | In the above, all tags except `input` are found in `dash.html` module. 249 | And for input tag, the following will be the sequence of searches: 250 | 251 | 1. Search for `input` in `dash.html` >> Not found 252 | 2. Search for `Input` in `dash.html` >> Not found 253 | 3. Search for `input` in `dash.dcc` >> Not found 254 | 4. Search for `Input` in `dash.dcc` >> Found -------------------------------------------------------------------------------- /html2dash/html2dash.py: -------------------------------------------------------------------------------- 1 | """html2dash 2 | 3 | Converts HTML to Dash components. 4 | 5 | Usage: 6 | from html2dash import html2dash 7 | app.layout = html2dash(Path("layout.html").read_text()) 8 | 9 | """ 10 | from __future__ import annotations 11 | from typing import Mapping 12 | from bs4 import BeautifulSoup, element, Comment 13 | from dash import html, dcc 14 | import re 15 | import logging 16 | import json 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | ATTRIBUTE_MAP = { 21 | "autocomplete": "autoComplete", 22 | "autofocus": "autoFocus", 23 | "class": "className", 24 | "colspan": "colSpan", 25 | "for": "htmlFor", 26 | "maxlength": "maxLength", 27 | "minlength": "minLength", 28 | "novalidate": "noValidate", 29 | "readonly": "readOnly", 30 | "rowspan": "rowSpan", 31 | "tabindex": "tabIndex", 32 | } 33 | 34 | 35 | def html2dash( 36 | html_str: str, 37 | module_list: list | None = None, 38 | element_map: Mapping[str, object] | None = None, 39 | parent_div: bool = True, 40 | on_missing_element: str = "warn", 41 | on_missing_attribute: str = "warn", 42 | ) -> html.Div | list[object]: 43 | """Convert the HTML string to dash components. 44 | 45 | Args: 46 | html_str (str): The HTML string to convert. 47 | module_list (list, optional): A list of modules to search for elements. 48 | Defaults to [html, dcc]. 49 | element_map (dict, optional): A dictionary mapping HTML elements to dash 50 | components. Defaults to {}. 51 | parent_div (bool, optional): Whether to enclose the converted Dash components 52 | on_missing_element (str, optional): What to do when an element is not found. 53 | Defaults to "warn". Can be "warn", "raise", or "ignore". 54 | on_missing_attribute (str, optional): What to do when an attribute is not found. 55 | Defaults to "warn". Can be "warn", "raise", or "ignore". 56 | 57 | Returns: 58 | html.Div: The converted Dash components enclosed inside a html.Div object. 59 | """ 60 | html_str_iter = f"{html_str}" 61 | soup = BeautifulSoup(html_str_iter, "xml") 62 | if soup.body is not None: 63 | soup = soup.body 64 | if module_list is None: 65 | module_list = [html, dcc] 66 | if element_map is None: 67 | element_map = {} 68 | settings = { 69 | "module-list": module_list, 70 | "element-map": element_map, 71 | "on-missing-element": on_missing_element, 72 | "on-missing-attribute": on_missing_attribute, 73 | } 74 | children = [parse_element(child, **settings) for child in soup.children] # type: ignore 75 | if parent_div: 76 | return html.Div(children=children) 77 | return children 78 | 79 | 80 | def parse_element(tag: element.Tag, **settings) -> object: 81 | """Parse the HTML element and return the Dash component. 82 | 83 | Args: 84 | tag (element.Tag): The HTML element to parse. 85 | settings (dict): The settings to use. 86 | 87 | Returns: 88 | object: The Dash component. 89 | """ 90 | if tag is None or isinstance(tag, Comment): 91 | return None 92 | elif isinstance(tag, element.NavigableString): 93 | text = str(tag) 94 | if text.strip(): 95 | return text 96 | return None 97 | dash_element = None 98 | for module in settings.get("module-list", []): 99 | mapped_element = settings["element-map"].get(tag.name) 100 | if mapped_element is not None: 101 | dash_element = mapped_element 102 | break 103 | elif hasattr(module, tag.name): 104 | dash_element = getattr(module, tag.name) 105 | break 106 | elif hasattr(module, tag.name.title()): 107 | dash_element = getattr(module, tag.name.title()) 108 | break 109 | if not dash_element: 110 | if settings.get("on-missing-element") == "warn": 111 | logger.warning( 112 | f"Could not find the element '{tag.name}'" f" in any of the modules." 113 | ) 114 | elif settings.get("on-missing-element") == "raise": 115 | raise ValueError( 116 | f"Could not find the element '{tag.name}'" f" in any of the modules." 117 | ) 118 | return None 119 | attrs = {k: v for k, v in tag.attrs.items()} 120 | attrs = fix_attrs(attrs) 121 | children = [] 122 | for child in tag.children: 123 | child_object = parse_element(child, **settings) # type: ignore 124 | if child_object is not None: 125 | children.append(child_object) 126 | if children: 127 | attrs["children"] = children 128 | while True: 129 | try: 130 | return dash_element(**attrs) 131 | except TypeError as e: 132 | match = re.search( 133 | r"received an unexpected keyword argument: `(.*)`", str(e) 134 | ) 135 | if match is None: 136 | raise e 137 | attrs.pop(match.group(1)) 138 | if settings["on-missing-attribute"] == "warn": 139 | logger.warning( 140 | f"Removed the attribute '{match.group(1)}' from the element '{tag.name}'" 141 | f" because it was not valid." 142 | ) 143 | elif settings.get("on-missing-attribute") == "raise": 144 | raise ValueError( 145 | f"Unrecognized attribute '{match.group(1)}' in the element '{tag.name}'" 146 | ) 147 | 148 | 149 | def fix_attrs(attrs: dict) -> dict: 150 | """Fix the attributes to be valid Dash attributes. 151 | 152 | Args: 153 | attrs (dict): The attributes to fix. 154 | 155 | Returns: 156 | dict: The fixed attributes. 157 | """ 158 | return_attrs = {} 159 | for k, v in attrs.items(): 160 | if v in ["true", "false"]: 161 | v = eval(v.title()) 162 | if k == "style": 163 | return_attrs[k] = style_str_to_dict(v) 164 | elif k in ATTRIBUTE_MAP: 165 | return_attrs[ATTRIBUTE_MAP[k]] = v 166 | elif k.startswith("data-") or k.startswith("aria-"): 167 | return_attrs[k] = v 168 | elif isinstance(v, list): 169 | return_attrs[k] = " ".join(v) 170 | else: 171 | if isinstance(v, str) and any([s in v for s in ["{", "["]]): 172 | try: 173 | return_attrs[fix_hyphenated_attr(k)] = json.loads(v) 174 | except Exception: 175 | return_attrs[fix_hyphenated_attr(k)] = v 176 | else: 177 | return_attrs[fix_hyphenated_attr(k)] = v 178 | return return_attrs 179 | 180 | 181 | def fix_hyphenated_attr(attr: str) -> str: 182 | """Fix the hyphenated attribute to be camel case. 183 | 184 | Args: 185 | attr (str): The attribute to fix. 186 | 187 | Returns: 188 | str: The fixed attribute. 189 | """ 190 | return re.sub(r"-(\w)", lambda m: m.group(1).upper(), attr) 191 | 192 | 193 | def style_str_to_dict(style_str: str) -> dict: 194 | """Convert the style string to a dictionary. 195 | 196 | Args: 197 | style_str (str): The style string to convert. 198 | 199 | Returns: 200 | dict: The converted style dictionary. 201 | """ 202 | style_dict = {} 203 | for item in style_str.split(";"): 204 | if ":" in item: 205 | key, value = item.split(":") 206 | style_dict[fix_hyphenated_attr(key.strip())] = value.strip() 207 | return style_dict 208 | -------------------------------------------------------------------------------- /examples/tabler.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 38 | 39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
Sales
47 |
48 | 63 |
64 |
65 |
75%
66 |
67 |
Conversion rate
68 |
69 | 72 | 7% 73 | 74 |
75 |
76 |
77 |
86 | 75% Complete 87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
Revenue
97 |
98 | 113 |
114 |
115 |
116 |
$4,300
117 |
118 | 121 | 8% 122 | 123 |
124 |
125 |
126 |
131 |
136 |
137 |
141 |
142 | 146 |
150 |
151 | 153 |
154 |
155 | 159 |
160 |
161 | 163 |
164 |
165 |
166 |
167 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
New clients
181 |
182 | 197 |
198 |
199 |
200 |
6,782
201 |
202 | 205 | 0% 206 | 207 |
208 |
209 |
214 |
219 |
220 |
224 |
228 | 232 |
236 |
237 | 239 |
240 |
241 | 247 |
248 |
249 | 251 |
252 |
253 |
254 |
258 | 262 |
266 |
267 | 269 |
270 |
271 | 277 |
278 |
279 | 281 |
282 |
283 |
284 |
285 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
Active users
300 |
301 | 316 |
317 |
318 |
319 |
2,986
320 |
321 | 324 | 4% 325 | 326 |
327 |
328 |
333 |
338 |
339 |
343 |
347 | 351 |
355 |
356 | 358 |
359 |
360 | 366 |
367 |
368 | 370 |
371 |
372 |
373 |
374 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 | 392 | 393 |
394 |
395 |
132 Sales
396 |
12 waiting payments
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 | 408 | 409 |
410 |
411 |
78 Orders
412 |
32 shipped
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 | 424 | 425 |
426 |
427 |
623 Shares
428 |
16 today
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 | 440 | 441 |
442 |
443 |
132 Likes
444 |
21 today
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |

Traffic summary

456 |
461 |
466 |
467 |
471 |
475 | 479 |
483 |
484 | 486 |
487 |
488 | 494 |
495 |
496 | 498 |
499 |
500 |
501 |
505 | 509 |
513 |
514 | 516 |
517 |
518 | 524 |
525 |
526 | 528 |
529 |
530 |
531 |
535 | 539 |
543 |
544 | 546 |
547 |
548 | 554 |
555 |
556 | 558 |
559 |
560 |
561 |
562 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |

Locations

576 |
577 |
578 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 |

595 | Using Storage 6854.45 MB of 8 GB 596 |

597 |
598 |
604 |
610 |
616 |
617 |
618 |
619 | 620 | Regular 621 | 915MB 625 |
626 |
627 | 628 | System 629 | 415MB 633 |
634 |
635 | 636 | Shared 637 | 201MB 641 |
642 |
643 | 644 | Free 645 | 612MB 649 |
650 |
651 |
652 |
653 |
654 |
655 |
656 |
659 |
660 |
661 |
662 |
663 | JL 664 |
665 |
666 |
667 | Jeffie Lewzey commented on your 668 | "I'm not a witch." post. 669 |
670 |
yesterday
671 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 | 686 |
687 |
688 |
689 | It's Mallory Hulme's birthday. Wish 690 | him well! 691 |
692 |
2 days ago
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 | 708 |
709 |
710 |
711 | Dunn Slane posted 712 | "Well, what do you want?". 713 |
714 |
today
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
723 |
724 | 730 |
731 |
732 |
733 | Emmy Levet created a new project 734 | Morning alarm clock. 735 |
736 |
4 days ago
737 |
738 |
739 |
740 |
741 |
742 |
743 |
744 |
745 |
746 | 752 |
753 |
754 |
755 | Maryjo Lebarree liked your photo. 756 |
757 |
2 days ago
758 |
759 |
760 |
761 |
762 |
763 |
764 | EP 765 |
766 |
767 |
768 | Egan Poetz registered new client as 769 | Trilia. 770 |
771 |
yesterday
772 |
773 |
774 |
775 |
776 |
777 |
778 | 784 |
785 |
786 |
787 | Kellie Skingley closed a new deal 788 | on project Pen Pineapple Apple Pen. 789 |
790 |
2 days ago
791 |
792 |
793 |
794 |
795 |
796 |
797 | 803 |
804 |
805 |
806 | Christabel Charlwood created a new 807 | project for Wikibox. 808 |
809 |
4 days ago
810 |
811 |
812 |
813 |
814 |
815 |
816 | HS 817 |
818 |
819 |
820 | Haskel Shelper change status of 821 | Tabler Icons from 822 | open to closed. 823 |
824 |
today
825 |
826 |
827 |
828 |
829 |
830 |
831 | 837 |
838 |
839 |
840 | Lorry Mion liked 841 | Tabler UI Kit. 842 |
843 |
yesterday
844 |
845 |
846 |
847 |
848 |
849 |
850 | 856 |
857 |
858 |
859 | Leesa Beaty posted new video. 860 |
861 |
2 days ago
862 |
863 |
864 |
865 |
866 |
867 |
868 | 874 |
875 |
876 |
877 | Perren Keemar and 3 others followed 878 | you. 879 |
880 |
2 days ago
881 |
882 |
883 |
884 |
885 |
886 |
887 | SA 888 |
889 |
890 |
891 | Sunny Airey upload 3 new photos to 892 | category Inspirations. 893 |
894 |
2 days ago
895 |
896 |
897 |
898 |
899 |
900 |
901 | 907 |
908 |
909 |
910 | Geoffry Flaunders made a 911 | $10 donation. 912 |
913 |
2 days ago
914 |
915 |
916 |
917 |
918 |
919 |
920 | 926 |
927 |
928 |
929 | Thatcher Keel created a profile. 930 |
931 |
3 days ago
932 |
933 |
934 |
935 |
936 |
937 |
938 | 944 |
945 |
946 |
947 | Dyann Escala hosted the event 948 | Tabler UI Birthday. 949 |
950 |
4 days ago
951 |
952 |
953 |
954 |
955 |
956 |
957 | 963 |
964 |
965 |
966 | Avivah Mugleston mentioned you on 967 | Best of 2020. 968 |
969 |
2 days ago
970 |
971 |
972 |
973 |
974 |
975 |
976 | AA 977 |
978 |
979 |
980 | Arlie Armstead sent a Review 981 | Request to Amanda Blake. 982 |
983 |
2 days ago
984 |
985 |
986 |
987 |
988 |
989 |
990 |
991 |
992 |
993 |
994 |
995 |
996 |
Development activity
997 |
998 |
999 |
1000 |
1001 |
1002 |
1007 |
1012 |
1013 |
1014 |
1015 |
1016 |
Today's Earning: $4,262.40
1017 |
1018 | +5% more than yesterday 1019 |
1020 |
1021 |
1022 |
1023 |
1024 |
1029 |
1030 |
1034 |
1038 | 1042 |
1046 |
1047 | 1049 |
1050 |
1051 | 1057 |
1058 |
1059 | 1061 |
1062 |
1063 |
1064 |
1065 |
1068 |
1069 |
1070 |
1071 |
1072 |
1073 |
1074 |
1075 | 1076 | 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | 1084 | 1090 | 1095 | 1096 | 1097 | 1098 | 1101 | 1107 | 1108 | 1109 | 1110 | 1116 | 1122 | 1123 | 1124 | 1125 | 1131 | 1136 | 1137 | 1138 | 1139 | 1145 | 1150 | 1151 | 1152 | 1153 |
UserCommitDate
1085 | 1089 | 1091 |
1092 | Fix dart Sass compatibility (#29755) 1093 |
1094 |
28 Nov 2019
1099 | JL 1100 | 1102 |
1103 | Change deprecated html tags to text decoration classes 1104 | (#29604) 1105 |
1106 |
27 Nov 2019
1111 | 1115 | 1117 |
1118 | justify-content:between ⇒ justify-content:space-between 1119 | (#29734) 1120 |
1121 |
26 Nov 2019
1126 | 1130 | 1132 |
1133 | Update change-version.js (#29736) 1134 |
1135 |
26 Nov 2019
1140 | 1144 | 1146 |
1147 | Regenerate package-lock.json (#29730) 1148 |
1149 |
25 Nov 2019
1154 | 1155 | 1156 | 1157 |
1158 |
1159 |
1160 |
1161 |
1162 |
1163 |
1164 |
1165 |
1166 |

Tabler Icons

1167 |
1168 | All icons come from the Tabler Icons set and are 1169 | MIT-licensed. Visit 1170 | tabler-icons.io, download any of the 4637 icons in SVG, PNG or React 1176 | and use them in your favourite design tools. 1177 |
1178 |
1179 | Download icons 1186 |
1187 |
1188 |
1189 |
1190 |
1191 |
1192 |
1193 |
1194 |
1195 |

Most Visited Pages

1196 |
1197 |
1198 | 1199 | 1200 | 1201 | 1202 | 1203 | 1204 | 1205 | 1206 | 1207 | 1208 | 1209 | 1214 | 1215 | 1216 | 1217 | 1231 | 1232 | 1233 | 1238 | 1239 | 1240 | 1241 | 1255 | 1256 | 1257 | 1262 | 1263 | 1264 | 1265 | 1279 | 1280 | 1281 | 1286 | 1287 | 1288 | 1289 | 1303 | 1304 | 1305 | 1310 | 1311 | 1312 | 1313 | 1327 | 1328 | 1329 | 1334 | 1335 | 1336 | 1337 | 1351 | 1352 | 1353 |
Page nameVisitorsUniqueBounce rate
1210 | / 1211 | 1212 | 1213 | 4,8963,65482.54% 1218 |
1223 |
1228 |
1229 |
1230 |
1234 | /form-elements.html 1235 | 1236 | 1237 | 3,6523,21576.29% 1242 |
1247 |
1252 |
1253 |
1254 |
1258 | /index.html 1259 | 1260 | 1261 | 3,2562,86572.65% 1266 |
1271 |
1276 |
1277 |
1278 |
1282 | /icons.html 1283 | 1284 | 1285 | 98686544.89% 1290 |
1295 |
1300 |
1301 |
1302 |
1306 | /docs/ 1307 | 1308 | 1309 | 91282241.12% 1314 |
1319 |
1324 |
1325 |
1326 |
1330 | /accordion.html 1331 | 1332 | 1333 | 85579832.65% 1338 |
1343 |
1348 |
1349 |
1350 |
1354 |
1355 |
1356 |
1357 |
1358 | 1366 |
1367 |
1368 |
1369 |
1370 |
1371 |
1372 |

Social Media Traffic

1373 |
1374 | 1375 | 1376 | 1377 | 1378 | 1379 | 1380 | 1381 | 1382 | 1383 | 1384 | 1385 | 1393 | 1394 | 1395 | 1396 | 1397 | 1405 | 1406 | 1407 | 1408 | 1409 | 1417 | 1418 | 1419 | 1420 | 1421 | 1429 | 1430 | 1431 | 1432 | 1433 | 1441 | 1442 | 1443 | 1444 | 1445 | 1453 | 1454 | 1455 | 1456 | 1457 | 1465 | 1466 | 1467 |
NetworkVisitors
Instagram3,550 1386 |
1387 |
1391 |
1392 |
Twitter1,798 1398 |
1399 |
1403 |
1404 |
Facebook1,245 1410 |
1411 |
1415 |
1416 |
TikTok986 1422 |
1423 |
1427 |
1428 |
Pinterest854 1434 |
1435 |
1439 |
1440 |
VK650 1446 |
1447 |
1451 |
1452 |
Pinterest420 1458 |
1459 |
1463 |
1464 |
1468 |
1469 |
1470 |
1471 |
1472 |
1473 |

Tasks

1474 |
1475 |
1476 | 1477 | 1478 | 1479 | 1484 | 1487 | 1490 | 1495 | 1500 | 1506 | 1507 | 1508 | 1513 | 1516 | 1519 | 1524 | 1529 | 1532 | 1533 | 1534 | 1539 | 1544 | 1547 | 1552 | 1557 | 1563 | 1564 | 1565 | 1570 | 1575 | 1578 | 1583 | 1588 | 1594 | 1595 | 1596 | 1601 | 1606 | 1609 | 1614 | 1619 | 1625 | 1626 | 1627 | 1632 | 1635 | 1638 | 1643 | 1648 | 1654 | 1655 | 1656 |
1480 | 1483 | 1485 | Extend the data model. 1486 | 1488 | August 05, 2021 1489 | 1491 | 1492 | 2/7 1493 | 1494 | 1496 | 1497 | 3 1499 | 1501 | 1505 |
1509 | 1512 | 1514 | Verify the event flow. 1515 | 1517 | January 01, 2019 1518 | 1520 | 1521 | 3/10 1522 | 1523 | 1525 | 1526 | 6 1528 | 1530 | JL 1531 |
1535 | 1538 | 1540 | Database backup and maintenance 1543 | 1545 | December 28, 2018 1546 | 1548 | 1549 | 0/6 1550 | 1551 | 1553 | 1554 | 1 1556 | 1558 | 1562 |
1566 | 1569 | 1571 | Identify the implementation team. 1574 | 1576 | November 11, 2020 1577 | 1579 | 1580 | 6/10 1581 | 1582 | 1584 | 1585 | 12 1587 | 1589 | 1593 |
1597 | 1600 | 1602 | Define users and workflow 1605 | 1607 | November 14, 2021 1608 | 1610 | 1611 | 3/7 1612 | 1613 | 1615 | 1616 | 5 1618 | 1620 | 1624 |
1628 | 1631 | 1633 | Check Pull Requests 1634 | 1636 | February 11, 2021 1637 | 1639 | 1640 | 2/9 1641 | 1642 | 1644 | 1645 | 3 1647 | 1649 | 1653 |
1657 |
1658 |
1659 |
1660 |
1661 |
1662 |
1663 |

Invoices

1664 |
1665 |
1666 |
1667 |
1668 | Show 1669 |
1670 | 1676 |
1677 | entries 1678 |
1679 |
1680 | Search: 1681 |
1682 | 1686 |
1687 |
1688 |
1689 |
1690 |
1691 | 1694 | 1695 | 1696 | 1701 | 1704 | 1705 | 1706 | 1707 | 1708 | 1709 | 1710 | 1711 | 1712 | 1713 | 1714 | 1715 | 1720 | 1721 | 1726 | 1730 | 1731 | 1732 | 1733 | 1734 | 1747 | 1748 | 1749 | 1754 | 1755 | 1760 | 1764 | 1765 | 1766 | 1767 | 1768 | 1781 | 1782 | 1783 | 1788 | 1789 | 1794 | 1798 | 1799 | 1800 | 1801 | 1802 | 1815 | 1816 | 1817 | 1822 | 1823 | 1828 | 1832 | 1833 | 1834 | 1838 | 1839 | 1852 | 1853 | 1854 | 1859 | 1860 | 1865 | 1869 | 1870 | 1871 | 1874 | 1875 | 1888 | 1889 | 1890 | 1895 | 1896 | 1901 | 1905 | 1906 | 1907 | 1911 | 1912 | 1925 | 1926 | 1927 | 1932 | 1933 | 1938 | 1942 | 1943 | 1944 | 1947 | 1948 | 1961 | 1962 | 1963 | 1968 | 1969 | 1974 | 1978 | 1979 | 1980 | 1983 | 1984 | 1997 | 1998 | 1999 |
1697 | 1700 | 1702 | No. 1703 | Invoice SubjectClientVAT No.CreatedStatusPrice
1716 | 1719 | 001401 1722 | Design Works 1725 | 1727 | 1728 | Carlson Limited 1729 | 8795662115 Dec 2017 Paid$887 1735 | 1736 | 1741 | 1745 | 1746 |
1750 | 1753 | 001402 1756 | UX Wireframes 1759 | 1761 | 1762 | Adobe 1763 | 8795642112 Apr 2017 Pending$1200 1769 | 1770 | 1775 | 1779 | 1780 |
1784 | 1787 | 001403 1790 | New Dashboard 1793 | 1795 | 1796 | Bluewolf 1797 | 8795262123 Oct 2017 Pending$534 1803 | 1804 | 1809 | 1813 | 1814 |
1818 | 1821 | 001404 1824 | Landing Page 1827 | 1829 | 1830 | Salesforce 1831 | 879534212 Sep 2017 1835 | Due in 2 1836 | Weeks 1837 | $1500 1840 | 1841 | 1846 | 1850 | 1851 |
1855 | 1858 | 001405 1861 | Marketing Templates 1864 | 1866 | 1867 | Printic 1868 | 8795662129 Jan 2018 1872 | Paid Today 1873 | $648 1876 | 1877 | 1882 | 1886 | 1887 |
1891 | 1894 | 001406 1897 | Sales Presentation 1900 | 1902 | 1903 | Tabdaq 1904 | 879566214 Feb 2018 1908 | Due in 3 1909 | Weeks 1910 | $300 1913 | 1914 | 1919 | 1923 | 1924 |
1928 | 1931 | 001407 1934 | Logo & Print 1937 | 1939 | 1940 | Apple 1941 | 8795662122 Mar 2018 1945 | Paid Today 1946 | $2500 1949 | 1950 | 1955 | 1959 | 1960 |
1964 | 1967 | 001408 1970 | Icons 1973 | 1975 | 1976 | Tookapic 1977 | 8795662113 May 2018 1981 | Paid Today 1982 | $940 1985 | 1986 | 1991 | 1995 | 1996 |
2000 |
2001 | 2031 |
2032 |
2033 | 2034 | 2035 | 2036 | 2091 | 2092 | --------------------------------------------------------------------------------