Mercurial > libervia-backend
comparison sat/tools/common/template.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | src/tools/common/template.py@00480cf83fa1 |
children | 5b26033c49a8 |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT: a jabber client | |
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 """ template generation """ | |
21 | |
22 from sat.core.constants import Const as C | |
23 from sat.core.i18n import _ | |
24 from sat.core import exceptions | |
25 from sat.core.log import getLogger | |
26 log = getLogger(__name__) | |
27 import os.path | |
28 from xml.sax.saxutils import quoteattr | |
29 import datetime | |
30 import time | |
31 import re | |
32 from babel import support | |
33 from babel import Locale | |
34 from babel.core import UnknownLocaleError | |
35 from babel import dates | |
36 import pygments | |
37 from pygments import lexers | |
38 from pygments import formatters | |
39 try: | |
40 import sat_templates | |
41 except ImportError: | |
42 raise exceptions.MissingModule(u'sat_templates module is not available, please install it or check your path to use template engine') | |
43 else: | |
44 sat_templates # to avoid pyflakes warning | |
45 | |
46 try: | |
47 import jinja2 | |
48 except: | |
49 raise exceptions.MissingModule(u'Missing module jinja2, please install it from http://jinja.pocoo.org or with pip install jinja2') | |
50 | |
51 from jinja2 import Markup as safe | |
52 from jinja2 import is_undefined | |
53 from lxml import etree | |
54 | |
55 HTML_EXT = ('html', 'xhtml') | |
56 DEFAULT_LOCALE = u'en_GB' | |
57 RE_ATTR_ESCAPE = re.compile(r'[^a-z_-]') | |
58 # TODO: handle external path (an additional search path for templates should be settable by user | |
59 # TODO: handle absolute URL (should be used for trusted use cases) only (e.g. jp) for security reason | |
60 | |
61 | |
62 class TemplateLoader(jinja2.FileSystemLoader): | |
63 | |
64 def __init__(self): | |
65 searchpath = os.path.dirname(sat_templates.__file__) | |
66 super(TemplateLoader, self).__init__(searchpath, followlinks=True) | |
67 | |
68 def parse_template(self, template): | |
69 """parse template path and return theme and relative URL | |
70 | |
71 @param template_path(unicode): path to template with parenthesis syntax | |
72 @return (tuple[(unicode,None),unicode]): theme and template_path | |
73 theme can be None if relative path is used | |
74 relative path is the path from search path with theme specified | |
75 e.g. default/blog/articles.html | |
76 """ | |
77 if template.startswith(u'('): | |
78 try: | |
79 theme_end = template.index(u')') | |
80 except IndexError: | |
81 raise ValueError(u"incorrect theme in template") | |
82 theme = template[1:theme_end] | |
83 template = template[theme_end+1:] | |
84 if not template or template.startswith(u'/'): | |
85 raise ValueError(u"incorrect path after template name") | |
86 template = os.path.join(theme, template) | |
87 elif template.startswith(u'/'): | |
88 # absolute path means no template | |
89 theme = None | |
90 raise NotImplementedError(u'absolute path is not implemented yet') | |
91 else: | |
92 theme = C.TEMPLATE_THEME_DEFAULT | |
93 template = os.path.join(theme, template) | |
94 return theme, template | |
95 | |
96 def get_default_template(self, theme, template_path): | |
97 """return default template path | |
98 | |
99 @param theme(unicode): theme used | |
100 @param template_path(unicode): path to the not found template | |
101 @return (unicode, None): default path or None if there is not | |
102 """ | |
103 ext = os.path.splitext(template_path)[1][1:] | |
104 path_elems = template_path.split(u'/') | |
105 if ext in HTML_EXT: | |
106 if path_elems[1] == u'error': | |
107 # if an inexisting error page is requested, we return base page | |
108 default_path = os.path.join(theme, u'error/base.html') | |
109 return default_path | |
110 if theme != C.TEMPLATE_THEME_DEFAULT: | |
111 # if template doesn't exists for this theme, we try with default | |
112 return os.path.join(C.TEMPLATE_THEME_DEFAULT, path_elems[1:]) | |
113 | |
114 def get_source(self, environment, template): | |
115 """relative path to template dir, with special theme handling | |
116 | |
117 if the path is just relative, "default" theme is used. | |
118 The theme can be specified in parenthesis just before the path | |
119 e.g.: (some_theme)path/to/template.html | |
120 """ | |
121 theme, template_path = self.parse_template(template) | |
122 try: | |
123 return super(TemplateLoader, self).get_source(environment, template_path) | |
124 except jinja2.exceptions.TemplateNotFound as e: | |
125 # in some special cases, a defaut template is returned if nothing is found | |
126 if theme is not None: | |
127 default_path = self.get_default_template(theme, template_path) | |
128 if default_path is not None: | |
129 return super(TemplateLoader, self).get_source(environment, default_path) | |
130 # if no default template is found, we re-raise the error | |
131 raise e | |
132 | |
133 | |
134 class Indexer(object): | |
135 """Index global to a page""" | |
136 | |
137 def __init__(self): | |
138 self._indexes = {} | |
139 | |
140 def next(self, value): | |
141 if value not in self._indexes: | |
142 self._indexes[value] = 0 | |
143 return 0 | |
144 self._indexes[value] += 1 | |
145 return self._indexes[value] | |
146 | |
147 def current(self, value): | |
148 return self._indexes.get(value) | |
149 | |
150 | |
151 class ScriptsHandler(object): | |
152 | |
153 def __init__(self, renderer, template_path, template_root_dir, root_path): | |
154 self.renderer = renderer | |
155 self.template_root_dir = template_root_dir | |
156 self.root_path = root_path | |
157 self.scripts = [] # we don't use a set because order may be important | |
158 dummy, self.theme, self.is_default_theme = renderer.getThemeData(template_path) | |
159 | |
160 def include(self, library_name, attribute='defer'): | |
161 """Mark that a script need to be imported. | |
162 | |
163 Must be used before base.html is extended, as <script> are generated there. | |
164 If called several time with the same library, it will be imported once. | |
165 @param library_name(unicode): name of the library to import | |
166 @param loading: | |
167 """ | |
168 if attribute not in ('defer', 'async', ''): | |
169 raise exceptions.DataError(_(u'Invalid attribute, please use one of "defer", "async" or ""')) | |
170 if library_name.endswith('.js'): | |
171 library_name = library_name[:-3] | |
172 if library_name not in self.scripts: | |
173 self.scripts.append((library_name, attribute)) | |
174 return u'' | |
175 | |
176 def generate_scripts(self): | |
177 """Generate the <script> elements | |
178 | |
179 @return (unicode): <scripts> HTML tags | |
180 """ | |
181 scripts = [] | |
182 tpl = u'<script src={src} {attribute}></script>' | |
183 for library, attribute in self.scripts: | |
184 path = self.renderer.getStaticPath(library, self.template_root_dir, self.theme, self.is_default_theme, '.js') | |
185 if path is None: | |
186 log.warning(_(u"Can't find {}.js javascript library").format(library)) | |
187 continue | |
188 path = os.path.join(self.root_path, path) | |
189 scripts.append(tpl.format( | |
190 src = quoteattr(path), | |
191 attribute = attribute, | |
192 )) | |
193 return safe(u'\n'.join(scripts)) | |
194 | |
195 | |
196 class Renderer(object): | |
197 | |
198 def __init__(self, host): | |
199 self.host = host | |
200 self.base_dir = os.path.dirname(sat_templates.__file__) # FIXME: should be modified if we handle use extra dirs | |
201 self.env = jinja2.Environment( | |
202 loader=TemplateLoader(), | |
203 autoescape=jinja2.select_autoescape(['html', 'xhtml', 'xml']), | |
204 trim_blocks=True, | |
205 lstrip_blocks=True, | |
206 extensions=['jinja2.ext.i18n'], | |
207 ) | |
208 self._locale_str = DEFAULT_LOCALE | |
209 self._locale = Locale.parse(self._locale_str) | |
210 self.installTranslations() | |
211 # we want to have access to SàT constants in templates | |
212 self.env.globals[u'C'] = C | |
213 # custom filters | |
214 self.env.filters['next_gidx'] = self._next_gidx | |
215 self.env.filters['cur_gidx'] = self._cur_gidx | |
216 self.env.filters['date_fmt'] = self._date_fmt | |
217 self.env.filters['xmlui_class'] = self._xmlui_class | |
218 self.env.filters['attr_escape'] = self.attr_escape | |
219 self.env.filters['item_filter'] = self._item_filter | |
220 self.env.filters['adv_format'] = self._adv_format | |
221 self.env.filters['dict_ext'] = self._dict_ext | |
222 self.env.filters['highlight'] = self.highlight | |
223 # custom tests | |
224 self.env.tests['in_the_past'] = self._in_the_past | |
225 self.icons_path = os.path.join(host.media_dir, u'fonts/fontello/svg') | |
226 | |
227 def installTranslations(self): | |
228 i18n_dir = os.path.join(self.base_dir, 'i18n') | |
229 self.translations = {} | |
230 for lang_dir in os.listdir(i18n_dir): | |
231 lang_path = os.path.join(i18n_dir, lang_dir) | |
232 if not os.path.isdir(lang_path): | |
233 continue | |
234 po_path = os.path.join(lang_path, 'LC_MESSAGES/sat.mo') | |
235 try: | |
236 with open(po_path, 'rb') as f: | |
237 self.translations[Locale.parse(lang_dir)] = support.Translations(f, 'sat') | |
238 except EnvironmentError: | |
239 log.error(_(u"Can't find template translation at {path}").format(path = po_path)) | |
240 except UnknownLocaleError as e: | |
241 log.error(_(u"Invalid locale name: {msg}").format(msg=e)) | |
242 else: | |
243 log.info(_(u'loaded {lang} templates translations').format(lang=lang_dir)) | |
244 self.env.install_null_translations(True) | |
245 | |
246 def setLocale(self, locale_str): | |
247 """set current locale | |
248 | |
249 change current translation locale and self self._locale and self._locale_str | |
250 """ | |
251 if locale_str == self._locale_str: | |
252 return | |
253 if locale_str == 'en': | |
254 # we default to GB English when it's not specified | |
255 # one of the main reason is to avoid the nonsense U.S. short date format | |
256 locale_str = 'en_GB' | |
257 try: | |
258 locale = Locale.parse(locale_str) | |
259 except ValueError as e: | |
260 log.warning(_(u"invalid locale value: {msg}").format(msg=e)) | |
261 locale_str = self._locale_str = DEFAULT_LOCALE | |
262 locale = Locale.parse(locale_str) | |
263 | |
264 locale_str = unicode(locale) | |
265 if locale_str != DEFAULT_LOCALE: | |
266 try: | |
267 translations = self.translations[locale] | |
268 except KeyError: | |
269 log.warning(_(u"Can't find locale {locale}".format(locale=locale))) | |
270 locale_str = DEFAULT_LOCALE | |
271 locale = Locale.parse(self._locale_str) | |
272 else: | |
273 self.env.install_gettext_translations(translations, True) | |
274 log.debug(_(u'Switched to {lang}').format(lang=locale.english_name)) | |
275 | |
276 if locale_str == DEFAULT_LOCALE: | |
277 self.env.install_null_translations(True) | |
278 | |
279 self._locale = locale | |
280 self._locale_str = locale_str | |
281 | |
282 def getThemeAndRoot(self, template): | |
283 """retrieve theme and root dir of a given tempalte | |
284 | |
285 @param template(unicode): template to parse | |
286 @return (tuple[unicode, unicode]): theme and absolute path to theme's root dir | |
287 """ | |
288 theme, dummy = self.env.loader.parse_template(template) | |
289 return theme, os.path.join(self.base_dir, theme) | |
290 | |
291 def getStaticPath(self, name, template_root_dir, theme, is_default, ext='.css'): | |
292 """retrieve path of a static file if it exists with current theme or default | |
293 | |
294 File will be looked at [theme]/static/[name][ext], and then default | |
295 if not found. | |
296 @param name(unicode): name of the file to look for | |
297 @param template_root_dir(unicode): absolute path to template root used | |
298 @param theme(unicode): name of the template theme used | |
299 @param is_default(bool): True if theme is the default theme | |
300 @return (unicode, None): relative path if found, else None | |
301 """ | |
302 file_ = None | |
303 path = os.path.join(theme, C.TEMPLATE_STATIC_DIR, name + ext) | |
304 if os.path.exists(os.path.join(template_root_dir, path)): | |
305 file_ = path | |
306 elif not is_default: | |
307 path = os.path.join(C.TEMPLATE_THEME_DEFAULT, C.TEMPLATE_STATIC_DIR, name + ext) | |
308 if os.path.exists(os.path.join(template_root_dir, path)): | |
309 file_.append(path) | |
310 return file_ | |
311 | |
312 def getThemeData(self, template_path): | |
313 """return template data got from template_path | |
314 | |
315 @return tuple(unicode, unicode, bool): | |
316 path_elems: elements of the path | |
317 theme: theme of the page | |
318 is_default: True if the theme is the default theme | |
319 """ | |
320 path_elems = [os.path.splitext(p)[0] for p in template_path.split(u'/')] | |
321 theme = path_elems.pop(0) | |
322 is_default = theme == C.TEMPLATE_THEME_DEFAULT | |
323 return (path_elems, theme, is_default) | |
324 | |
325 def getCSSFiles(self, template_path, template_root_dir): | |
326 """retrieve CSS files to use according to theme and template path | |
327 | |
328 for each element of the path, a .css file is looked for in /static, and returned if it exists. | |
329 previous element are kept by replacing '/' with '_', and styles.css is always returned. | |
330 For instance, if template_path is some_theme/blog/articles.html: | |
331 some_theme/static/styles.css is returned if it exists else default/static/styles.css | |
332 some_theme/static/blog.css is returned if it exists else default/static/blog.css (if it exists too) | |
333 some_theme/static/blog_articles.css is returned if it exists else default/static/blog_articles.css (if it exists too) | |
334 @param template_path(unicode): relative path to template file (e.g. some_theme/blog/articles.html) | |
335 @param template_root_dir(unicode): absolute path of the theme root dir used | |
336 @return list[unicode]: relative path to CSS files to use | |
337 """ | |
338 # TODO: some caching would be nice | |
339 css_files = [] | |
340 path_elems, theme, is_default = self.getThemeData(template_path) | |
341 for css in (u'fonts', u'styles'): | |
342 css_path = self.getStaticPath(css, template_root_dir, theme, is_default) | |
343 if css_path is not None: | |
344 css_files.append(css_path) | |
345 | |
346 for idx, path in enumerate(path_elems): | |
347 css_path = self.getStaticPath(u'_'.join(path_elems[:idx+1]), template_root_dir, theme, is_default) | |
348 if css_path is not None: | |
349 css_files.append(css_path) | |
350 | |
351 return css_files | |
352 | |
353 | |
354 ## custom filters ## | |
355 | |
356 @jinja2.contextfilter | |
357 def _next_gidx(self, ctx, value): | |
358 """Use next current global index as suffix""" | |
359 next_ = ctx['gidx'].next(value) | |
360 return value if next_ == 0 else u"{}_{}".format(value, next_) | |
361 | |
362 @jinja2.contextfilter | |
363 def _cur_gidx(self, ctx, value): | |
364 """Use current current global index as suffix""" | |
365 current = ctx['gidx'].current(value) | |
366 return value if not current else u"{}_{}".format(value, current) | |
367 | |
368 def _date_fmt(self, timestamp, fmt='short', date_only=False, auto_limit=None, auto_old_fmt=None): | |
369 try: | |
370 return self.date_fmt(timestamp, fmt, date_only, auto_limit, auto_old_fmt) | |
371 except Exception as e: | |
372 log.warning(_(u"Can't parse date: {msg}").format(msg=e)) | |
373 return timestamp | |
374 | |
375 def date_fmt(self, timestamp, fmt='short', date_only=False, auto_limit=7, auto_old_fmt='short', auto_new_fmt='relative'): | |
376 """format date according to locale | |
377 | |
378 @param timestamp(basestring, int): unix time | |
379 @param fmt(str): one of: | |
380 - short: e.g. u'31/12/17' | |
381 - medium: e.g. u'Apr 1, 2007' | |
382 - long: e.g. u'April 1, 2007' | |
383 - full: e.g. u'Sunday, April 1, 2007' | |
384 - relative: format in relative time | |
385 e.g.: 3 hours | |
386 note that this format is not precise | |
387 - iso: ISO 8601 format | |
388 e.g.: u'2007-04-01T19:53:23Z' | |
389 - auto: use auto_old_fmt if date is older than auto_limit | |
390 else use auto_new_fmt | |
391 - auto_day: shorcut to set auto format with change on day | |
392 old format will be short, and new format will be time only | |
393 or a free value which is passed to babel.dates.format_datetime | |
394 @param date_only(bool): if True, only display date (not datetime) | |
395 @param auto_limit (int): limit in days before using auto_old_fmt | |
396 use 0 to have a limit at last midnight (day change) | |
397 @param auto_old_fmt(unicode): format to use when date is older than limit | |
398 @param auto_new_fmt(unicode): format to use when date is equal to or more recent | |
399 than limit | |
400 | |
401 """ | |
402 if is_undefined(fmt): | |
403 fmt = u'short' | |
404 | |
405 if (auto_limit is not None or auto_old_fmt is not None) and fmt != 'auto': | |
406 raise ValueError(u'auto argument can only be used with auto fmt') | |
407 if fmt == 'auto_day': | |
408 fmt, auto_limit, auto_old_fmt, auto_new_fmt = 'auto', 0, 'short', 'HH:mm' | |
409 if fmt == 'auto': | |
410 if auto_limit == 0: | |
411 today = time.mktime(datetime.date.today().timetuple()) | |
412 if int(timestamp) < today: | |
413 fmt = auto_old_fmt | |
414 else: | |
415 fmt = auto_new_fmt | |
416 else: | |
417 days_delta = (time.time() - int(timestamp)) / 3600 | |
418 if days_delta > (auto_limit or 7): | |
419 fmt = auto_old_fmt | |
420 else: | |
421 fmt = auto_new_fmt | |
422 | |
423 if fmt == 'relative': | |
424 delta = int(timestamp) - time.time() | |
425 return dates.format_timedelta(delta, granularity="minute", add_direction=True, locale=self._locale_str) | |
426 elif fmt in ('short', 'long'): | |
427 formatter = dates.format_date if date_only else dates.format_datetime | |
428 return formatter(int(timestamp), format=fmt, locale=self._locale_str) | |
429 elif fmt == 'iso': | |
430 if date_only: | |
431 fmt = 'yyyy-MM-dd' | |
432 else: | |
433 fmt = "yyyy-MM-ddTHH:mm:ss'Z'" | |
434 return dates.format_datetime(int(timestamp), format=fmt) | |
435 else: | |
436 return dates.format_datetime(int(timestamp), format=fmt, locale=self._locale_str) | |
437 | |
438 def attr_escape(self, text): | |
439 """escape a text to a value usable as an attribute | |
440 | |
441 remove spaces, and put in lower case | |
442 """ | |
443 return RE_ATTR_ESCAPE.sub(u'_', text.strip().lower())[:50] | |
444 | |
445 def _xmlui_class(self, xmlui_item, fields): | |
446 """return classes computed from XMLUI fields name | |
447 | |
448 will return a string with a series of escaped {name}_{value} separated by spaces. | |
449 @param xmlui_item(xmlui.XMLUIPanel): XMLUI containing the widgets to use | |
450 @param fields(iterable(unicode)): names of the widgets to use | |
451 @return (unicode, None): computer string to use as class attribute value | |
452 None if no field was specified | |
453 """ | |
454 classes = [] | |
455 for name in fields: | |
456 escaped_name = self.attr_escape(name) | |
457 try: | |
458 for value in xmlui_item.widgets[name].values: | |
459 classes.append(escaped_name + '_' + self.attr_escape(value)) | |
460 except KeyError: | |
461 log.debug(_(u"ignoring field \"{name}\": it doesn't exists").format(name=name)) | |
462 continue | |
463 return u' '.join(classes) or None | |
464 | |
465 @jinja2.contextfilter | |
466 def _item_filter(self, ctx, item, filters): | |
467 """return item's value, filtered if suitable | |
468 | |
469 @param item(object): item to filter | |
470 value must have name and value attributes, | |
471 mostly used for XMLUI items | |
472 @param filters(dict[unicode, (callable, dict, None)]): map of name => filter | |
473 if filter is None, return the value unchanged | |
474 if filter is a callable, apply it | |
475 if filter is a dict, it can have following keys: | |
476 - filters: iterable of filters to apply | |
477 - filters_args: kwargs of filters in the same order as filters (use empty dict if needed) | |
478 - template: template to format where {value} is the filtered value | |
479 """ | |
480 value = item.value | |
481 filter_ = filters.get(item.name, None) | |
482 if filter_ is None: | |
483 return value | |
484 elif isinstance(filter_, dict): | |
485 filters_args = filter_.get(u'filters_args') | |
486 for idx, f_name in enumerate(filter_.get(u'filters', [])): | |
487 kwargs = filters_args[idx] if filters_args is not None else {} | |
488 filter_func = self.env.filters[f_name] | |
489 try: | |
490 eval_context_filter = filter_func.evalcontextfilter | |
491 except AttributeError: | |
492 eval_context_filter = False | |
493 | |
494 if eval_context_filter: | |
495 value = filter_func(ctx.eval_ctx, value, **kwargs) | |
496 else: | |
497 value = filter_func(value, **kwargs) | |
498 template = filter_.get(u'template') | |
499 if template: | |
500 # format will return a string, so we need to check first | |
501 # if the value is safe or not, and re-mark it after formatting | |
502 is_safe = isinstance(value, safe) | |
503 value = template.format(value=value) | |
504 if is_safe: | |
505 value = safe(value) | |
506 return value | |
507 | |
508 def _adv_format(self, value, template, **kwargs): | |
509 """Advancer formatter | |
510 | |
511 like format() method, but take care or special values like None | |
512 @param value(unicode): value to format | |
513 @param template(None, unicode): template to use with format() method. | |
514 It will be formatted using value=value and **kwargs | |
515 None to return value unchanged | |
516 @return (unicode): formatted value | |
517 """ | |
518 if template is None: | |
519 return value | |
520 # jinja use string when no special char is used, so we have to convert to unicode | |
521 return unicode(template).format(value=value, **kwargs) | |
522 | |
523 def _dict_ext(self, source_dict, extra_dict, key=None): | |
524 """extend source_dict with extra dict and return the result | |
525 | |
526 @param source_dict(dict): dictionary to extend | |
527 @param extra_dict(dict, None): dictionary to use to extend first one | |
528 None to return source_dict unmodified | |
529 @param key(unicode, None): if specified extra_dict[key] will be used | |
530 if it doesn't exists, a copy of unmodified source_dict is returned | |
531 @return (dict): resulting dictionary | |
532 """ | |
533 if extra_dict is None: | |
534 return source_dict | |
535 if key is not None: | |
536 extra_dict = extra_dict.get(key, {}) | |
537 ret = source_dict.copy() | |
538 ret.update(extra_dict) | |
539 return ret | |
540 | |
541 def highlight(self, code, lexer_name=None, lexer_opts=None, html_fmt_opts=None): | |
542 """Do syntax highlighting on code | |
543 | |
544 under the hood, pygments is used, check its documentation for options possible values | |
545 @param code(unicode): code or markup to highlight | |
546 @param lexer_name(unicode, None): name of the lexer to use | |
547 None to autodetect it | |
548 @param html_fmt_opts(dict, None): kword arguments to use for HtmlFormatter | |
549 @return (unicode): HTML markup with highlight classes | |
550 """ | |
551 if lexer_opts is None: | |
552 lexer_opts = {} | |
553 if html_fmt_opts is None: | |
554 html_fmt_opts = {} | |
555 if lexer_name is None: | |
556 lexer = lexers.guess_lexer(code, **lexer_opts) | |
557 else: | |
558 lexer = lexers.get_lexer_by_name(lexer_name, **lexer_opts) | |
559 formatter = formatters.HtmlFormatter(**html_fmt_opts) | |
560 return safe(pygments.highlight(code, lexer, formatter)) | |
561 | |
562 ## custom tests ## | |
563 | |
564 def _in_the_past(self, timestamp): | |
565 """check if a date is in the past | |
566 | |
567 @param timestamp(unicode, int): unix time | |
568 @return (bool): True if date is in the past | |
569 """ | |
570 return time.time() > int(timestamp) | |
571 | |
572 ## template methods ## | |
573 | |
574 def _icon_defs(self, *names): | |
575 """Define svg icons which will be used in the template, and use their name as id""" | |
576 svg_elt = etree.Element('svg', nsmap={None: 'http://www.w3.org/2000/svg'}, | |
577 width='0', height='0', style='display: block' | |
578 ) | |
579 defs_elt = etree.SubElement(svg_elt, 'defs') | |
580 for name in names: | |
581 path = os.path.join(self.icons_path, name + u'.svg') | |
582 icon_svg_elt = etree.parse(path).getroot() | |
583 # we use icon name as id, so we can retrieve them easily | |
584 icon_svg_elt.set('id', name) | |
585 if not icon_svg_elt.tag == '{http://www.w3.org/2000/svg}svg': | |
586 raise exceptions.DataError(u'invalid SVG element') | |
587 defs_elt.append(icon_svg_elt) | |
588 return safe(etree.tostring(svg_elt, encoding='unicode')) | |
589 | |
590 def _icon_use(self, name, cls=''): | |
591 return safe(u"""<svg class="svg-icon{cls}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> | |
592 <use href="#{name}"/> | |
593 </svg> | |
594 """.format( | |
595 name=name, | |
596 cls=(' ' + cls) if cls else '')) | |
597 | |
598 def render(self, template, theme=None, locale=DEFAULT_LOCALE, root_path=u'', media_path=u'', css_files=None, css_inline=False, **kwargs): | |
599 """render a template | |
600 . | |
601 @param template(unicode): template to render (e.g. blog/articles.html) | |
602 @param theme(unicode): template theme | |
603 @param root_path(unicode): prefix of the path/URL to use for template root | |
604 must end with a u'/' | |
605 @param media_path(unicode): prefix of the SàT media path/URL to use for template root | |
606 must end with a u'/' | |
607 @param css_files(list[unicode],None): CSS files to used | |
608 CSS files must be in static dir of the template | |
609 use None for automatic selection of CSS files based on template category | |
610 None is recommended. General static/style.css and theme file name will be used. | |
611 @param css_inline(bool): if True, CSS will be embedded in the HTML page | |
612 @param **kwargs: variable to transmit to the template | |
613 """ | |
614 if not template: | |
615 raise ValueError(u"template can't be empty") | |
616 if theme is not None: | |
617 # use want to set a theme, we add it to the template path | |
618 if template[0] == u'(': | |
619 raise ValueError(u"you can't specify theme in template path and in argument at the same time") | |
620 elif template[0] == u'/': | |
621 raise ValueError(u"you can't specify theme with absolute paths") | |
622 template= u'(' + theme + u')' + template | |
623 else: | |
624 theme, dummy = self.env.loader.parse_template(template) | |
625 | |
626 template_source = self.env.get_template(template) | |
627 template_root_dir = os.path.normpath(self.base_dir) # FIXME: should be modified if we handle use extra dirs | |
628 # XXX: template_path may have a different theme as first element than theme if a default page is used | |
629 template_path = template_source.filename[len(template_root_dir)+1:] | |
630 | |
631 if css_files is None: | |
632 css_files = self.getCSSFiles(template_path, template_root_dir) | |
633 | |
634 kwargs['icon_defs'] = self._icon_defs | |
635 kwargs['icon'] = self._icon_use | |
636 | |
637 if css_inline: | |
638 css_contents = [] | |
639 for css_file in css_files: | |
640 css_file_path = os.path.join(template_root_dir, css_file) | |
641 with open(css_file_path) as f: | |
642 css_contents.append(f.read()) | |
643 if css_contents: | |
644 kwargs['css_content'] = '\n'.join(css_contents) | |
645 | |
646 scripts_handler = ScriptsHandler(self, template_path, template_root_dir, root_path) | |
647 self.setLocale(locale) | |
648 # XXX: theme used in template arguments is the requested theme, which may differ from actual theme | |
649 # if the template doesn't exist in the requested theme. | |
650 return template_source.render(theme=theme, root_path=root_path, media_path=media_path, | |
651 css_files=css_files, locale=self._locale, | |
652 gidx=Indexer(), script=scripts_handler, | |
653 **kwargs) |