kash-shell 0.3.10__py3-none-any.whl → 0.3.11__py3-none-any.whl

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.
Files changed (40) hide show
  1. kash/actions/core/format_markdown_template.py +2 -5
  2. kash/actions/core/markdownify.py +2 -4
  3. kash/actions/core/readability.py +2 -4
  4. kash/actions/core/render_as_html.py +30 -11
  5. kash/actions/core/show_webpage.py +6 -11
  6. kash/actions/core/strip_html.py +2 -6
  7. kash/actions/core/{webpage_config.py → tabbed_webpage_config.py} +5 -3
  8. kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -4
  9. kash/commands/base/files_command.py +28 -10
  10. kash/commands/workspace/workspace_commands.py +1 -2
  11. kash/config/colors.py +2 -2
  12. kash/exec/action_decorators.py +6 -6
  13. kash/exec/llm_transforms.py +6 -3
  14. kash/exec/preconditions.py +6 -0
  15. kash/exec/resolve_args.py +4 -0
  16. kash/file_storage/file_store.py +20 -18
  17. kash/help/function_param_info.py +1 -1
  18. kash/local_server/local_server_routes.py +1 -7
  19. kash/model/items_model.py +74 -28
  20. kash/shell/utils/shell_function_wrapper.py +15 -15
  21. kash/text_handling/doc_normalization.py +1 -1
  22. kash/text_handling/markdown_render.py +1 -0
  23. kash/text_handling/markdown_utils.py +22 -0
  24. kash/utils/common/function_inspect.py +360 -110
  25. kash/utils/file_utils/file_ext.py +4 -0
  26. kash/utils/file_utils/file_formats_model.py +17 -1
  27. kash/web_gen/__init__.py +0 -4
  28. kash/web_gen/simple_webpage.py +52 -0
  29. kash/web_gen/tabbed_webpage.py +23 -16
  30. kash/web_gen/template_render.py +37 -2
  31. kash/web_gen/templates/base_styles.css.jinja +76 -56
  32. kash/web_gen/templates/base_webpage.html.jinja +85 -67
  33. kash/web_gen/templates/item_view.html.jinja +47 -37
  34. kash/web_gen/templates/simple_webpage.html.jinja +24 -0
  35. kash/web_gen/templates/tabbed_webpage.html.jinja +42 -32
  36. {kash_shell-0.3.10.dist-info → kash_shell-0.3.11.dist-info}/METADATA +5 -5
  37. {kash_shell-0.3.10.dist-info → kash_shell-0.3.11.dist-info}/RECORD +40 -38
  38. {kash_shell-0.3.10.dist-info → kash_shell-0.3.11.dist-info}/WHEEL +0 -0
  39. {kash_shell-0.3.10.dist-info → kash_shell-0.3.11.dist-info}/entry_points.txt +0 -0
  40. {kash_shell-0.3.10.dist-info → kash_shell-0.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,52 @@
1
+ from kash.model.items_model import Item
2
+ from kash.utils.file_utils.file_formats_model import Format
3
+ from kash.web_gen.template_render import render_web_template
4
+
5
+
6
+ def simple_webpage_render(
7
+ item: Item,
8
+ page_template: str = "simple_webpage.html.jinja",
9
+ add_title_h1: bool = True,
10
+ ) -> str:
11
+ """
12
+ Generate a simple web page from a single item.
13
+ If `add_title_h1` is True, the title will be inserted as an h1 heading above the body.
14
+ """
15
+ return render_web_template(
16
+ template_filename=page_template,
17
+ data={
18
+ "title": item.title,
19
+ "add_title_h1": add_title_h1,
20
+ "content_html": item.body_as_html(),
21
+ "thumbnail_url": item.thumbnail_url,
22
+ },
23
+ )
24
+
25
+
26
+ ## Tests
27
+
28
+
29
+ def test_render():
30
+ import os
31
+
32
+ from kash.model.items_model import ItemType
33
+
34
+ # Create a test item
35
+ item = Item(
36
+ type=ItemType.doc,
37
+ format=Format.html,
38
+ title="A Simple Web Page",
39
+ body="<p>This is a simple web page with <b>HTML content</b>.</p>",
40
+ )
41
+
42
+ # Generate HTML
43
+ html = simple_webpage_render(item)
44
+
45
+ os.makedirs("tmp", exist_ok=True)
46
+ with open("tmp/simple_webpage.html", "w") as f:
47
+ f.write(html)
48
+ print("Rendered simple webpage to tmp/simple_webpage.html")
49
+
50
+ # Basic validation
51
+ assert item.title and item.title in html
52
+ assert "<b>HTML content</b>" in html
@@ -2,7 +2,7 @@ import os
2
2
  from dataclasses import asdict, dataclass
3
3
 
4
4
  from frontmatter_format import read_yaml_file, to_yaml_string, write_yaml_file
5
- from prettyfmt import sanitize_title
5
+ from prettyfmt import abbrev_on_words, sanitize_title
6
6
 
7
7
  from kash.config.logger import get_logger
8
8
  from kash.exec.preconditions import has_thumbnail_url
@@ -12,7 +12,6 @@ from kash.model.paths_model import StorePath
12
12
  from kash.utils.common.type_utils import as_dataclass, not_none
13
13
  from kash.utils.errors import NoMatch
14
14
  from kash.utils.file_utils.file_formats_model import Format
15
- from kash.web_gen import base_templates_dir
16
15
  from kash.web_gen.template_render import render_web_template
17
16
  from kash.workspaces import current_ws
18
17
  from kash.workspaces.source_items import find_upstream_item
@@ -34,6 +33,7 @@ class TabbedWebpage:
34
33
  title: str
35
34
  tabs: list[TabInfo]
36
35
  show_tabs: bool = True
36
+ add_title_h1: bool = True
37
37
 
38
38
 
39
39
  def _fill_in_ids(tabs: list[TabInfo]):
@@ -42,7 +42,9 @@ def _fill_in_ids(tabs: list[TabInfo]):
42
42
  tab.id = f"tab_{i}"
43
43
 
44
44
 
45
- def webpage_config(items: list[Item], clean_headings: bool = False) -> Item:
45
+ def tabbed_webpage_config(
46
+ items: list[Item], clean_headings: bool = False, add_title_h1: bool = True
47
+ ) -> Item:
46
48
  """
47
49
  Get an item with the config for a tabbed web page.
48
50
  """
@@ -58,11 +60,15 @@ def webpage_config(items: list[Item], clean_headings: bool = False) -> Item:
58
60
  log.warning("Item has no thumbnail URL: %s", item)
59
61
  return None
60
62
 
61
- clean = clean_heading if clean_headings else sanitize_title
63
+ def clean_label(label: str) -> str:
64
+ if clean_headings:
65
+ return clean_heading(label)
66
+ else:
67
+ return abbrev_on_words(sanitize_title(label), max_len=40)
62
68
 
63
69
  tabs = [
64
70
  TabInfo(
65
- label=clean(item.abbrev_title()),
71
+ label=clean_label(item.abbrev_title()),
66
72
  store_path=item.store_path,
67
73
  thumbnail_url=get_thumbnail_url(item),
68
74
  )
@@ -70,7 +76,9 @@ def webpage_config(items: list[Item], clean_headings: bool = False) -> Item:
70
76
  ]
71
77
  _fill_in_ids(tabs)
72
78
  title = summary_heading([item.abbrev_title() for item in items])
73
- config = TabbedWebpage(title=title, tabs=tabs, show_tabs=len(tabs) > 1)
79
+ config = TabbedWebpage(
80
+ title=title, tabs=tabs, show_tabs=len(tabs) > 1, add_title_h1=add_title_h1
81
+ )
74
82
 
75
83
  config_item = Item(
76
84
  title=f"{title} (config)",
@@ -91,7 +99,9 @@ def _load_tab_content(config: TabbedWebpage):
91
99
  tab.content_html = html
92
100
 
93
101
 
94
- def webpage_generate(config_item: Item) -> str:
102
+ def tabbed_webpage_generate(
103
+ config_item: Item, page_template: str = "base_webpage.html.jinja", add_title_h1: bool = True
104
+ ) -> str:
95
105
  """
96
106
  Generate a web page using the supplied config.
97
107
  """
@@ -101,14 +111,15 @@ def webpage_generate(config_item: Item) -> str:
101
111
  _load_tab_content(tabbed_webpage)
102
112
 
103
113
  content = render_web_template(
104
- base_templates_dir, "tabbed_webpage.html.jinja", asdict(tabbed_webpage)
114
+ template_filename="tabbed_webpage.html.jinja",
115
+ data=asdict(tabbed_webpage),
105
116
  )
106
117
 
107
118
  return render_web_template(
108
- base_templates_dir,
109
- "base_webpage.html.jinja",
110
- {
119
+ page_template,
120
+ data={
111
121
  "title": tabbed_webpage.title,
122
+ "add_title_h1": add_title_h1,
112
123
  "content": content,
113
124
  },
114
125
  )
@@ -138,11 +149,7 @@ def test_render():
138
149
  new_config = as_dataclass(read_yaml_file("tmp/webpage_config.yaml"), TabbedWebpage)
139
150
  assert new_config == config
140
151
 
141
- html = render_web_template(
142
- base_templates_dir,
143
- "tabbed_webpage.html.jinja",
144
- asdict(config),
145
- )
152
+ html = render_web_template(template_filename="tabbed_webpage.html.jinja", data=asdict(config))
146
153
  with open("tmp/webpage.html", "w") as f:
147
154
  f.write(html)
148
155
  print("Rendered tabbed webpage to tmp/webpage.html")
@@ -1,12 +1,45 @@
1
+ from collections.abc import Iterator
2
+ from contextlib import contextmanager
3
+ from contextvars import ContextVar
1
4
  from pathlib import Path
2
5
 
3
6
  from jinja2 import Environment, FileSystemLoader
4
7
 
5
8
  from kash.config import colors
6
9
 
10
+ _base_templates_dir = Path(__file__).parent / "templates"
11
+ """Common base web page templates."""
12
+
13
+
14
+ _additional_template_dirs: ContextVar[list[Path] | None] = ContextVar(
15
+ "_additional_template_dirs", default=None
16
+ )
17
+
18
+
19
+ def get_template_dirs(*dirs: Path) -> list[Path]:
20
+ """
21
+ Returns template directories currently in context along with any
22
+ additional template directories.
23
+ """
24
+ additional = _additional_template_dirs.get() or []
25
+ return list(dirs) + [*additional, _base_templates_dir]
26
+
27
+
28
+ @contextmanager
29
+ def additional_template_dirs(*dirs: Path) -> Iterator[None]:
30
+ """
31
+ Context manager for temporarily adding template directories to the search path.
32
+ """
33
+ original = _additional_template_dirs.get()
34
+ current = [] if not original else original.copy()
35
+ token = _additional_template_dirs.set(current + list(dirs))
36
+ try:
37
+ yield
38
+ finally:
39
+ _additional_template_dirs.reset(token)
40
+
7
41
 
8
42
  def render_web_template(
9
- templates_dir: Path,
10
43
  template_filename: str,
11
44
  data: dict,
12
45
  autoescape: bool = True,
@@ -14,11 +47,13 @@ def render_web_template(
14
47
  ) -> str:
15
48
  """
16
49
  Render a Jinja2 template file with the given data, returning an HTML string.
50
+ Uses template directories from the base directory and any added via context manager.
17
51
  """
18
52
  if css_overrides is None:
19
53
  css_overrides = {}
20
54
 
21
- env = Environment(loader=FileSystemLoader(templates_dir), autoescape=autoescape)
55
+ search_paths = get_template_dirs()
56
+ env = Environment(loader=FileSystemLoader(search_paths), autoescape=autoescape)
22
57
 
23
58
  # Load and render the template.
24
59
  template = env.get_template(template_filename)
@@ -1,4 +1,5 @@
1
1
  :root {
2
+ {% block root_variables %}
2
3
  font-size: 16px;
3
4
  /* Adding Hack Nerd Font to all fonts for icon support, if it is installed. */
4
5
  --font-sans: "Source Sans 3 Variable", sans-serif, "Hack Nerd Font";
@@ -16,14 +17,17 @@
16
17
 
17
18
  --console-char-width: 88;
18
19
  --console-width: calc(var(--console-char-width) + 2rem);
20
+ {% endblock root_variables %}
19
21
  }
20
22
 
21
23
  {{ color_defs|safe }}
22
24
 
25
+ {% block selection_styles %}
23
26
  ::selection {
24
27
  background-color: var(--color-selection);
25
28
  color: inherit;
26
29
  }
30
+ {% endblock selection_styles %}
27
31
 
28
32
  {# TODO: Fix PDF issues and re-enable for prettier emoji. #}
29
33
  {# @font-face {
@@ -39,6 +43,7 @@
39
43
  U+1F900-1F9FF; /* Supplemental Symbols and Pictographs */
40
44
  } #}
41
45
 
46
+ {% block scrollbar_styles %}
42
47
  /* Scrollbar coloring. */
43
48
  /* For Webkit browsers (Chrome, Safari) */
44
49
  ::-webkit-scrollbar {
@@ -60,7 +65,9 @@
60
65
  scrollbar-width: thin;
61
66
  scrollbar-color: var(--color-scrollbar) var(--color-bg);
62
67
  }
68
+ {% endblock scrollbar_styles %}
63
69
 
70
+ {% block body_styles %}
64
71
  body {
65
72
  font-family: var(--font-serif);
66
73
  color: var(--color-text);
@@ -70,7 +77,9 @@ body {
70
77
  background-color: var(--color-bg);
71
78
  overflow-wrap: break-word; /* Don't let long words/URLs break layout. */
72
79
  }
80
+ {% endblock body_styles %}
73
81
 
82
+ {% block typography %}
74
83
  p {
75
84
  margin-bottom: 1rem;
76
85
  }
@@ -120,37 +129,6 @@ h4 {
120
129
  margin-bottom: 0.7rem;
121
130
  }
122
131
 
123
- /* Long text stylings, for nicely formatting blog post length or longer texts. */
124
-
125
- .long-text h1 {
126
- font-family: var(--font-serif);
127
- font-weight: 400;
128
- }
129
-
130
- .long-text h2 {
131
- font-family: var(--font-serif);
132
- font-weight: 400;
133
- font-style: italic;
134
- }
135
-
136
- .long-text h3 {
137
- font-family: var(--font-sans);
138
- font-weight: var(--font-weight-sans-bold);
139
- text-transform: uppercase;
140
- letter-spacing: 0.02em;
141
- }
142
-
143
- .long-text h4 {
144
- font-family: var(--font-serif);
145
- font-weight: 700;
146
- }
147
-
148
- .subtitle {
149
- font-family: var(--font-serif);
150
- font-style: italic;
151
- font-size: 1rem;
152
- }
153
-
154
132
  ul {
155
133
  list-style-type: none;
156
134
  margin-left: 2rem;
@@ -207,7 +185,42 @@ pre {
207
185
  letter-spacing: -0.025em;
208
186
  {# overflow-x: auto; #}
209
187
  }
188
+ {% endblock typography %}
189
+
190
+ {% block long_text_styles %}
191
+ /* Long text stylings, for nicely formatting blog post length or longer texts. */
192
+
193
+ .long-text h1 {
194
+ font-family: var(--font-serif);
195
+ font-weight: 400;
196
+ }
197
+
198
+ .long-text h2 {
199
+ font-family: var(--font-serif);
200
+ font-weight: 400;
201
+ font-style: italic;
202
+ }
203
+
204
+ .long-text h3 {
205
+ font-family: var(--font-sans);
206
+ font-weight: var(--font-weight-sans-bold);
207
+ text-transform: uppercase;
208
+ letter-spacing: 0.02em;
209
+ }
210
+
211
+ .long-text h4 {
212
+ font-family: var(--font-serif);
213
+ font-weight: 700;
214
+ }
210
215
 
216
+ .subtitle {
217
+ font-family: var(--font-serif);
218
+ font-style: italic;
219
+ font-size: 1rem;
220
+ }
221
+ {% endblock long_text_styles %}
222
+
223
+ {% block table_styles %}
211
224
  table {
212
225
  font-family: var(--font-sans);
213
226
  font-size: var(--font-size-small);
@@ -239,16 +252,6 @@ tbody tr:nth-child(even) {
239
252
  background-color: var(--color-bg-alt-solid);
240
253
  }
241
254
 
242
- nav {
243
- display: flex;
244
- flex-wrap: wrap;
245
- /* Allow wrapping */
246
- justify-content: center;
247
- /* Center the content */
248
- gap: 1rem;
249
- /* Add some space between the buttons */
250
- }
251
-
252
255
  /* Container for wide tables to allow tables to break out of parent width. */
253
256
  .table-container {
254
257
  {# max-width: calc(100vw - 6rem); #}
@@ -258,26 +261,24 @@ nav {
258
261
  box-sizing: border-box;
259
262
  margin-bottom: 1rem;
260
263
  background-color: var(--color-bg-solid);
261
-
262
264
  }
265
+ {% endblock table_styles %}
263
266
 
264
- /* Bleed wide on larger screens. */
265
- /* TODO: Don't make so wide if table itself isn't large? */
266
- @media (min-width: 768px) {
267
- table {
268
- width: calc(100vw - 6rem);
269
- }
270
- .table-container {
271
- width: calc(100vw - 6rem);
272
- }
267
+ {% block nav_styles %}
268
+ nav {
269
+ display: flex;
270
+ flex-wrap: wrap;
271
+ /* Allow wrapping */
272
+ justify-content: center;
273
+ /* Center the content */
274
+ gap: 1rem;
275
+ /* Add some space between the buttons */
273
276
  }
277
+ {% endblock nav_styles %}
274
278
 
275
- @media (max-width: 768px) {
276
- table {
277
- font-size: var(--font-size-smaller);
278
- }
279
- }
280
279
 
280
+
281
+ {% block footnote_styles %}
281
282
  /* Footnotes. */
282
283
  sup {
283
284
  font-size: 80%;
@@ -295,4 +296,23 @@ sup {
295
296
  color: var(--color-primary-light);
296
297
  text-decoration: none;
297
298
  }
299
+ {% endblock footnote_styles %}
300
+
301
+ {% block responsive_styles %}
302
+ /* Bleed wide on larger screens. */
303
+ /* TODO: Don't make so wide if table itself isn't large? */
304
+ @media (min-width: 768px) {
305
+ table {
306
+ width: calc(100vw - 6rem);
307
+ }
308
+ .table-container {
309
+ width: calc(100vw - 6rem);
310
+ }
311
+ }
298
312
 
313
+ @media (max-width: 768px) {
314
+ table {
315
+ font-size: var(--font-size-smaller);
316
+ }
317
+ }
318
+ {% endblock responsive_styles %}
@@ -2,10 +2,14 @@
2
2
  <html lang="en">
3
3
 
4
4
  <head>
5
+ {% block meta %}
5
6
  <meta charset="UTF-8" />
6
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>{{ title }}</title>
8
+ {% endblock meta %}
8
9
 
10
+ {% block title %}<title>{{ title }}</title>{% endblock title %}
11
+
12
+ {% block head_basic %}
9
13
  <link rel="preconnect" href="https://fonts.googleapis.com" />
10
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
15
  <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
@@ -26,12 +30,12 @@
26
30
 
27
31
  <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet" />
28
32
  <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js" defer></script>
33
+ {% endblock head_basic %}
29
34
 
30
- {% if extra_head %}
31
- {{ extra_head|safe }}
32
- {% endif %}
35
+ {% block head_extra %}{% endblock head_extra %}
33
36
 
34
37
  <style>
38
+ {% block font_faces %}
35
39
  /* https://fontsource.org/fonts/pt-serif/cdn */
36
40
  /* pt-serif-latin-400-normal */
37
41
  @font-face {
@@ -88,84 +92,98 @@
88
92
  src: url(https://cdn.jsdelivr.net/fontsource/fonts/source-sans-3:vf@latest/latin-wght-italic.woff2) format('woff2-variations');
89
93
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
90
94
  }
91
-
92
95
  {# Other decent sans serif options: Work Sans Variable, Nunito Sans Variable #}
96
+ {% endblock font_faces %}
93
97
 
98
+ {% block base_styles %}
94
99
  {% include "base_styles.css.jinja" %}
100
+ {% endblock base_styles %}
101
+
102
+ {% block content_styles %}
95
103
  {% include "content_styles.css.jinja" %}
96
-
97
- {% if extra_css %}
98
- {{ extra_css|safe }}
99
- {% endif %}
104
+ {% endblock content_styles %}
105
+
106
+ {% block custom_styles %}{% endblock custom_styles %}
100
107
  </style>
101
-
102
108
  </head>
103
109
 
104
110
  <body>
105
- {{ content|safe }}
106
-
107
- <script>
108
- document.addEventListener('DOMContentLoaded', () => {
109
- // Send messages to the parent window, in case we are in a viewport where that matters
110
- // (e.g. an iframe tooltip).
111
- // Request a resize of the parent viewport. This iframe size message format isn't
112
- // standardized by ResizeObserver, but is common. It is supported by Kerm.
113
- const content = document.body;
114
- console.log("Suggesting resize to parent:", content.offsetWidth, content.offsetHeight);
115
-
116
- window.parent.postMessage({
117
- type: 'resize',
118
- width: Math.max(content.offsetWidth, 600),
119
- height: Math.max(content.offsetHeight, 100)
120
- }, '*');
121
-
122
- // Wrap tables within the main content area for horizontal scrolling.
123
- const containers = [];
124
- document.querySelectorAll('.long-text').forEach(el => {
125
- const pane = el.querySelector('.tab-pane');
126
- containers.push(pane || el);
127
- });
128
- containers.forEach(container => {
129
- // Grab all tables, then only wrap the ones whose parent is this container.
130
- container.querySelectorAll('table').forEach(table => {
131
- if (table.parentElement !== container) {
132
- return; // Only direct children.
133
- }
134
- if (table.parentNode.classList.contains('table-container')) {
135
- return; // Already wrapped.
136
- }
137
- const wrapper = document.createElement('div');
138
- wrapper.className = 'table-container';
139
- container.insertBefore(wrapper, table);
140
- wrapper.appendChild(table);
141
- });
142
- });
111
+ {% block body_header %}{% endblock body_header %}
112
+
113
+ {% block main_content %}
114
+ {{ content|safe }}
115
+ {% endblock main_content %}
116
+
117
+ {% block body_footer %}{% endblock body_footer %}
118
+
119
+ {% block scripts %}
120
+ <script>
121
+ document.addEventListener('DOMContentLoaded', () => {
122
+ // Send messages to the parent window, in case we are in a viewport where that matters
123
+ // (e.g. an iframe tooltip).
124
+ // Request a resize of the parent viewport. This iframe size message format isn't
125
+ // standardized by ResizeObserver, but is common. It is supported by Kerm.
126
+ const content = document.body;
127
+ console.log("Suggesting resize to parent:", content.offsetWidth, content.offsetHeight);
143
128
 
144
- });
129
+ window.parent.postMessage({
130
+ type: 'resize',
131
+ width: Math.max(content.offsetWidth, 600),
132
+ height: Math.max(content.offsetHeight, 100)
133
+ }, '*');
145
134
 
146
- // Double-click to expand (e.g. expand tooltip to popover).
147
- document.addEventListener('dblclick', () => {
148
- console.log("Sending expand message to parent");
149
- window.parent.postMessage({
150
- type: 'expand'
151
- }, '*');
152
- });
135
+ // Wrap tables within the main content area for horizontal scrolling.
136
+ const containers = [];
137
+ document.querySelectorAll('.long-text').forEach(el => {
138
+ const pane = el.querySelector('.tab-pane');
139
+ containers.push(pane || el);
140
+ });
141
+ containers.forEach(container => {
142
+ // Find all tables within the container.
143
+ const tables = Array.from(container.querySelectorAll('table'));
144
+ tables.forEach(table => {
145
+ // Skip tables already in table-container divs
146
+ if (table.parentNode.classList.contains('table-container')) {
147
+ return;
148
+ }
149
+ try {
150
+ // Create the wrapper.
151
+ const wrapper = document.createElement('div');
152
+ wrapper.className = 'table-container';
153
+ // Get the parent and insert the wrapper where the table is, then move the table into the wrapper.
154
+ const parent = table.parentNode;
155
+ parent.insertBefore(wrapper, table);
156
+ wrapper.appendChild(table);
157
+ } catch (e) {
158
+ console.error("Error wrapping table:", e);
159
+ }
160
+ });
161
+ });
162
+ });
153
163
 
154
- // Escape to close tooltip or popover.
155
- document.addEventListener('keydown', (event) => {
156
- if (event.key === 'Escape') {
157
- console.log("Sending close message to parent");
164
+ // Double-click to expand (e.g. expand tooltip to popover).
165
+ document.addEventListener('dblclick', () => {
166
+ console.log("Sending expand message to parent");
158
167
  window.parent.postMessage({
159
- type: 'close'
168
+ type: 'expand'
160
169
  }, '*');
161
- }
162
- });
170
+ });
163
171
 
164
- {% if extra_footer_js %}
165
- {{ extra_footer_js|safe }}
166
- {% endif %}
167
- </script>
172
+ // Escape to close tooltip or popover.
173
+ document.addEventListener('keydown', (event) => {
174
+ if (event.key === 'Escape') {
175
+ console.log("Sending close message to parent");
176
+ window.parent.postMessage({
177
+ type: 'close'
178
+ }, '*');
179
+ }
180
+ });
168
181
 
182
+ {% block scripts_extra %}{% endblock scripts_extra %}
183
+ </script>
184
+ {% endblock scripts %}
185
+
186
+ {% block analytics %}{% endblock analytics %}
169
187
  </body>
170
188
 
171
189
  </html>