diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index ec2b1b3378..265ce774ae 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -673,6 +673,10 @@ class ANSIString(str, metaclass=ANSIMeta): and taken literally the second time around. """ + # A compiled Regex for the format mini-language: https://docs.python.org/3/library/string.html#formatspec + re_format = re.compile(r"(?i)(?P(?P.)?(?P\<|\>|\=|\^))?(?P\+|\-| )?(?P\#)?" + r"(?P0)?(?P\d+)?(?P\_|\,)?(?:\.(?P\d+))?" + r"(?Pb|c|d|e|E|f|F|g|G|n|o|s|x|X|%)?") def __new__(cls, *args, **kwargs): """ @@ -733,6 +737,47 @@ class ANSIString(str, metaclass=ANSIMeta): def __str__(self): return self._raw_string + def __format__(self, format_spec): + """ + This magic method covers ANSIString's behavior within a str.format() or f-string. + + Current features supported: fill, align, width. + + Args: + format_spec (str): The format specification passed by f-string or str.format(). This is a string such as + "0<30" which would mean "left justify to 30, filling with zeros". The full specification can be found + at https://docs.python.org/3/library/string.html#formatspec + + Returns: + ansi_str (str): The formatted ANSIString's .raw() form, for display. + """ + # This calls the compiled regex stored on ANSIString's class to analyze the format spec. + # It returns a dictionary. + format_data = self.re_format.match(format_spec).groupdict() + clean = self.clean() + base_output = ANSIString(self.raw()) + align = format_data.get('align', '<') + fill = format_data.get('fill', ' ') + + # Need to coerce width into an integer. We can be certain that it's numeric thanks to regex. + width = format_data.get('width', None) + if width is None: + width = len(clean) + else: + width = int(width) + + if align == '<': + base_output = self.ljust(width, fill) + elif align == '>': + base_output = self.rjust(width, fill) + elif align == '^': + base_output = self.center(width, fill) + elif align == '=': + pass + + # Return the raw string with ANSI markup, ready to be displayed. + return base_output.raw() + def __repr__(self): """ Let's make the repr the command that would actually be used to