import contextlib
import json
from abc import ABC, abstractmethod
from typing import Iterable, List, Union, Optional, Any, Tuple, Set, Dict, TypeVar, overload, Callable, NoReturn
from colorama import Style
from typing_extensions import Self, override, TypedDict, NotRequired, Unpack, final
from mcdreforged.minecraft.rtext.click_event import RClickAction, RClickEvent, RClickEventSingleValue
from mcdreforged.minecraft.rtext.hover_event import RHoverEvent, RHoverText
from mcdreforged.minecraft.rtext.schema import RTextJsonFormat
from mcdreforged.minecraft.rtext.style import RStyle, RColor, RColorClassic, RColorRGB, RItemClassic
from mcdreforged.utils import class_utils
class __Unset:
def __repr__(self):
return '<unset>'
_UNSET: Any = __Unset()
_T = TypeVar('_T')
[文档]
class RTextBase(ABC):
"""
An abstract base class of Minecraft text component
"""
class ToJsonKwargs(TypedDict):
json_format: NotRequired[RTextJsonFormat]
class FromJsonKwargs(TypedDict):
json_format: NotRequired[Optional[RTextJsonFormat]] # None means auto-detect
[文档]
@abstractmethod
def to_json_object(self, **kwargs: Unpack[ToJsonKwargs]) -> Union[dict, list]:
"""
Return an object representing its data that can be serialized into a json string
"""
raise NotImplementedError()
[文档]
def to_json_str(self, **kwargs: Unpack[ToJsonKwargs]) -> str:
"""
Return a json formatted str representing its data
It can be used as the second parameter in Minecraft command ``/tellraw <target> <message>`` and more
"""
return json.dumps(self.to_json_object(**kwargs), ensure_ascii=False, separators=(',', ':'))
[文档]
@abstractmethod
def to_plain_text(self) -> str:
"""
Return a plain text for console display
Click event and hover event will be ignored
"""
raise NotImplementedError()
[文档]
@abstractmethod
def to_colored_text(self) -> str:
"""
Return a colored text stained with ANSI escape code for console display
Click event and hover event will be ignored
"""
raise NotImplementedError()
[文档]
@abstractmethod
def to_legacy_text(self) -> str:
"""
Return a colored text stained with classic "§"-prefixed Minecraft formatting code
Click event and hover event will be ignored
"""
raise NotImplementedError()
[文档]
@abstractmethod
def copy(self) -> Self:
"""
Return a deep copy version of itself
"""
raise NotImplementedError()
[文档]
@abstractmethod
def set_color(self, color: RColor) -> Self:
"""
Set the color of the text and return the text component itself
"""
raise NotImplementedError()
[文档]
@abstractmethod
def set_styles(self, styles: Union[RStyle, Iterable[RStyle]]) -> Self:
"""
Set the styles of the text and return the text component itself
"""
raise NotImplementedError()
@overload
def set_click_event(self, event: RClickEvent, /) -> Self:
"""
Set the click event of the text directly (position-argument only version)
"""
...
@overload
def set_click_event(self, /, *, event: RClickEvent) -> Self:
"""
Set the click event of the text directly (kwargs-only version)
"""
...
@overload
def set_click_event(self, action: RClickAction[RClickEventSingleValue[_T]], value: _T, /) -> Self:
"""
Set the click event of the text by providing the action type and the action value (position-argument only version)
This overload works for click actions that accept exactly 1 argument
"""
...
@overload
def set_click_event(self, /, *, action: RClickAction[RClickEventSingleValue[_T]], value: _T) -> Self:
"""
Set the click event of the text by providing the action type and the action value (kwargs-only version)
This overload works for click actions that accept exactly 1 argument
"""
...
[文档]
@final
def set_click_event(
self,
arg1: Union[RClickEvent, RClickAction[RClickEventSingleValue[_T]]] = _UNSET,
arg2: _T = _UNSET, /, *,
event: RClickEvent = _UNSET,
action: RClickAction[RClickEventSingleValue[_T]] = _UNSET,
value: _T = _UNSET,
) -> Self:
"""
Set the click event
You can either directly provide the click event, or provide the action type and the action value separately
.. code-block:: python
text = RText('example')
text.set_click_event(RClickSuggestCommand('/say something')) # directly
text.set_click_event(event=RClickSuggestCommand('/say something')) # directly (kwarg)
text.set_click_event(RClickAction.suggest_command, '/say something') # action + value
text.set_click_event(action=RClickAction.suggest_command, value='/say something') # action + value (kwarg)
Method :meth:`c` is the short form of :meth:`set_click_event`
"""
def set_by_action_and_value(a: RClickAction[RClickEventSingleValue[_T]], v: _T):
if not issubclass(event_class := a.event_class, RClickEventSingleValue):
raise TypeError('Unsupported action type {} with event class {}'.format(type(a), event_class))
return self._set_click_event_direct(event_class.from_json_value(v))
# set by RClickEvent instance
if isinstance(arg1, RClickEvent) and arg2 is _UNSET and event is _UNSET and action is _UNSET and value is _UNSET:
return self._set_click_event_direct(arg1)
if arg1 is _UNSET and arg2 is _UNSET and isinstance(event, RClickEvent) and action is _UNSET and value is _UNSET:
return self._set_click_event_direct(event)
# set by RClickAction[RClickEventSingleValue[_T]] + value
if isinstance(arg1, RClickAction) and arg2 is not _UNSET and event is _UNSET and action is _UNSET and value is _UNSET:
return set_by_action_and_value(arg1, arg2)
if arg1 is _UNSET and arg2 is _UNSET and event is _UNSET and isinstance(action, RClickAction) and value is not _UNSET:
return set_by_action_and_value(action, value)
raise TypeError(f'Unexpected argument combination: {arg1=!r}, {arg2=!r}, {event=!r}, {action=!r}, {value=!r}')
@abstractmethod
def _set_click_event_direct(self, click_event: RClickEvent) -> Self:
raise NotImplementedError()
[文档]
@abstractmethod
def set_hover_event(self, hover_event: RHoverEvent) -> Self:
"""
Set the hover event
"""
raise NotImplementedError()
[文档]
def set_hover_text(self, *args) -> Self:
"""
Set the hover text
Method :meth:`h` is the short form of :meth:`set_hover_text`
:param args: The elements be used to create a :class:`RTextList` instance.
The :class:`RTextList` instance is used as the actual hover text
:return: The text component itself
"""
for arg in args:
# typo checker
if isinstance(arg, RHoverEvent):
raise TypeError(f'Argument can not be a RHoverEvent, got {arg!r}. If you want to directly set the hover event, use `set_hover_event` instead')
if len(args) == 1:
text = RTextBase.from_any(args[0])
else:
text = RTextList(*args)
self.set_hover_event(RHoverText(text=text))
return self
c = set_click_event
"""
Alias of :meth:`set_click_event`
"""
h = set_hover_text
"""
Alias of :meth:`set_hover_text`
"""
def __str__(self):
return self.to_plain_text()
def __add__(self, other):
return RTextList(self, other)
def __radd__(self, other):
return RTextList(other, self)
[文档]
@staticmethod
def from_any(text) -> 'RTextBase':
"""
Convert anything into a RText component
"""
if isinstance(text, RTextBase):
return text
return RText(text)
[文档]
@staticmethod
def join(divider: Any, iterable: Iterable[Any]) -> 'RTextBase':
"""
Just like method :meth:`str.join`, it concatenates any number of texts with *divider*
Example::
>>> text = RTextBase.join(',', [RText('1'), '2', 3])
>>> text.to_plain_text()
'1,2,3'
:param divider: The divider between elements. The divider object will be reused
:param iterable: The elements to be joined
"""
result = RTextList()
for i, item in enumerate(iterable):
if i > 0:
result.append(divider)
result.append(item)
return result
[文档]
@staticmethod
def format(fmt: str, *args, **kwargs) -> 'RTextBase':
"""
Just like method :meth:`str.format`, it uses *\\*args* and *\\*\\*kwargs* to build a formatted RText component based on the formatter *fmt*
Example::
>>> text = RTextBase.format('a={},b={},c={c}', RText('1', color=RColor.blue), '2', c=3)
>>> text.to_plain_text()
'a=1,b=2,c=3'
:param fmt: The formatter string
:param args: The given arguments
:param kwargs: The given keyword arguments
"""
args = list(args)
kwargs = kwargs.copy()
placeholder_counter = 0
rtext_elements: List[Tuple[str, RTextBase]] = []
def acquire_placeholder():
nonlocal placeholder_counter
ph = '@@MCDR#RText.FMT#PH#{}@@'.format(placeholder_counter)
placeholder_counter += 1
return ph
for i, arg in enumerate(args):
if isinstance(arg, RTextBase):
placeholder = acquire_placeholder()
rtext_elements.append((placeholder, arg))
args[i] = placeholder
for key, value in kwargs.items():
if isinstance(value, RTextBase):
placeholder = acquire_placeholder()
rtext_elements.append((placeholder, value))
kwargs[key] = placeholder
texts: List[Union[str, RTextBase]] = [fmt.format(*args, **kwargs)]
for placeholder, rtext in rtext_elements:
new_texts: List[Union[str, RTextBase]] = []
for text in texts:
processed_text: List[Union[str, RTextBase]] = []
if isinstance(text, str):
for j, ele in enumerate(text.split(placeholder)):
if j > 0:
processed_text.append(rtext)
if len(ele) > 0:
processed_text.append(ele)
else:
processed_text.append(text)
new_texts.extend(processed_text)
texts = new_texts
return RTextList(*texts)
@classmethod
def __from_json_list(cls, data: list, json_format: Optional[RTextJsonFormat]) -> 'RTextBase':
text = RTextList()
if len(data) == 0:
return text
lst = [cls.from_json_object(item, json_format=json_format) for item in data]
if data[0] != '':
text.set_header_text(lst[0])
text.append(*lst[1:])
return text
@classmethod
def __from_json_dict(cls, data: dict, json_format: Optional[RTextJsonFormat]) -> 'RTextBase':
text: RTextBase
if 'text' in data:
text = RText(data['text'])
elif 'translate' in data:
tr_args = data.get('with', [])
text = RTextTranslation(data['translate']).arg(*map(cls.from_json_object, tr_args))
else:
raise ValueError('No method to create RText from {}'.format(data))
if isinstance(siblings := data.get('extra'), list):
text_list = RTextList()
text_list.set_header_text(text)
text_list.append(*map(cls.from_json_object, siblings))
text = text_list
styles: List[RStyle] = []
for style in RStyle:
if data.get(style.name, False):
styles.append(style)
text.set_styles(styles)
if isinstance(color := data.get('color'), str):
with contextlib.suppress(ValueError):
text.set_color(RColor.from_mc_value(color))
if json_format is None:
json_format = RTextJsonFormat.guess(data)
with contextlib.suppress(KeyError):
if isinstance(click_event := data[json_format.value.click_event_key], dict):
text._set_click_event_direct(RClickEvent.from_json_object(click_event, json_format))
with contextlib.suppress(KeyError):
if isinstance(hover_event := data[json_format.value.hover_event_key], dict):
text.set_hover_event(RHoverEvent.from_json_object(hover_event, json_format))
return text
[文档]
@classmethod
def from_json_object(cls, data: Union[str, list, dict], **kwargs: Unpack[FromJsonKwargs]) -> 'RTextBase':
"""
Convert a json object into a :class:`RText <RTextBase>` component
Example::
>>> text = RTextBase.from_json_object({'text': 'my text', 'color': 'red'})
>>> text.to_plain_text()
'my text'
>>> text.to_json_object()['color']
'red'
:param data: A json object
.. versionadded:: v2.4.0
"""
json_format = kwargs.get('json_format')
if isinstance(data, str):
return cls.from_any(data)
if isinstance(data, list):
return cls.__from_json_list(data, json_format)
elif isinstance(data, dict):
return cls.__from_json_dict(data, json_format)
else:
raise TypeError('Unsupported data {!r}'.format(data))
[文档]
class RText(RTextBase):
"""
The regular text component class
"""
[文档]
def __init__(self, text: Any, color: Optional[RColor] = None, styles: Optional[Union[RStyle, Iterable[RStyle]]] = None):
"""
Create a :class:`RText` object with specific text, optional color and optional style
:param text: The content of the text. It will be converted into str
:param color: Optional, the color of the text
:param styles: Optional, the style of the text. It can be a single :class:`~mcdreforged.minecraft.rtext.style.RStyle`
or an iterable of :class:`~mcdreforged.minecraft.rtext.style.RStyle`
"""
self.__text: str = str(text) if type(text) is not str else text
self.__color: Optional[RColor] = None
self.__styles: Set[RStyle] = set()
self.__click_event: Optional[RClickEvent] = None
self.__hover_event: Optional[RHoverEvent] = None
if color is not None:
self.set_color(color)
if styles is not None:
self.set_styles(styles)
@override
def set_color(self, color: RColor) -> Self:
self.__color = color
return self
@override
def set_styles(self, styles: Union[RStyle, Iterable[RStyle]]) -> Self:
if isinstance(styles, RStyle):
styles = {styles}
elif isinstance(styles, Iterable):
styles = set(styles)
else:
raise TypeError('Unsupported style type {}'.format(type(styles)))
self.__styles = styles
return self
@override
def _set_click_event_direct(self, click_event: RClickEvent) -> Self:
self.__click_event = click_event
return self
@override
def set_hover_event(self, hover_event: RHoverEvent) -> Self:
self.__hover_event = hover_event
return self
@override
def to_json_object(self, **kwargs: Unpack[RTextBase.ToJsonKwargs]) -> Dict[str, Any]:
json_format = kwargs.get('json_format', RTextJsonFormat.default())
obj: Dict[str, Any] = {'text': self.__text}
if self.__color is not None:
obj['color'] = self.__color.name
for style in self.__styles:
obj[style.name] = True
if self.__click_event is not None:
obj[json_format.value.click_event_key] = self.__click_event.to_json_object(json_format)
if self.__hover_event is not None:
obj[json_format.value.hover_event_key] = self.__hover_event.to_json_object(json_format)
return obj
@override
def to_plain_text(self) -> str:
return self.__text
def __collect_classic_style_codes(self, mapper: Callable[[RItemClassic], str]) -> str:
colors: List[str] = []
if isinstance(self.__color, RColorClassic):
colors.append(mapper(self.__color))
elif isinstance(self.__color, RColorRGB):
colors.append(mapper(self.__color.to_classic()))
for style in self.__styles:
if isinstance(style, RItemClassic):
colors.append(mapper(style))
return ''.join(colors)
def _get_console_style_codes(self) -> str:
def code_getter(item: RItemClassic) -> str:
return item.console_code
return self.__collect_classic_style_codes(code_getter)
@override
def to_colored_text(self) -> str:
head = self._get_console_style_codes()
tail = Style.RESET_ALL if len(head) > 0 else ''
return head + self.to_plain_text() + tail
def _get_legacy_style_codes(self) -> str:
def code_getter(item: RItemClassic) -> str:
return item.mc_code
return self.__collect_classic_style_codes(code_getter)
@override
def to_legacy_text(self) -> str:
head = self._get_legacy_style_codes()
tail = RColor.reset.mc_code if len(head) > 0 else ''
return head + self.to_plain_text() + tail
def _copy_from(self, text: Self):
self.__text = text.__text
self.__color = text.__color
self.__styles = text.__styles.copy()
self.__click_event = text.__click_event
self.__hover_event = text.__hover_event
@override
def copy(self) -> 'RText':
copied = RText('')
copied._copy_from(self)
return copied
def __repr__(self) -> str:
return class_utils.represent(self, fields={
'text': self.__text,
'color': self.__color,
'styles': self.__styles,
'click_event': self.__click_event,
'hover_event': self.__hover_event,
})
def __eq__(self, other) -> bool:
"""
.. versionadded:: v2.15.0
"""
if type(other) != type(self):
return False
other: Self
return all((
self.__text == other.__text,
self.__color == other.__color,
self.__styles == other.__styles,
self.__click_event == other.__click_event,
self.__hover_event == other.__hover_event,
))
[文档]
class RTextList(RTextBase):
"""
A list of :class:`RTextBase` objects, a compound text component
"""
[文档]
def __init__(self, *args: Any):
"""
Use the given *\\*args* to create a :class:`RTextList`
:param args: The items in this :class:`RTextList`. They can be :class:`str`, :class:`RTextBase`
or any class implements ``__str__`` method. All non- :class:`RTextBase` items
will be converted to :class:`RText`
"""
self.header: RTextBase = RText('')
self.header_empty = True
self.children: List[RTextBase] = []
self.append(*args)
@override
def set_color(self, color: RColor) -> Self:
self.header.set_color(color)
self.header_empty = False
return self
@override
def set_styles(self, styles: Union[RStyle, Iterable[RStyle]]) -> Self:
self.header.set_styles(styles)
self.header_empty = False
return self
@override
def _set_click_event_direct(self, click_event: RClickEvent) -> Self:
# noinspection PyProtectedMember
self.header._set_click_event_direct(click_event)
self.header_empty = False
return self
@override
def set_hover_event(self, hover_event: RHoverEvent) -> Self:
self.header.set_hover_event(hover_event)
self.header_empty = False
return self
def set_header_text(self, header_text: RTextBase) -> Self:
"""
**Not public API**
:meta private:
"""
self.header = header_text
self.header_empty = False
return self
def __get_styling_header_rtext(self) -> RText:
def get(text: RTextBase) -> RText:
if isinstance(text, RText):
return text
elif isinstance(text, RTextList):
return get(text.header)
else:
raise ValueError(f'Unexpected RTextList header text type {type(text)}')
return get(self.header)
def append(self, *args) -> Self:
for obj in args:
self.children.append(RTextBase.from_any(obj))
return self
def is_empty(self) -> bool:
return len(self.children) == 0
@override
def to_json_object(self, **kwargs: Unpack[RTextBase.ToJsonKwargs]) -> Union[dict, list]:
ret: list = ['' if self.header_empty else self.header.to_json_object(**kwargs)]
ret.extend([rtext.to_json_object(**kwargs) for rtext in self.children])
return ret
@override
def to_plain_text(self) -> str:
return ''.join(rtext.to_plain_text() for rtext in self.children)
@override
def to_colored_text(self) -> str:
# noinspection PyProtectedMember
head = self.__get_styling_header_rtext()._get_console_style_codes()
tail = Style.RESET_ALL if len(head) > 0 else ''
return ''.join(
''.join([head, rtext.to_colored_text(), tail])
for rtext in self.children
)
@override
def to_legacy_text(self) -> str:
# noinspection PyProtectedMember
head = self.__get_styling_header_rtext()._get_legacy_style_codes()
tail = RColor.reset.mc_code if len(head) > 0 else ''
return ''.join(
''.join([head, rtext.to_legacy_text(), tail])
for rtext in self.children
)
@override
def copy(self) -> 'RTextList':
copied = RTextList()
copied.header = self.header.copy()
copied.header_empty = self.header_empty
copied.children = [child.copy() for child in self.children]
return copied
def __repr__(self) -> str:
return class_utils.represent(self, {
'header': None if self.header_empty else self.header,
'children': self.children
})
def __eq__(self, other) -> bool:
"""
.. versionadded:: v2.15.0
"""
if type(other) != type(self):
return False
other: Self
return all((
self.header == other.header,
self.header_empty == other.header_empty,
self.children == other.children,
))
[文档]
class RTextTranslation(RText):
"""
The translation text component class. It's almost the same as :class:`RText`
"""
[文档]
def __init__(self, translation_key: str, color: RColor = RColor.reset, styles: Optional[Union[RStyle, Iterable[RStyle]]] = None):
"""
Create a :class:`RTextTranslation` object with specific translation key.
The rest of the parameters are the same to the constructor of :class:`RText`
Use method :meth:`arg` to set the translation arguments, if the translation requires some arguments
Example::
RTextTranslation('advancements.nether.root.title', color=RColor.red)
:param translation_key: The translation key
:param color: Optional, the color of the text
:param styles: Optional, the style of the text. It can be a single :class:`~mcdreforged.minecraft.rtext.style.RStyle`
or a collection of :class:`~mcdreforged.minecraft.rtext.style.RStyle`
"""
super().__init__(translation_key, color, styles)
self.__translation_key: str = translation_key
self.__args: tuple = ()
self.__fallback: Optional[str] = None
[文档]
def arg(self, *args: Any) -> Self:
"""
Set the translation arguments
:param args: The translation arguments
"""
self.__args = args
return self
[文档]
def fallback(self, fallback: str) -> Self:
"""
Set the translation fallback
.. attention::
Works in Minecraft >= 1.19.4 only
:param fallback: The fallback text if the translation is unknown
"""
self.__fallback = fallback
return self
@override
def to_plain_text(self) -> str:
return self.__translation_key
@override
def to_json_object(self, **kwargs: Unpack[RTextBase.ToJsonKwargs]) -> Dict[str, Any]:
obj = super().to_json_object(**kwargs)
obj.pop('text')
obj['translate'] = self.__translation_key
if len(self.__args) > 0:
obj['with'] = [
arg.to_json_object()
if isinstance(arg, RTextBase)
else arg # keep it as-is
for arg in self.__args
]
if self.__fallback is not None:
obj['fallback'] = self.__fallback
return obj
@override
def _copy_from(self, text: Self):
super()._copy_from(text)
self.__translation_key = text.__translation_key
self.__args = text.__args
@override
def copy(self) -> 'RTextTranslation':
copied = RTextTranslation('')
copied._copy_from(self)
return copied
def __repr__(self) -> str:
return class_utils.represent(self, fields={
'key': self.__translation_key,
'args': self.__args,
'fallback': self.__fallback,
'color': self.__color,
'styles': self.__styles,
'click_event': self.__click_event,
'hover_event': self.__hover_event,
})
def __eq__(self, other) -> bool:
"""
.. versionadded:: v2.15.0
"""
if type(other) != type(self):
return False
other: Self
return all((
super().__eq__(other),
self.__translation_key == other.__translation_key,
self.__args == other.__args,
self.__fallback == other.__fallback,
))