panelmark-html 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Timothy Morris PhD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.2
2
+ Name: panelmark-html
3
+ Version: 0.1.0
4
+ Summary: Static HTML/CSS renderer for panelmark shells.
5
+ Author-email: Timothy Morris PhD <sirrommit@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 Timothy Morris PhD
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Repository, https://github.com/sirrommit/panelmark-html
29
+ Keywords: ui,layout,html,renderer,panelmark
30
+ Classifier: Development Status :: 3 - Alpha
31
+ Classifier: Intended Audience :: Developers
32
+ Classifier: Topic :: Software Development :: User Interfaces
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.10
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Requires-Python: >=3.10
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: panelmark>=0.1.0
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest; extra == "dev"
44
+ Requires-Dist: panelmark; extra == "dev"
45
+
46
+ # panelmark-html
47
+
48
+ **panelmark-html** is the static HTML/CSS renderer for the
49
+ [panelmark](https://github.com/sirrommit/panelmark) ecosystem. Given a
50
+ `panelmark` shell and its assigned interactions, it produces an HTML document
51
+ or fragment representing the shell's panel structure.
52
+
53
+ ---
54
+
55
+ ## What this package is
56
+
57
+ - A static renderer: layout model in, HTML string out.
58
+ - Useful for server-side rendering into Flask/Django templates, generating
59
+ reports or dashboards, and automated snapshot tests.
60
+ - The structural foundation that `panelmark-web` will build live interactivity
61
+ on top of.
62
+
63
+ ## What this package is not
64
+
65
+ - It does not handle browser events, keyboard or mouse input, or focus
66
+ transitions.
67
+ - It does not open sockets, HTTP routes, or sessions.
68
+ - It does not render interaction-internal state (item lists, form fields,
69
+ text content). Panel bodies are empty placeholders with stable DOM hooks;
70
+ filling them in with live content is the job of `panelmark-web`.
71
+ - It does not require JavaScript for its output to be valid HTML.
72
+
73
+ ## Relationship to panelmark-web
74
+
75
+ `panelmark-html` and `panelmark-web` are separate packages with distinct scopes.
76
+
77
+ | Package | Role |
78
+ |---------|------|
79
+ | **panelmark-html** | Static structure: panel layout, borders, headings, stable DOM hooks |
80
+ | **panelmark-web** | Live layer: WebSocket sessions, browser events, interaction rendering, draw-command updates |
81
+
82
+ `panelmark-web` depends on `panelmark-html` for its rendered structure. The
83
+ DOM hooks and CSS classes defined here are the stable contract between the two
84
+ packages.
85
+
86
+ ## Installation
87
+
88
+ ```
89
+ pip install panelmark-html
90
+ ```
91
+
92
+ ## Dependencies
93
+
94
+ - `panelmark` — core layout model and shell state machine
95
+ - No web framework dependency
96
+ - No JavaScript build step
97
+
98
+ ## Status
99
+
100
+ **Package maturity:** Pre-alpha. The public Python API (`render_fragment`,
101
+ `render_document`, `get_base_css`, `HTMLRenderer`) and higher-level rendering
102
+ features may still evolve.
103
+
104
+ **Hook contract:** The region-level DOM hooks (`data-pm-region`, `id`,
105
+ `data-pm-*` attributes) and CSS classes (`.pm-shell`, `.pm-split-*`,
106
+ `.pm-panel`, `.pm-panel-body`) are the intended stable substrate for
107
+ `panelmark-web` and are documented as such in
108
+ [docs/hook-contract.md](docs/hook-contract.md). These will not change
109
+ without a major version bump.
110
+
111
+ ## Quick start
112
+
113
+ ```python
114
+ from panelmark import Shell
115
+ from panelmark_html import render_fragment, get_base_css
116
+
117
+ LAYOUT = """
118
+ |=== <bold>My App</> ===|
119
+ |{$main$ }|
120
+ |==================|
121
+ |{2R $status$ }|
122
+ |==================|
123
+ """
124
+
125
+ shell = Shell(LAYOUT)
126
+ html = render_fragment(shell)
127
+ # Panel bodies are empty — fill them with panelmark-web or your own renderer.
128
+ print(html)
129
+ ```
130
+
131
+ For a complete HTML document including base CSS:
132
+
133
+ ```python
134
+ document = render_document(shell)
135
+ # Returns a full <html>...<body>...</body></html> string with embedded CSS.
136
+ ```
137
+
138
+ ## API
139
+
140
+ | Symbol | Description |
141
+ |--------|-------------|
142
+ | `render_fragment(shell)` | Renders the shell as an HTML fragment (no `<html>` or `<head>` wrapper). Returns a string. |
143
+ | `render_document(shell)` | Renders the shell as a complete HTML document, including the base CSS inline. Returns a string. |
144
+ | `get_base_css()` | Returns the base CSS string for manual inclusion in your own template. |
145
+ | `HTMLRenderer` | Low-level renderer class. Use this for custom integration or when you need per-region control. |
146
+
147
+ All symbols are importable from `panelmark_html`. Full API reference:
148
+ [panelmark-html rendering API](https://github.com/sirrommit/panelmark-docs/blob/main/docs/panelmark-html/rendering-api.md)
149
+
150
+ ## Documentation
151
+
152
+ | Document | Description |
153
+ |----------|-------------|
154
+ | [Hook Contract](docs/hook-contract.md) | Stable DOM interface: element structure, CSS classes, `data-pm-*` attributes, CSS custom properties. **Canonical location — this repo.** |
155
+ | [Rendering API Reference](https://github.com/sirrommit/panelmark-docs/blob/main/docs/panelmark-html/rendering-api.md) | Full reference for `render_fragment`, `render_document`, `get_base_css`, and `HTMLRenderer` |
156
+ | [Ecosystem Overview](https://github.com/sirrommit/panelmark-docs/blob/main/docs/ecosystem.md) | How panelmark-html fits into the panelmark package ecosystem |
157
+ | [Shell Language](https://github.com/sirrommit/panelmark-docs/blob/main/docs/shell-language/overview.md) | ASCII-art layout syntax reference |
158
+ | [panelmark-web](https://github.com/sirrommit/panelmark-web) | Live web runtime that builds on this package's hook contract |
@@ -0,0 +1,113 @@
1
+ # panelmark-html
2
+
3
+ **panelmark-html** is the static HTML/CSS renderer for the
4
+ [panelmark](https://github.com/sirrommit/panelmark) ecosystem. Given a
5
+ `panelmark` shell and its assigned interactions, it produces an HTML document
6
+ or fragment representing the shell's panel structure.
7
+
8
+ ---
9
+
10
+ ## What this package is
11
+
12
+ - A static renderer: layout model in, HTML string out.
13
+ - Useful for server-side rendering into Flask/Django templates, generating
14
+ reports or dashboards, and automated snapshot tests.
15
+ - The structural foundation that `panelmark-web` will build live interactivity
16
+ on top of.
17
+
18
+ ## What this package is not
19
+
20
+ - It does not handle browser events, keyboard or mouse input, or focus
21
+ transitions.
22
+ - It does not open sockets, HTTP routes, or sessions.
23
+ - It does not render interaction-internal state (item lists, form fields,
24
+ text content). Panel bodies are empty placeholders with stable DOM hooks;
25
+ filling them in with live content is the job of `panelmark-web`.
26
+ - It does not require JavaScript for its output to be valid HTML.
27
+
28
+ ## Relationship to panelmark-web
29
+
30
+ `panelmark-html` and `panelmark-web` are separate packages with distinct scopes.
31
+
32
+ | Package | Role |
33
+ |---------|------|
34
+ | **panelmark-html** | Static structure: panel layout, borders, headings, stable DOM hooks |
35
+ | **panelmark-web** | Live layer: WebSocket sessions, browser events, interaction rendering, draw-command updates |
36
+
37
+ `panelmark-web` depends on `panelmark-html` for its rendered structure. The
38
+ DOM hooks and CSS classes defined here are the stable contract between the two
39
+ packages.
40
+
41
+ ## Installation
42
+
43
+ ```
44
+ pip install panelmark-html
45
+ ```
46
+
47
+ ## Dependencies
48
+
49
+ - `panelmark` — core layout model and shell state machine
50
+ - No web framework dependency
51
+ - No JavaScript build step
52
+
53
+ ## Status
54
+
55
+ **Package maturity:** Pre-alpha. The public Python API (`render_fragment`,
56
+ `render_document`, `get_base_css`, `HTMLRenderer`) and higher-level rendering
57
+ features may still evolve.
58
+
59
+ **Hook contract:** The region-level DOM hooks (`data-pm-region`, `id`,
60
+ `data-pm-*` attributes) and CSS classes (`.pm-shell`, `.pm-split-*`,
61
+ `.pm-panel`, `.pm-panel-body`) are the intended stable substrate for
62
+ `panelmark-web` and are documented as such in
63
+ [docs/hook-contract.md](docs/hook-contract.md). These will not change
64
+ without a major version bump.
65
+
66
+ ## Quick start
67
+
68
+ ```python
69
+ from panelmark import Shell
70
+ from panelmark_html import render_fragment, get_base_css
71
+
72
+ LAYOUT = """
73
+ |=== <bold>My App</> ===|
74
+ |{$main$ }|
75
+ |==================|
76
+ |{2R $status$ }|
77
+ |==================|
78
+ """
79
+
80
+ shell = Shell(LAYOUT)
81
+ html = render_fragment(shell)
82
+ # Panel bodies are empty — fill them with panelmark-web or your own renderer.
83
+ print(html)
84
+ ```
85
+
86
+ For a complete HTML document including base CSS:
87
+
88
+ ```python
89
+ document = render_document(shell)
90
+ # Returns a full <html>...<body>...</body></html> string with embedded CSS.
91
+ ```
92
+
93
+ ## API
94
+
95
+ | Symbol | Description |
96
+ |--------|-------------|
97
+ | `render_fragment(shell)` | Renders the shell as an HTML fragment (no `<html>` or `<head>` wrapper). Returns a string. |
98
+ | `render_document(shell)` | Renders the shell as a complete HTML document, including the base CSS inline. Returns a string. |
99
+ | `get_base_css()` | Returns the base CSS string for manual inclusion in your own template. |
100
+ | `HTMLRenderer` | Low-level renderer class. Use this for custom integration or when you need per-region control. |
101
+
102
+ All symbols are importable from `panelmark_html`. Full API reference:
103
+ [panelmark-html rendering API](https://github.com/sirrommit/panelmark-docs/blob/main/docs/panelmark-html/rendering-api.md)
104
+
105
+ ## Documentation
106
+
107
+ | Document | Description |
108
+ |----------|-------------|
109
+ | [Hook Contract](docs/hook-contract.md) | Stable DOM interface: element structure, CSS classes, `data-pm-*` attributes, CSS custom properties. **Canonical location — this repo.** |
110
+ | [Rendering API Reference](https://github.com/sirrommit/panelmark-docs/blob/main/docs/panelmark-html/rendering-api.md) | Full reference for `render_fragment`, `render_document`, `get_base_css`, and `HTMLRenderer` |
111
+ | [Ecosystem Overview](https://github.com/sirrommit/panelmark-docs/blob/main/docs/ecosystem.md) | How panelmark-html fits into the panelmark package ecosystem |
112
+ | [Shell Language](https://github.com/sirrommit/panelmark-docs/blob/main/docs/shell-language/overview.md) | ASCII-art layout syntax reference |
113
+ | [panelmark-web](https://github.com/sirrommit/panelmark-web) | Live web runtime that builds on this package's hook contract |
@@ -0,0 +1,55 @@
1
+ """panelmark-html — static HTML/CSS renderer for panelmark shells.
2
+
3
+ This package converts a panelmark Shell into an HTML document or fragment.
4
+ It does not handle browser events, network transport, or live interaction
5
+ state. For a live web application, use panelmark-web, which builds on top
6
+ of this package.
7
+
8
+ Public API
9
+ ----------
10
+ render_fragment(shell, *, include_css=False) -> str
11
+ render_document(shell, *, title="panelmark", css_href=None, extra_head="") -> str
12
+ get_base_css() -> str
13
+ HTMLRenderer
14
+ """
15
+
16
+ from .renderer import HTMLRenderer
17
+ from .css import get_base_css
18
+
19
+
20
+ def render_fragment(shell, *, include_css: bool = False) -> str:
21
+ """Return an HTML fragment for *shell*.
22
+
23
+ Parameters
24
+ ----------
25
+ shell:
26
+ A ``panelmark.Shell`` instance.
27
+ include_css:
28
+ If True, prepend a ``<style>`` block with the base CSS.
29
+ """
30
+ return HTMLRenderer().render_fragment(shell, include_css=include_css)
31
+
32
+
33
+ def render_document(shell, *, title: str = 'panelmark',
34
+ css_href: str | None = None,
35
+ extra_head: str = '') -> str:
36
+ """Return a full HTML document for *shell*.
37
+
38
+ Parameters
39
+ ----------
40
+ shell:
41
+ A ``panelmark.Shell`` instance.
42
+ title:
43
+ Value for the ``<title>`` element.
44
+ css_href:
45
+ If provided, emit a ``<link>`` to this stylesheet instead of
46
+ inlining the base CSS.
47
+ extra_head:
48
+ Optional raw HTML string appended inside ``<head>``.
49
+ """
50
+ return HTMLRenderer().render_document(
51
+ shell, title=title, css_href=css_href, extra_head=extra_head
52
+ )
53
+
54
+
55
+ __all__ = ['HTMLRenderer', 'render_fragment', 'render_document', 'get_base_css']
@@ -0,0 +1,135 @@
1
+ _BASE_CSS = """\
2
+ /* panelmark-html base styles
3
+ *
4
+ * All visual values are controlled by CSS custom properties.
5
+ * Override any variable in your own stylesheet to theme the shell.
6
+ *
7
+ * Custom properties
8
+ * -----------------
9
+ * --pm-border-color border / divider colour
10
+ * --pm-border-width thickness of all borders and dividers
11
+ * --pm-gap gap between sibling panels (not used by default;
12
+ * override to add spacing between panels)
13
+ * --pm-radius corner radius on panels
14
+ * --pm-heading-font-weight heading font weight
15
+ * --pm-panel-padding padding inside panel body and heading
16
+ * --pm-focused-border-color border colour on the focused panel
17
+ * --pm-focused-border-width border thickness on the focused panel
18
+ */
19
+
20
+ :root {
21
+ --pm-border-color: #888;
22
+ --pm-border-width: 1px;
23
+ --pm-gap: 0px;
24
+ --pm-radius: 0px;
25
+ --pm-heading-font-weight: bold;
26
+ --pm-panel-padding: 0.5rem;
27
+ --pm-focused-border-color: #4a9eff;
28
+ --pm-focused-border-width: 2px;
29
+ }
30
+
31
+ /* Shell container --------------------------------------------------------- */
32
+
33
+ .pm-shell {
34
+ display: flex;
35
+ flex-direction: column;
36
+ box-sizing: border-box;
37
+ width: 100%;
38
+ height: 100%;
39
+ overflow: hidden;
40
+ font-family: monospace, monospace;
41
+ font-size: 0.875rem;
42
+ border: var(--pm-border-width) solid var(--pm-border-color);
43
+ border-radius: var(--pm-radius);
44
+ }
45
+
46
+ /* Splits ------------------------------------------------------------------ */
47
+
48
+ .pm-split {
49
+ display: flex;
50
+ flex: 1 1 0;
51
+ min-height: 0;
52
+ min-width: 0;
53
+ gap: var(--pm-gap);
54
+ }
55
+
56
+ .pm-split-h {
57
+ flex-direction: column;
58
+ }
59
+
60
+ .pm-split-v {
61
+ flex-direction: row;
62
+ }
63
+
64
+ /* Single-pixel dividers between sibling panels, without double borders */
65
+ .pm-split-h > * + * {
66
+ border-top: var(--pm-border-width) solid var(--pm-border-color);
67
+ }
68
+
69
+ .pm-split-v > * + * {
70
+ border-left: var(--pm-border-width) solid var(--pm-border-color);
71
+ }
72
+
73
+ /* Panels ------------------------------------------------------------------ */
74
+
75
+ .pm-panel[data-pm-focused="true"] {
76
+ outline: var(--pm-focused-border-width) solid var(--pm-focused-border-color);
77
+ outline-offset: -1px;
78
+ }
79
+
80
+ .pm-panel {
81
+ display: flex;
82
+ flex-direction: column;
83
+ flex: 1 1 0;
84
+ min-height: 0;
85
+ min-width: 0;
86
+ box-sizing: border-box;
87
+ overflow: hidden;
88
+ }
89
+
90
+ .pm-panel-heading {
91
+ flex-shrink: 0;
92
+ box-sizing: border-box;
93
+ padding: 0.25rem var(--pm-panel-padding);
94
+ font-weight: var(--pm-heading-font-weight);
95
+ border-bottom: var(--pm-border-width) solid var(--pm-border-color);
96
+ overflow: hidden;
97
+ text-overflow: ellipsis;
98
+ white-space: nowrap;
99
+ }
100
+
101
+ .pm-panel-body {
102
+ flex: 1 1 0;
103
+ min-height: 0;
104
+ box-sizing: border-box;
105
+ padding: var(--pm-panel-padding);
106
+ overflow: auto;
107
+ }
108
+ """
109
+
110
+
111
+ def get_base_css() -> str:
112
+ """Return the base CSS string for panelmark-html layouts.
113
+
114
+ The returned string uses CSS custom properties (variables) for all
115
+ visual values so that embedding applications can theme the shell
116
+ without modifying this stylesheet.
117
+
118
+ The focused-panel highlight is controlled by
119
+ ``--pm-focused-border-color`` and ``--pm-focused-border-width``.
120
+ These are used by the ``[data-pm-focused="true"]`` rule and are
121
+ updated live by ``panelmark-web`` after each key event.
122
+
123
+ Suggested usage::
124
+
125
+ # Inline in a document
126
+ page = render_document(shell, title="My App")
127
+
128
+ # Link an external copy
129
+ page = render_document(shell, css_href="/static/panelmark.css")
130
+ # then write get_base_css() to that path at build time
131
+
132
+ # Embed in a fragment
133
+ fragment = render_fragment(shell, include_css=True)
134
+ """
135
+ return _BASE_CSS
@@ -0,0 +1,166 @@
1
+ from html import escape
2
+
3
+ from panelmark.layout import HSplit, VSplit, Panel
4
+
5
+ from .css import get_base_css
6
+
7
+
8
+ class HTMLRenderer:
9
+ """Converts a panelmark Shell into an HTML fragment or full document.
10
+
11
+ Renders the shell's panel structure with stable DOM hooks. Each named
12
+ panel that has an assigned interaction receives ``data-pm-interaction``,
13
+ ``data-pm-focusable``, and ``data-pm-focused`` attributes. Named panels
14
+ without an assigned interaction receive ``data-pm-empty="true"``. Panel
15
+ bodies are always empty placeholders; interaction body content is deferred
16
+ to ``panelmark-web``.
17
+
18
+ CSS rules are provided by ``get_base_css()``.
19
+ """
20
+
21
+ def render_fragment(self, shell, *, include_css: bool = False) -> str:
22
+ """Return an HTML fragment for *shell*.
23
+
24
+ Parameters
25
+ ----------
26
+ shell:
27
+ A ``panelmark.Shell`` instance.
28
+ include_css:
29
+ If True, prepend a ``<style>`` block with the base CSS.
30
+ """
31
+ parts = []
32
+ if include_css:
33
+ css = get_base_css()
34
+ if css:
35
+ parts.append(f'<style>\n{css}\n</style>\n')
36
+ parts.append(self._render_shell(shell))
37
+ return ''.join(parts)
38
+
39
+ def render_document(self, shell, *, title: str = 'panelmark',
40
+ css_href: str | None = None,
41
+ extra_head: str = '') -> str:
42
+ """Return a full HTML document for *shell*.
43
+
44
+ Parameters
45
+ ----------
46
+ shell:
47
+ A ``panelmark.Shell`` instance.
48
+ title:
49
+ Value for the ``<title>`` element.
50
+ css_href:
51
+ If provided, emit a ``<link>`` to this stylesheet instead of
52
+ inlining the base CSS.
53
+ extra_head:
54
+ Optional raw HTML string appended inside ``<head>`` before
55
+ ``</head>``.
56
+ """
57
+ head_lines = [
58
+ ' <meta charset="UTF-8">',
59
+ ' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
60
+ f' <title>{escape(title)}</title>',
61
+ ]
62
+ if css_href:
63
+ head_lines.append(
64
+ f' <link rel="stylesheet" href="{escape(css_href, quote=True)}">'
65
+ )
66
+ else:
67
+ css = get_base_css()
68
+ if css:
69
+ head_lines.append(f' <style>\n{css}\n </style>')
70
+ if extra_head:
71
+ head_lines.append(f' {extra_head}')
72
+
73
+ head = '\n'.join(head_lines)
74
+ fragment = self._render_shell(shell)
75
+
76
+ return (
77
+ '<!DOCTYPE html>\n'
78
+ '<html lang="en">\n'
79
+ '<head>\n'
80
+ f'{head}\n'
81
+ '</head>\n'
82
+ '<body>\n'
83
+ f'{fragment}'
84
+ '</body>\n'
85
+ '</html>\n'
86
+ )
87
+
88
+ # ------------------------------------------------------------------
89
+ # Internal rendering
90
+ # ------------------------------------------------------------------
91
+
92
+ def _render_shell(self, shell) -> str:
93
+ node = shell.layout.root
94
+ if node is None:
95
+ return '<div class="pm-shell" data-pm-shell></div>\n'
96
+ inner = self._render_node(node, shell, indent=2)
97
+ return f'<div class="pm-shell" data-pm-shell>\n{inner}</div>\n'
98
+
99
+ def _render_node(self, node, shell, indent: int = 0) -> str:
100
+ if node is None:
101
+ return ''
102
+ pad = ' ' * indent
103
+
104
+ if isinstance(node, Panel):
105
+ return self._render_panel(node, shell, indent)
106
+
107
+ if isinstance(node, VSplit):
108
+ left = self._render_node(node.left, shell, indent + 2)
109
+ right = self._render_node(node.right, shell, indent + 2)
110
+ return (
111
+ f'{pad}<div class="pm-split pm-split-v">\n'
112
+ f'{left}'
113
+ f'{right}'
114
+ f'{pad}</div>\n'
115
+ )
116
+
117
+ if isinstance(node, HSplit):
118
+ top = self._render_node(node.top, shell, indent + 2)
119
+ bottom = self._render_node(node.bottom, shell, indent + 2)
120
+ return (
121
+ f'{pad}<div class="pm-split pm-split-h">\n'
122
+ f'{top}'
123
+ f'{bottom}'
124
+ f'{pad}</div>\n'
125
+ )
126
+
127
+ return ''
128
+
129
+ def _render_panel(self, node: Panel, shell, indent: int = 0) -> str:
130
+ pad = ' ' * indent
131
+ inner = ' ' * (indent + 2)
132
+
133
+ attrs = ['class="pm-panel"']
134
+ if node.name:
135
+ attrs.append(f'data-pm-region="{escape(node.name, quote=True)}"')
136
+ attrs.append('data-pm-kind="panel"')
137
+ attrs.append(f'id="pm-region-{escape(node.name, quote=True)}"')
138
+
139
+ if node.heading:
140
+ attrs.append(f'data-pm-heading="{escape(node.heading, quote=True)}"')
141
+
142
+ interaction = shell.interactions.get(node.name)
143
+ if interaction is not None:
144
+ cls = type(interaction)
145
+ qualified = f'{cls.__module__}.{cls.__qualname__}'
146
+ focusable = 'true' if interaction.is_focusable else 'false'
147
+ focused = 'true' if shell.focus == node.name else 'false'
148
+ attrs.append(f'data-pm-interaction="{escape(qualified, quote=True)}"')
149
+ attrs.append(f'data-pm-focusable="{focusable}"')
150
+ attrs.append(f'data-pm-focused="{focused}"')
151
+ else:
152
+ attrs.append('data-pm-empty="true"')
153
+
154
+ lines = [f'{pad}<section {" ".join(attrs)}>\n']
155
+
156
+ if node.heading:
157
+ lines.append(
158
+ f'{inner}<header class="pm-panel-heading">'
159
+ f'{escape(node.heading)}'
160
+ f'</header>\n'
161
+ )
162
+
163
+ lines.append(f'{inner}<div class="pm-panel-body"></div>\n')
164
+ lines.append(f'{pad}</section>\n')
165
+
166
+ return ''.join(lines)