euporie 2.3.2__py3-none-any.whl → 2.4.1__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/__main__.py +3 -1
- euporie/console/app.py +6 -4
- euporie/console/tabs/console.py +34 -9
- euporie/core/__init__.py +6 -1
- euporie/core/__main__.py +1 -1
- euporie/core/app.py +79 -109
- euporie/core/border.py +44 -14
- euporie/core/comm/base.py +5 -4
- euporie/core/comm/ipywidgets.py +11 -11
- euporie/core/comm/registry.py +12 -6
- euporie/core/commands.py +30 -23
- euporie/core/completion.py +1 -4
- euporie/core/config.py +15 -5
- euporie/core/convert/{base.py → core.py} +117 -53
- euporie/core/convert/formats/ansi.py +46 -25
- euporie/core/convert/formats/base64.py +3 -3
- euporie/core/convert/formats/common.py +38 -13
- euporie/core/convert/formats/formatted_text.py +54 -12
- euporie/core/convert/formats/html.py +5 -5
- euporie/core/convert/formats/jpeg.py +1 -1
- euporie/core/convert/formats/markdown.py +4 -4
- euporie/core/convert/formats/pdf.py +1 -1
- euporie/core/convert/formats/pil.py +5 -3
- euporie/core/convert/formats/png.py +7 -6
- euporie/core/convert/formats/rich.py +4 -3
- euporie/core/convert/formats/sixel.py +5 -5
- euporie/core/convert/utils.py +1 -1
- euporie/core/current.py +11 -5
- euporie/core/formatted_text/ansi.py +4 -8
- euporie/core/formatted_text/html.py +1630 -856
- euporie/core/formatted_text/markdown.py +177 -166
- euporie/core/formatted_text/table.py +20 -14
- euporie/core/formatted_text/utils.py +21 -10
- euporie/core/io.py +14 -14
- euporie/core/kernel.py +48 -37
- euporie/core/key_binding/bindings/micro.py +5 -1
- euporie/core/key_binding/bindings/mouse.py +2 -2
- euporie/core/keys.py +3 -0
- euporie/core/launch.py +5 -2
- euporie/core/lexers.py +13 -2
- euporie/core/log.py +135 -139
- euporie/core/margins.py +32 -14
- euporie/core/path.py +273 -0
- euporie/core/processors.py +35 -0
- euporie/core/renderer.py +21 -5
- euporie/core/style.py +34 -19
- euporie/core/tabs/base.py +101 -17
- euporie/core/tabs/notebook.py +72 -30
- euporie/core/terminal.py +56 -48
- euporie/core/utils.py +12 -16
- euporie/core/widgets/cell.py +6 -5
- euporie/core/widgets/cell_outputs.py +2 -2
- euporie/core/widgets/decor.py +74 -82
- euporie/core/widgets/dialog.py +132 -28
- euporie/core/widgets/display.py +76 -24
- euporie/core/widgets/file_browser.py +87 -31
- euporie/core/widgets/formatted_text_area.py +1 -3
- euporie/core/widgets/forms.py +79 -40
- euporie/core/widgets/inputs.py +23 -13
- euporie/core/widgets/layout.py +4 -3
- euporie/core/widgets/menu.py +368 -216
- euporie/core/widgets/page.py +99 -58
- euporie/core/widgets/pager.py +1 -1
- euporie/core/widgets/palette.py +30 -27
- euporie/core/widgets/search_bar.py +38 -25
- euporie/core/widgets/status_bar.py +103 -5
- euporie/data/desktop/euporie-console.desktop +7 -0
- euporie/data/desktop/euporie-notebook.desktop +7 -0
- euporie/hub/__main__.py +3 -1
- euporie/hub/app.py +9 -7
- euporie/notebook/__main__.py +3 -1
- euporie/notebook/app.py +7 -30
- euporie/notebook/tabs/__init__.py +7 -3
- euporie/notebook/tabs/display.py +18 -9
- euporie/notebook/tabs/edit.py +106 -23
- euporie/notebook/tabs/json.py +73 -0
- euporie/notebook/tabs/log.py +18 -8
- euporie/notebook/tabs/notebook.py +60 -41
- euporie/preview/__main__.py +3 -1
- euporie/preview/app.py +2 -1
- euporie/preview/tabs/notebook.py +23 -10
- euporie/web/tabs/web.py +149 -0
- euporie/web/widgets/webview.py +563 -0
- euporie-2.4.1.data/data/share/applications/euporie-console.desktop +7 -0
- euporie-2.4.1.data/data/share/applications/euporie-notebook.desktop +7 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/METADATA +6 -5
- euporie-2.4.1.dist-info/RECORD +129 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/WHEEL +1 -1
- euporie/core/url.py +0 -64
- euporie-2.3.2.dist-info/RECORD +0 -122
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/entry_points.txt +0 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -7,15 +7,20 @@ import re
|
|
7
7
|
from ast import literal_eval
|
8
8
|
from bisect import bisect_right
|
9
9
|
from collections.abc import Mapping
|
10
|
-
from functools import cached_property, lru_cache
|
10
|
+
from functools import cached_property, lru_cache, partial
|
11
11
|
from html.parser import HTMLParser
|
12
12
|
from itertools import zip_longest
|
13
13
|
from math import ceil
|
14
|
-
from
|
14
|
+
from operator import eq, ge, gt, le, lt
|
15
|
+
from typing import TYPE_CHECKING, NamedTuple, cast
|
16
|
+
from urllib.parse import urljoin
|
15
17
|
|
16
18
|
from flatlatex.data import subscript, superscript
|
17
19
|
from prompt_toolkit.application.current import get_app_session
|
18
|
-
from prompt_toolkit.
|
20
|
+
from prompt_toolkit.data_structures import Size
|
21
|
+
from prompt_toolkit.filters.base import Condition
|
22
|
+
from prompt_toolkit.filters.utils import _always as always
|
23
|
+
from prompt_toolkit.filters.utils import _never as never
|
19
24
|
from prompt_toolkit.formatted_text.base import StyleAndTextTuples
|
20
25
|
from prompt_toolkit.formatted_text.utils import split_lines
|
21
26
|
from prompt_toolkit.layout.dimension import Dimension
|
@@ -42,7 +47,7 @@ from euporie.core.border import (
|
|
42
47
|
UpperRightHalfDottedLine,
|
43
48
|
UpperRightHalfLine,
|
44
49
|
)
|
45
|
-
from euporie.core.convert.
|
50
|
+
from euporie.core.convert.core import convert, get_format
|
46
51
|
from euporie.core.convert.utils import data_pixel_size, pixels_to_cell_size
|
47
52
|
from euporie.core.current import get_app
|
48
53
|
from euporie.core.data_structures import DiBool, DiInt, DiStr
|
@@ -62,10 +67,8 @@ from euporie.core.formatted_text.utils import (
|
|
62
67
|
pad,
|
63
68
|
paste,
|
64
69
|
strip,
|
65
|
-
strip_one_trailing_newline,
|
66
70
|
truncate,
|
67
71
|
)
|
68
|
-
from euporie.core.url import load_url
|
69
72
|
|
70
73
|
|
71
74
|
class CssSelector(NamedTuple):
|
@@ -86,15 +89,16 @@ class CssSelector(NamedTuple):
|
|
86
89
|
|
87
90
|
|
88
91
|
if TYPE_CHECKING:
|
89
|
-
from
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
)
|
92
|
+
from pathlib import Path
|
93
|
+
from typing import Any, Callable, Generator, Iterator
|
94
|
+
|
95
|
+
from prompt_toolkit.filters.base import Filter
|
96
|
+
from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
|
97
|
+
from prompt_toolkit.mouse_events import MouseEvent
|
96
98
|
|
97
|
-
CssSelectors = dict[
|
99
|
+
CssSelectors = dict[
|
100
|
+
Filter, dict[tuple[tuple[CssSelector, ...], ...], dict[str, str]]
|
101
|
+
]
|
98
102
|
|
99
103
|
log = logging.getLogger(__name__)
|
100
104
|
|
@@ -119,6 +123,38 @@ _SELECTOR_RE = re.compile(
|
|
119
123
|
re.VERBOSE,
|
120
124
|
)
|
121
125
|
|
126
|
+
_AT_RULE_RE = re.compile(
|
127
|
+
r"""
|
128
|
+
(
|
129
|
+
@(?:color-profile|container|counter-style|document|font-face|font-feature-values|font-palette-values|keyframes|layer|media|page|supports)
|
130
|
+
[^@{]+
|
131
|
+
(?:
|
132
|
+
{[^{}]*}
|
133
|
+
|
|
134
|
+
{(?:[^{]+?{(?:[^{}]|{[^}]+?})+?}\s*)*?}
|
135
|
+
)
|
136
|
+
|
|
137
|
+
@(?:charset|import|namespace)[^;]*;
|
138
|
+
)
|
139
|
+
""",
|
140
|
+
re.VERBOSE,
|
141
|
+
)
|
142
|
+
|
143
|
+
_NESTED_AT_RULE_RE = re.compile(
|
144
|
+
r"@(?P<identifier>[\w-]+)\s+(?P<rule>[^{]*?)\s*{\s*(?P<part>.*)\s*}"
|
145
|
+
)
|
146
|
+
|
147
|
+
_MEDIA_QUERY_TARGET_RE = re.compile(
|
148
|
+
r"""
|
149
|
+
(?:
|
150
|
+
(?P<invert>not\s+)?
|
151
|
+
(?:only\s+)? # Ignore this
|
152
|
+
(?P<type>all|print|screen) |
|
153
|
+
(?:\((?P<feature>[^)]+)\))
|
154
|
+
)
|
155
|
+
""",
|
156
|
+
re.VERBOSE,
|
157
|
+
)
|
122
158
|
|
123
159
|
# List of elements which might not have a close tag
|
124
160
|
_VOID_ELEMENTS = (
|
@@ -330,6 +366,10 @@ def match_css_selector(
|
|
330
366
|
or (rule == "even" and sibling_element_index % 2 == 0)
|
331
367
|
)
|
332
368
|
continue
|
369
|
+
if pseudo.startswith(":link"):
|
370
|
+
pseudo = pseudo[5:]
|
371
|
+
matched = bool(element_attrs.get("href"))
|
372
|
+
continue
|
333
373
|
return False
|
334
374
|
|
335
375
|
if not matched:
|
@@ -443,7 +483,7 @@ def get_color(value: str) -> str:
|
|
443
483
|
hexes = []
|
444
484
|
for color_value in color_values:
|
445
485
|
if (int_value := get_integer(color_value)) is not None:
|
446
|
-
hexes.append(hex(int_value)[2:])
|
486
|
+
hexes.append(f"{hex(int_value)[2:]:02}")
|
447
487
|
else:
|
448
488
|
return ""
|
449
489
|
return "#" + "".join(hexes)
|
@@ -504,7 +544,7 @@ def css_dimension(
|
|
504
544
|
# TODO - process view-width units
|
505
545
|
# https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units
|
506
546
|
|
507
|
-
if units
|
547
|
+
if units in {"%", "vh", "vw"}:
|
508
548
|
assert available is not None
|
509
549
|
return number / 100 * available
|
510
550
|
|
@@ -541,7 +581,7 @@ def parse_css_content(content: str) -> dict[str, str]:
|
|
541
581
|
|
542
582
|
for declaration in content.split(";"):
|
543
583
|
name, _, value = declaration.partition(":")
|
544
|
-
name = name.strip()
|
584
|
+
name = name.strip().lower()
|
545
585
|
|
546
586
|
# Ignore "!important" tags for now - TODO
|
547
587
|
value = value.replace("!important", "").strip()
|
@@ -561,6 +601,8 @@ def parse_css_content(content: str) -> dict[str, str]:
|
|
561
601
|
bottom = values[2]
|
562
602
|
elif len(values) >= 4:
|
563
603
|
top, right, bottom, left, *_ = values
|
604
|
+
else:
|
605
|
+
top = right = bottom = left = ""
|
564
606
|
return top, right, bottom, left
|
565
607
|
|
566
608
|
# Compute values
|
@@ -659,6 +701,15 @@ def parse_css_content(content: str) -> dict[str, str]:
|
|
659
701
|
elif each_value in _LIST_STYLE_TYPES:
|
660
702
|
theme["list_style_type"] = each_value
|
661
703
|
|
704
|
+
elif name == "flex-flow":
|
705
|
+
values = set(value.split(" "))
|
706
|
+
directions = {"row", "row-reverse", "column", "column-reverse"}
|
707
|
+
wraps = {"nowrap", "wrap", "wrap-reverse"}
|
708
|
+
for value in values.intersection(directions):
|
709
|
+
theme["flex_direction"] = value
|
710
|
+
for value in values.intersection(wraps):
|
711
|
+
theme["flex_wrap"] = value
|
712
|
+
|
662
713
|
else:
|
663
714
|
name = name.replace("-", "_")
|
664
715
|
theme[name] = value
|
@@ -723,12 +774,15 @@ _DEFAULT_ELEMENT_CSS = {
|
|
723
774
|
"border_left_color": "",
|
724
775
|
"border_bottom_color": "",
|
725
776
|
"border_right_color": "",
|
777
|
+
# Flex
|
778
|
+
"flex_direction": "row",
|
779
|
+
"align_content": "normal",
|
726
780
|
# Position
|
727
781
|
"position": "static",
|
728
|
-
"top": "
|
729
|
-
"right": "
|
730
|
-
"bottom": "
|
731
|
-
"left": "
|
782
|
+
"top": "unset",
|
783
|
+
"right": "unset",
|
784
|
+
"bottom": "unset",
|
785
|
+
"left": "unset",
|
732
786
|
"z_index": "0",
|
733
787
|
# Lists
|
734
788
|
"list_style_type": "none",
|
@@ -771,8 +825,21 @@ class Theme(Mapping):
|
|
771
825
|
available_height: int,
|
772
826
|
) -> None:
|
773
827
|
"""Set the space available to the element for rendering."""
|
774
|
-
self.
|
775
|
-
|
828
|
+
if self.theme["position"] == "fixed":
|
829
|
+
# Space is given by position
|
830
|
+
position = self.position
|
831
|
+
dom = self.element.dom
|
832
|
+
assert dom.width is not None and dom.height is not None
|
833
|
+
self.available_width = (dom.width - position.right) - position.left
|
834
|
+
self.available_height = (dom.height - position.bottom) - position.top
|
835
|
+
|
836
|
+
# elif parent_theme := self.parent_theme:
|
837
|
+
# self.available_width = parent_theme.content_width
|
838
|
+
# self.available_height = parent_theme.content_height
|
839
|
+
|
840
|
+
else:
|
841
|
+
self.available_width = available_width
|
842
|
+
self.available_height = available_height
|
776
843
|
|
777
844
|
# Theme calculation methods
|
778
845
|
|
@@ -786,7 +853,7 @@ class Theme(Mapping):
|
|
786
853
|
**parent_theme.inherited_browser_css_theme,
|
787
854
|
**parent_theme.browser_css_theme,
|
788
855
|
}.items()
|
789
|
-
if k in _HERITABLE_PROPS
|
856
|
+
if k in _HERITABLE_PROPS or k.startswith("__")
|
790
857
|
}
|
791
858
|
|
792
859
|
else:
|
@@ -795,22 +862,40 @@ class Theme(Mapping):
|
|
795
862
|
@cached_property
|
796
863
|
def inherited_theme(self) -> dict[str, str]:
|
797
864
|
"""Calculate the theme inherited from the element's parent."""
|
865
|
+
theme: dict[str, str] = {}
|
866
|
+
parent = self.element.parent
|
867
|
+
|
868
|
+
# Text elements inherit from direct inline parents
|
869
|
+
if self.element.name == "text" and parent is not None:
|
870
|
+
inline_parent_themes = []
|
871
|
+
while parent.theme.d_inline:
|
872
|
+
inline_parent_themes.append(parent.theme.theme)
|
873
|
+
parent = parent.parent
|
874
|
+
if parent is None:
|
875
|
+
break
|
876
|
+
for inherited_theme in inline_parent_themes:
|
877
|
+
theme.update(inherited_theme)
|
878
|
+
|
879
|
+
# Inherit heritable properties from the parent element's theme
|
880
|
+
# unless the default value is unset
|
798
881
|
if (parent_theme := self.parent_theme) is not None:
|
799
882
|
browser_css = self.browser_css_theme
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
883
|
+
theme.update(
|
884
|
+
{
|
885
|
+
k: v
|
886
|
+
for part in (
|
887
|
+
parent_theme.inherited_theme,
|
888
|
+
parent_theme.dom_css_theme,
|
889
|
+
parent_theme.attributes_theme,
|
890
|
+
parent_theme.style_attribute_theme,
|
891
|
+
)
|
892
|
+
for k, v in part.items()
|
893
|
+
if (k in _HERITABLE_PROPS or k.startswith("__"))
|
894
|
+
and browser_css.get(k) != "unset"
|
895
|
+
}
|
896
|
+
)
|
811
897
|
|
812
|
-
|
813
|
-
return {}
|
898
|
+
return theme
|
814
899
|
|
815
900
|
@cached_property
|
816
901
|
def style_attribute_theme(self) -> dict[str, str]:
|
@@ -855,70 +940,76 @@ class Theme(Mapping):
|
|
855
940
|
"""Calculate the theme defined in CSS."""
|
856
941
|
specificity_rules = []
|
857
942
|
element = self.element
|
858
|
-
for
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
selector.pseudo or "",
|
866
|
-
element.name,
|
867
|
-
element.is_first_child_element,
|
868
|
-
element.is_last_child_element,
|
869
|
-
element.sibling_element_index,
|
870
|
-
**element.attrs,
|
871
|
-
):
|
872
|
-
continue
|
873
|
-
|
874
|
-
# All of the parent selectors should match a separate parent in order
|
875
|
-
# TODO - combinators
|
876
|
-
# https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors#combinators
|
877
|
-
unmatched_parents: list[Node] = [x for x in element.parents[::-1] if x]
|
878
|
-
|
879
|
-
_unmatched_parents: list[Node]
|
880
|
-
parent = element.parent
|
881
|
-
if parent and (
|
882
|
-
(selector.comb == ">" and parent)
|
883
|
-
# Pseudo-element selectors only match direct ancestors
|
884
|
-
or ((item := selector.item) and item.startswith("::"))
|
885
|
-
):
|
886
|
-
_unmatched_parents = [parent]
|
887
|
-
else:
|
888
|
-
_unmatched_parents = unmatched_parents
|
889
|
-
|
890
|
-
# TODO investigate caching element / selector chains so we don't have to
|
891
|
-
# iterate through every parent every time
|
892
|
-
|
893
|
-
# Iterate through selector items in reverse, skipping the last
|
894
|
-
for selector in selector_parts[-2::-1]:
|
895
|
-
for i, parent in enumerate(_unmatched_parents):
|
896
|
-
if parent and match_css_selector(
|
943
|
+
for condition, css_block in css.items():
|
944
|
+
if condition():
|
945
|
+
for selectors, rule in css_block.items():
|
946
|
+
for selector_parts in selectors:
|
947
|
+
# Last selector item should match the current element
|
948
|
+
selector = selector_parts[-1]
|
949
|
+
if not match_css_selector(
|
897
950
|
selector.item or "",
|
898
951
|
selector.attr or "",
|
899
952
|
selector.pseudo or "",
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
**
|
953
|
+
element.name,
|
954
|
+
element.is_first_child_element,
|
955
|
+
element.is_last_child_element,
|
956
|
+
element.sibling_element_index,
|
957
|
+
**element.attrs,
|
905
958
|
):
|
906
|
-
|
907
|
-
|
959
|
+
continue
|
960
|
+
|
961
|
+
# All of the parent selectors should match a separate parent in order
|
962
|
+
# TODO - combinators
|
963
|
+
# https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors#combinators
|
964
|
+
unmatched_parents: list[Node] = [
|
965
|
+
x for x in element.parents[::-1] if x
|
966
|
+
]
|
967
|
+
|
968
|
+
_unmatched_parents: list[Node]
|
969
|
+
parent = element.parent
|
970
|
+
if parent and (
|
971
|
+
(selector.comb == ">" and parent)
|
972
|
+
# Pseudo-element selectors only match direct ancestors
|
973
|
+
or ((item := selector.item) and item.startswith("::"))
|
974
|
+
):
|
975
|
+
_unmatched_parents = [parent]
|
976
|
+
else:
|
977
|
+
_unmatched_parents = unmatched_parents
|
978
|
+
|
979
|
+
# TODO investigate caching element / selector chains so we don't have to
|
980
|
+
# iterate through every parent every time
|
981
|
+
|
982
|
+
# Iterate through selector items in reverse, skipping the last
|
983
|
+
for selector in selector_parts[-2::-1]:
|
984
|
+
for i, parent in enumerate(_unmatched_parents):
|
985
|
+
if parent and match_css_selector(
|
986
|
+
selector.item or "",
|
987
|
+
selector.attr or "",
|
988
|
+
selector.pseudo or "",
|
989
|
+
parent.name,
|
990
|
+
parent.is_first_child_element,
|
991
|
+
parent.is_last_child_element,
|
992
|
+
parent.sibling_element_index,
|
993
|
+
**parent.attrs,
|
994
|
+
):
|
995
|
+
if selector.comb == ">" and (
|
996
|
+
parent := parent.parent
|
997
|
+
):
|
998
|
+
_unmatched_parents = [parent]
|
999
|
+
else:
|
1000
|
+
_unmatched_parents = unmatched_parents[i + 1 :]
|
1001
|
+
break
|
908
1002
|
else:
|
909
|
-
|
910
|
-
break
|
911
|
-
else:
|
912
|
-
break
|
1003
|
+
break
|
913
1004
|
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
1005
|
+
else:
|
1006
|
+
# Calculate selector specificity score
|
1007
|
+
specificity_rules.append(
|
1008
|
+
(selector_specificity(selector_parts), rule)
|
1009
|
+
)
|
1010
|
+
# We have already matched this rule, we don't need to keep checking
|
1011
|
+
# the rest of the selectors for this rule
|
1012
|
+
break
|
922
1013
|
|
923
1014
|
return {
|
924
1015
|
k: v
|
@@ -941,32 +1032,79 @@ class Theme(Mapping):
|
|
941
1032
|
@cached_property
|
942
1033
|
def d_block(self) -> bool:
|
943
1034
|
"""If the element a block element."""
|
944
|
-
|
1035
|
+
theme = self.theme
|
1036
|
+
parent_theme = self.parent_theme
|
1037
|
+
return theme["display"] in {"block", "flex"} and (
|
1038
|
+
parent_theme is None
|
1039
|
+
or (self.floated is None and not parent_theme.d_flex)
|
1040
|
+
or (
|
1041
|
+
parent_theme.d_flex and parent_theme["flex_direction"].startswith("col")
|
1042
|
+
)
|
1043
|
+
)
|
945
1044
|
|
946
1045
|
@cached_property
|
947
1046
|
def d_inline(self) -> bool:
|
948
1047
|
"""If the element an inline element."""
|
949
|
-
return self.theme["display"] == "inline" and self.
|
1048
|
+
return self.theme["display"] == "inline" and self.floated is None
|
950
1049
|
|
951
1050
|
@cached_property
|
952
1051
|
def d_inline_block(self) -> bool:
|
953
1052
|
"""If the element an inline element."""
|
954
|
-
|
1053
|
+
theme = self.theme
|
1054
|
+
parent_theme = self.parent_theme
|
1055
|
+
return (
|
1056
|
+
# Is an actual inline block
|
1057
|
+
theme["display"] == "inline-block"
|
1058
|
+
# Is floated
|
1059
|
+
or self.floated is not None
|
1060
|
+
# If flexed in the row direction
|
1061
|
+
or (
|
1062
|
+
parent_theme is not None
|
1063
|
+
and parent_theme.d_flex
|
1064
|
+
and parent_theme["flex_direction"].startswith("row")
|
1065
|
+
)
|
1066
|
+
)
|
1067
|
+
|
1068
|
+
@cached_property
|
1069
|
+
def d_flex(self) -> bool:
|
1070
|
+
"""If the element a block element."""
|
1071
|
+
return self.theme["display"] == "flex"
|
1072
|
+
|
1073
|
+
@cached_property
|
1074
|
+
def d_image(self) -> bool:
|
1075
|
+
"""If the element is an image."""
|
1076
|
+
return self.element.name in {"img", "svg"}
|
955
1077
|
|
956
1078
|
@cached_property
|
957
1079
|
def d_table(self) -> bool:
|
958
1080
|
"""If the element a block element."""
|
959
1081
|
return self.theme["display"] == "table"
|
960
1082
|
|
1083
|
+
@cached_property
|
1084
|
+
def d_table_cell(self) -> bool:
|
1085
|
+
"""If the element a block element."""
|
1086
|
+
return self.theme["display"] == "table-cell"
|
1087
|
+
|
961
1088
|
@cached_property
|
962
1089
|
def d_list_item(self) -> bool:
|
963
1090
|
"""If the element an inline element."""
|
964
|
-
return self.theme["display"] == "list-item" and self.
|
1091
|
+
return self.theme["display"] == "list-item" and self.floated is None
|
965
1092
|
|
966
1093
|
@cached_property
|
967
1094
|
def d_blocky(self) -> bool:
|
968
1095
|
"""If the element an inline element."""
|
969
|
-
return self.d_block or self.d_table or self.d_list_item
|
1096
|
+
return self.d_block or self.d_table or self.d_table_cell or self.d_list_item
|
1097
|
+
|
1098
|
+
@cached_property
|
1099
|
+
def floated(self) -> str | None:
|
1100
|
+
"""The float status of the element."""
|
1101
|
+
value = self.theme["float"]
|
1102
|
+
# Float property does not apply to flex-level boxes
|
1103
|
+
if value == "none" or (
|
1104
|
+
(parent_theme := self.parent_theme) and parent_theme.d_flex
|
1105
|
+
):
|
1106
|
+
return None
|
1107
|
+
return value
|
970
1108
|
|
971
1109
|
@property
|
972
1110
|
def min_width(self) -> int | None:
|
@@ -982,17 +1120,24 @@ class Theme(Mapping):
|
|
982
1120
|
@property
|
983
1121
|
def width(self) -> int | None:
|
984
1122
|
"""The pescribed width."""
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
1123
|
+
value = self.theme.get("width")
|
1124
|
+
|
1125
|
+
theme_width: int | float | None
|
1126
|
+
if value:
|
1127
|
+
if value == "min-content":
|
1128
|
+
theme_width = self.min_content_width
|
1129
|
+
elif value == "max-content":
|
1130
|
+
theme_width = self.max_content_width
|
1131
|
+
else:
|
1132
|
+
theme_width = css_dimension(
|
1133
|
+
value, vertical=False, available=self.available_width
|
1134
|
+
)
|
989
1135
|
if theme_width is not None:
|
990
|
-
return int(theme_width)
|
1136
|
+
return min(int(theme_width), self.available_width)
|
991
1137
|
|
992
|
-
|
993
|
-
element = self.element
|
1138
|
+
elif (element := self.element).name == "input":
|
994
1139
|
attrs = element.attrs
|
995
|
-
if
|
1140
|
+
if attrs.get("type") in {
|
996
1141
|
"text",
|
997
1142
|
"password",
|
998
1143
|
"email",
|
@@ -1003,6 +1148,9 @@ class Theme(Mapping):
|
|
1003
1148
|
}:
|
1004
1149
|
return try_eval(size) if (size := attrs.get("size")) else 20
|
1005
1150
|
|
1151
|
+
elif element.name == "text":
|
1152
|
+
return len(element.text)
|
1153
|
+
|
1006
1154
|
return None
|
1007
1155
|
|
1008
1156
|
@property
|
@@ -1016,47 +1164,150 @@ class Theme(Mapping):
|
|
1016
1164
|
return int(theme_width)
|
1017
1165
|
return None
|
1018
1166
|
|
1019
|
-
@
|
1020
|
-
def
|
1021
|
-
"""
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1167
|
+
@cached_property
|
1168
|
+
def min_content_width(self) -> int:
|
1169
|
+
"""Get maximum absolute child width."""
|
1170
|
+
if self.element.name == "text":
|
1171
|
+
return max([len(x) for x in self.element.text.split()] or [0])
|
1172
|
+
else:
|
1173
|
+
return max(
|
1174
|
+
[
|
1175
|
+
child.theme.min_content_width
|
1176
|
+
for child in self.element.renderable_contents
|
1177
|
+
]
|
1178
|
+
or [0]
|
1179
|
+
)
|
1180
|
+
|
1181
|
+
@cached_property
|
1182
|
+
def max_content_width(self) -> int:
|
1183
|
+
"""Get maximum absolute child width."""
|
1184
|
+
if self.element.name == "text":
|
1185
|
+
return max([len(x) for x in self.element.text.split("\n")] or [0])
|
1186
|
+
else:
|
1187
|
+
return sum(
|
1188
|
+
[
|
1189
|
+
child.theme.max_content_width
|
1190
|
+
for child in self.element.renderable_contents
|
1191
|
+
]
|
1192
|
+
or [0]
|
1026
1193
|
)
|
1027
|
-
if theme_height is not None:
|
1028
|
-
return int(theme_height)
|
1029
|
-
assert self.available_height is not None
|
1030
|
-
return self.available_height
|
1031
1194
|
|
1032
1195
|
@property
|
1033
1196
|
def content_width(self) -> int:
|
1034
1197
|
"""Return the width available for rendering the element's content."""
|
1035
1198
|
value = self.width
|
1036
|
-
|
1199
|
+
|
1200
|
+
border_box = self.theme.get("box_sizing") == "border-box"
|
1201
|
+
|
1202
|
+
# Use max-content-width as default for inline-blocks
|
1203
|
+
if value is None and self.d_inline_block and not self.d_image:
|
1204
|
+
value = self.max_content_width
|
1205
|
+
# Do not use border-box for inline-blocks if no width is given
|
1206
|
+
border_box = False
|
1207
|
+
|
1208
|
+
# If we do not have a value, use the available space
|
1037
1209
|
if value is None:
|
1038
|
-
|
1210
|
+
margin = self.margin
|
1211
|
+
value = (self.available_width or 0) - margin.left - margin.right
|
1212
|
+
|
1213
|
+
# Blocks without a set with are rendered with border-box box-sizing
|
1214
|
+
if self.d_blocky or self.d_inline_block:
|
1215
|
+
border_box = True
|
1216
|
+
|
1217
|
+
# Ignore box-sizing for table cells, as they include padding and borders
|
1218
|
+
if self.d_table_cell:
|
1219
|
+
border_box = False
|
1220
|
+
|
1221
|
+
# Remove padding and borders from available space if we are using box-sizing
|
1222
|
+
if border_box:
|
1223
|
+
padding = self.padding
|
1224
|
+
border_visibility = self.border_visibility
|
1225
|
+
value -= padding.left + padding.right
|
1226
|
+
value -= border_visibility.left + border_visibility.right
|
1227
|
+
|
1039
1228
|
# Apply min / max width constraints
|
1040
1229
|
if (max_width := self.max_width) is not None and max_width < value:
|
1041
1230
|
value = max_width
|
1042
1231
|
elif (min_width := self.min_width) is not None and min_width > value:
|
1043
1232
|
value = min_width
|
1044
|
-
# Remove padding and borders from content width for blocks
|
1045
|
-
if value and (self.d_blocky or self.d_inline_block):
|
1046
|
-
value -= self.padding.left + self.padding.right
|
1047
|
-
value -= self.border_visibility.left + self.border_visibility.right
|
1048
1233
|
|
1049
1234
|
return max(0, value)
|
1050
1235
|
|
1236
|
+
@property
|
1237
|
+
def min_height(self) -> int | None:
|
1238
|
+
"""The minimum permitted height."""
|
1239
|
+
if value := self.get("min_height"):
|
1240
|
+
theme_height = css_dimension(
|
1241
|
+
value, vertical=True, available=self.available_height
|
1242
|
+
)
|
1243
|
+
if theme_height is not None:
|
1244
|
+
return int(theme_height)
|
1245
|
+
return None
|
1246
|
+
|
1247
|
+
@property
|
1248
|
+
def height(self) -> int | None:
|
1249
|
+
"""The perscribed height."""
|
1250
|
+
# TODO - process min-/max-height
|
1251
|
+
if value := self.get("height"):
|
1252
|
+
theme_height = css_dimension(
|
1253
|
+
value, vertical=True, available=self.available_height
|
1254
|
+
)
|
1255
|
+
if theme_height is not None:
|
1256
|
+
return int(theme_height)
|
1257
|
+
return None
|
1258
|
+
|
1259
|
+
@property
|
1260
|
+
def max_height(self) -> int | None:
|
1261
|
+
"""The maximum permitted height."""
|
1262
|
+
if value := self.get("max_height"):
|
1263
|
+
theme_height = css_dimension(
|
1264
|
+
value, vertical=True, available=self.available_height
|
1265
|
+
)
|
1266
|
+
if theme_height is not None:
|
1267
|
+
return int(theme_height)
|
1268
|
+
return None
|
1269
|
+
|
1051
1270
|
@property
|
1052
1271
|
def content_height(self) -> int:
|
1053
1272
|
"""Return the height available for rendering the element's content."""
|
1054
1273
|
value = self.height
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1274
|
+
|
1275
|
+
border_box = self.theme.get("box_sizing") == "border-box"
|
1276
|
+
|
1277
|
+
# Use max-content-height as default for inline-blocks
|
1278
|
+
# if value is None and self.d_inline_block and not self.d_image:
|
1279
|
+
# value = self.max_content_height
|
1280
|
+
# # Do not use border-box for inline-blocks if no height is given
|
1281
|
+
# border_box = False
|
1282
|
+
|
1283
|
+
# If we do not have a value, use the available space
|
1284
|
+
if value is None:
|
1285
|
+
margin = self.margin
|
1286
|
+
value = (self.available_height or 0) - margin.top - margin.bottom
|
1287
|
+
|
1288
|
+
# Blocks without a set with are rendered with border-box box-sizing
|
1289
|
+
# (but not rendered tables as they include padding and borders)
|
1290
|
+
if self.d_blocky or self.d_inline_block:
|
1291
|
+
border_box = True
|
1292
|
+
|
1293
|
+
# Ignore box-sizing for table cells, as they include padding and borders
|
1294
|
+
if self.d_table_cell:
|
1295
|
+
border_box = False
|
1296
|
+
|
1297
|
+
# Remove padding and borders from available space if we are using box-sizing
|
1298
|
+
if border_box:
|
1299
|
+
padding = self.padding
|
1300
|
+
border_visibility = self.border_visibility
|
1301
|
+
value -= padding.top + padding.bottom
|
1302
|
+
value -= border_visibility.top + border_visibility.bottom
|
1303
|
+
|
1304
|
+
# Apply min / max height constraints
|
1305
|
+
if (max_height := self.max_height) is not None and max_height < value:
|
1306
|
+
value = max_height
|
1307
|
+
elif (min_height := self.min_height) is not None and min_height > value:
|
1308
|
+
value = min_height
|
1309
|
+
|
1310
|
+
return max(0, value)
|
1060
1311
|
|
1061
1312
|
@cached_property
|
1062
1313
|
def padding(self) -> DiInt:
|
@@ -1144,6 +1395,7 @@ class Theme(Mapping):
|
|
1144
1395
|
# Replace the margin on the parent
|
1145
1396
|
if (
|
1146
1397
|
(first_child := element.first_child_element)
|
1398
|
+
and first_child.prev_node_in_flow is None
|
1147
1399
|
and not self.border_visibility.top
|
1148
1400
|
and not self.padding.top
|
1149
1401
|
):
|
@@ -1157,6 +1409,7 @@ class Theme(Mapping):
|
|
1157
1409
|
values["top"] = max(child_theme.base_margin.top, values["top"])
|
1158
1410
|
if (
|
1159
1411
|
(last_child := element.last_child_element)
|
1412
|
+
and last_child.next_node_in_flow is None
|
1160
1413
|
and not self.padding.bottom
|
1161
1414
|
and not self.border_visibility.bottom
|
1162
1415
|
):
|
@@ -1183,9 +1436,18 @@ class Theme(Mapping):
|
|
1183
1436
|
return DiInt(**values)
|
1184
1437
|
|
1185
1438
|
@cached_property
|
1186
|
-
def
|
1439
|
+
def block_align(self) -> FormattedTextAlign:
|
1187
1440
|
"""Determine if the left and right margins are set to auto."""
|
1188
|
-
|
1441
|
+
# Temporarily use "justify_self" until flex / grid are implemented (TODO)
|
1442
|
+
if (self.theme["margin_left"] == self.theme["margin_right"] == "auto") or (
|
1443
|
+
self.d_inline_block and self.theme.get("justify_self") == "center"
|
1444
|
+
):
|
1445
|
+
return FormattedTextAlign.CENTER
|
1446
|
+
elif (self.theme["margin_left"] == "auto") or (
|
1447
|
+
self.d_inline_block and self.theme.get("justify_self") == "right"
|
1448
|
+
):
|
1449
|
+
return FormattedTextAlign.RIGHT
|
1450
|
+
return FormattedTextAlign.LEFT
|
1189
1451
|
|
1190
1452
|
@cached_property
|
1191
1453
|
def border_style(self) -> DiStr:
|
@@ -1208,11 +1470,11 @@ class Theme(Mapping):
|
|
1208
1470
|
elif border_color := get_color(color_str):
|
1209
1471
|
style += f" fg:{border_color}"
|
1210
1472
|
|
1211
|
-
if getattr(self.border_line, direction) in {
|
1212
|
-
|
1213
|
-
|
1214
|
-
}:
|
1215
|
-
|
1473
|
+
# if getattr(self.border_line, direction) in {
|
1474
|
+
# UpperRightEighthLine,
|
1475
|
+
# LowerLeftEighthLine,
|
1476
|
+
# }:
|
1477
|
+
style += f" bg:{self.background_color}"
|
1216
1478
|
|
1217
1479
|
output[direction] = style
|
1218
1480
|
|
@@ -1243,7 +1505,6 @@ class Theme(Mapping):
|
|
1243
1505
|
def border_line(self) -> DiLineStyle:
|
1244
1506
|
"""Calculate the line style."""
|
1245
1507
|
a_w = self.available_width
|
1246
|
-
self.d_inline
|
1247
1508
|
output = {}
|
1248
1509
|
for direction in ("top", "right", "bottom", "left"):
|
1249
1510
|
border_width = self.theme[f"border_{direction}_width"]
|
@@ -1294,7 +1555,13 @@ class Theme(Mapping):
|
|
1294
1555
|
"""Get the computed theme foreground color."""
|
1295
1556
|
# TODO - transparency
|
1296
1557
|
color_str = self.theme["color"]
|
1297
|
-
if
|
1558
|
+
# Use black as the default color if a bg color is set
|
1559
|
+
if color_str.startswith("var("):
|
1560
|
+
var = color_str[4:-1].replace("-", "_")
|
1561
|
+
color_str = self.theme.get(var, color_str)
|
1562
|
+
if color_str == "default" and self.theme["background_color"] != "default":
|
1563
|
+
color_str = "#000000"
|
1564
|
+
elif color_str == "transparent":
|
1298
1565
|
return self.background_color
|
1299
1566
|
if fg := get_color(color_str):
|
1300
1567
|
return fg
|
@@ -1308,9 +1575,16 @@ class Theme(Mapping):
|
|
1308
1575
|
"""Get the computed theme background color."""
|
1309
1576
|
# TODO - transparency
|
1310
1577
|
color_str = self.theme["background_color"]
|
1578
|
+
# Use white as the default bg color if a fg color is set
|
1579
|
+
# if color_str == "default" and self.theme["color"] != "default":
|
1580
|
+
# color_str = "#ffffff"
|
1581
|
+
if color_str.startswith("var("):
|
1582
|
+
var = color_str[4:-1].replace("-", "_")
|
1583
|
+
color_str = self.theme.get(var, color_str)
|
1311
1584
|
if color_str == "transparent":
|
1312
1585
|
if parent_theme := self.parent_theme:
|
1313
1586
|
return parent_theme.background_color
|
1587
|
+
color_str = ""
|
1314
1588
|
if bg := get_color(color_str):
|
1315
1589
|
return bg
|
1316
1590
|
elif self.parent_theme:
|
@@ -1381,6 +1655,19 @@ class Theme(Mapping):
|
|
1381
1655
|
@cached_property
|
1382
1656
|
def vertical_align(self) -> float:
|
1383
1657
|
"""The vertical alignment direction."""
|
1658
|
+
if (parent_theme := self.parent_theme) and parent_theme.d_flex:
|
1659
|
+
if self.theme.get("align_content") in {
|
1660
|
+
"normal",
|
1661
|
+
"flex-start",
|
1662
|
+
"start",
|
1663
|
+
"baseline",
|
1664
|
+
}:
|
1665
|
+
return 0
|
1666
|
+
elif self.theme.get("align_content") == "center":
|
1667
|
+
return 0.5
|
1668
|
+
else:
|
1669
|
+
return 1
|
1670
|
+
|
1384
1671
|
return _VERTICAL_ALIGNS.get(self.theme["vertical_align"], 1)
|
1385
1672
|
|
1386
1673
|
@cached_property
|
@@ -1418,12 +1705,13 @@ class Theme(Mapping):
|
|
1418
1705
|
def position(self) -> DiInt:
|
1419
1706
|
"""The position of an element with a relative, absolute or fixed position."""
|
1420
1707
|
# TODO - calculate position based on top, left, bottom,right, width, height
|
1708
|
+
soup_theme = self.element.dom.soup.theme
|
1421
1709
|
return DiInt(
|
1422
1710
|
top=int(
|
1423
1711
|
css_dimension(
|
1424
1712
|
self.theme["top"],
|
1425
1713
|
vertical=True,
|
1426
|
-
available=
|
1714
|
+
available=soup_theme.available_height,
|
1427
1715
|
)
|
1428
1716
|
or 0
|
1429
1717
|
),
|
@@ -1431,7 +1719,7 @@ class Theme(Mapping):
|
|
1431
1719
|
css_dimension(
|
1432
1720
|
self.theme["right"],
|
1433
1721
|
vertical=False,
|
1434
|
-
available=
|
1722
|
+
available=soup_theme.available_width,
|
1435
1723
|
)
|
1436
1724
|
or 0
|
1437
1725
|
),
|
@@ -1439,7 +1727,7 @@ class Theme(Mapping):
|
|
1439
1727
|
css_dimension(
|
1440
1728
|
self.theme["bottom"],
|
1441
1729
|
vertical=True,
|
1442
|
-
available=
|
1730
|
+
available=soup_theme.available_height,
|
1443
1731
|
)
|
1444
1732
|
or 0
|
1445
1733
|
),
|
@@ -1447,12 +1735,22 @@ class Theme(Mapping):
|
|
1447
1735
|
css_dimension(
|
1448
1736
|
self.theme["left"],
|
1449
1737
|
vertical=False,
|
1450
|
-
available=
|
1738
|
+
available=soup_theme.available_width,
|
1451
1739
|
)
|
1452
1740
|
or 0
|
1453
1741
|
),
|
1454
1742
|
)
|
1455
1743
|
|
1744
|
+
@cached_property
|
1745
|
+
def anchors(self) -> DiBool:
|
1746
|
+
"""Which position directions are set."""
|
1747
|
+
return DiBool(
|
1748
|
+
top=self.theme["top"] not in {"unset"},
|
1749
|
+
right=self.theme["right"] not in {"unset"},
|
1750
|
+
bottom=self.theme["bottom"] not in {"unset"},
|
1751
|
+
left=self.theme["left"] not in {"unset"},
|
1752
|
+
)
|
1753
|
+
|
1456
1754
|
@cached_property
|
1457
1755
|
def skip(self) -> bool:
|
1458
1756
|
"""Determine if the element should not be displayed."""
|
@@ -1481,7 +1779,7 @@ class Theme(Mapping):
|
|
1481
1779
|
"""Determine if the element is "in-flow"."""
|
1482
1780
|
return (
|
1483
1781
|
not self.skip
|
1484
|
-
and self.
|
1782
|
+
and self.floated is None
|
1485
1783
|
and self.theme["position"] not in {"absolute", "fixed"}
|
1486
1784
|
and self.element.name != "html"
|
1487
1785
|
)
|
@@ -1501,418 +1799,498 @@ class Theme(Mapping):
|
|
1501
1799
|
return self.theme.__len__()
|
1502
1800
|
|
1503
1801
|
|
1504
|
-
_BROWSER_CSS = {
|
1505
|
-
|
1506
|
-
|
1507
|
-
(CssSelector(item="
|
1508
|
-
|
1509
|
-
(CssSelector(item="command"),),
|
1510
|
-
(CssSelector(item="link"),),
|
1511
|
-
(CssSelector(item="meta"),),
|
1512
|
-
(CssSelector(item="noscript"),),
|
1513
|
-
(CssSelector(item="script"),),
|
1514
|
-
(CssSelector(item="style"),),
|
1515
|
-
(CssSelector(item="title"),),
|
1516
|
-
(CssSelector(item="option"),),
|
1517
|
-
(CssSelector(item="input", attr="[type=hidden]"),),
|
1518
|
-
): {"display": "none"},
|
1519
|
-
# Inline elements
|
1520
|
-
(
|
1521
|
-
(CssSelector(item="::before"),),
|
1522
|
-
(CssSelector(item="::after"),),
|
1523
|
-
(CssSelector(item="text"),),
|
1524
|
-
(CssSelector(item="abbr"),),
|
1525
|
-
(CssSelector(item="acronym"),),
|
1526
|
-
(CssSelector(item="audio"),),
|
1527
|
-
(CssSelector(item="bdi"),),
|
1528
|
-
(CssSelector(item="bdo"),),
|
1529
|
-
(CssSelector(item="big"),),
|
1530
|
-
(CssSelector(item="br"),),
|
1531
|
-
(CssSelector(item="canvas"),),
|
1532
|
-
(CssSelector(item="data"),),
|
1533
|
-
(CssSelector(item="datalist"),),
|
1534
|
-
(CssSelector(item="embed"),),
|
1535
|
-
(CssSelector(item="iframe"),),
|
1536
|
-
(CssSelector(item="label"),),
|
1537
|
-
(CssSelector(item="map"),),
|
1538
|
-
(CssSelector(item="meter"),),
|
1539
|
-
(CssSelector(item="object"),),
|
1540
|
-
(CssSelector(item="output"),),
|
1541
|
-
(CssSelector(item="picture"),),
|
1542
|
-
(CssSelector(item="progress"),),
|
1543
|
-
(CssSelector(item="q"),),
|
1544
|
-
(CssSelector(item="ruby"),),
|
1545
|
-
(CssSelector(item="select"),),
|
1546
|
-
(CssSelector(item="slot"),),
|
1547
|
-
(CssSelector(item="small"),),
|
1548
|
-
(CssSelector(item="span"),),
|
1549
|
-
(CssSelector(item="template"),),
|
1550
|
-
(CssSelector(item="textarea"),),
|
1551
|
-
(CssSelector(item="time"),),
|
1552
|
-
(CssSelector(item="tt"),),
|
1553
|
-
(CssSelector(item="video"),),
|
1554
|
-
(CssSelector(item="wbr"),),
|
1555
|
-
): {"display": "inline"},
|
1556
|
-
# Formatted inlines
|
1557
|
-
((CssSelector(item="a"),),): {
|
1558
|
-
"display": "inline",
|
1559
|
-
"text_decoration": "underline",
|
1560
|
-
"color": "ansibrightblue",
|
1561
|
-
},
|
1562
|
-
((CssSelector(item="b"),), (CssSelector(item="strong"),)): {
|
1563
|
-
"display": "inline",
|
1564
|
-
"font_weight": "bold",
|
1565
|
-
},
|
1566
|
-
(
|
1567
|
-
(CssSelector(item="cite"),),
|
1568
|
-
(CssSelector(item="dfn"),),
|
1569
|
-
(CssSelector(item="em"),),
|
1570
|
-
(CssSelector(item="i"),),
|
1571
|
-
(CssSelector(item="var"),),
|
1572
|
-
): {"display": "inline", "font_style": "italic"},
|
1573
|
-
((CssSelector(item="code"),),): {
|
1574
|
-
"display": "inline",
|
1575
|
-
},
|
1576
|
-
((CssSelector(item="del"),), (CssSelector(item="s"),)): {
|
1577
|
-
"display": "inline",
|
1578
|
-
"text_decoration": "line-through",
|
1579
|
-
},
|
1580
|
-
((CssSelector(item="img"),), (CssSelector(item="svg"),)): {
|
1581
|
-
"display": "inline-block",
|
1582
|
-
"overflow_x": "hidden",
|
1583
|
-
"overflow_y": "hidden",
|
1584
|
-
},
|
1585
|
-
((CssSelector(item="ins"),), (CssSelector(item="u"),)): {
|
1586
|
-
"display": "inline",
|
1587
|
-
"text_decoration": "underline",
|
1588
|
-
},
|
1589
|
-
((CssSelector(item="kbd"),),): {
|
1590
|
-
"display": "inline",
|
1591
|
-
"background_color": "#333344",
|
1592
|
-
"color": "#FFFFFF",
|
1593
|
-
},
|
1594
|
-
((CssSelector(item="mark"),),): {
|
1595
|
-
"display": "inline",
|
1596
|
-
"color": "black",
|
1597
|
-
"background_color": "#FFFF00",
|
1598
|
-
},
|
1599
|
-
((CssSelector(item="samp"),),): {
|
1600
|
-
"display": "inline",
|
1601
|
-
"background_color": "#334433",
|
1602
|
-
"color": "#FFFFFF",
|
1603
|
-
},
|
1604
|
-
((CssSelector(item="sub"),),): {"display": "inline", "vertical_align": "sub"},
|
1605
|
-
((CssSelector(item="sup"),),): {"display": "inline", "vertical_align": "super"},
|
1606
|
-
(
|
1802
|
+
_BROWSER_CSS: CssSelectors = {
|
1803
|
+
always: {
|
1804
|
+
# Set body background to white
|
1805
|
+
((CssSelector(item="body"),),): {"background_color": "#FFFFFF"},
|
1806
|
+
# Non-rendered elements
|
1607
1807
|
(
|
1608
|
-
CssSelector(item="
|
1609
|
-
CssSelector(item="
|
1610
|
-
|
1611
|
-
|
1612
|
-
|
1808
|
+
(CssSelector(item="head"),),
|
1809
|
+
(CssSelector(item="base"),),
|
1810
|
+
(CssSelector(item="command"),),
|
1811
|
+
(CssSelector(item="link"),),
|
1812
|
+
(CssSelector(item="meta"),),
|
1813
|
+
(CssSelector(item="noscript"),),
|
1814
|
+
(CssSelector(item="script"),),
|
1815
|
+
(CssSelector(item="style"),),
|
1816
|
+
(CssSelector(item="title"),),
|
1817
|
+
(CssSelector(item="option"),),
|
1818
|
+
(CssSelector(item="input", attr="[type=hidden]"),),
|
1819
|
+
): {"display": "none"},
|
1820
|
+
# Inline elements
|
1613
1821
|
(
|
1614
|
-
CssSelector(item="
|
1615
|
-
CssSelector(item="::after"),
|
1616
|
-
|
1617
|
-
|
1618
|
-
|
1619
|
-
|
1620
|
-
|
1621
|
-
|
1622
|
-
|
1623
|
-
|
1624
|
-
|
1625
|
-
|
1626
|
-
|
1627
|
-
|
1628
|
-
|
1629
|
-
|
1630
|
-
|
1631
|
-
|
1632
|
-
|
1633
|
-
|
1634
|
-
|
1635
|
-
|
1636
|
-
|
1637
|
-
|
1638
|
-
|
1639
|
-
|
1640
|
-
|
1641
|
-
|
1642
|
-
|
1643
|
-
|
1644
|
-
|
1645
|
-
|
1646
|
-
|
1647
|
-
|
1648
|
-
|
1649
|
-
|
1650
|
-
"
|
1651
|
-
|
1652
|
-
|
1653
|
-
|
1654
|
-
|
1655
|
-
"
|
1656
|
-
|
1657
|
-
|
1658
|
-
|
1659
|
-
"border_bottom_width": "2px",
|
1660
|
-
"border_left_width": "2px",
|
1661
|
-
"vertical_align": "middle",
|
1662
|
-
},
|
1663
|
-
((CssSelector(item="input", attr="[type=text]"),),): {
|
1664
|
-
"background_color": "#FFFFFF",
|
1665
|
-
"border_top_color": "#606060",
|
1666
|
-
"border_right_color": "#E9E7E3",
|
1667
|
-
"border_bottom_color": "#E9E7E3",
|
1668
|
-
"border_left_color": "#606060",
|
1669
|
-
"overflow_x": "hidden",
|
1670
|
-
},
|
1671
|
-
(
|
1672
|
-
(CssSelector(item="input", attr="[type=button]"),),
|
1673
|
-
(CssSelector(item="input", attr="[type=submit]"),),
|
1674
|
-
(CssSelector(item="input", attr="[type=reset]"),),
|
1675
|
-
): {
|
1676
|
-
"background_color": "#d4d0c8",
|
1677
|
-
"border_right": "#606060",
|
1678
|
-
"border_bottom": "#606060",
|
1679
|
-
"border_left": "#ffffff",
|
1680
|
-
"border_top": "#ffffff",
|
1681
|
-
},
|
1682
|
-
((CssSelector(item="button"),),): {
|
1683
|
-
"display": "inline-block",
|
1684
|
-
"color": "#000000",
|
1685
|
-
"border_top_style": "outset",
|
1686
|
-
"border_right_style": "outset",
|
1687
|
-
"border_bottom_style": "outset",
|
1688
|
-
"border_left_style": "outset",
|
1689
|
-
"border_top_width": "2px",
|
1690
|
-
"border_right_width": "2px",
|
1691
|
-
"border_bottom_width": "2px",
|
1692
|
-
"border_left_width": "2px",
|
1693
|
-
"background_color": "#d4d0c8",
|
1694
|
-
"border_right": "#606060",
|
1695
|
-
"border_bottom": "#606060",
|
1696
|
-
"border_left": "#ffffff",
|
1697
|
-
"border_top": "#ffffff",
|
1698
|
-
},
|
1699
|
-
# Headings
|
1700
|
-
((CssSelector(item="h1"),),): {
|
1701
|
-
"font_weight": "bold",
|
1702
|
-
"text_decoration": "underline",
|
1703
|
-
"border_bottom_style": "solid",
|
1704
|
-
"border_bottom_width": "thick",
|
1705
|
-
"padding_bottom": "2rem",
|
1706
|
-
"margin_top": "2rem",
|
1707
|
-
"margin_bottom": "2em",
|
1708
|
-
},
|
1709
|
-
((CssSelector(item="h2"),),): {
|
1710
|
-
"font_weight": "bold",
|
1711
|
-
"border_bottom_style": "double",
|
1712
|
-
"border_bottom_width": "thick",
|
1713
|
-
"padding_bottom": "1.5rem",
|
1714
|
-
"margin_top": "1.5rem",
|
1715
|
-
"margin_bottom": "1.5rem",
|
1716
|
-
},
|
1717
|
-
((CssSelector(item="h3"),),): {
|
1718
|
-
"font_weight": "bold",
|
1719
|
-
"font_style": "italic",
|
1720
|
-
"border_bottom_style": ":lower-left",
|
1721
|
-
"border_bottom_width": "thin",
|
1722
|
-
"padding_top": "1rem",
|
1723
|
-
"padding_bottom": "1rem",
|
1724
|
-
"margin_bottom": "1.5rem",
|
1725
|
-
},
|
1726
|
-
((CssSelector(item="h4"),),): {
|
1727
|
-
"text_decoration": "underline",
|
1728
|
-
"border_bottom_style": "solid",
|
1729
|
-
"border_bottom_width": "thin",
|
1730
|
-
"padding_top": "1rem",
|
1731
|
-
"padding_bottom": "1rem",
|
1732
|
-
"margin_bottom": "1.5rem",
|
1733
|
-
},
|
1734
|
-
((CssSelector(item="h5"),),): {
|
1735
|
-
"border_bottom_style": "dashed",
|
1736
|
-
"border_bottom_width": "thin",
|
1737
|
-
"margin_bottom": "1.5rem",
|
1738
|
-
},
|
1739
|
-
((CssSelector(item="h6"),),): {
|
1740
|
-
"font_style": "italic",
|
1741
|
-
"border_bottom_style": "dotted",
|
1742
|
-
"border_bottom_width": "thin",
|
1743
|
-
"margin_bottom": "1.5rem",
|
1744
|
-
},
|
1745
|
-
# Misc blocks
|
1746
|
-
((CssSelector(item="blockquote"),),): {
|
1747
|
-
"margin_top": "1em",
|
1748
|
-
"margin_bottom": "1em",
|
1749
|
-
"margin_right": "2em",
|
1750
|
-
"margin_left": "2em",
|
1751
|
-
},
|
1752
|
-
((CssSelector(item="hr"),),): {
|
1753
|
-
"margin_top": "1rem",
|
1754
|
-
"margin_bottom": "1rem",
|
1755
|
-
"border_top_width": "thin",
|
1756
|
-
"border_top_style": "solid",
|
1757
|
-
"border_top_color": "ansired",
|
1758
|
-
},
|
1759
|
-
((CssSelector(item="p"),),): {"margin_top": "1em", "margin_bottom": "1em"},
|
1760
|
-
((CssSelector(item="pre"),),): {
|
1761
|
-
"margin_top": "1em",
|
1762
|
-
"margin_bottom": "1em",
|
1763
|
-
"white_space": "pre",
|
1764
|
-
},
|
1765
|
-
# Lists
|
1766
|
-
((CssSelector(item="::marker"),),): {
|
1767
|
-
"display": "inline",
|
1768
|
-
"padding_right": "1em",
|
1769
|
-
"text_align": "right",
|
1770
|
-
},
|
1771
|
-
((CssSelector(item="ol"),),): {
|
1772
|
-
"list_style_type": "decimal",
|
1773
|
-
"list_style_position": "outside",
|
1774
|
-
"padding_left": "4em",
|
1775
|
-
"margin_top": "1em",
|
1776
|
-
"margin_bottom": "1em",
|
1777
|
-
},
|
1778
|
-
(
|
1779
|
-
(CssSelector(item="ul"),),
|
1780
|
-
(CssSelector(item="menu"),),
|
1781
|
-
(CssSelector(item="dir"),),
|
1782
|
-
): {
|
1783
|
-
"list_style_type": "disc",
|
1784
|
-
"list_style_position": "outside",
|
1785
|
-
"padding_left": "3em",
|
1786
|
-
"margin_top": "1em",
|
1787
|
-
"margin_bottom": "1em",
|
1788
|
-
},
|
1789
|
-
(
|
1790
|
-
(CssSelector(item="dir"), CssSelector(item="dir")),
|
1791
|
-
(CssSelector(item="dir"), CssSelector(item="menu")),
|
1792
|
-
(CssSelector(item="dir"), CssSelector(item="ul")),
|
1793
|
-
(CssSelector(item="ol"), CssSelector(item="dir")),
|
1794
|
-
(CssSelector(item="ol"), CssSelector(item="menu")),
|
1795
|
-
(CssSelector(item="ol"), CssSelector(item="ul")),
|
1796
|
-
(CssSelector(item="menu"), CssSelector(item="dir")),
|
1797
|
-
(CssSelector(item="menu"), CssSelector(item="menu")),
|
1798
|
-
(CssSelector(item="ul"), CssSelector(item="dir")),
|
1799
|
-
(CssSelector(item="ul"), CssSelector(item="menu")),
|
1800
|
-
(CssSelector(item="ul"), CssSelector(item="ul")),
|
1801
|
-
): {"margin_top": "0em", "margin_bottom": "0em", "list_style_type": "circle"},
|
1802
|
-
(
|
1803
|
-
(CssSelector(item="dir"), CssSelector(item="dl")),
|
1804
|
-
(CssSelector(item="dir"), CssSelector(item="ol")),
|
1805
|
-
(CssSelector(item="dl"), CssSelector(item="dir")),
|
1806
|
-
(CssSelector(item="dl"), CssSelector(item="dl")),
|
1807
|
-
(CssSelector(item="dl"), CssSelector(item="ol")),
|
1808
|
-
(CssSelector(item="dl"), CssSelector(item="menu")),
|
1809
|
-
(CssSelector(item="dl"), CssSelector(item="ul")),
|
1810
|
-
(CssSelector(item="ol"), CssSelector(item="dl")),
|
1811
|
-
(CssSelector(item="ol"), CssSelector(item="ol")),
|
1812
|
-
(CssSelector(item="menu"), CssSelector(item="dl")),
|
1813
|
-
(CssSelector(item="menu"), CssSelector(item="ol")),
|
1814
|
-
(CssSelector(item="ul"), CssSelector(item="dl")),
|
1815
|
-
(CssSelector(item="ul"), CssSelector(item="ol")),
|
1816
|
-
): {"margin_top": "0em", "margin_bottom": "0em"},
|
1817
|
-
((CssSelector(item="menu"), CssSelector(item="ul")),): {
|
1818
|
-
"list_style_type": "circle",
|
1819
|
-
"margin_top": "0em",
|
1820
|
-
"margin_bottom": "0em",
|
1821
|
-
},
|
1822
|
-
(
|
1823
|
-
(CssSelector(item="dir"), CssSelector(item="dir"), CssSelector(item="dir")),
|
1824
|
-
(CssSelector(item="dir"), CssSelector(item="dir"), CssSelector(item="menu")),
|
1825
|
-
(CssSelector(item="dir"), CssSelector(item="dir"), CssSelector(item="ul")),
|
1826
|
-
(CssSelector(item="dir"), CssSelector(item="menu"), CssSelector(item="dir")),
|
1827
|
-
(CssSelector(item="dir"), CssSelector(item="menu"), CssSelector(item="menu")),
|
1828
|
-
(CssSelector(item="dir"), CssSelector(item="menu"), CssSelector(item="ul")),
|
1829
|
-
(CssSelector(item="dir"), CssSelector(item="ol"), CssSelector(item="dir")),
|
1830
|
-
(CssSelector(item="dir"), CssSelector(item="ol"), CssSelector(item="menu")),
|
1831
|
-
(CssSelector(item="dir"), CssSelector(item="ol"), CssSelector(item="ul")),
|
1832
|
-
(CssSelector(item="dir"), CssSelector(item="ul"), CssSelector(item="dir")),
|
1833
|
-
(CssSelector(item="dir"), CssSelector(item="ul"), CssSelector(item="menu")),
|
1834
|
-
(CssSelector(item="dir"), CssSelector(item="ul"), CssSelector(item="ul")),
|
1835
|
-
(CssSelector(item="menu"), CssSelector(item="dir"), CssSelector(item="dir")),
|
1836
|
-
(CssSelector(item="menu"), CssSelector(item="dir"), CssSelector(item="menu")),
|
1837
|
-
(CssSelector(item="menu"), CssSelector(item="dir"), CssSelector(item="ul")),
|
1838
|
-
(CssSelector(item="menu"), CssSelector(item="menu"), CssSelector(item="dir")),
|
1839
|
-
(CssSelector(item="menu"), CssSelector(item="menu"), CssSelector(item="menu")),
|
1840
|
-
(CssSelector(item="menu"), CssSelector(item="menu"), CssSelector(item="ul")),
|
1841
|
-
(CssSelector(item="menu"), CssSelector(item="ol"), CssSelector(item="dir")),
|
1842
|
-
(CssSelector(item="menu"), CssSelector(item="ol"), CssSelector(item="menu")),
|
1843
|
-
(CssSelector(item="menu"), CssSelector(item="ol"), CssSelector(item="ul")),
|
1844
|
-
(CssSelector(item="menu"), CssSelector(item="ul"), CssSelector(item="dir")),
|
1845
|
-
(CssSelector(item="menu"), CssSelector(item="ul"), CssSelector(item="menu")),
|
1846
|
-
(CssSelector(item="menu"), CssSelector(item="ul"), CssSelector(item="ul")),
|
1847
|
-
(CssSelector(item="ol"), CssSelector(item="dir"), CssSelector(item="dir")),
|
1848
|
-
(CssSelector(item="ol"), CssSelector(item="dir"), CssSelector(item="menu")),
|
1849
|
-
(CssSelector(item="ol"), CssSelector(item="dir"), CssSelector(item="ul")),
|
1850
|
-
(CssSelector(item="ol"), CssSelector(item="menu"), CssSelector(item="dir")),
|
1851
|
-
(CssSelector(item="ol"), CssSelector(item="menu"), CssSelector(item="menu")),
|
1852
|
-
(CssSelector(item="ol"), CssSelector(item="menu"), CssSelector(item="ul")),
|
1853
|
-
(CssSelector(item="ol"), CssSelector(item="ol"), CssSelector(item="dir")),
|
1854
|
-
(CssSelector(item="ol"), CssSelector(item="ol"), CssSelector(item="menu")),
|
1855
|
-
(CssSelector(item="ol"), CssSelector(item="ol"), CssSelector(item="ul")),
|
1856
|
-
(CssSelector(item="ol"), CssSelector(item="ul"), CssSelector(item="dir")),
|
1857
|
-
(CssSelector(item="ol"), CssSelector(item="ul"), CssSelector(item="menu")),
|
1858
|
-
(CssSelector(item="ol"), CssSelector(item="ul"), CssSelector(item="ul")),
|
1859
|
-
(CssSelector(item="ul"), CssSelector(item="dir"), CssSelector(item="dir")),
|
1860
|
-
(CssSelector(item="ul"), CssSelector(item="dir"), CssSelector(item="menu")),
|
1861
|
-
(CssSelector(item="ul"), CssSelector(item="dir"), CssSelector(item="ul")),
|
1862
|
-
(CssSelector(item="ul"), CssSelector(item="menu"), CssSelector(item="dir")),
|
1863
|
-
(CssSelector(item="ul"), CssSelector(item="menu"), CssSelector(item="menu")),
|
1864
|
-
(CssSelector(item="ul"), CssSelector(item="menu"), CssSelector(item="ul")),
|
1865
|
-
(CssSelector(item="ul"), CssSelector(item="ol"), CssSelector(item="dir")),
|
1866
|
-
(CssSelector(item="ul"), CssSelector(item="ol"), CssSelector(item="menu")),
|
1867
|
-
(CssSelector(item="ul"), CssSelector(item="ol"), CssSelector(item="ul")),
|
1868
|
-
(CssSelector(item="ul"), CssSelector(item="ul"), CssSelector(item="dir")),
|
1869
|
-
(CssSelector(item="ul"), CssSelector(item="ul"), CssSelector(item="menu")),
|
1870
|
-
(CssSelector(item="ul"), CssSelector(item="ul"), CssSelector(item="ul")),
|
1871
|
-
): {"list_style_type": "square"},
|
1872
|
-
((CssSelector(item="li"),),): {"display": "list-item"},
|
1873
|
-
((CssSelector(item="details"),),): {
|
1874
|
-
"list_style_type": "disclosure-closed",
|
1875
|
-
"list_style_position": "inside",
|
1876
|
-
},
|
1877
|
-
((CssSelector(item="details", attr="[open]"), CssSelector(item="summary")),): {
|
1878
|
-
"list_style_type": "disclosure-open"
|
1879
|
-
},
|
1880
|
-
((CssSelector(item="summary"),),): {"display": "list-item", "font_weight": "bold"},
|
1881
|
-
# Dataframes for Jupyter
|
1882
|
-
((CssSelector(item=".dataframe"),),): {"_pt_class": "dataframe"},
|
1883
|
-
(
|
1884
|
-
(CssSelector(item=".dataframe"), CssSelector(item="td")),
|
1885
|
-
(CssSelector(item=".dataframe"), CssSelector(item="th")),
|
1886
|
-
): {
|
1887
|
-
"border_top_style": "hidden",
|
1888
|
-
"border_left_style": "hidden",
|
1889
|
-
"border_bottom_style": "hidden",
|
1890
|
-
"border_right_style": "hidden",
|
1891
|
-
"padding_left": "1em",
|
1892
|
-
},
|
1893
|
-
(
|
1822
|
+
(CssSelector(item="::before"),),
|
1823
|
+
(CssSelector(item="::after"),),
|
1824
|
+
(CssSelector(item="text"),),
|
1825
|
+
(CssSelector(item="abbr"),),
|
1826
|
+
(CssSelector(item="acronym"),),
|
1827
|
+
(CssSelector(item="audio"),),
|
1828
|
+
(CssSelector(item="bdi"),),
|
1829
|
+
(CssSelector(item="bdo"),),
|
1830
|
+
(CssSelector(item="big"),),
|
1831
|
+
(CssSelector(item="br"),),
|
1832
|
+
(CssSelector(item="canvas"),),
|
1833
|
+
(CssSelector(item="data"),),
|
1834
|
+
(CssSelector(item="datalist"),),
|
1835
|
+
(CssSelector(item="embed"),),
|
1836
|
+
(CssSelector(item="iframe"),),
|
1837
|
+
(CssSelector(item="label"),),
|
1838
|
+
(CssSelector(item="map"),),
|
1839
|
+
(CssSelector(item="meter"),),
|
1840
|
+
(CssSelector(item="object"),),
|
1841
|
+
(CssSelector(item="output"),),
|
1842
|
+
(CssSelector(item="picture"),),
|
1843
|
+
(CssSelector(item="progress"),),
|
1844
|
+
(CssSelector(item="q"),),
|
1845
|
+
(CssSelector(item="ruby"),),
|
1846
|
+
(CssSelector(item="select"),),
|
1847
|
+
(CssSelector(item="slot"),),
|
1848
|
+
(CssSelector(item="small"),),
|
1849
|
+
(CssSelector(item="span"),),
|
1850
|
+
(CssSelector(item="template"),),
|
1851
|
+
(CssSelector(item="textarea"),),
|
1852
|
+
(CssSelector(item="time"),),
|
1853
|
+
(CssSelector(item="tt"),),
|
1854
|
+
(CssSelector(item="video"),),
|
1855
|
+
(CssSelector(item="wbr"),),
|
1856
|
+
): {"display": "inline"},
|
1857
|
+
# Formatted inlines
|
1858
|
+
((CssSelector(item="a"),),): {
|
1859
|
+
"display": "inline",
|
1860
|
+
"text_decoration": "underline",
|
1861
|
+
"color": "ansibrightblue",
|
1862
|
+
},
|
1863
|
+
((CssSelector(item="b"),), (CssSelector(item="strong"),)): {
|
1864
|
+
"display": "inline",
|
1865
|
+
"font_weight": "bold",
|
1866
|
+
},
|
1894
1867
|
(
|
1895
|
-
CssSelector(item="
|
1896
|
-
CssSelector(item="
|
1897
|
-
|
1898
|
-
|
1899
|
-
|
1900
|
-
|
1901
|
-
|
1902
|
-
|
1903
|
-
|
1904
|
-
(CssSelector(item="
|
1905
|
-
|
1906
|
-
|
1907
|
-
|
1908
|
-
|
1909
|
-
|
1868
|
+
(CssSelector(item="cite"),),
|
1869
|
+
(CssSelector(item="dfn"),),
|
1870
|
+
(CssSelector(item="em"),),
|
1871
|
+
(CssSelector(item="i"),),
|
1872
|
+
(CssSelector(item="var"),),
|
1873
|
+
): {"display": "inline", "font_style": "italic"},
|
1874
|
+
((CssSelector(item="code"),),): {
|
1875
|
+
"display": "inline",
|
1876
|
+
},
|
1877
|
+
((CssSelector(item="del"),), (CssSelector(item="s"),)): {
|
1878
|
+
"display": "inline",
|
1879
|
+
"text_decoration": "line-through",
|
1880
|
+
},
|
1881
|
+
(
|
1882
|
+
(CssSelector(item="img", attr="[width=0]"),),
|
1883
|
+
(CssSelector(item="img", attr="[height=0]"),),
|
1884
|
+
(CssSelector(item="svg", attr="[width=0]"),),
|
1885
|
+
(CssSelector(item="svg", attr="[height=0]"),),
|
1886
|
+
): {"display": "none"},
|
1887
|
+
((CssSelector(item="img"),), (CssSelector(item="svg"),)): {
|
1888
|
+
"display": "inline-block",
|
1889
|
+
"overflow_x": "hidden",
|
1890
|
+
"overflow_y": "hidden",
|
1891
|
+
},
|
1892
|
+
((CssSelector(item="ins"),), (CssSelector(item="u"),)): {
|
1893
|
+
"display": "inline",
|
1894
|
+
"text_decoration": "underline",
|
1895
|
+
},
|
1896
|
+
((CssSelector(item="kbd"),),): {
|
1897
|
+
"display": "inline",
|
1898
|
+
"background_color": "#333344",
|
1899
|
+
"color": "#FFFFFF",
|
1900
|
+
},
|
1901
|
+
((CssSelector(item="mark"),),): {
|
1902
|
+
"display": "inline",
|
1903
|
+
"color": "black",
|
1904
|
+
"background_color": "#FFFF00",
|
1905
|
+
},
|
1906
|
+
((CssSelector(item="samp"),),): {
|
1907
|
+
"display": "inline",
|
1908
|
+
"background_color": "#334433",
|
1909
|
+
"color": "#FFFFFF",
|
1910
|
+
},
|
1911
|
+
((CssSelector(item="sub"),),): {"display": "inline", "vertical_align": "sub"},
|
1912
|
+
((CssSelector(item="sup"),),): {"display": "inline", "vertical_align": "super"},
|
1913
|
+
(
|
1914
|
+
(
|
1915
|
+
CssSelector(item="q"),
|
1916
|
+
CssSelector(item="::before"),
|
1917
|
+
),
|
1918
|
+
): {"content": "“"},
|
1910
1919
|
(
|
1911
|
-
|
1912
|
-
|
1913
|
-
|
1914
|
-
|
1915
|
-
|
1920
|
+
(
|
1921
|
+
CssSelector(item="q"),
|
1922
|
+
CssSelector(item="::after"),
|
1923
|
+
),
|
1924
|
+
): {"content": "”"},
|
1925
|
+
# Images
|
1926
|
+
(
|
1927
|
+
(CssSelector(item="img", attr="[_missing]"),),
|
1928
|
+
(CssSelector(item="svg", attr="[_missing]"),),
|
1929
|
+
): {
|
1930
|
+
"border_top_style": "solid",
|
1931
|
+
"border_right_style": "solid",
|
1932
|
+
"border_bottom_style": "solid",
|
1933
|
+
"border_left_style": "solid",
|
1934
|
+
"border_top_width": "1px",
|
1935
|
+
"border_right_width": "1px",
|
1936
|
+
"border_bottom_width": "1px",
|
1937
|
+
"border_left_width": "1px",
|
1938
|
+
},
|
1939
|
+
# Alignment
|
1940
|
+
((CssSelector(item="center"),), (CssSelector(item="caption"),)): {
|
1941
|
+
"text_align": "center",
|
1942
|
+
"display": "block",
|
1943
|
+
},
|
1944
|
+
# Tables
|
1945
|
+
((CssSelector(item="table"),),): {
|
1946
|
+
"display": "table",
|
1947
|
+
"border_collapse": "collapse",
|
1948
|
+
},
|
1949
|
+
(
|
1950
|
+
(CssSelector(item="td"),),
|
1951
|
+
(CssSelector(item="th"),),
|
1952
|
+
): {
|
1953
|
+
"border_top_width": "0px",
|
1954
|
+
"border_right_width": "0px",
|
1955
|
+
"border_bottom_width": "0px",
|
1956
|
+
"border_left_width": "0px",
|
1957
|
+
},
|
1958
|
+
((CssSelector(item="td"),),): {"display": "table-cell", "text_align": "unset"},
|
1959
|
+
((CssSelector(item="th"),),): {
|
1960
|
+
"display": "table-cell",
|
1961
|
+
"font_weight": "bold",
|
1962
|
+
"text_align": "center",
|
1963
|
+
},
|
1964
|
+
# Forms
|
1965
|
+
((CssSelector(item="input"),),): {
|
1966
|
+
"display": "inline-block",
|
1967
|
+
"white_space": "pre",
|
1968
|
+
"color": "#000000",
|
1969
|
+
"border_top_style": "inset",
|
1970
|
+
"border_right_style": "inset",
|
1971
|
+
"border_bottom_style": "inset",
|
1972
|
+
"border_left_style": "inset",
|
1973
|
+
"border_top_width": "2px",
|
1974
|
+
"border_right_width": "2px",
|
1975
|
+
"border_bottom_width": "2px",
|
1976
|
+
"border_left_width": "2px",
|
1977
|
+
"vertical_align": "middle",
|
1978
|
+
},
|
1979
|
+
((CssSelector(item="input", attr="[type=text]"),),): {
|
1980
|
+
"background_color": "#FAFAFA",
|
1981
|
+
"border_top_color": "#606060",
|
1982
|
+
"border_right_color": "#E9E7E3",
|
1983
|
+
"border_bottom_color": "#E9E7E3",
|
1984
|
+
"border_left_color": "#606060",
|
1985
|
+
"overflow_x": "hidden",
|
1986
|
+
},
|
1987
|
+
(
|
1988
|
+
(CssSelector(item="input", attr="[type=button]"),),
|
1989
|
+
(CssSelector(item="input", attr="[type=submit]"),),
|
1990
|
+
(CssSelector(item="input", attr="[type=reset]"),),
|
1991
|
+
): {
|
1992
|
+
"background_color": "#d4d0c8",
|
1993
|
+
"border_right": "#606060",
|
1994
|
+
"border_bottom": "#606060",
|
1995
|
+
"border_left": "#ffffff",
|
1996
|
+
"border_top": "#ffffff",
|
1997
|
+
},
|
1998
|
+
((CssSelector(item="button"),),): {
|
1999
|
+
"display": "inline-block",
|
2000
|
+
"color": "#000000",
|
2001
|
+
"border_top_style": "outset",
|
2002
|
+
"border_right_style": "outset",
|
2003
|
+
"border_bottom_style": "outset",
|
2004
|
+
"border_left_style": "outset",
|
2005
|
+
"border_top_width": "2px",
|
2006
|
+
"border_right_width": "2px",
|
2007
|
+
"border_bottom_width": "2px",
|
2008
|
+
"border_left_width": "2px",
|
2009
|
+
"background_color": "#d4d0c8",
|
2010
|
+
"border_right": "#606060",
|
2011
|
+
"border_bottom": "#606060",
|
2012
|
+
"border_left": "#ffffff",
|
2013
|
+
"border_top": "#ffffff",
|
2014
|
+
},
|
2015
|
+
# Headings
|
2016
|
+
((CssSelector(item="h1"),),): {
|
2017
|
+
"font_weight": "bold",
|
2018
|
+
"text_decoration": "underline",
|
2019
|
+
"border_bottom_style": "solid",
|
2020
|
+
"border_bottom_width": "thick",
|
2021
|
+
"padding_bottom": "2rem",
|
2022
|
+
"margin_top": "2rem",
|
2023
|
+
"margin_bottom": "2em",
|
2024
|
+
},
|
2025
|
+
((CssSelector(item="h2"),),): {
|
2026
|
+
"font_weight": "bold",
|
2027
|
+
"border_bottom_style": "double",
|
2028
|
+
"border_bottom_width": "thick",
|
2029
|
+
"padding_bottom": "1.5rem",
|
2030
|
+
"margin_top": "1.5rem",
|
2031
|
+
"margin_bottom": "1.5rem",
|
2032
|
+
},
|
2033
|
+
((CssSelector(item="h3"),),): {
|
2034
|
+
"font_weight": "bold",
|
2035
|
+
"font_style": "italic",
|
2036
|
+
"border_bottom_style": ":lower-left",
|
2037
|
+
"border_bottom_width": "thin",
|
2038
|
+
"padding_top": "1rem",
|
2039
|
+
"padding_bottom": "1rem",
|
2040
|
+
"margin_bottom": "1.5rem",
|
2041
|
+
},
|
2042
|
+
((CssSelector(item="h4"),),): {
|
2043
|
+
"text_decoration": "underline",
|
2044
|
+
"border_bottom_style": "solid",
|
2045
|
+
"border_bottom_width": "thin",
|
2046
|
+
"padding_top": "1rem",
|
2047
|
+
"padding_bottom": "1rem",
|
2048
|
+
"margin_bottom": "1.5rem",
|
2049
|
+
},
|
2050
|
+
((CssSelector(item="h5"),),): {
|
2051
|
+
"border_bottom_style": "dashed",
|
2052
|
+
"border_bottom_width": "thin",
|
2053
|
+
"margin_bottom": "1.5rem",
|
2054
|
+
},
|
2055
|
+
((CssSelector(item="h6"),),): {
|
2056
|
+
"font_style": "italic",
|
2057
|
+
"border_bottom_style": "dotted",
|
2058
|
+
"border_bottom_width": "thin",
|
2059
|
+
"margin_bottom": "1.5rem",
|
2060
|
+
},
|
2061
|
+
# Misc blocks
|
2062
|
+
((CssSelector(item="blockquote"),),): {
|
2063
|
+
"margin_top": "1em",
|
2064
|
+
"margin_bottom": "1em",
|
2065
|
+
"margin_right": "2em",
|
2066
|
+
"margin_left": "2em",
|
2067
|
+
},
|
2068
|
+
((CssSelector(item="hr"),),): {
|
2069
|
+
"margin_top": "1rem",
|
2070
|
+
"margin_bottom": "1rem",
|
2071
|
+
"border_top_width": "thin",
|
2072
|
+
"border_top_style": "solid",
|
2073
|
+
"border_top_color": "ansired",
|
2074
|
+
"width": "100%",
|
2075
|
+
},
|
2076
|
+
((CssSelector(item="p"),),): {"margin_top": "1em", "margin_bottom": "1em"},
|
2077
|
+
((CssSelector(item="pre"),),): {
|
2078
|
+
"margin_top": "1em",
|
2079
|
+
"margin_bottom": "1em",
|
2080
|
+
"white_space": "pre",
|
2081
|
+
},
|
2082
|
+
# Lists
|
2083
|
+
((CssSelector(item="::marker"),),): {
|
2084
|
+
"display": "inline",
|
2085
|
+
"padding_right": "1em",
|
2086
|
+
"text_align": "right",
|
2087
|
+
},
|
2088
|
+
((CssSelector(item="ol"),),): {
|
2089
|
+
"list_style_type": "decimal",
|
2090
|
+
"list_style_position": "outside",
|
2091
|
+
"padding_left": "4em",
|
2092
|
+
"margin_top": "1em",
|
2093
|
+
"margin_bottom": "1em",
|
2094
|
+
},
|
2095
|
+
(
|
2096
|
+
(CssSelector(item="ul"),),
|
2097
|
+
(CssSelector(item="menu"),),
|
2098
|
+
(CssSelector(item="dir"),),
|
2099
|
+
): {
|
2100
|
+
"list_style_type": "disc",
|
2101
|
+
"list_style_position": "outside",
|
2102
|
+
"padding_left": "3em",
|
2103
|
+
"margin_top": "1em",
|
2104
|
+
"margin_bottom": "1em",
|
2105
|
+
},
|
2106
|
+
(
|
2107
|
+
(CssSelector(item="dir"), CssSelector(item="dir")),
|
2108
|
+
(CssSelector(item="dir"), CssSelector(item="menu")),
|
2109
|
+
(CssSelector(item="dir"), CssSelector(item="ul")),
|
2110
|
+
(CssSelector(item="ol"), CssSelector(item="dir")),
|
2111
|
+
(CssSelector(item="ol"), CssSelector(item="menu")),
|
2112
|
+
(CssSelector(item="ol"), CssSelector(item="ul")),
|
2113
|
+
(CssSelector(item="menu"), CssSelector(item="dir")),
|
2114
|
+
(CssSelector(item="menu"), CssSelector(item="menu")),
|
2115
|
+
(CssSelector(item="ul"), CssSelector(item="dir")),
|
2116
|
+
(CssSelector(item="ul"), CssSelector(item="menu")),
|
2117
|
+
(CssSelector(item="ul"), CssSelector(item="ul")),
|
2118
|
+
): {"margin_top": "0em", "margin_bottom": "0em", "list_style_type": "circle"},
|
2119
|
+
(
|
2120
|
+
(CssSelector(item="dir"), CssSelector(item="dl")),
|
2121
|
+
(CssSelector(item="dir"), CssSelector(item="ol")),
|
2122
|
+
(CssSelector(item="dl"), CssSelector(item="dir")),
|
2123
|
+
(CssSelector(item="dl"), CssSelector(item="dl")),
|
2124
|
+
(CssSelector(item="dl"), CssSelector(item="ol")),
|
2125
|
+
(CssSelector(item="dl"), CssSelector(item="menu")),
|
2126
|
+
(CssSelector(item="dl"), CssSelector(item="ul")),
|
2127
|
+
(CssSelector(item="ol"), CssSelector(item="dl")),
|
2128
|
+
(CssSelector(item="ol"), CssSelector(item="ol")),
|
2129
|
+
(CssSelector(item="menu"), CssSelector(item="dl")),
|
2130
|
+
(CssSelector(item="menu"), CssSelector(item="ol")),
|
2131
|
+
(CssSelector(item="ul"), CssSelector(item="dl")),
|
2132
|
+
(CssSelector(item="ul"), CssSelector(item="ol")),
|
2133
|
+
): {"margin_top": "0em", "margin_bottom": "0em"},
|
2134
|
+
((CssSelector(item="menu"), CssSelector(item="ul")),): {
|
2135
|
+
"list_style_type": "circle",
|
2136
|
+
"margin_top": "0em",
|
2137
|
+
"margin_bottom": "0em",
|
2138
|
+
},
|
2139
|
+
(
|
2140
|
+
(CssSelector(item="dir"), CssSelector(item="dir"), CssSelector(item="dir")),
|
2141
|
+
(
|
2142
|
+
CssSelector(item="dir"),
|
2143
|
+
CssSelector(item="dir"),
|
2144
|
+
CssSelector(item="menu"),
|
2145
|
+
),
|
2146
|
+
(CssSelector(item="dir"), CssSelector(item="dir"), CssSelector(item="ul")),
|
2147
|
+
(
|
2148
|
+
CssSelector(item="dir"),
|
2149
|
+
CssSelector(item="menu"),
|
2150
|
+
CssSelector(item="dir"),
|
2151
|
+
),
|
2152
|
+
(
|
2153
|
+
CssSelector(item="dir"),
|
2154
|
+
CssSelector(item="menu"),
|
2155
|
+
CssSelector(item="menu"),
|
2156
|
+
),
|
2157
|
+
(CssSelector(item="dir"), CssSelector(item="menu"), CssSelector(item="ul")),
|
2158
|
+
(CssSelector(item="dir"), CssSelector(item="ol"), CssSelector(item="dir")),
|
2159
|
+
(CssSelector(item="dir"), CssSelector(item="ol"), CssSelector(item="menu")),
|
2160
|
+
(CssSelector(item="dir"), CssSelector(item="ol"), CssSelector(item="ul")),
|
2161
|
+
(CssSelector(item="dir"), CssSelector(item="ul"), CssSelector(item="dir")),
|
2162
|
+
(CssSelector(item="dir"), CssSelector(item="ul"), CssSelector(item="menu")),
|
2163
|
+
(CssSelector(item="dir"), CssSelector(item="ul"), CssSelector(item="ul")),
|
2164
|
+
(
|
2165
|
+
CssSelector(item="menu"),
|
2166
|
+
CssSelector(item="dir"),
|
2167
|
+
CssSelector(item="dir"),
|
2168
|
+
),
|
2169
|
+
(
|
2170
|
+
CssSelector(item="menu"),
|
2171
|
+
CssSelector(item="dir"),
|
2172
|
+
CssSelector(item="menu"),
|
2173
|
+
),
|
2174
|
+
(CssSelector(item="menu"), CssSelector(item="dir"), CssSelector(item="ul")),
|
2175
|
+
(
|
2176
|
+
CssSelector(item="menu"),
|
2177
|
+
CssSelector(item="menu"),
|
2178
|
+
CssSelector(item="dir"),
|
2179
|
+
),
|
2180
|
+
(
|
2181
|
+
CssSelector(item="menu"),
|
2182
|
+
CssSelector(item="menu"),
|
2183
|
+
CssSelector(item="menu"),
|
2184
|
+
),
|
2185
|
+
(
|
2186
|
+
CssSelector(item="menu"),
|
2187
|
+
CssSelector(item="menu"),
|
2188
|
+
CssSelector(item="ul"),
|
2189
|
+
),
|
2190
|
+
(CssSelector(item="menu"), CssSelector(item="ol"), CssSelector(item="dir")),
|
2191
|
+
(
|
2192
|
+
CssSelector(item="menu"),
|
2193
|
+
CssSelector(item="ol"),
|
2194
|
+
CssSelector(item="menu"),
|
2195
|
+
),
|
2196
|
+
(CssSelector(item="menu"), CssSelector(item="ol"), CssSelector(item="ul")),
|
2197
|
+
(CssSelector(item="menu"), CssSelector(item="ul"), CssSelector(item="dir")),
|
2198
|
+
(
|
2199
|
+
CssSelector(item="menu"),
|
2200
|
+
CssSelector(item="ul"),
|
2201
|
+
CssSelector(item="menu"),
|
2202
|
+
),
|
2203
|
+
(CssSelector(item="menu"), CssSelector(item="ul"), CssSelector(item="ul")),
|
2204
|
+
(CssSelector(item="ol"), CssSelector(item="dir"), CssSelector(item="dir")),
|
2205
|
+
(CssSelector(item="ol"), CssSelector(item="dir"), CssSelector(item="menu")),
|
2206
|
+
(CssSelector(item="ol"), CssSelector(item="dir"), CssSelector(item="ul")),
|
2207
|
+
(CssSelector(item="ol"), CssSelector(item="menu"), CssSelector(item="dir")),
|
2208
|
+
(
|
2209
|
+
CssSelector(item="ol"),
|
2210
|
+
CssSelector(item="menu"),
|
2211
|
+
CssSelector(item="menu"),
|
2212
|
+
),
|
2213
|
+
(CssSelector(item="ol"), CssSelector(item="menu"), CssSelector(item="ul")),
|
2214
|
+
(CssSelector(item="ol"), CssSelector(item="ol"), CssSelector(item="dir")),
|
2215
|
+
(CssSelector(item="ol"), CssSelector(item="ol"), CssSelector(item="menu")),
|
2216
|
+
(CssSelector(item="ol"), CssSelector(item="ol"), CssSelector(item="ul")),
|
2217
|
+
(CssSelector(item="ol"), CssSelector(item="ul"), CssSelector(item="dir")),
|
2218
|
+
(CssSelector(item="ol"), CssSelector(item="ul"), CssSelector(item="menu")),
|
2219
|
+
(CssSelector(item="ol"), CssSelector(item="ul"), CssSelector(item="ul")),
|
2220
|
+
(CssSelector(item="ul"), CssSelector(item="dir"), CssSelector(item="dir")),
|
2221
|
+
(CssSelector(item="ul"), CssSelector(item="dir"), CssSelector(item="menu")),
|
2222
|
+
(CssSelector(item="ul"), CssSelector(item="dir"), CssSelector(item="ul")),
|
2223
|
+
(CssSelector(item="ul"), CssSelector(item="menu"), CssSelector(item="dir")),
|
2224
|
+
(
|
2225
|
+
CssSelector(item="ul"),
|
2226
|
+
CssSelector(item="menu"),
|
2227
|
+
CssSelector(item="menu"),
|
2228
|
+
),
|
2229
|
+
(CssSelector(item="ul"), CssSelector(item="menu"), CssSelector(item="ul")),
|
2230
|
+
(CssSelector(item="ul"), CssSelector(item="ol"), CssSelector(item="dir")),
|
2231
|
+
(CssSelector(item="ul"), CssSelector(item="ol"), CssSelector(item="menu")),
|
2232
|
+
(CssSelector(item="ul"), CssSelector(item="ol"), CssSelector(item="ul")),
|
2233
|
+
(CssSelector(item="ul"), CssSelector(item="ul"), CssSelector(item="dir")),
|
2234
|
+
(CssSelector(item="ul"), CssSelector(item="ul"), CssSelector(item="menu")),
|
2235
|
+
(CssSelector(item="ul"), CssSelector(item="ul"), CssSelector(item="ul")),
|
2236
|
+
): {"list_style_type": "square"},
|
2237
|
+
((CssSelector(item="li"),),): {"display": "list-item"},
|
2238
|
+
((CssSelector(item="details"),),): {
|
2239
|
+
"list_style_type": "disclosure-closed",
|
2240
|
+
"list_style_position": "inside",
|
2241
|
+
},
|
2242
|
+
((CssSelector(item="details", attr="[open]"), CssSelector(item="summary")),): {
|
2243
|
+
"list_style_type": "disclosure-open"
|
2244
|
+
},
|
2245
|
+
((CssSelector(item="summary"),),): {
|
2246
|
+
"display": "list-item",
|
2247
|
+
"font_weight": "bold",
|
2248
|
+
},
|
2249
|
+
# Dataframes for Jupyter
|
2250
|
+
((CssSelector(item=".dataframe"),),): {"_pt_class": "dataframe"},
|
2251
|
+
(
|
2252
|
+
(CssSelector(item=".dataframe"), CssSelector(item="td")),
|
2253
|
+
(CssSelector(item=".dataframe"), CssSelector(item="th")),
|
2254
|
+
): {
|
2255
|
+
"border_top_style": "hidden",
|
2256
|
+
"border_left_style": "hidden",
|
2257
|
+
"border_bottom_style": "hidden",
|
2258
|
+
"border_right_style": "hidden",
|
2259
|
+
"padding_left": "1em",
|
2260
|
+
},
|
2261
|
+
(
|
2262
|
+
(
|
2263
|
+
CssSelector(item=".dataframe"),
|
2264
|
+
CssSelector(item="th"),
|
2265
|
+
),
|
2266
|
+
): {
|
2267
|
+
"_pt_class": "dataframe,th",
|
2268
|
+
},
|
2269
|
+
(
|
2270
|
+
(
|
2271
|
+
CssSelector(item=".dataframe"),
|
2272
|
+
CssSelector(item="th", pseudo=":first-child"),
|
2273
|
+
),
|
2274
|
+
(
|
2275
|
+
CssSelector(item=".dataframe"),
|
2276
|
+
CssSelector(item="th", pseudo=":last-child"),
|
2277
|
+
),
|
2278
|
+
(
|
2279
|
+
CssSelector(item=".dataframe"),
|
2280
|
+
CssSelector(item="td", pseudo=":last-child"),
|
2281
|
+
),
|
2282
|
+
): {"padding_right": "1em"},
|
2283
|
+
((CssSelector(item=".dataframe"), CssSelector(item="td")),): {
|
2284
|
+
"_pt_class": "dataframe,td bg:default"
|
2285
|
+
},
|
2286
|
+
(
|
2287
|
+
(
|
2288
|
+
CssSelector(item=".dataframe"),
|
2289
|
+
CssSelector(item="tr", pseudo=":nth-child(odd)"),
|
2290
|
+
CssSelector(item="td"),
|
2291
|
+
),
|
2292
|
+
): {"_pt_class": "dataframe,row-odd,td"},
|
2293
|
+
}
|
1916
2294
|
}
|
1917
2295
|
|
1918
2296
|
|
@@ -1942,8 +2320,7 @@ class Node:
|
|
1942
2320
|
self.before: Node | None = None
|
1943
2321
|
self.after: Node | None = None
|
1944
2322
|
|
1945
|
-
|
1946
|
-
self.theme = Theme(self, parent_theme=parent_theme)
|
2323
|
+
self.theme = Theme(self, parent_theme=parent.theme if parent else None)
|
1947
2324
|
|
1948
2325
|
def _outer_html(self, d: int = 0, attrs: bool = True) -> str:
|
1949
2326
|
dd = " " * d
|
@@ -1966,6 +2343,20 @@ class Node:
|
|
1966
2343
|
s += f"{dd}{self.text}"
|
1967
2344
|
return s
|
1968
2345
|
|
2346
|
+
@cached_property
|
2347
|
+
def preceding_text(self) -> str:
|
2348
|
+
"""Return the text preceding this element."""
|
2349
|
+
s = ""
|
2350
|
+
parent = self.parent
|
2351
|
+
while parent and not parent.theme.d_blocky:
|
2352
|
+
parent = parent.parent
|
2353
|
+
if parent:
|
2354
|
+
for node in parent.renderable_descendents:
|
2355
|
+
if node is self:
|
2356
|
+
break
|
2357
|
+
s += node.text
|
2358
|
+
return s
|
2359
|
+
|
1969
2360
|
@cached_property
|
1970
2361
|
def text(self) -> str:
|
1971
2362
|
"""Get the element's computed text."""
|
@@ -1973,21 +2364,64 @@ class Node:
|
|
1973
2364
|
if callable(transform := self.theme.text_transform):
|
1974
2365
|
text = transform(text)
|
1975
2366
|
|
1976
|
-
if not self.theme.preformatted:
|
1977
|
-
|
1978
|
-
|
1979
|
-
|
1980
|
-
|
1981
|
-
|
1982
|
-
|
1983
|
-
|
1984
|
-
|
1985
|
-
|
1986
|
-
|
1987
|
-
|
2367
|
+
# if False and not (preformatted := self.theme.preformatted):
|
2368
|
+
if not (preformatted := self.theme.preformatted):
|
2369
|
+
# 1. All spaces and tabs immediately before and after a line break are ignored
|
2370
|
+
text = re.sub(r"(\s+(?=\n)|(?<=\n)\s+)", "", text, re.MULTILINE)
|
2371
|
+
# 2. All tab characters are handled as space characters
|
2372
|
+
text = text.replace("\t", " ")
|
2373
|
+
# 3. Line breaks are converted to spaces
|
2374
|
+
text = text.replace("\n", " ")
|
2375
|
+
# 4. any space immediately following another space is ignored
|
2376
|
+
# (even across two separate inline elements)
|
2377
|
+
text = re.sub(r"\s\s+", " ", text)
|
2378
|
+
if not (
|
2379
|
+
preceding_text := self.preceding_text
|
2380
|
+
) or preceding_text.endswith(" "):
|
2381
|
+
text = text.lstrip(" ")
|
2382
|
+
# 5. Sequences of spaces at the beginning and end of an element are removed
|
2383
|
+
if text:
|
2384
|
+
parent = self.parent
|
2385
|
+
while (
|
2386
|
+
parent
|
2387
|
+
and parent.is_first_child_element
|
2388
|
+
and not parent.theme.d_blocky
|
2389
|
+
):
|
2390
|
+
parent = parent.parent
|
2391
|
+
if parent and parent.theme.d_blocky:
|
2392
|
+
if text[0] == " " and (self.is_first_child_node):
|
2393
|
+
text = text[1:]
|
2394
|
+
if text:
|
2395
|
+
parent = self.parent
|
2396
|
+
while (
|
2397
|
+
parent
|
2398
|
+
and parent.is_last_child_element
|
2399
|
+
and not parent.theme.d_blocky
|
2400
|
+
):
|
2401
|
+
parent = parent.parent
|
2402
|
+
if text[-1] == " " and (self.is_last_child_node):
|
2403
|
+
text = text[:-1]
|
2404
|
+
|
2405
|
+
# Remove space around text in block contexts
|
2406
|
+
if (
|
2407
|
+
text
|
2408
|
+
and (node := self.prev_node)
|
2409
|
+
and (node.theme.d_blocky or node.name == "br")
|
2410
|
+
):
|
2411
|
+
text = text.lstrip("\x20\x0a\x09\x0c\x0d")
|
2412
|
+
if (
|
2413
|
+
text
|
2414
|
+
and (node := self.next_node)
|
2415
|
+
and (node.theme.d_blocky or node.name == "br")
|
2416
|
+
):
|
2417
|
+
text = text.rstrip(" \t\r\n\x0c")
|
1988
2418
|
|
1989
|
-
|
1990
|
-
|
2419
|
+
elif preformatted and self.is_last_child_node:
|
2420
|
+
# TODO - align tabstops
|
2421
|
+
text = text.replace("\t", " ")
|
2422
|
+
# Remove one trailing newline
|
2423
|
+
if text[-1] == "\n":
|
2424
|
+
text = text[:-1]
|
1991
2425
|
|
1992
2426
|
return text
|
1993
2427
|
|
@@ -1995,6 +2429,35 @@ class Node:
|
|
1995
2429
|
"""Find all child elements of a given tag type."""
|
1996
2430
|
return [element for element in self.contents if element.name == tag]
|
1997
2431
|
|
2432
|
+
@cached_property
|
2433
|
+
def renderable_contents(self) -> list[Node]:
|
2434
|
+
"""List the node's contents including '::before' and '::after' elements."""
|
2435
|
+
# Do not add '::before' and '::after' elements to themselves
|
2436
|
+
if self.name.startswith("::") or self.name == "text":
|
2437
|
+
return self.contents
|
2438
|
+
|
2439
|
+
contents = []
|
2440
|
+
|
2441
|
+
# Add ::before node
|
2442
|
+
before_node = Node(dom=self.dom, name="::before", parent=self)
|
2443
|
+
if text := before_node.theme.theme.get("content", "").strip('"').strip("'"):
|
2444
|
+
before_node.contents.append(
|
2445
|
+
Node(dom=self.dom, name="text", parent=before_node, text=text)
|
2446
|
+
)
|
2447
|
+
contents.append(before_node)
|
2448
|
+
|
2449
|
+
contents.extend(self.contents)
|
2450
|
+
|
2451
|
+
# Add ::after node
|
2452
|
+
after_node = Node(dom=self.dom, name="::after", parent=self)
|
2453
|
+
if text := after_node.theme.theme.get("content", "").strip('"').strip("'"):
|
2454
|
+
after_node.contents.append(
|
2455
|
+
Node(dom=self.dom, name="text", parent=after_node, text=text)
|
2456
|
+
)
|
2457
|
+
contents.append(after_node)
|
2458
|
+
|
2459
|
+
return contents
|
2460
|
+
|
1998
2461
|
@property
|
1999
2462
|
def descendents(self) -> Generator[Node, None, None]:
|
2000
2463
|
"""Yield all descendent elements."""
|
@@ -2002,9 +2465,26 @@ class Node:
|
|
2002
2465
|
yield child
|
2003
2466
|
yield from child.descendents
|
2004
2467
|
|
2468
|
+
@property
|
2469
|
+
def renderable_descendents(self) -> Generator[Node, None, None]:
|
2470
|
+
"""Yield descendents, including pseudo and skipping inline elements."""
|
2471
|
+
for child in self.renderable_contents:
|
2472
|
+
if (
|
2473
|
+
child.theme.d_inline
|
2474
|
+
and child.renderable_contents
|
2475
|
+
and child.name != "text"
|
2476
|
+
):
|
2477
|
+
yield from child.renderable_descendents
|
2478
|
+
# elif (
|
2479
|
+
# child.name == "text" and self.name != "::block" and self.theme.d_blocky
|
2480
|
+
# ):
|
2481
|
+
# yield Node(dom=self.dom, name="::block", parent=self, contents=[child])
|
2482
|
+
else:
|
2483
|
+
yield child
|
2484
|
+
|
2005
2485
|
@cached_property
|
2006
2486
|
def parents(self) -> list[Node]:
|
2007
|
-
"""Yield all
|
2487
|
+
"""Yield all parent elements."""
|
2008
2488
|
parents = []
|
2009
2489
|
parent = self.parent
|
2010
2490
|
while parent is not None:
|
@@ -2157,17 +2637,20 @@ class Node:
|
|
2157
2637
|
class CustomHTMLParser(HTMLParser):
|
2158
2638
|
"""An HTML parser."""
|
2159
2639
|
|
2640
|
+
soup: Node
|
2641
|
+
curr: Node
|
2642
|
+
|
2160
2643
|
def __init__(self, dom: HTML) -> None:
|
2161
2644
|
"""Create a new parser instance."""
|
2162
2645
|
super().__init__()
|
2163
2646
|
self.dom = dom
|
2164
|
-
self.curr = self.soup = Node(name="::root", dom=dom, parent=None, attrs=[])
|
2165
2647
|
|
2166
2648
|
def parse(self, markup: str) -> Node:
|
2167
2649
|
"""Pare HTML markup."""
|
2168
|
-
|
2650
|
+
soup = Node(name="::root", dom=self.dom, parent=None, attrs=[])
|
2651
|
+
self.curr = soup
|
2169
2652
|
self.feed(markup)
|
2170
|
-
return
|
2653
|
+
return soup
|
2171
2654
|
|
2172
2655
|
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
2173
2656
|
"""Open a new element."""
|
@@ -2187,13 +2670,7 @@ class CustomHTMLParser(HTMLParser):
|
|
2187
2670
|
"""Create data (text) elements."""
|
2188
2671
|
self.autoclose()
|
2189
2672
|
self.curr.contents.append(
|
2190
|
-
Node(
|
2191
|
-
dom=self.dom,
|
2192
|
-
name="text",
|
2193
|
-
parent=self.curr,
|
2194
|
-
text=data,
|
2195
|
-
attrs=[],
|
2196
|
-
)
|
2673
|
+
Node(dom=self.dom, name="text", text=data, parent=self.curr, attrs=[])
|
2197
2674
|
)
|
2198
2675
|
|
2199
2676
|
def handle_endtag(self, tag: str) -> None:
|
@@ -2205,67 +2682,20 @@ class CustomHTMLParser(HTMLParser):
|
|
2205
2682
|
self.curr = self.curr.parent
|
2206
2683
|
|
2207
2684
|
|
2208
|
-
def
|
2685
|
+
def parse_style_sheet(css_str: str, dom: HTML) -> None:
|
2209
2686
|
"""Collect all CSS styles from style tags."""
|
2210
|
-
|
2211
|
-
|
2212
|
-
|
2213
|
-
|
2214
|
-
|
2215
|
-
|
2216
|
-
|
2217
|
-
|
2218
|
-
|
2219
|
-
|
2220
|
-
|
2221
|
-
|
2222
|
-
|
2223
|
-
# In case of a <style> tab, load first child's text
|
2224
|
-
elif child.name == "style":
|
2225
|
-
if child.contents:
|
2226
|
-
css_str = child.contents[0].text
|
2227
|
-
else:
|
2228
|
-
continue
|
2229
|
-
# Remove whitespace and newlines
|
2230
|
-
# css_str = css_str.strip().replace("\n", "")
|
2231
|
-
css_str = re.sub(r"\s*\n\s*", " ", css_str)
|
2232
|
-
# Remove comments
|
2233
|
-
css_str = re.sub(r"\/\*[^\*]+\*\/", "", css_str)
|
2234
|
-
# Replace ':before' and ':after' with '::before' and '::after'
|
2235
|
-
css_str = re.sub("(?<!:):(?=before|after)", "::", css_str)
|
2236
|
-
# Allow plain '@media screen' queries
|
2237
|
-
css_str = re.sub(
|
2238
|
-
r"""
|
2239
|
-
@media\s+screen\s*{\s*
|
2240
|
-
(.+? {
|
2241
|
-
(?:[^:}]+\s*:\s*[^;}]+\s*;\s*)*
|
2242
|
-
(?:[^:}]+\s*:\s*[^;}]+\s*;?\s*)?
|
2243
|
-
})
|
2244
|
-
\s*
|
2245
|
-
}
|
2246
|
-
""",
|
2247
|
-
"\\1",
|
2248
|
-
css_str,
|
2249
|
-
0,
|
2250
|
-
flags=re.DOTALL | re.VERBOSE,
|
2251
|
-
)
|
2252
|
-
# Remove other media queries for now - TODO
|
2253
|
-
# TODO - Far too slow sometimes!
|
2254
|
-
css_str = re.sub(
|
2255
|
-
r"""
|
2256
|
-
@media.+?{\s*
|
2257
|
-
(.+? {
|
2258
|
-
(?:[^:}]+\s*:\s*[^;}]+\s*;\s*)*
|
2259
|
-
(?:[^:}]+\s*:\s*[^;}]+\s*;?\s*)?
|
2260
|
-
})
|
2261
|
-
\s*
|
2262
|
-
}
|
2263
|
-
""",
|
2264
|
-
"",
|
2265
|
-
css_str,
|
2266
|
-
0,
|
2267
|
-
flags=re.DOTALL | re.VERBOSE,
|
2268
|
-
)
|
2687
|
+
dom_css = dom.css
|
2688
|
+
# Remove whitespace and newlines
|
2689
|
+
# css_str = css_str.strip().replace("\n", "")
|
2690
|
+
css_str = re.sub(r"\s*\n\s*", " ", css_str)
|
2691
|
+
# Remove comments
|
2692
|
+
css_str = re.sub(r"\/\*[^\*]+\*\/", "", css_str)
|
2693
|
+
# Replace ':before' and ':after' with '::before' and '::after'
|
2694
|
+
# for compatibility with old CSS and to make root selector work
|
2695
|
+
css_str = re.sub("(?<!:):(?=before|after|root)", "::", css_str)
|
2696
|
+
|
2697
|
+
def parse_part(condition: Filter, css_str: str) -> None:
|
2698
|
+
"""Parse a group of CSS rules."""
|
2269
2699
|
if css_str:
|
2270
2700
|
css_str = css_str.replace("\n", "").strip()
|
2271
2701
|
if css_str:
|
@@ -2281,54 +2711,176 @@ def parse_styles(soup: Node, base_url: UPath) -> CssSelectors:
|
|
2281
2711
|
)
|
2282
2712
|
for selector in map(str.strip, selectors.split(","))
|
2283
2713
|
)
|
2714
|
+
rules = dom_css.setdefault(condition, {})
|
2284
2715
|
if parsed_selectors in rules:
|
2285
2716
|
rules[parsed_selectors].update(rule_content)
|
2286
2717
|
else:
|
2287
2718
|
rules[parsed_selectors] = rule_content
|
2288
2719
|
|
2289
|
-
|
2720
|
+
# Split out nested at-rules - we need to process them separately
|
2721
|
+
for part in _AT_RULE_RE.split(css_str):
|
2722
|
+
if (
|
2723
|
+
part
|
2724
|
+
and part[0] == "@"
|
2725
|
+
and (m := _NESTED_AT_RULE_RE.match(part)) is not None
|
2726
|
+
):
|
2727
|
+
m_dict = m.groupdict()
|
2728
|
+
# Process '@media' queries
|
2729
|
+
if m_dict["identifier"] == "media":
|
2730
|
+
# Split each query - separated by "or" or ","
|
2731
|
+
queries = re.split("(?: or |,)", m_dict["rule"])
|
2732
|
+
query_conditions: Filter = never
|
2733
|
+
for query in queries:
|
2734
|
+
# Each query can be the logical sum of multiple targets
|
2735
|
+
target_conditions: Filter = always
|
2736
|
+
for target in query.split(" and "):
|
2737
|
+
if (
|
2738
|
+
target_m := _MEDIA_QUERY_TARGET_RE.match(target)
|
2739
|
+
) is not None:
|
2740
|
+
target_m_dict = target_m.groupdict()
|
2741
|
+
# Check for media type conditions
|
2742
|
+
if (media_type := target_m_dict["type"]) is not None:
|
2743
|
+
target_conditions &= (
|
2744
|
+
always if media_type in {"all", "screen"} else never
|
2745
|
+
)
|
2746
|
+
# Check for media feature confitions
|
2747
|
+
elif (
|
2748
|
+
media_feature := target_m_dict["feature"]
|
2749
|
+
) is not None:
|
2750
|
+
target_conditions &= parse_media_condition(
|
2751
|
+
media_feature, dom
|
2752
|
+
)
|
2753
|
+
# Check for logical 'NOT' inverting the target condition
|
2754
|
+
if target_m_dict["invert"]:
|
2755
|
+
target_conditions = ~target_conditions
|
2756
|
+
# Logical OR of all media queries
|
2757
|
+
query_conditions |= target_conditions
|
2758
|
+
# Parse the @media CSS block
|
2759
|
+
parse_part(query_conditions, m_dict["part"])
|
2760
|
+
# If we are outinside an at-rule block, parse the CSS and always use it
|
2761
|
+
else:
|
2762
|
+
parse_part(always, part)
|
2290
2763
|
|
2291
2764
|
|
2292
|
-
|
2293
|
-
"""
|
2765
|
+
def parse_media_condition(condition: str, dom: HTML) -> Filter:
|
2766
|
+
"""Convert media rules to conditions."""
|
2767
|
+
condition = condition.replace(" ", "")
|
2768
|
+
result: Filter
|
2294
2769
|
|
2295
|
-
|
2296
|
-
|
2770
|
+
for op_str, op_func in (("<=", le), (">=", ge), ("<", lt), (">", gt), ("=", eq)):
|
2771
|
+
if op_str in condition:
|
2772
|
+
operator = op_func
|
2773
|
+
feature, _, value = condition.partition(op_str)
|
2774
|
+
break
|
2775
|
+
else:
|
2776
|
+
feature, _, value = condition.partition(":")
|
2777
|
+
if feature.startswith("max-"):
|
2778
|
+
operator = le
|
2779
|
+
feature = feature[4:]
|
2780
|
+
elif feature.startswith("min-"):
|
2781
|
+
operator = ge
|
2782
|
+
feature = feature[4:]
|
2783
|
+
else:
|
2784
|
+
operator = eq
|
2297
2785
|
|
2298
|
-
|
2786
|
+
if feature == "width":
|
2787
|
+
result = Condition(
|
2788
|
+
lambda: operator(
|
2789
|
+
width := dom.width or 80,
|
2790
|
+
css_dimension(value, vertical=False, available=width) or 80,
|
2791
|
+
)
|
2792
|
+
)
|
2793
|
+
elif feature == "height":
|
2794
|
+
result = Condition(
|
2795
|
+
lambda: operator(
|
2796
|
+
height := dom.height or 24,
|
2797
|
+
css_dimension(value, vertical=True, available=height) or 24,
|
2798
|
+
)
|
2799
|
+
)
|
2800
|
+
elif feature == "aspect":
|
2801
|
+
result = Condition(
|
2802
|
+
lambda: operator(
|
2803
|
+
(width := (dom.width or 80)) / (height := (dom.height or 24)),
|
2804
|
+
(
|
2805
|
+
80
|
2806
|
+
if (dim := css_dimension(value, vertical=False, available=width))
|
2807
|
+
is None
|
2808
|
+
else dim
|
2809
|
+
)
|
2810
|
+
/ (
|
2811
|
+
24
|
2812
|
+
if (dim := css_dimension(value, vertical=True, available=height))
|
2813
|
+
is None
|
2814
|
+
else dim
|
2815
|
+
),
|
2816
|
+
)
|
2817
|
+
)
|
2299
2818
|
|
2300
|
-
|
2301
|
-
|
2302
|
-
|
2303
|
-
|
2304
|
-
|
2305
|
-
|
2306
|
-
|
2307
|
-
|
2308
|
-
|
2309
|
-
|
2310
|
-
|
2311
|
-
|
2312
|
-
|
2313
|
-
|
2314
|
-
|
2315
|
-
|
2316
|
-
|
2317
|
-
|
2318
|
-
|
2819
|
+
elif feature == "device-width":
|
2820
|
+
result = Condition(
|
2821
|
+
lambda: operator(
|
2822
|
+
(
|
2823
|
+
output.get_size()
|
2824
|
+
if (output := get_app_session()._output)
|
2825
|
+
else Size(24, 80)
|
2826
|
+
).columns,
|
2827
|
+
css_dimension(value, vertical=False, available=dom.width) or 80,
|
2828
|
+
)
|
2829
|
+
)
|
2830
|
+
elif feature == "device-height":
|
2831
|
+
result = Condition(
|
2832
|
+
lambda: operator(
|
2833
|
+
(
|
2834
|
+
output.get_size()
|
2835
|
+
if (output := get_app_session()._output)
|
2836
|
+
else Size(24, 80)
|
2837
|
+
).rows,
|
2838
|
+
css_dimension(value, vertical=True, available=dom.height) or 24,
|
2839
|
+
)
|
2840
|
+
)
|
2841
|
+
elif feature == "device-aspect":
|
2842
|
+
result = Condition(
|
2843
|
+
lambda: operator(
|
2844
|
+
(
|
2845
|
+
size := (
|
2846
|
+
output.get_size()
|
2847
|
+
if (output := get_app_session()._output)
|
2848
|
+
else Size(24, 80)
|
2849
|
+
)
|
2850
|
+
).columns
|
2851
|
+
/ size.rows,
|
2852
|
+
(css_dimension(value, vertical=False, available=dom.width) or 1)
|
2853
|
+
/ (css_dimension(value, vertical=True, available=dom.height) or 1),
|
2854
|
+
)
|
2319
2855
|
)
|
2320
2856
|
|
2321
|
-
|
2857
|
+
# elif feature == "color":
|
2858
|
+
# Check output color depth
|
2859
|
+
|
2860
|
+
else:
|
2861
|
+
result = always
|
2862
|
+
|
2863
|
+
return result
|
2864
|
+
|
2865
|
+
|
2866
|
+
class HTML:
|
2867
|
+
"""A HTML formatted text renderer.
|
2868
|
+
|
2869
|
+
Accepts a HTML string and renders it at a given width.
|
2870
|
+
"""
|
2322
2871
|
|
2323
2872
|
def __init__(
|
2324
2873
|
self,
|
2325
2874
|
markup: str,
|
2326
|
-
base:
|
2875
|
+
base: Path | str | None = None,
|
2327
2876
|
width: int | None = None,
|
2328
2877
|
height: int | None = None,
|
2329
2878
|
collapse_root_margin: bool = False,
|
2330
2879
|
fill: bool = True,
|
2880
|
+
css: CssSelectors | None = None,
|
2331
2881
|
browser_css: CssSelectors | None = None,
|
2882
|
+
mouse_handler: Callable[[Node, MouseEvent], NotImplementedOrNone] | None = None,
|
2883
|
+
paste_fixed: bool = True,
|
2332
2884
|
) -> None:
|
2333
2885
|
"""Initialize the markdown formatter.
|
2334
2886
|
|
@@ -2342,87 +2894,120 @@ class HTML:
|
|
2342
2894
|
collapse_root_margin: If :py:const:`True`, margins of the root element will
|
2343
2895
|
be collapsed
|
2344
2896
|
fill: Whether remaining space in block elements should be filled
|
2897
|
+
css: Base CSS to apply when rendering the HTML
|
2345
2898
|
browser_css: The browser CSS to use
|
2899
|
+
mouse_handler: A mouse handler function to use when links are clicked
|
2900
|
+
paste_fixed: Whether fixed elements should be pasted over the output
|
2346
2901
|
|
2347
2902
|
"""
|
2348
|
-
self.
|
2349
|
-
**(_BROWSER_CSS if browser_css is None else browser_css)
|
2350
|
-
}
|
2351
|
-
self.css: CssSelectors = {}
|
2352
|
-
|
2353
|
-
self.markup = markup
|
2903
|
+
self.markup = markup.strip()
|
2354
2904
|
self.base = UPath(base or ".")
|
2355
|
-
self.
|
2356
|
-
|
2357
|
-
self.
|
2905
|
+
self.title = ""
|
2906
|
+
|
2907
|
+
self.browser_css = browser_css or _BROWSER_CSS
|
2908
|
+
self.css: CssSelectors = css or {}
|
2909
|
+
|
2910
|
+
self.render_count = 0
|
2911
|
+
self.width = width
|
2912
|
+
self.height = height
|
2913
|
+
|
2358
2914
|
self.fill = fill
|
2915
|
+
self.collapse_root_margin = collapse_root_margin
|
2916
|
+
self.paste_fixed = paste_fixed
|
2359
2917
|
|
2360
|
-
self.
|
2918
|
+
self.mouse_handler = mouse_handler
|
2361
2919
|
|
2362
|
-
self.
|
2920
|
+
self.formatted_text: StyleAndTextTuples = []
|
2921
|
+
self.floats: dict[tuple[int, DiBool, DiInt], StyleAndTextTuples] = {}
|
2922
|
+
self.fixed: dict[tuple[int, DiBool, DiInt], StyleAndTextTuples] = {}
|
2923
|
+
self.fixed_mask: StyleAndTextTuples = []
|
2924
|
+
# self.images = []
|
2925
|
+
# self.anchors = []
|
2926
|
+
|
2927
|
+
self.assets_loaded = False
|
2928
|
+
|
2929
|
+
# Lazily load attributes
|
2930
|
+
|
2931
|
+
@cached_property
|
2932
|
+
def parser(self) -> CustomHTMLParser:
|
2933
|
+
"""Load the HTML parser."""
|
2934
|
+
return CustomHTMLParser(self)
|
2363
2935
|
|
2364
|
-
|
2365
|
-
|
2936
|
+
@cached_property
|
2937
|
+
def soup(self) -> Node:
|
2938
|
+
"""Parse the markup."""
|
2939
|
+
return self.parser.parse(self.markup)
|
2366
2940
|
|
2367
|
-
|
2368
|
-
|
2941
|
+
def load_assets(self) -> None:
|
2942
|
+
"""Load CSS styles and image resources.
|
2369
2943
|
|
2370
|
-
|
2944
|
+
Do not touch element's themes!
|
2945
|
+
"""
|
2371
2946
|
for child in self.soup.descendents:
|
2372
|
-
|
2373
|
-
|
2374
|
-
|
2947
|
+
# Set title
|
2948
|
+
if child.name == "title":
|
2949
|
+
if contents := child.contents:
|
2950
|
+
self.title = contents[0].text
|
2951
|
+
|
2952
|
+
# In case of a <link> style, load the url
|
2953
|
+
elif (
|
2954
|
+
child.name == "link"
|
2955
|
+
and (
|
2956
|
+
(attrs := child.attrs).get("rel") == "stylesheet"
|
2957
|
+
or (attrs.get("rel") == "preload" and attrs.get("as") == "style")
|
2958
|
+
)
|
2959
|
+
and (href := attrs.get("href", ""))
|
2960
|
+
):
|
2961
|
+
css_path = UPath(urljoin(str(self.base), href))
|
2962
|
+
try:
|
2963
|
+
css_str = css_path.read_text()
|
2964
|
+
except Exception:
|
2965
|
+
log.debug("Could not load file %s", css_path)
|
2966
|
+
else:
|
2967
|
+
parse_style_sheet(css_str, self)
|
2968
|
+
|
2969
|
+
# In case of a <style> tab, load first child's text
|
2970
|
+
elif child.name == "style":
|
2971
|
+
if child.contents:
|
2972
|
+
# Use unprocess text attribute to avoid loading the element's theme
|
2973
|
+
css_str = child.contents[0]._text
|
2974
|
+
parse_style_sheet(css_str, self)
|
2975
|
+
|
2976
|
+
# Load images
|
2977
|
+
elif child.name == "img" and (src := child.attrs.get("src")):
|
2978
|
+
data_path = UPath(urljoin(str(self.base), src))
|
2979
|
+
if data_path.exists():
|
2980
|
+
try:
|
2981
|
+
data = data_path.read_bytes()
|
2982
|
+
except Exception:
|
2983
|
+
log.info("Error loading file '%s'", data_path)
|
2984
|
+
else:
|
2985
|
+
if data:
|
2986
|
+
child.attrs["_data"] = data
|
2375
2987
|
else:
|
2376
2988
|
child.attrs["_missing"] = "true"
|
2377
2989
|
|
2378
|
-
self.
|
2379
|
-
self.fixes: dict[tuple[int, DiInt], StyleAndTextTuples] = {}
|
2380
|
-
|
2381
|
-
# Render the markup
|
2382
|
-
self.render(width, height)
|
2383
|
-
|
2384
|
-
def render_list_item_content(
|
2385
|
-
self,
|
2386
|
-
element: Node,
|
2387
|
-
left: int = 0,
|
2388
|
-
fill: bool = True,
|
2389
|
-
align_content: bool = True,
|
2390
|
-
) -> StyleAndTextTuples:
|
2391
|
-
"""Render a list item."""
|
2392
|
-
# Get element theme
|
2393
|
-
theme = element.theme
|
2394
|
-
# Get the bullet style
|
2395
|
-
list_style = theme.list_style_type
|
2396
|
-
bullet = list_style
|
2397
|
-
if list_style == "decimal":
|
2398
|
-
bullet = f"{element.attrs['value']}."
|
2399
|
-
# Add bullet element
|
2400
|
-
if bullet:
|
2401
|
-
bullet_element = Node(
|
2402
|
-
dom=self,
|
2403
|
-
name="::marker",
|
2404
|
-
parent=element,
|
2405
|
-
contents=[Node(dom=self, name="text", parent=element, text=bullet)],
|
2406
|
-
)
|
2407
|
-
if theme.list_style_position == "inside":
|
2408
|
-
element.contents.insert(0, bullet_element)
|
2409
|
-
else:
|
2410
|
-
element.marker = bullet_element
|
2411
|
-
# Render the list item
|
2412
|
-
ft = self.render_node_content(
|
2413
|
-
element,
|
2414
|
-
left=left,
|
2415
|
-
fill=fill,
|
2416
|
-
align_content=align_content,
|
2417
|
-
)
|
2418
|
-
return ft
|
2990
|
+
self.assets_loaded = True
|
2419
2991
|
|
2420
|
-
def render(self, width: int | None, height: int | None) ->
|
2992
|
+
def render(self, width: int | None, height: int | None) -> StyleAndTextTuples:
|
2421
2993
|
"""Render the current markup at a given size."""
|
2422
|
-
|
2994
|
+
no_w = width is None and self.width is None
|
2995
|
+
no_h = height is None and self.height is None
|
2996
|
+
if no_w or no_h:
|
2423
2997
|
size = get_app_session().output.get_size()
|
2424
|
-
|
2425
|
-
|
2998
|
+
if no_w:
|
2999
|
+
width = size.columns
|
3000
|
+
if no_h:
|
3001
|
+
height = size.rows
|
3002
|
+
if width is not None:
|
3003
|
+
self.width = width
|
3004
|
+
if height is not None:
|
3005
|
+
self.height = height
|
3006
|
+
assert self.width is not None
|
3007
|
+
assert self.height is not None
|
3008
|
+
|
3009
|
+
if not self.assets_loaded:
|
3010
|
+
self.load_assets()
|
2426
3011
|
|
2427
3012
|
ft = self.render_element(
|
2428
3013
|
self.soup,
|
@@ -2431,29 +3016,64 @@ class HTML:
|
|
2431
3016
|
fill=self.fill,
|
2432
3017
|
)
|
2433
3018
|
|
2434
|
-
# Draw floats
|
2435
|
-
for (_, position), float_ft in sorted(self.floats.items()):
|
2436
|
-
row = col = 0
|
2437
|
-
if (top := position.top) is not None:
|
2438
|
-
row = top
|
2439
|
-
elif (bottom := position.bottom) is not None:
|
2440
|
-
row = (
|
2441
|
-
sum(1 for _ in split_lines(ft))
|
2442
|
-
- sum(1 for _ in split_lines(float_ft))
|
2443
|
-
- bottom
|
2444
|
-
)
|
2445
|
-
if (left := position.left) is not None:
|
2446
|
-
col = left
|
2447
|
-
elif (right := position.right) is not None:
|
2448
|
-
row = max_line_width(ft) - max_line_width(float_ft) - right
|
2449
|
-
|
2450
|
-
ft = paste(ft, float_ft, row, col)
|
2451
|
-
|
2452
3019
|
# Apply "ReverseOverwrite"s
|
2453
3020
|
ft = apply_reverse_overwrites(ft)
|
2454
3021
|
|
3022
|
+
# Apply floats and fixed elements
|
3023
|
+
|
3024
|
+
def _paste_floats(
|
3025
|
+
floats: dict[tuple[int, DiBool, DiInt], StyleAndTextTuples],
|
3026
|
+
lower_ft: StyleAndTextTuples,
|
3027
|
+
) -> StyleAndTextTuples:
|
3028
|
+
"""Paste floats on top of rendering."""
|
3029
|
+
lower_ft_height = None
|
3030
|
+
for (_, anchors, position), float_ft in sorted(floats.items()):
|
3031
|
+
row = col = 0
|
3032
|
+
if anchors.top:
|
3033
|
+
row = position.top
|
3034
|
+
elif anchors.bottom:
|
3035
|
+
if lower_ft_height is None:
|
3036
|
+
lower_ft_height = sum(1 for _ in split_lines(lower_ft))
|
3037
|
+
row = (
|
3038
|
+
lower_ft_height
|
3039
|
+
- sum(1 for _ in split_lines(float_ft))
|
3040
|
+
- position.bottom
|
3041
|
+
)
|
3042
|
+
if anchors.left:
|
3043
|
+
col = position.left
|
3044
|
+
elif anchors.right:
|
3045
|
+
row = (
|
3046
|
+
max_line_width(lower_ft)
|
3047
|
+
- max_line_width(float_ft)
|
3048
|
+
- position.right
|
3049
|
+
)
|
3050
|
+
lower_ft = paste(float_ft, lower_ft, row, col)
|
3051
|
+
return lower_ft
|
3052
|
+
|
3053
|
+
# Draw floats
|
3054
|
+
if self.floats:
|
3055
|
+
ft = _paste_floats(self.floats, ft)
|
3056
|
+
|
3057
|
+
# Paste floats onto a mask, then onto the rendering if required
|
3058
|
+
if self.fixed:
|
3059
|
+
assert self.width is not None
|
3060
|
+
assert self.height is not None
|
3061
|
+
fixed_mask = cast(
|
3062
|
+
"StyleAndTextTuples", [("", (" " * self.width) + "\n")] * self.height
|
3063
|
+
)
|
3064
|
+
fixed_mask = _paste_floats(self.fixed, fixed_mask)
|
3065
|
+
fixed_mask = apply_reverse_overwrites(fixed_mask)
|
3066
|
+
if self.paste_fixed:
|
3067
|
+
ft = paste(fixed_mask, ft, 0, 0, transparent=True)
|
3068
|
+
self.fixed_mask = fixed_mask
|
3069
|
+
else:
|
3070
|
+
self.fixed_mask = []
|
3071
|
+
|
3072
|
+
self.render_count += 1
|
2455
3073
|
self.formatted_text = ft
|
2456
3074
|
|
3075
|
+
return ft
|
3076
|
+
|
2457
3077
|
def render_element(
|
2458
3078
|
self,
|
2459
3079
|
element: Node,
|
@@ -2516,6 +3136,63 @@ class HTML:
|
|
2516
3136
|
ft = [(style, text)]
|
2517
3137
|
return ft
|
2518
3138
|
|
3139
|
+
def render_ol_content(
|
3140
|
+
self,
|
3141
|
+
element: Node,
|
3142
|
+
left: int = 0,
|
3143
|
+
fill: bool = True,
|
3144
|
+
align_content: bool = True,
|
3145
|
+
) -> StyleAndTextTuples:
|
3146
|
+
"""Render lists, adding item numbers to child <li> elements."""
|
3147
|
+
# Assign a list index to each item. This can be set via the 'value' attributed
|
3148
|
+
_curr = 0
|
3149
|
+
for item in element.find_all("li"):
|
3150
|
+
_curr += 1
|
3151
|
+
_curr = int(item.attrs.setdefault("value", str(_curr)))
|
3152
|
+
# Render list as normal
|
3153
|
+
return self.render_node_content(
|
3154
|
+
element=element,
|
3155
|
+
left=left,
|
3156
|
+
fill=fill,
|
3157
|
+
align_content=align_content,
|
3158
|
+
)
|
3159
|
+
|
3160
|
+
render_ul_content = render_ol_content
|
3161
|
+
|
3162
|
+
def render_list_item_content(
|
3163
|
+
self,
|
3164
|
+
element: Node,
|
3165
|
+
left: int = 0,
|
3166
|
+
fill: bool = True,
|
3167
|
+
align_content: bool = True,
|
3168
|
+
) -> StyleAndTextTuples:
|
3169
|
+
"""Render a list item."""
|
3170
|
+
# Get element theme
|
3171
|
+
theme = element.theme
|
3172
|
+
# Get the bullet style
|
3173
|
+
list_style = theme.list_style_type
|
3174
|
+
bullet = list_style
|
3175
|
+
if list_style == "decimal":
|
3176
|
+
bullet = f"{element.attrs['value']}."
|
3177
|
+
# Add bullet element
|
3178
|
+
if bullet:
|
3179
|
+
bullet_element = Node(dom=self, name="::marker", parent=element)
|
3180
|
+
bullet_element.contents.append(
|
3181
|
+
Node(dom=self, name="text", parent=bullet_element, text=bullet)
|
3182
|
+
)
|
3183
|
+
if theme.list_style_position == "inside":
|
3184
|
+
element.contents.insert(0, bullet_element)
|
3185
|
+
else:
|
3186
|
+
element.marker = bullet_element
|
3187
|
+
# Render the list item
|
3188
|
+
ft = self.render_node_content(
|
3189
|
+
element,
|
3190
|
+
left=left,
|
3191
|
+
fill=fill,
|
3192
|
+
align_content=align_content,
|
3193
|
+
)
|
3194
|
+
return ft
|
3195
|
+
|
2519
3196
|
def render_table_content(
|
2520
3197
|
self,
|
2521
3198
|
element: Node,
|
@@ -2523,7 +3200,7 @@ class HTML:
|
|
2523
3200
|
fill: bool = True,
|
2524
3201
|
align_content: bool = True,
|
2525
3202
|
) -> StyleAndTextTuples:
|
2526
|
-
"""Render a
|
3203
|
+
"""Render a HTML table element.
|
2527
3204
|
|
2528
3205
|
Args:
|
2529
3206
|
element: The list of parsed elements to render
|
@@ -2542,7 +3219,7 @@ class HTML:
|
|
2542
3219
|
table_x_dim = Dimension(
|
2543
3220
|
min=table_theme.min_width,
|
2544
3221
|
preferred=table_theme.content_width if "width" in table_theme else None,
|
2545
|
-
max=table_theme.max_width or table_theme.
|
3222
|
+
max=table_theme.max_width or table_theme.content_width,
|
2546
3223
|
)
|
2547
3224
|
table = Table(
|
2548
3225
|
align=table_theme.text_align,
|
@@ -2579,7 +3256,6 @@ class HTML:
|
|
2579
3256
|
or table_theme.available_width,
|
2580
3257
|
)
|
2581
3258
|
cell = row.new_cell(
|
2582
|
-
# text=" ",
|
2583
3259
|
text=self.render_node_content(
|
2584
3260
|
td,
|
2585
3261
|
left=0,
|
@@ -2610,18 +3286,19 @@ class HTML:
|
|
2610
3286
|
for child in element.find_all("tfoot", recursive=False):
|
2611
3287
|
render_rows(child.contents)
|
2612
3288
|
|
3289
|
+
# TODO - process <colgroup> elements
|
3290
|
+
|
2613
3291
|
# Add cell contents
|
2614
3292
|
if td_map:
|
2615
3293
|
col_widths = table.calculate_col_widths()
|
2616
|
-
|
2617
3294
|
for row in table.rows:
|
2618
3295
|
for col_width, cell in zip(col_widths, row.cells):
|
2619
3296
|
if td := td_map.get(cell):
|
2620
3297
|
cell_padding = compute_padding(cell)
|
2621
3298
|
available_width = (
|
2622
|
-
|
2623
|
-
|
2624
|
-
- cell_padding.right
|
3299
|
+
table_x_dim.max
|
3300
|
+
if cell.colspan > 1
|
3301
|
+
else col_width - cell_padding.left - cell_padding.right
|
2625
3302
|
)
|
2626
3303
|
td.theme.update_space(
|
2627
3304
|
available_width, table_theme.available_height
|
@@ -2667,9 +3344,9 @@ class HTML:
|
|
2667
3344
|
content_width = theme.content_width
|
2668
3345
|
# content_height = theme.content_height
|
2669
3346
|
src = str(element.attrs.get("src", ""))
|
2670
|
-
path =
|
3347
|
+
path = self.base / src
|
2671
3348
|
|
2672
|
-
if data := element.attrs.get("_data"):
|
3349
|
+
if not element.attrs.get("_missing") and (data := element.attrs.get("_data")):
|
2673
3350
|
# Display it graphically
|
2674
3351
|
format_ = get_format(path, default="png")
|
2675
3352
|
cols, aspect = pixels_to_cell_size(*data_pixel_size(data, format_=format_))
|
@@ -2688,6 +3365,7 @@ class HTML:
|
|
2688
3365
|
to="formatted_text",
|
2689
3366
|
cols=cols,
|
2690
3367
|
rows=rows,
|
3368
|
+
fg=theme.color,
|
2691
3369
|
bg=theme.background_color,
|
2692
3370
|
path=path,
|
2693
3371
|
)
|
@@ -2699,18 +3377,21 @@ class HTML:
|
|
2699
3377
|
ft = [(f"{theme.style} {x[0]}", *x[1:]) for x in ft]
|
2700
3378
|
|
2701
3379
|
else:
|
2702
|
-
|
2703
|
-
|
2704
|
-
if
|
2705
|
-
|
2706
|
-
|
2707
|
-
|
2708
|
-
|
2709
|
-
|
2710
|
-
(
|
2711
|
-
|
2712
|
-
|
2713
|
-
|
3380
|
+
style = f"class:image,placeholder {theme.style}"
|
3381
|
+
ft = [(style, "🌄")]
|
3382
|
+
if content_width and content_width >= 7:
|
3383
|
+
ft.extend(
|
3384
|
+
[
|
3385
|
+
(style, " "),
|
3386
|
+
(
|
3387
|
+
style,
|
3388
|
+
(
|
3389
|
+
element.attrs.get("alt")
|
3390
|
+
or (path.name if path else "Image")
|
3391
|
+
),
|
3392
|
+
),
|
3393
|
+
]
|
3394
|
+
)
|
2714
3395
|
|
2715
3396
|
return ft
|
2716
3397
|
|
@@ -2726,12 +3407,23 @@ class HTML:
|
|
2726
3407
|
# HTMLParser clobber the case of element attributes
|
2727
3408
|
# We fix the SVG viewBox here
|
2728
3409
|
data = element._outer_html().replace(" viewbox=", " viewBox=")
|
2729
|
-
#
|
3410
|
+
# Display it graphically
|
3411
|
+
cols, aspect = pixels_to_cell_size(*data_pixel_size(data, format_="svg"))
|
3412
|
+
# Scale down the image to fit to width
|
3413
|
+
if content_width := theme.content_width:
|
3414
|
+
if cols == 0:
|
3415
|
+
cols = content_width
|
3416
|
+
else:
|
3417
|
+
cols = min(content_width, cols)
|
3418
|
+
rows = int(cols * aspect)
|
3419
|
+
# Convert the image to formatted-text
|
2730
3420
|
ft = convert(
|
2731
3421
|
data=data,
|
2732
3422
|
from_="svg",
|
2733
3423
|
to="formatted_text",
|
2734
|
-
cols=
|
3424
|
+
cols=cols,
|
3425
|
+
rows=rows or None,
|
3426
|
+
fg=theme.color,
|
2735
3427
|
bg=theme.background_color,
|
2736
3428
|
)
|
2737
3429
|
# Remove trailing new-lines
|
@@ -2755,7 +3447,7 @@ class HTML:
|
|
2755
3447
|
dom=self,
|
2756
3448
|
name="text",
|
2757
3449
|
parent=element,
|
2758
|
-
text=attrs.get("value", attrs.get("placeholder", "")),
|
3450
|
+
text=attrs.get("value", attrs.get("placeholder", " ")) or " ",
|
2759
3451
|
),
|
2760
3452
|
)
|
2761
3453
|
ft = self.render_node_content(
|
@@ -2766,16 +3458,6 @@ class HTML:
|
|
2766
3458
|
)
|
2767
3459
|
return ft
|
2768
3460
|
|
2769
|
-
def render_br_content(
|
2770
|
-
self,
|
2771
|
-
element: Node,
|
2772
|
-
left: int = 0,
|
2773
|
-
fill: bool = True,
|
2774
|
-
align_content: bool = True,
|
2775
|
-
) -> StyleAndTextTuples:
|
2776
|
-
"""Render line breaks."""
|
2777
|
-
return [("", "\n")]
|
2778
|
-
|
2779
3461
|
def render_node_content(
|
2780
3462
|
self,
|
2781
3463
|
element: Node,
|
@@ -2794,19 +3476,6 @@ class HTML:
|
|
2794
3476
|
line_height = 1
|
2795
3477
|
baseline = 0
|
2796
3478
|
|
2797
|
-
# Add "::before" and "::after" nodes
|
2798
|
-
# TODO - do we have to do this for every element?
|
2799
|
-
for name, pos in (("::before", 0), ("::after", 1)):
|
2800
|
-
if not element.name.startswith("::"):
|
2801
|
-
content_node = Node(dom=element.dom, name=name, parent=element)
|
2802
|
-
if text := content_node.theme.get("content", "").strip('"'):
|
2803
|
-
content_node.contents.append(
|
2804
|
-
Node(
|
2805
|
-
dom=element.dom, name="text", parent=content_node, text=text
|
2806
|
-
)
|
2807
|
-
)
|
2808
|
-
element.contents.insert(pos * len(element.contents), content_node)
|
2809
|
-
|
2810
3479
|
parent_theme = element.theme
|
2811
3480
|
|
2812
3481
|
d_blocky = d_inline = d_inline_block = False
|
@@ -2818,15 +3487,37 @@ class HTML:
|
|
2818
3487
|
|
2819
3488
|
content_width = parent_theme.content_width
|
2820
3489
|
|
2821
|
-
new_line = []
|
3490
|
+
new_line: StyleAndTextTuples = []
|
3491
|
+
|
3492
|
+
def flush() -> None:
|
3493
|
+
"""Add the current line to the rendered output."""
|
3494
|
+
nonlocal new_line, ft, left, line_height, baseline
|
3495
|
+
if new_line:
|
3496
|
+
# Pad the new-line to form an alignable block
|
3497
|
+
new_line = pad(new_line, style=parent_theme.style)
|
3498
|
+
if ft:
|
3499
|
+
# Combine with the output
|
3500
|
+
ft = join_lines([ft, new_line]) if ft else new_line
|
3501
|
+
else:
|
3502
|
+
ft = new_line
|
3503
|
+
# Reset the new line
|
3504
|
+
left = 0
|
3505
|
+
line_height = 1
|
3506
|
+
baseline = 0
|
3507
|
+
new_line = []
|
2822
3508
|
|
2823
3509
|
# Render each child node
|
2824
|
-
for child in element.
|
3510
|
+
for child in element.renderable_descendents:
|
2825
3511
|
theme = child.theme
|
2826
3512
|
|
2827
3513
|
if theme.skip:
|
2828
3514
|
continue
|
2829
3515
|
|
3516
|
+
# Start a new line if we encounter a <br> element
|
3517
|
+
if child.name == "br":
|
3518
|
+
flush()
|
3519
|
+
continue
|
3520
|
+
|
2830
3521
|
# We will start a new line if the previous item was a block
|
2831
3522
|
if ft and d_blocky and last_char(ft) != "\n":
|
2832
3523
|
line_height = 1
|
@@ -2836,12 +3527,16 @@ class HTML:
|
|
2836
3527
|
d_blocky = theme.d_blocky
|
2837
3528
|
d_inline = theme.d_inline
|
2838
3529
|
d_inline_block = theme.d_inline_block
|
3530
|
+
preformatted = theme.preformatted
|
3531
|
+
|
3532
|
+
available_width = parent_theme.content_width
|
3533
|
+
available_height = parent_theme.content_height
|
2839
3534
|
|
2840
3535
|
# Render the element
|
2841
3536
|
rendering = self.render_element(
|
2842
3537
|
child,
|
2843
|
-
available_width=
|
2844
|
-
available_height=
|
3538
|
+
available_width=available_width,
|
3539
|
+
available_height=available_height,
|
2845
3540
|
left=0 if d_blocky or d_inline_block else left,
|
2846
3541
|
fill=fill,
|
2847
3542
|
align_content=align_content,
|
@@ -2853,16 +3548,15 @@ class HTML:
|
|
2853
3548
|
|
2854
3549
|
# If the rendering was a positioned absolutely or fixed, store it and draw it later
|
2855
3550
|
if theme.theme["position"] == "fixed":
|
2856
|
-
|
2857
|
-
self.floats[(theme.z_index, theme.position)] = rendering
|
3551
|
+
self.fixed[(theme.z_index, theme.anchors, theme.position)] = rendering
|
2858
3552
|
|
2859
3553
|
# if theme.theme["position"] == "absolute":
|
2860
|
-
#
|
3554
|
+
# self.floats[(theme.z_index, theme.anchors, theme.position)] = rendering
|
2861
3555
|
|
2862
3556
|
# if theme.theme["position"] == "relative":
|
2863
3557
|
# ... TODO ..
|
2864
3558
|
|
2865
|
-
elif theme.
|
3559
|
+
elif theme.floated == "right":
|
2866
3560
|
lines = []
|
2867
3561
|
for ft_left, ft_right in zip_longest(
|
2868
3562
|
split_lines(pad(rendering, style=theme.style)),
|
@@ -2874,7 +3568,7 @@ class HTML:
|
|
2874
3568
|
float_width_right = fragment_list_width(float_lines_right[0])
|
2875
3569
|
continue
|
2876
3570
|
|
2877
|
-
elif theme.
|
3571
|
+
elif theme.floated == "left":
|
2878
3572
|
lines = []
|
2879
3573
|
for ft_left, ft_right in zip_longest(
|
2880
3574
|
lines,
|
@@ -2891,25 +3585,25 @@ class HTML:
|
|
2891
3585
|
# output, which could have been an inline-block
|
2892
3586
|
|
2893
3587
|
elif d_inline and (
|
3588
|
+
# parent_theme.d_inline or parent_theme.d_inline_block or preformatted
|
2894
3589
|
parent_theme.d_inline
|
2895
|
-
or
|
2896
|
-
or theme.preformatted
|
3590
|
+
or preformatted
|
2897
3591
|
):
|
2898
3592
|
new_line.extend(rendering)
|
2899
3593
|
|
2900
3594
|
elif d_inline or d_inline_block:
|
2901
3595
|
if d_inline:
|
2902
|
-
tokens = fragment_list_to_words(rendering)
|
3596
|
+
tokens = list(fragment_list_to_words(rendering))
|
2903
3597
|
else:
|
2904
3598
|
tokens = [rendering]
|
2905
3599
|
|
2906
|
-
tokens = list(tokens)
|
2907
|
-
|
2908
3600
|
for token in tokens:
|
2909
3601
|
token_lines = list(split_lines(token))
|
2910
3602
|
token_width = max(fragment_list_width(line) for line in token_lines)
|
2911
3603
|
token_height = len(token_lines)
|
2912
3604
|
|
3605
|
+
# Deal with floats
|
3606
|
+
|
2913
3607
|
float_width_right = (
|
2914
3608
|
fragment_list_width(float_lines_right[0])
|
2915
3609
|
if float_lines_right
|
@@ -2921,9 +3615,15 @@ class HTML:
|
|
2921
3615
|
else 0
|
2922
3616
|
)
|
2923
3617
|
|
3618
|
+
# If we have floats, transform the current new line and add one row
|
3619
|
+
# from each active float
|
2924
3620
|
if (
|
2925
|
-
|
2926
|
-
|
3621
|
+
new_line
|
3622
|
+
and (content_width - float_width_left - float_width_right)
|
3623
|
+
- left
|
3624
|
+
- token_width
|
3625
|
+
< 0
|
3626
|
+
):
|
2927
3627
|
new_rows = list(split_lines(new_line))
|
2928
3628
|
new_line_width = max(
|
2929
3629
|
fragment_list_width(line) for line in new_rows
|
@@ -2957,27 +3657,31 @@ class HTML:
|
|
2957
3657
|
how=parent_theme.text_align,
|
2958
3658
|
width=line_width,
|
2959
3659
|
style=parent_theme.style,
|
3660
|
+
placeholder="",
|
2960
3661
|
),
|
2961
3662
|
*ft_right,
|
2962
3663
|
]
|
2963
3664
|
)
|
2964
3665
|
|
2965
|
-
ft = join_lines([ft, *transformed_rows]) if ft else new_line
|
2966
|
-
|
2967
3666
|
float_lines_left = float_lines_left[line_height:]
|
2968
3667
|
float_lines_right = float_lines_right[line_height:]
|
2969
3668
|
|
3669
|
+
# Manually flush the transformed lines
|
3670
|
+
if ft:
|
3671
|
+
ft = join_lines([ft, *transformed_rows])
|
3672
|
+
else:
|
3673
|
+
ft = join_lines(transformed_rows)
|
3674
|
+
baseline = 0
|
2970
3675
|
new_rows = [[]]
|
2971
|
-
new_line = []
|
2972
|
-
line_height = 1
|
2973
3676
|
left = 0
|
2974
|
-
|
3677
|
+
line_height = 1
|
3678
|
+
new_line = []
|
2975
3679
|
|
2976
3680
|
if line_height == token_height == 1 or not new_line:
|
2977
3681
|
new_line.extend(token)
|
2978
3682
|
new_rows = [new_line]
|
2979
3683
|
baseline = int(theme.vertical_align * (token_height - 1))
|
2980
|
-
|
3684
|
+
line_height = max(line_height, token_height)
|
2981
3685
|
else:
|
2982
3686
|
new_line, baseline = concat(
|
2983
3687
|
ft_a=new_line,
|
@@ -2995,9 +3699,7 @@ class HTML:
|
|
2995
3699
|
# end of the output
|
2996
3700
|
else:
|
2997
3701
|
# Flush the latest line
|
2998
|
-
|
2999
|
-
ft = join_lines([ft, new_line]) if ft else new_line
|
3000
|
-
new_line = []
|
3702
|
+
flush()
|
3001
3703
|
# Start block elements on a new line
|
3002
3704
|
if ft and d_blocky and last_char(ft) != "\n":
|
3003
3705
|
ft.append(("", "\n"))
|
@@ -3045,6 +3747,7 @@ class HTML:
|
|
3045
3747
|
how=parent_theme.text_align,
|
3046
3748
|
width=line_width,
|
3047
3749
|
style=parent_theme.style + " nounderline",
|
3750
|
+
placeholder="",
|
3048
3751
|
),
|
3049
3752
|
*ft_right,
|
3050
3753
|
]
|
@@ -3052,8 +3755,7 @@ class HTML:
|
|
3052
3755
|
new_line = []
|
3053
3756
|
|
3054
3757
|
# Flush any current lines
|
3055
|
-
|
3056
|
-
ft = join_lines([ft, new_line]) if ft else new_line
|
3758
|
+
flush()
|
3057
3759
|
|
3058
3760
|
# Draw flex elements
|
3059
3761
|
# if parent_theme.get("flex") and parent_theme.get("flex-direction") == "column":
|
@@ -3092,12 +3794,6 @@ class HTML:
|
|
3092
3794
|
if d_inline:
|
3093
3795
|
ft = apply_style(ft, theme.style)
|
3094
3796
|
|
3095
|
-
# Remove trailing newline from the contents of pre-formatted elements
|
3096
|
-
if preformatted and (
|
3097
|
-
(parent_theme and not parent_theme.preformatted) or d_blocky
|
3098
|
-
):
|
3099
|
-
ft = strip_one_trailing_newline(ft)
|
3100
|
-
|
3101
3797
|
# If an element should not overflow it's width / height, truncate it
|
3102
3798
|
if not d_inline and not preformatted:
|
3103
3799
|
if theme.get("overflow_x") == "hidden":
|
@@ -3112,20 +3808,44 @@ class HTML:
|
|
3112
3808
|
)
|
3113
3809
|
else:
|
3114
3810
|
ft = truncate(
|
3115
|
-
ft,
|
3811
|
+
ft,
|
3812
|
+
content_width,
|
3813
|
+
placeholder="",
|
3814
|
+
ignore_whitespace=True,
|
3815
|
+
style=theme.style,
|
3116
3816
|
)
|
3117
3817
|
|
3118
|
-
|
3119
|
-
|
3120
|
-
|
3121
|
-
|
3122
|
-
|
3123
|
-
|
3818
|
+
# Truncate or expand the height
|
3819
|
+
overflow_y = theme.get("overflow_y") in {"hidden", "auto"}
|
3820
|
+
pad_height = d_blocky and theme.height is not None
|
3821
|
+
if overflow_y or pad_height:
|
3822
|
+
target_height = None
|
3823
|
+
if (min_height := theme.min_height) and min_height > content_height:
|
3824
|
+
target_height = min_height
|
3825
|
+
if (max_height := theme.max_height) and max_height < content_height:
|
3826
|
+
target_height = max_height
|
3827
|
+
elif height := theme.height:
|
3828
|
+
target_height = height
|
3829
|
+
|
3830
|
+
if target_height is not None:
|
3831
|
+
# Truncate elements with hidden overflows
|
3832
|
+
if overflow_y:
|
3833
|
+
lines = []
|
3834
|
+
for i, line in enumerate(split_lines(ft)):
|
3835
|
+
if i <= target_height:
|
3836
|
+
lines.append(line)
|
3837
|
+
else:
|
3838
|
+
lines = list(split_lines(ft))
|
3839
|
+
|
3840
|
+
# Pad height of block elements to theme height
|
3841
|
+
if pad_height and len(lines) < target_height:
|
3842
|
+
lines.extend([[]] * (target_height - len(lines)))
|
3843
|
+
|
3844
|
+
ft = join_lines(lines)
|
3124
3845
|
|
3125
3846
|
# Align content
|
3126
|
-
if align_content and
|
3847
|
+
if align_content and d_blocky:
|
3127
3848
|
alignment = theme.text_align
|
3128
|
-
|
3129
3849
|
if alignment != FormattedTextAlign.LEFT:
|
3130
3850
|
ft = align(
|
3131
3851
|
ft,
|
@@ -3133,10 +3853,11 @@ class HTML:
|
|
3133
3853
|
width=None if d_inline_block else content_width,
|
3134
3854
|
style=theme.style,
|
3135
3855
|
ignore_whitespace=True,
|
3856
|
+
placeholder="",
|
3136
3857
|
)
|
3137
3858
|
|
3138
3859
|
# # Fill space around block elements so they fill the content width
|
3139
|
-
if ft and (fill and d_blocky) or d_inline_block:
|
3860
|
+
if ft and ((fill and d_blocky and not theme.d_table) or d_inline_block):
|
3140
3861
|
pad_width = None
|
3141
3862
|
if d_blocky:
|
3142
3863
|
pad_width = content_width
|
@@ -3145,11 +3866,12 @@ class HTML:
|
|
3145
3866
|
pad_width = max_line_width(ft)
|
3146
3867
|
else:
|
3147
3868
|
pad_width = content_width
|
3869
|
+
style = theme.style
|
3148
3870
|
ft = pad(
|
3149
3871
|
ft,
|
3150
3872
|
width=pad_width,
|
3151
3873
|
char=" ",
|
3152
|
-
style=
|
3874
|
+
style=style,
|
3153
3875
|
)
|
3154
3876
|
|
3155
3877
|
# Use the rendered content width from now on for inline elements
|
@@ -3157,7 +3879,7 @@ class HTML:
|
|
3157
3879
|
content_width = max_line_width(ft)
|
3158
3880
|
|
3159
3881
|
# Add padding & border
|
3160
|
-
if d_blocky or d_inline_block
|
3882
|
+
if d_blocky or d_inline_block:
|
3161
3883
|
padding = theme.padding
|
3162
3884
|
border_visibility = theme.border_visibility
|
3163
3885
|
if (any(padding) or any(border_visibility)) and not (
|
@@ -3173,6 +3895,33 @@ class HTML:
|
|
3173
3895
|
padding=padding,
|
3174
3896
|
)
|
3175
3897
|
|
3898
|
+
# Draw borders and padding on text inside inline elements
|
3899
|
+
elif element.name == "text":
|
3900
|
+
padding = theme.padding
|
3901
|
+
border_visibility = theme.border_visibility
|
3902
|
+
if (
|
3903
|
+
padding.left
|
3904
|
+
or padding.right
|
3905
|
+
or border_visibility.left
|
3906
|
+
or border_visibility.right
|
3907
|
+
):
|
3908
|
+
if not element.is_first_child_node:
|
3909
|
+
border_visibility = border_visibility._replace(left=False)
|
3910
|
+
padding = padding._replace(left=0)
|
3911
|
+
if not element.is_last_child_node:
|
3912
|
+
border_visibility = border_visibility._replace(right=False)
|
3913
|
+
padding = padding._replace(right=0)
|
3914
|
+
if any(padding) or any(border_visibility):
|
3915
|
+
ft = add_border(
|
3916
|
+
ft,
|
3917
|
+
style=f"{theme.style} nounderline",
|
3918
|
+
border_grid=theme.border_grid,
|
3919
|
+
width=content_width if not ft else None,
|
3920
|
+
border_visibility=border_visibility,
|
3921
|
+
border_style=theme.border_style,
|
3922
|
+
padding=padding,
|
3923
|
+
)
|
3924
|
+
|
3176
3925
|
# The "::marker" element is drawn in the margin, before any padding
|
3177
3926
|
# If the element has no margin, it can end up in the parent's padding
|
3178
3927
|
# We use [ReverseOverwrite] fragments to ensure the marker is ignored
|
@@ -3194,13 +3943,15 @@ class HTML:
|
|
3194
3943
|
parent_style = parent_theme.style if parent_theme else ""
|
3195
3944
|
|
3196
3945
|
# Render the margin
|
3197
|
-
if d_blocky and theme.
|
3946
|
+
# if d_blocky and (alignment := theme.block_align) != FormattedTextAlign.LEFT:
|
3947
|
+
if (alignment := theme.block_align) != FormattedTextAlign.LEFT:
|
3198
3948
|
# Center block contents if margin_left and margin_right are "auto"
|
3199
3949
|
ft = align(
|
3200
3950
|
ft,
|
3201
|
-
how=
|
3951
|
+
how=alignment,
|
3202
3952
|
width=theme.available_width,
|
3203
3953
|
style=parent_style,
|
3954
|
+
placeholder="",
|
3204
3955
|
)
|
3205
3956
|
|
3206
3957
|
elif any(margin := theme.margin):
|
@@ -3214,18 +3965,40 @@ class HTML:
|
|
3214
3965
|
|
3215
3966
|
# Ensure hidden content is blank and styled like the parent
|
3216
3967
|
if theme.hidden:
|
3217
|
-
ft =
|
3218
|
-
|
3219
|
-
|
3220
|
-
|
3221
|
-
|
3222
|
-
|
3223
|
-
|
3968
|
+
ft = cast(
|
3969
|
+
"StyleAndTextTuples",
|
3970
|
+
[
|
3971
|
+
(
|
3972
|
+
parent_style,
|
3973
|
+
"\n".join([" " * len(x) for x in text.split("\n")]),
|
3974
|
+
*rest,
|
3975
|
+
)
|
3976
|
+
for style, text, *rest in ft
|
3977
|
+
],
|
3978
|
+
)
|
3979
|
+
|
3980
|
+
# Apply mouse handler to links
|
3981
|
+
if (
|
3982
|
+
(parent := element.parent)
|
3983
|
+
and parent.name == "a"
|
3984
|
+
and callable(handler := self.mouse_handler)
|
3985
|
+
and (href := parent.attrs.get("href"))
|
3986
|
+
):
|
3987
|
+
element.attrs["_link_path"] = self.base / href
|
3988
|
+
ft = cast(
|
3989
|
+
"StyleAndTextTuples",
|
3990
|
+
[
|
3991
|
+
(style, text, *(rest or [partial(handler, element)]))
|
3992
|
+
for style, text, *rest in ft
|
3993
|
+
],
|
3994
|
+
)
|
3224
3995
|
|
3225
3996
|
return ft
|
3226
3997
|
|
3227
3998
|
def __pt_formatted_text__(self) -> StyleAndTextTuples:
|
3228
3999
|
"""Return formatted text."""
|
4000
|
+
if not self.formatted_text:
|
4001
|
+
self.render(width=None, height=None)
|
3229
4002
|
return self.formatted_text
|
3230
4003
|
|
3231
4004
|
|
@@ -3237,18 +4010,19 @@ if __name__ == "__main__":
|
|
3237
4010
|
from prompt_toolkit.styles.style import Style
|
3238
4011
|
|
3239
4012
|
from euporie.core.app import BaseApp
|
4013
|
+
from euporie.core.path import parse_path
|
3240
4014
|
from euporie.core.style import HTML_STYLE
|
3241
4015
|
|
3242
|
-
path =
|
4016
|
+
path = parse_path(sys.argv[1])
|
3243
4017
|
|
3244
4018
|
with create_app_session(input=BaseApp.load_input(), output=BaseApp.load_output()):
|
3245
4019
|
with set_app(BaseApp()):
|
3246
|
-
|
3247
|
-
|
3248
|
-
path.
|
4020
|
+
print_formatted_text(
|
4021
|
+
HTML(
|
4022
|
+
path.read_text(),
|
3249
4023
|
base=path,
|
3250
4024
|
collapse_root_margin=False,
|
3251
4025
|
fill=True,
|
3252
|
-
)
|
3253
|
-
|
3254
|
-
|
4026
|
+
),
|
4027
|
+
style=Style(HTML_STYLE),
|
4028
|
+
)
|