comparison sat/tools/common/template.py @ 3478:b65175eb7769

tools (common/template): new `fallback` settings: `fallback` can be used to change fallback behaviour. By default, fallback is done on `default` theme, this can be set to an other theme with a string, or to a list of fallback. The list can also be empty is no fallback is desired (notably usefull for "main" themes, on which other themes may fallback). This setting replaces `css_default_fallback`.
author Goffi <goffi@goffi.org>
date Fri, 19 Mar 2021 14:01:52 +0100
parents 29f8122f00f3
children be6d91572633
comparison
equal deleted inserted replaced
3477:9498f32ba6f7 3478:b65175eb7769
80 80
81 class TemplateLoader(jinja2.BaseLoader): 81 class TemplateLoader(jinja2.BaseLoader):
82 """A template loader which handle site, theme and absolute paths""" 82 """A template loader which handle site, theme and absolute paths"""
83 # TODO: list_templates should be implemented 83 # TODO: list_templates should be implemented
84 84
85 def __init__(self, sites_paths, trusted=False): 85 def __init__(self, sites_paths, sites_themes, trusted=False):
86 """ 86 """
87 @param trusted(bool): if True, absolue template paths will be allowed 87 @param trusted(bool): if True, absolue template paths will be allowed
88 be careful when using this option and sure that you can trust the template, 88 be careful when using this option and sure that you can trust the template,
89 as this allow the template to open any file on the system that the 89 as this allow the template to open any file on the system that the
90 launching user can access. 90 launching user can access.
91 """ 91 """
92 if not sites_paths or not "" in sites_paths: 92 if not sites_paths or not "" in sites_paths:
93 raise exceptions.InternalError("Invalid sites_paths") 93 raise exceptions.InternalError("Invalid sites_paths")
94 super(jinja2.BaseLoader, self).__init__() 94 super(jinja2.BaseLoader, self).__init__()
95 self.sites_paths = sites_paths 95 self.sites_paths = sites_paths
96 self.sites_themes = sites_themes
96 self.trusted = trusted 97 self.trusted = trusted
97 98
98 @staticmethod 99 @staticmethod
99 def parse_template(template): 100 def parse_template(template):
100 """Parse template path and return site, theme and path 101 """Parse template path and return site, theme and path
161 162
162 @staticmethod 163 @staticmethod
163 def getSitesAndThemes( 164 def getSitesAndThemes(
164 site: str, 165 site: str,
165 theme: str, 166 theme: str,
166 default_fallback: bool=True 167 settings: Optional[dict] = None,
167 ) -> List[Tuple[str, str]]: 168 ) -> List[Tuple[str, str]]:
168 """Get sites and themes to check for template/file 169 """Get sites and themes to check for template/file
169 170
170 Will add default theme and default site in search list when suitable 171 Will add default theme and default site in search list when suitable. Settings'
172 `fallback` can be used to modify behaviour: themes in this list will then be used
173 instead of default (it can also be empty list or None, in which case no fallback
174 is used).
175
171 @param site: site requested 176 @param site: site requested
172 @param theme: theme requested 177 @param theme: theme requested
173 @return: site and theme couples to check 178 @return: site and theme couples to check
174 """ 179 """
180 if settings is None:
181 settings = {}
175 sites_and_themes = [[site, theme]] 182 sites_and_themes = [[site, theme]]
176 if theme != C.TEMPLATE_THEME_DEFAULT and default_fallback: 183 fallback = settings.get("fallback", [C.TEMPLATE_THEME_DEFAULT])
177 sites_and_themes.append([site, C.TEMPLATE_THEME_DEFAULT]) 184 for fb_theme in fallback:
178 if site and default_fallback: 185 if theme != fb_theme:
179 # the site is not the default one, so we add default at the end 186 sites_and_themes.append([site, fb_theme])
180 sites_and_themes.append(['', C.TEMPLATE_THEME_DEFAULT]) 187 if site:
188 for fb_theme in fallback:
189 sites_and_themes.append(["", fb_theme])
181 return sites_and_themes 190 return sites_and_themes
182 191
183 def _get_template_f(self, site, theme, path_elts): 192 def _get_template_f(self, site, theme, path_elts):
184 """Look for template and return opened file if found 193 """Look for template and return opened file if found
185 194
192 - absolute file path, or None if not found 201 - absolute file path, or None if not found
193 """ 202 """
194 if site is None: 203 if site is None:
195 raise exceptions.InternalError( 204 raise exceptions.InternalError(
196 "_get_template_f must not be used with absolute path") 205 "_get_template_f must not be used with absolute path")
197 for site, theme in self.getSitesAndThemes(site, theme): 206 settings = self.sites_themes[site][theme]['settings']
207 for site_to_check, theme_to_check in self.getSitesAndThemes(
208 site, theme, settings):
198 try: 209 try:
199 base_path = self.sites_paths[site] 210 base_path = self.sites_paths[site_to_check]
200 except KeyError: 211 except KeyError:
201 log.warning(_("Unregistered site requested: {site}").format( 212 log.warning(_("Unregistered site requested: {site_to_check}").format(
202 site=site)) 213 site_to_check=site_to_check))
203 filepath = os.path.join(base_path, C.TEMPLATE_TPL_DIR, theme, *path_elts) 214 filepath = os.path.join(
215 base_path,
216 C.TEMPLATE_TPL_DIR,
217 theme_to_check,
218 *path_elts
219 )
204 f = utils.open_if_exists(filepath, 'r') 220 f = utils.open_if_exists(filepath, 'r')
205 if f is not None: 221 if f is not None:
206 return f, filepath 222 return f, filepath
207 return None, None 223 return None, None
208 224
396 try: 412 try:
397 with theme_settings.open() as f: 413 with theme_settings.open() as f:
398 settings = json.load(f) 414 settings = json.load(f)
399 except Exception as e: 415 except Exception as e:
400 log.warning(_( 416 log.warning(_(
401 "Can't load theme settings at {path}").format( 417 "Can't load theme settings at {path}: {e}").format(
402 path=theme_settings)) 418 path=theme_settings, e=e))
403 else: 419 else:
404 log.debug( 420 log.debug(
405 f"found settings for theme {p.name!r} at {theme_settings}") 421 f"found settings for theme {p.name!r} at {theme_settings}")
422 fallback = settings.get("fallback")
423 if fallback is None:
424 settings["fallback"] = []
425 elif isinstance(fallback, str):
426 settings["fallback"] = [fallback]
427 elif not isinstance(fallback, list):
428 raise ValueError(
429 'incorrect type for "fallback" in settings '
430 f'({type(fallback)}) at {theme_settings}: {fallback}'
431 )
406 theme_data['settings'] = settings 432 theme_data['settings'] = settings
407 browser_path = p / BROWSER_DIR 433 browser_path = p / BROWSER_DIR
408 if browser_path.is_dir(): 434 if browser_path.is_dir():
409 theme_data['browser_path'] = browser_path 435 theme_data['browser_path'] = browser_path
410 browser_meta_path = browser_path / BROWSER_META_FILE 436 browser_meta_path = browser_path / BROWSER_META_FILE
417 f"Can't parse browser metadata at {browser_meta_path}: {e}" 443 f"Can't parse browser metadata at {browser_meta_path}: {e}"
418 ) 444 )
419 continue 445 continue
420 446
421 self.env = Environment( 447 self.env = Environment(
422 loader=TemplateLoader(sites_paths=self.sites_paths, trusted=trusted), 448 loader=TemplateLoader(
449 sites_paths=self.sites_paths,
450 sites_themes=self.sites_themes,
451 trusted=trusted
452 ),
423 autoescape=jinja2.select_autoescape(["html", "xhtml", "xml"]), 453 autoescape=jinja2.select_autoescape(["html", "xhtml", "xml"]),
424 trim_blocks=True, 454 trim_blocks=True,
425 lstrip_blocks=True, 455 lstrip_blocks=True,
426 extensions=["jinja2.ext.i18n"], 456 extensions=["jinja2.ext.i18n"],
427 ) 457 )
548 578
549 self._locale = locale 579 self._locale = locale
550 self._locale_str = locale_str 580 self._locale_str = locale_str
551 581
552 def getThemeAndRoot(self, template): 582 def getThemeAndRoot(self, template):
553 """retrieve theme and root dir of a given tempalte 583 """retrieve theme and root dir of a given template
554 584
555 @param template(unicode): template to parse 585 @param template(unicode): template to parse
556 @return (tuple[unicode, unicode]): theme and absolute path to theme's root dir 586 @return (tuple[unicode, unicode]): theme and absolute path to theme's root dir
557 @raise NotFound: requested site has not been found 587 @raise NotFound: requested site has not been found
558 """ 588 """
575 605
576 def getStaticPath( 606 def getStaticPath(
577 self, 607 self,
578 template_data: TemplateData, 608 template_data: TemplateData,
579 filename: str, 609 filename: str,
580 default_fallback: bool=True 610 settings: Optional[dict]=None
581 ) -> Optional[TemplateData]: 611 ) -> Optional[TemplateData]:
582 """Retrieve path of a static file if it exists with current theme or default 612 """Retrieve path of a static file if it exists with current theme or default
583 613
584 File will be looked at <site_root_dir>/<theme_dir>/<static_dir>/filename, 614 File will be looked at <site_root_dir>/<theme_dir>/<static_dir>/filename,
585 then <site_root_dir>/<default_theme_dir>/<static_dir>/filename anf finally 615 then <site_root_dir>/<default_theme_dir>/<static_dir>/filename anf finally
587 In case of absolute URL, base dir of template is used as base. For instance if 617 In case of absolute URL, base dir of template is used as base. For instance if
588 template is an absolute template to "/some/path/template.html", file will be 618 template is an absolute template to "/some/path/template.html", file will be
589 checked at "/some/path/<filename>" 619 checked at "/some/path/<filename>"
590 @param template_data: data of current template 620 @param template_data: data of current template
591 @param filename: name of the file to retrieve 621 @param filename: name of the file to retrieve
592 @param default_fallback: if True, default theme will be checked if the file is 622 @param settings: theme settings, can be used to modify behaviour
593 not found in current theme, then default site with default theme will be used.
594 @return: built template data instance where .path is 623 @return: built template data instance where .path is
595 the relative path to the file, from theme root dir. 624 the relative path to the file, from theme root dir.
596 None if not found. 625 None if not found.
597 """ 626 """
598 if template_data.site is None: 627 if template_data.site is None:
608 else: 637 else:
609 return None 638 return None
610 639
611 sites_and_themes = TemplateLoader.getSitesAndThemes(template_data.site, 640 sites_and_themes = TemplateLoader.getSitesAndThemes(template_data.site,
612 template_data.theme, 641 template_data.theme,
613 default_fallback) 642 settings)
614 for site, theme in sites_and_themes: 643 for site, theme in sites_and_themes:
615 site_root_dir = self.sites_paths[site] 644 site_root_dir = self.sites_paths[site]
616 relative_path = os.path.join(C.TEMPLATE_STATIC_DIR, filename) 645 relative_path = os.path.join(C.TEMPLATE_STATIC_DIR, filename)
617 absolute_path = os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, 646 absolute_path = os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR,
618 theme, relative_path) 647 theme, relative_path)
625 self, 654 self,
626 template_data: TemplateData, 655 template_data: TemplateData,
627 css_files: list, 656 css_files: list,
628 css_files_noscript: list, 657 css_files_noscript: list,
629 name_root: str, 658 name_root: str,
630 default_fallback: bool 659 settings: dict
631 660
632 ) -> None: 661 ) -> None:
633 """Append found css to css_files and css_files_noscript 662 """Append found css to css_files and css_files_noscript
634 663
635 @param css_files: list to fill of relative path to found css file 664 @param css_files: list to fill of relative path to found css file
636 @param css_files_noscript: list to fill of relative path to found css file 665 @param css_files_noscript: list to fill of relative path to found css file
637 with "_noscript" suffix 666 with "_noscript" suffix
638 """ 667 """
639 name = name_root + ".css" 668 name = name_root + ".css"
640 css_path = self.getStaticPath(template_data, name, default_fallback) 669 css_path = self.getStaticPath(template_data, name, settings)
641 if css_path is not None: 670 if css_path is not None:
642 css_files.append(self.getFrontURL(css_path)) 671 css_files.append(self.getFrontURL(css_path))
643 noscript_name = name_root + "_noscript.css" 672 noscript_name = name_root + "_noscript.css"
644 noscript_path = self.getStaticPath( 673 noscript_path = self.getStaticPath(
645 template_data, noscript_name, default_fallback) 674 template_data, noscript_name, settings)
646 if noscript_path is not None: 675 if noscript_path is not None:
647 css_files_noscript.append(self.getFrontURL(noscript_path)) 676 css_files_noscript.append(self.getFrontURL(noscript_path))
648 677
649 def getCSSFiles(self, template_data): 678 def getCSSFiles(self, template_data):
650 """Retrieve CSS files to use according template_data 679 """Retrieve CSS files to use according template_data
665 else default/static/blog.css (if it exists too) 694 else default/static/blog.css (if it exists too)
666 - some_theme/static/blog_articles.css is returned if it exists 695 - some_theme/static/blog_articles.css is returned if it exists
667 else default/static/blog_articles.css (if it exists too) 696 else default/static/blog_articles.css (if it exists too)
668 and for each found files, if same file with _noscript suffix exists, it is put 697 and for each found files, if same file with _noscript suffix exists, it is put
669 in noscript list (for instance (some_theme/static/styles_noscript.css)). 698 in noscript list (for instance (some_theme/static/styles_noscript.css)).
670 The behaviour may be changed with theme settings: if "css_default_fallback" is 699 The behaviour may be changed with theme settings: if "fallback" is set, specified
671 False, only CSS from the theme is returned if it exists, default CSS is never 700 themes will be checked instead of default. The theme will be checked in given
672 used. 701 order, and "fallback" may be None or empty list to not check anything.
673 @param template_data(TemplateData): data of the current template 702 @param template_data(TemplateData): data of the current template
674 @return (tuple[list[unicode], list[unicode]]): a tuple with: 703 @return (tuple[list[unicode], list[unicode]]): a tuple with:
675 - front URLs of CSS files to use 704 - front URLs of CSS files to use
676 - front URLs of CSS files to use when scripts are not enabled 705 - front URLs of CSS files to use when scripts are not enabled
677 """ 706 """
681 path_elems = template_data.path.split('/') 710 path_elems = template_data.path.split('/')
682 path_elems[-1] = os.path.splitext(path_elems[-1])[0] 711 path_elems[-1] = os.path.splitext(path_elems[-1])[0]
683 site = template_data.site 712 site = template_data.site
684 if site is None: 713 if site is None:
685 # absolute path 714 # absolute path
686 default_fallback = True 715 settings = {}
687 else: 716 else:
688 default_fallback = ( 717 settings = self.sites_themes[site][template_data.theme]['settings']
689 self.sites_themes[site][template_data.theme]['settings'] 718
690 ).get('css_default_fallback', True) 719 css_path = self.getStaticPath(template_data, 'fonts.css', settings)
691
692 css_path = self.getStaticPath(template_data, 'fonts.css', default_fallback)
693 if css_path is not None: 720 if css_path is not None:
694 css_files.append(self.getFrontURL(css_path)) 721 css_files.append(self.getFrontURL(css_path))
695 722
696 for name_root in ('styles', 'styles_extra', 'highlight'): 723 for name_root in ('styles', 'styles_extra', 'highlight'):
697 self._appendCSSPaths( 724 self._appendCSSPaths(
698 template_data, css_files, css_files_noscript, name_root, default_fallback) 725 template_data, css_files, css_files_noscript, name_root, settings)
699 726
700 for idx in range(len(path_elems)): 727 for idx in range(len(path_elems)):
701 name_root = "_".join(path_elems[:idx+1]) 728 name_root = "_".join(path_elems[:idx+1])
702 self._appendCSSPaths( 729 self._appendCSSPaths(
703 template_data, css_files, css_files_noscript, name_root, default_fallback) 730 template_data, css_files, css_files_noscript, name_root, settings)
704 731
705 return css_files, css_files_noscript 732 return css_files, css_files_noscript
706 733
707 ## custom filters ## 734 ## custom filters ##
708 735