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