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('%s>' % 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
''' % 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)