Skip to content

Commit

Permalink
Fix collapsing on identical reprs (#5) and performance improvements.
Browse files Browse the repository at this point in the history
Caching of reprs broke collapsing behavior that depended on unique UUIDs to tie label
elements to hidden checkbox inputs. To fix that, labels and inputs are now connected
implicitly by nesting elements. This change to the HTML structure prevents a pure CSS
solution to collapsing, so a JS snippet was added to restore collapsing behavior.

Skipping UUID generation substantially reduced HTML generation time and slightly
decreased output HTML size. Another performance boost was achieved by removing
singledispatch which has a substantial overhead and replacing with simple isinstance
checks.
  • Loading branch information
aazuspan committed Nov 9, 2022
1 parent a175315 commit 1590c19
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 71 deletions.
188 changes: 134 additions & 54 deletions docs/notebooks/demo.ipynb

Large diffs are not rendered by default.

16 changes: 7 additions & 9 deletions eerepr/html.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import datetime
import uuid
from functools import singledispatch
from itertools import chain
from typing import Any

Expand All @@ -18,7 +16,6 @@
]


@singledispatch
def convert_to_html(obj: Any, key=None) -> str:
"""Converts a Python object (not list or dict) to an HTML <li> element.
Expand All @@ -29,6 +26,11 @@ def convert_to_html(obj: Any, key=None) -> str:
key : str, optional
The key to prepend to the object value, in the case of a dictionary value or list element.
"""
if isinstance(obj, list):
return list_to_html(obj, key)
elif isinstance(obj, dict):
return dict_to_html(obj, key)

key_html = f"<span class='eerepr-key'>{key}:</span>" if key is not None else ""
return (
"<li class='eerepr-terminal'>"
Expand All @@ -38,7 +40,6 @@ def convert_to_html(obj: Any, key=None) -> str:
)


@convert_to_html.register(list)
def list_to_html(obj: list, key=None) -> str:
"""Convert a Python list to an HTML <li> element."""
contents = str(obj)
Expand All @@ -51,7 +52,6 @@ def list_to_html(obj: list, key=None) -> str:
return _make_collapsible_li(header, children)


@convert_to_html.register(dict)
def dict_to_html(obj: dict, key=None) -> str:
"""Convert a Python dictionary to an HTML <li> element."""
obj = _sort_dict(obj)
Expand All @@ -78,12 +78,10 @@ def _sort_dict(obj: dict) -> dict:

def _make_collapsible_li(header, children) -> str:
"""Package a header and children into a collapsible list element"""
data_id = "section-" + str(uuid.uuid4())

return (
"<li>"
f"<input id='{data_id}' type='checkbox' class='eerepr-collapser'>"
f"<label for='{data_id}' class='eerepr-header'>{header}</label>"
f"<label class='eerepr-header-closed'>{header}"
f"<input type='checkbox' class='eerepr-collapser'></label>"
f"<ul class='eerepr-list'>{''.join(children)}</ul>"
"</li>"
)
Expand Down
11 changes: 11 additions & 0 deletions eerepr/repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ def _load_css():
return read_text("eerepr.static.css", "style.css")


@lru_cache(maxsize=None)
def _load_js():
"""
Note: JS is only needed because the CSS `:has()` selector isn't well supported yet, preventing
a pure CSS solution to the collapsible lists.
"""
return read_text("eerepr.static.js", "script.js")


def _attach_html_repr(cls: Type, repr: Callable) -> None:
"""Add a HTML repr method to an EE class. Only overwrite the method if it was set by this function."""
if not hasattr(cls, REPR_HTML) or cls.__name__ in reprs_set:
Expand All @@ -35,11 +44,13 @@ def _ee_repr(obj: Union[ee.Element, ee.ComputedObject]) -> str:
return f"<pre>{escape(repr(obj))}</pre>"

css = _load_css()
js = _load_js()
body = convert_to_html(info)

return (
"<div>"
f"<style>{css}</style>"
f"<script>{js}</script>"
f"<div class='eerepr'>"
f"<ul>{body}</ul>"
"</div>"
Expand Down
20 changes: 12 additions & 8 deletions eerepr/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ body.vscode-dark {
padding-left: 0 !important;
}

.eerepr-header {
.eerepr-header-open,
.eerepr-header-closed {
color: var(--font-color-secondary);
cursor: pointer;
margin: 0;
}

.eerepr-header:hover {
.eerepr-header-open:hover,
.eerepr-header-closed:hover {
color: var(--font-color-primary);
}

Expand All @@ -75,24 +77,26 @@ body.vscode-dark {
display: none;
}

/* Hide by default */
.eerepr-list {
.eerepr-header-closed + .eerepr-list {
display: none;
}

/* Reveal when expanded */
.eerepr-collapser:checked + .eerepr-header + .eerepr-list {
.eerepr-header-open + .eerepr-list {
display: block;
}

.eerepr-header::before {
.eerepr-header-closed::before {
display: inline-block;
content: "▼";
margin-right: 6px;
transform: rotate(-90deg);
transition: transform 0.2s;
}

.eerepr-collapser:checked + .eerepr-header::before {
.eerepr-header-open::before {
transform: rotate(0deg);
display: inline-block;
content: "▼";
margin-right: 6px;
transition: transform 0.2s;
}
Empty file added eerepr/static/js/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions eerepr/static/js/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
function toggleHeader() {
const parent = this.parentElement;
const open = "eerepr-header-open";
const closed = "eerepr-header-closed";
parent.className = parent.className === open ? closed : open;
}

for (let c of document.getElementsByClassName("eerepr-collapser")) {
c.onclick = toggleHeader;
}
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ install_requires =
[options.package_data]
eerepr =
static/css/*
static/js/*

[options.extras_require]
dev =
Expand Down

0 comments on commit 1590c19

Please sign in to comment.