1from __future__ import annotations
2
3from typing import TYPE_CHECKING, Callable, Iterable, List, Tuple, Union, cast
4
5from prompt_toolkit.mouse_events import MouseEvent
6
7if TYPE_CHECKING:
8 from typing_extensions import Protocol
9
10 from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
11
12__all__ = [
13 "OneStyleAndTextTuple",
14 "StyleAndTextTuples",
15 "MagicFormattedText",
16 "AnyFormattedText",
17 "to_formatted_text",
18 "is_formatted_text",
19 "Template",
20 "merge_formatted_text",
21 "FormattedText",
22]
23
24OneStyleAndTextTuple = Union[
25 Tuple[str, str], Tuple[str, str, Callable[[MouseEvent], "NotImplementedOrNone"]]
26]
27
28# List of (style, text) tuples.
29StyleAndTextTuples = List[OneStyleAndTextTuple]
30
31
32if TYPE_CHECKING:
33 from typing_extensions import TypeGuard
34
35 class MagicFormattedText(Protocol):
36 """
37 Any object that implements ``__pt_formatted_text__`` represents formatted
38 text.
39 """
40
41 def __pt_formatted_text__(self) -> StyleAndTextTuples: ...
42
43
44AnyFormattedText = Union[
45 str,
46 "MagicFormattedText",
47 StyleAndTextTuples,
48 Callable[[], "AnyFormattedText"],
49 None,
50]
51
52
53def to_formatted_text(
54 value: AnyFormattedText, style: str = "", auto_convert: bool = False
55) -> FormattedText:
56 """
57 Convert the given value (which can be formatted text) into a list of text
58 fragments. (Which is the canonical form of formatted text.) The outcome is
59 always a `FormattedText` instance, which is a list of (style, text) tuples.
60
61 It can take a plain text string, an `HTML` or `ANSI` object, anything that
62 implements `__pt_formatted_text__` or a callable that takes no arguments and
63 returns one of those.
64
65 :param style: An additional style string which is applied to all text
66 fragments.
67 :param auto_convert: If `True`, also accept other types, and convert them
68 to a string first.
69 """
70 result: FormattedText | StyleAndTextTuples
71
72 if value is None:
73 result = []
74 elif isinstance(value, str):
75 result = [("", value)]
76 elif isinstance(value, list):
77 result = value # StyleAndTextTuples
78 elif hasattr(value, "__pt_formatted_text__"):
79 result = cast("MagicFormattedText", value).__pt_formatted_text__()
80 elif callable(value):
81 return to_formatted_text(value(), style=style)
82 elif auto_convert:
83 result = [("", f"{value}")]
84 else:
85 raise ValueError(
86 "No formatted text. Expecting a unicode object, "
87 f"HTML, ANSI or a FormattedText instance. Got {value!r}"
88 )
89
90 # Apply extra style.
91 if style:
92 result = cast(
93 StyleAndTextTuples,
94 [(style + " " + item_style, *rest) for item_style, *rest in result],
95 )
96
97 # Make sure the result is wrapped in a `FormattedText`. Among other
98 # reasons, this is important for `print_formatted_text` to work correctly
99 # and distinguish between lists and formatted text.
100 if isinstance(result, FormattedText):
101 return result
102 else:
103 return FormattedText(result)
104
105
106def is_formatted_text(value: object) -> TypeGuard[AnyFormattedText]:
107 """
108 Check whether the input is valid formatted text (for use in assert
109 statements).
110 In case of a callable, it doesn't check the return type.
111 """
112 if callable(value):
113 return True
114 if isinstance(value, (str, list)):
115 return True
116 if hasattr(value, "__pt_formatted_text__"):
117 return True
118 return False
119
120
121class FormattedText(StyleAndTextTuples):
122 """
123 A list of ``(style, text)`` tuples.
124
125 (In some situations, this can also be ``(style, text, mouse_handler)``
126 tuples.)
127 """
128
129 def __pt_formatted_text__(self) -> StyleAndTextTuples:
130 return self
131
132 def __repr__(self) -> str:
133 return f"FormattedText({super().__repr__()})"
134
135
136class Template:
137 """
138 Template for string interpolation with formatted text.
139
140 Example::
141
142 Template(' ... {} ... ').format(HTML(...))
143
144 :param text: Plain text.
145 """
146
147 def __init__(self, text: str) -> None:
148 assert "{0}" not in text
149 self.text = text
150
151 def format(self, *values: AnyFormattedText) -> AnyFormattedText:
152 def get_result() -> AnyFormattedText:
153 # Split the template in parts.
154 parts = self.text.split("{}")
155 assert len(parts) - 1 == len(values)
156
157 result = FormattedText()
158 for part, val in zip(parts, values):
159 result.append(("", part))
160 result.extend(to_formatted_text(val))
161 result.append(("", parts[-1]))
162 return result
163
164 return get_result
165
166
167def merge_formatted_text(items: Iterable[AnyFormattedText]) -> AnyFormattedText:
168 """
169 Merge (Concatenate) several pieces of formatted text together.
170 """
171
172 def _merge_formatted_text() -> AnyFormattedText:
173 result = FormattedText()
174 for i in items:
175 result.extend(to_formatted_text(i))
176 return result
177
178 return _merge_formatted_text