import threading import re,sys from itertools import izip, count, chain from decorator import decorator from contextlib import contextmanager from redbeans.formats import BaseFormat, Format from redbeans.creole import tokenize from redbeans.parser import Parser, ParserException from redbeans.tokens import * from . import config, NoResultFound from . import custom, db, dependencies from .model import (Element, Propval, Prop) from .flavors import FLAVORS, FORMATS from .benchmark import benchmarking class WikiException(Exception): pass class NoContentException(WikiException): pass class moverride(object): def __init__(self, kw): self.kw = kw def __enter__(self): self.old_moverrides = dict(eval_state.moverrides) eval_state.moverrides.update(self.kw) def __exit__(self, exc_type, exc_val, exc_tb): eval_state.moverrides = self.old_moverrides class eoverride(object): def __init__(self, kw): self.kw = kw def __enter__(self): self.old_eoverrides = dict(eval_state.eoverrides) eval_state.eoverrides.update(self.kw) def __exit__(self, exc_type, exc_val, exc_tb): eval_state.eoverrides = self.old_eoverrides class poverride(object): def __init__(self, kw): self.kw = kw def __enter__(self): self.old_poverrides = dict(eval_state.poverrides) eval_state.poverrides.update(self.kw) def __exit__(self, exc_type, exc_val, exc_tb): eval_state.poverrides = self.old_poverrides def recursive_get(elm, prop_name, *args, **restrictions): if len(args) > 0: filter, = args assert filter in ('prune', 'invert', 'exclude') else: filter = 'prune' ret = to_python(elm, prop_name) new = [([elm], list(ret), False)] if filter != 'prune': # We only add things once they pass filters ret = [] if filter != 'invert': ret = [elm] + ret while len(new) > 0: newnew = [] # "pruned" indicates that this element would have been pruned for path, l, pruned in new: for n in l: if n in path: raise WikiException( "Infinite %s loop in recursive_get: %s!" % (prop_name, '->'.join(path+[n]))) passed_restr = all((to_python(n, r) if r in n else None) == restrictions[r] for r in (unicode(sr, 'utf-8') for sr in restrictions)) if passed_restr or filter == 'invert': if filter == 'exclude': # We only add it after it meets the restrictions ret.append(n) if filter == 'invert' and (not passed_restr or pruned): ret.append(n) if prop_name in n: adding = to_python(n, prop_name) if filter == 'prune': ret += adding newnew.append((path+[n], adding, not passed_restr or pruned)) new = newnew return ret def has_ancestor(element, ancestor): return ancestor.isAncestorOf(element) def safe_getattr(object, name, *args): assert '__' not in name return getattr(object, name, *args) def baz_eval_wrap(arg, toPython): if isinstance(arg, Element): return attrelm(arg, toPython) elif isinstance(arg, list): return [baz_eval_wrap(a, toPython) for a in arg] elif isinstance(arg, dict): return dict((baz_eval_wrap(k, toPython), baz_eval_wrap(arg[k], toPython)) for k in arg) else: return arg def baz_eval_wrap_func(func, toPython): @decorator def helper(func, *args, **kw): args = [baz_eval_unwrap(a) for a in args] kw = dict((k,baz_eval_unwrap(v)) for k,v in kw.items()) return baz_eval_wrap(func(*args, **kw), toPython) return helper(func) def baz_eval_unwrap(ret): if isinstance(ret, attrelm): return ret.element__ elif isinstance(ret, list): return [baz_eval_unwrap(r) for r in ret] elif isinstance(ret, dict): return dict((baz_eval_unwrap(k), baz_eval_unwrap(ret[k])) for k in ret) elif isinstance(ret, str): return unicode(ret, 'utf-8') else: return ret def baz_eval(expr, toPython=True): if len(expr.strip()) == 0: return u'' elemlocs = _elemdict(toPython=toPython) if '__' in expr: # Protect against hackery raise WikiException("The string '__' is not allowed to appear in expressions!") else: try: ret = eval(expr,{"__builtins__": {}, "defined": (lambda s: s in elemlocs), "get": (lambda s,d=None: elemlocs.get(s,d)), "repr": repr, "str": str, "True": True, "False": False, "None": None, "dict": baz_eval_wrap_func( lambda *args,**kw: dict(*args, **kw), toPython), "any": any, "all": all, "hasattr": hasattr, "getattr": safe_getattr, "list_props": baz_eval_wrap_func(lambda e: list(e), toPython), "recursive_get": baz_eval_wrap_func(recursive_get, toPython), "has_ancestor": baz_eval_wrap_func(has_ancestor, toPython), "get_references": baz_eval_wrap_func(get_references, toPython), "get_mentions": baz_eval_wrap_func(get_mentions, toPython), "has_errors": baz_eval_wrap_func(has_errors, toPython), }, elemlocs) return baz_eval_unwrap(ret) except AssertionError: raise except Exception,e: #raise #!!! if (isinstance(e, NameError) and re.search("^global name '(\w+)' is not defined$", e.args[0])): extra = " (If you're using a generator expression, try a list comprehension.)" else: extra = '' raise WikiException(repr(e)+extra) def full_eval(expr): """Returns a generator for tokens representing the given baz_eval expr.""" ret = baz_eval(expr, toPython=False) if isinstance(ret, basestring): return [Text(ret)] else: return ret def unicode_eval(expr): ret = baz_eval(expr) ret = unicode(ret) return ret def render_allow_override(element, prop_name, default=None, eoverrides={}, moverrides={}): if prop_name in eval_state.moverrides: return eval_state.moverrides[prop_name] else: prop_name = get_propname(prop_name) eval_state.dependencies.addDep(element[prop_name]) if prop_name in element: return render_propval(element, prop_name, eoverrides=eoverrides, moverrides=moverrides) else: return default def render_propval(element, prop_name, eoverrides={}, moverrides={}): """Render the propval as plain text, tracking dependencies.""" # TODO(xavid): theoretically this should actively be position-independent parser = Parser(FORMATS['txt'], macro_func, link_func) old_eoverrides = dict(eval_state.eoverrides) old_moverrides = dict(eval_state.moverrides) old_parser = eval_state.mainparser eval_state.eoverrides.update(eoverrides) eval_state.moverrides.update(moverrides) eval_state.mainparser = parser ret = parser.render(recurse(element, prop_name)) eval_state.eoverrides = old_eoverrides eval_state.moverrides = old_moverrides eval_state.mainparser = old_parser return ret QUOTES = frozenset("'\"") GROUPING = {'(': ')', '[': ']'} def parse_macro_args(argstr): """Parse an argument string into a map of names to values. name will be an integer for positional parameters and a string for keyword parameters.""" escaping = False build = "" name = 1 nextname = 1 nomore = False nest = [] quote = None retmap = {} for i in xrange(len(argstr)): char = argstr[i] if len(nest) == 0 and quote is None and char.isspace(): if build: retmap[name] = build if name == nextname: nextname += 1 build = "" name = nextname nomore = False else: if nomore: raise WikiException("Text after close paren in %s arg!" % name) build += char if char == '\\' and not escaping: escaping = True else: if (quote is None and char in QUOTES): quote = char elif char == quote and not escaping: quote = None elif len(nest) > 0 and nest[-1] == char: nest.pop() elif char in GROUPING: nest.append(GROUPING[char]) elif (char == '=' and len(argstr) > i+1 and argstr[i+1] != '=' and argstr[i-1] not in ('=','!') and len(nest) == 0): if isinstance(name,int) and len(build) > 1: name = build[:-1] build = "" escaping = False if build: retmap[name] = build return retmap def _list_descendants(e): assert e.ename is not None eval_state.dependencies.addChildrenDep(e) # -! make more efficient, possibly move to model everyone = e.getDescendants() for dude in everyone: eval_state.dependencies.addChildrenDep(dude) return everyone def safesplit_tags(toks, splitters): retlist = [([], None)] macro_level = 0 for t in toks: if (macro_level == 0 and t.op == ENTITY and t.style == MACRO and t.arg[0] in splitters): retlist.append(([], t.arg)) else: if t.style == MACRO: if t.op == START: macro_level += 1 elif t.op == END: macro_level -= 1 retlist[-1][0].append(t) return retlist def safesplit(toks, splitters): return [p[0] for p in safesplit_tags(toks, splitters)] def get_overridden_element(ename): if hasattr(eval_state, 'dependencies'): if ename in eval_state.eoverrides: return eval_state.eoverrides[ename] elif ename == custom.ME_ELEMENT: return eval_state.me elif ename == custom.PARENT_ELEMENT: eval_state.dependencies.addParentDep(eval_state.me) return eval_state.me.parent return None def get_element(ename): e = get_overridden_element(ename) if e is not None: return e else: try: return Element.get(ename) except NoResultFound: if hasattr(eval_state, 'dependencies'): eval_state.dependencies.addExistsDep(ename) raise # TODO(xavid): If current's always defined, this could be simplified. def get_current_element(): """Like get_element(u'leaf'), but allows explicit overriding by setting current.""" if u'current' in eval_state.eoverrides: return eval_state.eoverrides[u'current'] else: return get_element(custom.LEAF_ELEMENT) def get_propname(pname): if pname in eval_state.poverrides: return eval_state.poverrides[pname] else: return pname def to_python(ename, pname): ls = list(recurse(ename, pname, to_python=True)) assert len(ls) == 1 return ls[0] def recurse(ename, pname, to_python=False, content=None, argstr=u''): if isinstance(ename,Element): e = ename ename = e.ename else: e = get_element(ename) pname = get_propname(pname) assert pname is not None assert e is not None if pname == custom.ELEMENT_NAME_PROP: # TODO(xavid): Figure out what sort of dependency this should introduce if to_python: yield e.ename else: yield Text(e.ename) else: if hasattr(eval_state, 'dependencies'): eval_state.dependencies.addPropvalDep(e, pname) if pname == custom.NAME_PROP: if ename not in eval_state.mentions: eval_state.mentions.append(ename) oldme = eval_state.me oldeover = dict(eval_state.eoverrides) oldmover = dict(eval_state.moverrides) oldpover = dict(eval_state.poverrides) try: eval_state.me = e eval_state.poverrides['this'] = pname # Maybe update leaf if ename != custom.PARENT_ELEMENT: eval_state.eoverrides[custom.LEAF_ELEMENT] = e # Add macro params if content is not None: def render_content(argstr, cont): newleaf = eval_state.eoverrides[custom.LEAF_ELEMENT] eval_state.eoverrides[custom.LEAF_ELEMENT] = oldeover[ custom.LEAF_ELEMENT] for t in content: yield t eval_state.eoverrides[custom.LEAF_ELEMENT] = newleaf eval_state.moverrides[u'content'] = render_content elif ename != custom.PARENT_ELEMENT: eval_state.moverrides[u'content'] = None if ename != custom.PARENT_ELEMENT or argstr: eval_state.moverrides[u'args'] = argstr if argstr: for var,arg in parse_macro_args(argstr).items(): eval_state.moverrides[ u'arg'+unicode(var)] = (lambda a,c: [Text(baz_eval(arg))]) try: pv = e[pname] except KeyError: raise WikiException( "Element ##%s## (called as ##%s## by ##%s##) has no property ##%s##!" % (e.ename if e is not None else None, ename, oldme.ename if oldme is not None else None, pname)) value = unicode(pv.value, 'utf-8') if to_python: yield FLAVORS[pv.prop.flavor].toPython( value, eval_state.mainparser, propval=pv) else: for t in FLAVORS[pv.prop.flavor].tokenize( value, eval_state.mainparser, propval=pv): yield t finally: eval_state.me = oldme eval_state.poverrides = oldpover eval_state.eoverrides = oldeover eval_state.moverrides = oldmover class nice_gen(object): def __init__(self, gen): self.__gen = gen def __iter__(self): return self def next(self): return self.__gen.next() def __add__(self, other): return chain(self, other) class attrelm(object): def __init__(self, element, toPython): self.element__ = element self.__toPython = toPython def __getattr__(self, a): if a is None: raise KeyError() if not isinstance(a, unicode): a = unicode(a, 'utf-8') if self.__toPython: ret = to_python(self.element__, a) # wrap elements we get back if isinstance(ret,Element): return attrelm(ret, self.__toPython) elif isinstance(ret,list): return [attrelm(r, self.__toPython) if isinstance(r,Element) else r for r in ret] else: return ret else: return nice_gen(recurse(self.element__, a)) def __str__(self): return '<@Element %s>' % self.element__.ename def __eq__(self, other): return isinstance(other,attrelm) and self.element__ == other.element__ def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(self.element__) class _elemdict(object): def __init__(self, toPython): self.__toPython = toPython self.__locals = {} def __contains__(self, key): if key in self.__locals: return True if not isinstance(key, unicode): key = unicode(key,'utf-8') if key is None: return False if key in eval_state.eoverrides or key in eval_state.moverrides: return True try: e = Element.get(key) return True except NoResultFound: if hasattr(eval_state, 'dependencies'): eval_state.dependencies.addExistsDep(key) return False def __getitem__(self,key): if key in self.__locals: return self.__locals[key] key = unicode(key, 'utf-8') if key not in self: raise KeyError("No element '%s' exists!"%key) if key in eval_state.moverrides: return eval_state.moverrides[key] else: return attrelm(get_element(key), self.__toPython) def __setitem__(self, key, value): self.__locals[key] = value def __delitem__(self, key): del self.__locals[key] def get(self,key,default=None): try: return self[key] except KeyError: return default # Macro-creating helpers def environment(name, texname=None, tagname=None, texmode='environment', doc = ''): if texname is None: texname = name def env(argstr, content): args = parse_macro_args(argstr) subclasses = [] for key,val in args.items(): if key == 'subclass': subclasses.append(baz_eval(val)) else: raise WikiException("Unknown arg '%s' in %s environment!" % (key,name)) if eval_state.format == FORMATS['html']: if tagname is not None: yield Literal('<%s>' % tagname) for t in content: yield t yield Literal('' % tagname) else: yield Literal('
' % ( name, ''.join(' '+c for c in subclasses))) for t in content: yield t yield Literal('
') elif eval_state.format == FORMATS['tex']: if texmode == 'environment': yield Literal('\\begin{%s}\n' % texname) for t in content: yield t yield Literal('\\end{%s}' % texname) else: yield Literal('\\%s{' % texname) for t in content: yield t yield Literal('}') else: for t in content: yield t env.__doc__ = doc return env def atom(doc='', default="", **kw): def ato(argstr): for k in kw: if eval_state.format == FORMATS[k]: yield Literal(kw[k]) return else: yield Text(default) ato.__doc__ = doc return ato def leafprop(pname): def lp(argstr): return recurse(custom.LEAF_ELEMENT, pname) return lp def divided(name,divider,*inner): """A macro that turns something like <>Foo<>Bar<> into
Foo
Bar
""" def div(argstr, content): parts = safesplit(content, (divider,)) args = parse_macro_args(argstr) subclasses = [] for key,val in args.items(): if key == 'subclass': subclasses.append(baz_eval(val)) else: raise WikiException("Unknown arg '%s' in %s divided!" % (key,name)) if len(parts) > len(inner): yield _error("Too many %ss in a %s!"%(divider,name)) else: if eval_state.format == FORMATS['html']: yield Literal('
' % (name,''.join( ' '+c for c in subclasses))) for i in xrange(len(parts)): yield Literal('
' % (inner[i])) for t in parts[i]: yield t yield Literal('
') yield Literal('
') elif eval_state.format == FORMATS['tex']: yield Literal('\\begin{%s}\n' % (name)) for i in xrange(len(parts)): yield Literal('\\%s{' % (inner[i])) for t in parts[i]: yield t yield Literal('}') yield Literal('\n\\end{%s}' % (name)) else: for p in parts: for t in p: yield t return div def inline_format(style, doc=''): def fmt(argstr, content): yield Start(style) for t in content: yield t yield End(style) fmt.__doc__ = doc fmt.hidden = 'latexalike' return fmt def illegal(name,*container): def ill(argstr, content=None): yield _error('"%s" may only be used in a %s!' % (name, ' or '.join('"%s"' % c for c in container))) ill.hidden = 'illegal' return ill # Helpers used by our macros class noescape(object): """Wrap a format such that it's escape method is a nop.""" def __init__(self,format): self.__format = format def escape(self,text): return text def __getattr__(self,attr): return getattr(self.__format,attr) class maxwe(object): def __cmp__(self,other): return 1 MAXWE = maxwe() class Macros(object): #@staticmethod #def nowiki(argstr, content): # """Prevent wiki markup in the content from being evaluated. # # Unlike ##~{{{}}}##, the font style is unchanged.""" # return parser.escape(content) @staticmethod def par(argstr): """Start a new paragraph.""" yield Entity(ENV_BREAK) @staticmethod def block(argstr, content): """Treat the content as its own block, separate from surroundings.""" for t in content: yield t @staticmethod def format(argstr, content): """Include the markup, unsanitized, in the given format, with macros evaluated. Use like {{{<>Raw HTML<>}}}.""" args = parse_macro_args(argstr) fmt = baz_eval(args[1]) if eval_state.format == FORMATS[fmt]: for t in content: if t.op == TEXT: yield Literal(t.arg) else: yield t @staticmethod def let(argstr, content): """Define local names for elements or other data. Use like {{{<>...<>}}}.""" args = parse_macro_args(argstr) oldmover = dict(eval_state.moverrides) oldeover = dict(eval_state.eoverrides) for k,v in args.items(): # TODO: come up with better evaulation logic with the lazy # evaluate magic/baz_eval v = baz_eval(v) if isinstance(v, Element): eval_state.eoverrides[k] = v else: eval_state.moverrides[k] = v for t in content: yield t eval_state.eoverrides = oldeover eval_state.moverrides = oldmover @staticmethod def plet(argstr, content): """Define local prop names. Use like {{{<><><>}}}.""" args = parse_macro_args(argstr) oldpover = dict(eval_state.poverrides) for k,v in args.items(): # TODO: come up with better evaulation logic with the lazy # evaluate magic/baz_eval v = baz_eval(v) eval_state.poverrides[k] = v for t in content: yield t eval_state.poverrides = oldpover @staticmethod def content(argstr): """When evaluating a reference, the content of the macro. For self-closing macros, content evaluates to None. When not in reference context, raises an exception.""" raise NoContentException() @staticmethod def if_(argstr, content): """Evaluate the content only if a condition is true. Can contain {{{<>}}} and/or {{{<>}}}. For example: {{{ <> he <> she <> they <> }}}""" parttags = safesplit_tags(content, ('else', 'elif')) cond = argstr while True: val = baz_eval(cond) if val: break else: parttags = parttags[1:] if len(parttags) <= 0: return name, argstr = parttags[0][1] if name == 'else': break else: cond = argstr for t in parttags[0][0]: yield t # You don't need to put staticmethod() when you add things to this at runtime... else_ = staticmethod(illegal('else','if')) elif_ = staticmethod(illegal('elif','if')) @staticmethod def link(argstr, content=None): """Builds a link dynamically, evaluating and joining arguments. Use like {{{<>Packet<>}}} or {{{<>}}}.""" print >>sys.stderr, "LINK", repr(argstr), repr(content) args = parse_macro_args(argstr) dest = ''.join(baz_eval(args[a]) for a in sorted(args)) if content is not None: yield Start(LINK, dest) for t in content: yield t yield End(LINK, dest) else: yield Entity(LINK, dest) @staticmethod def foreach(argstr, content=None): """Evaluate the content once for each member of some group. The basic form is {{{<>...<>}}}. "group" can either be an element, in which case the content is evaluated with var set to each of that element's descendents in turn, or a particular element's prop (generally of flavor references), in which case the content is evaluated with var set to each of the elements referenced in that prop value, in turn. Nested loops can be created in a single macro by adding {{{foreach group2 var2}}} after the first var name. Parameters after the var name can be conditions filtering the specified group; these should generally be in parentheses. foreach also takes a large number of optional keyword parameters, which take a value after an =: orderBy: a prop the group members have they should be ordered by groupBy: the group members should be grouped by the specified prop. If the value contains a comma, the markup after the comma is used as a group header. ifNone: markup that should be evaluated if the group is empty. asTree: put the evaluations of the content in a nested list structure reflecting the inheritance tree of the group. Doesn't support nested loops. recursive: if the group was based on a references-flavor prop and group members have that prop, evaluate the content for the elements referenced in their values for the prop, and so on. childrenOnly: use the specified element's children only, not all of its descendents. """ args = parse_macro_args(argstr) if 1 not in args or 2 not in args: raise WikiException("{{{foreach}}} takes at least two parameters! See [[edit:Macros]]!") if content is None: raise WikiException("{{{foreach}}} must have content! See [[edit:Macros]]!") # We need to be able to iterate over this multiple times content = list(content) typ = args[1] del args[1] nam = args[2] del args[2] subtyps = [] subnams = [] next = 3 while next in args and args[next] == 'foreach': del args[next] subtyps.append(args[next + 1]) del args[next + 1] subnams.append(args[next + 2]) del args[next + 2] next += 3 restr = set() orderBy = [] groupBy = None asTree = False recursive = False childrenOnly = False ifNone = None for key,arg in args.items(): if not isinstance(key,int): if key.lower() == 'orderby': orderBy +=arg.strip().split() elif key.lower() == 'groupby': if ',' in arg: var,groupMarkup = arg.split(',',1) else: var = arg groupMarkup = '' var = var.strip() if ' ' in var: var = var.split() groupBy = var[0] groupName = var[1] else: groupBy = groupName = var elif key.lower() == 'ifnone': ifNone = baz_eval(arg) elif key.lower() == 'astree': asTree = baz_eval(arg) elif key.lower() == 'recursive': recursive = baz_eval(arg) elif key.lower() == 'childrenonly': childrenOnly = baz_eval(arg) else: yield _error("Unknown foreach parameter '%s'!" % key) else: restr.add(arg) # TODO(xavid): create separate "for each ancestor var" and # "for var in list" syntaxes. list_or_elm = baz_eval(typ) if not asTree: def get_list(list_or_elm): if isinstance(list_or_elm, Element): # Now do a traversal: if childrenOnly: eval_state.dependencies.addChildrenDep(list_or_elm) return list_or_elm.getChildren() else: return _list_descendants(list_or_elm) else: assert isinstance(list_or_elm, list), repr(list_or_elm) return list_or_elm def do_overrides(subst): for substnam in subst: if hasattr(subst[substnam], 'ename'): eval_state.eoverrides[substnam] = subst[substnam] else: eval_state.moverrides[substnam] = subst[substnam] with benchmarking('foreach get_list %s' % typ): lst = get_list(list_or_elm) nambits = nam.split(',') if len(nambits) == 1: subst_list = [{nam: e} for e in lst] else: if not all(len(nambits) == len(e) for e in lst): raise WikiException("Length error unpacking for %s!" % nam) subst_list = [dict(zip(nambits, e)) for e in lst] old_eover = dict(eval_state.eoverrides) old_mover = dict(eval_state.moverrides) # Compute any nested loop levels for styp, snam in zip(subtyps, subnams): new_subst_list = [] for subst in subst_list: do_overrides(subst) new_lst = get_list(baz_eval(styp)) for new_e in new_lst: new_subst = dict(subst) new_subst[snam] = new_e new_subst_list.append(new_subst) subst_list = new_subst_list # Handle filters if len(restr) > 0: with benchmarking('foreach filtering %s' % restr): filtlst = [] for subst in subst_list: do_overrides(subst) if all(baz_eval(r) for r in restr): filtlst.append(subst) subst_list = filtlst if len(subst_list) == 0 and ifNone is not None: for t in tokenize(ifNone): yield t # Sort by the nam element always for f in reversed(orderBy): subst_list.sort(key=lambda subst: subst[nam][f].render() if f in subst[nam] else MAXWE) def stickon(subst): do_overrides(subst) for t in content: yield t if groupBy: groups = {} for subst in subst_list: e = subst[nam] val = e[groupBy] if groupBy in e else MAXWE if val in groups: groups[val].append(subst) else: groups[val] = [subst] for g in sorted(groups.keys()): eval_state.moverrides[groupName] = g if g != MAXWE else "???" if groupMarkup: for t in tokenize(groupMarkup): yield t for subst in groups[g]: for t in stickon(subst): yield t else: for subst in subst_list: with benchmarking('foreach subst=%s' % subst): for t in stickon(subst): yield t eval_state.eoverrides = old_eover eval_state.moverrides = old_mover else: # asTree def dig(e,depth): assert e.ename is not None eval_state.dependencies.addChildrenDep(e) old_overrides = dict(eval_state.eoverrides) eval_state.eoverrides[nam] = e yield Start(UNORDERED_ITEM, depth) for t in content: yield t yield End(UNORDERED_ITEM, depth) eval_state.eoverrides = old_overrides lst = e.getChildren() if len(lst) > 0: for f in reversed(orderBy): lst.sort(key=lambda e:e[f] if f in e else MAXWE) for e in lst: for y in dig(e,depth+1): yield y assert isinstance(list_or_elm, Element) for t in dig(list_or_elm, 1): yield t @staticmethod def comment(argstr, content=None): """Suppress any content. Useful for leaving comments only relevant to people editing a prop's value.""" return () @staticmethod def withheaders(argstr, content): # More descriptive name """Renders the (up to) four arguments as headers around the content.""" args = parse_macro_args(argstr) headfoot = ["","","",""] for key,val in args.items(): if key in (1,2,3,4): headfoot[key-1] = baz_eval(val) else: raise WikiException("Unknown arg '%s' in card!" % (key)) cont = list(content) if cont: if eval_state.format == FORMATS['html']: yield Literal('''
%s
%s
%s
''' % tuple(headfoot)) for t in cont: yield t yield Literal('''
''') elif eval_state.format == FORMATS['tex']: yield Literal(r'''\cleardoublepage \resetnumbering \headers{%s}{%s}{%s}{%s} ''' % tuple(headfoot)) for t in cont: yield t yield Literal(r''' \cleardoublepage ''') center = staticmethod(environment('center',tagname='center', doc="""Center the content.""")) right = staticmethod(environment('right', texname='flushright', doc="""Right-justify the content.""")) left = staticmethod(environment('left', texname='flushleft', doc="""Left-justify the content.""")) dbox = staticmethod(environment( 'dbox', texmode='command', doc="""Put a double box around the content""")) clearpage = staticmethod(atom(html=u"
",tex=r'\clearpage', doc="Start a new logical page.")) cleardoublepage = staticmethod(atom(html=u"
",tex=r'\cleardoublepage', doc="Start a new piece of paper.")) gap = staticmethod(atom(html=u'
', tex=r'\break\vfill', doc="Insert a small gap.")) # LaTeX-alike macros textbf = staticmethod(inline_format(BOLD, doc="Bold.")) textit = staticmethod(inline_format(ITALIC, doc="Italic.")) emph = staticmethod(inline_format(ITALIC, doc="Italic.")) texttt = staticmethod(inline_format(MONOSPACE, doc="Monospace.")) uline = staticmethod(inline_format(UNDERLINE, doc="Underline.")) sout = staticmethod(inline_format(STRIKE, doc="Strike.")) @staticmethod def eval_(argstr): """Evaluate the given expression and output its value. E.g., {{{<>}}} will output 8.""" result = unicode_eval(argstr) assert isinstance(result, unicode), result yield Text(result) @staticmethod def cache(argstr): """Evaluate the specified prop of an element cachably. This means the evaluation will ignore any context, but that it can take advantage of caching. Limited context can be provided in the form of {{{let}}}-style keyword parameters.""" args = parse_macro_args(argstr) if 1 not in args or 2 in args: raise WikiException( "##cache## takes only one non-keyword parameter!") ename, prop_name = args[1].split('.') element = get_element(ename) #print >>sys.stderr, "CACHING %s.%s" % (element.ename, prop_name) del args[1] let = {} for k,v in args.items(): let[k] = baz_eval(v) import conversion flat = conversion.flatten_filters({'let': let}) ce = conversion.get_from_cache(element.ename, prop_name, format=eval_state.extension+flat) if ce is None: result, deps, metadata = evaluate(element[prop_name], eval_state.extension, let=let, reentrant=True) if dependencies.DISCORDIA not in deps: conversion.cache_propval(element.ename, prop_name, eval_state.extension+flat, result, deps, metadata) else: result = ce['value'] deps = ce['dependencies'] metadata = ce['metadata'] for d in deps: if d == dependencies.DISCORDIA: eval_state.dependencies.makeUncacheable() break else: assert len(d) == 4, d eval_state.dependencies.addRawDep(*d) for i in metadata['images']: eval_state.images.add(i) yield Literal(result) # TODO(xavid): combine with other foreach syntax @staticmethod def foreachpropin(argstr, content): """Evaluate the content once for each prop in the given element. Use like {{{<> * <><>}}}. Can be given an extra ##flavor=whatever## arg to limit it to props of a given flavor.""" args = parse_macro_args(argstr) if 1 not in args or 2 not in args: raise WikiException( "##foreachpropin## takes two parameters!") ename = args[1] var = args[2] if 'flavor' in args: flavor = baz_eval(args['flavor']) else: flavor = None # We need to loop through content more than once content = list(content) element = get_element(ename) for pname in element: if pname != get_propname(u'this'): if flavor is not None: if Prop.get(pname).flavor != flavor: continue with poverride({var: pname}): for t in content: yield t @staticmethod def sourceof(argstr): """Render the source of the given prop of an element. Use like {{{<>}}}.""" args = parse_macro_args(argstr) if 1 not in args or len(args) != 1: raise WikiException( "##sourceof## takes only one parameter!") ename, prop_name = args[1].split('.') element = get_element(ename) prop_name = get_propname(prop_name) if prop_name not in element: raise WikiException( "##%s## has no prop ##%s## to get the source of!" % (element.ename, prop_name)) yield Start(CODEBLOCK) yield Text(unicode(element[prop_name].value, 'utf-8')) yield End(CODEBLOCK) @staticmethod def propname(argstr): """Render the propname of the given prop; useful in {{{<>}}}.""" args = parse_macro_args(argstr) if 1 not in args or len(args) != 1: raise WikiException( "##propname## takes only one parameter!") prop_name = get_propname(args[1]) yield Text(prop_name) @staticmethod def foreachmacro(argstr, content): """Evaluate content once for each built-in macro. Sets the argument to the name of the built-in macro. Optionally, a second argument selects a class of macro hidden by default.""" args = parse_macro_args(argstr) if 1 not in args or len(args) not in (1,2): raise WikiException( "##foreachmacro## takes one or two parameters!") var = args[1] expected_hidden = args[2] if 2 in args else None for m in dir(macros): if not m.startswith('_') and getattr(getattr(macros, m), 'hidden', None) == expected_hidden: if m.endswith('_'): m = m[:-1] with moverride({var: m}): for t in content: yield t @staticmethod def macrodoc(argstr): """Given a built-in macro, render its help text.""" args = parse_macro_args(argstr) if 1 not in args or len(args) != 1: raise WikiException( "##macrodoc## takes only one parameter!") name = args[1] if name in eval_state.moverrides: name = eval_state.moverrides[name] if hasattr(macros, name+'_'): name = name+'_' elif not hasattr(macros, name): raise WikiException("No such macro ##%s##!" % name) doc = getattr(getattr(macros, name), '__doc__', None) if doc is not None: for t in tokenize(doc): yield t @staticmethod def macro(argstr, content=None): """Evaluate the built-in macro named by the given expression.""" argstr = argstr.strip() if ' ' in argstr: name, argstr = argstr.split(' ', 1) else: name = argstr argstr = "" if name in eval_state.moverrides: name = eval_state.moverrides[name] if content is not None: return getattr(macros, name)(argstr, content) else: return getattr(macros, name)(argstr) @staticmethod def default(argstr, content=""): """Special indicator macro. Propvals that //start// with this macro will be considered 'abstract', and the interface will encourage users to override them in children. Has no effect on evaluation.""" for t in content: yield t @staticmethod def year(argstr): """Evauates to the current year.""" from datetime import datetime yield Text(unicode(datetime.now().year)) def link_func(href, sty=LINK): if (':' not in href or href[0].isupper()) and '/' not in href: # Normal context-based interpretation if (eval_state.topthis is not None and Prop.get(eval_state.topthis).visible): href = 'get:'+href else: href = 'edit:'+href implicit = True else: implicit = False if ':' in href: pref,rest = href.split(':',1) rest = rest.strip() if pref in ('get','edit'): ext = None if '.' in rest: ename,pname = rest.split('.',1) if '.' in pname: pname, ext = pname.split('.', 1) ext = '.' + ext else: ename = rest pname = None if ename in eval_state.eoverrides: ename = eval_state.eoverrides[ename].ename if sty == IMAGE: eval_state.images.add('%s.%s' % (ename, pname)) if pref == 'get': text, info = custom.product_link( ename, pname, eval_state.dependencies, render_propval, extension=ext) elif pref == 'edit': text, info = custom.edit_link( ename, pname, eval_state.dependencies, render_propval, extension=ext) if ename not in eval_state.mentions: eval_state.mentions.append(ename) return text, info elif not rest.startswith('//'): # Check interwiki try: iw = Element.get(u'InterwikiConfig') except NoResultFound: eval_state.dependencies.addExistsDep(u'InterwikiConfig') else: eval_state.dependencies.addDep(iw[u'prefix']) eval_state.dependencies.addDep(iw[u'url']) if u'prefix' in iw and u'url' in iw: for i in _list_descendants(iw): eval_state.dependencies.addDep(i[u'prefix']) if unicode(i[u'prefix'].value, 'utf-8') == pref: eval_state.dependencies.addDep(i[u'url']) return (href, dict(url=unicode(i[u'url'].value, 'utf-8')+rest, style='external')) # None matched if href.startswith('/'): abshref = custom.local_link(href) else: abshref = href return href, dict(url=abshref, style='external') def _error(message): if eval_state.catch_errors: return Error('[error]') elif not custom.is_omniscient(): raise Exception(message) else: eval_state.dependencies.makeUncacheable() eval_state.errors.append(message) return Error(message) MACRO_NAME_PAT = re.compile('^([^\s#]+)(?:\#\S+)?') def macro_func(name, argstr=u'', content=None): try: # Allow macros to have #foo suffix, to allow nesting m=MACRO_NAME_PAT.match(name) assert m is not None name = m.group(1) origname = name argstr = argstr.strip() if argstr else u'' if ('.' not in name and name not in eval_state.moverrides and not hasattr(macros,name) and not hasattr(macros,name+'_')): curr = get_current_element() try: if name in curr: get_element(name) except NoResultFound: eval_state.dependencies.addExistsDep(name) name = "%s.%s" % (curr.ename, name) else: name += u'.'+custom.SUBSTITUTION_PROP maced=True else: maced=False if '.' in name: elm,pnam = name.strip().split('.',1) try: r = recurse(elm, pnam, content=content, argstr=argstr) for t in r: yield t except NoResultFound: if maced: yield _error( "No element or macro ##%s## exists evaluating %s.%s!" % (origname, eval_state.eoverrides[custom.LEAF_ELEMENT].ename, eval_state.poverrides['this'])) else: yield _error("No element ##%s## exists!" % elm) else: if name in eval_state.moverrides: ove = eval_state.moverrides[name] if hasattr(ove, '__call__'): for t in ove(argstr, content): yield t else: yield Text(unicode(ove)) return if hasattr(macros,name+'_'): name = name+'_' if hasattr(macros, name): if content is not None: gen = getattr(macros,name)(argstr=argstr,content=content) else: gen = getattr(macros,name)(argstr=argstr) if not hasattr(gen, '__iter__'): yield _error('Error: <<%s %s>> return non-iterator "%s"!' % (name, argstr, repr(gen))) return for t in gen: yield t else: yield _error('Error: no such macro %s!' % name) except NoContentException: raise except WikiException,e: yield _error(e.args[0]) # Set or add to this to change what macros are available macros = Macros() # This is a bit of a hack; we could allow some state to get passed through # AbstractMarkup.evaluate... eval_state = threading.local() def init_eval_state(element, propname, extension, flavor, let={}, catch_errors=False): eval_state.dependencies = dependencies.Dependencies() eval_state.topthis = propname if propname is not None: eval_state.dependencies.addDep(element[propname]) eval_state.me = element eval_state.eoverrides = { custom.CURRENT_ELEMENT: element, custom.LEAF_ELEMENT: element } eval_state.moverrides = {u'args':""} eval_state.poverrides = {u'this': propname} for k,v in let.items(): if isinstance(v, Element): eval_state.eoverrides[k] = v else: eval_state.moverrides[k] = v eval_state.images = set() eval_state.mentions = [] eval_state.errors = [] eval_state.extension = extension if extension in FORMATS: fmt = FORMATS[extension] mainparser = Parser(fmt, macro_func, link_func) eval_state.format = fmt eval_state.mainparser = mainparser else: assert flavor.binary # binary flavors interpret a string as a parser, for great justice. eval_state.mainparser = extension eval_state.catch_errors = catch_errors def evaluate(thing, extension, toPython=False, let={}, element=None, flavor=None, reentrant=False, catchErrors=False): if isinstance(thing, Propval): element = thing.element propval = thing propname = propval.prop.name assert element is not None assert propname is not None assert propval.value is not None, propval if flavor is None: flavor = FLAVORS[propval.prop.flavor] if flavor.binary: wikitext = propval.value else: wikitext = unicode(propval.value, 'utf-8') else: propval = None propname = None wikitext = thing assert flavor is not None if not reentrant: assert not hasattr(eval_state, 'dependencies'), eval_state.__dict__ else: old_eval_state = dict(eval_state.__dict__) try: init_eval_state(element, propname, extension, flavor, let, catch_errors=catchErrors) desc = ("%s.%s" % (propval.element.ename, propval.prop.name) if propval is not None else 'wikitext') if not toPython: with benchmarking('evaluating '+desc): res = flavor.evaluate(wikitext, eval_state.mainparser, propval=propval) else: with benchmarking('python-converting '+desc): res = flavor.toPython(wikitext, eval_state.mainparser, propval=propval) except NoContentException,e: if toPython: raise assert propval is not None and not propval.prop.visible res = eval_state.mainparser.render([ Text(u"Content unspecified, raw markup: "), Start(CODEBLOCK), Text(wikitext), End(CODEBLOCK)]) except ParserException, e: if toPython: raise res = eval_state.mainparser.render([ _error(u"Parser Error: %s" % (e.args[0],))]) except WikiException,e: if toPython: raise res = eval_state.mainparser.render([ _error(u"Wiki Error: %s" % (e.args[0],))]) finally: deps = eval_state.dependencies images = eval_state.images clear_eval_state() if reentrant: eval_state.__dict__.update(old_eval_state) # rendered result, dependencies, metadata return res, deps, {'images': images} def clear_eval_state(): eval_state.__dict__.clear() class RefExtractingFormat(BaseFormat): def __init__(self): self.refs = [] self.link_toks = None def text(self, text): if self.link_toks is not None: self.link_toks.append(Text(text)) return u'' def start(self, t, arg=None): if t == LINK: self.link_toks = [] elif self.link_toks is not None: self.link_toks.append(Start(t, arg)) return u'' def end(self, t, arg=None): if t == LINK: if 'ename' in arg: self.refs.append((arg['ename'], self.link_toks)) self.link_toks = None elif self.link_toks is not None: self.link_toks.append(End(t, arg)) return u'' def entity(self, t, arg=None): if self.link_toks is not None: self.link_toks.append(Entity(t, arg)) return u'' def ref_link_func(h, sty): if sty == LINK and '/' not in h and '.' not in h: try: ename = get_element(h).ename except NoResultFound: ename = h return '', dict(ename=ename) else: return '', {} def get_reference_enames_with_labels(text_or_elm, propname=None): if propname is not None: assert not FLAVORS[text_or_elm[propname].prop.flavor].binary text = unicode(text_or_elm[propname].value, 'utf-8') eval_state.dependencies.addDep(text_or_elm[propname]) else: assert isinstance(text_or_elm, unicode) text = text_or_elm old_mentions = eval_state.mentions old_errors = eval_state.errors ref = RefExtractingFormat() eval_state.mainparser.parse(text, format=ref, link_func=ref_link_func) eval_state.mentions = old_mentions eval_state.errors = old_errors return ref.refs def get_reference_enames(text_or_elm, propname=None): return [e for e,l in get_reference_enames_with_labels(text_or_elm, propname)] def get_references(text_or_elm, propname=None): return [Element.get(e) for e in get_reference_enames(text_or_elm, propname)] def get_eval_state_delta(which, text_or_elm, propname=None): if propname is not None: assert not FLAVORS[text_or_elm[propname].prop.flavor].binary text = unicode(text_or_elm[propname].value, 'utf-8') eval_state.dependencies.addDep(text_or_elm[propname]) else: assert isinstance(text_or_elm, unicode) text = text_or_elm old_mentions = eval_state.mentions old_errors = eval_state.errors setattr(eval_state, which, []) eval_state.mainparser.parse(text, format=Format()) ret = getattr(eval_state, which) eval_state.mentions = old_mentions eval_state.errors = old_errors return ret def get_mention_enames(text_or_elm, propname=None): return get_eval_state_delta('mentions', text_or_elm, propname) def get_mentions(text_or_elm, propname=None): """Return all elements linked to or named in this wikitext or propval.""" return [Element.get(e) for e in get_mention_enames(text_or_elm, propname)] def has_errors(elm, propname): """Return true if any errors are encountered evaluating this propval. This probably only works properly if the logged in user is omniscient; we could catch the non-wiki exceptions that can arise, but we don't.""" try: return len(get_eval_state_delta('errors', elm, propname)) > 0 except WikiException: return True def get_format(): return eval_state.format def addDeps(deps): eval_state.dependencies.update(deps)