base.py 5.1 KB

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