1 # -*- coding: utf-8 -*-
3 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
7 from collections import defaultdict
11 """Transforms XML to some text.
12 Used instead of XSLT which is difficult and cumbersome.
15 def __init__(self, options=None):
18 self._options.append(options)
19 self.text_filters = []
21 def register_text_filter(self, fun):
22 self.text_filters.append(fun)
24 def filter_text(self, text):
25 for flt in self.text_filters:
31 def generate(self, document):
32 """Generate text from node using handlers defined in class."""
33 output = self._handle_element(document)
34 return u''.join([x for x in flatten(output) if x is not None])
38 """Returnes merged scoped options for current node.
40 # Here we can see how a decision not to return the modified map
41 # leads to a need for a hack.
42 return reduce(lambda a, b: a.update(b) or a, self._options, defaultdict(lambda: False))
45 def options(self, opts):
46 """Sets options overrides for current and child nodes
48 self._options.append(opts)
51 def _handle_for_element(self, element):
54 # from nose.tools import set_trace
56 if element.tag[0] == '{':
57 for nshort, nhref in element.nsmap.items():
59 if element.tag.index('{%s}' % nhref) == 0:
61 tagname = element.tag[len('{%s}' % nhref):]
66 raise ValueError("Strange ns for tag: %s, nsmap: %s" %
67 (element.tag, element.nsmap))
72 meth_name = "handle_%s__%s" % (ns, tagname)
74 meth_name = "handle_%s" % (tagname,)
76 handler = getattr(self, meth_name, None)
79 def next(self, element):
84 sibling = element.getnext()
85 if sibling is not None: return sibling # found a new branch to dig into
86 element = element.getparent()
87 if element is None: return None # end of tree
89 def _handle_element(self, element):
90 if isinstance(element, etree._Comment): return None
92 handler = self._handle_for_element(element)
95 options_scopes = len(self._options)
98 pre = [self.filter_text(element.text)]
99 post = [self.filter_text(element.tail)]
101 vals = handler(element)
102 # depending on number of returned values, vals can be None, a value, or a tuple.
103 # how poorly designed is that? 9 lines below are needed just to unpack this.
105 return [self.filter_text(element.tail)]
107 if not isinstance(vals, tuple):
108 return [vals, self.filter_text(element.tail)]
110 pre = [vals[0], self.filter_text(element.text)]
111 post = [vals[1], self.filter_text(element.tail)]
113 out = pre + [self._handle_element(child) for child in element] + post
115 # clean up option scopes if necessary
116 self._options = self._options[0:options_scopes]
120 def tag_open_close(name_, classes_=None, **attrs):
121 u"""Creates tag beginning and end.
123 >>> tag_open_close("a", "klass", x=u"ą<")
124 (u'<a x="\\u0105<" class="klass">', u'</a>')
128 if isinstance(classes_, (tuple, list)): classes_ = ' '.join(classes_)
129 attrs['class'] = classes_
131 e = etree.Element(name_)
133 for k, v in attrs.items():
135 pre, post = etree.tostring(e, encoding=unicode).split(u"> <")
136 return pre + u">", u"<" + post
138 def tag(name_, classes_=None, **attrs):
139 """Returns a handler which wraps node contents in tag `name', with class attribute
140 set to `classes' and other attributes according to keyword paramters
142 def _hnd(self, element):
143 return tag_open_close(name_, classes_, **attrs)
147 def tagged(name, classes=None, **attrs):
148 """Handler decorator which wraps handler output in tag `name', with class attribute
149 set to `classes' and other attributes according to keyword paramters
152 if isinstance(classes, (tuple,list)): classes = ' '.join(classes)
153 attrs['class'] = classes
154 a = ''.join([' %s="%s"' % (k,v) for (k,v) in attrs.items()])
156 def _wrap(self, element):
160 prepend = "<%s%s>" % (name, a)
161 append = "</%s>" % name
163 if isinstance(r, tuple):
164 return prepend + r[0], r[1] + append
165 return prepend + r + append
170 def ifoption(**options):
171 """Decorator which enables node only when options are set
174 def _handler(self, *args, **kw):
176 for k, v in options.items():
179 return f(self, *args, **kw)
183 def flatten(l, ltypes=(list, tuple)):
184 """flatten function from BasicPropery/BasicTypes package
190 while isinstance(l[i], ltypes):