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