""" Inline functions (nested form). This parser accepts nested inlinefunctions on the form ``` $funcname(arg, arg, ...) ``` embedded in any text where any arg can be another $funcname{} call. This functionality is turned off by default - to activate, `settings.INLINEFUNC_ENABLED` must be set to `True`. Each token starts with "$funcname(" where there must be no space between the $funcname and (. It ends with a matched ending parentesis. ")". Inside the inlinefunc definition, one can use `\` to escape. This is mainly needed for escaping commas in flowing text (which would otherwise be interpreted as an argument separator), or to escape `}` when not intended to close the function block. Enclosing text in matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will also escape *everything* within without needing to escape individual characters. The available inlinefuncs are defined as global-level functions in modules defined by `settings.INLINEFUNC_MODULES`. They are identified by their function name (and ignored if this name starts with `_`). They should be on the following form: ```python def funcname (*args, **kwargs): # ... ``` Here, the arguments given to `$funcname(arg1,arg2)` will appear as the `*args` tuple. This will be populated by the arguments given to the inlinefunc in-game - the only part that will be available from in-game. `**kwargs` are not supported from in-game but are only used internally by Evennia to make details about the caller available to the function. The kwarg passed to all functions is `session`, the Sessionobject for the object seeing the string. This may be `None` if the string is sent to a non-puppetable object. The inlinefunc should never raise an exception. There are two reserved function names: - "nomatch": This is called if the user uses a functionname that is not registered. The nomatch function will get the name of the not-found function as its first argument followed by the normal arguments to the given function. If not defined the default effect is to print `` to replace the unknown function. - "stackfull": This is called when the maximum nested function stack is reached. When this happens, the original parsed string is returned and the result of the `stackfull` inlinefunc is appended to the end. By default this is an error message. Error handling: Syntax errors, notably not completely closing all inlinefunc blocks, will lead to the entire string remaining unparsed. """ import re from django.conf import settings from evennia.utils import utils # example/testing inline functions def pad(*args, **kwargs): """ Inlinefunc. Pads text to given width. Args: text (str, optional): Text to pad. width (str, optional): Will be converted to integer. Width of padding. align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'. fillchar (str, optional): Character used for padding. Defaults to a space. Kwargs: session (Session): Session performing the pad. Example: `$pad(text, width, align, fillchar)` """ text, width, align, fillchar = "", 78, 'c', ' ' nargs = len(args) if nargs > 0: text = args[0] if nargs > 1: width = int(args[1]) if args[1].strip().isdigit() else 78 if nargs > 2: align = args[2] if args[2] in ('c', 'l', 'r') else 'c' if nargs > 3: fillchar = args[3] return utils.pad(text, width=width, align=align, fillchar=fillchar) def crop(*args, **kwargs): """ Inlinefunc. Crops ingoing text to given widths. Args: text (str, optional): Text to crop. width (str, optional): Will be converted to an integer. Width of crop in characters. suffix (str, optional): End string to mark the fact that a part of the string was cropped. Defaults to `[...]`. Kwargs: session (Session): Session performing the crop. Example: `$crop(text, width=78, suffix='[...]')` """ text, width, suffix = "", 78, "[...]" nargs = len(args) if nargs > 0: text = args[0] if nargs > 1: width = int(args[1]) if args[1].strip().isdigit() else 78 if nargs > 2: suffix = args[2] return utils.crop(text, width=width, suffix=suffix) def clr(*args, **kwargs): """ Inlinefunc. Colorizes nested text. Args: startclr (str, optional): An ANSI color abbreviation without the prefix `|`, such as `r` (red foreground) or `[r` (red background). text (str, optional): Text endclr (str, optional): The color to use at the end of the string. Defaults to `|n` (reset-color). Kwargs: session (Session): Session object triggering inlinefunc. Example: `$clr(startclr, text, endclr)` """ text = "" nargs = len(args) if nargs > 0: color = args[0].strip() if nargs > 1: text = args[1] text = "|" + color + text if nargs > 2: text += "|" + args[2].strip() else: text += "|n" return text # we specify a default nomatch function to use if no matching func was # found. This will be overloaded by any nomatch function defined in # the imported modules. _INLINE_FUNCS = {"nomatch": lambda *args, **kwargs: "", "stackfull": lambda *args, **kwargs: "\n (not parsed: inlinefunc stack size exceeded.)"} # load custom inline func modules. for module in utils.make_iter(settings.INLINEFUNC_MODULES): try: _INLINE_FUNCS.update(utils.callables_from_module(module)) except ImportError as err: if module == "server.conf.inlinefuncs": # a temporary warning since the default module changed name raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should " "be renamed to mygame/server/conf/inlinefuncs.py (note the S at the end)." % err) else: raise # remove the core function if we include examples in this module itself #_INLINE_FUNCS.pop("inline_func_parse", None) # The stack size is a security measure. Set to <=0 to disable. try: _STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE except AttributeError: _STACK_MAXSIZE = 20 # regex definitions _RE_STARTTOKEN = re.compile(r"(?.*?)(?.*?)(?(?(?(?\\'|\\"|\\\)|\\$\w+\()| # escaped tokens should re-appear in text (?P[\w\s.-\/#!%\^&\*;:=\-_`~\|\(}{\[\]]+|\"{1}|\'{1}) # everything else should also be included""", re.UNICODE + re.IGNORECASE + re.VERBOSE + re.DOTALL) # Cache for function lookups. _PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000) class ParseStack(list): """ Custom stack that always concatenates strings together when the strings are added next to one another. Tuples are stored separately and None is used to mark that a string should be broken up into a new chunk. Below is the resulting stack after separately appending 3 strings, None, 2 strings, a tuple and finally 2 strings: [string + string + string, None string + string, tuple, string + string] """ def __init__(self, *args, **kwargs): super(ParseStack, self).__init__(*args, **kwargs) # always start stack with the empty string list.append(self, "") # indicates if the top of the stack is a string or not self._string_last = True def append(self, item): """ The stack will merge strings, add other things as normal """ if isinstance(item, basestring): if self._string_last: self[-1] += item else: list.append(self, item) self._string_last = True else: # everything else is added as normal list.append(self, item) self._string_last = False class InlinefuncError(RuntimeError): pass def parse_inlinefunc(string, strip=False, **kwargs): """ Parse the incoming string. Args: string (str): The incoming string to parse. strip (bool, optional): Whether to strip function calls rather than execute them. Kwargs: session (Session): This is sent to this function by Evennia when triggering it. It is passed to the inlinefunc. kwargs (any): All other kwargs are also passed on to the inlinefunc. """ global _PARSING_CACHE if string in _PARSING_CACHE: # stack is already cached stack = _PARSING_CACHE[string] else: # not a cached string. if not _RE_STARTTOKEN.search(string): # if there are no unescaped start tokens at all, return immediately. return string # build a new cache entry stack = ParseStack() ncallable = 0 for match in _RE_TOKEN.finditer(string): gdict = match.groupdict() if gdict["singlequote"]: stack.append(gdict["singlequote"]) elif gdict["doublequote"]: stack.append(gdict["doublequote"]) elif gdict["end"]: if ncallable <= 0: stack.append(")") continue args = [] while stack: operation = stack.pop() if callable(operation): if not strip: stack.append((operation, [arg for arg in reversed(args)])) ncallable -= 1 break else: args.append(operation) elif gdict["start"]: funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1) try: # try to fetch the matching inlinefunc from storage stack.append(_INLINE_FUNCS[funcname]) except KeyError: stack.append(_INLINE_FUNCS["nomatch"]) stack.append(funcname) ncallable += 1 elif gdict["escaped"]: # escaped tokens token = gdict["escaped"].lstrip("\\") stack.append(token) elif gdict["comma"]: if ncallable > 0: # commas outside strings and inside a callable are # used to mark argument separation - we use None # in the stack to indicate such a separation. stack.append(None) else: # no callable active - just a string stack.append(",") else: # the rest stack.append(gdict["rest"]) if ncallable > 0: # this means not all inlinefuncs were complete return string if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack): # if stack is larger than limit, throw away parsing return string + gdict["stackfull"](*args, **kwargs) else: # cache the result _PARSING_CACHE[string] = stack # run the stack recursively def _run_stack(item, depth=0): retval = item if isinstance(item, tuple): if strip: return "" else: func, arglist = item args = [""] for arg in arglist: if arg is None: # an argument-separating comma - start a new arg args.append("") else: # all other args should merge into one string args[-1] += _run_stack(arg, depth=depth+1) # execute the inlinefunc at this point or strip it. kwargs["inlinefunc_stack_depth"] = depth retval = "" if strip else func(*args, **kwargs) return utils.to_str(retval, force_string=True) # execute the stack from the cache return "".join(_run_stack(item) for item in _PARSING_CACHE[string]) # Nick templating # """ This supports the use of replacement templates in nicks: This happens in two steps: 1) The user supplies a template that is converted to a regex according to the unix-like templating language. 2) This regex is tested against nicks depending on which nick replacement strategy is considered (most commonly inputline). 3) If there is a template match and there are templating markers, these are replaced with the arguments actually given. @desc $1 $2 $3 This will be converted to the following regex: \@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+) Supported template markers (through fnmatch) * matches anything (non-greedy) -> .*? ? matches any single character -> [seq] matches any entry in sequence [!seq] matches entries not in sequence Custom arg markers $N argument position (1-99) """ import fnmatch _RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)") _RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)") _RE_NICK_SPACE = re.compile(r"\\ ") class NickTemplateInvalid(ValueError): pass def initialize_nick_templates(in_template, out_template): """ Initialize the nick templates for matching and remapping a string. Args: in_template (str): The template to be used for nick recognition. out_template (str): The template to be used to replace the string matched by the in_template. Returns: regex (regex): Regex to match against strings template (str): Template with markers {arg1}, {arg2}, etc for replacement using the standard .format method. Raises: NickTemplateInvalid: If the in/out template does not have a matching number of $args. """ # create the regex for in_template regex_string = fnmatch.translate(in_template) n_inargs = len(_RE_NICK_ARG.findall(regex_string)) regex_string = _RE_NICK_SPACE.sub("\s+", regex_string) regex_string = _RE_NICK_ARG.sub(lambda m: "(?P.+?)" % m.group(2), regex_string) # create the out_template template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template) # validate the tempaltes - they should at least have the same number of args n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template)) if n_inargs != n_outargs: print n_inargs, n_outargs raise NickTemplateInvalid return re.compile(regex_string), template_string def parse_nick_template(string, template_regex, outtemplate): """ Parse a text using a template and map it to another template Args: string (str): The input string to processj template_regex (regex): A template regex created with initialize_nick_template. outtemplate (str): The template to which to map the matches produced by the template_regex. This should have $1, $2, etc to match the regex. """ match = template_regex.match(string) if match: return outtemplate.format(**match.groupdict()) return string