Merge branch 'universal' into edumed-ofop
[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         for flt in self.text_filters:
26             if text is None:
27                 return None
28             text = flt(text)
29         return text
30
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])
35
36     @property
37     def options(self):
38         """Returnes merged scoped options for current node.
39         """
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))
43
44     @options.setter
45     def options(self, opts):
46         """Sets options overrides for current and child nodes
47         """
48         self._options.append(opts)
49
50
51     def _handle_for_element(self, element):
52         ns = None
53         tagname = None
54 #        from nose.tools import set_trace
55
56         if isinstance(element, etree._Comment): return None
57
58         if element.tag[0] == '{':
59             for nshort, nhref in element.nsmap.items():
60                 try:
61                     if element.tag.index('{%s}' % nhref) == 0:
62                         ns = nshort
63                         tagname  = element.tag[len('{%s}' % nhref):]
64                         break
65                 except ValueError:
66                     pass
67             if not ns:
68                 raise ValueError("Strange ns for tag: %s, nsmap: %s" %
69                                  (element.tag, element.nsmap))
70         else:
71             tagname = element.tag
72
73         if ns:
74             meth_name = "handle_%s__%s" % (ns, tagname)
75         else:
76             meth_name = "handle_%s" % (tagname,)
77
78         handler = getattr(self, meth_name, None)
79         return handler
80
81     def next(self, element):
82         if len(element):
83             return element[0]
84
85         while True:
86             sibling = element.getnext()
87             if sibling is not None: return sibling  # found a new branch to dig into
88             element = element.getparent()
89             if element is None: return None  # end of tree
90
91     def _handle_element(self, element):
92         handler = self._handle_for_element(element)
93         # How many scopes
94         try:
95             options_scopes = len(self._options)
96
97             if handler is None:
98                 pre = [self.filter_text(element.text)]
99                 post = []
100             else:
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.
104                 if vals is None:
105                     return []
106                 else:
107                     if not isinstance(vals, tuple):
108                         return [vals, self.filter_text(element.tail)]
109                     else:
110                         pre = [vals[0], self.filter_text(element.text)]
111                         post = [vals[1], self.filter_text(element.tail)]
112
113             out = pre + [self._handle_element(child) for child in element] + post
114         finally:
115             # clean up option scopes if necessary
116             self._options = self._options[0:options_scopes]
117         return out
118
119
120 def tag(name, classes=None, **attrs):
121     """Returns a handler which wraps node contents in tag `name', with class attribute
122     set to `classes' and other attributes according to keyword paramters
123     """
124     if classes:
125         if isinstance(classes, (tuple, list)): classes = ' '.join(classes)
126         attrs['class'] = classes
127     a = ''.join([' %s="%s"' % (k,v) for (k,v) in attrs.items()])
128     def _hnd(self, element):
129         return "<%s%s>" % (name, a), "</%s>" % name
130     return _hnd
131
132
133 def tagged(name, classes=None, **attrs):
134     """Handler decorator which wraps handler output in tag `name', with class attribute
135     set to `classes' and other attributes according to keyword paramters
136     """
137     if classes:
138         if isinstance(classes, (tuple,list)): classes = ' '.join(classes)
139         attrs['class'] = classes
140     a = ''.join([' %s="%s"' % (k,v) for (k,v) in attrs.items()])
141     def _decor(f):
142         def _wrap(self, element):
143             r = f(self, element)
144             if r is None: return
145
146             prepend = "<%s%s>" % (name, a)
147             append = "</%s>" % name
148
149             if isinstance(r, tuple):
150                 return prepend + r[0], r[1] + append
151             return prepend + r + append
152         return _wrap
153     return _decor
154
155
156 def ifoption(**options):
157     """Decorator which enables node only when options are set
158     """
159     def _decor(f):
160         def _handler(self, *args, **kw):
161             opts = self.options
162             for k, v in options.items():
163                 if opts[k] != v:
164                     return
165             return f(self, *args, **kw)
166         return _handler
167     return _decor
168
169 def flatten(l, ltypes=(list, tuple)):
170     """flatten function from BasicPropery/BasicTypes package
171     """
172     ltype = type(l)
173     l = list(l)
174     i = 0
175     while i < len(l):
176         while isinstance(l[i], ltypes):
177             if not l[i]:
178                 l.pop(i)
179                 i -= 1
180                 break
181             else:
182                 l[i:i + 1] = l[i]
183         i += 1
184     return ltype(l)