fixes for edge cases
[librarian.git] / src / librarian / elements / base.py
1 import copy
2 import re
3 from lxml import etree
4 from librarian import dcparser, RDFNS
5 from librarian.util import get_translation
6
7 def last_words(text, n):
8     words = []
9     for w in reversed(text.split()):
10         words.append(w)
11         if len(w) > 2:
12             n -= 1
13             if not n: break
14     if n:
15         return n, text
16     else:
17         return n, ' '.join(reversed(words))
18
19
20 class WLElement(etree.ElementBase):
21     SECTION_PRECEDENCE = None
22     ASIDE = False
23
24     TXT_TOP_MARGIN = 0
25     TXT_BOTTOM_MARGIN = 0
26     TXT_PREFIX = ""
27     TXT_SUFFIX = ""
28
29     HTML_TAG = None
30     HTML_ATTR = {}
31     HTML_CLASS = None
32
33     EPUB_TAG = None
34     EPUB_ATTR = {}
35     EPUB_CLASS = None
36     EPUB_START_CHUNK = False
37    
38     CAN_HAVE_TEXT = True
39     STRIP = False
40
41     text_substitutions = [
42         (u'---', u'—'),
43         (u'--', u'–'),
44         #(u'...', u'…'),  # Temporary turnoff for epub
45         (u',,', u'„'),
46         (u'"', u'”'),
47         ('\ufeff', ''),
48
49         ("'", "\u2019"),    # This was enabled for epub.
50     ]
51
52     @property
53     def meta_object(self):
54         if not hasattr(self, '_meta_object'):
55             elem = self.find(RDFNS('RDF'))
56             if elem is not None:
57                 self._meta_object = dcparser.BookInfo.from_element(elem)
58             else:
59                 self._meta_object = None
60         return self._meta_object
61
62     @property
63     def meta(self):
64         if self.meta_object is not None:
65             return self.meta_object
66         else:
67             if self.getparent() is not None:
68                 return self.getparent().meta
69             else:
70                 return self.document.base_meta
71
72     @property
73     def gettext(self):
74         return get_translation(self.meta.language).gettext
75
76     def in_context_of(self, setting):
77         parent = self.getparent()
78         if parent is None:
79             return False
80         try:
81             return getattr(parent, setting)
82         except AttributeError:
83             return parent.in_context_of(setting)
84
85     def signal(self, signal):
86         parent = self.getparent()
87         if parent is not None:
88             parent.signal(signal)
89     
90     def raw_printable_text(self, builder):
91         from librarian.html import raw_printable_text
92
93         # TODO: podtagi, wyroznienia, etc
94         t = ''
95         t += self.normalize_text(self.text, builder)
96         for c in self:
97             if not isinstance(c, WLElement):
98                 continue
99             if c.tag not in ('pe', 'pa', 'pt', 'pr', 'motyw'):
100                 t += c.raw_printable_text(builder)
101             t += self.normalize_text(c.tail, builder)
102         return t
103     
104     def normalize_text(self, text, builder):
105         text = text or ''
106         for e, s in self.text_substitutions:
107             text = text.replace(e, s)
108             # FIXME: TEmporary turnoff
109 #        text = re.sub(r'\s+', ' ', text)
110 ### TODO: Added now for epub
111
112         if getattr(builder, 'hyphenator', None) is not None:
113             newt = ''
114             wlist = re.compile(r'\w+|[^\w]', re.UNICODE).findall(text)
115             for w in wlist:
116                 newt += builder.hyphenator.inserted(w, u'\u00AD')
117             text = newt
118
119         if builder.orphans:
120             text = re.sub(r'(?<=\s\w)\s+', u'\u00A0', text)
121
122         return text
123
124     def _build_inner(self, builder, build_method):
125         child_count = len(self)
126         if self.CAN_HAVE_TEXT and self.text:
127             text = self.normalize_text(self.text, builder)
128             if self.STRIP:
129                 text = text.lstrip()
130                 if not child_count:
131                     text = text.rstrip()
132             builder.push_text(text)
133         for i, child in enumerate(self):
134             if isinstance(child, WLElement):
135                 getattr(child, build_method)(builder)
136             if self.CAN_HAVE_TEXT and child.tail:
137                 text = self.normalize_text(child.tail, builder)
138                 if self.STRIP and i == child_count - 1:
139                     text = text.rstrip()
140                 builder.push_text(text)
141
142     def _txt_build_inner(self, builder):
143         self._build_inner(builder, 'txt_build')
144
145     def txt_build(self, builder):
146         if hasattr(self, 'TXT_LEGACY_TOP_MARGIN'):
147             builder.push_legacy_margin(self.TXT_LEGACY_TOP_MARGIN)
148         else:
149             builder.push_margin(self.TXT_TOP_MARGIN)
150         builder.push_text(self.TXT_PREFIX, True)
151         self._txt_build_inner(builder)
152         builder.push_text(self.TXT_SUFFIX, True)
153         if hasattr(self, 'TXT_LEGACY_BOTTOM_MARGIN'):
154             builder.push_legacy_margin(self.TXT_LEGACY_BOTTOM_MARGIN)
155         else:
156             builder.push_margin(self.TXT_BOTTOM_MARGIN)
157
158     def _html_build_inner(self, builder):
159         self._build_inner(builder, 'html_build')
160
161     def get_html_attr(self, builder):
162         attr = self.HTML_ATTR.copy()
163         if self.HTML_CLASS:
164             attr['class'] = self.HTML_CLASS
165         # always copy the id attribute (?)
166         if self.attrib.get('id'):
167             attr['id'] = self.attrib['id']
168         elif getattr(self, 'SHOULD_HAVE_ID', False) and '_compat_section_id' in self.attrib:
169             attr['id'] = self.attrib['_compat_section_id']
170         return attr
171
172     def html_build(self, builder):
173         if self.HTML_TAG:
174             builder.start_element(
175                 self.HTML_TAG,
176                 self.get_html_attr(builder),
177             )
178
179         self._html_build_inner(builder)
180         if self.HTML_TAG:
181             builder.end_element()
182
183     def _epub_build_inner(self, builder):
184         self._build_inner(builder, 'epub_build')
185
186     def get_epub_attr(self, builder):
187         attr = self.EPUB_ATTR.copy()
188         if self.EPUB_CLASS:
189             attr['class'] = self.EPUB_CLASS
190         return attr
191
192     def epub_build(self, builder):
193         from librarian.elements.masters import Master
194
195         # TEMPORARY
196         self.CAN_HAVE_TEXT = True
197         self.STRIP = False
198        
199         start_chunk = self.EPUB_START_CHUNK and isinstance(self.getparent(), Master)
200
201         if start_chunk:
202             builder.start_chunk()
203
204         fragment = None
205         if self.SECTION_PRECEDENCE and not self.in_context_of('NO_TOC'):
206             if not start_chunk:
207                 fragment = 'sub%d' % builder.assign_section_number()
208                 self.attrib['id'] = fragment
209
210             builder.add_toc_entry(
211                 fragment,
212                 self.raw_printable_text(builder),
213                 self.SECTION_PRECEDENCE
214             )
215             
216         if self.EPUB_TAG:
217             attr = self.get_epub_attr(builder)
218             if fragment:
219                 attr['id'] = fragment
220             builder.start_element(
221                 self.EPUB_TAG,
222                 attr
223             )
224
225         self._epub_build_inner(builder)
226         if self.EPUB_TAG:
227             builder.end_element()
228
229     def validate(self):
230         from librarian.elements.masters import Master
231         from librarian.elements.blocks import DlugiCytat, PoezjaCyt
232         from librarian.elements.footnotes import Footnote
233
234         if self.SECTION_PRECEDENCE:
235             assert isinstance(self.getparent(), (Master, DlugiCytat, PoezjaCyt, Footnote)), \
236                     'Header {} inside a <{}> instead of a master.'.format(
237                             etree.tostring(self, encoding='unicode'), self.getparent().tag)
238
239         for c in self:
240             if isinstance(c, WLElement):
241                 c.validate()
242
243
244     def sanitize(self):
245         # TODO: Remove insanity here.
246         for e in self:
247             if isinstance(e, WLElement):
248                 e.sanitize()
249
250     def snip(self, words, before=None, sub=False):
251         if sub and self.ASIDE:
252             return words, []
253
254         snippet = []
255         if before is not None:
256             i = self.index(before)
257         else:
258             i = len(self)
259
260         while i > 0:
261             i -= 1
262             if self[i].tail:
263                 if words:
264                     words, text = last_words(self[i].tail, words)
265                     snippet = [('text', text)] + snippet
266
267             if words:
268                 words, subsnip = self[i].snip(words, sub=True)
269                 snippet = subsnip + snippet
270
271         if words and self.text:
272             words, text = last_words(self.text, words)
273             snippet = [('text', text)] + snippet
274                     
275         snippet = [('start', self.tag, self.attrib)] + snippet + [('end',)]
276
277         if not sub and words and not self.ASIDE:
278             # do we dare go up?
279             parent = self.getparent()
280             if parent is not None and parent.CAN_HAVE_TEXT:
281                 words, parsnip = parent.snip(words, before=self)
282                 return words, parsnip[:-1] + snippet + parsnip[-1:]
283
284         return words, snippet
285
286     def get_snippet(self, words=15):
287         from librarian.parser import parser
288
289         words, snippet = self.getparent().snip(words=words, before=self)
290         
291         cursor = snipelem = parser.makeelement('snippet')
292         snipelem._meta_object = self.meta
293         for s in snippet:
294             if s[0] == 'start':
295                 elem = parser.makeelement(s[1], **s[2])
296                 cursor.append(elem)
297                 cursor = elem
298             elif s[0] == 'end':
299                 cursor = cursor.getparent()
300             else:
301                 if len(cursor):
302                     cursor[-1].tail = (cursor[-1].tail or '') + s[1]
303                 else:
304                     cursor.text = (cursor.text or '') + s[1]
305
306         return snipelem
307
308     def get_link(self):
309         sec = getattr(self, 'SHOULD_HAVE_ID', False) and self.attrib.get('_compat_section_id')
310         if sec:
311             return sec
312         parent_index = self.getparent().index(self)
313         if parent_index:
314             return self.getparent()[parent_index - 1].get_link()
315         else:
316             return self.getparent().get_link()
317
318
319 class Snippet(WLElement):
320     pass