1"""Writing HTML pages that use the beadnet components."""
3from __future__ import annotations
5import html
6import os
7import shutil
8from typing import Optional, Sequence, Union
10from .store import Store
12STATIC = os.path.join(os.path.dirname(__file__), "static")
14PAGE_CSS = """
15body { margin: 0; background: var(--bn-bg, #fff); color: var(--bn-fg, #1d1d1b);
16 font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
17main { max-width: 1100px; margin: 0 auto; padding: 16px; }
18h1 { font-size: 22px; margin: 8px 0 4px; }
19h2 { font-size: 17px; margin: 28px 0 4px; }
20p.lead { color: var(--bn-muted, #666); margin: 0 0 12px; }
21"""
24def run_block_html(src: str, kind: str = "run", heading: Optional[str] = None) -> str:
25 """The default set of components for one run (or saved model) at src."""
26 parts = []
27 if heading:
28 parts.append(f"<h2>{html.escape(heading)}</h2>")
29 parts.append(f'<bn-run src="{html.escape(src)}">')
30 if kind == "run":
31 parts += [" <bn-controls></bn-controls>", " <bn-animation></bn-animation>", " <bn-waveform></bn-waveform>",
32 " <bn-spectrogram></bn-spectrogram>", " <bn-info></bn-info>"]
33 else:
34 parts += [" <bn-controls></bn-controls>", " <bn-model></bn-model>", " <bn-info></bn-info>"]
35 parts.append("</bn-run>")
36 return "\n".join(parts)
39def page_html(body: str, title: str = "beadnet", script: str = "beadnet/beadnet.js", lead: str = "") -> str:
40 lead_html = f'<p class="lead">{html.escape(lead)}</p>' if lead else ""
41 return f"""<!doctype html>
42<html lang="en">
43<head>
44<meta charset="utf-8">
45<meta name="viewport" content="width=device-width, initial-scale=1">
46<title>{html.escape(title)}</title>
47<style>{PAGE_CSS}</style>
48<script type="module" src="{html.escape(script)}"></script>
49</head>
50<body>
51<main>
52<h1>{html.escape(title)}</h1>
53{lead_html}
54{body}
55</main>
56</body>
57</html>
58"""
61def write_page(path: str, runs: Union[str, Sequence[str]], title: str = "beadnet", lead: str = "",
62 copy_js: bool = False) -> str:
63 """Write an HTML page showing one or more runs (or saved models) with the default components.
65 Paths in the page are relative to the page's directory. The script is
66 referenced as beadnet/beadnet.js next to the page: `beadnet serve` provides
67 it, or pass copy_js=True to copy it there for any static server.
68 """
69 if isinstance(runs, str):
70 runs = [runs]
71 d = os.path.dirname(os.path.abspath(path))
72 os.makedirs(d, exist_ok=True)
73 blocks = []
74 for r in runs:
75 st = Store(r)
76 a = st.attrs().get("beadnet", {})
77 kind = "run" if a.get("kind") == "run" else "model"
78 name = a.get("model", {}).get("name") or os.path.basename(os.path.normpath(r))
79 rel = os.path.relpath(os.path.abspath(r), d).replace(os.sep, "/")
80 blocks.append(run_block_html(rel, kind, heading=name if len(runs) > 1 else None))
81 with open(path, "w") as f:
82 f.write(page_html("\n\n".join(blocks), title=title, lead=lead))
83 if copy_js:
84 copy_static(d)
85 return path
88def copy_static(directory: str) -> str:
89 """Copy the components (beadnet/beadnet.js and the viewer) into directory/beadnet/."""
90 dst = os.path.join(directory, "beadnet")
91 os.makedirs(dst, exist_ok=True)
92 for name in os.listdir(STATIC):
93 shutil.copy2(os.path.join(STATIC, name), os.path.join(dst, name))
94 return dst