euporie 2.8.13__py3-none-any.whl → 2.8.15__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.
- euporie/console/tabs/console.py +4 -0
- euporie/core/__init__.py +1 -1
- euporie/core/cache.py +36 -0
- euporie/core/clipboard.py +16 -1
- euporie/core/config.py +1 -1
- euporie/core/convert/datum.py +25 -9
- euporie/core/convert/formats/ansi.py +25 -21
- euporie/core/convert/formats/base64.py +3 -1
- euporie/core/convert/formats/common.py +4 -4
- euporie/core/convert/formats/ft.py +6 -3
- euporie/core/convert/formats/html.py +22 -24
- euporie/core/convert/formats/markdown.py +4 -2
- euporie/core/convert/formats/pil.py +3 -1
- euporie/core/convert/formats/png.py +6 -4
- euporie/core/convert/formats/rich.py +3 -1
- euporie/core/convert/formats/sixel.py +3 -3
- euporie/core/convert/formats/svg.py +3 -1
- euporie/core/ft/html.py +192 -125
- euporie/core/ft/table.py +1 -1
- euporie/core/kernel/__init__.py +7 -3
- euporie/core/kernel/base.py +13 -0
- euporie/core/kernel/local.py +8 -3
- euporie/core/layout/containers.py +5 -0
- euporie/core/tabs/kernel.py +6 -1
- euporie/core/widgets/cell_outputs.py +39 -8
- euporie/core/widgets/display.py +11 -4
- euporie/core/widgets/forms.py +11 -5
- euporie/core/widgets/menu.py +9 -8
- euporie/preview/tabs/notebook.py +15 -4
- {euporie-2.8.13.dist-info → euporie-2.8.15.dist-info}/METADATA +2 -1
- {euporie-2.8.13.dist-info → euporie-2.8.15.dist-info}/RECORD +36 -35
- {euporie-2.8.13.data → euporie-2.8.15.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.13.data → euporie-2.8.15.data}/data/share/applications/euporie-notebook.desktop +0 -0
- {euporie-2.8.13.dist-info → euporie-2.8.15.dist-info}/WHEEL +0 -0
- {euporie-2.8.13.dist-info → euporie-2.8.15.dist-info}/entry_points.txt +0 -0
- {euporie-2.8.13.dist-info → euporie-2.8.15.dist-info}/licenses/LICENSE +0 -0
@@ -8,6 +8,8 @@ from euporie.core.convert.registry import register
|
|
8
8
|
from euporie.core.filters import have_modules
|
9
9
|
|
10
10
|
if TYPE_CHECKING:
|
11
|
+
from typing import Any
|
12
|
+
|
11
13
|
from euporie.core.convert.datum import Datum
|
12
14
|
|
13
15
|
|
@@ -18,7 +20,7 @@ async def latex_to_svg_py_ziamath(
|
|
18
20
|
rows: int | None = None,
|
19
21
|
fg: str | None = None,
|
20
22
|
bg: str | None = None,
|
21
|
-
|
23
|
+
**kwargs: Any,
|
22
24
|
) -> str:
|
23
25
|
"""Convert LaTeX to SVG using :py:mod:`ziamath`."""
|
24
26
|
import ziamath as zm
|
euporie/core/ft/html.py
CHANGED
@@ -21,6 +21,7 @@ from prompt_toolkit.data_structures import Size
|
|
21
21
|
from prompt_toolkit.filters.base import Condition
|
22
22
|
from prompt_toolkit.filters.utils import _always as always
|
23
23
|
from prompt_toolkit.filters.utils import _never as never
|
24
|
+
from prompt_toolkit.filters.utils import to_filter
|
24
25
|
from prompt_toolkit.formatted_text.utils import split_lines
|
25
26
|
from prompt_toolkit.layout.containers import WindowAlign
|
26
27
|
from prompt_toolkit.layout.dimension import Dimension
|
@@ -108,7 +109,7 @@ if TYPE_CHECKING:
|
|
108
109
|
from typing import Any, Callable
|
109
110
|
|
110
111
|
from fsspec.spec import AbstractFileSystem
|
111
|
-
from prompt_toolkit.filters.base import Filter
|
112
|
+
from prompt_toolkit.filters.base import Filter, FilterOrBool
|
112
113
|
from prompt_toolkit.formatted_text.base import StyleAndTextTuples
|
113
114
|
from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
|
114
115
|
from prompt_toolkit.mouse_events import MouseEvent
|
@@ -191,6 +192,23 @@ _GRID_TEMPLATE_REPEAT_RE = re.compile(
|
|
191
192
|
r"repeat\(\s*(?P<count>\d+)\s*,\s*(?P<value>[^)]+?)\s*\)"
|
192
193
|
)
|
193
194
|
|
195
|
+
_MATHJAX_TAG_RE = re.compile(
|
196
|
+
r"""
|
197
|
+
^(?P<beginning>.*?)
|
198
|
+
(?P<middle>
|
199
|
+
\\\[(?P<display_bracket>.*?)\\\]
|
200
|
+
|
|
201
|
+
\\\((?P<inline_bracket>.*?)\\\)
|
202
|
+
|
|
203
|
+
\$\$(?P<display_dollar>.*?)\$\$
|
204
|
+
|
|
205
|
+
\$(?P<inline_dollar>.*?)(?: \$(?=\$\$) | (?<!\$)\$(?!\$) )
|
206
|
+
)
|
207
|
+
(?P<end>.*)$
|
208
|
+
""",
|
209
|
+
re.MULTILINE | re.VERBOSE,
|
210
|
+
)
|
211
|
+
|
194
212
|
# List of elements which might not have a close tag
|
195
213
|
_VOID_ELEMENTS = (
|
196
214
|
"area",
|
@@ -1038,7 +1056,17 @@ class Theme(Mapping):
|
|
1038
1056
|
"""Calculate the theme defined in CSS."""
|
1039
1057
|
specificity_rules = []
|
1040
1058
|
element = self.element
|
1059
|
+
# Pre-compute element attributes once
|
1060
|
+
element_name = element.name
|
1061
|
+
element_is_first = element.is_first_child_element
|
1062
|
+
element_is_last = element.is_last_child_element
|
1063
|
+
element_sibling_idx = element.sibling_element_index
|
1064
|
+
element_attrs = element.attrs
|
1065
|
+
element_parent = element.parent
|
1066
|
+
element_parents_rev = [x for x in element.parents[::-1] if x]
|
1067
|
+
|
1041
1068
|
for condition, css_block in css.items():
|
1069
|
+
# TODO - cache CSS within condition blocks
|
1042
1070
|
if condition():
|
1043
1071
|
for selectors, rule in css_block.items():
|
1044
1072
|
for selector_parts in selectors:
|
@@ -1048,42 +1076,42 @@ class Theme(Mapping):
|
|
1048
1076
|
selector.item or "",
|
1049
1077
|
selector.attr or "",
|
1050
1078
|
selector.pseudo or "",
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
**
|
1079
|
+
element_name,
|
1080
|
+
element_is_first,
|
1081
|
+
element_is_last,
|
1082
|
+
element_sibling_idx,
|
1083
|
+
**element_attrs,
|
1056
1084
|
):
|
1057
1085
|
continue
|
1058
1086
|
|
1059
1087
|
# All of the parent selectors should match a separate parent in order
|
1060
1088
|
# TODO - combinators
|
1061
1089
|
# https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors#combinators
|
1062
|
-
unmatched_parents: list[Node]
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
_unmatched_parents: list[Node]
|
1067
|
-
parent = element.parent
|
1068
|
-
if parent and (
|
1069
|
-
(selector.comb == ">" and parent)
|
1090
|
+
unmatched_parents: list[Node]
|
1091
|
+
if element_parent and (
|
1092
|
+
(selector.comb == ">" and element_parent)
|
1070
1093
|
# Pseudo-element selectors only match direct ancestors
|
1071
1094
|
or ((item := selector.item) and item.startswith("::"))
|
1072
1095
|
):
|
1073
|
-
|
1096
|
+
unmatched_parents = [element_parent]
|
1074
1097
|
else:
|
1075
|
-
|
1098
|
+
unmatched_parents = element_parents_rev[:]
|
1076
1099
|
|
1077
1100
|
# TODO investigate caching element / selector chains so we don't have to
|
1078
1101
|
# iterate through every parent every time
|
1079
1102
|
|
1080
1103
|
# Iterate through selector items in reverse, skipping the last
|
1081
1104
|
for selector in selector_parts[-2::-1]:
|
1082
|
-
|
1105
|
+
# Pre-compute selector attributes
|
1106
|
+
item = selector.item or ""
|
1107
|
+
attrs = selector.attr or ""
|
1108
|
+
pseudo = selector.pseudo or ""
|
1109
|
+
parent: Node | None
|
1110
|
+
for i, parent in enumerate(unmatched_parents):
|
1083
1111
|
if parent and match_css_selector(
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1112
|
+
item,
|
1113
|
+
attrs,
|
1114
|
+
pseudo,
|
1087
1115
|
parent.name,
|
1088
1116
|
parent.is_first_child_element,
|
1089
1117
|
parent.is_last_child_element,
|
@@ -1093,9 +1121,9 @@ class Theme(Mapping):
|
|
1093
1121
|
if selector.comb == ">" and (
|
1094
1122
|
parent := parent.parent
|
1095
1123
|
):
|
1096
|
-
|
1124
|
+
unmatched_parents = [parent]
|
1097
1125
|
else:
|
1098
|
-
|
1126
|
+
unmatched_parents = element_parents_rev[i + 1 :]
|
1099
1127
|
break
|
1100
1128
|
else:
|
1101
1129
|
break
|
@@ -1109,6 +1137,10 @@ class Theme(Mapping):
|
|
1109
1137
|
# the rest of the selectors for this rule
|
1110
1138
|
break
|
1111
1139
|
|
1140
|
+
# Shortcut in case of no rules
|
1141
|
+
if not specificity_rules:
|
1142
|
+
return {}
|
1143
|
+
|
1112
1144
|
# Move !important rules to the end
|
1113
1145
|
rules = [
|
1114
1146
|
(k, v)
|
@@ -1300,10 +1332,7 @@ class Theme(Mapping):
|
|
1300
1332
|
return max([len(x) for x in self.element.text.split()] or [0])
|
1301
1333
|
else:
|
1302
1334
|
return max(
|
1303
|
-
[
|
1304
|
-
child.theme.min_content_width
|
1305
|
-
for child in self.element.renderable_contents
|
1306
|
-
]
|
1335
|
+
[child.theme.min_content_width for child in self.element.contents]
|
1307
1336
|
or [0]
|
1308
1337
|
)
|
1309
1338
|
|
@@ -1314,10 +1343,7 @@ class Theme(Mapping):
|
|
1314
1343
|
return max([len(x) for x in self.element.text.split("\n")] or [0])
|
1315
1344
|
else:
|
1316
1345
|
return sum(
|
1317
|
-
[
|
1318
|
-
child.theme.max_content_width
|
1319
|
-
for child in self.element.renderable_contents
|
1320
|
-
]
|
1346
|
+
[child.theme.max_content_width for child in self.element.contents]
|
1321
1347
|
or [0]
|
1322
1348
|
)
|
1323
1349
|
|
@@ -1375,7 +1401,7 @@ class Theme(Mapping):
|
|
1375
1401
|
|
1376
1402
|
@property
|
1377
1403
|
def height(self) -> int | None:
|
1378
|
-
"""The
|
1404
|
+
"""The prescribed height."""
|
1379
1405
|
# TODO - process min-/max-height
|
1380
1406
|
if value := self.get("height"):
|
1381
1407
|
theme_height = css_dimension(
|
@@ -1914,12 +1940,12 @@ class Theme(Mapping):
|
|
1914
1940
|
def skip(self) -> bool:
|
1915
1941
|
"""Determine if the element should not be displayed."""
|
1916
1942
|
return (
|
1917
|
-
|
1918
|
-
|
1919
|
-
|
1920
|
-
and not self.preformatted
|
1921
|
-
and not element.text
|
1943
|
+
(
|
1944
|
+
(element := self.element).name in ("::before", "::after")
|
1945
|
+
and self.theme.get("content") in (None, "normal")
|
1922
1946
|
)
|
1947
|
+
or "none" in self.theme["display"]
|
1948
|
+
or (element.name == "::text" and not self.preformatted and not element.text)
|
1923
1949
|
or (self.theme["position"] == "absolute" and self.hidden)
|
1924
1950
|
)
|
1925
1951
|
|
@@ -2169,13 +2195,13 @@ _BROWSER_CSS: CssSelectors = {
|
|
2169
2195
|
CssSelector(item="q"),
|
2170
2196
|
CssSelector(item="::before"),
|
2171
2197
|
),
|
2172
|
-
): {"content": "“"},
|
2198
|
+
): {"content": "'“'"},
|
2173
2199
|
(
|
2174
2200
|
(
|
2175
2201
|
CssSelector(item="q"),
|
2176
2202
|
CssSelector(item="::after"),
|
2177
2203
|
),
|
2178
|
-
): {"content": "”"},
|
2204
|
+
): {"content": "'”'"},
|
2179
2205
|
# Images
|
2180
2206
|
(
|
2181
2207
|
(CssSelector(item="img", attr="[_missing]"),),
|
@@ -2778,6 +2804,7 @@ _BROWSER_CSS: CssSelectors = {
|
|
2778
2804
|
"_pt_class": "markdown,code",
|
2779
2805
|
},
|
2780
2806
|
(
|
2807
|
+
(CssSelector(item="::latex"),),
|
2781
2808
|
(
|
2782
2809
|
CssSelector(item="::root", attr="[_initial_format=markdown]"),
|
2783
2810
|
CssSelector(item=".math"),
|
@@ -2824,7 +2851,7 @@ _BROWSER_CSS: CssSelectors = {
|
|
2824
2851
|
CssSelector(item="*"),
|
2825
2852
|
),
|
2826
2853
|
): {
|
2827
|
-
"
|
2854
|
+
"_pt_class": "markdown,code,block",
|
2828
2855
|
},
|
2829
2856
|
(
|
2830
2857
|
(
|
@@ -2841,7 +2868,12 @@ _BROWSER_CSS: CssSelectors = {
|
|
2841
2868
|
"overflow_y": "hidden",
|
2842
2869
|
"vertical_align": "middle",
|
2843
2870
|
},
|
2844
|
-
}
|
2871
|
+
},
|
2872
|
+
get_app().config.filters.wrap_cell_outputs: {
|
2873
|
+
((CssSelector(item="table.dataframe"),),): {
|
2874
|
+
"max_width": "100%",
|
2875
|
+
},
|
2876
|
+
},
|
2845
2877
|
}
|
2846
2878
|
|
2847
2879
|
|
@@ -2868,9 +2900,6 @@ class Node:
|
|
2868
2900
|
self.contents: list[Node] = contents or []
|
2869
2901
|
self.closed = False
|
2870
2902
|
self.marker: Node | None = None
|
2871
|
-
self.before: Node | None = None
|
2872
|
-
self.after: Node | None = None
|
2873
|
-
|
2874
2903
|
self.theme = Theme(self, parent_theme=parent.theme if parent else None)
|
2875
2904
|
|
2876
2905
|
def reset(self) -> None:
|
@@ -2911,22 +2940,49 @@ class Node:
|
|
2911
2940
|
@cached_property
|
2912
2941
|
def preceding_text(self) -> str:
|
2913
2942
|
"""Return the text preceding this element."""
|
2914
|
-
s = ""
|
2915
2943
|
parent = self.parent
|
2916
|
-
|
2944
|
+
# Move up to the current block element
|
2945
|
+
while parent and not (parent.theme.d_blocky or parent.theme.d_inline_block):
|
2917
2946
|
parent = parent.parent
|
2918
|
-
if parent:
|
2919
|
-
|
2920
|
-
|
2921
|
-
|
2922
|
-
|
2923
|
-
|
2947
|
+
if not parent:
|
2948
|
+
return ""
|
2949
|
+
|
2950
|
+
def _text_descendents(node: Node) -> Generator[Node]:
|
2951
|
+
for child in node.contents:
|
2952
|
+
if child.name == "::text":
|
2953
|
+
yield child
|
2954
|
+
elif (
|
2955
|
+
child.contents
|
2956
|
+
and (child.theme.d_inline or child.theme.d_inline_block)
|
2957
|
+
and child.theme.in_flow
|
2958
|
+
):
|
2959
|
+
yield from _text_descendents(child)
|
2960
|
+
|
2961
|
+
last_valid_text = ""
|
2962
|
+
for node in _text_descendents(parent):
|
2963
|
+
if node is self:
|
2964
|
+
return last_valid_text
|
2965
|
+
elif text := node.text:
|
2966
|
+
last_valid_text = text
|
2967
|
+
return ""
|
2924
2968
|
|
2925
2969
|
@cached_property
|
2926
2970
|
def text(self) -> str:
|
2927
2971
|
"""Get the element's computed text."""
|
2928
|
-
|
2929
|
-
|
2972
|
+
text = self._text
|
2973
|
+
|
2974
|
+
# Allow replacing context of pseudo-elements
|
2975
|
+
if (
|
2976
|
+
(parent := self.parent)
|
2977
|
+
and parent.name.startswith("::")
|
2978
|
+
and (_text := parent.theme.theme.get("content", "").strip())
|
2979
|
+
):
|
2980
|
+
if _text.startswith('"') and _text.endswith('"'):
|
2981
|
+
text = _text.strip('"')
|
2982
|
+
elif _text.startswith("'") and _text.endswith("'"):
|
2983
|
+
text = _text.strip("'")
|
2984
|
+
|
2985
|
+
if text:
|
2930
2986
|
if not (preformatted := self.theme.preformatted):
|
2931
2987
|
# 1. All spaces and tabs immediately before and after a line break are ignored
|
2932
2988
|
text = re.sub(r"(\s+(?=\n)|(?<=\n)\s+)", "", text, flags=re.MULTILINE)
|
@@ -2997,44 +3053,6 @@ class Node:
|
|
2997
3053
|
if element.name == tag:
|
2998
3054
|
yield element
|
2999
3055
|
|
3000
|
-
@cached_property
|
3001
|
-
def renderable_contents(self) -> list[Node]:
|
3002
|
-
"""List the node's contents including '::before' and '::after' elements."""
|
3003
|
-
# Do not add '::before' and '::after' elements to themselves
|
3004
|
-
if self.name.startswith("::"):
|
3005
|
-
return self.contents
|
3006
|
-
|
3007
|
-
contents = []
|
3008
|
-
|
3009
|
-
# Add ::before node
|
3010
|
-
before_node = Node(dom=self.dom, name="::before", parent=self)
|
3011
|
-
if (text := before_node.theme.theme.get("content", "").strip()) and (
|
3012
|
-
(text.startswith('"') and text.endswith('"'))
|
3013
|
-
or (text.startswith("'") and text.endswith("'"))
|
3014
|
-
):
|
3015
|
-
text = text.strip('"').strip("'")
|
3016
|
-
before_node.contents.append(
|
3017
|
-
Node(dom=self.dom, name="::text", parent=before_node, text=text)
|
3018
|
-
)
|
3019
|
-
contents.append(before_node)
|
3020
|
-
|
3021
|
-
contents.extend(self.contents)
|
3022
|
-
|
3023
|
-
# Add ::after node
|
3024
|
-
after_node = Node(dom=self.dom, name="::after", parent=self)
|
3025
|
-
|
3026
|
-
if (text := after_node.theme.theme.get("content", "")) and (
|
3027
|
-
(text.startswith('"') and text.endswith('"'))
|
3028
|
-
or (text.startswith("'") and text.endswith("'"))
|
3029
|
-
):
|
3030
|
-
text = text.strip('"').strip("'")
|
3031
|
-
after_node.contents.append(
|
3032
|
-
Node(dom=self.dom, name="::text", parent=after_node, text=text)
|
3033
|
-
)
|
3034
|
-
contents.append(after_node)
|
3035
|
-
|
3036
|
-
return contents
|
3037
|
-
|
3038
3056
|
@property
|
3039
3057
|
def descendents(self) -> Generator[Node]:
|
3040
3058
|
"""Yield all descendent elements."""
|
@@ -3044,18 +3062,10 @@ class Node:
|
|
3044
3062
|
|
3045
3063
|
@property
|
3046
3064
|
def renderable_descendents(self) -> Generator[Node]:
|
3047
|
-
"""Yield
|
3048
|
-
for child in self.
|
3049
|
-
if
|
3050
|
-
child.theme.d_inline
|
3051
|
-
and child.renderable_contents
|
3052
|
-
and child.name != "::text"
|
3053
|
-
):
|
3065
|
+
"""Yield descendent elements with actual text content."""
|
3066
|
+
for child in self.contents:
|
3067
|
+
if child.theme.d_inline and child.contents and child.name != "::text":
|
3054
3068
|
yield from child.renderable_descendents
|
3055
|
-
# elif (
|
3056
|
-
# child.name == "::text" and self.name != "::block" and self.theme.d_blocky
|
3057
|
-
# ):
|
3058
|
-
# yield Node(dom=self.dom, name="::block", parent=self, contents=[child])
|
3059
3069
|
else:
|
3060
3070
|
yield child
|
3061
3071
|
|
@@ -3473,6 +3483,7 @@ class HTML:
|
|
3473
3483
|
fill: bool = True,
|
3474
3484
|
css: CssSelectors | None = None,
|
3475
3485
|
browser_css: CssSelectors | None = None,
|
3486
|
+
mathjax: FilterOrBool = True,
|
3476
3487
|
mouse_handler: Callable[[Node, MouseEvent], NotImplementedOrNone] | None = None,
|
3477
3488
|
paste_fixed: bool = True,
|
3478
3489
|
defer_assets: bool = False,
|
@@ -3494,6 +3505,7 @@ class HTML:
|
|
3494
3505
|
fill: Whether remaining space in block elements should be filled
|
3495
3506
|
css: Base CSS to apply when rendering the HTML
|
3496
3507
|
browser_css: The browser CSS to use
|
3508
|
+
mathjax: Whether to search for LaTeX in MathJax tags
|
3497
3509
|
mouse_handler: A mouse handler function to use when links are clicked
|
3498
3510
|
paste_fixed: Whether fixed elements should be pasted over the output
|
3499
3511
|
defer_assets: Whether to render the page before remote assets are loaded
|
@@ -3508,6 +3520,7 @@ class HTML:
|
|
3508
3520
|
|
3509
3521
|
self.browser_css = browser_css or _BROWSER_CSS
|
3510
3522
|
self.css: CssSelectors = css or {}
|
3523
|
+
self.mathjax = to_filter(mathjax)
|
3511
3524
|
self.defer_assets = defer_assets
|
3512
3525
|
|
3513
3526
|
self.render_count = 0
|
@@ -3552,6 +3565,7 @@ class HTML:
|
|
3552
3565
|
|
3553
3566
|
Do not touch element's themes!
|
3554
3567
|
"""
|
3568
|
+
mathjax = self.mathjax()
|
3555
3569
|
|
3556
3570
|
def _process_css(data: bytes) -> None:
|
3557
3571
|
try:
|
@@ -3606,6 +3620,68 @@ class HTML:
|
|
3606
3620
|
self._url_fs_map[url] = fs
|
3607
3621
|
self._url_cbs[url] = partial(_process_img, child)
|
3608
3622
|
|
3623
|
+
# Convert non-HTML MathJax tags in texts to <::latex> special tags
|
3624
|
+
elif mathjax and child.name == "::text":
|
3625
|
+
text = child._text
|
3626
|
+
nodes = []
|
3627
|
+
while text:
|
3628
|
+
# Split out MathJax parts
|
3629
|
+
if (match := re.match(_MATHJAX_TAG_RE, text)) is not None:
|
3630
|
+
groups = match.groupdict()
|
3631
|
+
before = groups["beginning"]
|
3632
|
+
latex = groups["display_bracket"] or groups["display_dollar"]
|
3633
|
+
b_attrs: list[tuple[str, str | None]] | None
|
3634
|
+
if latex:
|
3635
|
+
b_attrs = [("style", "display: block")]
|
3636
|
+
else:
|
3637
|
+
latex = groups["inline_bracket"] or groups["inline_dollar"]
|
3638
|
+
b_attrs = []
|
3639
|
+
text = groups["end"]
|
3640
|
+
else:
|
3641
|
+
break
|
3642
|
+
# Add the text before the MathJax tag
|
3643
|
+
if before:
|
3644
|
+
nodes.append(
|
3645
|
+
Node(
|
3646
|
+
dom=self,
|
3647
|
+
name="::text",
|
3648
|
+
parent=child.parent,
|
3649
|
+
text=before,
|
3650
|
+
)
|
3651
|
+
)
|
3652
|
+
# Add the LaTeX node
|
3653
|
+
if latex:
|
3654
|
+
latex_node = Node(
|
3655
|
+
dom=self, name="::latex", parent=child.parent, attrs=b_attrs
|
3656
|
+
)
|
3657
|
+
latex_node.contents = [
|
3658
|
+
Node(dom=self, name="::text", parent=latex_node, text=latex)
|
3659
|
+
]
|
3660
|
+
nodes.append(latex_node)
|
3661
|
+
if nodes:
|
3662
|
+
parent = child.parent
|
3663
|
+
# Add a text node for any remaining text
|
3664
|
+
if text:
|
3665
|
+
nodes.append(
|
3666
|
+
Node(dom=self, name="::text", parent=parent, text=text)
|
3667
|
+
)
|
3668
|
+
# Replace the original text node with the new nodes
|
3669
|
+
if parent is not None:
|
3670
|
+
index = parent.contents.index(child)
|
3671
|
+
parent.contents[index : index + 1] = nodes
|
3672
|
+
|
3673
|
+
# Add pseudo-elements just before rendering
|
3674
|
+
if child.name not in _VOID_ELEMENTS and not child.name.startswith("::"):
|
3675
|
+
before_node = Node(dom=self, name="::before", parent=child)
|
3676
|
+
before_node.contents.append(
|
3677
|
+
Node(dom=self, name="::text", parent=before_node)
|
3678
|
+
)
|
3679
|
+
after_node = Node(dom=self, name="::after", parent=child)
|
3680
|
+
after_node.contents.append(
|
3681
|
+
Node(dom=self, name="::text", parent=after_node)
|
3682
|
+
)
|
3683
|
+
child.contents = [before_node, *child.contents, after_node]
|
3684
|
+
|
3609
3685
|
self._dom_processed = True
|
3610
3686
|
|
3611
3687
|
async def load_assets(self) -> None:
|
@@ -3647,7 +3723,7 @@ class HTML:
|
|
3647
3723
|
self, width: int | None, height: int | None
|
3648
3724
|
) -> StyleAndTextTuples:
|
3649
3725
|
"""Render the current markup at a given size, asynchronously."""
|
3650
|
-
# log.debug("Rendering at (%
|
3726
|
+
# log.debug("Rendering at (%r, %r)", width, height)
|
3651
3727
|
no_w = width is None and self.width is None
|
3652
3728
|
no_h = height is None and self.height is None
|
3653
3729
|
if no_w or no_h:
|
@@ -3732,7 +3808,7 @@ class HTML:
|
|
3732
3808
|
self.render_count += 1
|
3733
3809
|
self.formatted_text = ft
|
3734
3810
|
|
3735
|
-
# Load assets after initial render and
|
3811
|
+
# Load assets after initial render and request a re-render when loaded
|
3736
3812
|
if self.defer_assets and not self._assets_loaded:
|
3737
3813
|
loop = asyncio.get_event_loop()
|
3738
3814
|
task = loop.create_task(self.load_assets())
|
@@ -3934,7 +4010,7 @@ class HTML:
|
|
3934
4010
|
table_x_dim = Dimension(
|
3935
4011
|
min=table_theme.min_width,
|
3936
4012
|
preferred=table_theme.content_width if "width" in table_theme else None,
|
3937
|
-
max=table_theme.max_width
|
4013
|
+
max=table_theme.max_width,
|
3938
4014
|
)
|
3939
4015
|
|
3940
4016
|
table = Table(
|
@@ -4044,7 +4120,7 @@ class HTML:
|
|
4044
4120
|
fill=True,
|
4045
4121
|
)
|
4046
4122
|
if ft_caption:
|
4047
|
-
ft.extend(ft_caption)
|
4123
|
+
ft.extend([*ft_caption, ("", "\n")])
|
4048
4124
|
|
4049
4125
|
ft.extend(ft_table)
|
4050
4126
|
|
@@ -4414,7 +4490,7 @@ class HTML:
|
|
4414
4490
|
) -> StyleAndTextTuples:
|
4415
4491
|
"""Display images rendered as ANSI art."""
|
4416
4492
|
theme = element.theme
|
4417
|
-
# HTMLParser
|
4493
|
+
# HTMLParser clobbers the case of element attributes
|
4418
4494
|
element.attrs["xmlns"] = "http://www.w3.org/2000/svg"
|
4419
4495
|
element.attrs["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
|
4420
4496
|
# We fix the SVG viewBox here
|
@@ -4517,7 +4593,7 @@ class HTML:
|
|
4517
4593
|
|
4518
4594
|
# Render each child node
|
4519
4595
|
for child, rendering in zip(coros, renderings):
|
4520
|
-
# Start a new line if we
|
4596
|
+
# Start a new line if we just passed a <br> element
|
4521
4597
|
if child.name == "br":
|
4522
4598
|
flush()
|
4523
4599
|
continue
|
@@ -4577,10 +4653,7 @@ class HTML:
|
|
4577
4653
|
# current output. This might involve re-aligning the last line in the
|
4578
4654
|
# output, which could have been an inline-block
|
4579
4655
|
|
4580
|
-
elif d_inline and (
|
4581
|
-
# parent_theme.d_inline or parent_theme.d_inline_block or preformatted
|
4582
|
-
parent_theme.d_inline or preformatted
|
4583
|
-
):
|
4656
|
+
elif d_inline and (parent_theme.d_inline or preformatted):
|
4584
4657
|
new_line.extend(rendering)
|
4585
4658
|
|
4586
4659
|
elif d_inline or d_inline_block:
|
@@ -4798,14 +4871,6 @@ class HTML:
|
|
4798
4871
|
ignore_whitespace=True,
|
4799
4872
|
style=theme.style,
|
4800
4873
|
)
|
4801
|
-
else:
|
4802
|
-
ft = truncate(
|
4803
|
-
ft,
|
4804
|
-
content_width,
|
4805
|
-
placeholder="",
|
4806
|
-
ignore_whitespace=True,
|
4807
|
-
style=theme.style,
|
4808
|
-
)
|
4809
4874
|
|
4810
4875
|
# Truncate or expand the height
|
4811
4876
|
overflow_y = theme.get("overflow_y") in {"hidden", "auto"}
|
@@ -4986,6 +5051,7 @@ if __name__ == "__main__":
|
|
4986
5051
|
import sys
|
4987
5052
|
|
4988
5053
|
from prompt_toolkit.application.current import create_app_session, set_app
|
5054
|
+
from prompt_toolkit.formatted_text.utils import to_formatted_text
|
4989
5055
|
from prompt_toolkit.shortcuts.utils import print_formatted_text
|
4990
5056
|
from prompt_toolkit.styles.style import Style
|
4991
5057
|
|
@@ -5000,11 +5066,12 @@ if __name__ == "__main__":
|
|
5000
5066
|
set_app(DummyApp()),
|
5001
5067
|
):
|
5002
5068
|
print_formatted_text(
|
5003
|
-
|
5004
|
-
|
5005
|
-
|
5006
|
-
|
5007
|
-
|
5069
|
+
to_formatted_text(
|
5070
|
+
asyncio.run(
|
5071
|
+
HTML(
|
5072
|
+
path.read_text(), base=path, collapse_root_margin=False
|
5073
|
+
)._render(None, None)
|
5074
|
+
)
|
5008
5075
|
),
|
5009
5076
|
style=Style(HTML_STYLE),
|
5010
5077
|
)
|
euporie/core/ft/table.py
CHANGED
euporie/core/kernel/__init__.py
CHANGED
@@ -12,11 +12,15 @@ if TYPE_CHECKING:
|
|
12
12
|
from euporie.core.kernel.base import BaseKernel, KernelInfo, MsgCallbacks
|
13
13
|
from euporie.core.tabs.kernel import KernelTab
|
14
14
|
|
15
|
-
KERNEL_REGISTRY = {
|
16
|
-
"local": "euporie.core.kernel.local:LocalPythonKernel",
|
17
|
-
}
|
15
|
+
KERNEL_REGISTRY = {}
|
18
16
|
if find_spec("jupyter_client"):
|
19
17
|
KERNEL_REGISTRY["jupyter"] = "euporie.core.kernel.jupyter:JupyterKernel"
|
18
|
+
KERNEL_REGISTRY.update(
|
19
|
+
{
|
20
|
+
"local": "euporie.core.kernel.local:LocalPythonKernel",
|
21
|
+
"none": "euporie.core.kernel.base:NoKernel",
|
22
|
+
}
|
23
|
+
)
|
20
24
|
|
21
25
|
|
22
26
|
def list_kernels() -> list[KernelInfo]:
|
euporie/core/kernel/base.py
CHANGED
@@ -499,6 +499,19 @@ class BaseKernel(ABC):
|
|
499
499
|
class NoKernel(BaseKernel):
|
500
500
|
"""A `None` kernel."""
|
501
501
|
|
502
|
+
@classmethod
|
503
|
+
def variants(cls) -> list[KernelInfo]:
|
504
|
+
"""Return available kernel specifications."""
|
505
|
+
return [
|
506
|
+
KernelInfo(
|
507
|
+
name="none",
|
508
|
+
display_name="No Kernel",
|
509
|
+
factory=cls,
|
510
|
+
kind="new",
|
511
|
+
type=cls,
|
512
|
+
)
|
513
|
+
]
|
514
|
+
|
502
515
|
@property
|
503
516
|
def spec(self) -> dict[str, str]:
|
504
517
|
"""The kernelspec metadata for the current kernel instance."""
|
euporie/core/kernel/local.py
CHANGED
@@ -24,7 +24,7 @@ from euporie.core.app.current import get_app
|
|
24
24
|
from euporie.core.kernel.base import BaseKernel, KernelInfo, MsgCallbacks
|
25
25
|
|
26
26
|
if TYPE_CHECKING:
|
27
|
-
from typing import Any, Callable, Unpack
|
27
|
+
from typing import Any, Callable, TextIO, Unpack
|
28
28
|
|
29
29
|
from euporie.core.tabs.kernel import KernelTab
|
30
30
|
|
@@ -175,7 +175,7 @@ class LocalPythonKernel(BaseKernel):
|
|
175
175
|
virtual_env_path = Path(
|
176
176
|
os.environ["VIRTUAL_ENV"], "lib", "python{}.{}", "site-packages"
|
177
177
|
)
|
178
|
-
p_ver = sys.version_info[:2]
|
178
|
+
p_ver = tuple(str(x) for x in sys.version_info[:2])
|
179
179
|
|
180
180
|
# Predict version from py[thon]-x.x in the $VIRTUAL_ENV
|
181
181
|
re_m = re.search(r"\bpy(?:thon)?([23])\.(\d+)\b", os.environ["VIRTUAL_ENV"])
|
@@ -631,7 +631,12 @@ class InputBuiltin(BaseHook):
|
|
631
631
|
self._is_password = is_password
|
632
632
|
update_wrapper(self, input)
|
633
633
|
|
634
|
-
def __call__(
|
634
|
+
def __call__(
|
635
|
+
self,
|
636
|
+
prompt: str = "",
|
637
|
+
stream: TextIO | None = None,
|
638
|
+
echo_char: str | None = None,
|
639
|
+
) -> str:
|
635
640
|
"""Get input from user via callback."""
|
636
641
|
if (callbacks := self.callbacks) and (get_input := callbacks.get("get_input")):
|
637
642
|
# Clear any previous input
|