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