1#!/usr/local/bin/python3.8
2
3
4__license__   = 'GPL v3'
5__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
6
7
8import copy
9
10from lxml import html, etree
11from lxml.html.builder import HTML, HEAD, TITLE, STYLE, DIV, BODY, \
12        STRONG, BR, SPAN, A, HR, UL, LI, H2, H3, IMG, P as PT, \
13        TABLE, TD, TR
14
15from calibre import strftime, isbytestring
16
17
18def attrs(*args, **kw):
19    rescale = kw.pop('rescale', None)
20    if rescale is not None:
21        kw['data-calibre-rescale'] = str(rescale)
22    if args:
23        kw['class'] = ' '.join(args)
24    return kw
25
26# Regular templates
27
28
29class Template:
30
31    IS_HTML = True
32
33    def __init__(self, lang=None):
34        self.html_lang = lang
35
36    def generate(self, *args, **kwargs):
37        if 'style' not in kwargs:
38            kwargs['style'] = ''
39        for key in kwargs.keys():
40            if isbytestring(kwargs[key]):
41                kwargs[key] = kwargs[key].decode('utf-8', 'replace')
42            if kwargs[key] is None:
43                kwargs[key] = ''
44        args = list(args)
45        for i in range(len(args)):
46            if isbytestring(args[i]):
47                args[i] = args[i].decode('utf-8', 'replace')
48            if args[i] is None:
49                args[i] = ''
50
51        self._generate(*args, **kwargs)
52
53        return self
54
55    def render(self, *args, **kwargs):
56        if self.IS_HTML:
57            return html.tostring(self.root, encoding='utf-8',
58                    include_meta_content_type=True, pretty_print=True)
59        return etree.tostring(self.root, encoding='utf-8', xml_declaration=True,
60                pretty_print=True)
61
62
63class EmbeddedContent(Template):
64
65    def _generate(self, article, style=None, extra_css=None):
66        content = article.content if article.content else ''
67        summary = article.summary if article.summary else ''
68        text = content if len(content) > len(summary) else summary
69        head = HEAD(TITLE(article.title))
70        if style:
71            head.append(STYLE(style, type='text/css'))
72        if extra_css:
73            head.append(STYLE(extra_css, type='text/css'))
74
75        if isbytestring(text):
76            text = text.decode('utf-8', 'replace')
77        elements = html.fragments_fromstring(text)
78        self.root = HTML(head,
79                BODY(H2(article.title), DIV()))
80        div = self.root.find('body').find('div')
81        if elements and isinstance(elements[0], str):
82            div.text = elements[0]
83            elements = list(elements)[1:]
84        for elem in elements:
85            if hasattr(elem, 'getparent'):
86                elem.getparent().remove(elem)
87            else:
88                elem = SPAN(elem)
89            div.append(elem)
90
91
92class IndexTemplate(Template):
93
94    def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None):
95        self.IS_HTML = False
96        date = strftime(datefmt)
97        head = HEAD(TITLE(title))
98        if style:
99            head.append(STYLE(style, type='text/css'))
100        if extra_css:
101            head.append(STYLE(extra_css, type='text/css'))
102        ul = UL(attrs('calibre_feed_list'))
103        for i, feed in enumerate(feeds):
104            if len(feed):
105                li = LI(A(feed.title, attrs('feed', rescale=120,
106                    href='feed_%d/index.html'%i)), id='feed_%d'%i)
107                ul.append(li)
108        div = DIV(
109                PT(IMG(src=masthead,alt="masthead"),style='text-align:center'),
110                PT(date, style='text-align:right'),
111                ul,
112                attrs(rescale=100))
113        self.root = HTML(head, BODY(div))
114        if self.html_lang:
115            self.root.set('lang', self.html_lang)
116
117
118class FeedTemplate(Template):
119
120    def get_navbar(self, f, feeds, top=True):
121        if len(feeds) < 2:
122            return DIV()
123        navbar = DIV('| ', attrs('calibre_navbar', rescale=70,
124            style='text-align:center'))
125        if not top:
126            hr = HR()
127            navbar.append(hr)
128            navbar.text = None
129            hr.tail = '| '
130
131        if f+1 < len(feeds):
132            link = A(_('Next section'), href='../feed_%d/index.html'%(f+1))
133            link.tail = ' | '
134            navbar.append(link)
135        link = A(_('Main menu'), href="../index.html")
136        link.tail = ' | '
137        navbar.append(link)
138        if f > 0:
139            link = A(_('Previous section'), href='../feed_%d/index.html'%(f-1))
140            link.tail = ' |'
141            navbar.append(link)
142        if top:
143            navbar.append(HR())
144        return navbar
145
146    def _generate(self, f, feeds, cutoff, extra_css=None, style=None):
147        from calibre.utils.cleantext import clean_xml_chars
148        feed = feeds[f]
149        head = HEAD(TITLE(feed.title))
150        if style:
151            head.append(STYLE(style, type='text/css'))
152        if extra_css:
153            head.append(STYLE(extra_css, type='text/css'))
154        body = BODY()
155        body.append(self.get_navbar(f, feeds))
156
157        div = DIV(
158                H2(feed.title,
159                    attrs('calibre_feed_title', rescale=160)),
160                attrs(rescale=100)
161              )
162        body.append(div)
163        if getattr(feed, 'image', None):
164            div.append(DIV(IMG(
165                alt=feed.image_alt if feed.image_alt else '',
166                src=feed.image_url
167                ),
168                attrs('calibre_feed_image')))
169        if getattr(feed, 'description', None):
170            d = DIV(clean_xml_chars(feed.description), attrs('calibre_feed_description', rescale=80))
171            d.append(BR())
172            div.append(d)
173        ul = UL(attrs('calibre_article_list'))
174        for i, article in enumerate(feed.articles):
175            if not getattr(article, 'downloaded', False):
176                continue
177            li = LI(
178                    A(article.title, attrs('article', rescale=120,
179                                    href=article.url)),
180                    SPAN(article.formatted_date, attrs('article_date')),
181                    attrs(rescale=100, id='article_%d'%i,
182                            style='padding-bottom:0.5em')
183                    )
184            if article.summary:
185                li.append(DIV(clean_xml_chars(cutoff(article.text_summary)),
186                    attrs('article_description', rescale=70)))
187            ul.append(li)
188        div.append(ul)
189        div.append(self.get_navbar(f, feeds, top=False))
190        self.root = HTML(head, body)
191        if self.html_lang:
192            self.root.set('lang', self.html_lang)
193
194
195class NavBarTemplate(Template):
196
197    def _generate(self, bottom, feed, art, number_of_articles_in_feed,
198                 two_levels, url, __appname__, prefix='', center=True,
199                 extra_css=None, style=None):
200        head = HEAD(TITLE('navbar'))
201        if style:
202            head.append(STYLE(style, type='text/css'))
203        if extra_css:
204            head.append(STYLE(extra_css, type='text/css'))
205
206        if prefix and not prefix.endswith('/'):
207            prefix += '/'
208        align = 'center' if center else 'left'
209
210        navbar = DIV(attrs('calibre_navbar', rescale=70,
211            style='text-align:'+align))
212        if bottom:
213            if not url.startswith('file://'):
214                navbar.append(HR())
215                text = 'This article was downloaded by '
216                p = PT(text, STRONG(__appname__), A(url, href=url, rel='calibre-downloaded-from'),
217                        style='text-align:left; max-width: 100%; overflow: hidden;')
218                p[0].tail = ' from '
219                navbar.append(p)
220                navbar.append(BR())
221            navbar.append(BR())
222        else:
223            next_art = 'feed_%d'%(feed+1) if art == number_of_articles_in_feed - 1 \
224                    else 'article_%d'%(art+1)
225            up = '../..' if art == number_of_articles_in_feed - 1 else '..'
226            href = '%s%s/%s/index.html'%(prefix, up, next_art)
227            navbar.text = '| '
228            navbar.append(A(_('Next'), href=href))
229        href = '%s../index.html#article_%d'%(prefix, art)
230        next(navbar.iterchildren(reversed=True)).tail = ' | '
231        navbar.append(A(_('Section menu'), href=href))
232        href = '%s../../index.html#feed_%d'%(prefix, feed)
233        next(navbar.iterchildren(reversed=True)).tail = ' | '
234        navbar.append(A(_('Main menu'), href=href))
235        if art > 0 and not bottom:
236            href = '%s../article_%d/index.html'%(prefix, art-1)
237            next(navbar.iterchildren(reversed=True)).tail = ' | '
238            navbar.append(A(_('Previous'), href=href))
239        next(navbar.iterchildren(reversed=True)).tail = ' | '
240        if not bottom:
241            navbar.append(HR())
242
243        self.root = HTML(head, BODY(navbar))
244
245
246# Touchscreen templates
247class TouchscreenIndexTemplate(Template):
248
249    def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None):
250        self.IS_HTML = False
251        date = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
252        masthead_p = etree.Element("p")
253        masthead_p.set("style","text-align:center")
254        masthead_img = etree.Element("img")
255        masthead_img.set("src",masthead)
256        masthead_img.set("alt","masthead")
257        masthead_p.append(masthead_img)
258
259        head = HEAD(TITLE(title))
260        if style:
261            head.append(STYLE(style, type='text/css'))
262        if extra_css:
263            head.append(STYLE(extra_css, type='text/css'))
264
265        toc = TABLE(attrs('toc'),width="100%",border="0",cellpadding="3px")
266        for i, feed in enumerate(feeds):
267            if len(feed):
268                tr = TR()
269                tr.append(TD(attrs(rescale=120), A(feed.title, href='feed_%d/index.html'%i)))
270                tr.append(TD('%s' % len(feed.articles), style="text-align:right"))
271                toc.append(tr)
272        div = DIV(
273                masthead_p,
274                H3(attrs('publish_date'),date),
275                DIV(attrs('divider')),
276                toc)
277        self.root = HTML(head, BODY(div))
278        if self.html_lang:
279            self.root.set('lang', self.html_lang)
280
281
282class TouchscreenFeedTemplate(Template):
283
284    def _generate(self, f, feeds, cutoff, extra_css=None, style=None):
285        from calibre.utils.cleantext import clean_xml_chars
286
287        def trim_title(title,clip=18):
288            if len(title)>clip:
289                tokens = title.split(' ')
290                new_title_tokens = []
291                new_title_len = 0
292                if len(tokens[0]) > clip:
293                    return tokens[0][:clip] + '...'
294                for token in tokens:
295                    if len(token) + new_title_len < clip:
296                        new_title_tokens.append(token)
297                        new_title_len += len(token)
298                    else:
299                        new_title_tokens.append('...')
300                        title = ' '.join(new_title_tokens)
301                        break
302            return title
303
304        self.IS_HTML = False
305        feed = feeds[f]
306
307        # Construct the navbar
308        navbar_t = TABLE(attrs('touchscreen_navbar'))
309        navbar_tr = TR()
310
311        # Previous Section
312        link = ''
313        if f > 0:
314            link = A(attrs('feed_link'),
315                     trim_title(feeds[f-1].title),
316                     href='../feed_%d/index.html' % int(f-1))
317        navbar_tr.append(TD(attrs('feed_prev'),link))
318
319        # Up to Sections
320        link = A(_('Sections'), href="../index.html")
321        navbar_tr.append(TD(attrs('feed_up'),link))
322
323        # Next Section
324        link = ''
325        if f < len(feeds)-1:
326            link = A(attrs('feed_link'),
327                     trim_title(feeds[f+1].title),
328                     href='../feed_%d/index.html' % int(f+1))
329        navbar_tr.append(TD(attrs('feed_next'),link))
330        navbar_t.append(navbar_tr)
331        top_navbar = navbar_t
332        bottom_navbar = copy.copy(navbar_t)
333        # print "\n%s\n" % etree.tostring(navbar_t, pretty_print=True)
334
335        # Build the page
336        head = HEAD(TITLE(feed.title))
337        if style:
338            head.append(STYLE(style, type='text/css'))
339        if extra_css:
340            head.append(STYLE(extra_css, type='text/css'))
341        body = BODY()
342        div = DIV(
343                top_navbar,
344                H2(feed.title, attrs('feed_title'))
345                )
346        body.append(div)
347
348        if getattr(feed, 'image', None):
349            div.append(DIV(IMG(
350                alt=feed.image_alt if feed.image_alt else '',
351                src=feed.image_url
352                ),
353                attrs('calibre_feed_image')))
354        if getattr(feed, 'description', None):
355            d = DIV(clean_xml_chars(feed.description), attrs('calibre_feed_description', rescale=80))
356            d.append(BR())
357            div.append(d)
358
359        for i, article in enumerate(feed.articles):
360            if not getattr(article, 'downloaded', False):
361                continue
362
363            div_td = DIV(attrs('article_summary'),
364                    A(article.title, attrs('summary_headline',rescale=120,
365                                    href=article.url)))
366            if article.author:
367                div_td.append(DIV(article.author,
368                    attrs('summary_byline', rescale=100)))
369            if article.summary:
370                div_td.append(DIV(cutoff(article.text_summary),
371                    attrs('summary_text', rescale=100)))
372            div.append(div_td)
373
374        div.append(bottom_navbar)
375        self.root = HTML(head, body)
376        if self.html_lang:
377            self.root.set('lang', self.html_lang)
378
379
380class TouchscreenNavBarTemplate(Template):
381
382    def _generate(self, bottom, feed, art, number_of_articles_in_feed,
383                 two_levels, url, __appname__, prefix='', center=True,
384                 extra_css=None, style=None):
385        head = HEAD(TITLE('navbar'))
386        if style:
387            head.append(STYLE(style, type='text/css'))
388        if extra_css:
389            head.append(STYLE(extra_css, type='text/css'))
390
391        navbar = DIV()
392        navbar_t = TABLE(attrs('touchscreen_navbar'))
393        navbar_tr = TR()
394
395        if bottom and not url.startswith('file://'):
396            navbar.append(HR())
397            text = 'This article was downloaded by '
398            p = PT(text, STRONG(__appname__), A(url, href=url, rel='calibre-downloaded-from'),
399                    style='text-align:left; max-width: 100%; overflow: hidden;')
400            p[0].tail = ' from '
401            navbar.append(p)
402            navbar.append(BR())
403        # | Previous
404        if art > 0:
405            link = A(attrs('article_link'),_('Previous'),href='%s../article_%d/index.html'%(prefix, art-1))
406            navbar_tr.append(TD(attrs('article_prev'),link))
407        else:
408            navbar_tr.append(TD(attrs('article_prev'),''))
409
410        # | Articles | Sections |
411        link = A(attrs('articles_link'),_('Articles'), href='%s../index.html#article_%d'%(prefix, art))
412        navbar_tr.append(TD(attrs('article_articles_list'),link))
413
414        link = A(attrs('sections_link'),_('Sections'), href='%s../../index.html#feed_%d'%(prefix, feed))
415        navbar_tr.append(TD(attrs('article_sections_list'),link))
416
417        # | Next
418        next_art = 'feed_%d'%(feed+1) if art == number_of_articles_in_feed - 1 \
419                else 'article_%d'%(art+1)
420        up = '../..' if art == number_of_articles_in_feed - 1 else '..'
421
422        link = A(attrs('article_link'), _('Next'), href='%s%s/%s/index.html'%(prefix, up, next_art))
423        navbar_tr.append(TD(attrs('article_next'),link))
424        navbar_t.append(navbar_tr)
425        navbar.append(navbar_t)
426        # print "\n%s\n" % etree.tostring(navbar, pretty_print=True)
427
428        self.root = HTML(head, BODY(navbar))
429