8890f4e8867ad3cde5b37bbb11b010882dfb9b04
[librarian.git] / librarian / xmlutils.py
1 # -*- coding: utf-8 -*-
2 #
3 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
5 #
6 from lxml import etree
7 from collections import defaultdict
8
9
10 class Xmill(object):
11     """Transforms XML to some text.
12     Used instead of XSLT which is difficult and cumbersome.
13
14     """
15     def __init__(self, options=None):
16         self._options = []
17         if options:
18             self._options.append(options)
19         self.text_filters = []
20
21     def register_text_filter(self, fun):
22         self.text_filters.append(fun)
23
24     def filter_text(self, text):
25         # TODO: just work on the tree and let lxml handle escaping.
26         e = etree.Element("x")
27         e.text = text
28         # This whole mixing text with ML is so wrong.
29         output = etree.tostring(e, encoding=unicode)[3:-4]
30         for flt in self.text_filters:
31             output = flt(output)
32         return output
33
34
35     def generate(self, document):
36         """Generate text from node using handlers defined in class."""
37         output = self._handle_element(document)
38         return u''.join([x for x in flatten(output) if x is not None])
39
40     @property
41     def options(self):
42         """Returnes merged scoped options for current node.
43         """
44         # Here we can see how a decision not to return the modified map
45         # leads to a need for a hack.
46         return reduce(lambda a, b: a.update(b) or a, self._options, defaultdict(lambda: None))
47
48     @options.setter
49     def options(self, opts):
50         """Sets options overrides for current and child nodes
51         """
52         self._options.append(opts)
53
54
55     def _handle_for_element(self, element):
56         ns = None
57         tagname = None
58 #        from nose.tools import set_trace
59
60         if element.tag[0] == '{':
61             for nshort, nhref in element.nsmap.items():
62                 try:
63                     if element.tag.index('{%s}' % nhref) == 0:
64                         ns = nshort
65                         tagname  = element.tag[len('{%s}' % nhref):]
66                         break
67                 except ValueError:
68                     pass
69             if not ns:
70                 raise ValueError("Strange ns for tag: %s, nsmap: %s" %
71                                  (element.tag, element.nsmap))
72         else:
73             tagname = element.tag
74
75         if ns:
76             meth_name = "handle_%s__%s" % (ns, tagname)
77         else:
78             meth_name = "handle_%s" % (tagname,)
79
80         handler = getattr(self, meth_name, None)
81         return handler
82
83     def next(self, element):
84         if len(element):
85             return element[0]
86
87         while True:
88             sibling = element.getnext()
89             if sibling is not None: return sibling  # found a new branch to dig into
90             element = element.getparent()
91             if element is None: return None  # end of tree
92
93     def _handle_element(self, element):
94         if isinstance(element, etree._Comment): return None
95         
96         handler = self._handle_for_element(element)
97         # How many scopes
98         try:
99             options_scopes = len(self._options)
100
101             if handler is None:
102                 pre = [self.filter_text(element.text)]
103                 post = [self.filter_text(element.tail)]
104             else:
105                 vals = handler(element)
106                 # depending on number of returned values, vals can be None, a value, or a tuple.
107                 # how poorly designed is that? 9 lines below are needed just to unpack this.
108                 if vals is None:
109                     return [self.filter_text(element.tail)]
110                 else:
111                     if not isinstance(vals, tuple):
112                         return [vals, self.filter_text(element.tail)]
113                     else:
114                         pre = [vals[0], self.filter_text(element.text)]
115                         post = [vals[1], self.filter_text(element.tail)]
116
117             out = pre + [self._handle_element(child) for child in element] + post
118         finally:
119             # clean up option scopes if necessary
120             self._options = self._options[0:options_scopes]
121         return out
122
123
124 def tag_open_close(name_, classes_=None, **attrs):
125     u"""Creates tag beginning and end.
126     
127     >>> tag_open_close("a", "klass", x=u"ą<")
128     (u'<a x="\\u0105&lt;" class="klass">', u'</a>')
129
130     """
131     if classes_:
132         if isinstance(classes_, (tuple, list)): classes_ = ' '.join(classes_)
133         attrs['class'] = classes_
134
135     e = etree.Element(name_)
136     e.text = " "
137     for k, v in attrs.items():
138         e.attrib[k] = v
139     pre, post = etree.tostring(e, encoding=unicode).split(u"> <")
140     return pre + u">", u"<" + post
141
142 def tag(name_, classes_=None, **attrs):
143     """Returns a handler which wraps node contents in tag `name', with class attribute
144     set to `classes' and other attributes according to keyword paramters
145     """
146     def _hnd(self, element):
147         return tag_open_close(name_, classes_, **attrs)
148     return _hnd
149
150
151 def tagged(name, classes=None, **attrs):
152     """Handler decorator which wraps handler output in tag `name', with class attribute
153     set to `classes' and other attributes according to keyword paramters
154     """
155     if classes:
156         if isinstance(classes, (tuple,list)): classes = ' '.join(classes)
157         attrs['class'] = classes
158     a = ''.join([' %s="%s"' % (k,v) for (k,v) in attrs.items()])
159     def _decor(f):
160         def _wrap(self, element):
161             r = f(self, element)
162             if r is None: return
163
164             prepend = "<%s%s>" % (name, a)
165             append = "</%s>" % name
166
167             if isinstance(r, tuple):
168                 return prepend + r[0], r[1] + append
169             return prepend + r + append
170         return _wrap
171     return _decor
172
173
174 def ifoption(**options):
175     """Decorator which enables node only when options are set
176     """
177     def _decor(f):
178         def _handler(self, *args, **kw):
179             opts = self.options
180             for k, v in options.items():
181                 if opts[k] != v:
182                     return
183             return f(self, *args, **kw)
184         return _handler
185     return _decor
186
187 def flatten(l, ltypes=(list, tuple)):
188     """flatten function from BasicPropery/BasicTypes package
189     """
190     ltype = type(l)
191     l = list(l)
192     i = 0
193     while i < len(l):
194         while isinstance(l[i], ltypes):
195             if not l[i]:
196                 l.pop(i)
197                 i -= 1
198                 break
199             else:
200                 l[i:i + 1] = l[i]
201         i += 1
202     return ltype(l)