X-Git-Url: https://git.mdrn.pl/librarian.git/blobdiff_plain/0604bdd5f693da9f1c78f9d9fa2276f0c7b6c17b..HEAD:/src/librarian/elements/base.py?ds=sidebyside diff --git a/src/librarian/elements/base.py b/src/librarian/elements/base.py index fb5e3b1..2d656ae 100644 --- a/src/librarian/elements/base.py +++ b/src/librarian/elements/base.py @@ -1,11 +1,29 @@ -# -*- coding: utf-8 - +# This file is part of Librarian, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Wolne Lektury. See NOTICE for more information. +# +import copy import re from lxml import etree from librarian import dcparser, RDFNS +from librarian.util import get_translation + +def last_words(text, n): + words = [] + for w in reversed(text.split()): + words.append(w) + if len(w) > 2: + n -= 1 + if not n: break + if n: + return n, text + else: + return n, ' '.join(reversed(words)) class WLElement(etree.ElementBase): + SECTION_PRECEDENCE = None + ASIDE = False + TXT_TOP_MARGIN = 0 TXT_BOTTOM_MARGIN = 0 TXT_PREFIX = "" @@ -14,18 +32,26 @@ class WLElement(etree.ElementBase): HTML_TAG = None HTML_ATTR = {} HTML_CLASS = None - HTML_SECTION = False - + + EPUB_TAG = None + EPUB_ATTR = {} + EPUB_CLASS = None + EPUB_START_CHUNK = False + + FB2_TAG = None + CAN_HAVE_TEXT = True STRIP = False + NUMBERING = None text_substitutions = [ - (u'---', u'—'), - (u'--', u'–'), - (u'...', u'…'), - (u',,', u'„'), - (u'"', u'”'), + ('---', '—'), + ('--', '–'), + ('...', '…'), + (',,', '„'), + ('"', '”'), ('\ufeff', ''), + ("'", "\u2019"), ] @property @@ -37,7 +63,7 @@ class WLElement(etree.ElementBase): else: self._meta_object = None return self._meta_object - + @property def meta(self): if self.meta_object is not None: @@ -47,93 +73,326 @@ class WLElement(etree.ElementBase): return self.getparent().meta else: return self.document.base_meta + + @property + def gettext(self): + return get_translation(self.meta.language).gettext + + def in_context_of(self, setting): + parent = self.getparent() + if parent is None: + return False + try: + return getattr(parent, setting) + except AttributeError: + return parent.in_context_of(setting) + + def get_context_map(self, setting, key, default=None): + parent = self.getparent() + if parent is None: + return default + try: + return getattr(parent, setting)[key] + except AttributeError: + return parent.get_context_map(setting, key, default) + + def signal(self, signal): + parent = self.getparent() + if parent is not None: + parent.signal(signal) + + def raw_printable_text(self, builder): + from librarian.html import raw_printable_text + + # TODO: podtagi, wyroznienia, etc + t = '' + t += self.normalize_text(self.text, builder) + for c in self: + if not isinstance(c, WLElement): + continue + if c.tag not in ('pe', 'pa', 'pt', 'pr', 'motyw'): + t += c.raw_printable_text(builder) + t += self.normalize_text(c.tail, builder) + return t - def normalize_text(self, text): + def normalize_text(self, text, builder): text = text or '' for e, s in self.text_substitutions: text = text.replace(e, s) - text = re.sub(r'\s+', ' ', text) + + if getattr(builder, 'normalize_whitespace', False): + text = re.sub(r'\s+', ' ', text) + + if getattr(builder, 'hyphenator', None) is not None: + newt = '' + wlist = re.compile(r'\w+|[^\w]', re.UNICODE).findall(text) + for w in wlist: + newt += builder.hyphenator.inserted(w, '\u00AD') + text = newt + + if builder.orphans: + text = re.sub(r'(?<=\s\w)\s+', '\u00A0', text) + return text - def _build_inner(self, builder, build_method): + def build_inner(self, builder): + build_method = builder.build_method_fn child_count = len(self) if self.CAN_HAVE_TEXT and self.text: - text = self.normalize_text(self.text) + text = self.normalize_text(self.text, builder) if self.STRIP: text = text.lstrip() if not child_count: text = text.rstrip() builder.push_text(text) for i, child in enumerate(self): + real_child_count = 0 if isinstance(child, WLElement): getattr(child, build_method)(builder) + self.after_child(builder, real_child_count) + real_child_count += 1 + + # FIXME base builder api + elif getattr(builder, 'debug', False) and child.tag is etree.Comment: + builder.process_comment(child) if self.CAN_HAVE_TEXT and child.tail: - text = self.normalize_text(child.tail) + text = self.normalize_text(child.tail, builder) if self.STRIP and i == child_count - 1: text = text.rstrip() builder.push_text(text) - def _txt_build_inner(self, builder): - self._build_inner(builder, 'txt_build') + def after_child(self, builder, child_count): + fn = getattr(builder, 'after_child_fn', None) + if fn: + getattr(self, builder.after_child_fn)(builder, child_count) + + def txt_after_child(self, builder, child_count): + pass + + def txt_build_inner(self, builder): + self.build_inner(builder) def txt_build(self, builder): - if hasattr(self, 'TXT_LEGACY_TOP_MARGIN'): - builder.push_legacy_margin(self.TXT_LEGACY_TOP_MARGIN) - else: - builder.push_margin(self.TXT_TOP_MARGIN) + builder.push_margin(self.TXT_TOP_MARGIN) builder.push_text(self.TXT_PREFIX, True) - self._txt_build_inner(builder) + self.txt_build_inner(builder) builder.push_text(self.TXT_SUFFIX, True) - if hasattr(self, 'TXT_LEGACY_BOTTOM_MARGIN'): - builder.push_legacy_margin(self.TXT_LEGACY_BOTTOM_MARGIN) - else: - builder.push_margin(self.TXT_BOTTOM_MARGIN) + builder.push_margin(self.TXT_BOTTOM_MARGIN) - def _html_build_inner(self, builder): - self._build_inner(builder, 'html_build') + def html_build_inner(self, builder): + self.build_inner(builder) def get_html_attr(self, builder): attr = self.HTML_ATTR.copy() if self.HTML_CLASS: attr['class'] = self.HTML_CLASS - # always copy the id attribute (?) - if self.attrib.get('id'): - attr['id'] = self.attrib['id'] + if builder.with_ids: + # always copy the id attribute (?) + if self.attrib.get('id'): + attr['id'] = self.attrib['id'] + if self.attrib.get('_id'): + attr['id'] = self.attrib['_id'] return attr - + def html_build(self, builder): - if self.HTML_SECTION: + # Do we need a number? + numbering = self.numbering + if numbering == 'main': + if builder.with_numbering and self.has_visible_numbering: + builder.add_visible_number(self) + + if self.HTML_TAG: builder.start_element( - 'a', {"name": "f18", "class": "target"} + self.HTML_TAG, + self.get_html_attr(builder), ) - builder.push_text(" ") + + self.html_build_inner(builder) + if self.HTML_TAG: builder.end_element() + def fb2_build(self, builder): + if self.SECTION_PRECEDENCE: + builder.start_section(self.SECTION_PRECEDENCE) + builder.start_element('title') + builder.start_element('p') + + if self.FB2_TAG: builder.start_element( - "a", {"href": "#f18", "class": "anchor"} + self.FB2_TAG, + #self.get_fb2_attr(builder), ) - builder.push_text("18") + + self.build_inner(builder) + if self.FB2_TAG: + builder.end_element() + if self.SECTION_PRECEDENCE: + builder.end_element() builder.end_element() - - if self.HTML_TAG: - builder.start_element( - self.HTML_TAG, - self.get_html_attr(builder), - ) + def epub_build_inner(self, builder): + self.build_inner(builder) + + def get_epub_attr(self, builder): + attr = self.EPUB_ATTR.copy() + if self.EPUB_CLASS: + attr['class'] = self.EPUB_CLASS + return attr + + def epub_build(self, builder): + from librarian.elements.masters import Master + + # TEMPORARY + self.CAN_HAVE_TEXT = True + self.STRIP = False + + start_chunk = self.EPUB_START_CHUNK and isinstance(self.getparent(), Master) + + if start_chunk: + builder.start_chunk() + + fragment = None + if self.SECTION_PRECEDENCE and not self.in_context_of('NO_TOC'): + if not start_chunk: + fragment = 'sub%d' % builder.assign_section_number() + self.attrib['id'] = fragment - if self.HTML_SECTION: + builder.add_toc_entry( + fragment, + self.raw_printable_text(builder), + self.SECTION_PRECEDENCE + ) + + if self.EPUB_TAG: + attr = self.get_epub_attr(builder) + if fragment: + attr['id'] = fragment + if builder.debug: + chunkno, sourceline = 0, self.sourceline + if builder.splits: + chunkno, sourceline = len(builder.splits), sourceline - builder.splits[-1] + attr['data-debug'] = f'{chunkno}:{sourceline}' builder.start_element( - "a", {"name": "sec34"} + self.EPUB_TAG, + attr ) - builder.end_element() - self._html_build_inner(builder) - if self.HTML_TAG: + self.epub_build_inner(builder) + if self.EPUB_TAG: builder.end_element() + def validate(self): + from librarian.elements.masters import Master + from librarian.elements.blocks import DlugiCytat, PoezjaCyt + from librarian.elements.footnotes import Footnote + + if self.SECTION_PRECEDENCE: + assert isinstance(self.getparent(), (Master, DlugiCytat, PoezjaCyt, Footnote)), \ + 'Header {} inside a <{}> instead of a master.'.format( + etree.tostring(self, encoding='unicode'), self.getparent().tag) + + for c in self: + if isinstance(c, WLElement): + c.validate() + + def sanitize(self): # TODO: Remove insanity here. for e in self: if isinstance(e, WLElement): e.sanitize() + + def snip(self, words, before=None, sub=False): + if sub and self.ASIDE: + return words, [] + + snippet = [] + if before is not None: + i = self.index(before) + else: + i = len(self) + + while i > 0: + i -= 1 + if self[i].tail: + if words: + words, text = last_words(self[i].tail, words) + snippet = [('text', text)] + snippet + + if words: + words, subsnip = self[i].snip(words, sub=True) + snippet = subsnip + snippet + + if words and self.text: + words, text = last_words(self.text, words) + snippet = [('text', text)] + snippet + + snippet = [('start', self.tag, self.attrib)] + snippet + [('end',)] + + if not sub and words and not self.ASIDE: + # do we dare go up? + parent = self.getparent() + if parent is not None and parent.CAN_HAVE_TEXT: + words, parsnip = parent.snip(words, before=self) + return words, parsnip[:-1] + snippet + parsnip[-1:] + + return words, snippet + + def get_snippet(self, words=15): + from librarian.parser import parser + + words, snippet = self.getparent().snip(words=words, before=self) + + cursor = snipelem = parser.makeelement('snippet') + snipelem._meta_object = self.meta + for s in snippet: + if s[0] == 'start': + elem = parser.makeelement(s[1], **s[2]) + cursor.append(elem) + cursor = elem + elif s[0] == 'end': + cursor = cursor.getparent() + else: + if len(cursor): + cursor[-1].tail = (cursor[-1].tail or '') + s[1] + else: + cursor.text = (cursor.text or '') + s[1] + + return snipelem + + @property + def numbering(self): + numbering = self.NUMBERING + if numbering is None or self.in_context_of('DISABLE_NUMBERING'): + return None + numbering = self.get_context_map('SUPPRESS_NUMBERING', numbering, numbering) + return numbering + + @property + def id_prefix(self): + prefix = self.numbering + if prefix == 'main': + # TODO: self.context.main_numbering_prefix + prefix = 'f' # default numbering prefix + return prefix + + def assign_id(self, document): + numbering = self.numbering + if numbering: + number = str(document.counters[numbering]) + self.attrib['_id'] = self.id_prefix + number + document.counters[numbering] += 1 + + if numbering == 'main': + self.attrib['_visible_numbering'] = str(document.counters['_visible']) + document.counters['_visible'] += 1 + + if numbering == 'fn': + self.attrib['_visible_numbering'] = number + + def get_link(self): + return self.attrib.get('_id') or self.getparent().get_link() + + +class Snippet(WLElement): + pass