""" Inlinefunc This is a simple inline text language for use to custom-format text in Evennia. It is applied BEFORE ANSI/MUX parsing is applied. To activate Inlinefunc, settings.INLINEFUNC_ENABLED must be set. The format is straightforward: {funcname([arg1,arg2,...]) text {/funcname Example: "This is {pad(50,c,-) a center-padded text{/pad of width 50." -> "This is -------------- a center-padded text--------------- of width 50." This can be inserted in any text, operated on by the parse_inlinefunc function. funcname() (no space is allowed between the name and the argument tuple) is picked from a selection of valid functions from settings.INLINEFUNC_MODULES. Commands can be nested, and will applied inside-out. For correct parsing their end-tags must match the starting tags in reverse order. Example: "The time is {pad(30){time(){/time{/padright now." -> "The time is Oct 25, 11:09 right now." An inline function should have the following call signature: def funcname(text, *args) where the text is always the part between {funcname(args) and {/funcname and the *args are taken from the appropriate part of the call. It is important that the inline function properly clean the incoming args, checking their type and replacing them with sane defaults if needed. If impossible to resolve, the unmodified text should be returned. The inlinefunc should never cause a traceback. """ import re from django.conf import settings from src.utils import utils # inline functions def pad(text, *args, **kwargs): "Pad to width. pad(text, width=78, align='c', fillchar=' ')" width = 78 align = 'c' fillchar = ' ' for iarg, arg in enumerate(args): if iarg == 0: width = int(arg) if arg.isdigit() else width elif iarg == 1: align = arg if arg in ('c', 'l', 'r') else align elif iarg == 2: fillchar = arg[0] else: break return utils.pad(text, width=width, align=align, fillchar=fillchar) def crop(text, *args, **kwargs): "Crop to width. crop(text, width=78, suffix='[...]')" width = 78 suffix = "[...]" for iarg, arg in enumerate(args): if iarg == 0: width = int(arg) if arg.isdigit() else width elif iarg == 1: suffix = arg else: break return utils.crop(text, width=width, suffix=suffix) def wrap(text, *args, **kwargs): "Wrap/Fill text to width. fill(text, width=78, indent=0)" width = 78 indent = 0 for iarg, arg in enumerate(args): if iarg == 0: width = int(arg) if arg.isdigit() else width elif iarg == 1: indent = int(arg) if arg.isdigit() else indent return utils.wrap(text, width=width, indent=indent) def time(text, *args, **kwargs): "Inserts current time" import time strformat = "%h %d, %H:%M" if args and args[0]: strformat = str(args[0]) return time.strftime(strformat) def you(text, *args, **kwargs): "Inserts your name" name = "You" sess = kwargs.get("session") if sess and sess.puppet: name = sess.puppet.key return name # load functions from module (including this one, if using default settings) _INLINE_FUNCS = {} for module in utils.make_iter(settings.INLINEFUNC_MODULES): _INLINE_FUNCS.update(utils.all_from_module(module)) _INLINE_FUNCS.pop("inline_func_parse", None) # dynamically build regexes for found functions _RE_FUNCFULL = r"\{%s\((.*?)\)(.*?){/%s" _RE_FUNCFULL_SINGLE = r"\{%s\((.*?)\)" _RE_FUNCSTART = r"\{((?:%s))" _RE_FUNCEND = r"\{/((?:%s))" _RE_FUNCSPLIT = r"(\{/*(?:%s)(?:\(.*?\))*)" _RE_FUNCCLEAN = r"\{%s\(.*?\)|\{/%s" _INLINE_FUNCS = dict((key, (func, re.compile(_RE_FUNCFULL % (key, key), re.DOTALL & re.MULTILINE), re.compile(_RE_FUNCFULL_SINGLE % key, re.DOTALL & re.MULTILINE))) for key, func in _INLINE_FUNCS.items() if callable(func)) _FUNCSPLIT_REGEX = re.compile(_RE_FUNCSPLIT % r"|".join([key for key in _INLINE_FUNCS]), re.DOTALL & re.MULTILINE) _FUNCSTART_REGEX = re.compile(_RE_FUNCSTART % r"|".join([key for key in _INLINE_FUNCS]), re.DOTALL & re.MULTILINE) _FUNCEND_REGEX = re.compile(_RE_FUNCEND % r"|".join([key for key in _INLINE_FUNCS]), re.DOTALL & re.MULTILINE) _FUNCCLEAN_REGEX = re.compile("|".join([_RE_FUNCCLEAN % (key, key) for key in _INLINE_FUNCS]), re.DOTALL & re.MULTILINE) # inline parser functions def _execute_inline_function(funcname, text, session): """ Get the enclosed text between {funcname(...) and {/funcname and execute the inline function to replace the whole block with the result. Note that this lookup is "dumb" - we just grab the first end tag we find. So to work correctly this function must be called "inside out" on a nested function tree, so each call only works on a "flat" tag. """ def subfunc(match): "replace the entire block with the result of the function call" args = [part.strip() for part in match.group(1).split(",")] intext = match.group(2) kwargs = {"session":session} return _INLINE_FUNCS[funcname][0](intext, *args, **kwargs) return _INLINE_FUNCS[funcname][1].sub(subfunc, text) def _execute_inline_single_function(funcname, text, session): """ Get the arguments of a single function call (no matching end tag) and execute it with an empty text input. """ def subfunc(match): "replace the single call with the result of the function call" args = [part.strip() for part in match.group(1).split(",")] kwargs = {"session":session} return _INLINE_FUNCS[funcname][0]("", *args, **kwargs) return _INLINE_FUNCS[funcname][2].sub(subfunc, text) def parse_inlinefunc(text, strip=False, session=None): """ Parse inline function-replacement. strip - remove all supported inlinefuncs from text session - session calling for the parsing """ if strip: # strip all functions return _FUNCCLEAN_REGEX.sub("", text) stack = [] for part in _FUNCSPLIT_REGEX.split(text): endtag = _FUNCEND_REGEX.match(part) if endtag: # an end tag endname = endtag.group(1) while stack: new_part = stack.pop() part = new_part + part # add backwards -> fowards starttag = _FUNCSTART_REGEX.match(new_part) if starttag: startname = starttag.group(1) if startname == endname: part = _execute_inline_function(startname, part, session) break stack.append(part) # handle single functions without matching end tags; these are treated # as being called with an empty string as text argument. outstack = [] for part in _FUNCSPLIT_REGEX.split("".join(stack)): starttag = _FUNCSTART_REGEX.match(part) if starttag: startname = starttag.group(1) part = _execute_inline_single_function(startname, part, session) outstack.append(part) return "".join(outstack) def _test(): # this should all be handled s = "This is a text with a{pad(78,c,-)text {pad(5)of{/pad {pad(30)nice{/pad size{/pad inside {pad(4,l)it{/pad." s2 = "This is a text with a----------------text of nice size---------------- inside it ." t = parse_inlinefunc(s) assert(t == s2) return t