comparison sat/tools/common/template.py @ 3268:85c9cfcd4f5e

tools (common/template): theme settings with possibility to disable default fallback for CSS: - themes can now have a `settings.json` file at their root to modify the renderer behaviour - the `css_default_fallback` boolean (true by default) can be used to disable the fallback mechanism when CSS files are checked. Useful when a theme doesn't follow the CSS naming of default theme. - introduced some type hints
author Goffi <goffi@goffi.org>
date Sun, 03 May 2020 17:01:39 +0200
parents 2eeca6fd08f7
children 430204a3cc10
comparison
equal deleted inserted replaced
3267:2eeca6fd08f7 3268:85c9cfcd4f5e
22 import time 22 import time
23 import re 23 import re
24 import json 24 import json
25 from pathlib import Path 25 from pathlib import Path
26 from collections import namedtuple 26 from collections import namedtuple
27 from typing import Optional, List, Tuple
27 from xml.sax.saxutils import quoteattr 28 from xml.sax.saxutils import quoteattr
28 from babel import support 29 from babel import support
29 from babel import Locale 30 from babel import Locale
30 from babel.core import UnknownLocaleError 31 from babel.core import UnknownLocaleError
31 import pygments 32 import pygments
157 theme=theme, reserved=TPL_RESERVED_CHARS)) 158 theme=theme, reserved=TPL_RESERVED_CHARS))
158 159
159 return TemplateData(site, theme, template_path) 160 return TemplateData(site, theme, template_path)
160 161
161 @staticmethod 162 @staticmethod
162 def getSitesAndThemes(site, theme): 163 def getSitesAndThemes(
164 site: str,
165 theme: str,
166 default_fallback: bool=True
167 ) -> List[Tuple[str, str]]:
163 """Get sites and themes to check for template/file 168 """Get sites and themes to check for template/file
164 169
165 Will add default theme and default site in search list when suitable 170 Will add default theme and default site in search list when suitable
166 @param site(unicode): site requested 171 @param site: site requested
167 @param theme(unicode): theme requested 172 @param theme: theme requested
168 @return (list[tuple[unicode, unicode]]): site and theme couples to check 173 @return: site and theme couples to check
169 """ 174 """
170 sites_and_themes = [[site, theme]] 175 sites_and_themes = [[site, theme]]
171 if theme != C.TEMPLATE_THEME_DEFAULT: 176 if theme != C.TEMPLATE_THEME_DEFAULT and default_fallback:
172 sites_and_themes.append([site, C.TEMPLATE_THEME_DEFAULT]) 177 sites_and_themes.append([site, C.TEMPLATE_THEME_DEFAULT])
173 if site: 178 if site and default_fallback:
174 # the site is not the default one, so we add default at the end 179 # the site is not the default one, so we add default at the end
175 sites_and_themes.append(['', C.TEMPLATE_THEME_DEFAULT]) 180 sites_and_themes.append(['', C.TEMPLATE_THEME_DEFAULT])
176 return sites_and_themes 181 return sites_and_themes
177 182
178 def _get_template_f(self, site, theme, path_elts): 183 def _get_template_f(self, site, theme, path_elts):
381 tpl_path = Path(site_path) / C.TEMPLATE_TPL_DIR 386 tpl_path = Path(site_path) / C.TEMPLATE_TPL_DIR
382 for p in tpl_path.iterdir(): 387 for p in tpl_path.iterdir():
383 if not p.is_dir(): 388 if not p.is_dir():
384 continue 389 continue
385 log.debug(f"theme found for {site or 'default site'}: {p.name}") 390 log.debug(f"theme found for {site or 'default site'}: {p.name}")
386 theme_data = self.sites_themes.setdefault(site, {})[p.name] = {'path': p} 391 theme_data = self.sites_themes.setdefault(site, {})[p.name] = {
392 'path': p,
393 'settings': {}}
394 theme_settings = p / "settings.json"
395 if theme_settings.is_file:
396 try:
397 with theme_settings.open() as f:
398 settings = json.load(f)
399 except Exception as e:
400 log.warning(_(
401 "Can't load theme settings at {path}").format(
402 path=theme_settings))
403 else:
404 log.debug(
405 f"found settings for theme {p.name!r} at {theme_settings}")
406 theme_data['settings'] = settings
387 browser_path = p / BROWSER_DIR 407 browser_path = p / BROWSER_DIR
388 if browser_path.is_dir(): 408 if browser_path.is_dir():
389 theme_data['browser_path'] = browser_path 409 theme_data['browser_path'] = browser_path
390 browser_meta_path = browser_path / BROWSER_META_FILE 410 browser_meta_path = browser_path / BROWSER_META_FILE
391 if browser_meta_path.is_file(): 411 if browser_meta_path.is_file():
546 try: 566 try:
547 return self.sites_themes[site_name] 567 return self.sites_themes[site_name]
548 except KeyError: 568 except KeyError:
549 raise exceptions.NotFound(f"no theme found for {site_name}") 569 raise exceptions.NotFound(f"no theme found for {site_name}")
550 570
551 def getStaticPath(self, template_data, filename): 571 def getStaticPath(
572 self,
573 template_data: TemplateData,
574 filename: str,
575 default_fallback: bool=True
576 ) -> Optional[TemplateData]:
552 """Retrieve path of a static file if it exists with current theme or default 577 """Retrieve path of a static file if it exists with current theme or default
553 578
554 File will be looked at <site_root_dir>/<theme_dir>/<static_dir>/filename, 579 File will be looked at <site_root_dir>/<theme_dir>/<static_dir>/filename,
555 then <site_root_dir>/<default_theme_dir>/<static_dir>/filename anf finally 580 then <site_root_dir>/<default_theme_dir>/<static_dir>/filename anf finally
556 <default_site>/<default_theme_dir>/<static_dir> (i.e. sat_templates). 581 <default_site>/<default_theme_dir>/<static_dir> (i.e. sat_templates).
557 In case of absolue URL, base dir of template is used as base. For instance if 582 In case of absolute URL, base dir of template is used as base. For instance if
558 template is an absolute template to "/some/path/template.html", file will be 583 template is an absolute template to "/some/path/template.html", file will be
559 checked at "/some/path/<filename>" 584 checked at "/some/path/<filename>"
560 @param template_data(TemplateData): data of current template 585 @param template_data: data of current template
561 @return (TemplateData, None): built template data instance where .path is 586 @param filename: name of the file to retrieve
587 @param default_fallback: if True, default theme will be checked if the file is
588 not found in current theme, then default site with default theme will be used.
589 @return: built template data instance where .path is
562 the relative path to the file, from theme root dir. 590 the relative path to the file, from theme root dir.
563 None if not found. 591 None if not found.
564 """ 592 """
565 if template_data.site is None: 593 if template_data.site is None:
566 # we have and absolue path 594 # we have an absolue path
567 if (not template_data.theme is None 595 if (not template_data.theme is None
568 or not template_data.path.startswith('/')): 596 or not template_data.path.startswith('/')):
569 raise exceptions.InternalError( 597 raise exceptions.InternalError(
570 "invalid template data, was expecting absolute URL") 598 "invalid template data, was expecting absolute URL")
571 static_dir = os.path.dirname(template_data.path) 599 static_dir = os.path.dirname(template_data.path)
574 return TemplateData(site=None, theme=None, path=file_path) 602 return TemplateData(site=None, theme=None, path=file_path)
575 else: 603 else:
576 return None 604 return None
577 605
578 sites_and_themes = TemplateLoader.getSitesAndThemes(template_data.site, 606 sites_and_themes = TemplateLoader.getSitesAndThemes(template_data.site,
579 template_data.theme) 607 template_data.theme,
608 default_fallback)
580 for site, theme in sites_and_themes: 609 for site, theme in sites_and_themes:
581 site_root_dir = self.sites_paths[site] 610 site_root_dir = self.sites_paths[site]
582 relative_path = os.path.join(C.TEMPLATE_STATIC_DIR, filename) 611 relative_path = os.path.join(C.TEMPLATE_STATIC_DIR, filename)
583 absolute_path = os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, 612 absolute_path = os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR,
584 theme, relative_path) 613 theme, relative_path)
585 if os.path.exists(absolute_path): 614 if os.path.exists(absolute_path):
586 return TemplateData(site=site, theme=theme, path=relative_path) 615 return TemplateData(site=site, theme=theme, path=relative_path)
587 616
588 return None 617 return None
589 618
590 def _appendCSSPaths(self, template_data, css_files, css_files_noscript, name_root): 619 def _appendCSSPaths(
620 self,
621 template_data: TemplateData,
622 css_files: list,
623 css_files_noscript: list,
624 name_root: str,
625 default_fallback: bool
626
627 ) -> None:
591 """Append found css to css_files and css_files_noscript 628 """Append found css to css_files and css_files_noscript
592 629
593 @param css_files(list): list to fill of relative path to found css file 630 @param css_files: list to fill of relative path to found css file
594 @param css_files_noscript(list): list to fill of relative path to found css file 631 @param css_files_noscript: list to fill of relative path to found css file
595 with "_noscript" suffix 632 with "_noscript" suffix
596 """ 633 """
597 name = name_root + ".css" 634 name = name_root + ".css"
598 css_path = self.getStaticPath(template_data, name) 635 css_path = self.getStaticPath(template_data, name, default_fallback)
599 if css_path is not None: 636 if css_path is not None:
600 css_files.append(self.getFrontURL(css_path)) 637 css_files.append(self.getFrontURL(css_path))
601 noscript_name = name_root + "_noscript.css" 638 noscript_name = name_root + "_noscript.css"
602 noscript_path = self.getStaticPath(template_data, noscript_name) 639 noscript_path = self.getStaticPath(
640 template_data, noscript_name, default_fallback)
603 if noscript_path is not None: 641 if noscript_path is not None:
604 css_files_noscript.append(self.getFrontURL(noscript_path)) 642 css_files_noscript.append(self.getFrontURL(noscript_path))
605 643
606 def getCSSFiles(self, template_data): 644 def getCSSFiles(self, template_data):
607 """Retrieve CSS files to use according template_data 645 """Retrieve CSS files to use according template_data
622 else default/static/blog.css (if it exists too) 660 else default/static/blog.css (if it exists too)
623 - some_theme/static/blog_articles.css is returned if it exists 661 - some_theme/static/blog_articles.css is returned if it exists
624 else default/static/blog_articles.css (if it exists too) 662 else default/static/blog_articles.css (if it exists too)
625 and for each found files, if same file with _noscript suffix exists, it is put 663 and for each found files, if same file with _noscript suffix exists, it is put
626 in noscript list (for instance (some_theme/static/styles_noscript.css)). 664 in noscript list (for instance (some_theme/static/styles_noscript.css)).
665 The behaviour may be changed with theme settings: if "css_default_fallback" is
666 False, only CSS from the theme is returned if it exists, default CSS is never
667 used.
627 @param template_data(TemplateData): data of the current template 668 @param template_data(TemplateData): data of the current template
628 @return (tuple[list[unicode], list[unicode]]): a tuple with: 669 @return (tuple[list[unicode], list[unicode]]): a tuple with:
629 - front URLs of CSS files to use 670 - front URLs of CSS files to use
630 - front URLs of CSS files to use when scripts are not enabled 671 - front URLs of CSS files to use when scripts are not enabled
631 """ 672 """
632 # TODO: some caching would be nice 673 # TODO: some caching would be nice
633 css_files = [] 674 css_files = []
634 css_files_noscript = [] 675 css_files_noscript = []
635 path_elems = template_data.path.split('/') 676 path_elems = template_data.path.split('/')
636 path_elems[-1] = os.path.splitext(path_elems[-1])[0] 677 path_elems[-1] = os.path.splitext(path_elems[-1])[0]
637 678 site = template_data.site
638 css_path = self.getStaticPath(template_data, 'fonts.css') 679 if site is None:
680 # absolute path
681 default_fallback = True
682 else:
683 default_fallback = (
684 self.sites_themes[site][template_data.theme]['settings']
685 ).get('css_default_fallback', True)
686
687 css_path = self.getStaticPath(template_data, 'fonts.css', default_fallback)
639 if css_path is not None: 688 if css_path is not None:
640 css_files.append(self.getFrontURL(css_path)) 689 css_files.append(self.getFrontURL(css_path))
641 690
642 for name_root in ('styles', 'styles_extra', 'highlight'): 691 for name_root in ('styles', 'styles_extra', 'highlight'):
643 self._appendCSSPaths(template_data, css_files, css_files_noscript, name_root) 692 self._appendCSSPaths(
693 template_data, css_files, css_files_noscript, name_root, default_fallback)
644 694
645 for idx in range(len(path_elems)): 695 for idx in range(len(path_elems)):
646 name_root = "_".join(path_elems[:idx+1]) 696 name_root = "_".join(path_elems[:idx+1])
647 self._appendCSSPaths(template_data, css_files, css_files_noscript, name_root) 697 self._appendCSSPaths(
698 template_data, css_files, css_files_noscript, name_root, default_fallback)
648 699
649 return css_files, css_files_noscript 700 return css_files, css_files_noscript
650 701
651 ## custom filters ## 702 ## custom filters ##
652 703