1from __future__ import annotations
2
3from collections.abc import Callable, Iterable
4from typing import TYPE_CHECKING, Union, cast
5
6from prompt_toolkit.mouse_events import MouseEvent
7
8if TYPE_CHECKING:
9 from typing_extensions import Protocol
10
11 from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
12
13__all__ = [
14 "OneStyleAndTextTuple",
15 "StyleAndTextTuples",
16 "MagicFormattedText",
17 "AnyFormattedText",
18 "to_formatted_text",
19 "is_formatted_text",
20 "Template",
21 "merge_formatted_text",
22 "FormattedText",
23]
24
25OneStyleAndTextTuple = (
26 tuple[str, str] | tuple[str, str, Callable[[MouseEvent], "NotImplementedOrNone"]]
27)
28
29
30# List of (style, text) tuples.
31StyleAndTextTuples = list[OneStyleAndTextTuple]
32
33
34if TYPE_CHECKING:
35 from typing import TypeGuard
36
37 class MagicFormattedText(Protocol):
38 """
39 Any object that implements ``__pt_formatted_text__`` represents formatted
40 text.
41 """
42
43 def __pt_formatted_text__(self) -> StyleAndTextTuples: ...
44
45
46AnyFormattedText = Union[
47 str,
48 "MagicFormattedText",
49 StyleAndTextTuples,
50 Callable[[], "AnyFormattedText"],
51 None,
52]
53
54
55def to_formatted_text(
56 value: AnyFormattedText, style: str = "", auto_convert: bool = False
57) -> FormattedText:
58 """
59 Convert the given value (which can be formatted text) into a list of text
60 fragments. (Which is the canonical form of formatted text.) The outcome is
61 always a `FormattedText` instance, which is a list of (style, text) tuples.
62
63 It can take a plain text string, an `HTML` or `ANSI` object, anything that
64 implements `__pt_formatted_text__` or a callable that takes no arguments and
65 returns one of those.
66
67 :param style: An additional style string which is applied to all text
68 fragments.
69 :param auto_convert: If `True`, also accept other types, and convert them
70 to a string first.
71 """
72 result: FormattedText | StyleAndTextTuples
73
74 if value is None:
75 result = []
76 elif isinstance(value, str):
77 result = [("", value)]
78 elif isinstance(value, list):
79 result = value # StyleAndTextTuples
80 elif hasattr(value, "__pt_formatted_text__"):
81 result = cast("MagicFormattedText", value).__pt_formatted_text__()
82 elif callable(value):
83 return to_formatted_text(value(), style=style)
84 elif auto_convert:
85 result = [("", f"{value}")]
86 else:
87 raise ValueError(
88 f"No formatted text. Expecting a unicode object, 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