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