import threading
import re, sys, types
from itertools import izip, count, chain
from decorator import decorator
from contextlib import contextmanager
from redbeans import latex
from redbeans.formats import BaseFormat, Format
from redbeans.creole import tokenize
from redbeans.parser import Parser
from redbeans.tokens import *
from . import custom, db, dependencies, cache, structure
from .flavors import FLAVORS, FORMATS, Reference
from .benchmark import benchmarking
class WikiException(Exception):
pass
class NoContentException(WikiException):
pass
class ElementNotFound(WikiException):
pass
class PropNotFound(WikiException):
pass
class moverride(object):
def __init__(self, kw):
for v in kw.values():
assert isinstance(v, unicode), kw
for k in kw:
assert not k[0].isupper() and k[0] != '*', 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
for k in kw:
assert k[0].islower(), kw
assert hasattr(kw[k], 'ename') or hasattr(kw[k], 'element') or kw[k] is None, 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 push_current(eoverride):
def __init__(self, curr):
eover = {u'current': curr}
if hasattr(curr, 'owner') and curr.owner is not None:
# Reference
eover[u'owner'] = curr.owner
eoverride.__init__(self, eover)
def __enter__(self):
eval_state.current_stack.append(self.kw[u'current'])
eoverride.__enter__(self)
def __exit__(self, exc_type, exc_val, exc_tb):
eoverride.__exit__(self, exc_type, exc_val, exc_tb)
assert eval_state.current_stack[-1] == self.kw[u'current']
eval_state.current_stack.pop()
class poverride(object):
def __init__(self, kw):
for k in kw:
assert k.startswith('*'), 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
class context(object):
def __init__(self, kw):
self.kw = kw
def __enter__(self):
self.old_context = dict(eval_state.context)
eval_state.context.update(self.kw)
def __exit__(self, exc_type, exc_val, exc_tb):
eval_state.context = self.old_context
# Use a weird singleton becuse None and False could be valid return values.
NO_MEMO = (None,)
def get_memo_or_push(key):
ret, restr = cache.get_memo(key + get_context_tag(), (NO_MEMO, None))
found = False
if ret is not NO_MEMO:
# Check restr
for typ, name, val in restr:
if typ == 'eoverride':
if eval_state.eoverrides.get(name, None) != val:
break
elif typ == 'moverride':
if eval_state.moverrides.get(name, None) != val:
break
elif typ == 'poverride':
if eval_state.poverrides.get(name, None) != val:
break
else:
found = True
if found:
return ret
else:
eval_state.memoable_stack.append(set())
return NO_MEMO
def set_memo_and_pop(key, val):
memoable = eval_state.memoable_stack.pop()
restr = set()
for typ, name in memoable:
if typ == 'eoverride':
restr.add(('eoverride', name,
eval_state.eoverrides.get(name, None)))
elif typ == 'poverride':
restr.add(('poverride', name,
eval_state.poverrides.get(name, None)))
elif typ == 'moverride':
restr.add(('moverride', name,
eval_state.moverrides.get(name, None)))
else:
assert False, memoable
cache.set_memo(key + get_context_tag(), (val, restr))
eval_state.memoable_stack[-1].update(memoable)
def recursive_get(elm, prop_name, *args, **restrictions):
incl_elm = True
with_owner = False
if len(args) > 0:
if len(args) > 1:
assert len(args) == 2, args
with_owner = args[1]
filter = args[0]
# prune: include the first thing that fails the restrictions,
# but don't recurse into that thing.
# exclude: exclude anything that fails the restrictions.
# exclude!: exclude, but don't include elm itself.
# invert: include only everything that would be excluded by exclude,
# but would've been included without restrictions.
assert filter in ('prune', 'invert', 'exclude', 'exclude!')
if filter.startswith('exclude'):
if '!' in filter:
incl_elm = False
filter = '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' and incl_elm:
ret = [elm] + ret
if with_owner:
ret = [(e, with_owner) for e in 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 n.has_propval(r) else None)
== restrictions[r]
for r in (unicode(sr, 'utf-8')
for sr in restrictions))
if passed_restr or filter == 'invert':
if (filter == 'exclude' or
(filter == 'invert' and (not passed_restr or pruned))):
# We only add it after it meets the restrictions
if with_owner:
if n.has_propval(u'subowned') and to_python(n, u'subowned'):
ret.append((n, path[-1]))
else:
ret.append((n, with_owner))
else:
ret.append(n)
if n.has_propval(prop_name):
adding = to_python(n, prop_name)
if filter == 'prune':
assert not with_owner
ret += adding
newnew.append((path+[n], adding,
not passed_restr or pruned))
new = newnew
return ret
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 error(self, 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:
assert isinstance(arg['ename'], unicode), repr(arg)
self.refs.append((arg, self.link_toks))
#assert False, 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):
#print >>sys.stderr, "r_l_f", h, sty
if sty == LINK and not h.startswith('/') and '://' not in h:
args = None
if '/' in h:
args = h.split('/')
h = args.pop(0)
if '.' not in h and ' ' not in h:
elm = get_element(h)
if elm is not None:
ename = elm.ename
else:
ename = h
return u'', dict(ename=ename, args=args,
owner=get_overridden_element(u'owner'))
return u'', {}
def get_reference_enames_with_labels(text_or_elm, propname=None, propval=None):
if propval is not None:
# Could be memoized
key = 'g_r_e_w_l:%s.%s' % (propval.element.ename, propval.propname)
ret = get_memo_or_push(key)
if ret is not NO_MEMO:
return ret
if propname is not None:
assert not structure.get_flavor(propname).binary
pv = text_or_elm.get_propval(propname)
text = unicode(pv.value, 'utf-8')
eval_state.dependencies.addDep(pv)
else:
assert isinstance(text_or_elm, unicode)
text = text_or_elm
old_mentions = eval_state.mentions
old_errors = eval_state.errors
eval_state.errors = []
ref = RefExtractingFormat()
with context({'in_get_refs': True}):
eval_state.mainparser.parse(text, format=ref, link_func=ref_link_func)
eval_state.mentions = old_mentions
#assert not eval_state.errors, (foo, eval_state.errors)
new_errors = eval_state.errors
eval_state.errors = old_errors
#print >>sys.stderr, "Refs:", ref.refs
if propval is not None:
set_memo_and_pop(key, (ref.refs, new_errors))
return ref.refs, new_errors
def get_reference_enames(text_or_elm, propname=None, propval=None):
with_labels, errors = get_reference_enames_with_labels(text_or_elm,
propname,
propval)
return [d for d,l in with_labels], errors
def get_references(text_or_elm, propname=None, propval=None):
with_args, errors = get_reference_enames(text_or_elm, propname, propval)
return [structure.get_element(d['ename']) for d in with_args]
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 [structure.get_element(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 has_ancestor(element, ancestor):
if hasattr(element, 'element'):
element = element.element
return ancestor.is_ancestor_of(element)
def is_a(element, ancestor):
if hasattr(element, 'element'):
element = element.element
return ancestor == element or ancestor.is_ancestor_of(element)
def safe_getattr(object, name, *args):
assert '__' not in name
return getattr(object, name, *args)
# We do this for the exception handling; if builtin hasattr() gets any
# exception, it silently returns False.
def has_prop(obj, name):
assert isinstance(obj, attrelm), obj
try:
getattr(obj, name)
except PropNotFound:
return False
return True
def baz_eval_wrap(arg, toPython):
if hasattr(arg, 'element'):
return attrelm(arg.element, toPython, arg.args, arg.owner)
elif hasattr(arg, 'ename'):
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.unwrap__()
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 get_context(key):
return eval_state.context.get(key, False)
def get_context_tag():
tag = ''
for k in eval_state.context:
val = eval_state.context[k]
assert val in (True, False), eval_state.context
if val:
tag += '+' + k
return tag
BAZ_EVAL_GLOBALS = dict((toPython, {"__builtins__": {},
"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,
"len": len,
"hasattr": has_prop,
"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),
"is_a": baz_eval_wrap_func(is_a, 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),
"get_context": get_context
})
for toPython in (False, True))
def baz_eval(expr, toPython=True):
if len(expr.strip()) == 0:
return u''
if '__' in expr:
# Protect against hackery
raise WikiException("The string '__' is not allowed to appear in expressions!")
else:
expr = expr.replace('\n', ' ')
key = 'baz_eval%s:%s' % (toPython, expr)
ret = get_memo_or_push(key)
if ret is not NO_MEMO:
return ret
#print >>sys.stderr, 'baz_eval', expr
elemlocs = _elemdict(toPython=toPython)
try:
eval_globals = dict(BAZ_EVAL_GLOBALS[toPython])
eval_globals.update(
{"defined": (lambda s: s in elemlocs),
"get": (lambda s,d=None: elemlocs.get(s,d))})
#print >>sys.stderr, expr
ret = eval(expr, eval_globals, elemlocs)
ret = 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)
else:
set_memo_and_pop(key, ret)
return ret
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)
if hasattr(ret, '__call__'):
gen = ret(None, None)
ret = tokens_text(gen)
else:
ret = unicode(ret)
return ret
def string_tokenize(s):
return FLAVORS['string'].tokenize(s, eval_state.mainparser)
def tokens_text(toks):
ret = u''
for t in toks:
if t.op == ENTITY and t.style == MACRO:
assert t.arg[1] is None, t
ret += unicode_eval(t.arg[0])
else:
assert t.op == TEXT, t
ret += t.arg
return ret
def render_allow_override(element, prop_name, default=None,
eoverrides={}, moverrides={}):
# Could be an moverride.
eval_state.memoable_stack[-1].add(('moverride', prop_name))
for v in moverrides.values():
assert isinstance(v, unicode)
if prop_name in eval_state.moverrides:
return eval_state.moverrides[prop_name]
else:
prop_name = get_propname(prop_name)
eval_state.dependencies.addPropvalDep(element, prop_name)
if element.has_propval(prop_name):
return render_propval(element, prop_name, eoverrides=eoverrides,
moverrides=moverrides)
else:
return default
def render_propval(element, prop_name, eoverrides={}, moverrides={},
content=None):
"""Render the propval as plain text, tracking dependencies."""
for v in moverrides.values():
assert isinstance(v, unicode)
for k in eoverrides:
assert k[0].islower(), eoverrides
assert hasattr(eoverrides[k], 'ename'), eoverrides
for k in moverrides:
assert not k[0].isupper() and k[0] != '*', moverrides
# TODO(xavid): theoretically this should actively be position-independent
parser = Parser(FORMATS['txt'], macro_func, link_func, makeRestricted)
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, content=content))
eval_state.eoverrides = old_eoverrides
eval_state.moverrides = old_moverrides
eval_state.mainparser = old_parser
return ret
KW_ARG_PAT = re.compile(r'^(\w+)[=]([^=].*)$')
def parse_macro_args(arglist):
"""Parse an argument list into a map of names to values.
name will be an integer for positional parameters and a string for
keyword parameters."""
nextname = 1
retmap = {}
for word in arglist:
m = KW_ARG_PAT.search(word)
if m:
retmap[m.group(1)] = m.group(2)
else:
retmap[nextname] = word
nextname += 1
return retmap
def _list_descendants(e):
assert e.ename is not None
eval_state.dependencies.addChildrenDep(e)
# -! make more efficient, possibly move to structure
everyone = e.get_descendants()
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):
# Only allow overridden elements that start with a lowercase letter.
# TODO(xavid): we can probably clean up lots of stuff given this new
# simplifying assumption.
assert isinstance(ename, unicode), repr(ename)
if hasattr(eval_state, 'dependencies') and ename[0].islower():
# parent can't be overridden, for optimization reasons.
if ename == custom.PARENT_ELEMENT:
eval_state.dependencies.addParentDep(eval_state.me)
return eval_state.me.get_parent()
else:
eval_state.memoable_stack[-1].add(('eoverride', ename))
if ename in eval_state.eoverrides:
return eval_state.eoverrides[ename]
elif ename == custom.ME_ELEMENT:
return eval_state.me
return None
def get_element(ename):
e = get_overridden_element(ename)
if e is not None:
return e
else:
elm = structure.get_element(ename)
if elm is None:
if hasattr(eval_state, 'dependencies'):
eval_state.dependencies.addExistsDep(ename)
return None
return elm
def get_current_element():
"""Like get_element(u'leaf'), but allows explicit overriding by setting
current."""
#print >>sys.stderr, "g_c_e", eval_state.eoverrides, get_element(custom.LEAF_ELEMENT)
eval_state.memoable_stack[-1].add(('eoverride', u'current'))
if u'current' in eval_state.eoverrides:
ret = eval_state.eoverrides[u'current']
if ret is not None:
return ret
return get_element(custom.LEAF_ELEMENT)
def get_current_for_prop(propname):
for curr in reversed(eval_state.current_stack):
if curr.has_propval(propname):
return curr
return None
def get_propname(pname):
if pname[0] != '*' and pname != u'this':
return pname
if pname != u'this':
eval_state.memoable_stack[-1].add(('poverride', pname))
if pname in eval_state.poverrides:
return eval_state.poverrides[pname]
else:
raise WikiException("Undefined propname '%s'!" % pname)
def get_moverride(mname):
if mname in eval_state.moverrides:
return eval_state.moverrides[mname]
else:
return None
class Arg(object):
def __init__(self, raw):
self.raw = raw
def __call__(self, args, content):
yield Text(unicode_eval(self.raw))
def to_python(ename_or_e, pname):
pname = get_propname(pname)
if hasattr(ename_or_e, 'ename'):
memo_ename = ename_or_e.ename
elif hasattr(ename_or_e, 'element'):
# It's a Reference, we need to include the args.
memo_ename = u"%s/%s" % (ename_or_e.element.ename,
'/'.join(ename_or_e.args))
else:
memo_ename = get_overridden_element(ename_or_e)
if memo_ename is None:
memo_ename = ename_or_e
key = 'to_python:%s.%s' % (memo_ename, pname)
ret = get_memo_or_push(key)
if ret is not NO_MEMO:
return ret
ls = list(recurse(ename_or_e, pname, to_python=True))
#print >>sys.stderr, "to_python", ls
assert len(ls) == 1
set_memo_and_pop(key, ls[0])
return ls[0]
def recurse(ename, pname, to_python=False, content=None, arglist=[]):
#print >>sys.stderr, "recurse", ename, pname
assert ename is not None
if hasattr(ename, 'ename'):
e = ename
ename = e.ename
assert hasattr(e, 'ename'), e
elif hasattr(ename, 'element'):
e = ename
ename = e.element.ename
else:
e = get_element(ename)
if e is None:
raise ElementNotFound(ename)
pname = get_propname(pname)
assert pname is not None
assert e is not None
extra_mover = {}
extra_eover = {}
orig_e = e
if hasattr(e, 'element'):
# This is a flavors.Reference.
for i in xrange(len(e.args)):
extra_mover[unicode(i+1)] = e.args[i]
if e.owner:
extra_eover = {u'owner': e.owner}
e = e.element
assert hasattr(e, 'ename'), e
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'):
assert hasattr(e, 'get_propval'), e
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)
old_current = list(eval_state.current_stack)
added_current = False
try:
eval_state.me = e
eval_state.poverrides['this'] = pname
# Maybe update leaf
if ename != custom.PARENT_ELEMENT:
eval_state.eoverrides[custom.LEAF_ELEMENT] = orig_e
eval_state.eoverrides[custom.CURRENT_ELEMENT] = orig_e
eval_state.current_stack.append(orig_e)
added_current = True
# Add macro params
if content is not None:
content = list(content)
def render_content(arglist, 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 arglist:
eval_state.moverrides[u'args'] = u' '.join(arglist)
if arglist:
for var,arg in parse_macro_args(arglist).items():
eval_state.moverrides[
u'arg'+unicode(var)] = Arg(arg)
flavor = structure.get_flavor(pname)
if flavor.owned:
eval_state.eoverrides[u'owner'] = orig_e
eval_state.moverrides.update(extra_mover)
eval_state.eoverrides.update(extra_eover)
pv = e.get_propval(pname)
if pv is None:
if pname == custom.SUBSTITUTION_PROP:
if to_python:
yield tokens_text(macros.contexteval())
else:
for t in macros.contexteval():
yield t
return
else:
raise PropNotFound(
"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 flavor.toPython(
value, eval_state.mainparser, propval=pv)
else:
for t in flavor.tokenize(
value, eval_state.mainparser, propval=pv):
yield t
# If we haven't thrown an exception yet, sancheck some things
# before cleaning up. We don't do this in finally, because
# we don't really want to fail asserts if something below
# us already had an assertion failure.
if added_current:
assert eval_state.current_stack[-1] == orig_e, (
eval_state.current_stack, eval_state.current_stack[-1],
orig_e)
assert eval_state.current_stack[:-1] == old_current, (eval_state.current_stack, old_current)
finally:
eval_state.me = oldme
eval_state.poverrides = oldpover
eval_state.eoverrides = oldeover
eval_state.moverrides = oldmover
eval_state.current_stack = old_current
#print >>sys.stderr, "end of recurse", ename, pname
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, args=None, owner=None):
assert hasattr(element, 'ename') or hasattr(element, 'element'), element
self.element__ = element
self.__toPython = toPython
self.__args = args
self.__owner = owner
def unwrap__(self):
if self.__args is None and self.__owner is None:
return self.element__
else:
return Reference(self.element__, self.__args, self.__owner)
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)
#print >>sys.stderr, "attrelm", self.element__, a, repr(ret), structure.get_flavor(a)
# wrap elements we get back
return baz_eval_wrap(ret, self.__toPython)
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 or key == custom.PARENT_ELEMENT:
return True
if not isinstance(key, unicode):
key = unicode(key,'utf-8')
if key is None:
return False
if not key[0].isupper():
eval_state.memoable_stack[-1].add(('eoverride', key))
eval_state.memoable_stack[-1].add(('moverride', key))
if key in eval_state.eoverrides or key in eval_state.moverrides:
return True
curr = get_current_for_prop(key)
foo.append(curr)
if curr is not None:
return True
if hasattr(macros, key):
return True
e = structure.get_element(key)
if e is not None:
return True
else:
if hasattr(eval_state, 'dependencies'):
eval_state.dependencies.addExistsDep(key)
return False
def __getitem__(self,key):
foo.append(key)
if key in self.__locals:
return self.__locals[key]
if key == custom.PARENT_ELEMENT:
eval_state.dependencies.addParentDep(eval_state.me)
return attrelm(eval_state.me.get_parent(), self.__toPython)
key = unicode(key, 'utf-8')
if key not in self:
foo.append(False)
raise KeyError("No element '%s' exists!"%key)
if not key[0].isupper() and key in eval_state.moverrides:
return eval_state.moverrides[key]
elif key[0] == '_' and key[1:] in eval_state.moverrides:
return eval_state.moverrides[key[1:]]
elif key not in eval_state.eoverrides and hasattr(macros, key):
return tokens_text(getattr(macros, key)(None, None))
else:
if not key[0].isupper() and key not in eval_state.eoverrides:
curr = get_current_for_prop(key)
if curr is not None:
assert curr.has_propval(key)
return getattr(attrelm(curr, self.__toPython), key)
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
foo = []
# Macro-creating helpers
def environment(name, texname=None, tagname=None, texmode='environment',
doc = ''):
if texname is None:
texname = name
def env(arglist, content):
args = parse_macro_args(arglist)
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 Entity(ENV_BREAK, True)
yield Literal('%s>' % tagname)
else:
yield Literal('
' % (
name, ''.join(' '+c for c in subclasses)))
for t in content:
yield t
yield Entity(ENV_BREAK, True)
yield Literal('
')
elif eval_state.format == FORMATS['tex']:
if texmode == 'environment':
yield Literal('\\begin{%s}\n' % texname)
for t in content:
yield t
yield Entity(ENV_BREAK, True)
yield Literal('\\end{%s}' % texname)
else:
yield Literal('\\%s{' % texname)
for t in content:
yield t
yield Entity(ENV_BREAK, True)
yield Literal('}')
else:
for t in content:
yield t
yield Entity(ENV_BREAK, True)
env.__doc__ = doc
return env
def atom(default="", doc='', **kw):
def ato(arglist, content=None):
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(arglist):
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(arglist, content):
parts = safesplit(content, (divider,))
args = parse_macro_args(arglist)
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):
for e in _error("Too many %ss in a %s!"%(divider,name)):
yield e
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, arg=None, doc=''):
def fmt(arglist, content=[]):
#if style == FOOTNOTE: assert False, eval_state.format.link_toks
yield Start(style, arg)
for t in content:
yield t
yield End(style, arg)
fmt.__doc__ = doc
fmt.hidden = 'latexalike'
return fmt
def arg_index(num):
def argi(arglist, content=None):
"""For references with arguments."""
arg_name = u'arg%d' % num
eval_state.memoable_stack[-1].add(('moverride', arg_name))
if arg_name in eval_state.moverrides:
for t in string_tokenize(eval_state.moverrides[arg_name].raw):
yield t
elif u'1' in eval_state.moverrides or u'arg1' in eval_state.moverrides:
# We have some explicit num_ofs, just not this high
pass
else:
#assert False, (num, eval_state.moverrides)
raise WikiException("No arg %s: %s" % (num, eval_state.moverrides))
return argi
def illegal(name,*container):
def ill(arglist, content=None):
return _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(arglist, content):
# """Prevent wiki markup in the content from being evaluated.
#
# Unlike ##~{{{}}}##, the font style is unchanged."""
# return parser.escape(content)
@staticmethod
def par(arglist):
"""Start a new paragraph."""
yield Entity(ENV_BREAK)
@staticmethod
def block(arglist, content):
"""Treat the content as its own block, separate from surroundings."""
for t in content:
yield t
@staticmethod
def format(arglist, content):
"""Include the markup, unsanitized, in the given format, with
macros evaluated.
Use like {{{<>Raw HTML<>}}}."""
args = parse_macro_args(arglist)
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(arglist, content):
"""Define local names for elements or other data.
Use like {{{<>...<>}}}."""
args = parse_macro_args(arglist)
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 hasattr(v, 'ename'):
eval_state.eoverrides[k] = v
else:
assert (isinstance(v, unicode)
or hasattr(v, '__call__')
or v is None), (v, eval_state.moverrides)
eval_state.moverrides[k] = v
for t in content:
yield t
eval_state.eoverrides = oldeover
eval_state.moverrides = oldmover
@staticmethod
def plet(arglist, content):
"""Define local prop names.
Use like {{{<><><>}}}."""
args = parse_macro_args(arglist)
oldpover = dict(eval_state.poverrides)
pstubst = {}
for k,v in args.items():
# TODO: come up with better evaulation logic with the lazy
# evaluate magic/baz_eval
v = baz_eval(v)
psubst['*' + k] = v
with poverride(psubst):
for t in content:
yield t
@staticmethod
def content(arglist):
"""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(arglist, 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 = u' '.join(arglist)
while True:
val = baz_eval(cond)
#assert False, (arglist, val)
if val:
break
else:
parttags = parttags[1:]
if len(parttags) <= 0:
return
name, arglist = parttags[0][1]
if name == 'else':
break
else:
cond = u' '.join(arglist)
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(arglist, content=None, style=LINK):
"""Builds a link dynamically, evaluating and joining arguments.
Use like {{{<>Packet<>}}}
or {{{<>}}}."""
#print >>sys.stderr, "LINK", repr(arglist), repr(content)
args = parse_macro_args(arglist)
dest = ''.join(unicode_eval(args[a]) for a in sorted(args))
#print >>sys.stderr, "LINK dest=", dest
if content is not None:
yield Start(style, dest)
for t in content:
yield t
yield End(style, dest)
else:
yield Entity(style, dest)
@staticmethod
def image(arglist, content=None):
return Macros.link(arglist, content, style=IMAGE)
@staticmethod
def count(arglist, content=None):
"""Counts the number of elements that would be matched by foreach."""
return Macros.foreach(arglist, content=content, count=True)
#@staticmethod
#def sum(arglist, content=None):
# total = 0
# for t in Macros.foreach(arglist, content=content):
# #assert t.op == TEXT, repr(t)
# if t.op == TEXT:
# try:
# total += int(t.arg)
# except ValueError:
# #assert False, repr(t.arg)
# pass
# return [Text(unicode(total))]
@staticmethod
def foreach(arglist, content=None, count=False):
"""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.
You can also sum counts by doing something like:
{{{<>}}}.
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.
inRowsOf: put the evaluations in a table with the specified number
of columns. {{{<>}}} may be used to break an
evaluation into several rows.
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(arglist)
if 1 not in args or 2 not in args:
raise WikiException("{{{foreach}}} takes at least two parameters! See [[edit:Macros]]!")
#print >>sys.stderr, "In foreach", args
# We need to be able to iterate over this multiple times
typ = args[1]
del args[1]
nam = args[2]
del args[2]
subtyps = []
subnams = []
next = 3
while next in args and args[next] in ('foreach', 'count'):
if args[next] == 'count':
count = True
del args[next]
subtyps.append(args[next + 1])
del args[next + 1]
subnams.append(args[next + 2])
del args[next + 2]
next += 3
if content is None:
if not count:
raise WikiException("{{{foreach}}} must have content! See [[edit:Macros]]!")
else:
content = []
content = list(content)
restr = set()
orderBy = []
groupBy = None
asTree = False
recursive = False
childrenOnly = False
ifNone = None
inRowsOf = 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)
elif key.lower() == 'inrowsof':
inRowsOf = baz_eval(arg)
else:
for e in _error("Unknown foreach parameter '%s'!" % key):
yield e
else:
restr.add(arg)
# TODO(xavid): create separate "for each ancestor var" and
# "for var in list" syntaxes.
list_or_elm = baz_eval(typ)
#print >>sys.stderr, "l_o_e", typ, list_or_elm
if not asTree:
def get_list(list_or_elm):
if hasattr(list_or_elm, 'ename'):
# Now do a traversal:
if childrenOnly:
eval_state.dependencies.addChildrenDep(list_or_elm)
return list_or_elm.get_children()
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') or
hasattr(subst[substnam], 'element')):
eval_state.eoverrides[substnam] = subst[substnam]
else:
assert isinstance(subst[substnam], (unicode, types.FunctionType)), (subst, substnam)
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:
for e in lst:
assert e is not None, (typ, lst, list_or_elm)
subst_list = [{nam: e} for e in lst]
else:
for e in lst:
for f in e:
assert f is not None, lst
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:
if new_e is None:
raise WikiException("Got None evaluating %s!"
% (styp))
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)
try:
if all(baz_eval(r) for r in restr):
filtlst.append(subst)
except NoContentException:
pass
subst_list = filtlst
if count:
yield Text(unicode(len(subst_list)))
elif len(subst_list) == 0 and ifNone is not None:
for t in tokenize(ifNone, makeRestricted):
yield t
else:
# Sort by the nam element always
for f in reversed(orderBy):
subst_list.sort(key=lambda subst:
to_python(subst[nambits[0]], f)
if subst[nambits[0]].has_propval(f)
else MAXWE)
def stickon(subst, cont):
do_overrides(subst)
for t in cont:
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()):
assert isinstance(g, unicode), repr(g)
eval_state.moverrides[groupName] = (g if g != MAXWE
else u"???")
if groupMarkup:
for t in tokenize(groupMarkup, makeRestricted):
yield t
for subst in groups[g]:
for t in stickon(subst, content):
yield t
else:
if inRowsOf:
this_row = []
content_bits = safesplit(content, 'break')
content = content_bits[0]
content_bits = content_bits[1:]
def end_row():
yield End(TABLE_ROW)
for c in content_bits:
yield Start(TABLE_ROW)
for s in this_row:
yield Start(TABLE_CELL)
for t in stickon(s, c):
yield t
yield End(TABLE_CELL)
yield End(TABLE_ROW)
del this_row[:]
for subst in subst_list:
if inRowsOf:
if len(this_row) == 0:
yield Start(TABLE_ROW)
yield Start(TABLE_CELL)
#with benchmarking(u'foreach subst=%s' % subst):
for t in stickon(subst, content):
yield t
if inRowsOf:
yield End(TABLE_CELL)
this_row.append(subst)
if len(this_row) == inRowsOf:
for t in end_row():
yield t
if inRowsOf and len(this_row) > 0:
for t in end_row():
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 count:
yield Text(unicode(len(lst)))
elif 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 hasattr(list_or_elm, 'ename')
for t in dig(list_or_elm, 1):
yield t
_break = staticmethod(illegal('break', 'foreach', 'similar'))
@staticmethod
def contexteval(arglist=None):
# TODO(xavid): actually explain this better.
"""Call as {{{<>}}} in an element's substitution
to allow that element to serve as a context for property/macro
evaluation."""
leaf = get_element(u'leaf')
ccontent = get_moverride(u'content')
# This is for members/contacts lists.
is_refs = False
#print >>sys.stderr, "contexteval", get_context()
eover_dict = {}
current = get_current_element()
if hasattr(leaf, 'element'):
if leaf.owner is not None:
eover_dict[u'owner'] = leaf.owner
leaf = leaf.element
if (get_overridden_element(u'owner') is None and current != leaf
and u'owner' not in eover_dict):
eover_dict[u'owner'] = current
with eoverride(eover_dict):
if get_context('in_get_refs'):
is_refs = True
href = leaf.ename
i = 1
while get_moverride(u'arg%d' % i):
href += u'/' + tokens_text(
string_tokenize(get_moverride(u'arg%d' % i).raw))
i += 1
yield Start(LINK, href)
with context({'in_get_refs': False}):
with push_current(leaf):
if ccontent is not None:
cnt = 0
for t in ccontent(None, None):
cnt += 1
yield t
if cnt == 0:
for t in recurse(leaf, u'name'): yield t
else:
for t in recurse(leaf, u'name'): yield t
if is_refs:
yield End(LINK, href)
#assert False, eval_state.eoverrides
@staticmethod
def comment(arglist, content=None):
"""Suppress any content.
Useful for leaving comments only relevant to people editing a prop's
value."""
return ()
@staticmethod
def withheaders(arglist, content): # More descriptive name
"""Renders the (up to) four arguments as headers around the content."""
args = parse_macro_args(arglist)
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"""))
figure = staticmethod(environment('figure', doc="Make a floating figure."))
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."))
smallskip = staticmethod(atom(html=u'',
tex=r'\par\smallskip ',
doc="Insert a small gap."))
medskip = staticmethod(atom(html=u'',
tex=r'\par\medskip ',
doc="Insert a medium gap."))
bigskip = staticmethod(atom(html=u'',
tex=r'\par\bigskip ',
doc="Insert a big gap."))
tableofcontents = staticmethod(atom(html=u'',
tex=u'\tableofcontents',
doc="Insert a table of contents."))
# LaTeX-alike macro punct
ldots = staticmethod(atom(u'\u2026', doc="Ellipsis."))
# LaTeX-alike non-punct special chars
ae = staticmethod(atom(u'\u00E6', doc="ae"))
AE = staticmethod(atom(u'\u00C6', doc="AE"))
# 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."))
footnote = staticmethod(inline_format(FOOTNOTE, doc="Footnote."))
Tiny = staticmethod(inline_format(SIZE, -5, doc="Small text."))
tiny = staticmethod(inline_format(SIZE, -4, doc="Small text."))
SMALL = staticmethod(inline_format(SIZE, -3, doc="Small text."))
Small = staticmethod(inline_format(SIZE, -2, doc="Small text."))
small = staticmethod(inline_format(SIZE, -1, doc="Small text."))
normalsize = staticmethod(inline_format(SIZE, 0, doc="Normal font size."))
large = staticmethod(inline_format(SIZE, 1, doc="Large text."))
Large = staticmethod(inline_format(SIZE, 2, doc="Rather large text."))
LARGE = staticmethod(inline_format(SIZE, 3, doc="Very large text."))
huge = staticmethod(inline_format(SIZE, 4, doc="Huge text."))
Huge = staticmethod(inline_format(SIZE, 5, doc="Very huge text."))
@staticmethod
def noindent(arglist, content=None):
"""No indentation."""
if content is None:
yield Entity(NOINDENT)
else:
yield Start(NOINDENT)
for t in content:
yield t
yield End(NOINDENT)
@staticmethod
def _eval(arglist):
"""Evaluate the given expression and output its value.
E.g., {{{<>}}} will output 8."""
result = unicode_eval(u' '.join(arglist))
assert isinstance(result, unicode), result
yield Text(result)
@staticmethod
def raw(arglist):
"""Return a macro's value as a string, unevaluated.
Useful for args of macro-like elements."""
argstr = u' '.join(arglist).strip()
eval_state.memoable_stack[-1].add(('moverride', argstr))
if (argstr in eval_state.moverrides
and hasattr(eval_state.moverrides[argstr], 'raw')):
yield Text(eval_state.moverrides[argstr].raw)
else:
yield Text(unicode_eval(argstr))
@staticmethod
def cache(arglist, content=None, editable=False):
"""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(arglist)
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)
if hasattr(element, 'element'):
# flavors.Reference
for i in xrange(len(element.args)):
let[str(i+1)] = element.args[i]
element = element.element
import conversion
flat = conversion.flatten_filters({'let': let})
ce = conversion.get_from_cache(element.ename, prop_name,
format=eval_state.extension,
cache_tag=flat)
if ce is None:
pv = element.get_propval(prop_name)
if pv is None:
raise WikiException('##%s## has no property ##%s##!'
% (ename, prop_name))
result, deps, metadata = evaluate(pv,
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']
pv = None
for d in deps:
if d == dependencies.DISCORDIA:
eval_state.dependencies.makeUncacheable()
break
elif d == dependencies.OMNISCIENCE:
eval_state.dependencies.makeRestricted()
else:
assert len(d) == 3, d
eval_state.dependencies.addRawDep(*d)
for i in metadata['images']:
eval_state.images.append(i)
if editable:
eval_state.dependencies.makeRestricted()
yield Literal('
')
if isinstance(result, basestring):
yield Literal(result)
else:
# This is a list of arbitrary objects from an ExtractingFormat
for i in result:
yield Literal(i)
if editable:
# This macro/dep should not be in bazbase
from bazki.util import jsarg_escape
if pv is None:
pv = element.get_propval(prop_name)
val = unicode(pv.value, 'utf-8')
yield Literal(u'
'
% (
element.ename, prop_name,
jsarg_escape(val),
element.ename, prop_name))
yield Text(u'edit')
yield Literal('')
@staticmethod
def editable(arglist):
"""Like {{{<>}}}, but adds an edit link."""
editable = eval_state.format == FORMATS['html']
for t in Macros.cache(arglist, editable=editable):
yield t
# TODO(xavid): combine with other foreach syntax
@staticmethod
def foreachpropin(arglist, 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(arglist)
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.list_props():
if pname != get_propname(u'this'):
if flavor is not None:
if structure.get_prop(pname).flavor != flavor:
continue
with poverride({'*' + var: pname}):
for t in content:
yield t
@staticmethod
def foreachimage(arglist, content):
args = parse_macro_args(arglist)
if 1 not in args or 2 not in args:
raise WikiException("{{{foreachimage}}} takes at least two parameters!")
old_images = eval_state.images
eval_state.images = []
propval = baz_eval(args[1])
nam = args[2]
content = list(content)
latched_images = eval_state.images
eval_state.images = old_images
for i in latched_images:
with moverride({nam: i}):
for t in content:
yield t
@staticmethod
def sourceof(arglist):
"""Render the source of the given prop of an element.
Use like {{{<>}}}."""
args = parse_macro_args(arglist)
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(arglist):
"""Render the propname of the given prop; useful in
{{{<>}}}."""
args = parse_macro_args(arglist)
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(arglist, 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(arglist)
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(arglist):
"""Given a built-in macro, render its help text."""
args = parse_macro_args(arglist)
if 1 not in args or len(args) != 1:
raise WikiException(
"##macrodoc## takes only one parameter!")
name = args[1]
eval_state.memoable_stack[-1].add(('moverride', name))
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, makeRestricted):
yield t
@staticmethod
def macro(arglist, content=None):
"""Evaluate the built-in macro named by the given expression."""
argstr = u' '.join(arglist).strip()
if ' ' in argstr:
name, argstr = argstr.split(' ', 1)
else:
name = argstr
argstr = ""
eval_state.memoable_stack[-1].add(('moverride', name))
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(arglist, 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(arglist):
"""Evauates to the current year."""
from datetime import datetime
yield Text(unicode(datetime.now().year))
@staticmethod
def label(arglist, content):
if not get_context('in_get_refs'):
lab = tokens_text(content)
assert lab not in eval_state.label_map, (lab, eval_state.label_map)
eval_state.label_map[lab] = eval_state.figure
return ()
@staticmethod
def ref(arglist, content):
lab = tokens_text(content)
yield Entity(REF, lab)
@staticmethod
def subfigure(arglist, content):
import traceback
#traceback.print_stack()
import conversion, translators
ename, pname_with_filters = u' '.join(arglist).split('.', 1)
filters = {}
pname = conversion.extract_filters(pname_with_filters, filters)
width, height = conversion.cache_and_get_dimensions(ename, pname,
filters)
assert width is not None and height is not None, (width, height, ename,
pname, filters)
# TODO(xavid): better understand whether and/or why we need this list()
eval_state.subfigures.append((ename+'.'+pname_with_filters,
width, height, list(content)))
return ()
@staticmethod
def table(arglist, content):
"""Start a table, with various options.
border: include a border, default True.
align: cell alignment, default 'left'.
figure: Treat this as the numbered figure, with each column as
subfigures labeled in the second row.
font: a macro name to wrap each cell with
"""
# TODO(xavid): way to switch into x (equal width) mode (mode: 'equal')
args = parse_macro_args(arglist)
border = baz_eval(args['border']) if 'border' in args else True
align = baz_eval(args['align']) if 'align' in args else 'left'
figure = baz_eval(args['figure']) if 'figure' in args else None
font = baz_eval(args['font']) if 'font' in args else None
if figure is True:
figure = eval_state.last_figure + 1
if not get_context('in_get_refs'):
eval_state.last_figure = figure
eval_state.subfigures = []
params = {}
if not border:
params['border'] = False
if align != 'left':
params['align'] = align
# TODO(xavid): DTWT for nested tables
row = 0
subfigure_ord = ord('a')
yield Start(TABLE, params)
for t in content:
if t.op == END and t.style == TABLE_CELL:
if font:
yield End(MACRO, font)
eval_state.figure = None
yield t
if t.op == START:
if t.style == TABLE_ROW:
row += 1
elif t.style == TABLE_CELL:
if font:
yield Start(MACRO, (font, None))
if row == 2 and figure is not None:
label = u'%s%s' % (figure, unichr(subfigure_ord))
yield Text(u'(%s) ' % (label,))
eval_state.figure = label
subfigure_ord += 1
if len(eval_state.subfigures) > 0:
imgs = []
caps = []
specs = []
for img, width, height, caption in eval_state.subfigures:
imgs.append(img)
specs.append('w{%spt}' % (width * latex.PX_TO_PT + 5))
caps.append(caption)
yield Start(TABLE_ROW)
for img in imgs:
yield Start(TABLE_CELL)
yield Entity(IMAGE, img)
yield End(TABLE_CELL)
yield End(TABLE_ROW)
yield Start(TABLE_ROW)
for cap in caps:
yield Start(TABLE_CELL)
if font:
yield Start(MACRO, (font, None))
label = u'%s%s' % (figure, unichr(subfigure_ord))
yield Text(u'(%s) ' % (label,))
eval_state.figure = label
subfigure_ord += 1
for t in cap:
yield t
if font:
yield End(MACRO, font)
eval_state.figure = None
yield End(TABLE_CELL)
yield End(TABLE_ROW)
params['specs'] = specs
del eval_state.subfigures
yield End(TABLE, params)
@staticmethod
def tablerow(arglist, content):
yield Start(TABLE_ROW)
for t in content:
yield t
yield End(TABLE_ROW)
@staticmethod
def tablecell(arglist, content):
args = parse_macro_args(arglist)
start_dict = {}
if 'colspan' in args:
start_dict['colspan'] = baz_eval(args['colspan'])
yield Start(TABLE_CELL, start_dict)
for t in content:
yield t
yield End(TABLE_CELL)
_1 = staticmethod(arg_index(1))
_2 = staticmethod(arg_index(2))
_3 = staticmethod(arg_index(3))
_4 = staticmethod(arg_index(4))
_5 = staticmethod(arg_index(5))
_6 = staticmethod(arg_index(6))
_7 = staticmethod(arg_index(7))
_8 = staticmethod(arg_index(8))
_9 = staticmethod(arg_index(9))
def link_func(href, sty=LINK):
implicit = False
if (':' not in href and not href.startswith('/')) or href[0].isupper():
if '/' not in href and ' ' not in href:
# Normal context-based interpretation
if (eval_state.topthis is not None
and structure.get_prop(eval_state.topthis).visible):
href = 'get:'+href
else:
href = 'edit:'+href
implicit = True
elif '/' in href and (' ' not in href
or href.find(' ') > href.find('/')):
href = 'get:'+href
implicit = True
if ':' in href:
pref,rest = href.split(':',1)
rest = rest.strip()
if pref in ('get','edit'):
ext = None
if '.' in rest and not rest.endswith('.'):
ename,pname = rest.split('.',1)
if '.' in pname:
pname, ext = pname.rsplit('.', 1)
if ext[0].isdigit() and ext.endswith('in'):
pname = pname + '.' + ext
ext = None
else:
ext = '.' + ext
else:
ename = rest
pname = None
args = None
render_func = render_propval
if '/' in ename:
args = ename.split('/')
ename = args.pop(0)
mover = {}
for i in xrange(len(args)):
mover[unicode(i+1)] = args[i]
def render_with_content(element, prop_name):
return render_propval(element, prop_name,
moverrides=mover)
render_func = render_with_content
if ename in eval_state.eoverrides:
elm = eval_state.eoverrides[ename]
if hasattr(elm, 'element'):
# Reference
# TODO(xavid): how should args/owner be handled here?
elm = elm.element
ename = elm.ename
else:
elm = structure.get_element(ename)
if sty == IMAGE:
eval_state.images.append('%s.%s' % (ename, pname))
with push_current(elm):
if pref == 'get':
text, info = custom.product_link(
ename, pname, eval_state.dependencies,
render_func, extension=ext, args=args)
elif pref == 'edit':
text, info = custom.edit_link(
ename, pname, eval_state.dependencies,
render_func, extension=ext)
if ename not in eval_state.mentions:
eval_state.mentions.append(ename)
return text, info
elif not rest.startswith('//'):
# Check interwiki
iw = structure.get_element(u'InterwikiConfig')
if iw is None:
eval_state.dependencies.addExistsDep(u'InterwikiConfig')
else:
eval_state.dependencies.addDep(iw.get_propval(u'prefix'))
eval_state.dependencies.addDep(iw.get_propval(u'url'))
if iw.has_propval(u'prefix') and iw.has_propval(u'url'):
for i in _list_descendants(iw):
eval_state.dependencies.addDep(i.get_propval(u'prefix'))
if unicode(i.get_prop(u'prefix'), 'utf-8') == pref:
eval_state.dependencies.addDep(i.get_propval(u'url'))
return (href, dict(url=unicode(i.get_prop(u'url'),
'utf-8')+rest,
style='external'))
# None matched
if href.startswith('/'):
abshref = custom.local_link(href)
style = 'internal'
else:
abshref = href
if '://' in href:
style = 'external'
else:
style = 'internal'
return href, dict(url=abshref, style=style)
def _error(message):
if eval_state.catch_errors:
yield Error('[error]')
else:
eval_state.dependencies.makeRestricted()
eval_state.errors.append(message)
yield Start(ERROR)
for t in tokenize(message, makeRestricted):
yield t
yield End(ERROR)
MACRO_NAME_PAT = re.compile('^([^\s#]+)(?:\#\S+)?')
def macro_func(name, arglist=[], content=None):
#print >>sys.stderr, "macro_func", name, arglist, content
try:
# Allow macros to have #foo suffix, to allow nesting
m=MACRO_NAME_PAT.match(name)
if m is None:
raise WikiException("Weird macro name '%s'!" % name)
name = m.group(1)
origname = name
arglist = arglist or []
maced = False
if '.' not in name and not name[0].isupper() and not arglist:
eval_state.memoable_stack[-1].add(('moverride', name))
overridable = True
else:
overridable = False
if ('.' not in name
and (name[0].isupper()
or (overridable and name not in eval_state.moverrides))
and not hasattr(macros,name)
and not hasattr(macros,'_'+name)):
if get_overridden_element(name) is None:
curr = get_current_for_prop(name)
if curr is not None:
assert curr.has_propval(name)
eval_state.dependencies.addExistsDep(name)
elm,pnam = curr, name
else:
elm = name
pnam = custom.SUBSTITUTION_PROP
else:
elm = name
pnam = custom.SUBSTITUTION_PROP
maced=True
elif '.' in name:
elm,pnam = name.strip().rsplit('.', 1)
if '.' in elm:
# This could be a reference, where foo.owner itself refers
# to an element.
elm = baz_eval(elm)
if elm is None:
raise WikiException("%s evaluates to None!" % name)
else:
# Non-element-reference case
if (not name[0].isupper() and overridable
and name in eval_state.moverrides):
ove = eval_state.moverrides[name]
if hasattr(ove, '__call__'):
for t in ove(arglist, content):
yield t
elif ove is None:
pass
else:
yield Text(ove)
return
if hasattr(macros,'_'+name):
name = '_'+name
if hasattr(macros, name):
if content is not None:
gen = getattr(macros,name)(arglist=arglist,content=content)
else:
gen = getattr(macros,name)(arglist=arglist)
if not hasattr(gen, '__iter__'):
for e in _error(
'Error: {{{<<%s %s>>}}} return non-iterator "%s"!'
% (name, u' '.join(arglist), repr(gen))):
yield e
return
for t in gen:
yield t
else:
for e in _error('Error: no such macro %s!' % name):
yield e
return # End non-element-reference case
#print >>sys.stderr, "maced:", name
# Element reference case
if pnam != custom.ELEMENT_NAME_PROP:
flav = structure.get_flavor(get_propname(pnam))
assert flav is not None, (pnam, flav)
if flav.is_ref:
elm = to_python(elm, pnam)
if elm is None:
raise WikiException(
"%s is a reference that evaluates to None!" % name)
pnam = custom.SUBSTITUTION_PROP
try:
try:
assert elm is not None, name
r = recurse(elm, pnam, content=content,
arglist=arglist)
for t in r:
yield t
except ElementNotFound:
# This hack is a DWIM for Gameki.
if isinstance(elm, unicode) and len(elm) >= 2 and elm[0].islower() and elm[1].isupper():
r = recurse(elm[1:], pnam, content=content,
arglist=arglist)
for t in r:
yield t
else:
raise
except ElementNotFound:
if maced:
leaf = eval_state.eoverrides[custom.LEAF_ELEMENT]
if leaf is not None:
eval_bit = '%s.%s' % (leaf.ename,
eval_state.poverrides['this'])
else:
eval_bit = 'wikitext'
for e in _error(
"No element or macro ##%s## exists evaluating %s! (Tried %s.)"
% (origname,
eval_bit,
name)):
yield e
else:
assert False, (elm, eval_state.eoverrides)
for e in _error("No element ##%s## exists!" % elm):
yield e
except NoContentException:
raise
except WikiException,e:
for e in _error(e.args[0]):
yield e
# Set or add to this to change what macros are available
macros = Macros()
# These accent macrs can't be set directly
def accent(subst):
def amac(arglist, content):
s = tokens_text(content)
import unicodedata
# TODO(xavid): use XeLaTeX, which handles unicode better, and stop
# doing this.
yield Text(unicodedata.normalize('NFC', s+subst))
return amac
ACCENT_MACROS = {"'": u'\u0301',
'`': u'\u0300',
'"': u'\u0308',
'~': u'\u0303',
'=': u'\u0304',
}
for sym in ACCENT_MACROS:
setattr(macros, sym, accent(ACCENT_MACROS[sym]))
# 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.get_propval(propname))
eval_state.me = element
eval_state.eoverrides = {
custom.CURRENT_ELEMENT: element,
custom.LEAF_ELEMENT: element
}
if element is None:
eval_state.current_stack = []
else:
eval_state.current_stack = [element]
eval_state.moverrides = {u'args': u""}
eval_state.poverrides = {u'this': propname}
for k,v in let.items():
if hasattr(v, 'ename'):
eval_state.eoverrides[k] = v
else:
assert isinstance(v, unicode), let
eval_state.moverrides[k] = v
eval_state.images = []
eval_state.mentions = []
eval_state.errors = []
eval_state.context = {}
eval_state.memoable_stack = [set()]
eval_state.last_figure = 0
eval_state.label_map = {}
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
# See placeholder() in redbeans/formats.py.
PLACEHOLDER_PAT = re.compile('<<<([^>]+)>>>')
def unplaceholder(match):
try:
return eval_state.label_map[match.group(1)]
except KeyError:
#assert False, (match.group(), match.group(1))
return "???"
def evaluate(thing, extension, toPython=False, let={}, element=None,
flavor=None, reentrant=False, catchErrors=False):
if hasattr(thing, 'element'):
element = thing.element
propval = thing
propname = propval.propname
assert element is not None
assert propname is not None
assert propval.value is not None, propval
if flavor is None:
flavor = structure.get_flavor(propval.propname)
if flavor.binary:
wikitext = propval.value
else:
wikitext = unicode(propval.value, 'utf-8')
prop_owned = flavor.owned
else:
propval = None
propname = None
wikitext = thing
prop_owned = False
assert flavor is not None
if not reentrant:
assert not hasattr(eval_state, 'dependencies'), repr(eval_state.__dict__)
else:
old_eval_state = dict(eval_state.__dict__)
init_eval_state(element, propname, extension, flavor, let,
catch_errors=catchErrors)
if prop_owned:
eval_state.eoverrides[u'owner'] = element
try:
desc = ("%s.%s" % (propval.element.ename, propval.propname)
if propval is not None else 'wikitext')
if not toPython:
with benchmarking('evaluating '+desc):
res = flavor.evaluate(wikitext, eval_state.mainparser,
propval=propval)
# It might not be a string for an ExtractingFormat.
if isinstance(res, basestring):
res = PLACEHOLDER_PAT.sub(unplaceholder, res)
else:
with benchmarking('python-converting '+desc):
res = flavor.toPython(wikitext, eval_state.mainparser,
propval=propval)
except NoContentException,e:
raise
if toPython:
raise
makeRestricted()
res = eval_state.mainparser.render([
Text(u"Content unspecified, raw markup: "),
Start(CODEBLOCK),
Text(wikitext),
End(CODEBLOCK)])
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
errors = eval_state.errors
clear_eval_state()
if reentrant:
eval_state.__dict__.update(old_eval_state)
# rendered result, dependencies, metadata
metadata = {'images': images}
if errors:
metadata['errors'] = errors
return res, deps, metadata
def clear_eval_state():
eval_state.__dict__.clear()
def get_eval_state_delta(which, text_or_elm, propname=None):
if propname is not None:
assert not structure.get_flavor(propname).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_format():
return eval_state.format
def get_topthis():
return eval_state.topthis
def addDeps(deps):
eval_state.dependencies.update(deps)
def makeRestricted():
eval_state.dependencies.makeRestricted()