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