Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/projspec/html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
def dict_to_html(data: dict, title="Data", open_level=2) -> str:
"""
Convert a nested dictionary to expandable HTML using <details> tags.

Args:
data: The dictionary to convert
title: Title for the details element
open_level: whether to set elements as expanded; yes if > 0, and will
decrement for inner levels.

Returns:
String containing HTML with expandable details elements
"""
# With help from Claude Sonnet 4.
if not isinstance(data, dict):
return f"<span>{data}</span>"

if not data:
return ""
open = "open" if open_level > 0 else "closed"

html = [
f'<details {open} style="margin-left: 20px; margin-bottom: 10px;"><summary style="cursor: pointer; color: #2c5aa0; padding: 5px;"><strong>{title}</strong></summary>'
]

for key, value in data.items():
if isinstance(value, dict):
html.append(dict_to_html(value, key, open_level - 1))
elif isinstance(value, (list, tuple)):
html.append(
f'<details style="margin-left: 20px; margin-bottom: 10px;"><summary style="cursor: pointer; color: #2c5aa0; padding: 5px;"><strong>{key}</strong></summary>'
)
for i, item in enumerate(value):
if isinstance(item, dict):
html.append(
dict_to_html(item, f"{key}[{i}]", open_level - 1)
)
else:
html.append(f'<div style=" margin: 5px 0;"> {item}</div>')
html.append("</details>")
else:
html.append(
f'<div style=" margin: 5px 0;"><strong>{key}:</strong> {value}</div>'
)

html.append("</details>")
return "".join(html)
14 changes: 11 additions & 3 deletions src/projspec/proj/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,15 @@ def to_dict(self, compact=True) -> dict:
url=self.url,
storage_options=self.storage_options,
)
dic["klass"] = "project"
if not compact:
dic["klass"] = "project"
return dic.to_dict(compact=compact)

def _repr_html_(self):
from projspec.html import dict_to_html

return dict_to_html(self.to_dict(), title=self.url)

@staticmethod
def from_dict(dic):
from projspec.utils import from_dict
Expand Down Expand Up @@ -285,9 +291,11 @@ def to_dict(self, compact=True) -> dict:
dic = AttrDict(
_contents=self.contents,
_artifacts=self.artifacts,
subpath=self.subpath,
klass=["projspec", self.snake_name()],
)
if self.subpath:
dic["subpath"] = self.subpath
if not compact:
dic["klass"] = ["projspec", self.snake_name()]
return dic.to_dict(compact=compact)

@classmethod
Expand Down
1 change: 1 addition & 0 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def test_basic():
assert proj.artifacts
assert proj.children
repr(proj)
proj._repr_html_()


def test_contains():
Expand Down