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