html_compose.gallery.app

  1import re
  2from typing import cast
  3from urllib.parse import urlencode
  4
  5import html_compose as ht
  6from html_compose.base_types import Node
  7from html_compose.document import _livereload_script_tag, document_generator
  8from html_compose.elements import div, meta, title
  9
 10from .. import resource
 11from .impl import ShowcaseResult, resolve_environment
 12from .resources import GALLERY_STYLES, NAV_STYLES, IframeScripts
 13
 14
 15class Settings:
 16    livereload_host = "localhost"
 17    livereload_port = 51353
 18
 19
 20def doc(body):
 21    return document_generator(
 22        head=[
 23            meta(charset="UTF-8"),
 24            meta(
 25                name="viewport", content="width=device-width, initial-scale=1"
 26            ),
 27            ht.style()[GALLERY_STYLES],
 28            title["Gallery"],
 29            _livereload_script_tag(
 30                dict(
 31                    host=Settings.livereload_host,
 32                    port=Settings.livereload_port,
 33                    proxy_host=None,
 34                    proxy_uri=None,
 35                )
 36            ),
 37        ],
 38        body=body,
 39    )
 40
 41
 42def showcase_id(result: ShowcaseResult) -> str:
 43    """Generate a stable ID for a showcase, used for anchors and element IDs."""
 44    base = f"{result.func_module}-{result.func_name}"
 45    if result.name:
 46        base += f"-{result.name}"
 47    # Sanitize for use as HTML id - keep only alphanumeric and hyphens
 48    sanitized = re.sub(r"[^a-zA-Z0-9-]", "-", base)
 49    # Collapse multiple hyphens
 50    sanitized = re.sub(r"-+", "-", sanitized)
 51    return sanitized.lower().strip("-")
 52
 53
 54def main_left_panel(modules: dict[str, list[ShowcaseResult]]):
 55    panel_sections = []
 56
 57    for module, results in modules.items():
 58        showcases_by_function: dict[str, list[tuple[str, ShowcaseResult]]] = {}
 59        function_groups = []
 60        for r in results:
 61            grouped_showcases = showcases_by_function.setdefault(
 62                r.func_name, []
 63            )
 64            # Use showcase name, or function name with variant number if unnamed
 65            if r.name:
 66                display = r.name
 67            else:
 68                variant_num = len(grouped_showcases) + 1
 69                display = f"{r.func_name.replace('_', ' ')} #{variant_num}"
 70            grouped_showcases.append((display, r))
 71
 72        for call_name, grouped_showcases in showcases_by_function.items():
 73            showcase_links = [
 74                ht.li()[ht.a(href=f"#{showcase_id(result)}")[name]]
 75                for name, result in grouped_showcases
 76            ]
 77            function_groups.append(
 78                ht.details()[ht.summary()[call_name], ht.ul()[showcase_links]]
 79            )
 80
 81        module_section = ht.details(open=True)[
 82            ht.summary()[module], ht.ul()[function_groups]
 83        ]
 84        panel_sections.append(module_section)
 85    return [ht.style()[NAV_STYLES], ht.ul()[panel_sections]]
 86
 87
 88def get_showcase(result: ShowcaseResult) -> Node:
 89    """Create a self-contained page for a single showcase entry."""
 90    env = resolve_environment(result.env)
 91    # Use the showcase name if provided, otherwise use the function name
 92    display_name = result.name or result.func_name.replace("_", " ")
 93    resource_elements = resource.to_elements(
 94        js=env.js, css=env.css, fonts=env.fonts
 95    )
 96    broken = result.success is False
 97    card_id = showcase_id(result)
 98    standalone_url = f"/showcase/{result.func_module}/{result.func_name}"
 99    if result.name:
100        standalone_url += f"?{urlencode({'name': result.name})}"
101
102    header = ht.div(class_="o-showcase-header")[
103        ht.span(class_="o-showcase-title")[
104            "[broken] " if broken else "", display_name
105        ],
106        ht.a(
107            class_="o-showcase-link",
108            href=standalone_url,
109            target="_blank",
110            title="Open standalone",
111        )["↗"],
112    ]
113
114    if env.isolation == "shadow-dom":
115        content = ht.div(class_="o-showcase-content")[
116            ht.template(shadowrootmode="open")[
117                resource_elements, ht.unsafe_text(cast(str, result.result))
118            ]
119        ]
120    elif env.isolation == "iframe":
121        iframe_content = [
122            # inject CSS to reliably hide scrollbars
123            ht.style["html, body { overflow: hidden; }"],
124            resource_elements,
125            ht.unsafe_text(cast(str, result.result)),
126        ]
127        content = ht.iframe(
128            id=f"iframe-{card_id}",
129            class_="o-showcase-content",
130            srcdoc=ht.render(iframe_content),
131        )
132    else:
133        # no isolation
134        content = ht.div(class_="o-showcase-content")[
135            ht.unsafe_text(cast(str, result.result))
136        ]
137
138    return ht.div(class_="o-showcase-card", id=card_id)[header, content]
139
140
141def get_showcases(content: list[ShowcaseResult]) -> list[Node]:
142    return [get_showcase(result) for result in content]
143
144
145def main_body(
146    modules: dict[str, list[ShowcaseResult]], content: list[ShowcaseResult]
147):
148    resources = None
149    for e in content:
150        env = resolve_environment(e.env)
151        # There can only be one 'none' isolation environment and it
152        # sets up the whole page
153        if env.isolation == "none":
154            resources = resource.to_elements(
155                js=env.js, css=env.css, fonts=env.fonts
156            )
157
158    return [
159        resources,
160        ht.script()[ht.unsafe_text(IframeScripts.parent)],
161        div(class_="o-gallery-container")[
162            div(class_="o-left-panel")[
163                ht.template(shadowrootmode="open")[main_left_panel(modules)]
164            ],
165            div(class_="o-center-panel")[get_showcases(content)],
166            div(class_="o-right-panel"),
167        ],
168    ]
169
170
171def main_route(content: list[ShowcaseResult]) -> str:
172    """
173    Main gallery page showing all showcases
174    """
175    modules: dict[str, list[ShowcaseResult]] = {}
176    for result in content:
177        modules.setdefault(result.func_module, []).append(result)
178
179    body_content = main_body(modules, content)
180    return doc(body_content)
181
182
183def generate_showcase(result: ShowcaseResult) -> str:
184    """
185    Single page view of a showcase
186    """
187    assert result.success, "Cannot generate showcase for failed result"
188    assert isinstance(result.result, str)
189    env = resolve_environment(result.env)
190    resources = resource.to_elements(js=env.js, css=env.css, fonts=env.fonts)
191    return doc(
192        [
193            resources,
194            ht.script()[ht.unsafe_text(IframeScripts.parent)],
195            ht.unsafe_text(result.result),
196        ]
197    )
class Settings:
16class Settings:
17    livereload_host = "localhost"
18    livereload_port = 51353
livereload_host = 'localhost'
livereload_port = 51353
def doc(body):
21def doc(body):
22    return document_generator(
23        head=[
24            meta(charset="UTF-8"),
25            meta(
26                name="viewport", content="width=device-width, initial-scale=1"
27            ),
28            ht.style()[GALLERY_STYLES],
29            title["Gallery"],
30            _livereload_script_tag(
31                dict(
32                    host=Settings.livereload_host,
33                    port=Settings.livereload_port,
34                    proxy_host=None,
35                    proxy_uri=None,
36                )
37            ),
38        ],
39        body=body,
40    )
def showcase_id(result: html_compose.gallery.impl.ShowcaseResult) -> str:
43def showcase_id(result: ShowcaseResult) -> str:
44    """Generate a stable ID for a showcase, used for anchors and element IDs."""
45    base = f"{result.func_module}-{result.func_name}"
46    if result.name:
47        base += f"-{result.name}"
48    # Sanitize for use as HTML id - keep only alphanumeric and hyphens
49    sanitized = re.sub(r"[^a-zA-Z0-9-]", "-", base)
50    # Collapse multiple hyphens
51    sanitized = re.sub(r"-+", "-", sanitized)
52    return sanitized.lower().strip("-")

Generate a stable ID for a showcase, used for anchors and element IDs.

def main_left_panel(modules: dict[str, list[html_compose.gallery.impl.ShowcaseResult]]):
55def main_left_panel(modules: dict[str, list[ShowcaseResult]]):
56    panel_sections = []
57
58    for module, results in modules.items():
59        showcases_by_function: dict[str, list[tuple[str, ShowcaseResult]]] = {}
60        function_groups = []
61        for r in results:
62            grouped_showcases = showcases_by_function.setdefault(
63                r.func_name, []
64            )
65            # Use showcase name, or function name with variant number if unnamed
66            if r.name:
67                display = r.name
68            else:
69                variant_num = len(grouped_showcases) + 1
70                display = f"{r.func_name.replace('_', ' ')} #{variant_num}"
71            grouped_showcases.append((display, r))
72
73        for call_name, grouped_showcases in showcases_by_function.items():
74            showcase_links = [
75                ht.li()[ht.a(href=f"#{showcase_id(result)}")[name]]
76                for name, result in grouped_showcases
77            ]
78            function_groups.append(
79                ht.details()[ht.summary()[call_name], ht.ul()[showcase_links]]
80            )
81
82        module_section = ht.details(open=True)[
83            ht.summary()[module], ht.ul()[function_groups]
84        ]
85        panel_sections.append(module_section)
86    return [ht.style()[NAV_STYLES], ht.ul()[panel_sections]]
def get_showcase( result: html_compose.gallery.impl.ShowcaseResult) -> Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]:
 89def get_showcase(result: ShowcaseResult) -> Node:
 90    """Create a self-contained page for a single showcase entry."""
 91    env = resolve_environment(result.env)
 92    # Use the showcase name if provided, otherwise use the function name
 93    display_name = result.name or result.func_name.replace("_", " ")
 94    resource_elements = resource.to_elements(
 95        js=env.js, css=env.css, fonts=env.fonts
 96    )
 97    broken = result.success is False
 98    card_id = showcase_id(result)
 99    standalone_url = f"/showcase/{result.func_module}/{result.func_name}"
100    if result.name:
101        standalone_url += f"?{urlencode({'name': result.name})}"
102
103    header = ht.div(class_="o-showcase-header")[
104        ht.span(class_="o-showcase-title")[
105            "[broken] " if broken else "", display_name
106        ],
107        ht.a(
108            class_="o-showcase-link",
109            href=standalone_url,
110            target="_blank",
111            title="Open standalone",
112        )["↗"],
113    ]
114
115    if env.isolation == "shadow-dom":
116        content = ht.div(class_="o-showcase-content")[
117            ht.template(shadowrootmode="open")[
118                resource_elements, ht.unsafe_text(cast(str, result.result))
119            ]
120        ]
121    elif env.isolation == "iframe":
122        iframe_content = [
123            # inject CSS to reliably hide scrollbars
124            ht.style["html, body { overflow: hidden; }"],
125            resource_elements,
126            ht.unsafe_text(cast(str, result.result)),
127        ]
128        content = ht.iframe(
129            id=f"iframe-{card_id}",
130            class_="o-showcase-content",
131            srcdoc=ht.render(iframe_content),
132        )
133    else:
134        # no isolation
135        content = ht.div(class_="o-showcase-content")[
136            ht.unsafe_text(cast(str, result.result))
137        ]
138
139    return ht.div(class_="o-showcase-card", id=card_id)[header, content]

Create a self-contained page for a single showcase entry.

def get_showcases( content: list[html_compose.gallery.impl.ShowcaseResult]) -> list[typing.Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, typing.Iterable[typing.Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, typing.Iterable[ForwardRef('Node')], typing.Callable[[], ForwardRef('Node')], typing.Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], typing.Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], typing.Callable[[], typing.Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, typing.Iterable[ForwardRef('Node')], typing.Callable[[], ForwardRef('Node')], typing.Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], typing.Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], typing.Callable[[html_compose.base_types.ElementBase], typing.Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, typing.Iterable[ForwardRef('Node')], typing.Callable[[], ForwardRef('Node')], typing.Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], typing.Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], typing.Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], typing.Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, typing.Iterable[ForwardRef('Node')], typing.Callable[[], ForwardRef('Node')], typing.Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], typing.Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]]:
142def get_showcases(content: list[ShowcaseResult]) -> list[Node]:
143    return [get_showcase(result) for result in content]
def main_body( modules: dict[str, list[html_compose.gallery.impl.ShowcaseResult]], content: list[html_compose.gallery.impl.ShowcaseResult]):
146def main_body(
147    modules: dict[str, list[ShowcaseResult]], content: list[ShowcaseResult]
148):
149    resources = None
150    for e in content:
151        env = resolve_environment(e.env)
152        # There can only be one 'none' isolation environment and it
153        # sets up the whole page
154        if env.isolation == "none":
155            resources = resource.to_elements(
156                js=env.js, css=env.css, fonts=env.fonts
157            )
158
159    return [
160        resources,
161        ht.script()[ht.unsafe_text(IframeScripts.parent)],
162        div(class_="o-gallery-container")[
163            div(class_="o-left-panel")[
164                ht.template(shadowrootmode="open")[main_left_panel(modules)]
165            ],
166            div(class_="o-center-panel")[get_showcases(content)],
167            div(class_="o-right-panel"),
168        ],
169    ]
def main_route(content: list[html_compose.gallery.impl.ShowcaseResult]) -> str:
172def main_route(content: list[ShowcaseResult]) -> str:
173    """
174    Main gallery page showing all showcases
175    """
176    modules: dict[str, list[ShowcaseResult]] = {}
177    for result in content:
178        modules.setdefault(result.func_module, []).append(result)
179
180    body_content = main_body(modules, content)
181    return doc(body_content)

Main gallery page showing all showcases

def generate_showcase(result: html_compose.gallery.impl.ShowcaseResult) -> str:
184def generate_showcase(result: ShowcaseResult) -> str:
185    """
186    Single page view of a showcase
187    """
188    assert result.success, "Cannot generate showcase for failed result"
189    assert isinstance(result.result, str)
190    env = resolve_environment(result.env)
191    resources = resource.to_elements(js=env.js, css=env.css, fonts=env.fonts)
192    return doc(
193        [
194            resources,
195            ht.script()[ht.unsafe_text(IframeScripts.parent)],
196            ht.unsafe_text(result.result),
197        ]
198    )

Single page view of a showcase