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.
- kash/actions/core/markdownify_html.py +1 -4
- kash/actions/core/minify_html.py +4 -5
- kash/actions/core/render_as_html.py +9 -7
- kash/actions/core/save_sidematter_meta.py +47 -0
- kash/actions/core/zip_sidematter.py +47 -0
- kash/commands/base/basic_file_commands.py +7 -4
- kash/commands/base/diff_commands.py +6 -4
- kash/commands/base/files_command.py +31 -30
- kash/commands/base/general_commands.py +3 -2
- kash/commands/base/logs_commands.py +6 -4
- kash/commands/base/reformat_command.py +3 -2
- kash/commands/base/search_command.py +4 -3
- kash/commands/base/show_command.py +9 -7
- kash/commands/help/assistant_commands.py +6 -4
- kash/commands/help/help_commands.py +7 -4
- kash/commands/workspace/selection_commands.py +18 -16
- kash/commands/workspace/workspace_commands.py +39 -26
- kash/config/setup.py +2 -27
- kash/docs/markdown/topics/a1_what_is_kash.md +26 -18
- kash/exec/action_decorators.py +2 -2
- kash/exec/action_exec.py +56 -50
- kash/exec/fetch_url_items.py +36 -9
- kash/exec/preconditions.py +2 -2
- kash/exec/resolve_args.py +4 -1
- kash/exec/runtime_settings.py +1 -0
- kash/file_storage/file_store.py +59 -23
- kash/file_storage/item_file_format.py +91 -26
- kash/help/help_types.py +1 -1
- kash/llm_utils/llms.py +6 -1
- kash/local_server/local_server_commands.py +2 -1
- kash/mcp/mcp_server_commands.py +3 -2
- kash/mcp/mcp_server_routes.py +1 -1
- kash/model/actions_model.py +31 -30
- kash/model/compound_actions_model.py +4 -3
- kash/model/exec_model.py +30 -3
- kash/model/items_model.py +114 -57
- kash/model/params_model.py +4 -4
- kash/shell/output/shell_output.py +1 -2
- kash/utils/file_formats/chat_format.py +7 -4
- kash/utils/file_utils/file_ext.py +1 -0
- kash/utils/file_utils/file_formats.py +4 -2
- kash/utils/file_utils/file_formats_model.py +12 -0
- kash/utils/text_handling/doc_normalization.py +1 -1
- kash/utils/text_handling/markdown_footnotes.py +224 -0
- kash/utils/text_handling/markdown_utils.py +532 -41
- kash/utils/text_handling/markdownify_utils.py +2 -1
- kash/web_gen/templates/components/tooltip_scripts.js.jinja +186 -1
- kash/web_gen/templates/components/youtube_popover_scripts.js.jinja +223 -0
- kash/web_gen/templates/components/youtube_popover_styles.css.jinja +150 -0
- kash/web_gen/templates/content_styles.css.jinja +53 -1
- kash/web_gen/templates/youtube_webpage.html.jinja +47 -0
- kash/web_gen/webpage_render.py +103 -0
- kash/workspaces/workspaces.py +0 -5
- kash/xonsh_custom/custom_shell.py +4 -3
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/METADATA +33 -24
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/RECORD +59 -54
- kash/llm_utils/llm_features.py +0 -72
- kash/web_gen/simple_webpage.py +0 -55
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/entry_points.txt +0 -0
- {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
|