Merge branch 'master' into with-dvcs
[redakcja.git] / apps / catalogue / xml_tools.py
1 from functools import wraps
2 import re
3
4 from lxml import etree
5 from catalogue.constants import TRIM_BEGIN, TRIM_END, MASTERS
6
7 RE_TRIM_BEGIN = re.compile("^<!--%s-->$" % TRIM_BEGIN, re.M)
8 RE_TRIM_END = re.compile("^<!--%s-->$" % TRIM_END, re.M)
9
10
11 class ParseError(BaseException):
12     pass
13
14
15 def obj_memoized(f):
16     """
17         A decorator that caches return value of object methods.
18         The cache is kept with the object, in a _obj_memoized property.
19     """
20     @wraps(f)
21     def wrapper(self, *args, **kwargs):
22         if not hasattr(self, '_obj_memoized'):
23             self._obj_memoized = {}
24         key = (f.__name__,) + args + tuple(sorted(kwargs.iteritems()))
25         try:
26             return self._obj_memoized[key]
27         except TypeError:
28             return f(self, *args, **kwargs)
29         except KeyError:
30             self._obj_memoized[key] = f(self, *args, **kwargs)
31             return self._obj_memoized[key]
32     return wrapper
33
34
35 class GradedText(object):
36     _edoc = None
37
38     ROOT = 'utwor'
39     RDF = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}RDF'
40
41     def __init__(self, text):
42         self._text = text
43
44     @obj_memoized
45     def is_xml(self):
46         """
47             Determines if it's a well-formed XML.
48
49             >>> GradedText("<a/>").is_xml()
50             True
51             >>> GradedText("<a>").is_xml()
52             False
53         """
54         try:
55             self._edoc = etree.fromstring(self._text)
56         except etree.XMLSyntaxError:
57             return False
58         return True
59
60     @obj_memoized
61     def is_wl(self):
62         """
63             Determines if it's an XML with a <utwor> and a master tag.
64
65             >>> GradedText("<utwor><powiesc></powiesc></utwor>").is_wl()
66             True
67             >>> GradedText("<a></a>").is_wl()
68             False
69         """
70         if self.is_xml():
71             e = self._edoc
72             # FIXME: there could be comments
73             ret = e.tag == self.ROOT and (
74                 len(e) == 1 and e[0].tag in MASTERS or
75                 len(e) == 2 and e[0].tag == self.RDF 
76                     and e[1].tag in MASTERS)
77             if ret:
78                 self._master = e[-1].tag
79             del self._edoc
80             return ret
81         else:
82             return False
83
84     @obj_memoized
85     def is_broken_wl(self):
86         """
87             Determines if it at least looks like broken WL file
88             and not just some untagged text.
89
90             >>> GradedText("<utwor><</utwor>").is_broken_wl()
91             True
92             >>> GradedText("some text").is_broken_wl()
93             False
94         """
95         if self.is_wl():
96             return True
97         text = self._text.strip()
98         return text.startswith('<utwor>') and text.endswith('</utwor>')
99
100     def master(self):
101         """
102             Gets the master tag.
103
104             >>> GradedText("<utwor><powiesc></powiesc></utwor>").master()
105             'powiesc'
106         """
107         assert self.is_wl()
108         return self._master
109
110     @obj_memoized
111     def has_trim_begin(self):
112         return RE_TRIM_BEGIN.search(self._text)
113
114     @obj_memoized
115     def has_trim_end(self):
116         return RE_TRIM_END.search(self._text)
117
118
119 def _trim(text, trim_begin=True, trim_end=True):
120     """ 
121         Cut off everything before RE_TRIM_BEGIN and after RE_TRIM_END, so
122         that eg. one big XML file can be compiled from many small XML files.
123     """
124     if trim_begin:
125         text = RE_TRIM_BEGIN.split(text, maxsplit=1)[-1]
126     if trim_end:
127         text = RE_TRIM_END.split(text, maxsplit=1)[0]
128     return text
129
130
131 def compile_text(parts):
132     """ 
133         Compiles full text from an iterable of parts,
134         trimming where applicable.
135     """
136     texts = []
137     trim_begin = False
138     text = ''
139     for next_text in parts:
140         if not next_text:
141             continue
142         # trim the end, because there's more non-empty text
143         # don't trim beginning, if `text' is the first non-empty part
144         texts.append(_trim(text, trim_begin=trim_begin))
145         trim_begin = True
146         text = next_text
147     # don't trim the end, because there's no more text coming after `text'
148     # only trim beginning if it's not still the first non-empty
149     texts.append(_trim(text, trim_begin=trim_begin, trim_end=False))
150     return "".join(texts)
151
152
153 def change_master(text, master):
154     """
155         Changes the master tag in a WL document.
156     """
157     e = etree.fromstring(text)
158     e[-1].tag = master
159     return etree.tostring(e, encoding="utf-8")
160
161
162 def basic_structure(text, master):
163     e = etree.fromstring('''<utwor>
164 <master>
165 <!--%s--><!--%s-->
166 </master>
167 </utwor>''' % (TRIM_BEGIN, TRIM_END))
168     e[0].tag = master
169     e[0][0].tail = "\n"*3 + text + "\n"*3
170     return etree.tostring(e, encoding="utf-8")
171
172
173 def add_trim_begin(text):
174     trim_tag = etree.Comment(TRIM_BEGIN)
175     e = etree.fromstring(text)
176     for master in e[::-1]:
177         if master.tag in MASTERS:
178             break
179     if master.tag not in MASTERS:
180         raise ParseError('No master tag found!')
181
182     master.insert(0, trim_tag)
183     trim_tag.tail = '\n\n\n' + (master.text or '')
184     master.text = '\n'
185     return etree.tostring(e, encoding="utf-8")
186
187
188 def add_trim_end(text):
189     trim_tag = etree.Comment(TRIM_END)
190     e = etree.fromstring(text)
191     for master in e[::-1]:
192         if master.tag in MASTERS:
193             break
194     if master.tag not in MASTERS:
195         raise ParseError('No master tag found!')
196
197     master.append(trim_tag)
198     trim_tag.tail = '\n'
199     prev = trim_tag.getprevious()
200     if prev is not None:
201         prev.tail = (prev.tail or '') + '\n\n\n'
202     else:
203         master.text = (master.text or '') + '\n\n\n'
204     return etree.tostring(e, encoding="utf-8")