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.
Files changed (83) 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 +4 -8
  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/basic_file_commands.py +21 -3
  10. kash/commands/base/files_command.py +29 -10
  11. kash/commands/extras/parse_uv_lock.py +12 -3
  12. kash/commands/workspace/selection_commands.py +1 -1
  13. kash/commands/workspace/workspace_commands.py +2 -3
  14. kash/config/colors.py +2 -2
  15. kash/config/env_settings.py +2 -42
  16. kash/config/logger.py +30 -25
  17. kash/config/logger_basic.py +6 -6
  18. kash/config/settings.py +23 -7
  19. kash/config/setup.py +33 -5
  20. kash/config/text_styles.py +25 -22
  21. kash/embeddings/cosine.py +12 -4
  22. kash/embeddings/embeddings.py +16 -6
  23. kash/embeddings/text_similarity.py +10 -4
  24. kash/exec/__init__.py +3 -0
  25. kash/exec/action_decorators.py +10 -25
  26. kash/exec/action_exec.py +43 -23
  27. kash/exec/llm_transforms.py +6 -3
  28. kash/exec/preconditions.py +10 -12
  29. kash/exec/resolve_args.py +4 -0
  30. kash/exec/runtime_settings.py +134 -0
  31. kash/exec/shell_callable_action.py +5 -3
  32. kash/file_storage/file_store.py +37 -38
  33. kash/file_storage/item_file_format.py +6 -3
  34. kash/file_storage/store_filenames.py +6 -3
  35. kash/help/function_param_info.py +1 -1
  36. kash/llm_utils/init_litellm.py +16 -0
  37. kash/llm_utils/llm_api_keys.py +6 -2
  38. kash/llm_utils/llm_completion.py +11 -4
  39. kash/local_server/local_server_routes.py +1 -7
  40. kash/mcp/mcp_cli.py +3 -2
  41. kash/mcp/mcp_server_routes.py +11 -12
  42. kash/media_base/transcription_deepgram.py +15 -2
  43. kash/model/__init__.py +1 -1
  44. kash/model/actions_model.py +6 -54
  45. kash/model/exec_model.py +79 -0
  46. kash/model/items_model.py +102 -35
  47. kash/model/operations_model.py +38 -15
  48. kash/model/paths_model.py +2 -0
  49. kash/shell/output/shell_output.py +10 -8
  50. kash/shell/shell_main.py +2 -2
  51. kash/shell/utils/exception_printing.py +2 -2
  52. kash/shell/utils/shell_function_wrapper.py +15 -15
  53. kash/text_handling/doc_normalization.py +16 -8
  54. kash/text_handling/markdown_render.py +1 -0
  55. kash/text_handling/markdown_utils.py +105 -2
  56. kash/utils/common/format_utils.py +2 -8
  57. kash/utils/common/function_inspect.py +360 -110
  58. kash/utils/common/inflection.py +22 -0
  59. kash/utils/common/task_stack.py +4 -15
  60. kash/utils/errors.py +14 -9
  61. kash/utils/file_utils/file_ext.py +4 -0
  62. kash/utils/file_utils/file_formats_model.py +32 -1
  63. kash/utils/file_utils/file_sort_filter.py +10 -3
  64. kash/web_gen/__init__.py +0 -4
  65. kash/web_gen/simple_webpage.py +52 -0
  66. kash/web_gen/tabbed_webpage.py +23 -16
  67. kash/web_gen/template_render.py +37 -2
  68. kash/web_gen/templates/base_styles.css.jinja +84 -59
  69. kash/web_gen/templates/base_webpage.html.jinja +85 -67
  70. kash/web_gen/templates/item_view.html.jinja +47 -37
  71. kash/web_gen/templates/simple_webpage.html.jinja +24 -0
  72. kash/web_gen/templates/tabbed_webpage.html.jinja +42 -32
  73. kash/workspaces/__init__.py +12 -3
  74. kash/workspaces/workspace_dirs.py +58 -0
  75. kash/workspaces/workspace_importing.py +1 -1
  76. kash/workspaces/workspaces.py +26 -90
  77. {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/METADATA +7 -7
  78. {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/RECORD +81 -76
  79. kash/shell/utils/argparse_utils.py +0 -20
  80. kash/utils/lang_utils/inflection.py +0 -18
  81. {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/WHEEL +0 -0
  82. {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/entry_points.txt +0 -0
  83. {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) -> pd.DataFrame:
126
- df = pd.DataFrame([file.__dict__ for file in self.files])
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
@@ -1,4 +0,0 @@
1
- from pathlib import Path
2
-
3
- base_templates_dir = Path(__file__).parent / "templates"
4
- """Common base web page templates."""
@@ -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,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: 2.5rem;
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.7rem;
115
- margin-bottom: 0.7rem;
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
- /* 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
- }
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 %}