templateexporter.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. """This module defines TemplateExporter, a highly configurable converter
  2. that uses Jinja2 to export notebook files into different formats.
  3. """
  4. # Copyright (c) IPython Development Team.
  5. # Distributed under the terms of the Modified BSD License.
  6. from __future__ import print_function, absolute_import
  7. import os
  8. import uuid
  9. import json
  10. from traitlets import HasTraits, Unicode, List, Dict, Bool, default, observe
  11. from traitlets.config import Config
  12. from traitlets.utils.importstring import import_item
  13. from ipython_genutils import py3compat
  14. from jupyter_core.paths import jupyter_path
  15. from jupyter_core.utils import ensure_dir_exists
  16. from jinja2 import (
  17. TemplateNotFound, Environment, ChoiceLoader, FileSystemLoader, BaseLoader,
  18. DictLoader
  19. )
  20. from nbconvert import filters
  21. from .exporter import Exporter
  22. # Jinja2 extensions to load.
  23. JINJA_EXTENSIONS = ['jinja2.ext.loopcontrols']
  24. default_filters = {
  25. 'indent': filters.indent,
  26. 'markdown2html': filters.markdown2html,
  27. 'markdown2asciidoc': filters.markdown2asciidoc,
  28. 'ansi2html': filters.ansi2html,
  29. 'filter_data_type': filters.DataTypeFilter,
  30. 'get_lines': filters.get_lines,
  31. 'highlight2html': filters.Highlight2HTML,
  32. 'highlight2latex': filters.Highlight2Latex,
  33. 'ipython2python': filters.ipython2python,
  34. 'posix_path': filters.posix_path,
  35. 'markdown2latex': filters.markdown2latex,
  36. 'markdown2rst': filters.markdown2rst,
  37. 'comment_lines': filters.comment_lines,
  38. 'strip_ansi': filters.strip_ansi,
  39. 'strip_dollars': filters.strip_dollars,
  40. 'strip_files_prefix': filters.strip_files_prefix,
  41. 'html2text': filters.html2text,
  42. 'add_anchor': filters.add_anchor,
  43. 'ansi2latex': filters.ansi2latex,
  44. 'wrap_text': filters.wrap_text,
  45. 'escape_latex': filters.escape_latex,
  46. 'citation2latex': filters.citation2latex,
  47. 'path2url': filters.path2url,
  48. 'add_prompts': filters.add_prompts,
  49. 'ascii_only': filters.ascii_only,
  50. 'prevent_list_blocks': filters.prevent_list_blocks,
  51. 'get_metadata': filters.get_metadata,
  52. 'convert_pandoc': filters.convert_pandoc,
  53. 'json_dumps': json.dumps,
  54. 'strip_trailing_newline': filters.strip_trailing_newline,
  55. }
  56. class ExtensionTolerantLoader(BaseLoader):
  57. """A template loader which optionally adds a given extension when searching.
  58. Constructor takes two arguments: *loader* is another Jinja loader instance
  59. to wrap. *extension* is the extension, which will be added to the template
  60. name if finding the template without it fails. This should include the dot,
  61. e.g. '.tpl'.
  62. """
  63. def __init__(self, loader, extension):
  64. self.loader = loader
  65. self.extension = extension
  66. def get_source(self, environment, template):
  67. try:
  68. return self.loader.get_source(environment, template)
  69. except TemplateNotFound:
  70. if template.endswith(self.extension):
  71. raise TemplateNotFound(template)
  72. return self.loader.get_source(environment, template+self.extension)
  73. def list_templates(self):
  74. return self.loader.list_templates()
  75. class TemplateExporter(Exporter):
  76. """
  77. Exports notebooks into other file formats. Uses Jinja 2 templating engine
  78. to output new formats. Inherit from this class if you are creating a new
  79. template type along with new filters/preprocessors. If the filters/
  80. preprocessors provided by default suffice, there is no need to inherit from
  81. this class. Instead, override the template_file and file_extension
  82. traits via a config file.
  83. Filters available by default for templates:
  84. {filters}
  85. """
  86. # finish the docstring
  87. __doc__ = __doc__.format(filters='- ' + '\n - '.join(
  88. sorted(default_filters.keys())))
  89. _template_cached = None
  90. def _invalidate_template_cache(self, change=None):
  91. self._template_cached = None
  92. @property
  93. def template(self):
  94. if self._template_cached is None:
  95. self._template_cached = self._load_template()
  96. return self._template_cached
  97. _environment_cached = None
  98. def _invalidate_environment_cache(self, change=None):
  99. self._environment_cached = None
  100. self._invalidate_template_cache()
  101. @property
  102. def environment(self):
  103. if self._environment_cached is None:
  104. self._environment_cached = self._create_environment()
  105. return self._environment_cached
  106. @property
  107. def default_config(self):
  108. c = Config({
  109. 'RegexRemovePreprocessor': {
  110. 'enabled': True
  111. },
  112. 'TagRemovePreprocessor': {
  113. 'enabled': True
  114. }
  115. })
  116. c.merge(super(TemplateExporter, self).default_config)
  117. return c
  118. template_file = Unicode(
  119. help="Name of the template file to use"
  120. ).tag(config=True, affects_template=True)
  121. raw_template = Unicode('', help="raw template string").tag(affects_environment=True)
  122. _last_template_file = ""
  123. _raw_template_key = "<memory>"
  124. @observe('template_file')
  125. def _template_file_changed(self, change):
  126. new = change['new']
  127. if new == 'default':
  128. self.template_file = self.default_template
  129. return
  130. # check if template_file is a file path
  131. # rather than a name already on template_path
  132. full_path = os.path.abspath(new)
  133. if os.path.isfile(full_path):
  134. template_dir, template_file = os.path.split(full_path)
  135. if template_dir not in [ os.path.abspath(p) for p in self.template_path ]:
  136. self.template_path = [template_dir] + self.template_path
  137. self.template_file = template_file
  138. @default('template_file')
  139. def _template_file_default(self):
  140. return self.default_template
  141. @observe('raw_template')
  142. def _raw_template_changed(self, change):
  143. if not change['new']:
  144. self.template_file = self.default_template or self._last_template_file
  145. self._invalidate_template_cache()
  146. default_template = Unicode(u'').tag(affects_template=True)
  147. template_path = List(['.']).tag(config=True, affects_environment=True)
  148. default_template_path = Unicode(
  149. os.path.join("..", "templates"),
  150. help="Path where the template files are located."
  151. ).tag(affects_environment=True)
  152. template_skeleton_path = Unicode(
  153. os.path.join("..", "templates", "skeleton"),
  154. help="Path where the template skeleton files are located.",
  155. ).tag(affects_environment=True)
  156. template_data_paths = List(
  157. jupyter_path('nbconvert','templates'),
  158. help="Path where templates can be installed too."
  159. ).tag(affects_environment=True)
  160. #Extension that the template files use.
  161. template_extension = Unicode(".tpl").tag(config=True, affects_environment=True)
  162. exclude_input = Bool(False,
  163. help = "This allows you to exclude code cell inputs from all templates if set to True."
  164. ).tag(config=True)
  165. exclude_input_prompt = Bool(False,
  166. help = "This allows you to exclude input prompts from all templates if set to True."
  167. ).tag(config=True)
  168. exclude_output = Bool(False,
  169. help = "This allows you to exclude code cell outputs from all templates if set to True."
  170. ).tag(config=True)
  171. exclude_output_prompt = Bool(False,
  172. help = "This allows you to exclude output prompts from all templates if set to True."
  173. ).tag(config=True)
  174. exclude_code_cell = Bool(False,
  175. help = "This allows you to exclude code cells from all templates if set to True."
  176. ).tag(config=True)
  177. exclude_markdown = Bool(False,
  178. help = "This allows you to exclude markdown cells from all templates if set to True."
  179. ).tag(config=True)
  180. exclude_raw = Bool(False,
  181. help = "This allows you to exclude raw cells from all templates if set to True."
  182. ).tag(config=True)
  183. exclude_unknown = Bool(False,
  184. help = "This allows you to exclude unknown cells from all templates if set to True."
  185. ).tag(config=True)
  186. extra_loaders = List(
  187. help="Jinja loaders to find templates. Will be tried in order "
  188. "before the default FileSystem ones.",
  189. ).tag(affects_environment=True)
  190. filters = Dict(
  191. help="""Dictionary of filters, by name and namespace, to add to the Jinja
  192. environment."""
  193. ).tag(config=True, affects_environment=True)
  194. raw_mimetypes = List(
  195. help="""formats of raw cells to be included in this Exporter's output."""
  196. ).tag(config=True)
  197. @default('raw_mimetypes')
  198. def _raw_mimetypes_default(self):
  199. return [self.output_mimetype, '']
  200. # TODO: passing config is wrong, but changing this revealed more complicated issues
  201. def __init__(self, config=None, **kw):
  202. """
  203. Public constructor
  204. Parameters
  205. ----------
  206. config : config
  207. User configuration instance.
  208. extra_loaders : list[of Jinja Loaders]
  209. ordered list of Jinja loader to find templates. Will be tried in order
  210. before the default FileSystem ones.
  211. template_file : str (optional, kw arg)
  212. Template to use when exporting.
  213. """
  214. super(TemplateExporter, self).__init__(config=config, **kw)
  215. self.observe(self._invalidate_environment_cache,
  216. list(self.traits(affects_environment=True)))
  217. self.observe(self._invalidate_template_cache,
  218. list(self.traits(affects_template=True)))
  219. def _load_template(self):
  220. """Load the Jinja template object from the template file
  221. This is triggered by various trait changes that would change the template.
  222. """
  223. # this gives precedence to a raw_template if present
  224. with self.hold_trait_notifications():
  225. if self.template_file != self._raw_template_key:
  226. self._last_template_file = self.template_file
  227. if self.raw_template:
  228. self.template_file = self._raw_template_key
  229. if not self.template_file:
  230. raise ValueError("No template_file specified!")
  231. # First try to load the
  232. # template by name with extension added, then try loading the template
  233. # as if the name is explicitly specified.
  234. template_file = self.template_file
  235. self.log.debug("Attempting to load template %s", template_file)
  236. self.log.debug(" template_path: %s", os.pathsep.join(self.template_path))
  237. return self.environment.get_template(template_file)
  238. def from_notebook_node(self, nb, resources=None, **kw):
  239. """
  240. Convert a notebook from a notebook node instance.
  241. Parameters
  242. ----------
  243. nb : :class:`~nbformat.NotebookNode`
  244. Notebook node
  245. resources : dict
  246. Additional resources that can be accessed read/write by
  247. preprocessors and filters.
  248. """
  249. nb_copy, resources = super(TemplateExporter, self).from_notebook_node(nb, resources, **kw)
  250. resources.setdefault('raw_mimetypes', self.raw_mimetypes)
  251. resources['global_content_filter'] = {
  252. 'include_code': not self.exclude_code_cell,
  253. 'include_markdown': not self.exclude_markdown,
  254. 'include_raw': not self.exclude_raw,
  255. 'include_unknown': not self.exclude_unknown,
  256. 'include_input': not self.exclude_input,
  257. 'include_output': not self.exclude_output,
  258. 'include_input_prompt': not self.exclude_input_prompt,
  259. 'include_output_prompt': not self.exclude_output_prompt,
  260. 'no_prompt': self.exclude_input_prompt and self.exclude_output_prompt,
  261. }
  262. # Top level variables are passed to the template_exporter here.
  263. output = self.template.render(nb=nb_copy, resources=resources)
  264. output = output.lstrip('\r\n')
  265. return output, resources
  266. def _register_filter(self, environ, name, jinja_filter):
  267. """
  268. Register a filter.
  269. A filter is a function that accepts and acts on one string.
  270. The filters are accessible within the Jinja templating engine.
  271. Parameters
  272. ----------
  273. name : str
  274. name to give the filter in the Jinja engine
  275. filter : filter
  276. """
  277. if jinja_filter is None:
  278. raise TypeError('filter')
  279. isclass = isinstance(jinja_filter, type)
  280. constructed = not isclass
  281. #Handle filter's registration based on it's type
  282. if constructed and isinstance(jinja_filter, py3compat.string_types):
  283. #filter is a string, import the namespace and recursively call
  284. #this register_filter method
  285. filter_cls = import_item(jinja_filter)
  286. return self._register_filter(environ, name, filter_cls)
  287. if constructed and hasattr(jinja_filter, '__call__'):
  288. #filter is a function, no need to construct it.
  289. environ.filters[name] = jinja_filter
  290. return jinja_filter
  291. elif isclass and issubclass(jinja_filter, HasTraits):
  292. #filter is configurable. Make sure to pass in new default for
  293. #the enabled flag if one was specified.
  294. filter_instance = jinja_filter(parent=self)
  295. self._register_filter(environ, name, filter_instance)
  296. elif isclass:
  297. #filter is not configurable, construct it
  298. filter_instance = jinja_filter()
  299. self._register_filter(environ, name, filter_instance)
  300. else:
  301. #filter is an instance of something without a __call__
  302. #attribute.
  303. raise TypeError('filter')
  304. def register_filter(self, name, jinja_filter):
  305. """
  306. Register a filter.
  307. A filter is a function that accepts and acts on one string.
  308. The filters are accessible within the Jinja templating engine.
  309. Parameters
  310. ----------
  311. name : str
  312. name to give the filter in the Jinja engine
  313. filter : filter
  314. """
  315. return self._register_filter(self.environment, name, jinja_filter)
  316. def default_filters(self):
  317. """Override in subclasses to provide extra filters.
  318. This should return an iterable of 2-tuples: (name, class-or-function).
  319. You should call the method on the parent class and include the filters
  320. it provides.
  321. If a name is repeated, the last filter provided wins. Filters from
  322. user-supplied config win over filters provided by classes.
  323. """
  324. return default_filters.items()
  325. def _create_environment(self):
  326. """
  327. Create the Jinja templating environment.
  328. """
  329. here = os.path.dirname(os.path.realpath(__file__))
  330. additional_paths = self.template_data_paths
  331. for path in additional_paths:
  332. try:
  333. ensure_dir_exists(path, mode=0o700)
  334. except OSError:
  335. pass
  336. paths = self.template_path + \
  337. additional_paths + \
  338. [os.path.join(here, self.default_template_path),
  339. os.path.join(here, self.template_skeleton_path)]
  340. loaders = self.extra_loaders + [
  341. ExtensionTolerantLoader(FileSystemLoader(paths), self.template_extension),
  342. DictLoader({self._raw_template_key: self.raw_template})
  343. ]
  344. environment = Environment(
  345. loader=ChoiceLoader(loaders),
  346. extensions=JINJA_EXTENSIONS
  347. )
  348. environment.globals['uuid4'] = uuid.uuid4
  349. # Add default filters to the Jinja2 environment
  350. for key, value in self.default_filters():
  351. self._register_filter(environment, key, value)
  352. # Load user filters. Overwrite existing filters if need be.
  353. if self.filters:
  354. for key, user_filter in self.filters.items():
  355. self._register_filter(environment, key, user_filter)
  356. return environment