kash-shell 0.3.28__py3-none-any.whl → 0.3.30__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 (61) hide show
  1. kash/actions/core/markdownify_html.py +1 -4
  2. kash/actions/core/minify_html.py +4 -5
  3. kash/actions/core/render_as_html.py +9 -7
  4. kash/actions/core/save_sidematter_meta.py +47 -0
  5. kash/actions/core/zip_sidematter.py +47 -0
  6. kash/commands/base/basic_file_commands.py +7 -4
  7. kash/commands/base/diff_commands.py +6 -4
  8. kash/commands/base/files_command.py +31 -30
  9. kash/commands/base/general_commands.py +3 -2
  10. kash/commands/base/logs_commands.py +6 -4
  11. kash/commands/base/reformat_command.py +3 -2
  12. kash/commands/base/search_command.py +4 -3
  13. kash/commands/base/show_command.py +9 -7
  14. kash/commands/help/assistant_commands.py +6 -4
  15. kash/commands/help/help_commands.py +7 -4
  16. kash/commands/workspace/selection_commands.py +18 -16
  17. kash/commands/workspace/workspace_commands.py +39 -26
  18. kash/config/setup.py +2 -27
  19. kash/docs/markdown/topics/a1_what_is_kash.md +26 -18
  20. kash/exec/action_decorators.py +2 -2
  21. kash/exec/action_exec.py +56 -50
  22. kash/exec/fetch_url_items.py +36 -9
  23. kash/exec/preconditions.py +2 -2
  24. kash/exec/resolve_args.py +4 -1
  25. kash/exec/runtime_settings.py +1 -0
  26. kash/file_storage/file_store.py +59 -23
  27. kash/file_storage/item_file_format.py +91 -26
  28. kash/help/help_types.py +1 -1
  29. kash/llm_utils/llms.py +6 -1
  30. kash/local_server/local_server_commands.py +2 -1
  31. kash/mcp/mcp_server_commands.py +3 -2
  32. kash/mcp/mcp_server_routes.py +1 -1
  33. kash/model/actions_model.py +31 -30
  34. kash/model/compound_actions_model.py +4 -3
  35. kash/model/exec_model.py +30 -3
  36. kash/model/items_model.py +114 -57
  37. kash/model/params_model.py +4 -4
  38. kash/shell/output/shell_output.py +1 -2
  39. kash/utils/file_formats/chat_format.py +7 -4
  40. kash/utils/file_utils/file_ext.py +1 -0
  41. kash/utils/file_utils/file_formats.py +4 -2
  42. kash/utils/file_utils/file_formats_model.py +12 -0
  43. kash/utils/text_handling/doc_normalization.py +1 -1
  44. kash/utils/text_handling/markdown_footnotes.py +224 -0
  45. kash/utils/text_handling/markdown_utils.py +532 -41
  46. kash/utils/text_handling/markdownify_utils.py +2 -1
  47. kash/web_gen/templates/components/tooltip_scripts.js.jinja +186 -1
  48. kash/web_gen/templates/components/youtube_popover_scripts.js.jinja +223 -0
  49. kash/web_gen/templates/components/youtube_popover_styles.css.jinja +150 -0
  50. kash/web_gen/templates/content_styles.css.jinja +53 -1
  51. kash/web_gen/templates/youtube_webpage.html.jinja +47 -0
  52. kash/web_gen/webpage_render.py +103 -0
  53. kash/workspaces/workspaces.py +0 -5
  54. kash/xonsh_custom/custom_shell.py +4 -3
  55. {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/METADATA +33 -24
  56. {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/RECORD +59 -54
  57. kash/llm_utils/llm_features.py +0 -72
  58. kash/web_gen/simple_webpage.py +0 -55
  59. {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/WHEEL +0 -0
  60. {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/entry_points.txt +0 -0
  61. {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+ from flowmark import flowmark_markdown, line_wrap_by_sentence
8
+ from marko import Markdown
9
+ from marko.ext import footnote
10
+
11
+ from kash.utils.text_handling.markdown_utils import comprehensive_transform_tree
12
+
13
+
14
+ def _normalize_footnotes_in_markdown(content: str) -> str:
15
+ """
16
+ Ensure blank lines between consecutive footnote definitions.
17
+
18
+ Marko has a bug where consecutive footnotes without blank lines are parsed
19
+ as a single footnote. This adds blank lines where needed.
20
+ """
21
+ lines = content.split("\n")
22
+ result = []
23
+ i = 0
24
+
25
+ while i < len(lines):
26
+ line = lines[i]
27
+ result.append(line)
28
+
29
+ # Check if this is a footnote definition
30
+ if re.match(r"^\[\^[^\]]+\]:", line):
31
+ # Look ahead to see if the next non-empty line is also a footnote
32
+ j = i + 1
33
+ while j < len(lines) and not lines[j].strip():
34
+ result.append(lines[j])
35
+ j += 1
36
+
37
+ if j < len(lines) and re.match(r"^\[\^[^\]]+\]:", lines[j]):
38
+ # Next non-empty line is also a footnote, add blank line
39
+ result.append("")
40
+
41
+ i = j
42
+ else:
43
+ i += 1
44
+
45
+ return "\n".join(result)
46
+
47
+
48
+ @dataclass
49
+ class FootnoteInfo:
50
+ """
51
+ Information about a single footnote definition.
52
+ """
53
+
54
+ footnote_id: str # The footnote ID with caret (e.g., "^123", "^foo")
55
+ content: str # The rendered markdown content of the footnote
56
+ raw_element: footnote.FootnoteDef # The original marko element
57
+
58
+
59
+ @dataclass
60
+ class MarkdownFootnotes:
61
+ """
62
+ Container for all footnotes in a markdown document with fast lookup.
63
+
64
+ Provides efficient access to footnote definitions by their IDs.
65
+ IDs are stored with the leading caret (^) to avoid collisions.
66
+ """
67
+
68
+ footnotes: dict[str, FootnoteInfo] = field(default_factory=dict)
69
+ """Dictionary mapping footnote IDs (with ^) to FootnoteInfo objects."""
70
+
71
+ @staticmethod
72
+ def from_markdown(content: str, markdown_parser: Markdown | None = None) -> MarkdownFootnotes:
73
+ """
74
+ Extract all footnotes from markdown content.
75
+
76
+ Args:
77
+ content: The markdown content to parse
78
+ markdown_parser: Optional custom markdown parser. If None, uses default flowmark setup.
79
+
80
+ Returns:
81
+ MarkdownFootnotes instance with all footnotes indexed by ID
82
+ """
83
+ if markdown_parser is None:
84
+ markdown_parser = flowmark_markdown(line_wrap_by_sentence(is_markdown=True))
85
+
86
+ # Normalize to work around marko bug with consecutive footnotes
87
+ normalized_content = _normalize_footnotes_in_markdown(content)
88
+ document = markdown_parser.parse(normalized_content)
89
+ return MarkdownFootnotes.from_document(document, markdown_parser)
90
+
91
+ @staticmethod
92
+ def from_document(document: Any, markdown_parser: Markdown | None = None) -> MarkdownFootnotes:
93
+ """
94
+ Extract all footnotes from a parsed markdown document.
95
+
96
+ Args:
97
+ document: A parsed marko document object
98
+ markdown_parser: The markdown parser used (needed for rendering).
99
+ If None, uses default flowmark setup.
100
+
101
+ Returns:
102
+ MarkdownFootnotes instance with all footnotes indexed by ID
103
+ """
104
+ if markdown_parser is None:
105
+ markdown_parser = flowmark_markdown(line_wrap_by_sentence(is_markdown=True))
106
+
107
+ footnotes_dict: dict[str, FootnoteInfo] = {}
108
+
109
+ def collect_footnote(element: Any) -> None:
110
+ if isinstance(element, footnote.FootnoteDef):
111
+ content_parts = []
112
+ if hasattr(element, "children") and element.children:
113
+ for child in element.children:
114
+ rendered = markdown_parser.renderer.render(child)
115
+ content_parts.append(rendered)
116
+
117
+ rendered_content = "".join(content_parts).strip()
118
+
119
+ footnote_id = f"^{element.label}"
120
+ footnotes_dict[footnote_id] = FootnoteInfo(
121
+ footnote_id=footnote_id,
122
+ content=rendered_content,
123
+ raw_element=element,
124
+ )
125
+
126
+ comprehensive_transform_tree(document, collect_footnote)
127
+
128
+ return MarkdownFootnotes(footnotes=footnotes_dict)
129
+
130
+ def get(self, footnote_id: str, default: FootnoteInfo | None = None) -> FootnoteInfo | None:
131
+ """
132
+ Get a footnote by its ID.
133
+
134
+ Args:
135
+ footnote_id: The footnote ID (with or without leading ^)
136
+ default: Default value if footnote not found
137
+
138
+ Returns:
139
+ FootnoteInfo if found, otherwise default value
140
+ """
141
+ if not footnote_id.startswith("^"):
142
+ footnote_id = f"^{footnote_id}"
143
+ return self.footnotes.get(footnote_id, default)
144
+
145
+ def __getitem__(self, footnote_id: str) -> FootnoteInfo:
146
+ """
147
+ Get a footnote by its ID using dictionary-style access.
148
+
149
+ Args:
150
+ footnote_id: The footnote ID (with or without leading ^)
151
+
152
+ Returns:
153
+ FootnoteInfo for the ID
154
+
155
+ Raises:
156
+ KeyError: If the footnote ID is not found
157
+ """
158
+ if not footnote_id.startswith("^"):
159
+ footnote_id = f"^{footnote_id}"
160
+ return self.footnotes[footnote_id]
161
+
162
+ def __contains__(self, footnote_id: str) -> bool:
163
+ """
164
+ Check if a footnote exists.
165
+
166
+ Args:
167
+ footnote_id: The footnote ID (with or without leading ^)
168
+ """
169
+ if not footnote_id.startswith("^"):
170
+ footnote_id = f"^{footnote_id}"
171
+ return footnote_id in self.footnotes
172
+
173
+ def __len__(self) -> int:
174
+ """Return the number of footnotes."""
175
+ return len(self.footnotes)
176
+
177
+ def __iter__(self):
178
+ """Iterate over footnote IDs (with carets)."""
179
+ return iter(self.footnotes)
180
+
181
+ def items(self):
182
+ """Return (footnote_id, FootnoteInfo) pairs."""
183
+ return self.footnotes.items()
184
+
185
+ def values(self):
186
+ """Return FootnoteInfo objects."""
187
+ return self.footnotes.values()
188
+
189
+ def keys(self):
190
+ """Return footnote IDs (with carets)."""
191
+ return self.footnotes.keys()
192
+
193
+
194
+ def extract_footnote_references(content: str, markdown_parser: Markdown | None = None) -> list[str]:
195
+ """
196
+ Extract all footnote reference IDs used in the content.
197
+
198
+ This finds all FootnoteRef elements (e.g., [^123] in the text) as opposed
199
+ to FootnoteDef elements which are the definitions.
200
+
201
+ Args:
202
+ content: The markdown content to parse
203
+ markdown_parser: Optional custom markdown parser
204
+
205
+ Returns:
206
+ List of unique footnote IDs that are referenced (with the ^)
207
+ """
208
+ if markdown_parser is None:
209
+ markdown_parser = flowmark_markdown(line_wrap_by_sentence(is_markdown=True))
210
+
211
+ normalized_content = _normalize_footnotes_in_markdown(content)
212
+ document = markdown_parser.parse(normalized_content)
213
+ references: list[str] = []
214
+ seen: set[str] = set()
215
+
216
+ def collect_references(element: Any) -> None:
217
+ if isinstance(element, footnote.FootnoteRef):
218
+ footnote_id = f"^{element.label}"
219
+ if footnote_id not in seen:
220
+ seen.add(footnote_id)
221
+ references.append(footnote_id)
222
+
223
+ comprehensive_transform_tree(document, collect_references)
224
+ return references