Changed code to support older Python versions
This commit is contained in:
parent
eb92d2d36f
commit
582458cdd0
5027 changed files with 794942 additions and 4 deletions
225
venv/lib/python3.11/site-packages/mcstatus/motd/__init__.py
Normal file
225
venv/lib/python3.11/site-packages/mcstatus/motd/__init__.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import typing as t
|
||||
from dataclasses import dataclass
|
||||
|
||||
from mcstatus.motd.components import Formatting, MinecraftColor, ParsedMotdComponent, TranslationTag, WebColor
|
||||
from mcstatus.motd.simplifies import get_unused_elements, squash_nearby_strings
|
||||
from mcstatus.motd.transformers import AnsiTransformer, HtmlTransformer, MinecraftTransformer, PlainTransformer
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from mcstatus.responses import RawJavaResponseMotd, RawJavaResponseMotdWhenDict # circular import
|
||||
else:
|
||||
RawJavaResponseMotdWhenDict = dict
|
||||
|
||||
__all__ = ["Motd"]
|
||||
|
||||
MOTD_COLORS_RE = re.compile(r"([\xA7|&][0-9A-FK-OR])", re.IGNORECASE)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Motd:
|
||||
"""Represents parsed MOTD."""
|
||||
|
||||
parsed: list[ParsedMotdComponent]
|
||||
"""Parsed MOTD, which then will be transformed.
|
||||
|
||||
Bases on this attribute, you can easily write your own MOTD-to-something parser.
|
||||
"""
|
||||
raw: RawJavaResponseMotd
|
||||
"""MOTD in raw format, just like the server gave."""
|
||||
bedrock: bool = False
|
||||
"""Is server Bedrock Edition? Some details may change in work of this class."""
|
||||
|
||||
@classmethod
|
||||
def parse(
|
||||
cls,
|
||||
raw: RawJavaResponseMotd, # type: ignore # later, we overwrite the type
|
||||
*,
|
||||
bedrock: bool = False,
|
||||
) -> Self:
|
||||
"""Parse a raw MOTD to less raw MOTD (:attr:`.parsed` attribute).
|
||||
|
||||
:param raw: Raw MOTD, directly from server.
|
||||
:param bedrock: Is server Bedrock Edition? Nothing changes here, just sets attribute.
|
||||
:returns: New :class:`.Motd` instance.
|
||||
"""
|
||||
original_raw = raw.copy() if hasattr(raw, "copy") else raw # type: ignore # Cannot access "copy" for type "str"
|
||||
if isinstance(raw, list):
|
||||
raw: RawJavaResponseMotdWhenDict = {"extra": raw}
|
||||
|
||||
if isinstance(raw, str):
|
||||
parsed = cls._parse_as_str(raw, bedrock=bedrock)
|
||||
elif isinstance(raw, dict):
|
||||
parsed = cls._parse_as_dict(raw, bedrock=bedrock)
|
||||
else:
|
||||
raise TypeError(f"Expected list, string or dict data, got {raw.__class__!r} ({raw!r}), report this!")
|
||||
|
||||
return cls(parsed, original_raw, bedrock)
|
||||
|
||||
@staticmethod
|
||||
def _parse_as_str(raw: str, *, bedrock: bool = False) -> list[ParsedMotdComponent]:
|
||||
"""Parse a MOTD when it's string.
|
||||
|
||||
.. note:: This method returns a lot of empty strings, use :meth:`Motd.simplify` to remove them.
|
||||
|
||||
:param raw: Raw MOTD, directly from server.
|
||||
:param bedrock: Is server Bedrock Edition?
|
||||
Ignores :attr:`MinecraftColor.MINECOIN_GOLD` if it's :obj:`False`.
|
||||
:returns: :obj:`ParsedMotdComponent` list, which need to be passed to ``__init__``.
|
||||
"""
|
||||
parsed_motd: list[ParsedMotdComponent] = []
|
||||
|
||||
split_raw = MOTD_COLORS_RE.split(raw)
|
||||
for element in split_raw:
|
||||
clean_element = element.lstrip("&§").lower()
|
||||
standardized_element = element.replace("&", "§").lower()
|
||||
|
||||
if standardized_element == "§g" and not bedrock:
|
||||
parsed_motd.append(element) # minecoin_gold on java server, treat as string
|
||||
continue
|
||||
|
||||
if standardized_element.startswith("§"):
|
||||
try:
|
||||
parsed_motd.append(MinecraftColor(clean_element))
|
||||
except ValueError:
|
||||
try:
|
||||
parsed_motd.append(Formatting(clean_element))
|
||||
except ValueError:
|
||||
# just a text
|
||||
parsed_motd.append(element)
|
||||
else:
|
||||
parsed_motd.append(element)
|
||||
|
||||
return parsed_motd
|
||||
|
||||
@classmethod
|
||||
def _parse_as_dict(
|
||||
cls,
|
||||
item: RawJavaResponseMotdWhenDict,
|
||||
*,
|
||||
bedrock: bool = False,
|
||||
auto_add: list[ParsedMotdComponent] | None = None,
|
||||
) -> list[ParsedMotdComponent]:
|
||||
"""Parse a MOTD when it's dict.
|
||||
|
||||
:param item: :class:`dict` directly from the server.
|
||||
:param bedrock: Is the server Bedrock Edition?
|
||||
Nothing does here, just going to :meth:`._parse_as_str` while parsing ``text`` field.
|
||||
:param auto_add: Values to add on this item.
|
||||
Most time, this is :class:`Formatting` from top level.
|
||||
:returns: :obj:`ParsedMotdComponent` list, which need to be passed to ``__init__``.
|
||||
"""
|
||||
parsed_motd: list[ParsedMotdComponent] = auto_add if auto_add is not None else []
|
||||
|
||||
if (color := item.get("color")) is not None:
|
||||
parsed_motd.append(cls._parse_color(color))
|
||||
|
||||
for style_key, style_val in Formatting.__members__.items():
|
||||
lowered_style_key = style_key.lower()
|
||||
if item.get(lowered_style_key) is False:
|
||||
try:
|
||||
parsed_motd.remove(style_val)
|
||||
except ValueError:
|
||||
# some servers set the formatting keys to false here, even without it ever being set to true before
|
||||
continue
|
||||
elif item.get(lowered_style_key) is not None:
|
||||
parsed_motd.append(style_val)
|
||||
|
||||
if (text := item.get("text")) is not None:
|
||||
parsed_motd.extend(cls._parse_as_str(text, bedrock=bedrock))
|
||||
if (translate := item.get("translate")) is not None:
|
||||
parsed_motd.append(TranslationTag(translate))
|
||||
parsed_motd.append(Formatting.RESET)
|
||||
|
||||
if "extra" in item:
|
||||
auto_add = list(filter(lambda e: type(e) is Formatting and e != Formatting.RESET, parsed_motd))
|
||||
|
||||
for element in item["extra"]:
|
||||
parsed_motd.extend(
|
||||
cls._parse_as_dict(element, auto_add=auto_add.copy())
|
||||
if isinstance(element, dict)
|
||||
else auto_add + cls._parse_as_str(element, bedrock=bedrock)
|
||||
)
|
||||
|
||||
return parsed_motd
|
||||
|
||||
@staticmethod
|
||||
def _parse_color(color: str) -> ParsedMotdComponent:
|
||||
"""Parse a color string."""
|
||||
try:
|
||||
return MinecraftColor[color.upper()]
|
||||
except KeyError:
|
||||
if color == "reset":
|
||||
# Minecraft servers actually can't return {"reset": True}, instead, they treat
|
||||
# reset as a color and set {"color": "reset"}. However logically, reset is
|
||||
# a formatting, and it resets both color and other formatting, so we use
|
||||
# `Formatting.RESET` here.
|
||||
#
|
||||
# see `color` field in
|
||||
# https://minecraft.wiki/w/Java_Edition_protocol/Chat?oldid=2763811#Shared_between_all_components
|
||||
return Formatting.RESET
|
||||
|
||||
# Last attempt: try parsing as HTML (hex rgb) color. Some servers use these to
|
||||
# achieve gradients.
|
||||
try:
|
||||
return WebColor.from_hex(color)
|
||||
except ValueError:
|
||||
raise ValueError(f"Unable to parse color: {color!r}, report this!")
|
||||
|
||||
def simplify(self) -> Self:
|
||||
"""Create new MOTD without unused elements.
|
||||
|
||||
After parsing, the MOTD may contain some unused elements, like empty strings, or formattings/colors
|
||||
that don't apply to anything. This method is responsible for creating a new motd with all such elements
|
||||
removed, providing a much cleaner representation.
|
||||
|
||||
:returns: New simplified MOTD, with any unused elements removed.
|
||||
"""
|
||||
parsed = self.parsed.copy()
|
||||
old_parsed: list[ParsedMotdComponent] | None = None
|
||||
|
||||
while parsed != old_parsed:
|
||||
old_parsed = parsed.copy()
|
||||
unused_elements = get_unused_elements(parsed)
|
||||
parsed = [el for index, el in enumerate(parsed) if index not in unused_elements]
|
||||
|
||||
parsed = squash_nearby_strings(parsed)
|
||||
return self.__class__(parsed, self.raw, bedrock=self.bedrock)
|
||||
|
||||
def to_plain(self) -> str:
|
||||
"""Get plain text from a MOTD, without any colors/formatting.
|
||||
|
||||
This is just a shortcut to :class:`~mcstatus.motd.transformers.PlainTransformer`.
|
||||
"""
|
||||
return PlainTransformer().transform(self.parsed)
|
||||
|
||||
def to_minecraft(self) -> str:
|
||||
"""Get Minecraft variant from a MOTD.
|
||||
|
||||
This is just a shortcut to :class:`~mcstatus.motd.transformers.MinecraftTransformer`.
|
||||
|
||||
.. note:: This will always use ``§``, even if in original MOTD used ``&``.
|
||||
"""
|
||||
return MinecraftTransformer().transform(self.parsed)
|
||||
|
||||
def to_html(self) -> str:
|
||||
"""Get HTML from a MOTD.
|
||||
|
||||
This is just a shortcut to :class:`~mcstatus.motd.transformers.HtmlTransformer`.
|
||||
"""
|
||||
return HtmlTransformer(bedrock=self.bedrock).transform(self.parsed)
|
||||
|
||||
def to_ansi(self) -> str:
|
||||
"""Get ANSI variant from a MOTD.
|
||||
|
||||
This is just a shortcut to :class:`~mcstatus.motd.transformers.AnsiTransformer`.
|
||||
|
||||
.. note:: We support only ANSI 24 bit colors, please implement your own transformer if you need other standards.
|
||||
|
||||
.. seealso:: https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
"""
|
||||
return AnsiTransformer().transform(self.parsed)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
126
venv/lib/python3.11/site-packages/mcstatus/motd/components.py
Normal file
126
venv/lib/python3.11/site-packages/mcstatus/motd/components.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
|
||||
class Formatting(Enum):
|
||||
"""Enum for Formatting codes.
|
||||
|
||||
See `Minecraft wiki <https://minecraft.wiki/w/Formatting_codes#Formatting_codes>`__
|
||||
for more info.
|
||||
|
||||
.. note::
|
||||
:attr:`.STRIKETHROUGH` and :attr:`.UNDERLINED` don't work on Bedrock, which our parser
|
||||
doesn't keep it in mind. See `MCPE-41729 <https://bugs.mojang.com/browse/MCPE-41729>`_.
|
||||
"""
|
||||
|
||||
BOLD = "l"
|
||||
ITALIC = "o"
|
||||
UNDERLINED = "n"
|
||||
STRIKETHROUGH = "m"
|
||||
OBFUSCATED = "k"
|
||||
RESET = "r"
|
||||
|
||||
|
||||
class MinecraftColor(Enum):
|
||||
"""Enum for Color codes.
|
||||
|
||||
See `Minecraft wiki <https://minecraft.wiki/w/Formatting_codes#Color_codes>`_
|
||||
for more info.
|
||||
"""
|
||||
|
||||
BLACK = "0"
|
||||
DARK_BLUE = "1"
|
||||
DARK_GREEN = "2"
|
||||
DARK_AQUA = "3"
|
||||
DARK_RED = "4"
|
||||
DARK_PURPLE = "5"
|
||||
GOLD = "6"
|
||||
GRAY = "7"
|
||||
DARK_GRAY = "8"
|
||||
BLUE = "9"
|
||||
GREEN = "a"
|
||||
AQUA = "b"
|
||||
RED = "c"
|
||||
LIGHT_PURPLE = "d"
|
||||
YELLOW = "e"
|
||||
WHITE = "f"
|
||||
|
||||
# Only for bedrock
|
||||
MINECOIN_GOLD = "g"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WebColor:
|
||||
"""Raw HTML color from MOTD.
|
||||
|
||||
Can be found in MOTD when someone uses gradient.
|
||||
|
||||
.. note:: Actually supported in Minecraft 1.16+ only.
|
||||
"""
|
||||
|
||||
hex: str
|
||||
rgb: tuple[int, int, int]
|
||||
|
||||
@classmethod
|
||||
def from_hex(cls, hex: str) -> Self:
|
||||
"""Construct web color using hex color string.
|
||||
|
||||
:raises ValueError: Invalid hex color string.
|
||||
:returns: New :class:`WebColor` instance.
|
||||
"""
|
||||
hex = hex.lstrip("#")
|
||||
|
||||
if len(hex) not in (3, 6):
|
||||
raise ValueError(f"Got too long/short hex color: {'#' + hex!r}")
|
||||
if len(hex) == 3:
|
||||
hex = "{0}{0}{1}{1}{2}{2}".format(*hex)
|
||||
|
||||
try:
|
||||
rgb = t.cast("tuple[int, int, int]", tuple(int(hex[i : i + 2], 16) for i in (0, 2, 4)))
|
||||
except ValueError:
|
||||
raise ValueError(f"Failed to parse given hex color: {'#' + hex!r}")
|
||||
|
||||
return cls.from_rgb(rgb)
|
||||
|
||||
@classmethod
|
||||
def from_rgb(cls, rgb: tuple[int, int, int]) -> Self:
|
||||
"""Construct web color using rgb color tuple.
|
||||
|
||||
:raises ValueError: When RGB color is out of its 8-bit range.
|
||||
:returns: New :class:`WebColor` instance.
|
||||
"""
|
||||
cls._check_rgb(rgb)
|
||||
|
||||
hex = "#{:02x}{:02x}{:02x}".format(*rgb)
|
||||
return cls(hex, rgb)
|
||||
|
||||
@staticmethod
|
||||
def _check_rgb(rgb: tuple[int, int, int]) -> None:
|
||||
index_to_color_name = {0: "red", 1: "green", 2: "blue"}
|
||||
|
||||
for index, value in enumerate(rgb):
|
||||
if not 255 >= value >= 0:
|
||||
color_name = index_to_color_name[index]
|
||||
raise ValueError(f"RGB color byte out of its 8-bit range (0-255) for {color_name} ({value=})")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TranslationTag:
|
||||
"""Represents a ``translate`` field in server's answer.
|
||||
|
||||
This just exists, but is completely ignored by our transformers.
|
||||
You can find translation tags in :attr:`Motd.parsed <mcstatus.motd.Motd.parsed>` attribute.
|
||||
|
||||
.. seealso:: `Minecraft's wiki. <https://minecraft.wiki/w/Raw_JSON_text_format#Translated_Text>`__
|
||||
"""
|
||||
|
||||
id: str
|
||||
|
||||
|
||||
ParsedMotdComponent: TypeAlias = "Formatting | MinecraftColor | WebColor | TranslationTag | str"
|
||||
194
venv/lib/python3.11/site-packages/mcstatus/motd/simplifies.py
Normal file
194
venv/lib/python3.11/site-packages/mcstatus/motd/simplifies.py
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
from collections.abc import Sequence
|
||||
|
||||
from mcstatus.motd.components import Formatting, MinecraftColor, ParsedMotdComponent, WebColor
|
||||
|
||||
_PARSED_MOTD_COMPONENTS_TYPEVAR = t.TypeVar("_PARSED_MOTD_COMPONENTS_TYPEVAR", bound="list[ParsedMotdComponent]")
|
||||
|
||||
|
||||
def get_unused_elements(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
|
||||
"""Get indices of all items which are unused and can be safely removed from the MOTD.
|
||||
|
||||
This is a wrapper method around several unused item collection methods.
|
||||
"""
|
||||
to_remove: set[int] = set()
|
||||
|
||||
for simplifier in [
|
||||
get_double_items,
|
||||
get_double_colors,
|
||||
get_formatting_before_color,
|
||||
get_meaningless_resets_and_colors,
|
||||
get_empty_text,
|
||||
get_end_non_text,
|
||||
]:
|
||||
to_remove.update(simplifier(parsed))
|
||||
|
||||
return to_remove
|
||||
|
||||
|
||||
def squash_nearby_strings(parsed: _PARSED_MOTD_COMPONENTS_TYPEVAR) -> _PARSED_MOTD_COMPONENTS_TYPEVAR:
|
||||
"""Squash duplicate strings together.
|
||||
|
||||
Note that this function doesn't create a copy of passed array, it modifies it.
|
||||
This is what those typevars are for in the function signature.
|
||||
"""
|
||||
# in order to not break indexes, we need to fill values and then remove them after the loop
|
||||
fillers: set[int] = set()
|
||||
for index, item in enumerate(parsed):
|
||||
if not isinstance(item, str):
|
||||
continue
|
||||
|
||||
try:
|
||||
next_item = parsed[index + 1]
|
||||
except IndexError: # Last item (without any next item)
|
||||
break
|
||||
|
||||
if isinstance(next_item, str):
|
||||
parsed[index + 1] = item + next_item
|
||||
fillers.add(index)
|
||||
|
||||
for already_removed, index_to_remove in enumerate(fillers):
|
||||
parsed.pop(index_to_remove - already_removed)
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
def get_double_items(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
|
||||
"""Get indices of all doubled items that can be removed.
|
||||
|
||||
Removes any items that are followed by an item of the same kind (compared using ``__eq__``).
|
||||
"""
|
||||
to_remove: set[int] = set()
|
||||
|
||||
for index, item in enumerate(parsed):
|
||||
try:
|
||||
next_item = parsed[index + 1]
|
||||
except IndexError: # Last item (without any next item)
|
||||
break
|
||||
|
||||
if isinstance(item, (Formatting, MinecraftColor, WebColor)) and item == next_item:
|
||||
to_remove.add(index)
|
||||
|
||||
return to_remove
|
||||
|
||||
|
||||
def get_double_colors(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
|
||||
"""Get indices of all doubled color items.
|
||||
|
||||
As colors (obviously) override each other, we only ever care about the last one, ignore
|
||||
the previous ones. (for example: specifying red color, then orange, then yellow, then some text
|
||||
will just result in yellow text)
|
||||
"""
|
||||
to_remove: set[int] = set()
|
||||
|
||||
prev_color: int | None = None
|
||||
for index, item in enumerate(parsed):
|
||||
if isinstance(item, (MinecraftColor, WebColor)):
|
||||
# If we found a color after another, remove the previous color
|
||||
if prev_color is not None:
|
||||
to_remove.add(prev_color)
|
||||
prev_color = index
|
||||
|
||||
# If we find a string, that's what our color we found previously applies to,
|
||||
# set prev_color to None, marking this color as used
|
||||
if isinstance(item, str):
|
||||
prev_color = None
|
||||
|
||||
return to_remove
|
||||
|
||||
|
||||
def get_formatting_before_color(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
|
||||
"""Obtain indices of all unused formatting items before colors.
|
||||
|
||||
Colors override any formatting before them, meaning we only ever care about the color, and can
|
||||
ignore all formatting before it. (For example: specifying bold formatting, then italic, then yellow,
|
||||
will just result in yellow text.)
|
||||
"""
|
||||
to_remove: set[int] = set()
|
||||
|
||||
collected_formattings = []
|
||||
for index, item in enumerate(parsed):
|
||||
# Collect the indices of formatting items
|
||||
if isinstance(item, Formatting):
|
||||
collected_formattings.append(index)
|
||||
|
||||
# Only run checks if we have some collected formatting items
|
||||
if len(collected_formattings) == 0:
|
||||
continue
|
||||
|
||||
# If there's a string after some formattings, the formattings apply to it.
|
||||
# This means they're not unused, remove them.
|
||||
if isinstance(item, str) and not item.isspace():
|
||||
collected_formattings = []
|
||||
continue
|
||||
|
||||
# If there's a color after some formattings, these formattings will be overridden
|
||||
# as colors reset everything. This makes these formattings pointless, mark them
|
||||
# for removal.
|
||||
if isinstance(item, (MinecraftColor, WebColor)):
|
||||
to_remove.update(collected_formattings)
|
||||
collected_formattings = []
|
||||
return to_remove
|
||||
|
||||
|
||||
def get_empty_text(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
|
||||
"""Get indices of all empty text items.
|
||||
|
||||
Empty strings in motd serve no purpose and can be marked for removal.
|
||||
"""
|
||||
to_remove: set[int] = set()
|
||||
|
||||
for index, item in enumerate(parsed):
|
||||
if isinstance(item, str) and len(item) == 0:
|
||||
to_remove.add(index)
|
||||
|
||||
return to_remove
|
||||
|
||||
|
||||
def get_end_non_text(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
|
||||
"""Get indices of all trailing items, found after the last text component.
|
||||
|
||||
Any color/formatting items only make sense when they apply to some text.
|
||||
If there are some at the end, after the last text, they're pointless and
|
||||
can be removed.
|
||||
"""
|
||||
to_remove: set[int] = set()
|
||||
|
||||
for rev_index, item in enumerate(reversed(parsed)):
|
||||
# The moment we find our last string, stop the loop
|
||||
if isinstance(item, str):
|
||||
break
|
||||
|
||||
# Remove any color/formatting that doesn't apply to text
|
||||
if isinstance(item, (MinecraftColor, WebColor, Formatting)):
|
||||
index = len(parsed) - 1 - rev_index
|
||||
to_remove.add(index)
|
||||
|
||||
return to_remove
|
||||
|
||||
|
||||
def get_meaningless_resets_and_colors(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
|
||||
to_remove: set[int] = set()
|
||||
|
||||
active_color: MinecraftColor | WebColor | None = None
|
||||
active_formatting: Formatting | None = None
|
||||
for index, item in enumerate(parsed):
|
||||
if isinstance(item, (MinecraftColor, WebColor)):
|
||||
if active_color == item:
|
||||
to_remove.add(index)
|
||||
active_color = item
|
||||
continue
|
||||
if isinstance(item, Formatting):
|
||||
if item == Formatting.RESET:
|
||||
if active_color is None and active_formatting is None:
|
||||
to_remove.add(index)
|
||||
continue
|
||||
active_color, active_formatting = None, None
|
||||
continue
|
||||
if active_formatting == item:
|
||||
to_remove.add(index)
|
||||
active_formatting = item
|
||||
|
||||
return to_remove
|
||||
223
venv/lib/python3.11/site-packages/mcstatus/motd/transformers.py
Normal file
223
venv/lib/python3.11/site-packages/mcstatus/motd/transformers.py
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import typing as t
|
||||
from collections.abc import Callable, Sequence
|
||||
|
||||
from mcstatus.motd.components import Formatting, MinecraftColor, ParsedMotdComponent, TranslationTag, WebColor
|
||||
|
||||
_HOOK_RETURN_TYPE = t.TypeVar("_HOOK_RETURN_TYPE")
|
||||
_END_RESULT_TYPE = t.TypeVar("_END_RESULT_TYPE")
|
||||
|
||||
|
||||
class BaseTransformer(abc.ABC, t.Generic[_HOOK_RETURN_TYPE, _END_RESULT_TYPE]):
|
||||
"""Base motd transformer class.
|
||||
|
||||
Motd transformer is responsible for providing a way to generate an alternative representation
|
||||
of motd, such as one that is able to be printed in the terminal.
|
||||
"""
|
||||
|
||||
def transform(self, motd_components: Sequence[ParsedMotdComponent]) -> _END_RESULT_TYPE:
|
||||
return self._format_output([handled for component in motd_components for handled in self._handle_component(component)])
|
||||
|
||||
@abc.abstractmethod
|
||||
def _format_output(self, results: list[_HOOK_RETURN_TYPE]) -> _END_RESULT_TYPE: ...
|
||||
|
||||
def _handle_component(
|
||||
self, component: ParsedMotdComponent
|
||||
) -> tuple[_HOOK_RETURN_TYPE, _HOOK_RETURN_TYPE] | tuple[_HOOK_RETURN_TYPE]:
|
||||
handler: Callable[[ParsedMotdComponent], _HOOK_RETURN_TYPE] = {
|
||||
MinecraftColor: self._handle_minecraft_color,
|
||||
WebColor: self._handle_web_color,
|
||||
Formatting: self._handle_formatting,
|
||||
TranslationTag: self._handle_translation_tag,
|
||||
str: self._handle_str,
|
||||
}[type(component)]
|
||||
|
||||
additional = None
|
||||
if isinstance(component, MinecraftColor):
|
||||
additional = self._handle_formatting(Formatting.RESET)
|
||||
|
||||
return (additional, handler(component)) if additional is not None else (handler(component),)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _handle_str(self, element: str, /) -> _HOOK_RETURN_TYPE: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def _handle_translation_tag(self, _: TranslationTag, /) -> _HOOK_RETURN_TYPE: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def _handle_web_color(self, element: WebColor, /) -> _HOOK_RETURN_TYPE: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def _handle_formatting(self, element: Formatting, /) -> _HOOK_RETURN_TYPE: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def _handle_minecraft_color(self, element: MinecraftColor, /) -> _HOOK_RETURN_TYPE: ...
|
||||
|
||||
|
||||
class NothingTransformer(BaseTransformer[str, str]):
|
||||
"""Transformer that transforms all elements into empty strings.
|
||||
|
||||
This transformer acts as a base for other transformers with string result type.
|
||||
"""
|
||||
|
||||
def _format_output(self, results: list[str]) -> str:
|
||||
return "".join(results)
|
||||
|
||||
def _handle_str(self, element: str, /) -> str:
|
||||
return ""
|
||||
|
||||
def _handle_minecraft_color(self, element: MinecraftColor, /) -> str:
|
||||
return ""
|
||||
|
||||
def _handle_web_color(self, element: WebColor, /) -> str:
|
||||
return ""
|
||||
|
||||
def _handle_formatting(self, element: Formatting, /) -> str:
|
||||
return ""
|
||||
|
||||
def _handle_translation_tag(self, element: TranslationTag, /) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class PlainTransformer(NothingTransformer):
|
||||
def _handle_str(self, element: str, /) -> str:
|
||||
return element
|
||||
|
||||
|
||||
class MinecraftTransformer(PlainTransformer):
|
||||
def _handle_component(self, component: ParsedMotdComponent) -> tuple[str, str] | tuple[str]:
|
||||
result = super()._handle_component(component)
|
||||
if len(result) == 2:
|
||||
return (result[1],)
|
||||
return result
|
||||
|
||||
def _handle_minecraft_color(self, element: MinecraftColor, /) -> str:
|
||||
return "§" + element.value
|
||||
|
||||
def _handle_formatting(self, element: Formatting, /) -> str:
|
||||
return "§" + element.value
|
||||
|
||||
|
||||
class HtmlTransformer(PlainTransformer):
|
||||
"""Formatter for HTML variant of a MOTD.
|
||||
|
||||
.. warning::
|
||||
You should implement obfuscated CSS class yourself (name - ``obfuscated``).
|
||||
See `this answer <https://stackoverflow.com/a/30313558>`_ as example.
|
||||
"""
|
||||
|
||||
FORMATTING_TO_HTML_TAGS = {
|
||||
Formatting.BOLD: "b",
|
||||
Formatting.STRIKETHROUGH: "s",
|
||||
Formatting.ITALIC: "i",
|
||||
Formatting.UNDERLINED: "u",
|
||||
}
|
||||
MINECRAFT_COLOR_TO_RGB_BEDROCK = {
|
||||
MinecraftColor.BLACK: ((0, 0, 0), (0, 0, 0)),
|
||||
MinecraftColor.DARK_BLUE: ((0, 0, 170), (0, 0, 42)),
|
||||
MinecraftColor.DARK_GREEN: ((0, 170, 0), (0, 42, 0)),
|
||||
MinecraftColor.DARK_AQUA: ((0, 170, 170), (0, 42, 42)),
|
||||
MinecraftColor.DARK_RED: ((170, 0, 0), (42, 0, 0)),
|
||||
MinecraftColor.DARK_PURPLE: ((170, 0, 170), (42, 0, 42)),
|
||||
MinecraftColor.GOLD: ((255, 170, 0), (64, 42, 0)),
|
||||
MinecraftColor.GRAY: ((170, 170, 170), (42, 42, 42)),
|
||||
MinecraftColor.DARK_GRAY: ((85, 85, 85), (21, 21, 21)),
|
||||
MinecraftColor.BLUE: ((85, 85, 255), (21, 21, 63)),
|
||||
MinecraftColor.GREEN: ((85, 255, 85), (21, 63, 21)),
|
||||
MinecraftColor.AQUA: ((85, 255, 255), (21, 63, 63)),
|
||||
MinecraftColor.RED: ((255, 85, 85), (63, 21, 21)),
|
||||
MinecraftColor.LIGHT_PURPLE: ((255, 85, 255), (63, 21, 63)),
|
||||
MinecraftColor.YELLOW: ((255, 255, 85), (63, 63, 21)),
|
||||
MinecraftColor.WHITE: ((255, 255, 255), (63, 63, 63)),
|
||||
MinecraftColor.MINECOIN_GOLD: ((221, 214, 5), (55, 53, 1)),
|
||||
}
|
||||
MINECRAFT_COLOR_TO_RGB_JAVA = MINECRAFT_COLOR_TO_RGB_BEDROCK.copy()
|
||||
MINECRAFT_COLOR_TO_RGB_JAVA[MinecraftColor.GOLD] = ((255, 170, 0), (42, 42, 0))
|
||||
|
||||
def __init__(self, *, bedrock: bool = False) -> None:
|
||||
self.bedrock = bedrock
|
||||
self.on_reset: list[str] = []
|
||||
|
||||
def transform(self, motd_components: Sequence[ParsedMotdComponent]) -> str:
|
||||
self.on_reset = []
|
||||
return super().transform(motd_components)
|
||||
|
||||
def _format_output(self, results: list[str]) -> str:
|
||||
return "<p>" + super()._format_output(results) + "".join(self.on_reset) + "</p>"
|
||||
|
||||
def _handle_minecraft_color(self, element: MinecraftColor, /) -> str:
|
||||
color_map = self.MINECRAFT_COLOR_TO_RGB_BEDROCK if self.bedrock else self.MINECRAFT_COLOR_TO_RGB_JAVA
|
||||
fg_color, bg_color = color_map[element]
|
||||
|
||||
self.on_reset.append("</span>")
|
||||
return f"<span style='color:rgb{fg_color};text-shadow:0 0 1px rgb{bg_color}'>"
|
||||
|
||||
def _handle_web_color(self, element: WebColor, /) -> str:
|
||||
self.on_reset.append("</span>")
|
||||
return f"<span style='color:rgb{element.rgb}'>"
|
||||
|
||||
def _handle_formatting(self, element: Formatting, /) -> str:
|
||||
if element is Formatting.RESET:
|
||||
to_return = "".join(self.on_reset)
|
||||
self.on_reset = []
|
||||
return to_return
|
||||
|
||||
if element is Formatting.OBFUSCATED:
|
||||
self.on_reset.append("</span>")
|
||||
return "<span class=obfuscated>"
|
||||
|
||||
tag_name = self.FORMATTING_TO_HTML_TAGS[element]
|
||||
self.on_reset.append(f"</{tag_name}>")
|
||||
return f"<{tag_name}>"
|
||||
|
||||
|
||||
class AnsiTransformer(PlainTransformer):
|
||||
FORMATTING_TO_ANSI_TAGS = {
|
||||
Formatting.BOLD: "1",
|
||||
Formatting.STRIKETHROUGH: "9",
|
||||
Formatting.ITALIC: "3",
|
||||
Formatting.UNDERLINED: "4",
|
||||
Formatting.OBFUSCATED: "5",
|
||||
}
|
||||
MINECRAFT_COLOR_TO_RGB = {
|
||||
MinecraftColor.BLACK: (0, 0, 0),
|
||||
MinecraftColor.DARK_BLUE: (0, 0, 170),
|
||||
MinecraftColor.DARK_GREEN: (0, 170, 0),
|
||||
MinecraftColor.DARK_AQUA: (0, 170, 170),
|
||||
MinecraftColor.DARK_RED: (170, 0, 0),
|
||||
MinecraftColor.DARK_PURPLE: (170, 0, 170),
|
||||
MinecraftColor.GOLD: (255, 170, 0),
|
||||
MinecraftColor.GRAY: (170, 170, 170),
|
||||
MinecraftColor.DARK_GRAY: (85, 85, 85),
|
||||
MinecraftColor.BLUE: (85, 85, 255),
|
||||
MinecraftColor.GREEN: (85, 255, 85),
|
||||
MinecraftColor.AQUA: (85, 255, 255),
|
||||
MinecraftColor.RED: (255, 85, 85),
|
||||
MinecraftColor.LIGHT_PURPLE: (255, 85, 255),
|
||||
MinecraftColor.YELLOW: (255, 255, 85),
|
||||
MinecraftColor.WHITE: (255, 255, 255),
|
||||
MinecraftColor.MINECOIN_GOLD: (221, 214, 5),
|
||||
}
|
||||
|
||||
def ansi_color(self, color: tuple[int, int, int] | MinecraftColor) -> str:
|
||||
"""Transform RGB color to ANSI color code."""
|
||||
if isinstance(color, MinecraftColor):
|
||||
color = self.MINECRAFT_COLOR_TO_RGB[color]
|
||||
|
||||
return "\033[38;2;{0};{1};{2}m".format(*color)
|
||||
|
||||
def _format_output(self, results: list[str]) -> str:
|
||||
return "\033[0m" + super()._format_output(results) + "\033[0m"
|
||||
|
||||
def _handle_minecraft_color(self, element: MinecraftColor, /) -> str:
|
||||
return self.ansi_color(element)
|
||||
|
||||
def _handle_web_color(self, element: WebColor, /) -> str:
|
||||
return self.ansi_color(element.rgb)
|
||||
|
||||
def _handle_formatting(self, element: Formatting, /) -> str:
|
||||
if element is Formatting.RESET:
|
||||
return "\033[0m"
|
||||
return "\033[" + self.FORMATTING_TO_ANSI_TAGS[element] + "m"
|
||||
Loading…
Add table
Add a link
Reference in a new issue