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:
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 )
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.
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')]]]]]:
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 ]
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
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