Drop lots of legacy code. Support Python 3.7-3.11.
[librarian.git] / src / librarian / builders / html.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 from urllib.request import urlopen
5 from lxml import etree
6 from librarian.html import add_anchors, add_table_of_contents, add_table_of_themes
7 from librarian import OutputFile
8
9
10 class HtmlBuilder:
11     file_extension = "html"
12     with_anchors = True
13     with_themes = True
14     with_toc = True
15     with_footnotes = True
16     with_nota_red = True
17     no_externalities = False
18     orphans = True
19
20     def __init__(self, base_url=None):
21         self._base_url = base_url
22
23         self.tree = text = etree.Element('div', **{'id': 'book-text'})
24         self.header = etree.Element('h1')
25
26         self.footnotes = etree.Element('div', id='footnotes')
27         self.footnote_counter = 0
28
29         self.nota_red = etree.Element('div', id='nota_red')
30
31         self.cursors = {
32             None: text,
33             'header': self.header,
34             'footnotes': self.footnotes,
35             'nota_red': self.nota_red,
36         }
37         self.current_cursors = [text]
38
39     @property
40     def base_url(self):
41         if self._base_url is not None:
42             return self._base_url
43         else:
44             return 'https://wolnelektury.pl/media/book/pictures/{}/'.format(self.document.meta.url.slug)
45
46     @property
47     def cursor(self):
48         return self.current_cursors[-1]
49
50     def enter_fragment(self, fragment):
51         cursor = self.cursors.get(fragment, self.cursor)
52         self.current_cursors.append(cursor)
53
54     def exit_fragment(self):
55         self.current_cursors.pop()
56
57     def create_fragment(self, name, element):
58         assert name not in self.cursors
59         self.cursors[name] = element
60
61     def forget_fragment(self, name):
62         del self.cursors[name]
63
64     def preprocess(self, document):
65         document._compat_assign_ordered_ids()
66         document._compat_assign_section_ids()
67
68     def build(self, document, **kwargs):
69         self.document = document
70
71         self.preprocess(document)
72         document.tree.getroot().html_build(self)
73         self.postprocess(document)
74         return self.output()
75
76     def output(self):
77         return OutputFile.from_bytes(
78             etree.tostring(
79                 self.tree,
80                 method='html',
81                 encoding='utf-8',
82                 pretty_print=True
83             )
84         )
85
86     def postprocess(self, document):
87         _ = document.tree.getroot().master.gettext
88
89         if document.meta.translators:
90             self.enter_fragment('header')
91             self.start_element('span', {'class': 'translator'})
92             self.push_text(_("translated by") + " ")
93             self.push_text(
94                 ", ".join(
95                     translator.readable()
96                     for translator in document.meta.translators
97                 )
98             )
99             self.exit_fragment()
100
101         if len(self.header):
102             self.tree.insert(0, self.header)
103             
104         if self.with_anchors:
105             add_anchors(self.tree)
106         if self.with_nota_red and len(self.nota_red):
107             self.tree.append(self.nota_red)
108         if self.with_themes:
109             add_table_of_themes(self.tree)
110         if self.with_toc:
111             add_table_of_contents(self.tree)
112
113         if self.footnote_counter:
114             fnheader = etree.Element("h3")
115             fnheader.text = _("Footnotes")
116             self.footnotes.insert(0, fnheader)
117             self.tree.append(self.footnotes)
118
119     def start_element(self, tag, attrib=None):
120         self.current_cursors.append(etree.SubElement(
121             self.cursor,
122             tag,
123             **(attrib or {})
124         ))
125
126     def end_element(self):
127         self.current_cursors.pop()
128
129     def push_text(self, text):
130         cursor = self.cursor
131         if len(cursor):
132             cursor[-1].tail = (cursor[-1].tail or '') + text
133         else:
134             cursor.text = (cursor.text or '') + text
135
136
137 class StandaloneHtmlBuilder(HtmlBuilder):
138     css_url = "https://static.wolnelektury.pl/css/compressed/book_text.css"
139
140     def postprocess(self, document):
141         super(StandaloneHtmlBuilder, self).postprocess(document)
142
143         tree = etree.Element('html')
144         body = etree.SubElement(tree, 'body')
145         body.append(self.tree)
146         self.tree = tree
147
148         head = etree.Element('head')
149         tree.insert(0, head)
150
151
152         etree.SubElement(head, 'meta', charset='utf-8')
153         etree.SubElement(head, 'title').text = document.meta.title
154
155         etree.SubElement(
156             head,
157             'meta',
158             name="viewport",
159             content="width=device-width, initial-scale=1, maximum-scale=1"
160         )
161
162         if self.no_externalities:
163             etree.SubElement(
164                 head, 'style',
165             ).text = urlopen(self.css_url).read().decode('utf-8')
166         else:
167             etree.SubElement(
168                 head,
169                 'link',
170                 href=self.css_url,
171                 rel="stylesheet",
172                 type="text/css",
173             )
174
175             etree.SubElement(
176                 body, 'script',
177                 src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"
178             )
179
180             etree.SubElement(
181                 body,
182                 "script",
183                 src="http://malsup.github.io/min/jquery.cycle2.min.js"
184             )
185
186
187 class SnippetHtmlBuilder(HtmlBuilder):
188     with_anchors = False
189     with_themes = False
190     with_toc = False
191     with_footnotes = False
192     with_nota_red = False
193     with_refs = False
194
195             
196 class DaisyHtmlBuilder(StandaloneHtmlBuilder):
197     file_extension = 'xhtml'
198     with_anchors = False
199     with_themes = False
200     with_toc = False
201     with_footnotes = False
202     with_nota_red = False
203     with_deep_identifiers = False
204     no_externalities = True
205
206     def output(self):
207         tree = etree.ElementTree(self.tree)
208         tree.docinfo.public_id = '-//W3C//DTD XHTML 1.0 Transitional//EN'
209         tree.docinfo.system_url = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'
210         return OutputFile.from_bytes(
211             etree.tostring(
212                 tree,
213                 encoding='utf-8',
214                 pretty_print=True,
215                 xml_declaration=True
216             )
217         )
218