excercises: uporzadkuj, zastap, luki
[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, element.tail]
98                     else:
99                         pre = [vals[0], element.text]
100                         post = [vals[1], element.tail]
101
102             out = pre + [self._handle_element(child) for child in element] + post
103         finally:
104             # clean up option scopes if necessary
105             self._options = self._options[0:options_scopes]
106         return out
107
108
109 def tag(name, classes=None, **attrs):
110     """Returns a handler which wraps node contents in tag `name', with class attribute
111     set to `classes' and other attributes according to keyword paramters
112     """
113     if classes:
114         if isinstance(classes, (tuple, list)): classes = ' '.join(classes)
115         attrs['class'] = classes
116     a = ''.join([' %s="%s"' % (k,v) for (k,v) in attrs.items()])
117     def _hnd(self, element):
118         return "<%s%s>" % (name, a), "</%s>" % name
119     return _hnd
120
121
122 def tagged(name, classes=None, **attrs):
123     """Handler decorator which wraps handler output in tag `name', with class attribute
124     set to `classes' and other attributes according to keyword paramters
125     """
126     if classes:
127         if isinstance(classes, (tuple,list)): classes = ' '.join(classes)
128         attrs['class'] = classes
129     a = ''.join([' %s="%s"' % (k,v) for (k,v) in attrs.items()])
130     def _decor(f):
131         def _wrap(self, element):
132             r = f(self, element)
133             if r is None: return
134
135             prepend = "<%s%s>" % (name, a)
136             append = "</%s>" % name
137
138             if isinstance(r, tuple):
139                 return prepend + r[0], r[1] + append
140             return prepend + r + append
141         return _wrap
142     return _decor
143
144
145 def ifoption(**options):
146     """Decorator which enables node only when options are set
147     """
148     def _decor(f):
149         def _handler(self, *args, **kw):
150             opts = self.options
151             for k, v in options.items():
152                 if opts[k] != v:
153                     return
154             return f(self, *args, **kw)
155         return _handler
156     return _decor
157
158 def flatten(l, ltypes=(list, tuple)):
159     """flatten function from BasicPropery/BasicTypes package
160     """
161     ltype = type(l)
162     l = list(l)
163     i = 0
164     while i < len(l):
165         while isinstance(l[i], ltypes):
166             if not l[i]:
167                 l.pop(i)
168                 i -= 1
169                 break
170             else:
171                 l[i:i + 1] = l[i]
172         i += 1
173     return ltype(l)