base.py 5.0 KB

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