templateexporter.py
321 lines
| 11.9 KiB
| text/x-python
|
PythonLexer
|
r13664 | """This module defines TemplateExporter, a highly configurable converter | ||
|
r10690 | that uses Jinja2 to export notebook files into different formats. | ||
|
r9578 | """ | ||
|
r9819 | |||
|
r9578 | #----------------------------------------------------------------------------- | ||
|
r9665 | # Copyright (c) 2013, the IPython Development Team. | ||
|
r9578 | # | ||
# Distributed under the terms of the Modified BSD License. | ||||
# | ||||
# The full license is in the file COPYING.txt, distributed with this software. | ||||
#----------------------------------------------------------------------------- | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
|
r10677 | |||
|
r10430 | from __future__ import print_function, absolute_import | ||
|
r9665 | |||
# Stdlib imports | ||||
|
r9819 | import os | ||
|
r9665 | |||
|
r14043 | # other libs/dependencies are imported at runtime | ||
# to move ImportErrors to runtime when the requirement is actually needed | ||||
|
r11089 | |||
|
r9665 | # IPython imports | ||
|
r12505 | from IPython.utils.traitlets import MetaHasTraits, Unicode, List, Dict, Any | ||
|
r11367 | from IPython.utils.importstring import import_item | ||
|
r12505 | from IPython.utils import py3compat, text | ||
|
r9701 | |||
|
r11089 | from IPython.nbconvert import filters | ||
|
r12505 | from .exporter import Exporter | ||
|
r10430 | |||
|
r10431 | #----------------------------------------------------------------------------- | ||
# Globals and constants | ||||
#----------------------------------------------------------------------------- | ||||
|
r9665 | |||
|
r10431 | #Jinja2 extensions to load. | ||
JINJA_EXTENSIONS = ['jinja2.ext.loopcontrols'] | ||||
|
r10838 | default_filters = { | ||
|
r12402 | 'indent': text.indent, | ||
|
r11685 | 'markdown2html': filters.markdown2html, | ||
|
r11089 | 'ansi2html': filters.ansi2html, | ||
'filter_data_type': filters.DataTypeFilter, | ||||
'get_lines': filters.get_lines, | ||||
|
r15767 | 'highlight2html': filters.Highlight2HTML, | ||
|
r13505 | 'highlight2latex': filters.Highlight2Latex, | ||
|
r11711 | 'ipython2python': filters.ipython2python, | ||
|
r11972 | 'posix_path': filters.posix_path, | ||
|
r11089 | 'markdown2latex': filters.markdown2latex, | ||
'markdown2rst': filters.markdown2rst, | ||||
|
r11685 | 'comment_lines': filters.comment_lines, | ||
'strip_ansi': filters.strip_ansi, | ||||
'strip_dollars': filters.strip_dollars, | ||||
'strip_files_prefix': filters.strip_files_prefix, | ||||
'html2text' : filters.html2text, | ||||
|
r11302 | 'add_anchor': filters.add_anchor, | ||
|
r11089 | 'ansi2latex': filters.ansi2latex, | ||
|
r11685 | 'wrap_text': filters.wrap_text, | ||
|
r11972 | 'escape_latex': filters.escape_latex, | ||
|
r12451 | 'citation2latex': filters.citation2latex, | ||
'path2url': filters.path2url, | ||||
|
r12752 | 'add_prompts': filters.add_prompts, | ||
|
r15434 | 'ascii_only': filters.ascii_only, | ||
|
r18427 | 'prevent_list_blocks': filters.prevent_list_blocks, | ||
|
r10838 | } | ||
|
r10431 | #----------------------------------------------------------------------------- | ||
|
r10677 | # Class | ||
|
r9665 | #----------------------------------------------------------------------------- | ||
|
r10677 | |||
|
r12505 | class TemplateExporter(Exporter): | ||
|
r10690 | """ | ||
Exports notebooks into other file formats. Uses Jinja 2 templating engine | ||||
to output new formats. Inherit from this class if you are creating a new | ||||
|
r12219 | template type along with new filters/preprocessors. If the filters/ | ||
preprocessors provided by default suffice, there is no need to inherit from | ||||
|
r10690 | this class. Instead, override the template_file and file_extension | ||
traits via a config file. | ||||
|
r11750 | |||
|
r10838 | {filters} | ||
""" | ||||
|
r10874 | |||
# finish the docstring | ||||
|
r10838 | __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys())) | ||
|
r11745 | template_file = Unicode(u'default', | ||
|
r11733 | config=True, | ||
|
r10431 | help="Name of the template file to use") | ||
|
r11745 | def _template_file_changed(self, name, old, new): | ||
|
r12505 | if new == 'default': | ||
|
r11745 | self.template_file = self.default_template | ||
else: | ||||
self.template_file = new | ||||
|
r11860 | self.template = None | ||
|
r11844 | self._load_template() | ||
|
r11745 | default_template = Unicode(u'') | ||
|
r11844 | template = Any() | ||
environment = Any() | ||||
|
r9768 | |||
|
r11655 | template_path = List(['.'], config=True) | ||
|
r11844 | def _template_path_changed(self, name, old, new): | ||
self._load_template() | ||||
|
r11648 | |||
default_template_path = Unicode( | ||||
os.path.join("..", "templates"), | ||||
|
r10690 | help="Path where the template files are located.") | ||
template_skeleton_path = Unicode( | ||||
|
r11648 | os.path.join("..", "templates", "skeleton"), | ||
|
r10690 | help="Path where the template skeleton files are located.") | ||
#Jinja block definitions | ||||
|
r10693 | jinja_comment_block_start = Unicode("", config=True) | ||
jinja_comment_block_end = Unicode("", config=True) | ||||
jinja_variable_block_start = Unicode("", config=True) | ||||
jinja_variable_block_end = Unicode("", config=True) | ||||
jinja_logic_block_start = Unicode("", config=True) | ||||
jinja_logic_block_end = Unicode("", config=True) | ||||
|
r10690 | |||
|
r10624 | #Extension that the template files use. | ||
|
r10690 | template_extension = Unicode(".tpl", config=True) | ||
|
r10624 | |||
|
r11367 | filters = Dict(config=True, | ||
help="""Dictionary of filters, by name and namespace, to add to the Jinja | ||||
environment.""") | ||||
|
r11383 | |||
|
r13678 | raw_mimetypes = List(config=True, | ||
|
r13664 | help="""formats of raw cells to be included in this Exporter's output.""" | ||
) | ||||
|
r13678 | def _raw_mimetypes_default(self): | ||
|
r13832 | return [self.output_mimetype, ''] | ||
|
r13664 | |||
|
r11429 | |||
|
r11367 | def __init__(self, config=None, extra_loaders=None, **kw): | ||
|
r10690 | """ | ||
Public constructor | ||||
|
r10485 | |||
|
r10690 | Parameters | ||
---------- | ||||
config : config | ||||
User configuration instance. | ||||
|
r11279 | extra_loaders : list[of Jinja Loaders] | ||
|
r11845 | ordered list of Jinja loader to find templates. Will be tried in order | ||
before the default FileSystem ones. | ||||
|
r11745 | template : str (optional, kw arg) | ||
Template to use when exporting. | ||||
|
r10690 | """ | ||
|
r13473 | super(TemplateExporter, self).__init__(config=config, **kw) | ||
|
r9665 | |||
|
r11383 | #Init | ||
|
r11852 | self._init_template() | ||
|
r11279 | self._init_environment(extra_loaders=extra_loaders) | ||
|
r11383 | self._init_filters() | ||
|
r9624 | |||
|
r10862 | |||
|
r11844 | def _load_template(self): | ||
|
r11848 | """Load the Jinja template object from the template file | ||
This is a no-op if the template attribute is already defined, | ||||
or the Jinja environment is not setup yet. | ||||
This is triggered by various trait changes that would change the template. | ||||
""" | ||||
|
r14043 | from jinja2 import TemplateNotFound | ||
|
r11844 | if self.template is not None: | ||
return | ||||
# called too early, do nothing | ||||
if self.environment is None: | ||||
return | ||||
# Try different template names during conversion. First try to load the | ||||
# template by name with extension added, then try loading the template | ||||
# as if the name is explicitly specified, then try the name as a | ||||
# 'flavor', and lastly just try to load the template by module name. | ||||
|
r11952 | try_names = [] | ||
if self.template_file: | ||||
try_names.extend([ | ||||
self.template_file + self.template_extension, | ||||
self.template_file, | ||||
]) | ||||
|
r11844 | for try_name in try_names: | ||
self.log.debug("Attempting to load template %s", try_name) | ||||
try: | ||||
self.template = self.environment.get_template(try_name) | ||||
|
r11951 | except (TemplateNotFound, IOError): | ||
|
r11844 | pass | ||
|
r11951 | except Exception as e: | ||
self.log.warn("Unexpected exception loading template: %s", try_name, exc_info=True) | ||||
|
r11844 | else: | ||
self.log.info("Loaded template %s", try_name) | ||||
break | ||||
|
r13627 | |||
|
r11376 | def from_notebook_node(self, nb, resources=None, **kw): | ||
|
r10690 | """ | ||
Convert a notebook from a notebook node instance. | ||||
Parameters | ||||
---------- | ||||
|
r17122 | nb : :class:`~IPython.nbformat.current.NotebookNode` | ||
|
r13598 | Notebook node | ||
resources : dict | ||||
Additional resources that can be accessed read/write by | ||||
preprocessors and filters. | ||||
|
r10690 | """ | ||
|
r12505 | nb_copy, resources = super(TemplateExporter, self).from_notebook_node(nb, resources, **kw) | ||
|
r13678 | resources.setdefault('raw_mimetypes', self.raw_mimetypes) | ||
|
r9578 | |||
|
r11844 | self._load_template() | ||
|
r11745 | |||
|
r11844 | if self.template is not None: | ||
|
r11745 | output = self.template.render(nb=nb_copy, resources=resources) | ||
else: | ||||
raise IOError('template file "%s" could not be found' % self.template_file) | ||||
|
r11367 | return output, resources | ||
|
r9578 | |||
|
r11367 | |||
|
r11547 | def register_filter(self, name, jinja_filter): | ||
|
r10690 | """ | ||
Register a filter. | ||||
A filter is a function that accepts and acts on one string. | ||||
The filters are accesible within the Jinja templating engine. | ||||
Parameters | ||||
---------- | ||||
name : str | ||||
name to give the filter in the Jinja engine | ||||
filter : filter | ||||
""" | ||||
|
r11547 | if jinja_filter is None: | ||
|
r11428 | raise TypeError('filter') | ||
|
r11547 | isclass = isinstance(jinja_filter, type) | ||
|
r11428 | constructed = not isclass | ||
#Handle filter's registration based on it's type | ||||
|
r11547 | if constructed and isinstance(jinja_filter, py3compat.string_types): | ||
|
r11428 | #filter is a string, import the namespace and recursively call | ||
#this register_filter method | ||||
|
r11547 | filter_cls = import_item(jinja_filter) | ||
|
r11428 | return self.register_filter(name, filter_cls) | ||
|
r11547 | if constructed and hasattr(jinja_filter, '__call__'): | ||
|
r11428 | #filter is a function, no need to construct it. | ||
|
r11547 | self.environment.filters[name] = jinja_filter | ||
return jinja_filter | ||||
|
r11428 | |||
|
r11547 | elif isclass and isinstance(jinja_filter, MetaHasTraits): | ||
|
r11428 | #filter is configurable. Make sure to pass in new default for | ||
#the enabled flag if one was specified. | ||||
|
r11547 | filter_instance = jinja_filter(parent=self) | ||
self.register_filter(name, filter_instance ) | ||||
|
r11428 | |||
elif isclass: | ||||
#filter is not configurable, construct it | ||||
|
r11547 | filter_instance = jinja_filter() | ||
self.register_filter(name, filter_instance) | ||||
|
r11428 | |||
|
r10578 | else: | ||
|
r11428 | #filter is an instance of something without a __call__ | ||
#attribute. | ||||
raise TypeError('filter') | ||||
|
r10578 | |||
|
r10588 | |||
|
r11852 | def _init_template(self): | ||
|
r11733 | """ | ||
Make sure a template name is specified. If one isn't specified, try to | ||||
build one from the information we know. | ||||
""" | ||||
|
r11745 | self._template_file_changed('template_file', self.template_file, self.template_file) | ||
|
r11733 | |||
|
r11279 | def _init_environment(self, extra_loaders=None): | ||
|
r10690 | """ | ||
Create the Jinja templating environment. | ||||
""" | ||||
|
r14043 | from jinja2 import Environment, ChoiceLoader, FileSystemLoader | ||
|
r11197 | here = os.path.dirname(os.path.realpath(__file__)) | ||
|
r11279 | loaders = [] | ||
if extra_loaders: | ||||
loaders.extend(extra_loaders) | ||||
|
r11655 | paths = self.template_path | ||
|
r11648 | paths.extend([os.path.join(here, self.default_template_path), | ||
os.path.join(here, self.template_skeleton_path)]) | ||||
loaders.append(FileSystemLoader(paths)) | ||||
|
r11279 | |||
self.environment = Environment( | ||||
loader= ChoiceLoader(loaders), | ||||
|
r10485 | extensions=JINJA_EXTENSIONS | ||
) | ||||
|
r10690 | |||
#Set special Jinja2 syntax that will not conflict with latex. | ||||
|
r10693 | if self.jinja_logic_block_start: | ||
|
r10690 | self.environment.block_start_string = self.jinja_logic_block_start | ||
|
r10693 | if self.jinja_logic_block_end: | ||
|
r10690 | self.environment.block_end_string = self.jinja_logic_block_end | ||
|
r10693 | if self.jinja_variable_block_start: | ||
|
r10690 | self.environment.variable_start_string = self.jinja_variable_block_start | ||
|
r10693 | if self.jinja_variable_block_end: | ||
|
r10690 | self.environment.variable_end_string = self.jinja_variable_block_end | ||
|
r10693 | if self.jinja_comment_block_start: | ||
|
r10690 | self.environment.comment_start_string = self.jinja_comment_block_start | ||
|
r10693 | if self.jinja_comment_block_end: | ||
|
r10690 | self.environment.comment_end_string = self.jinja_comment_block_end | ||
|
r10485 | |||
|
r11383 | |||
def _init_filters(self): | ||||
""" | ||||
Register all of the filters required for the exporter. | ||||
""" | ||||
#Add default filters to the Jinja2 environment | ||||
|
r11547 | for key, value in default_filters.items(): | ||
|
r11383 | self.register_filter(key, value) | ||
#Load user filters. Overwrite existing filters if need be. | ||||
if self.filters: | ||||
|
r11547 | for key, user_filter in self.filters.items(): | ||
|
r11383 | self.register_filter(key, user_filter) | ||