Fix merge conflicts

This commit is contained in:
Griatch 2022-06-04 13:26:58 +02:00
commit 1290f648d5
20 changed files with 769 additions and 761 deletions

View file

@ -166,6 +166,12 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
- New `at_pre_object_receive(obj, source_location)` method on Objects. Called on - New `at_pre_object_receive(obj, source_location)` method on Objects. Called on
destination, mimicking behavior of `at_pre_move` hook - returning False will abort move. destination, mimicking behavior of `at_pre_move` hook - returning False will abort move.
- New `at_pre_object_leave(obj, destination)` method on Objects. Called on - New `at_pre_object_leave(obj, destination)` method on Objects. Called on
- The db pickle-serializer now checks for methods `__serialize_dbobjs__` and `__deserialize_dbobjs__`
to allow custom packing/unpacking of nested dbobjs, to allow storing in Attribute.
- Optimizations to rpsystem contrib performance. Breaking change: `.get_sdesc()` will
now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal)
- Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal)
- Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal)
## Evennia 0.9.5 ## Evennia 0.9.5

View file

@ -193,7 +193,7 @@ or in the chat.
[pep8]: http://www.python.org/dev/peps/pep-0008 [pep8]: http://www.python.org/dev/peps/pep-0008
[pep8tool]: https://pypi.python.org/pypi/pep8 [pep8tool]: https://pypi.python.org/pypi/pep8
[googlestyle]: http://www.sphinx-doc.org/en/stable/ext/example_google.html [googlestyle]: https://www.sphinx-doc.org/en/master/usage/extensions/example_google.html
[githubmarkdown]: https://help.github.com/articles/github-flavored-markdown/ [githubmarkdown]: https://help.github.com/articles/github-flavored-markdown/
[markdown-hilight]: https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting [markdown-hilight]: https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting
[command-docstrings]: https://github.com/evennia/evennia/wiki/Using%20MUX%20As%20a%20Standard#documentation-policy [command-docstrings]: https://github.com/evennia/evennia/wiki/Using%20MUX%20As%20a%20Standard#documentation-policy

View file

@ -152,6 +152,8 @@ mv-local:
@echo "Documentation built (multiversion + autodocs)." @echo "Documentation built (multiversion + autodocs)."
@echo "To see result, open evennia/docs/build/html/<version>/index.html in a browser." @echo "To see result, open evennia/docs/build/html/<version>/index.html in a browser."
# note - don't run the following manually, the result will clash with the result
# of the github actions!
deploy: deploy:
make _multiversion-deploy make _multiversion-deploy
@echo "Documentation deployed." @echo "Documentation deployed."

View file

@ -328,13 +328,13 @@ values into a string representation before storing it to the database. This is d
With a single object, we mean anything that is *not iterable*, like numbers, strings or custom class With a single object, we mean anything that is *not iterable*, like numbers, strings or custom class
instances without the `__iter__` method. instances without the `__iter__` method.
* You can generally store any non-iterable Python entity that can be pickled. * You can generally store any non-iterable Python entity that can be _pickled_.
* Single database objects/typeclasses can be stored, despite them normally not being possible * Single database objects/typeclasses can be stored, despite them normally not being possible
to pickle. Evennia wil convert them to an internal representation using their classname, to pickle. Evennia will convert them to an internal representation using theihr classname,
database-id and creation-date with a microsecond precision. When retrieving, the object database-id and creation-date with a microsecond precision. When retrieving, the object
instance will be re-fetched from the database using this information. instance will be re-fetched from the database using this information.
* To convert the database object, Evennia must know it's there. If you *hide* a database object * If you 'hide' a db-obj as a property on a custom class, Evennia will not be
inside a non-iterable class, you will run into errors - this is not supported! able to find it to serialize it. For that you need to help it out (see below).
```{code-block} python ```{code-block} python
:caption: Valid assignments :caption: Valid assignments
@ -345,16 +345,55 @@ obj.db.test1 = False
# a database object (will be stored as an internal representation) # a database object (will be stored as an internal representation)
obj.db.test2 = myobj obj.db.test2 = myobj
``` ```
As mentioned, Evennia will not be able to automatically serialize db-objects
'hidden' in arbitrary properties on an object. This will lead to an error
when saving the Attribute.
```{code-block} python ```{code-block} python
:caption: Invalid, 'hidden' dbobject :caption: Invalid, 'hidden' dbobject
# example of storing an invalid, "hidden" dbobject in Attribute
# example of an invalid, "hidden" dbobject
class Container: class Container:
def __init__(self, mydbobj): def __init__(self, mydbobj):
# no way for Evennia to know this is a database object! # no way for Evennia to know this is a database object!
self.mydbobj = mydbobj self.mydbobj = mydbobj
# let's assume myobj is a db-object
container = Container(myobj) container = Container(myobj)
obj.db.invalid = container # will cause error! obj.db.mydata = container # will raise error!
```
By adding two methods `__serialize_dbobjs__` and `__deserialize_dbobjs__` to the
object you want to save, you can pre-serialize and post-deserialize all 'hidden'
objects before Evennia's main serializer gets to work. Inside these methods, use Evennia's
[evennia.utils.dbserialize.dbserialize](api:evennia.utils.dbserialize.dbserialize) and
[dbunserialize](api:evennia.utils.dbserialize.dbunserialize) functions to safely
serialize the db-objects you want to store.
```{code-block} python
:caption: Fixing an invalid 'hidden' dbobj for storing in Attribute
from evennia.utils import dbserialize # important
class Container:
def __init__(self, mydbobj):
# A 'hidden' db-object
self.mydbobj = mydbobj
def __serialize_dbobjs__(self):
"""This is called before serialization and allows
us to custom-handle those 'hidden' dbobjs"""
self.mydbobj = dbserialize.dbserialize(self.mydbobj
def __deserialize_dbobjs__(self):
"""This is called after deserialization and allows you to
restore the 'hidden' dbobjs you serialized before"""
self.mydbobj = dbserialize.dbunserialize(self.mydbobj)
# let's assume myobj is a db-object
container = Container(myobj)
obj.db.mydata = container # will now work fine!
``` ```
### Storing multiple objects ### Storing multiple objects
@ -404,6 +443,12 @@ obj.db.test8[2]["test"] = 5
# test8 is now [4,2,{"test":5}] # test8 is now [4,2,{"test":5}]
``` ```
Note that if make some advanced iterable object, and store an db-object on it in
a way such that it is _not_ returned by iterating over it, you have created a
'hidden' db-object. See [the previous section](#storing-single-objects) for how
to tell Evennia how to serialize such hidden objects safely.
### Retrieving Mutable objects ### Retrieving Mutable objects
A side effect of the way Evennia stores Attributes is that *mutable* iterables (iterables that can A side effect of the way Evennia stores Attributes is that *mutable* iterables (iterables that can

View file

@ -129,10 +129,11 @@ Contains a very useful list of things to think about when starting your new MUD.
Essential reading for the design of any persistent game Essential reading for the design of any persistent game
world, written by the co-creator of the original game *MUD*. Published in 2003 but it's still as world, written by the co-creator of the original game *MUD*. Published in 2003 but it's still as
relevant now as when it came out. Covers everything you need to know and then some. relevant now as when it came out. Covers everything you need to know and then some.
- Zed A. Shaw *Learn Python the Hard way* ([homepage](https://learnpythonthehardway.org/)) - Despite
the imposing name this book is for the absolute Python/programming beginner. One learns the language When the rights to Designing Virtual Worlds returned to him, Richard Bartle
by gradually creating a small text game! It has been used by multiple users before moving on to made the PDF of his Designing Virtual Worlds freely available through his own
Evennia. *Update: This used to be free to read online, this is no longer the case.* website ([Designing Virtual Worlds](https://mud.co.uk/dvw/)). A direct link to
the PDF can be found [here](https://mud.co.uk/richard/DesigningVirtualWorlds.pdf).
- David M. Beazley *Python Essential Reference (4th ed)* - David M. Beazley *Python Essential Reference (4th ed)*
([amazon page](https://www.amazon.com/Python-Essential-Reference-David-Beazley/dp/0672329786/)) - ([amazon page](https://www.amazon.com/Python-Essential-Reference-David-Beazley/dp/0672329786/)) -
Our recommended book on Python; it not only efficiently summarizes the language but is also Our recommended book on Python; it not only efficiently summarizes the language but is also

View file

@ -2732,7 +2732,7 @@ class CmdExamine(ObjManipCommand):
return return
if ndb_attr and ndb_attr[0]: if ndb_attr and ndb_attr[0]:
return "\n " + " \n".join( return "\n " + "\n ".join(
sorted(self.format_single_attribute(attr) for attr in ndb_attr) sorted(self.format_single_attribute(attr) for attr in ndb_attr)
) )

View file

@ -328,4 +328,4 @@ class GametimeScript(DefaultScript):
callback() callback()
seconds = real_seconds_until(**self.db.gametime) seconds = real_seconds_until(**self.db.gametime)
self.restart(interval=seconds) self.start(interval=seconds,force_restart=True)

View file

@ -4,7 +4,6 @@ Roleplaying emotes and language - Griatch, 2015
""" """
from .rpsystem import EmoteError, SdescError, RecogError, LanguageError # noqa from .rpsystem import EmoteError, SdescError, RecogError, LanguageError # noqa
from .rpsystem import ordered_permutation_regex, regex_tuple_from_key_alias # noqa
from .rpsystem import parse_language, parse_sdescs_and_recogs, send_emote # noqa from .rpsystem import parse_language, parse_sdescs_and_recogs, send_emote # noqa
from .rpsystem import SdescHandler, RecogHandler # noqa from .rpsystem import SdescHandler, RecogHandler # noqa
from .rpsystem import RPCommand, CmdEmote, CmdSay, CmdSdesc, CmdPose, CmdRecog, CmdMask # noqa from .rpsystem import RPCommand, CmdEmote, CmdSay, CmdSdesc, CmdPose, CmdRecog, CmdMask # noqa

View file

@ -53,7 +53,7 @@ Add `RPSystemCmdSet` from this module to your CharacterCmdSet:
# ... # ...
from evennia.contrib.rpg.rpsystem import RPSystemCmdSet <--- from evennia.contrib.rpg.rpsystem.rpsystem import RPSystemCmdSet <---
class CharacterCmdSet(default_cmds.CharacterCmdset): class CharacterCmdSet(default_cmds.CharacterCmdset):
# ... # ...
@ -69,7 +69,7 @@ the typeclasses in this module:
```python ```python
# in mygame/typeclasses/characters.py # in mygame/typeclasses/characters.py
from evennia.contrib.rpg import ContribRPCharacter from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPCharacter
class Character(ContribRPCharacter): class Character(ContribRPCharacter):
# ... # ...
@ -79,7 +79,7 @@ class Character(ContribRPCharacter):
```python ```python
# in mygame/typeclasses/objects.py # in mygame/typeclasses/objects.py
from evennia.contrib.rpg import ContribRPObject from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject
class Object(ContribRPObject): class Object(ContribRPObject):
# ... # ...
@ -89,7 +89,7 @@ class Object(ContribRPObject):
```python ```python
# in mygame/typeclasses/rooms.py # in mygame/typeclasses/rooms.py
from evennia.contrib.rpg import ContribRPRoom from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPRoom
class Room(ContribRPRoom): class Room(ContribRPRoom):
# ... # ...
@ -125,7 +125,7 @@ Extra Installation Instructions:
1. In typeclasses/character.py: 1. In typeclasses/character.py:
Import the `ContribRPCharacter` class: Import the `ContribRPCharacter` class:
`from evennia.contrib.rpg.rpsystem import ContribRPCharacter` `from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPCharacter`
Inherit ContribRPCharacter: Inherit ContribRPCharacter:
Change "class Character(DefaultCharacter):" to Change "class Character(DefaultCharacter):" to
`class Character(ContribRPCharacter):` `class Character(ContribRPCharacter):`
@ -133,13 +133,13 @@ Extra Installation Instructions:
Add `super().at_object_creation()` as the top line. Add `super().at_object_creation()` as the top line.
2. In `typeclasses/rooms.py`: 2. In `typeclasses/rooms.py`:
Import the `ContribRPRoom` class: Import the `ContribRPRoom` class:
`from evennia.contrib.rpg.rpsystem import ContribRPRoom` `from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPRoom`
Inherit `ContribRPRoom`: Inherit `ContribRPRoom`:
Change `class Room(DefaultRoom):` to Change `class Room(DefaultRoom):` to
`class Room(ContribRPRoom):` `class Room(ContribRPRoom):`
3. In `typeclasses/objects.py` 3. In `typeclasses/objects.py`
Import the `ContribRPObject` class: Import the `ContribRPObject` class:
`from evennia.contrib.rpg.rpsystem import ContribRPObject` `from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject`
Inherit `ContribRPObject`: Inherit `ContribRPObject`:
Change `class Object(DefaultObject):` to Change `class Object(DefaultObject):` to
`class Object(ContribRPObject):` `class Object(ContribRPObject):`
@ -149,18 +149,15 @@ Extra Installation Instructions:
""" """
import re import re
from re import escape as re_escape from string import punctuation
import itertools
from django.conf import settings from django.conf import settings
from evennia.objects.objects import DefaultObject, DefaultCharacter from evennia.objects.objects import DefaultObject, DefaultCharacter
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.commands.command import Command from evennia.commands.command import Command
from evennia.commands.cmdset import CmdSet from evennia.commands.cmdset import CmdSet
from evennia.utils import ansi from evennia.utils import ansi, logger
from evennia.utils.utils import lazy_property, make_iter, variable_from_module from evennia.utils.utils import lazy_property, make_iter, variable_from_module
_REGEX_TUPLE_CACHE = {}
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1)) _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
# ------------------------------------------------------------ # ------------------------------------------------------------
# Emote parser # Emote parser
@ -189,13 +186,13 @@ _EMOTE_MULTIMATCH_ERROR = """|RMultiple possibilities for {ref}:
_RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.UNICODE _RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.UNICODE
_RE_PREFIX = re.compile(r"^%s" % _PREFIX, re.UNICODE) _RE_PREFIX = re.compile(rf"^{_PREFIX}", re.UNICODE)
# This regex will return groups (num, word), where num is an optional counter to # This regex will return groups (num, word), where num is an optional counter to
# separate multimatches from one another and word is the first word in the # separate multimatches from one another and word is the first word in the
# marker. So entering "/tall man" will return groups ("", "tall") # marker. So entering "/tall man" will return groups ("", "tall")
# and "/2-tall man" will return groups ("2", "tall"). # and "/2-tall man" will return groups ("2", "tall").
_RE_OBJ_REF_START = re.compile(r"%s(?:([0-9]+)%s)*(\w+)" % (_PREFIX, _NUM_SEP), _RE_FLAGS) _RE_OBJ_REF_START = re.compile(rf"{_PREFIX}(?:([0-9]+){_NUM_SEP})*(\w+)", _RE_FLAGS)
_RE_LEFT_BRACKETS = re.compile(r"\{+", _RE_FLAGS) _RE_LEFT_BRACKETS = re.compile(r"\{+", _RE_FLAGS)
_RE_RIGHT_BRACKETS = re.compile(r"\}+", _RE_FLAGS) _RE_RIGHT_BRACKETS = re.compile(r"\}+", _RE_FLAGS)
@ -239,97 +236,7 @@ class LanguageError(Exception):
pass pass
def _dummy_process(text, *args, **kwargs):
"Pass-through processor"
return text
# emoting mechanisms # emoting mechanisms
def ordered_permutation_regex(sentence):
"""
Builds a regex that matches 'ordered permutations' of a sentence's
words.
Args:
sentence (str): The sentence to build a match pattern to
Returns:
regex (re object): Compiled regex object represented the
possible ordered permutations of the sentence, from longest to
shortest.
Example:
The sdesc_regex for an sdesc of " very tall man" will
result in the following allowed permutations,
regex-matched in inverse order of length (case-insensitive):
"the very tall man", "the very tall", "very tall man",
"very tall", "the very", "tall man", "the", "very", "tall",
and "man".
We also add regex to make sure it also accepts num-specifiers,
like /2-tall.
"""
# escape {#nnn} markers from sentence, replace with nnn
sentence = _RE_REF.sub(r"\1", sentence)
# escape {##nnn} markers, replace with nnn
sentence = _RE_REF_LANG.sub(r"\1", sentence)
# escape self-ref marker from sentence
sentence = _RE_SELF_REF.sub(r"", sentence)
# ordered permutation algorithm
words = sentence.split()
combinations = itertools.product((True, False), repeat=len(words))
solution = []
for combination in combinations:
comb = []
for iword, word in enumerate(words):
if combination[iword]:
comb.append(word)
elif comb:
break
if comb:
solution.append(
_PREFIX
+ r"[0-9]*%s*%s(?=\W|$)+" % (_NUM_SEP, re_escape(" ".join(comb)).rstrip("\\"))
)
# combine into a match regex, first matching the longest down to the shortest components
regex = r"|".join(sorted(set(solution), key=lambda item: (-len(item), item)))
return regex
def regex_tuple_from_key_alias(obj):
"""
This will build a regex tuple for any object, not just from those
with sdesc/recog handlers. It's used as a legacy mechanism for
being able to mix this contrib with objects not using sdescs, but
note that creating the ordered permutation regex dynamically for
every object will add computational overhead.
Args:
obj (Object): This object's key and eventual aliases will
be used to build the tuple.
Returns:
regex_tuple (tuple): A tuple
(ordered_permutation_regex, obj, key/alias)
"""
global _REGEX_TUPLE_CACHE
permutation_string = " ".join([obj.key] + obj.aliases.all())
cache_key = f"{obj.id} {permutation_string}"
if cache_key not in _REGEX_TUPLE_CACHE:
_REGEX_TUPLE_CACHE[cache_key] = (
re.compile(ordered_permutation_regex(permutation_string), _RE_FLAGS),
obj,
obj.key,
)
return _REGEX_TUPLE_CACHE[cache_key]
def parse_language(speaker, emote): def parse_language(speaker, emote):
""" """
Parse the emote for language. This is Parse the emote for language. This is
@ -375,9 +282,9 @@ def parse_language(speaker, emote):
langname, saytext = say_match.groups() langname, saytext = say_match.groups()
istart, iend = say_match.start(), say_match.end() istart, iend = say_match.start(), say_match.end()
# the key is simply the running match in the emote # the key is simply the running match in the emote
key = "##%i" % imatch key = f"##{imatch}"
# replace say with ref markers in emote # replace say with ref markers in emote
emote = emote[:istart] + "{%s}" % key + emote[iend:] emote = "{start}{{{key}}}{end}".format( start=emote[:istart], key=key, end=emote[iend:] )
mapping[key] = (langname, saytext) mapping[key] = (langname, saytext)
if errors: if errors:
@ -430,24 +337,20 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
- says, "..." are - says, "..." are
""" """
# Load all candidate regex tuples [(regex, obj, sdesc/recog),...] # build a list of candidates with all possible referrable names
candidate_regexes = ( # include 'me' keyword for self-ref
([(_RE_SELF_REF, sender, sender.sdesc.get())] if hasattr(sender, "sdesc") else []) candidate_map = [(sender, 'me')]
+ ( for obj in candidates:
[sender.recog.get_regex_tuple(obj) for obj in candidates] # check if sender has any recogs for obj and add
if hasattr(sender, "recog") if hasattr(sender, "recog"):
else [] if recog := sender.recog.get(obj):
) candidate_map.append((obj, recog))
+ [obj.sdesc.get_regex_tuple() for obj in candidates if hasattr(obj, "sdesc")] # check if obj has an sdesc and add
+ [ if hasattr(obj, "sdesc"):
regex_tuple_from_key_alias(obj) # handle objects without sdescs candidate_map.append((obj, obj.sdesc.get()))
for obj in candidates # if no sdesc, include key plus aliases instead
if not (hasattr(obj, "recog") and hasattr(obj, "sdesc")) else:
] candidate_map.extend( [(obj, obj.key)] + [(obj, alias) for alias in obj.aliases.all()] )
)
# filter out non-found data
candidate_regexes = [tup for tup in candidate_regexes if tup]
# escape mapping syntax on the form {#id} if it exists already in emote, # escape mapping syntax on the form {#id} if it exists already in emote,
# if so it is replaced with just "id". # if so it is replaced with just "id".
@ -468,18 +371,48 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
# first see if there is a number given (e.g. 1-tall) # first see if there is a number given (e.g. 1-tall)
num_identifier, _ = marker_match.groups("") # return "" if no match, rather than None num_identifier, _ = marker_match.groups("") # return "" if no match, rather than None
istart0 = marker_match.start() match_index = marker_match.start()
istart = istart0 # split the emote string at the reference marker, to process everything after it
head = string[:match_index]
tail = string[match_index+1:]
# loop over all candidate regexes and match against the string following the match if search_mode:
matches = ((reg.match(string[istart:]), obj, text) for reg, obj, text in candidate_regexes) # match the candidates against the whole search string after the marker
rquery = "".join([r"\b(" + re.escape(word.strip(punctuation)) + r").*" for word in iter(tail.split())])
matches = ((re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map)
# filter out any non-matching candidates
bestmatches = [(obj, match.group()) for match, obj, text in matches if match]
# score matches by how long part of the string was matched else:
matches = [(match.end() if match else -1, obj, text) for match, obj, text in matches] # to find the longest match, we start from the marker and lengthen the
maxscore = max(score for score, obj, text in matches) # match query one word at a time.
word_list = []
bestmatches = []
# preserve punctuation when splitting
tail = re.split('(\W)', tail)
iend = 0
for i, item in enumerate(tail):
# don't add non-word characters to the search query
if not item.isalpha():
continue
word_list.append(item)
rquery = "".join([r"\b(" + re.escape(word) + r").*" for word in word_list])
# match candidates against the current set of words
matches = ((re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map)
matches = [(obj, match.group()) for match, obj, text in matches if match]
if len(matches) == 0:
# no matches at this length, keep previous iteration as best
break
# since this is the longest match so far, set latest match set as best matches
bestmatches = matches
# save current index as end point of matched text
iend = i
# save search string
matched_text = "".join(tail[1:iend])
# recombine remainder of emote back into a string
tail = "".join(tail[iend+1:])
# we have a valid maxscore, extract all matches with this value
bestmatches = [(obj, text) for score, obj, text in matches if maxscore == score != -1]
nmatches = len(bestmatches) nmatches = len(bestmatches)
if not nmatches: if not nmatches:
@ -488,12 +421,11 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
nmatches = 0 nmatches = 0
elif nmatches == 1: elif nmatches == 1:
# an exact match. # an exact match.
obj = bestmatches[0][0] obj, match_str = bestmatches[0]
nmatches = 1
elif all(bestmatches[0][0].id == obj.id for obj, text in bestmatches): elif all(bestmatches[0][0].id == obj.id for obj, text in bestmatches):
# multi-match but all matches actually reference the same # multi-match but all matches actually reference the same
# obj (could happen with clashing recogs + sdescs) # obj (could happen with clashing recogs + sdescs)
obj = bestmatches[0][0] obj, match_str = bestmatches[0]
nmatches = 1 nmatches = 1
else: else:
# multi-match. # multi-match.
@ -501,7 +433,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
inum = min(max(0, int(num_identifier) - 1), nmatches - 1) if num_identifier else None inum = min(max(0, int(num_identifier) - 1), nmatches - 1) if num_identifier else None
if inum is not None: if inum is not None:
# A valid inum is given. Use this to separate data. # A valid inum is given. Use this to separate data.
obj = bestmatches[inum][0] obj, match_str = bestmatches[inum]
nmatches = 1 nmatches = 1
else: else:
# no identifier given - a real multimatch. # no identifier given - a real multimatch.
@ -519,12 +451,9 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
# case sensitive mode # case sensitive mode
# internal flags for the case used for the original /query # internal flags for the case used for the original /query
# - t for titled input (like /Name) # - t for titled input (like /Name)
# - ^ for all upercase input (likle /NAME) # - ^ for all upercase input (like /NAME)
# - v for lower-case input (like /name) # - v for lower-case input (like /name)
# - ~ for mixed case input (like /nAmE) # - ~ for mixed case input (like /nAmE)
matchtext = marker_match.group()
if not _RE_SELF_REF.match(matchtext):
# self-refs are kept as-is, others are parsed by case
matchtext = marker_match.group().lstrip(_PREFIX) matchtext = marker_match.group().lstrip(_PREFIX)
if matchtext.istitle(): if matchtext.istitle():
case = "t" case = "t"
@ -533,21 +462,21 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
elif matchtext.islower(): elif matchtext.islower():
case = "v" case = "v"
key = "#%i%s" % (obj.id, case) key = f"#{obj.id}{case}"
string = string[:istart0] + "{%s}" % key + string[istart + maxscore :] # recombine emote with matched text replaced by ref
string = f"{head}{{{key}}}{tail}"
mapping[key] = obj mapping[key] = obj
else: else:
# multimatch error # multimatch error
refname = marker_match.group() refname = marker_match.group()
reflist = [ reflist = [
"%s%s%s (%s%s)" "{num}{sep}{name} ({text}{key})".format(
% ( num=inum + 1,
inum + 1, sep=_NUM_SEP,
_NUM_SEP, name=_RE_PREFIX.sub("", refname),
_RE_PREFIX.sub("", refname), text=text,
text, key=f" ({sender.key})" if sender == ob else "",
" (%s)" % sender.key if sender == ob else "",
) )
for inum, (ob, text) in enumerate(obj) for inum, (ob, text) in enumerate(obj)
] ]
@ -611,7 +540,7 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
sender.msg(str(err)) sender.msg(str(err))
return return
skey = "#%i" % sender.id skey = f"#{sender.id}"
# we escape the object mappings since we'll do the language ones first # we escape the object mappings since we'll do the language ones first
# (the text could have nested object mappings). # (the text could have nested object mappings).
@ -619,66 +548,45 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
# if anonymous_add is passed as a kwarg, collect and remove it from kwargs # if anonymous_add is passed as a kwarg, collect and remove it from kwargs
if "anonymous_add" in kwargs: if "anonymous_add" in kwargs:
anonymous_add = kwargs.pop("anonymous_add") anonymous_add = kwargs.pop("anonymous_add")
if anonymous_add and not any(1 for tag in obj_mapping if tag.startswith(skey)): # make sure to catch all possible self-refs
# no self-reference in the emote - add to the end self_refs = [f"{skey}{ref}" for ref in ('t','^','v','~','')]
obj_mapping[skey] = sender if anonymous_add and not any(1 for tag in obj_mapping if tag in self_refs):
# no self-reference in the emote - add it
if anonymous_add == "first": if anonymous_add == "first":
possessive = "" if emote.startswith("'") else " " # add case flag for initial caps
emote = "%s%s%s" % ("{{%s}}" % skey, possessive, emote) skey += 't'
# don't put a space after the self-ref if it's a possessive emote
femote = "{key}{emote}" if emote.startswith("'") else "{key} {emote}"
else: else:
emote = "%s [%s]" % (emote, "{{%s}}" % skey) # add it to the end
femote = "{emote} [{key}]"
emote = femote.format( key="{{"+ skey +"}}", emote=emote )
obj_mapping[skey] = sender
# broadcast emote to everyone # broadcast emote to everyone
for receiver in receivers: for receiver in receivers:
# first handle the language mapping, which always produce different keys ##nn # first handle the language mapping, which always produce different keys ##nn
receiver_lang_mapping = {} if hasattr(receiver, "process_language") and callable(receiver.process_language):
try: receiver_lang_mapping = {
process_language = receiver.process_language key: receiver.process_language(saytext, sender, langname)
except AttributeError: for key, (langname, saytext) in language_mapping.items()
process_language = _dummy_process }
for key, (langname, saytext) in language_mapping.items(): else:
# color says receiver_lang_mapping = {
receiver_lang_mapping[key] = process_language(saytext, sender, langname) key: saytext for key, (langname, saytext) in language_mapping.items()
}
# map the language {##num} markers. This will convert the escaped sdesc markers on # map the language {##num} markers. This will convert the escaped sdesc markers on
# the form {{#num}} to {#num} markers ready to sdesc-map in the next step. # the form {{#num}} to {#num} markers ready to sdesc-map in the next step.
sendemote = emote.format(**receiver_lang_mapping) sendemote = emote.format(**receiver_lang_mapping)
# handle sdesc mappings. we make a temporary copy that we can modify # map the ref keys to sdescs
try:
process_sdesc = receiver.process_sdesc
except AttributeError:
process_sdesc = _dummy_process
try:
process_recog = receiver.process_recog
except AttributeError:
process_recog = _dummy_process
try:
recog_get = receiver.recog.get
receiver_sdesc_mapping = dict(
(ref, process_recog(recog_get(obj), obj, ref=ref, **kwargs))
for ref, obj in obj_mapping.items()
)
except AttributeError:
receiver_sdesc_mapping = dict( receiver_sdesc_mapping = dict(
( (
ref, ref,
process_sdesc(obj.sdesc.get(), obj, ref=ref) obj.get_display_name(receiver, ref=ref, noid=True),
if hasattr(obj, "sdesc")
else process_sdesc(obj.key, obj, ref=ref),
) )
for ref, obj in obj_mapping.items() for ref, obj in obj_mapping.items()
) )
# make sure receiver always sees their real name
rkey_start = "#%i" % receiver.id
rkey_keep_case = rkey_start + "~" # signifies keeping the case
for rkey in (key for key in receiver_sdesc_mapping if key.startswith(rkey_start)):
# we could have #%i^, #%it etc depending on input case - we want the
# self-reference to retain case.
receiver_sdesc_mapping[rkey] = process_sdesc(
receiver.key, receiver, ref=rkey_keep_case, **kwargs
)
# do the template replacement of the sdesc/recog {#num} markers # do the template replacement of the sdesc/recog {#num} markers
receiver.msg(sendemote.format(**receiver_sdesc_mapping), from_obj=sender, **kwargs) receiver.msg(sendemote.format(**receiver_sdesc_mapping), from_obj=sender, **kwargs)
@ -713,17 +621,13 @@ class SdescHandler:
""" """
self.obj = obj self.obj = obj
self.sdesc = "" self.sdesc = ""
self.sdesc_regex = ""
self._cache() self._cache()
def _cache(self): def _cache(self):
""" """
Cache data from storage Cache data from storage
""" """
self.sdesc = self.obj.attributes.get("_sdesc", default="") self.sdesc = self.obj.attributes.get("_sdesc", default=self.obj.key)
sdesc_regex = self.obj.attributes.get("_sdesc_regex", default="")
self.sdesc_regex = re.compile(sdesc_regex, _RE_FLAGS)
def add(self, sdesc, max_length=60): def add(self, sdesc, max_length=60):
""" """
@ -759,17 +663,13 @@ class SdescHandler:
if len(cleaned_sdesc) > max_length: if len(cleaned_sdesc) > max_length:
raise SdescError( raise SdescError(
"Short desc can max be %i chars long (was %i chars)." "Short desc can max be {} chars long (was {} chars).".format(max_length, len(cleaned_sdesc))
% (max_length, len(cleaned_sdesc))
) )
# store to attributes # store to attributes
sdesc_regex = ordered_permutation_regex(cleaned_sdesc)
self.obj.attributes.add("_sdesc", sdesc) self.obj.attributes.add("_sdesc", sdesc)
self.obj.attributes.add("_sdesc_regex", sdesc_regex)
# local caching # local caching
self.sdesc = sdesc self.sdesc = sdesc
self.sdesc_regex = re.compile(sdesc_regex, _RE_FLAGS)
return sdesc return sdesc
@ -781,15 +681,6 @@ class SdescHandler:
""" """
return self.sdesc or self.obj.key return self.sdesc or self.obj.key
def get_regex_tuple(self):
"""
Return data for sdesc/recog handling
Returns:
tup (tuple): tuple (sdesc_regex, obj, sdesc)
"""
return self.sdesc_regex, self.obj, self.sdesc
class RecogHandler: class RecogHandler:
@ -802,7 +693,6 @@ class RecogHandler:
_recog_ref2recog _recog_ref2recog
_recog_obj2recog _recog_obj2recog
_recog_obj2regex
""" """
@ -817,7 +707,6 @@ class RecogHandler:
self.obj = obj self.obj = obj
# mappings # mappings
self.ref2recog = {} self.ref2recog = {}
self.obj2regex = {}
self.obj2recog = {} self.obj2recog = {}
self._cache() self._cache()
@ -826,11 +715,7 @@ class RecogHandler:
Load data to handler cache Load data to handler cache
""" """
self.ref2recog = self.obj.attributes.get("_recog_ref2recog", default={}) self.ref2recog = self.obj.attributes.get("_recog_ref2recog", default={})
obj2regex = self.obj.attributes.get("_recog_obj2regex", default={})
obj2recog = self.obj.attributes.get("_recog_obj2recog", default={}) obj2recog = self.obj.attributes.get("_recog_obj2recog", default={})
self.obj2regex = dict(
(obj, re.compile(regex, _RE_FLAGS)) for obj, regex in obj2regex.items() if obj
)
self.obj2recog = dict((obj, recog) for obj, recog in obj2recog.items() if obj) self.obj2recog = dict((obj, recog) for obj, recog in obj2recog.items() if obj)
def add(self, obj, recog, max_length=60): def add(self, obj, recog, max_length=60):
@ -873,31 +758,27 @@ class RecogHandler:
if len(cleaned_recog) > max_length: if len(cleaned_recog) > max_length:
raise RecogError( raise RecogError(
"Recog string cannot be longer than %i chars (was %i chars)" "Recog string cannot be longer than {} chars (was {} chars)".format(max_length, len(cleaned_recog))
% (max_length, len(cleaned_recog))
) )
# mapping #dbref:obj # mapping #dbref:obj
key = "#%i" % obj.id key = f"#{obj.id}"
self.obj.attributes.get("_recog_ref2recog", default={})[key] = recog self.obj.attributes.get("_recog_ref2recog", default={})[key] = recog
self.obj.attributes.get("_recog_obj2recog", default={})[obj] = recog self.obj.attributes.get("_recog_obj2recog", default={})[obj] = recog
regex = ordered_permutation_regex(cleaned_recog)
self.obj.attributes.get("_recog_obj2regex", default={})[obj] = regex
# local caching # local caching
self.ref2recog[key] = recog self.ref2recog[key] = recog
self.obj2recog[obj] = recog self.obj2recog[obj] = recog
self.obj2regex[obj] = re.compile(regex, _RE_FLAGS)
return recog return recog
def get(self, obj): def get(self, obj):
""" """
Get recog replacement string, if one exists, otherwise Get recog replacement string, if one exists.
get sdesc and as a last resort, the object's key.
Args: Args:
obj (Object): The object, whose sdesc to replace obj (Object): The object, whose sdesc to replace
Returns: Returns:
recog (str): The replacement string to use. recog (str or None): The replacement string to use, or
None if there is no recog for this object.
Notes: Notes:
This method will respect a "enable_recog" lock set on This method will respect a "enable_recog" lock set on
@ -908,10 +789,10 @@ class RecogHandler:
# check an eventual recog_masked lock on the object # check an eventual recog_masked lock on the object
# to avoid revealing masked characters. If lock # to avoid revealing masked characters. If lock
# does not exist, pass automatically. # does not exist, pass automatically.
return self.obj2recog.get(obj, obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key) return self.obj2recog.get(obj, None)
else: else:
# recog_mask log not passed, disable recog # recog_mask lock not passed, disable recog
return obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key return None
def all(self): def all(self):
""" """
@ -932,19 +813,9 @@ class RecogHandler:
""" """
if obj in self.obj2recog: if obj in self.obj2recog:
del self.obj.db._recog_obj2recog[obj] del self.obj.db._recog_obj2recog[obj]
del self.obj.db._recog_obj2regex[obj] del self.obj.db._recog_ref2recog[f"#{obj.id}"]
del self.obj.db._recog_ref2recog["#%i" % obj.id]
self._cache() self._cache()
def get_regex_tuple(self, obj):
"""
Returns:
rec (tuple): Tuple (recog_regex, obj, recog)
"""
if obj in self.obj2recog and obj.access(self.obj, "enable_recog", default=True):
return self.obj2regex[obj], obj, self.obj2regex[obj]
return None
# ------------------------------------------------------------ # ------------------------------------------------------------
# RP Commands # RP Commands
@ -994,8 +865,8 @@ class CmdEmote(RPCommand): # replaces the main emote
# we also include ourselves here. # we also include ourselves here.
emote = self.args emote = self.args
targets = self.caller.location.contents targets = self.caller.location.contents
if not emote.endswith((".", "?", "!")): # If emote is not punctuated, if not emote.endswith((".", "?", "!", '"')): # If emote is not punctuated or speech,
emote = "%s." % emote # add a full-stop for good measure. emote += "." # add a full-stop for good measure.
send_emote(self.caller, targets, emote, anonymous_add="first") send_emote(self.caller, targets, emote, anonymous_add="first")
@ -1025,7 +896,6 @@ class CmdSay(RPCommand): # replaces standard say
# calling the speech modifying hook # calling the speech modifying hook
speech = caller.at_pre_say(self.args) speech = caller.at_pre_say(self.args)
# preparing the speech with sdesc/speech parsing.
targets = self.caller.location.contents targets = self.caller.location.contents
send_emote(self.caller, targets, speech, anonymous_add=None) send_emote(self.caller, targets, speech, anonymous_add=None)
@ -1061,7 +931,7 @@ class CmdSdesc(RPCommand): # set/look at own sdesc
except AttributeError: except AttributeError:
caller.msg(f"Cannot set sdesc on {caller.key}.") caller.msg(f"Cannot set sdesc on {caller.key}.")
return return
caller.msg("%s's sdesc was set to '%s'." % (caller.key, sdesc)) caller.msg(f"{caller.key}'s sdesc was set to '{sdesc}'.")
class CmdPose(RPCommand): # set current pose and default pose class CmdPose(RPCommand): # set current pose and default pose
@ -1121,8 +991,8 @@ class CmdPose(RPCommand): # set current pose and default pose
caller.msg("Usage: pose <pose-text> OR pose obj = <pose-text>") caller.msg("Usage: pose <pose-text> OR pose obj = <pose-text>")
return return
if not pose.endswith("."): if not pose.endswith((".", "?", "!", '"')):
pose = "%s." % pose pose += "."
if target: if target:
# affect something else # affect something else
target = caller.search(target) target = caller.search(target)
@ -1134,18 +1004,18 @@ class CmdPose(RPCommand): # set current pose and default pose
else: else:
target = caller target = caller
target_name = target.sdesc.get() if hasattr(target, "sdesc") else target.key
if not target.attributes.has("pose"): if not target.attributes.has("pose"):
caller.msg("%s cannot be posed." % target.key) caller.msg(f"{target_name} cannot be posed.")
return return
target_name = target.sdesc.get() if hasattr(target, "sdesc") else target.key
# set the pose # set the pose
if self.reset: if self.reset:
pose = target.db.pose_default pose = target.db.pose_default
target.db.pose = pose target.db.pose = pose
elif self.default: elif self.default:
target.db.pose_default = pose target.db.pose_default = pose
caller.msg("Default pose is now '%s %s'." % (target_name, pose)) caller.msg(f"Default pose is now '{target_name} {pose}'.")
return return
else: else:
# set the pose. We do one-time ref->sdesc mapping here. # set the pose. We do one-time ref->sdesc mapping here.
@ -1157,12 +1027,12 @@ class CmdPose(RPCommand): # set current pose and default pose
pose = parsed.format(**mapping) pose = parsed.format(**mapping)
if len(target_name) + len(pose) > 60: if len(target_name) + len(pose) > 60:
caller.msg("Your pose '%s' is too long." % pose) caller.msg(f"'{pose}' is too long.")
return return
target.db.pose = pose target.db.pose = pose
caller.msg("Pose will read '%s %s'." % (target_name, pose)) caller.msg(f"Pose will read '{target_name} {pose}'.")
class CmdRecog(RPCommand): # assign personal alias to object in room class CmdRecog(RPCommand): # assign personal alias to object in room
@ -1241,12 +1111,12 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
caller.msg(_EMOTE_NOMATCH_ERROR.format(ref=sdesc)) caller.msg(_EMOTE_NOMATCH_ERROR.format(ref=sdesc))
elif nmatches > 1: elif nmatches > 1:
reflist = [ reflist = [
"{}{}{} ({}{})".format( "{num}{sep}{sdesc} ({recog}{key})".format(
inum + 1, num=inum + 1,
_NUM_SEP, sep=_NUM_SEP,
_RE_PREFIX.sub("", sdesc), sdesc=_RE_PREFIX.sub("", sdesc),
caller.recog.get(obj), recog=caller.recog.get(obj) or "no recog",
" (%s)" % caller.key if caller == obj else "", key=f" ({caller.key})" if caller == obj else "",
) )
for inum, obj in enumerate(matches) for inum, obj in enumerate(matches)
] ]
@ -1262,7 +1132,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
if forget_mode: if forget_mode:
# remove existing recog # remove existing recog
caller.recog.remove(obj) caller.recog.remove(obj)
caller.msg("%s will now know them only as '%s'." % (caller.key, obj.recog.get(obj))) caller.msg("You will now know them only as '{}'.".format( obj.get_display_name(caller, noid=True) ))
else: else:
# set recog # set recog
sdesc = obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key sdesc = obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
@ -1271,7 +1141,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
except RecogError as err: except RecogError as err:
caller.msg(err) caller.msg(err)
return return
caller.msg("%s will now remember |w%s|n as |w%s|n." % (caller.key, sdesc, alias)) caller.msg("You will now remember |w{}|n as |w{}|n.".format(sdesc, alias))
class CmdMask(RPCommand): class CmdMask(RPCommand):
@ -1302,14 +1172,14 @@ class CmdMask(RPCommand):
caller.msg("You are already wearing a mask.") caller.msg("You are already wearing a mask.")
return return
sdesc = _RE_CHAREND.sub("", self.args) sdesc = _RE_CHAREND.sub("", self.args)
sdesc = "%s |H[masked]|n" % sdesc sdesc = f"{sdesc} |H[masked]|n"
if len(sdesc) > 60: if len(sdesc) > 60:
caller.msg("Your masked sdesc is too long.") caller.msg("Your masked sdesc is too long.")
return return
caller.db.unmasked_sdesc = caller.sdesc.get() caller.db.unmasked_sdesc = caller.sdesc.get()
caller.locks.add("enable_recog:false()") caller.locks.add("enable_recog:false()")
caller.sdesc.add(sdesc) caller.sdesc.add(sdesc)
caller.msg("You wear a mask as '%s'." % sdesc) caller.msg(f"You wear a mask as '{sdesc}'.")
else: else:
# unmask # unmask
old_sdesc = caller.db.unmasked_sdesc old_sdesc = caller.db.unmasked_sdesc
@ -1319,7 +1189,7 @@ class CmdMask(RPCommand):
del caller.db.unmasked_sdesc del caller.db.unmasked_sdesc
caller.locks.remove("enable_recog") caller.locks.remove("enable_recog")
caller.sdesc.add(old_sdesc) caller.sdesc.add(old_sdesc)
caller.msg("You remove your mask and are again '%s'." % old_sdesc) caller.msg(f"You remove your mask and are again '{old_sdesc}'.")
class RPSystemCmdSet(CmdSet): class RPSystemCmdSet(CmdSet):
@ -1346,6 +1216,9 @@ class ContribRPObject(DefaultObject):
This class is meant as a mix-in or parent for objects in an This class is meant as a mix-in or parent for objects in an
rp-heavy game. It implements the base functionality for poses. rp-heavy game. It implements the base functionality for poses.
""" """
@lazy_property
def sdesc(self):
return SdescHandler(self)
def at_object_creation(self): def at_object_creation(self):
""" """
@ -1357,6 +1230,10 @@ class ContribRPObject(DefaultObject):
self.db.pose = "" self.db.pose = ""
self.db.pose_default = "is here." self.db.pose_default = "is here."
# initializing sdesc
self.db._sdesc = ""
self.sdesc.add("Something")
def search( def search(
self, self,
searchdata, searchdata,
@ -1529,6 +1406,22 @@ class ContribRPObject(DefaultObject):
multimatch_string=multimatch_string, multimatch_string=multimatch_string,
) )
def get_posed_sdesc(self, sdesc, **kwargs):
"""
Displays the object with its current pose string.
Returns:
pose (str): A string containing the object's sdesc and
current or default pose.
"""
# get the current pose, or default if no pose is set
pose = self.db.pose or self.db.pose_default
# return formatted string, or sdesc as fallback
return f"{sdesc} {pose}" if pose else sdesc
def get_display_name(self, looker, **kwargs): def get_display_name(self, looker, **kwargs):
""" """
Displays the name of the object in a viewer-aware manner. Displays the name of the object in a viewer-aware manner.
@ -1539,28 +1432,41 @@ class ContribRPObject(DefaultObject):
Keyword Args: Keyword Args:
pose (bool): Include the pose (if available) in the return. pose (bool): Include the pose (if available) in the return.
ref (str): The reference marker found in string to replace.
This is on the form #{num}{case}, like '#12^', where
the number is a processing location in the string and the
case symbol indicates the case of the original tag input
- `t` - input was Titled, like /Tall
- `^` - input was all uppercase, like /TALL
- `v` - input was all lowercase, like /tall
- `~` - input case should be kept, or was mixed-case
noid (bool): Don't show DBREF even if viewer has control access.
Returns: Returns:
name (str): A string of the sdesc containing the name of the object, name (str): A string of the sdesc containing the name of the object,
if this is defined. if this is defined. By default, included the DBREF if this user
including the DBREF if this user is privileged to control is privileged to control said object.
said object.
Notes:
The RPObject version doesn't add color to its display.
""" """
idstr = "(#%s)" % self.id if self.access(looker, access_type="control") else "" ref = kwargs.get("ref","~")
if looker == self: if looker == self:
# always show your own key
sdesc = self.key sdesc = self.key
else: else:
try: try:
recog = looker.recog.get(self) # get the sdesc looker should see
sdesc = looker.get_sdesc(self, ref=ref)
except AttributeError: except AttributeError:
recog = None # use own sdesc as a fallback
sdesc = recog or (hasattr(self, "sdesc") and self.sdesc.get()) or self.key sdesc = self.sdesc.get()
pose = " %s" % (self.db.pose or "") if kwargs.get("pose", False) else ""
return "%s%s%s" % (sdesc, idstr, pose) # add dbref is looker has control access and `noid` is not set
if self.access(looker, access_type="control") and not kwargs.get("noid",False):
sdesc = f"{sdesc}(#{self.id})"
return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc
def return_appearance(self, looker): def return_appearance(self, looker):
""" """
@ -1569,6 +1475,10 @@ class ContribRPObject(DefaultObject):
Args: Args:
looker (Object): Object doing the looking. looker (Object): Object doing the looking.
Returns:
string (str): A string containing the name, appearance and contents
of the object.
""" """
if not looker: if not looker:
return "" return ""
@ -1592,6 +1502,7 @@ class ContribRPObject(DefaultObject):
string += "\n|wExits:|n " + ", ".join(exits) string += "\n|wExits:|n " + ", ".join(exits)
if users or things: if users or things:
string += "\n " + "\n ".join(users + things) string += "\n " + "\n ".join(users + things)
return string return string
@ -1608,11 +1519,6 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
This is a character class that has poses, sdesc and recog. This is a character class that has poses, sdesc and recog.
""" """
# Handlers
@lazy_property
def sdesc(self):
return SdescHandler(self)
@lazy_property @lazy_property
def recog(self): def recog(self):
return RecogHandler(self) return RecogHandler(self)
@ -1627,29 +1533,45 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
Keyword Args: Keyword Args:
pose (bool): Include the pose (if available) in the return. pose (bool): Include the pose (if available) in the return.
ref (str): The reference marker found in string to replace.
This is on the form #{num}{case}, like '#12^', where
the number is a processing location in the string and the
case symbol indicates the case of the original tag input
- `t` - input was Titled, like /Tall
- `^` - input was all uppercase, like /TALL
- `v` - input was all lowercase, like /tall
- `~` - input case should be kept, or was mixed-case
noid (bool): Don't show DBREF even if viewer has control access.
Returns: Returns:
name (str): A string of the sdesc containing the name of the object, name (str): A string of the sdesc containing the name of the object,
if this is defined. if this is defined. By default, included the DBREF if this user
including the DBREF if this user is privileged to control is privileged to control said object.
said object.
Notes: Notes:
The RPCharacter version of this method colors its display to make The RPCharacter version adds additional processing to sdescs to make
characters stand out from other objects. characters stand out from other objects.
""" """
idstr = "(#%s)" % self.id if self.access(looker, access_type="control") else "" ref = kwargs.get("ref","~")
if looker == self: if looker == self:
sdesc = self.key # process your key as recog since you recognize yourself
sdesc = self.process_recog(self.key,self)
else: else:
try: try:
recog = looker.recog.get(self) # get the sdesc looker should see, with formatting
sdesc = looker.get_sdesc(self, process=True, ref=ref)
except AttributeError: except AttributeError:
recog = None # use own sdesc as a fallback
sdesc = recog or (hasattr(self, "sdesc") and self.sdesc.get()) or self.key sdesc = self.sdesc.get()
pose = " %s" % (self.db.pose or "is here.") if kwargs.get("pose", False) else ""
return "|c%s|n%s%s" % (sdesc, idstr, pose) # add dbref is looker has control access and `noid` is not set
if self.access(looker, access_type="control") and not kwargs.get("noid",False):
sdesc = f"{sdesc}(#{self.id})"
return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc
def at_object_creation(self): def at_object_creation(self):
""" """
@ -1658,10 +1580,8 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
super().at_object_creation() super().at_object_creation()
self.db._sdesc = "" self.db._sdesc = ""
self.db._sdesc_regex = ""
self.db._recog_ref2recog = {} self.db._recog_ref2recog = {}
self.db._recog_obj2regex = {}
self.db._recog_obj2recog = {} self.db._recog_obj2recog = {}
self.cmdset.add(RPSystemCmdSet, persistent=True) self.cmdset.add(RPSystemCmdSet, persistent=True)
@ -1679,8 +1599,38 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
""" """
if kwargs.get("whisper"): if kwargs.get("whisper"):
return f'/me whispers "{message}"' return f'/Me whispers "{message}"'
return f'/me says, "{message}"' return f'/Me says, "{message}"'
def get_sdesc(self, obj, process=False, **kwargs):
"""
Single method to handle getting recogs with sdesc fallback in an
aware manner, to allow separate processing of recogs from sdescs.
Gets the sdesc or recog for obj from the view of self.
Args:
obj (Object): the object whose sdesc or recog is being gotten
Keyword Args:
process (bool): If True, the sdesc/recog is run through the
appropriate process method for self - .process_sdesc or
.process_recog
"""
# always see own key
if obj == self:
recog = self.key
sdesc = self.key
else:
# first check if we have a recog for this object
recog = self.recog.get(obj)
# set sdesc to recog, using sdesc as a fallback, or the object's key if no sdesc
sdesc = recog or (hasattr(obj, "sdesc") and obj.sdesc.get()) or obj.key
if process:
# process the sdesc as a recog if a recog was found, else as an sdesc
sdesc = (self.process_recog if recog else self.process_sdesc)(sdesc, obj, **kwargs)
return sdesc
def process_sdesc(self, sdesc, obj, **kwargs): def process_sdesc(self, sdesc, obj, **kwargs):
""" """
@ -1721,7 +1671,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
sdesc = sdesc.upper() sdesc = sdesc.upper()
elif "v" in ref: elif "v" in ref:
sdesc = sdesc.lower() sdesc = sdesc.lower()
return "|b%s|n" % sdesc return f"|b{sdesc}|n"
def process_recog(self, recog, obj, **kwargs): def process_recog(self, recog, obj, **kwargs):
""" """
@ -1732,14 +1682,15 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
translated from the original sdesc at this point. translated from the original sdesc at this point.
obj (Object): The object the recog:ed string belongs to. obj (Object): The object the recog:ed string belongs to.
This is not used by default. This is not used by default.
Kwargs:
ref (str): See process_sdesc.
Returns: Returns:
recog (str): The modified recog string. recog (str): The modified recog string.
""" """
return self.process_sdesc(recog, obj, **kwargs) if not recog:
return ""
return f"|m{recog}|n"
def process_language(self, text, speaker, language, **kwargs): def process_language(self, text, speaker, language, **kwargs):
""" """
@ -1762,4 +1713,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
the evennia.contrib.rpg.rplanguage module. the evennia.contrib.rpg.rplanguage module.
""" """
return "%s|w%s|n" % ("|W(%s)" % language if language else "", text) return "{label}|w{text}|n".format(
label=f"|W({language})" if language else "",
text=text
)

View file

@ -96,7 +96,7 @@ recog01 = "Mr Receiver"
recog02 = "Mr Receiver2" recog02 = "Mr Receiver2"
recog10 = "Mr Sender" recog10 = "Mr Sender"
emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."' emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."'
case_emote = "/me looks at /first, then /FIRST, /First and /Colliding twice." case_emote = "/Me looks at /first. Then, /me looks at /FIRST, /First and /Colliding twice."
class TestRPSystem(BaseEvenniaTest): class TestRPSystem(BaseEvenniaTest):
@ -113,41 +113,11 @@ class TestRPSystem(BaseEvenniaTest):
rpsystem.ContribRPCharacter, key="Receiver2", location=self.room rpsystem.ContribRPCharacter, key="Receiver2", location=self.room
) )
def test_ordered_permutation_regex(self):
self.assertEqual(
rpsystem.ordered_permutation_regex(sdesc0),
"/[0-9]*-*A\\ nice\\ sender\\ of\\ emotes(?=\\W|$)+|"
"/[0-9]*-*nice\\ sender\\ of\\ emotes(?=\\W|$)+|"
"/[0-9]*-*A\\ nice\\ sender\\ of(?=\\W|$)+|"
"/[0-9]*-*sender\\ of\\ emotes(?=\\W|$)+|"
"/[0-9]*-*nice\\ sender\\ of(?=\\W|$)+|"
"/[0-9]*-*A\\ nice\\ sender(?=\\W|$)+|"
"/[0-9]*-*nice\\ sender(?=\\W|$)+|"
"/[0-9]*-*of\\ emotes(?=\\W|$)+|"
"/[0-9]*-*sender\\ of(?=\\W|$)+|"
"/[0-9]*-*A\\ nice(?=\\W|$)+|"
"/[0-9]*-*emotes(?=\\W|$)+|"
"/[0-9]*-*sender(?=\\W|$)+|"
"/[0-9]*-*nice(?=\\W|$)+|"
"/[0-9]*-*of(?=\\W|$)+|"
"/[0-9]*-*A(?=\\W|$)+",
)
def test_sdesc_handler(self): def test_sdesc_handler(self):
self.speaker.sdesc.add(sdesc0) self.speaker.sdesc.add(sdesc0)
self.assertEqual(self.speaker.sdesc.get(), sdesc0) self.assertEqual(self.speaker.sdesc.get(), sdesc0)
self.speaker.sdesc.add("This is {#324} ignored") self.speaker.sdesc.add("This is {#324} ignored")
self.assertEqual(self.speaker.sdesc.get(), "This is 324 ignored") self.assertEqual(self.speaker.sdesc.get(), "This is 324 ignored")
self.speaker.sdesc.add("Testing three words")
self.assertEqual(
self.speaker.sdesc.get_regex_tuple()[0].pattern,
"/[0-9]*-*Testing\ three\ words(?=\W|$)+|"
"/[0-9]*-*Testing\ three(?=\W|$)+|"
"/[0-9]*-*three\ words(?=\W|$)+|"
"/[0-9]*-*Testing(?=\W|$)+|"
"/[0-9]*-*three(?=\W|$)+|"
"/[0-9]*-*words(?=\W|$)+",
)
def test_recog_handler(self): def test_recog_handler(self):
self.speaker.sdesc.add(sdesc0) self.speaker.sdesc.add(sdesc0)
@ -156,12 +126,8 @@ class TestRPSystem(BaseEvenniaTest):
self.speaker.recog.add(self.receiver2, recog02) self.speaker.recog.add(self.receiver2, recog02)
self.assertEqual(self.speaker.recog.get(self.receiver1), recog01) self.assertEqual(self.speaker.recog.get(self.receiver1), recog01)
self.assertEqual(self.speaker.recog.get(self.receiver2), recog02) self.assertEqual(self.speaker.recog.get(self.receiver2), recog02)
self.assertEqual(
self.speaker.recog.get_regex_tuple(self.receiver1)[0].pattern,
"/[0-9]*-*Mr\\ Receiver(?=\\W|$)+|/[0-9]*-*Receiver(?=\\W|$)+|/[0-9]*-*Mr(?=\\W|$)+",
)
self.speaker.recog.remove(self.receiver1) self.speaker.recog.remove(self.receiver1)
self.assertEqual(self.speaker.recog.get(self.receiver1), sdesc1) self.assertEqual(self.speaker.recog.get(self.receiver1), None)
self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2}) self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2})
@ -198,6 +164,24 @@ class TestRPSystem(BaseEvenniaTest):
result, result,
) )
def test_get_sdesc(self):
looker = self.speaker # Sender
target = self.receiver1 # Receiver1
looker.sdesc.add(sdesc0) # A nice sender of emotes
target.sdesc.add(sdesc1) # The first receiver of emotes.
# sdesc with no processing
self.assertEqual(looker.get_sdesc(target), "The first receiver of emotes.")
# sdesc with processing
self.assertEqual(looker.get_sdesc(target, process=True), "|bThe first receiver of emotes.|n")
looker.recog.add(target, recog01) # Mr Receiver
# recog with no processing
self.assertEqual(looker.get_sdesc(target), "Mr Receiver")
# recog with processing
self.assertEqual(looker.get_sdesc(target, process=True), "|mMr Receiver|n")
def test_send_emote(self): def test_send_emote(self):
speaker = self.speaker speaker = self.speaker
receiver1 = self.receiver1 receiver1 = self.receiver1
@ -212,18 +196,18 @@ class TestRPSystem(BaseEvenniaTest):
rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False) rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False)
self.assertEqual( self.assertEqual(
self.out0, self.out0,
"With a flair, |bSender|n looks at |bThe first receiver of emotes.|n " "With a flair, |mSender|n looks at |bThe first receiver of emotes.|n "
'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n', 'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
) )
self.assertEqual( self.assertEqual(
self.out1, self.out1,
"With a flair, |bA nice sender of emotes|n looks at |bReceiver1|n and " "With a flair, |bA nice sender of emotes|n looks at |mReceiver1|n and "
'|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n', '|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
) )
self.assertEqual( self.assertEqual(
self.out2, self.out2,
"With a flair, |bA nice sender of emotes|n looks at |bThe first " "With a flair, |bA nice sender of emotes|n looks at |bThe first "
'receiver of emotes.|n and |bReceiver2|n. She says |w"This is a test."|n', 'receiver of emotes.|n and |mReceiver2|n. She says |w"This is a test."|n',
) )
def test_send_case_sensitive_emote(self): def test_send_case_sensitive_emote(self):
@ -241,20 +225,21 @@ class TestRPSystem(BaseEvenniaTest):
rpsystem.send_emote(speaker, receivers, case_emote) rpsystem.send_emote(speaker, receivers, case_emote)
self.assertEqual( self.assertEqual(
self.out0, self.out0,
"|bSender|n looks at |bthe first receiver of emotes.|n, then " "|mSender|n looks at |bthe first receiver of emotes.|n. Then, |mSender|n "
"|bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n and " "looks at |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n "
"|bAnother nice colliding sdesc-guy for tests|n twice.", "and |bAnother nice colliding sdesc-guy for tests|n twice.",
) )
self.assertEqual( self.assertEqual(
self.out1, self.out1,
"|bA nice sender of emotes|n looks at |bReceiver1|n, then |bReceiver1|n, " "|bA nice sender of emotes|n looks at |mReceiver1|n. Then, "
"|bReceiver1|n and |bAnother nice colliding sdesc-guy for tests|n twice.", "|ba nice sender of emotes|n looks at |mReceiver1|n, |mReceiver1|n "
"and |bAnother nice colliding sdesc-guy for tests|n twice."
) )
self.assertEqual( self.assertEqual(
self.out2, self.out2,
"|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n, " "|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n. "
"then |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of " "Then, |ba nice sender of emotes|n looks at |bTHE FIRST RECEIVER OF EMOTES.|n, "
"emotes.|n and |bReceiver2|n twice.", "|bThe first receiver of emotes.|n and |mReceiver2|n twice.",
) )
def test_rpsearch(self): def test_rpsearch(self):
@ -265,18 +250,6 @@ class TestRPSystem(BaseEvenniaTest):
self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1) self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1)
self.assertEqual(self.speaker.search("colliding"), self.receiver2) self.assertEqual(self.speaker.search("colliding"), self.receiver2)
def test_regex_tuple_from_key_alias(self):
self.speaker.aliases.add("foo bar")
self.speaker.aliases.add("this thing is a long thing")
t0 = time.time()
result = rpsystem.regex_tuple_from_key_alias(self.speaker)
t1 = time.time()
result = rpsystem.regex_tuple_from_key_alias(self.speaker)
t2 = time.time()
# print(f"t1: {t1 - t0}, t2: {t2 - t1}")
self.assertLess(t2 - t1, 10**-4)
self.assertEqual(result, (Anything, self.speaker, self.speaker.key))
class TestRPSystemCommands(BaseEvenniaCommandTest): class TestRPSystemCommands(BaseEvenniaCommandTest):
def setUp(self): def setUp(self):
@ -305,7 +278,7 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
self.call( self.call(
rpsystem.CmdRecog(), rpsystem.CmdRecog(),
"barfoo as friend", "barfoo as friend",
"Char will now remember BarFoo Character as friend.", "You will now remember BarFoo Character as friend.",
) )
self.call( self.call(
rpsystem.CmdRecog(), rpsystem.CmdRecog(),
@ -316,6 +289,6 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
self.call( self.call(
rpsystem.CmdRecog(), rpsystem.CmdRecog(),
"friend", "friend",
"Char will now know them only as 'BarFoo Character'", "You will now know them only as 'BarFoo Character'",
cmdstring="forget", cmdstring="forget",
) )

View file

@ -286,7 +286,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
categories = make_iter(category) if category else [] categories = make_iter(category) if category else []
n_keys = len(keys) n_keys = len(keys)
n_categories = len(categories) n_categories = len(categories)
unique_categories = sorted(set(categories)) unique_categories = set(categories)
n_unique_categories = len(unique_categories) n_unique_categories = len(unique_categories)
dbmodel = self.model.__dbclass__.__name__.lower() dbmodel = self.model.__dbclass__.__name__.lower()

View file

@ -142,6 +142,13 @@ class TestTypedObjectManager(BaseEvenniaTest):
[self.obj1], [self.obj1],
) )
def test_get_tag_with_any_including_nones(self):
self.obj1.tags.add("tagA", "categoryA")
self.assertEqual(
self._manager("get_by_tag", ["tagA", "tagB"], ["categoryA", "categoryB", None], match="any"),
[self.obj1],
)
def test_get_tag_withnomatch(self): def test_get_tag_withnomatch(self):
self.obj1.tags.add("tagC", "categoryC") self.obj1.tags.add("tagC", "categoryC")
self.assertEqual( self.assertEqual(

View file

@ -12,6 +12,7 @@ evennia.OPTION_CLASSES
from pickle import dumps from pickle import dumps
from django.db.utils import OperationalError, ProgrammingError
from django.conf import settings from django.conf import settings
from evennia.utils.utils import class_from_module, callables_from_module from evennia.utils.utils import class_from_module, callables_from_module
from evennia.utils import logger from evennia.utils import logger
@ -167,7 +168,6 @@ class GlobalScriptContainer(Container):
# store a hash representation of the setup # store a hash representation of the setup
script.attributes.add("_global_script_settings", compare_hash, category="settings_hash") script.attributes.add("_global_script_settings", compare_hash, category="settings_hash")
script.start()
return script return script
@ -183,9 +183,16 @@ class GlobalScriptContainer(Container):
# populate self.typeclass_storage # populate self.typeclass_storage
self.load_data() self.load_data()
# start registered scripts # make sure settings-defined scripts are loaded
for key in self.loaded_data: for key in self.loaded_data:
self._load_script(key) self._load_script(key)
# start all global scripts
try:
for script in self._get_scripts():
script.start()
except (OperationalError, ProgrammingError):
# this can happen if db is not loaded yet (such as when building docs)
pass
def load_data(self): def load_data(self):
""" """

View file

@ -239,6 +239,9 @@ class _SaverMutable(object):
def __gt__(self, other): def __gt__(self, other):
return self._data > other return self._data > other
def __or__(self, other):
return self._data | other
@_save @_save
def __setitem__(self, key, value): def __setitem__(self, key, value):
self._data.__setitem__(key, self._convert_mutables(value)) self._data.__setitem__(key, self._convert_mutables(value))
@ -450,7 +453,9 @@ def deserialize(obj):
elif tname in ("_SaverOrderedDict", "OrderedDict"): elif tname in ("_SaverOrderedDict", "OrderedDict"):
return OrderedDict([(_iter(key), _iter(val)) for key, val in obj.items()]) return OrderedDict([(_iter(key), _iter(val)) for key, val in obj.items()])
elif tname in ("_SaverDefaultDict", "defaultdict"): elif tname in ("_SaverDefaultDict", "defaultdict"):
return defaultdict(obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()}) return defaultdict(
obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()}
)
elif tname in _DESERIALIZE_MAPPING: elif tname in _DESERIALIZE_MAPPING:
return _DESERIALIZE_MAPPING[tname](_iter(val) for val in obj) return _DESERIALIZE_MAPPING[tname](_iter(val) for val in obj)
elif is_iter(obj): elif is_iter(obj):
@ -602,7 +607,9 @@ def to_pickle(data):
def process_item(item): def process_item(item):
"""Recursive processor and identification of data""" """Recursive processor and identification of data"""
dtype = type(item) dtype = type(item)
if dtype in (str, int, float, bool, bytes, SafeString): if dtype in (str, int, float, bool, bytes, SafeString):
return item return item
elif dtype == tuple: elif dtype == tuple:
@ -612,7 +619,10 @@ def to_pickle(data):
elif dtype in (dict, _SaverDict): elif dtype in (dict, _SaverDict):
return dict((process_item(key), process_item(val)) for key, val in item.items()) return dict((process_item(key), process_item(val)) for key, val in item.items())
elif dtype in (defaultdict, _SaverDefaultDict): elif dtype in (defaultdict, _SaverDefaultDict):
return defaultdict(item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items())) return defaultdict(
item.default_factory,
((process_item(key), process_item(val)) for key, val in item.items()),
)
elif dtype in (set, _SaverSet): elif dtype in (set, _SaverSet):
return set(process_item(val) for val in item) return set(process_item(val) for val in item)
elif dtype in (OrderedDict, _SaverOrderedDict): elif dtype in (OrderedDict, _SaverOrderedDict):
@ -620,7 +630,20 @@ def to_pickle(data):
elif dtype in (deque, _SaverDeque): elif dtype in (deque, _SaverDeque):
return deque(process_item(val) for val in item) return deque(process_item(val) for val in item)
elif hasattr(item, "__iter__"): # not one of the base types
if hasattr(item, "__serialize_dbobjs__"):
# Allows custom serialization of any dbobjects embedded in
# the item that Evennia will otherwise not found (these would
# otherwise lead to an error). Use the dbserialize helper from
# this method.
try:
item.__serialize_dbobjs__()
except TypeError:
# we catch typerrors so we can handle both classes (requiring
# classmethods) and instances
pass
if hasattr(item, "__iter__"):
# we try to conserve the iterable class, if not convert to list # we try to conserve the iterable class, if not convert to list
try: try:
return item.__class__([process_item(val) for val in item]) return item.__class__([process_item(val) for val in item])
@ -678,7 +701,10 @@ def from_pickle(data, db_obj=None):
elif dtype == dict: elif dtype == dict:
return dict((process_item(key), process_item(val)) for key, val in item.items()) return dict((process_item(key), process_item(val)) for key, val in item.items())
elif dtype == defaultdict: elif dtype == defaultdict:
return defaultdict(item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items())) return defaultdict(
item.default_factory,
((process_item(key), process_item(val)) for key, val in item.items()),
)
elif dtype == set: elif dtype == set:
return set(process_item(val) for val in item) return set(process_item(val) for val in item)
elif dtype == OrderedDict: elif dtype == OrderedDict:
@ -692,6 +718,18 @@ def from_pickle(data, db_obj=None):
return item.__class__(process_item(val) for val in item) return item.__class__(process_item(val) for val in item)
except (AttributeError, TypeError): except (AttributeError, TypeError):
return [process_item(val) for val in item] return [process_item(val) for val in item]
if hasattr(item, "__deserialize_dbobjs__"):
# this allows the object to custom-deserialize any embedded dbobjs
# that we previously serialized with __serialize_dbobjs__.
# use the dbunserialize helper in this module.
try:
item.__deserialize_dbobjs__()
except TypeError:
# handle recoveries both of classes (requiring classmethods
# or instances
pass
return item return item
def process_tree(item, parent): def process_tree(item, parent):

View file

@ -274,12 +274,13 @@ import inspect
from ast import literal_eval from ast import literal_eval
from fnmatch import fnmatch from fnmatch import fnmatch
from math import ceil
from inspect import isfunction, getargspec from inspect import isfunction, getargspec
from django.conf import settings from django.conf import settings
from evennia import Command, CmdSet from evennia import Command, CmdSet
from evennia.utils import logger from evennia.utils import logger
from evennia.utils.evtable import EvTable from evennia.utils.evtable import EvTable, EvColumn
from evennia.utils.ansi import strip_ansi from evennia.utils.ansi import strip_ansi
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
from evennia.commands import cmdhandler from evennia.commands import cmdhandler
@ -1210,7 +1211,6 @@ class EvMenu:
Args: Args:
optionlist (list): List of (key, description) tuples for every optionlist (list): List of (key, description) tuples for every
option related to this node. option related to this node.
caller (Object, Account or None, optional): The caller of the node.
Returns: Returns:
options (str): The formatted option display. options (str): The formatted option display.
@ -1229,7 +1229,7 @@ class EvMenu:
table = [] table = []
for key, desc in optionlist: for key, desc in optionlist:
if key or desc: if key or desc:
desc_string = ": %s" % desc if desc else "" desc_string = f": {desc}" if desc else ""
table_width_max = max( table_width_max = max(
table_width_max, table_width_max,
max(m_len(p) for p in key.split("\n")) max(m_len(p) for p in key.split("\n"))
@ -1239,42 +1239,31 @@ class EvMenu:
raw_key = strip_ansi(key) raw_key = strip_ansi(key)
if raw_key != key: if raw_key != key:
# already decorations in key definition # already decorations in key definition
table.append(" |lc%s|lt%s|le%s" % (raw_key, key, desc_string)) table.append(f" |lc{raw_key}|lt{key}|le{desc_string}")
else: else:
# add a default white color to key # add a default white color to key
table.append(" |lc%s|lt|w%s|n|le%s" % (raw_key, raw_key, desc_string)) table.append(f" |lc{raw_key}|lt|w{key}|n|le{desc_string}")
ncols = _MAX_TEXT_WIDTH // table_width_max # number of ncols ncols = _MAX_TEXT_WIDTH // table_width_max # number of columns
if ncols < 0: if ncols < 0:
# no visible option at all # no visible options at all
return "" return ""
ncols = ncols + 1 if ncols == 0 else ncols ncols = 1 if ncols == 0 else ncols
# get the amount of rows needed (start with 4 rows)
nrows = 4
while nrows * ncols < nlist:
nrows += 1
ncols = nlist // nrows # number of full columns
nlastcol = nlist % nrows # number of elements in last column
# get the final column count # minimum number of rows in a column
ncols = ncols + 1 if nlastcol > 0 else ncols min_rows = 4
if ncols > 1:
# only extend if longer than one column
table.extend([" " for i in range(nrows - nlastcol)])
# build the actual table grid # split the items into columns
table = [table[icol * nrows : (icol * nrows) + nrows] for icol in range(0, ncols)] split = max(min_rows, ceil(len(table)/ncols))
max_end = len(table)
cols_list = []
for icol in range(ncols):
start = icol*split
end = min(start+split,max_end)
cols_list.append(EvColumn(*table[start:end]))
# adjust the width of each column return str(EvTable(table=cols_list, border="none"))
for icol in range(len(table)):
col_width = (
max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep
)
table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]]
# format the table into columns
return str(EvTable(table=table, border="none"))
def node_formatter(self, nodetext, optionstext): def node_formatter(self, nodetext, optionstext):
""" """

View file

@ -67,7 +67,7 @@ class TimeScript(DefaultScript):
callback(*args, **kwargs) callback(*args, **kwargs)
seconds = real_seconds_until(**self.db.gametime) seconds = real_seconds_until(**self.db.gametime)
self.restart(interval=seconds) self.start(interval=seconds,force_restart=True)
# Access functions # Access functions

View file

@ -62,10 +62,12 @@ class TestDbSerialize(TestCase):
self.obj.db.test.sort(key=lambda d: str(d)) self.obj.db.test.sort(key=lambda d: str(d))
self.assertEqual(self.obj.db.test, [{0: 1}, {1: 0}]) self.assertEqual(self.obj.db.test, [{0: 1}, {1: 0}])
def test_dict(self): def test_saverdict(self):
self.obj.db.test = {"a": True} self.obj.db.test = {"a": True}
self.obj.db.test.update({"b": False}) self.obj.db.test.update({"b": False})
self.assertEqual(self.obj.db.test, {"a": True, "b": False}) self.assertEqual(self.obj.db.test, {"a": True, "b": False})
self.obj.db.test |= {"c": 5}
self.assertEqual(self.obj.db.test, {"a": True, "b": False, "c": 5})
@parameterized.expand( @parameterized.expand(
[ [
@ -88,27 +90,30 @@ class TestDbSerialize(TestCase):
self.assertIsInstance(value, base_type) self.assertIsInstance(value, base_type)
self.assertNotIsInstance(value, saver_type) self.assertNotIsInstance(value, saver_type)
self.assertEqual(value, default_value) self.assertEqual(value, default_value)
self.obj.db.test = {'a': True} self.obj.db.test = {"a": True}
self.obj.db.test.update({'b': False}) self.obj.db.test.update({"b": False})
self.assertEqual(self.obj.db.test, {'a': True, 'b': False}) self.assertEqual(self.obj.db.test, {"a": True, "b": False})
def test_defaultdict(self): def test_defaultdict(self):
from collections import defaultdict from collections import defaultdict
# baseline behavior for a defaultdict # baseline behavior for a defaultdict
_dd = defaultdict(list) _dd = defaultdict(list)
_dd['a'] _dd["a"]
self.assertEqual(_dd, {'a': []}) self.assertEqual(_dd, {"a": []})
# behavior after defaultdict is set as attribute # behavior after defaultdict is set as attribute
dd = defaultdict(list) dd = defaultdict(list)
self.obj.db.test = dd self.obj.db.test = dd
self.obj.db.test['a'] self.obj.db.test["a"]
self.assertEqual(self.obj.db.test, {'a': []}) self.assertEqual(self.obj.db.test, {"a": []})
self.obj.db.test['a'].append(1) self.obj.db.test["a"].append(1)
self.assertEqual(self.obj.db.test, {'a': [1]}) self.assertEqual(self.obj.db.test, {"a": [1]})
self.obj.db.test['a'].append(2) self.obj.db.test["a"].append(2)
self.assertEqual(self.obj.db.test, {'a': [1, 2]}) self.assertEqual(self.obj.db.test, {"a": [1, 2]})
self.obj.db.test['a'].append(3) self.obj.db.test["a"].append(3)
self.assertEqual(self.obj.db.test, {'a': [1, 2, 3]}) self.assertEqual(self.obj.db.test, {"a": [1, 2, 3]})
self.obj.db.test |= {"b": [5, 6]}
self.assertEqual(self.obj.db.test, {"a": [1, 2, 3], "b": [5, 6]})

View file

@ -7,20 +7,20 @@ import mock
class TestText2Html(TestCase): class TestText2Html(TestCase):
def test_re_color(self): def test_format_styles(self):
parser = text2html.HTML_PARSER parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_color("foo")) self.assertEqual("foo", parser.format_styles("foo"))
self.assertEqual( self.assertEqual(
'<span class="color-001">red</span>foo', '<span class="color-001">red</span>foo',
parser.re_color(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"), parser.format_styles(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"),
) )
self.assertEqual( self.assertEqual(
'<span class="bgcolor-001">red</span>foo', '<span class="bgcolor-001">red</span>foo',
parser.re_color(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"), parser.format_styles(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
) )
self.assertEqual( self.assertEqual(
'<span class="bgcolor-001"><span class="color-002">red</span></span>foo', '<span class="bgcolor-001 color-002">red</span>foo',
parser.re_color( parser.format_styles(
ansi.ANSI_BACK_RED ansi.ANSI_BACK_RED
+ ansi.ANSI_UNHILITE + ansi.ANSI_UNHILITE
+ ansi.ANSI_GREEN + ansi.ANSI_GREEN
@ -29,63 +29,37 @@ class TestText2Html(TestCase):
+ "foo" + "foo"
), ),
) )
@unittest.skip("parser issues")
def test_re_bold(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_bold("foo"))
self.assertEqual( self.assertEqual(
# "a <strong>red</strong>foo", # TODO: why not? 'a <span class="underline">red</span>foo',
"a <strong>redfoo</strong>", parser.format_styles(
parser.re_bold("a " + ansi.ANSI_HILITE + "red" + ansi.ANSI_UNHILITE + "foo"),
)
@unittest.skip("parser issues")
def test_re_underline(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_underline("foo"))
self.assertEqual(
'a <span class="underline">red</span>' + ansi.ANSI_NORMAL + "foo",
parser.re_underline(
"a " "a "
+ ansi.ANSI_UNDERLINE + ansi.ANSI_UNDERLINE
+ "red" + "red"
+ ansi.ANSI_NORMAL # TODO: why does it keep it? + ansi.ANSI_NORMAL
+ "foo" + "foo"
), ),
) )
@unittest.skip("parser issues")
def test_re_blinking(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_blinking("foo"))
self.assertEqual( self.assertEqual(
'a <span class="blink">red</span>' + ansi.ANSI_NORMAL + "foo", 'a <span class="blink">red</span>foo',
parser.re_blinking( parser.format_styles(
"a " "a "
+ ansi.ANSI_BLINK + ansi.ANSI_BLINK
+ "red" + "red"
+ ansi.ANSI_NORMAL # TODO: why does it keep it? + ansi.ANSI_NORMAL
+ "foo" + "foo"
), ),
) )
@unittest.skip("parser issues")
def test_re_inversing(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_inversing("foo"))
self.assertEqual( self.assertEqual(
'a <span class="inverse">red</span>' + ansi.ANSI_NORMAL + "foo", 'a <span class="bgcolor-007 color-000">red</span>foo',
parser.re_inversing( parser.format_styles(
"a " "a "
+ ansi.ANSI_INVERSE + ansi.ANSI_INVERSE
+ "red" + "red"
+ ansi.ANSI_NORMAL # TODO: why does it keep it? + ansi.ANSI_NORMAL
+ "foo" + "foo"
), ),
) )
@unittest.skip("parser issues")
def test_remove_bells(self): def test_remove_bells(self):
parser = text2html.HTML_PARSER parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.remove_bells("foo")) self.assertEqual("foo", parser.remove_bells("foo"))
@ -95,7 +69,7 @@ class TestText2Html(TestCase):
"a " "a "
+ ansi.ANSI_BEEP + ansi.ANSI_BEEP
+ "red" + "red"
+ ansi.ANSI_NORMAL # TODO: why does it keep it? + ansi.ANSI_NORMAL
+ "foo" + "foo"
), ),
) )
@ -110,7 +84,6 @@ class TestText2Html(TestCase):
self.assertEqual("foo", parser.convert_linebreaks("foo")) self.assertEqual("foo", parser.convert_linebreaks("foo"))
self.assertEqual("a<br> redfoo<br>", parser.convert_linebreaks("a\n redfoo\n")) self.assertEqual("a<br> redfoo<br>", parser.convert_linebreaks("a\n redfoo\n"))
@unittest.skip("parser issues")
def test_convert_urls(self): def test_convert_urls(self):
parser = text2html.HTML_PARSER parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.convert_urls("foo")) self.assertEqual("foo", parser.convert_urls("foo"))
@ -118,7 +91,6 @@ class TestText2Html(TestCase):
'a <a href="http://redfoo" target="_blank">http://redfoo</a> runs', 'a <a href="http://redfoo" target="_blank">http://redfoo</a> runs',
parser.convert_urls("a http://redfoo runs"), parser.convert_urls("a http://redfoo runs"),
) )
# TODO: doesn't URL encode correctly
def test_sub_mxp_links(self): def test_sub_mxp_links(self):
parser = text2html.HTML_PARSER parser = text2html.HTML_PARSER
@ -186,22 +158,22 @@ class TestText2Html(TestCase):
self.assertEqual("foo", text2html.parse_html("foo")) self.assertEqual("foo", text2html.parse_html("foo"))
self.maxDiff = None self.maxDiff = None
self.assertEqual( self.assertEqual(
# TODO: note that the blink is currently *not* correctly aborted
# with |n here! This is probably not possible to correctly handle
# with regex - a stateful parser may be needed.
# blink back-cyan normal underline red green yellow blue magenta cyan back-green
text2html.parse_html("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"), text2html.parse_html("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"),
'<span class="blink">' '<span class="blink bgcolor-006">'
'<span class="bgcolor-006">Hello</span>' # noqa 'Hello'
'<span class="underline">' '</span><span class="underline color-009">'
'<span class="color-009">W</span>' # noqa 'W'
'<span class="color-010">o</span>' '</span><span class="underline color-010">'
'<span class="color-011">r</span>' 'o'
'<span class="color-012">l</span>' '</span><span class="underline color-011">'
'<span class="color-013">d</span>' 'r'
'<span class="color-014">!' '</span><span class="underline color-012">'
'<span class="bgcolor-002">!</span>' # noqa 'l'
"</span>" '</span><span class="underline color-013">'
"</span>" 'd'
"</span>", '</span><span class="underline color-014">'
'!'
'</span><span class="underline bgcolor-002 color-014">'
'!'
'</span>',
) )

View file

@ -12,11 +12,10 @@ import re
from html import escape as html_escape from html import escape as html_escape
from .ansi import * from .ansi import *
# All xterm256 RGB equivalents # All xterm256 RGB equivalents
XTERM256_FG = "\033[38;5;%sm" XTERM256_FG = "\033[38;5;{}m"
XTERM256_BG = "\033[48;5;%sm" XTERM256_BG = "\033[48;5;{}m"
class TextToHTMLparser(object): class TextToHTMLparser(object):
@ -25,77 +24,65 @@ class TextToHTMLparser(object):
""" """
tabstop = 4 tabstop = 4
# mapping html color name <-> ansi code.
hilite = ANSI_HILITE
unhilite = ANSI_UNHILITE # this will be stripped - there is no css equivalent.
normal = ANSI_NORMAL # "
underline = ANSI_UNDERLINE
blink = ANSI_BLINK
inverse = ANSI_INVERSE # this will produce an outline; no obvious css equivalent?
colorcodes = [
("color-000", unhilite + ANSI_BLACK), # pure black
("color-001", unhilite + ANSI_RED),
("color-002", unhilite + ANSI_GREEN),
("color-003", unhilite + ANSI_YELLOW),
("color-004", unhilite + ANSI_BLUE),
("color-005", unhilite + ANSI_MAGENTA),
("color-006", unhilite + ANSI_CYAN),
("color-007", unhilite + ANSI_WHITE), # light grey
("color-008", hilite + ANSI_BLACK), # dark grey
("color-009", hilite + ANSI_RED),
("color-010", hilite + ANSI_GREEN),
("color-011", hilite + ANSI_YELLOW),
("color-012", hilite + ANSI_BLUE),
("color-013", hilite + ANSI_MAGENTA),
("color-014", hilite + ANSI_CYAN),
("color-015", hilite + ANSI_WHITE), # pure white
] + [("color-%03i" % (i + 16), XTERM256_FG % ("%i" % (i + 16))) for i in range(240)]
colorback = [ style_codes = [
("bgcolor-000", ANSI_BACK_BLACK), # pure black # non-color style markers
("bgcolor-001", ANSI_BACK_RED), ANSI_NORMAL,
("bgcolor-002", ANSI_BACK_GREEN), ANSI_UNDERLINE,
("bgcolor-003", ANSI_BACK_YELLOW), ANSI_HILITE,
("bgcolor-004", ANSI_BACK_BLUE), ANSI_UNHILITE,
("bgcolor-005", ANSI_BACK_MAGENTA), ANSI_INVERSE,
("bgcolor-006", ANSI_BACK_CYAN), ANSI_BLINK,
("bgcolor-007", ANSI_BACK_WHITE), # light grey ANSI_INV_HILITE,
("bgcolor-008", hilite + ANSI_BACK_BLACK), # dark grey ANSI_BLINK_HILITE,
("bgcolor-009", hilite + ANSI_BACK_RED), ANSI_INV_BLINK,
("bgcolor-010", hilite + ANSI_BACK_GREEN), ANSI_INV_BLINK_HILITE,
("bgcolor-011", hilite + ANSI_BACK_YELLOW), ]
("bgcolor-012", hilite + ANSI_BACK_BLUE),
("bgcolor-013", hilite + ANSI_BACK_MAGENTA),
("bgcolor-014", hilite + ANSI_BACK_CYAN),
("bgcolor-015", hilite + ANSI_BACK_WHITE), # pure white
] + [("bgcolor-%03i" % (i + 16), XTERM256_BG % ("%i" % (i + 16))) for i in range(240)]
# make sure to escape [ ansi_color_codes = [
# colorcodes = [(c, code.replace("[", r"\[")) for c, code in colorcodes] # Foreground colors
# colorback = [(c, code.replace("[", r"\[")) for c, code in colorback] ANSI_BLACK,
fg_colormap = dict((code, clr) for clr, code in colorcodes) ANSI_RED,
bg_colormap = dict((code, clr) for clr, code in colorback) ANSI_GREEN,
ANSI_YELLOW,
ANSI_BLUE,
ANSI_MAGENTA,
ANSI_CYAN,
ANSI_WHITE,
]
# create stop markers xterm_fg_codes = [XTERM256_FG.format(i + 16) for i in range(240)]
fgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m|\033\[0m|$"
bgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m|\033\[0m|$"
bgfgstop = bgstop[:-2] + fgstop
fgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m)" ansi_bg_codes = [
bgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m)" # Background colors
bgfgstart = bgstart + r"((?:\033\[1m|\033\[22m){0,1}\033\[[3-4][0-8].*?m){0,1}" ANSI_BACK_BLACK,
ANSI_BACK_RED,
ANSI_BACK_GREEN,
ANSI_BACK_YELLOW,
ANSI_BACK_BLUE,
ANSI_BACK_MAGENTA,
ANSI_BACK_CYAN,
ANSI_BACK_WHITE,
]
# extract color markers, tagging the start marker and the text marked xterm_bg_codes = [XTERM256_BG.format(i + 16) for i in range(240)]
re_fgs = re.compile(fgstart + "(.*?)(?=" + fgstop + ")")
re_bgs = re.compile(bgstart + "(.*?)(?=" + bgstop + ")") re_style = re.compile(
re_bgfg = re.compile(bgfgstart + "(.*?)(?=" + bgfgstop + ")") r"({})".format(
"|".join(
style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes
).replace("[", r"\[")
)
)
colorlist = (
[ANSI_UNHILITE + code for code in ansi_color_codes]
+ [ANSI_HILITE + code for code in ansi_color_codes]
+ xterm_fg_codes
)
bglist = ansi_bg_codes + [ANSI_HILITE + code for code in ansi_bg_codes] + xterm_bg_codes
re_normal = re.compile(normal.replace("[", r"\["))
re_hilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (hilite.replace("[", r"\["), fgstop, bgstop))
re_unhilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (unhilite.replace("[", r"\["), fgstop, bgstop))
re_uline = re.compile("(?:%s)(.*?)(?=%s|%s)" % (underline.replace("[", r"\["), fgstop, bgstop))
re_blink = re.compile("(?:%s)(.*?)(?=%s|%s)" % (blink.replace("[", r"\["), fgstop, bgstop))
re_inverse = re.compile("(?:%s)(.*?)(?=%s|%s)" % (inverse.replace("[", r"\["), fgstop, bgstop))
re_string = re.compile( re_string = re.compile(
r"(?P<htmlchars>[<&>])|(?P<tab>[\t]+)|(?P<lineend>\r\n|\r|\n)", r"(?P<htmlchars>[<&>])|(?P<tab>[\t]+)|(?P<lineend>\r\n|\r|\n)",
re.S | re.M | re.I, re.S | re.M | re.I,
@ -106,100 +93,6 @@ class TextToHTMLparser(object):
re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL) re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL) re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
def _sub_bgfg(self, colormatch):
# print("colormatch.groups()", colormatch.groups())
bgcode, fgcode, text = colormatch.groups()
if not fgcode:
ret = r"""<span class="%s">%s</span>""" % (
self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")),
text,
)
else:
ret = r"""<span class="%s"><span class="%s">%s</span></span>""" % (
self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")),
self.fg_colormap.get(fgcode, self.bg_colormap.get(fgcode, "err")),
text,
)
return ret
def _sub_fg(self, colormatch):
code, text = colormatch.groups()
return r"""<span class="%s">%s</span>""" % (self.fg_colormap.get(code, "err"), text)
def _sub_bg(self, colormatch):
code, text = colormatch.groups()
return r"""<span class="%s">%s</span>""" % (self.bg_colormap.get(code, "err"), text)
def re_color(self, text):
"""
Replace ansi colors with html color class names. Let the
client choose how it will display colors, if it wishes to.
Args:
text (str): the string with color to replace.
Returns:
text (str): Re-colored text.
"""
text = self.re_bgfg.sub(self._sub_bgfg, text)
text = self.re_fgs.sub(self._sub_fg, text)
text = self.re_bgs.sub(self._sub_bg, text)
text = self.re_normal.sub("", text)
return text
def re_bold(self, text):
"""
Clean out superfluous hilights rather than set <strong>to make
it match the look of telnet.
Args:
text (str): Text to process.
Returns:
text (str): Processed text.
"""
text = self.re_hilite.sub(r"<strong>\1</strong>", text)
return self.re_unhilite.sub(r"\1", text) # strip unhilite - there is no equivalent in css.
def re_underline(self, text):
"""
Replace ansi underline with html underline class name.
Args:
text (str): Text to process.
Returns:
text (str): Processed text.
"""
return self.re_uline.sub(r'<span class="underline">\1</span>', text)
def re_blinking(self, text):
"""
Replace ansi blink with custom blink css class
Args:
text (str): Text to process.
Returns:
text (str): Processed text.
"""
return self.re_blink.sub(r'<span class="blink">\1</span>', text)
def re_inversing(self, text):
"""
Replace ansi inverse with custom inverse css class
Args:
text (str): Text to process.
Returns:
text (str): Processed text.
"""
return self.re_inverse.sub(r'<span class="inverse">\1</span>', text)
def remove_bells(self, text): def remove_bells(self, text):
""" """
Remove ansi specials Remove ansi specials
@ -211,7 +104,7 @@ class TextToHTMLparser(object):
text (str): Processed text. text (str): Processed text.
""" """
return text.replace("\07", "") return text.replace(ANSI_BEEP, "")
def remove_backspaces(self, text): def remove_backspaces(self, text):
""" """
@ -315,6 +208,128 @@ class TextToHTMLparser(object):
return text return text
return None return None
def format_styles(self, text):
"""
Takes a string with parsed ANSI codes and replaces them with
HTML spans and CSS classes.
Args:
text (str): The string to process.
Returns:
text (str): Processed text.
"""
# split out the ANSI codes and clean out any empty items
str_list = [substr for substr in self.re_style.split(text) if substr]
# initialize all the flags and classes
classes = []
clean = True
inverse = False
# default color is light grey - unhilite + white
hilight = ANSI_UNHILITE
fg = ANSI_WHITE
# default bg is black
bg = ANSI_BACK_BLACK
for i, substr in enumerate(str_list):
# reset all current styling
if substr == ANSI_NORMAL and not clean:
# replace with close existing tag
str_list[i] = "</span>"
# reset to defaults
classes = []
clean = True
inverse = False
hilight = ANSI_UNHILITE
fg = ANSI_WHITE
bg = ANSI_BACK_BLACK
# change color
elif substr in self.ansi_color_codes + self.xterm_fg_codes:
# erase ANSI code from output
str_list[i] = ""
# set new color
fg = substr
# change bg color
elif substr in self.ansi_bg_codes + self.xterm_bg_codes:
# erase ANSI code from output
str_list[i] = ""
# set new bg
bg = substr
# non-color codes
elif substr in self.style_codes:
# erase ANSI code from output
str_list[i] = ""
# hilight codes
if substr in (ANSI_HILITE, ANSI_UNHILITE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
# set new hilight status
hilight = ANSI_UNHILITE if substr == ANSI_UNHILITE else ANSI_HILITE
# inversion codes
if substr in (ANSI_INVERSE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
inverse = True
# blink codes
if (
substr in (ANSI_BLINK, ANSI_BLINK_HILITE, ANSI_INV_BLINK_HILITE)
and "blink" not in classes
):
classes.append("blink")
# underline
if substr == ANSI_UNDERLINE and "underline" not in classes:
classes.append("underline")
else:
# normal text, add text back to list
if not str_list[i - 1]:
# prior entry was cleared, which means style change
# get indices for the fg and bg codes
bg_index = self.bglist.index(bg)
try:
color_index = self.colorlist.index(hilight + fg)
except ValueError:
# xterm256 colors don't have the hilight codes
color_index = self.colorlist.index(fg)
if inverse:
# inverse means swap fg and bg indices
bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0"))
color_class = "color-{}".format(str(bg_index).rjust(3, "0"))
else:
# use fg and bg indices for classes
bg_class = "bgcolor-{}".format(str(bg_index).rjust(3, "0"))
color_class = "color-{}".format(str(color_index).rjust(3, "0"))
# black bg is the default, don't explicitly style
if bg_class != "bgcolor-000":
classes.append(bg_class)
# light grey text is the default, don't explicitly style
if color_class != "color-007":
classes.append(color_class)
# define the new style span
prefix = '<span class="{}">'.format(" ".join(classes))
# close any prior span
if not clean:
prefix = "</span>" + prefix
# add span to output
str_list[i - 1] = prefix
# clean out color classes to easily update next time
classes = [cls for cls in classes if "color" not in cls]
# flag as currently being styled
clean = False
# close span if necessary
if not clean:
str_list.append("</span>")
# recombine back into string
return "".join(str_list)
def parse(self, text, strip_ansi=False): def parse(self, text, strip_ansi=False):
""" """
Main access function, converts a text containing ANSI codes Main access function, converts a text containing ANSI codes
@ -328,19 +343,14 @@ class TextToHTMLparser(object):
text (str): Parsed text. text (str): Parsed text.
""" """
# print(f"incoming text:\n{text}")
# parse everything to ansi first # parse everything to ansi first
text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True) text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True)
# convert all ansi to html # convert all ansi to html
result = re.sub(self.re_string, self.sub_text, text) result = re.sub(self.re_string, self.sub_text, text)
result = re.sub(self.re_mxplink, self.sub_mxp_links, result) result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result) result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result)
result = self.re_color(result)
result = self.re_bold(result)
result = self.re_underline(result)
result = self.re_blinking(result)
result = self.re_inversing(result)
result = self.remove_bells(result) result = self.remove_bells(result)
result = self.format_styles(result)
result = self.convert_linebreaks(result) result = self.convert_linebreaks(result)
result = self.remove_backspaces(result) result = self.remove_backspaces(result)
result = self.convert_urls(result) result = self.convert_urls(result)

View file

@ -149,7 +149,7 @@ An "emitter" object must have a function
// kwargs (obj): keyword-args to listener // kwargs (obj): keyword-args to listener
// //
emit: function (cmdname, args, kwargs) { emit: function (cmdname, args, kwargs) {
if (kwargs.cmdid) { if (kwargs.cmdid && (kwargs.cmdid in cmdmap)) {
cmdmap[kwargs.cmdid].apply(this, [args, kwargs]); cmdmap[kwargs.cmdid].apply(this, [args, kwargs]);
delete cmdmap[kwargs.cmdid]; delete cmdmap[kwargs.cmdid];
} }