1 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
7 from librarian import dcparser, RDFNS
8 from librarian.util import get_translation
10 def last_words(text, n):
12 for w in reversed(text.split()):
20 return n, ' '.join(reversed(words))
23 class WLElement(etree.ElementBase):
24 SECTION_PRECEDENCE = None
39 EPUB_START_CHUNK = False
47 text_substitutions = [
58 def meta_object(self):
59 if not hasattr(self, '_meta_object'):
60 elem = self.find(RDFNS('RDF'))
62 self._meta_object = dcparser.BookInfo.from_element(elem)
64 self._meta_object = None
65 return self._meta_object
69 if self.meta_object is not None:
70 return self.meta_object
72 if self.getparent() is not None:
73 return self.getparent().meta
75 return self.document.base_meta
79 return get_translation(self.meta.language).gettext
81 def in_context_of(self, setting):
82 parent = self.getparent()
86 return getattr(parent, setting)
87 except AttributeError:
88 return parent.in_context_of(setting)
90 def get_context_map(self, setting, key, default=None):
91 parent = self.getparent()
95 return getattr(parent, setting)[key]
96 except AttributeError:
97 return parent.get_context_map(setting, key, default)
99 def signal(self, signal):
100 parent = self.getparent()
101 if parent is not None:
102 parent.signal(signal)
104 def raw_printable_text(self, builder):
105 from librarian.html import raw_printable_text
107 # TODO: podtagi, wyroznienia, etc
109 t += self.normalize_text(self.text, builder)
111 if not isinstance(c, WLElement):
113 if c.tag not in ('pe', 'pa', 'pt', 'pr', 'motyw'):
114 t += c.raw_printable_text(builder)
115 t += self.normalize_text(c.tail, builder)
118 def normalize_text(self, text, builder):
120 for e, s in self.text_substitutions:
121 text = text.replace(e, s)
123 if getattr(builder, 'normalize_whitespace', False):
124 text = re.sub(r'\s+', ' ', text)
126 if getattr(builder, 'hyphenator', None) is not None:
128 wlist = re.compile(r'\w+|[^\w]', re.UNICODE).findall(text)
130 newt += builder.hyphenator.inserted(w, '\u00AD')
134 text = re.sub(r'(?<=\s\w)\s+', '\u00A0', text)
138 def build_inner(self, builder):
139 build_method = builder.build_method_fn
140 child_count = len(self)
141 if self.CAN_HAVE_TEXT and self.text:
142 text = self.normalize_text(self.text, builder)
147 builder.push_text(text)
148 for i, child in enumerate(self):
150 if isinstance(child, WLElement):
151 getattr(child, build_method)(builder)
152 self.after_child(builder, real_child_count)
153 real_child_count += 1
155 # FIXME base builder api
156 elif getattr(builder, 'debug', False) and child.tag is etree.Comment:
157 builder.process_comment(child)
158 if self.CAN_HAVE_TEXT and child.tail:
159 text = self.normalize_text(child.tail, builder)
160 if self.STRIP and i == child_count - 1:
162 builder.push_text(text)
164 def after_child(self, builder, child_count):
165 fn = getattr(builder, 'after_child_fn', None)
167 getattr(self, builder.after_child_fn)(builder, child_count)
169 def txt_after_child(self, builder, child_count):
172 def txt_build_inner(self, builder):
173 self.build_inner(builder)
175 def txt_build(self, builder):
176 builder.push_margin(self.TXT_TOP_MARGIN)
177 builder.push_text(self.TXT_PREFIX, True)
178 self.txt_build_inner(builder)
179 builder.push_text(self.TXT_SUFFIX, True)
180 builder.push_margin(self.TXT_BOTTOM_MARGIN)
182 def html_build_inner(self, builder):
183 self.build_inner(builder)
185 def get_html_attr(self, builder):
186 attr = self.HTML_ATTR.copy()
188 attr['class'] = self.HTML_CLASS
190 # always copy the id attribute (?)
191 if self.attrib.get('id'):
192 attr['id'] = self.attrib['id']
193 if self.attrib.get('_id'):
194 attr['id'] = self.attrib['_id']
197 def html_build(self, builder):
198 # Do we need a number?
199 numbering = self.numbering
200 if numbering == 'main':
201 if builder.with_numbering and self.has_visible_numbering:
202 builder.add_visible_number(self)
205 builder.start_element(
207 self.get_html_attr(builder),
210 self.html_build_inner(builder)
212 builder.end_element()
214 def fb2_build(self, builder):
215 if self.SECTION_PRECEDENCE:
216 builder.start_section(self.SECTION_PRECEDENCE)
217 builder.start_element('title')
218 builder.start_element('p')
221 builder.start_element(
223 #self.get_fb2_attr(builder),
226 self.build_inner(builder)
228 builder.end_element()
229 if self.SECTION_PRECEDENCE:
230 builder.end_element()
231 builder.end_element()
233 def epub_build_inner(self, builder):
234 self.build_inner(builder)
236 def get_epub_attr(self, builder):
237 attr = self.EPUB_ATTR.copy()
239 attr['class'] = self.EPUB_CLASS
242 def epub_build(self, builder):
243 from librarian.elements.masters import Master
246 self.CAN_HAVE_TEXT = True
249 start_chunk = self.EPUB_START_CHUNK and isinstance(self.getparent(), Master)
252 builder.start_chunk()
255 if self.SECTION_PRECEDENCE and not self.in_context_of('NO_TOC'):
257 fragment = 'sub%d' % builder.assign_section_number()
258 self.attrib['id'] = fragment
260 builder.add_toc_entry(
262 self.raw_printable_text(builder),
263 self.SECTION_PRECEDENCE
267 attr = self.get_epub_attr(builder)
269 attr['id'] = fragment
271 chunkno, sourceline = 0, self.sourceline
273 chunkno, sourceline = len(builder.splits), sourceline - builder.splits[-1]
274 attr['data-debug'] = f'{chunkno}:{sourceline}'
275 builder.start_element(
280 self.epub_build_inner(builder)
282 builder.end_element()
285 from librarian.elements.masters import Master
286 from librarian.elements.blocks import DlugiCytat, PoezjaCyt
287 from librarian.elements.footnotes import Footnote
289 if self.SECTION_PRECEDENCE:
290 assert isinstance(self.getparent(), (Master, DlugiCytat, PoezjaCyt, Footnote)), \
291 'Header {} inside a <{}> instead of a master.'.format(
292 etree.tostring(self, encoding='unicode'), self.getparent().tag)
295 if isinstance(c, WLElement):
300 # TODO: Remove insanity here.
302 if isinstance(e, WLElement):
305 def snip(self, words, before=None, sub=False):
306 if sub and self.ASIDE:
310 if before is not None:
311 i = self.index(before)
319 words, text = last_words(self[i].tail, words)
320 snippet = [('text', text)] + snippet
323 words, subsnip = self[i].snip(words, sub=True)
324 snippet = subsnip + snippet
326 if words and self.text:
327 words, text = last_words(self.text, words)
328 snippet = [('text', text)] + snippet
330 snippet = [('start', self.tag, self.attrib)] + snippet + [('end',)]
332 if not sub and words and not self.ASIDE:
334 parent = self.getparent()
335 if parent is not None and parent.CAN_HAVE_TEXT:
336 words, parsnip = parent.snip(words, before=self)
337 return words, parsnip[:-1] + snippet + parsnip[-1:]
339 return words, snippet
341 def get_snippet(self, words=15):
342 from librarian.parser import parser
344 words, snippet = self.getparent().snip(words=words, before=self)
346 cursor = snipelem = parser.makeelement('snippet')
347 snipelem._meta_object = self.meta
350 elem = parser.makeelement(s[1], **s[2])
354 cursor = cursor.getparent()
357 cursor[-1].tail = (cursor[-1].tail or '') + s[1]
359 cursor.text = (cursor.text or '') + s[1]
365 numbering = self.NUMBERING
366 if numbering is None or self.in_context_of('DISABLE_NUMBERING'):
368 numbering = self.get_context_map('SUPPRESS_NUMBERING', numbering, numbering)
373 prefix = self.numbering
375 # TODO: self.context.main_numbering_prefix
376 prefix = 'f' # default numbering prefix
379 def assign_id(self, document):
380 numbering = self.numbering
382 number = str(document.counters[numbering])
383 self.attrib['_id'] = self.id_prefix + number
384 document.counters[numbering] += 1
386 if numbering == 'main':
387 self.attrib['_visible_numbering'] = str(document.counters['_visible'])
388 document.counters['_visible'] += 1
390 if numbering == 'fn':
391 self.attrib['_visible_numbering'] = number
394 return self.attrib.get('_id') or self.getparent().get_link()
397 class Snippet(WLElement):