kash-shell 0.3.10__py3-none-any.whl → 0.3.12__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.
- kash/actions/core/format_markdown_template.py +2 -5
- kash/actions/core/markdownify.py +2 -4
- kash/actions/core/readability.py +2 -4
- kash/actions/core/render_as_html.py +30 -11
- kash/actions/core/show_webpage.py +6 -11
- kash/actions/core/strip_html.py +4 -8
- kash/actions/core/{webpage_config.py → tabbed_webpage_config.py} +5 -3
- kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -4
- kash/commands/base/basic_file_commands.py +21 -3
- kash/commands/base/files_command.py +29 -10
- kash/commands/extras/parse_uv_lock.py +12 -3
- kash/commands/workspace/selection_commands.py +1 -1
- kash/commands/workspace/workspace_commands.py +2 -3
- kash/config/colors.py +2 -2
- kash/config/env_settings.py +2 -42
- kash/config/logger.py +30 -25
- kash/config/logger_basic.py +6 -6
- kash/config/settings.py +23 -7
- kash/config/setup.py +33 -5
- kash/config/text_styles.py +25 -22
- kash/embeddings/cosine.py +12 -4
- kash/embeddings/embeddings.py +16 -6
- kash/embeddings/text_similarity.py +10 -4
- kash/exec/__init__.py +3 -0
- kash/exec/action_decorators.py +10 -25
- kash/exec/action_exec.py +43 -23
- kash/exec/llm_transforms.py +6 -3
- kash/exec/preconditions.py +10 -12
- kash/exec/resolve_args.py +4 -0
- kash/exec/runtime_settings.py +134 -0
- kash/exec/shell_callable_action.py +5 -3
- kash/file_storage/file_store.py +37 -38
- kash/file_storage/item_file_format.py +6 -3
- kash/file_storage/store_filenames.py +6 -3
- kash/help/function_param_info.py +1 -1
- kash/llm_utils/init_litellm.py +16 -0
- kash/llm_utils/llm_api_keys.py +6 -2
- kash/llm_utils/llm_completion.py +11 -4
- kash/local_server/local_server_routes.py +1 -7
- kash/mcp/mcp_cli.py +3 -2
- kash/mcp/mcp_server_routes.py +11 -12
- kash/media_base/transcription_deepgram.py +15 -2
- kash/model/__init__.py +1 -1
- kash/model/actions_model.py +6 -54
- kash/model/exec_model.py +79 -0
- kash/model/items_model.py +102 -35
- kash/model/operations_model.py +38 -15
- kash/model/paths_model.py +2 -0
- kash/shell/output/shell_output.py +10 -8
- kash/shell/shell_main.py +2 -2
- kash/shell/utils/exception_printing.py +2 -2
- kash/shell/utils/shell_function_wrapper.py +15 -15
- kash/text_handling/doc_normalization.py +16 -8
- kash/text_handling/markdown_render.py +1 -0
- kash/text_handling/markdown_utils.py +105 -2
- kash/utils/common/format_utils.py +2 -8
- kash/utils/common/function_inspect.py +360 -110
- kash/utils/common/inflection.py +22 -0
- kash/utils/common/task_stack.py +4 -15
- kash/utils/errors.py +14 -9
- kash/utils/file_utils/file_ext.py +4 -0
- kash/utils/file_utils/file_formats_model.py +32 -1
- kash/utils/file_utils/file_sort_filter.py +10 -3
- kash/web_gen/__init__.py +0 -4
- kash/web_gen/simple_webpage.py +52 -0
- kash/web_gen/tabbed_webpage.py +23 -16
- kash/web_gen/template_render.py +37 -2
- kash/web_gen/templates/base_styles.css.jinja +84 -59
- kash/web_gen/templates/base_webpage.html.jinja +85 -67
- kash/web_gen/templates/item_view.html.jinja +47 -37
- kash/web_gen/templates/simple_webpage.html.jinja +24 -0
- kash/web_gen/templates/tabbed_webpage.html.jinja +42 -32
- kash/workspaces/__init__.py +12 -3
- kash/workspaces/workspace_dirs.py +58 -0
- kash/workspaces/workspace_importing.py +1 -1
- kash/workspaces/workspaces.py +26 -90
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/METADATA +7 -7
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/RECORD +81 -76
- kash/shell/utils/argparse_utils.py +0 -20
- kash/utils/lang_utils/inflection.py +0 -18
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -48,6 +48,7 @@ class Format(Enum):
|
|
|
48
48
|
"""`md_html` is Markdown with HTML, used for example when we structure Markdown with divs."""
|
|
49
49
|
html = "html"
|
|
50
50
|
"""`markdown` should be simple and clean Markdown that we can use with LLMs."""
|
|
51
|
+
epub = "epub"
|
|
51
52
|
yaml = "yaml"
|
|
52
53
|
diff = "diff"
|
|
53
54
|
python = "python"
|
|
@@ -56,12 +57,14 @@ class Format(Enum):
|
|
|
56
57
|
xonsh = "xonsh"
|
|
57
58
|
json = "json"
|
|
58
59
|
csv = "csv"
|
|
60
|
+
xlsx = "xlsx"
|
|
59
61
|
npz = "npz"
|
|
60
62
|
log = "log"
|
|
61
63
|
|
|
62
64
|
# Media formats.
|
|
63
65
|
pdf = "pdf"
|
|
64
66
|
docx = "docx"
|
|
67
|
+
pptx = "pptx"
|
|
65
68
|
jpeg = "jpeg"
|
|
66
69
|
png = "png"
|
|
67
70
|
gif = "gif"
|
|
@@ -100,6 +103,13 @@ class Format(Enum):
|
|
|
100
103
|
self.log,
|
|
101
104
|
]
|
|
102
105
|
|
|
106
|
+
@property
|
|
107
|
+
def is_simple_text(self) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Is this plaintext or close to it, like Markdown?
|
|
110
|
+
"""
|
|
111
|
+
return self in [self.plaintext, self.markdown, self.md_html]
|
|
112
|
+
|
|
103
113
|
@property
|
|
104
114
|
def is_doc(self) -> bool:
|
|
105
115
|
return self in [
|
|
@@ -108,6 +118,7 @@ class Format(Enum):
|
|
|
108
118
|
self.html,
|
|
109
119
|
self.pdf,
|
|
110
120
|
self.docx,
|
|
121
|
+
self.pptx,
|
|
111
122
|
]
|
|
112
123
|
|
|
113
124
|
@property
|
|
@@ -126,9 +137,17 @@ class Format(Enum):
|
|
|
126
137
|
def is_code(self) -> bool:
|
|
127
138
|
return self in [self.python, self.shellscript, self.xonsh, self.json, self.yaml]
|
|
128
139
|
|
|
140
|
+
@property
|
|
141
|
+
def is_markdown(self) -> bool:
|
|
142
|
+
return self in [self.markdown, self.md_html]
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def is_html(self) -> bool:
|
|
146
|
+
return self in [self.html, self.md_html]
|
|
147
|
+
|
|
129
148
|
@property
|
|
130
149
|
def is_data(self) -> bool:
|
|
131
|
-
return self in [self.csv, self.npz]
|
|
150
|
+
return self in [self.csv, self.xlsx, self.npz]
|
|
132
151
|
|
|
133
152
|
@property
|
|
134
153
|
def is_binary(self) -> bool:
|
|
@@ -166,6 +185,7 @@ class Format(Enum):
|
|
|
166
185
|
Format.markdown: MediaType.text,
|
|
167
186
|
Format.md_html: MediaType.text,
|
|
168
187
|
Format.html: MediaType.webpage,
|
|
188
|
+
Format.epub: MediaType.text,
|
|
169
189
|
Format.yaml: MediaType.text,
|
|
170
190
|
Format.diff: MediaType.text,
|
|
171
191
|
Format.python: MediaType.text,
|
|
@@ -175,11 +195,13 @@ class Format(Enum):
|
|
|
175
195
|
Format.csv: MediaType.text,
|
|
176
196
|
Format.log: MediaType.text,
|
|
177
197
|
Format.pdf: MediaType.text,
|
|
198
|
+
Format.xlsx: MediaType.text,
|
|
178
199
|
Format.jpeg: MediaType.image,
|
|
179
200
|
Format.png: MediaType.image,
|
|
180
201
|
Format.gif: MediaType.image,
|
|
181
202
|
Format.svg: MediaType.image,
|
|
182
203
|
Format.docx: MediaType.text,
|
|
204
|
+
Format.pptx: MediaType.text,
|
|
183
205
|
Format.mp3: MediaType.audio,
|
|
184
206
|
Format.m4a: MediaType.audio,
|
|
185
207
|
Format.mp4: MediaType.video,
|
|
@@ -200,6 +222,7 @@ class Format(Enum):
|
|
|
200
222
|
FileExt.diff.value: Format.diff,
|
|
201
223
|
FileExt.json.value: Format.json,
|
|
202
224
|
FileExt.csv.value: Format.csv,
|
|
225
|
+
FileExt.xlsx.value: Format.xlsx,
|
|
203
226
|
FileExt.npz.value: Format.npz,
|
|
204
227
|
FileExt.log.value: Format.log,
|
|
205
228
|
FileExt.py.value: Format.python,
|
|
@@ -207,6 +230,7 @@ class Format(Enum):
|
|
|
207
230
|
FileExt.xsh.value: Format.xonsh,
|
|
208
231
|
FileExt.pdf.value: Format.pdf,
|
|
209
232
|
FileExt.docx.value: Format.docx,
|
|
233
|
+
FileExt.pptx.value: Format.pptx,
|
|
210
234
|
FileExt.jpg.value: Format.jpeg,
|
|
211
235
|
FileExt.png.value: Format.png,
|
|
212
236
|
FileExt.gif.value: Format.gif,
|
|
@@ -214,6 +238,7 @@ class Format(Enum):
|
|
|
214
238
|
FileExt.mp3.value: Format.mp3,
|
|
215
239
|
FileExt.m4a.value: Format.m4a,
|
|
216
240
|
FileExt.mp4.value: Format.mp4,
|
|
241
|
+
FileExt.epub.value: Format.epub,
|
|
217
242
|
}
|
|
218
243
|
return ext_to_format.get(file_ext.value, None)
|
|
219
244
|
|
|
@@ -228,10 +253,12 @@ class Format(Enum):
|
|
|
228
253
|
Format.md_html: FileExt.md,
|
|
229
254
|
Format.html: FileExt.html,
|
|
230
255
|
Format.plaintext: FileExt.txt,
|
|
256
|
+
Format.epub: FileExt.epub,
|
|
231
257
|
Format.yaml: FileExt.yml,
|
|
232
258
|
Format.diff: FileExt.diff,
|
|
233
259
|
Format.json: FileExt.json,
|
|
234
260
|
Format.csv: FileExt.csv,
|
|
261
|
+
Format.xlsx: FileExt.xlsx,
|
|
235
262
|
Format.npz: FileExt.npz,
|
|
236
263
|
Format.log: FileExt.log,
|
|
237
264
|
Format.python: FileExt.py,
|
|
@@ -239,6 +266,7 @@ class Format(Enum):
|
|
|
239
266
|
Format.xonsh: FileExt.xsh,
|
|
240
267
|
Format.pdf: FileExt.pdf,
|
|
241
268
|
Format.docx: FileExt.docx,
|
|
269
|
+
Format.pptx: FileExt.pptx,
|
|
242
270
|
Format.jpeg: FileExt.jpg,
|
|
243
271
|
Format.png: FileExt.png,
|
|
244
272
|
Format.gif: FileExt.gif,
|
|
@@ -260,6 +288,7 @@ class Format(Enum):
|
|
|
260
288
|
"text/html": Format.html,
|
|
261
289
|
"text/diff": Format.diff,
|
|
262
290
|
"text/x-diff": Format.diff,
|
|
291
|
+
"application/epub+zip": Format.epub,
|
|
263
292
|
"application/yaml": Format.yaml,
|
|
264
293
|
"application/x-yaml": Format.yaml,
|
|
265
294
|
"text/x-python": Format.python,
|
|
@@ -269,9 +298,11 @@ class Format(Enum):
|
|
|
269
298
|
"text/x-xonsh": Format.xonsh,
|
|
270
299
|
"application/json": Format.json,
|
|
271
300
|
"text/csv": Format.csv,
|
|
301
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": Format.xlsx,
|
|
272
302
|
"application/x-npz": Format.npz,
|
|
273
303
|
"application/pdf": Format.pdf,
|
|
274
304
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": Format.docx,
|
|
305
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": Format.pptx,
|
|
275
306
|
"image/jpeg": Format.jpeg,
|
|
276
307
|
"image/png": Format.png,
|
|
277
308
|
"image/gif": Format.gif,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from datetime import UTC, datetime
|
|
2
4
|
from enum import Enum
|
|
3
5
|
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
4
7
|
|
|
5
8
|
import humanfriendly
|
|
6
|
-
import pandas as pd
|
|
7
9
|
from funlog import log_calls
|
|
8
10
|
from prettyfmt import fmt_path
|
|
9
11
|
from pydantic.dataclasses import dataclass
|
|
@@ -12,6 +14,9 @@ from kash.config.logger import get_logger
|
|
|
12
14
|
from kash.utils.errors import FileNotFound, InvalidInput
|
|
13
15
|
from kash.utils.file_utils.file_walk import IgnoreFilter, walk_by_dir
|
|
14
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pandas import DataFrame
|
|
19
|
+
|
|
15
20
|
log = get_logger(__name__)
|
|
16
21
|
|
|
17
22
|
|
|
@@ -122,8 +127,10 @@ class FileListing:
|
|
|
122
127
|
size_matching: int
|
|
123
128
|
since_timestamp: float
|
|
124
129
|
|
|
125
|
-
def as_dataframe(self) ->
|
|
126
|
-
|
|
130
|
+
def as_dataframe(self) -> DataFrame:
|
|
131
|
+
from pandas import DataFrame
|
|
132
|
+
|
|
133
|
+
df = DataFrame([file.__dict__ for file in self.files])
|
|
127
134
|
return df
|
|
128
135
|
|
|
129
136
|
@property
|
kash/web_gen/__init__.py
CHANGED
|
@@ -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
|
kash/web_gen/tabbed_webpage.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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=
|
|
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(
|
|
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
|
|
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
|
-
|
|
114
|
+
template_filename="tabbed_webpage.html.jinja",
|
|
115
|
+
data=asdict(tabbed_webpage),
|
|
105
116
|
)
|
|
106
117
|
|
|
107
118
|
return render_web_template(
|
|
108
|
-
|
|
109
|
-
|
|
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")
|
kash/web_gen/template_render.py
CHANGED
|
@@ -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
|
-
|
|
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,10 +77,15 @@ 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
|
}
|
|
86
|
+
pre {
|
|
87
|
+
margin-bottom: 1rem;
|
|
88
|
+
}
|
|
77
89
|
|
|
78
90
|
b, strong {
|
|
79
91
|
font-weight: var(--font-weight-sans-bold);
|
|
@@ -105,14 +117,14 @@ h1 {
|
|
|
105
117
|
|
|
106
118
|
h2 {
|
|
107
119
|
font-size: 1.4rem;
|
|
108
|
-
margin-top:
|
|
120
|
+
margin-top: 1.2rem;
|
|
109
121
|
margin-bottom: 1rem;
|
|
110
122
|
}
|
|
111
123
|
|
|
112
124
|
h3 {
|
|
113
125
|
font-size: 1.09rem;
|
|
114
|
-
margin-top: 1.
|
|
115
|
-
margin-bottom: 0.
|
|
126
|
+
margin-top: 1.5rem;
|
|
127
|
+
margin-bottom: 0.5rem;
|
|
116
128
|
}
|
|
117
129
|
|
|
118
130
|
h4 {
|
|
@@ -120,37 +132,6 @@ h4 {
|
|
|
120
132
|
margin-bottom: 0.7rem;
|
|
121
133
|
}
|
|
122
134
|
|
|
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
135
|
ul {
|
|
155
136
|
list-style-type: none;
|
|
156
137
|
margin-left: 2rem;
|
|
@@ -207,7 +188,42 @@ pre {
|
|
|
207
188
|
letter-spacing: -0.025em;
|
|
208
189
|
{# overflow-x: auto; #}
|
|
209
190
|
}
|
|
191
|
+
{% endblock typography %}
|
|
192
|
+
|
|
193
|
+
{% block long_text_styles %}
|
|
194
|
+
/* Long text stylings, for nicely formatting blog post length or longer texts. */
|
|
195
|
+
|
|
196
|
+
.long-text h1 {
|
|
197
|
+
font-family: var(--font-serif);
|
|
198
|
+
font-weight: 400;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.long-text h2 {
|
|
202
|
+
font-family: var(--font-serif);
|
|
203
|
+
font-weight: 400;
|
|
204
|
+
font-style: italic;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.long-text h3 {
|
|
208
|
+
font-family: var(--font-sans);
|
|
209
|
+
font-weight: var(--font-weight-sans-bold);
|
|
210
|
+
text-transform: uppercase;
|
|
211
|
+
letter-spacing: 0.02em;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.long-text h4 {
|
|
215
|
+
font-family: var(--font-serif);
|
|
216
|
+
font-weight: 700;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.subtitle {
|
|
220
|
+
font-family: var(--font-serif);
|
|
221
|
+
font-style: italic;
|
|
222
|
+
font-size: 1rem;
|
|
223
|
+
}
|
|
224
|
+
{% endblock long_text_styles %}
|
|
210
225
|
|
|
226
|
+
{% block table_styles %}
|
|
211
227
|
table {
|
|
212
228
|
font-family: var(--font-sans);
|
|
213
229
|
font-size: var(--font-size-small);
|
|
@@ -239,16 +255,6 @@ tbody tr:nth-child(even) {
|
|
|
239
255
|
background-color: var(--color-bg-alt-solid);
|
|
240
256
|
}
|
|
241
257
|
|
|
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
258
|
/* Container for wide tables to allow tables to break out of parent width. */
|
|
253
259
|
.table-container {
|
|
254
260
|
{# max-width: calc(100vw - 6rem); #}
|
|
@@ -258,26 +264,24 @@ nav {
|
|
|
258
264
|
box-sizing: border-box;
|
|
259
265
|
margin-bottom: 1rem;
|
|
260
266
|
background-color: var(--color-bg-solid);
|
|
261
|
-
|
|
262
267
|
}
|
|
268
|
+
{% endblock table_styles %}
|
|
263
269
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
{% block nav_styles %}
|
|
271
|
+
nav {
|
|
272
|
+
display: flex;
|
|
273
|
+
flex-wrap: wrap;
|
|
274
|
+
/* Allow wrapping */
|
|
275
|
+
justify-content: center;
|
|
276
|
+
/* Center the content */
|
|
277
|
+
gap: 1rem;
|
|
278
|
+
/* Add some space between the buttons */
|
|
273
279
|
}
|
|
280
|
+
{% endblock nav_styles %}
|
|
274
281
|
|
|
275
|
-
@media (max-width: 768px) {
|
|
276
|
-
table {
|
|
277
|
-
font-size: var(--font-size-smaller);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
282
|
|
|
283
|
+
|
|
284
|
+
{% block footnote_styles %}
|
|
281
285
|
/* Footnotes. */
|
|
282
286
|
sup {
|
|
283
287
|
font-size: 80%;
|
|
@@ -288,6 +292,8 @@ sup {
|
|
|
288
292
|
padding: 0 0.15rem;
|
|
289
293
|
border-radius: 4px;
|
|
290
294
|
transition: all 0.15s ease-in-out;
|
|
295
|
+
font-style: normal;
|
|
296
|
+
font-weight: normal;
|
|
291
297
|
}
|
|
292
298
|
|
|
293
299
|
.footnote-ref a:hover, .footnote:hover {
|
|
@@ -295,4 +301,23 @@ sup {
|
|
|
295
301
|
color: var(--color-primary-light);
|
|
296
302
|
text-decoration: none;
|
|
297
303
|
}
|
|
304
|
+
{% endblock footnote_styles %}
|
|
298
305
|
|
|
306
|
+
{% block responsive_styles %}
|
|
307
|
+
/* Bleed wide on larger screens. */
|
|
308
|
+
/* TODO: Don't make so wide if table itself isn't large? */
|
|
309
|
+
@media (min-width: 768px) {
|
|
310
|
+
table {
|
|
311
|
+
width: calc(100vw - 6rem);
|
|
312
|
+
}
|
|
313
|
+
.table-container {
|
|
314
|
+
width: calc(100vw - 6rem);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@media (max-width: 768px) {
|
|
319
|
+
table {
|
|
320
|
+
font-size: var(--font-size-smaller);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
{% endblock responsive_styles %}
|