##// END OF EJS Templates
fix markdown images...
MinRK -
Show More
@@ -1,519 +1,520 b''
1 """This module defines Exporter, a highly configurable converter
1 """This module defines Exporter, a highly configurable converter
2 that uses Jinja2 to export notebook files into different formats.
2 that uses Jinja2 to export notebook files into different formats.
3 """
3 """
4
4
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Copyright (c) 2013, the IPython Development Team.
6 # Copyright (c) 2013, the IPython Development Team.
7 #
7 #
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9 #
9 #
10 # The full license is in the file COPYING.txt, distributed with this software.
10 # The full license is in the file COPYING.txt, distributed with this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 from __future__ import print_function, absolute_import
17 from __future__ import print_function, absolute_import
18
18
19 # Stdlib imports
19 # Stdlib imports
20 import io
20 import io
21 import os
21 import os
22 import inspect
22 import inspect
23 import copy
23 import copy
24 import collections
24 import collections
25 import datetime
25 import datetime
26
26
27 # other libs/dependencies
27 # other libs/dependencies
28 from jinja2 import Environment, FileSystemLoader, ChoiceLoader, TemplateNotFound
28 from jinja2 import Environment, FileSystemLoader, ChoiceLoader, TemplateNotFound
29
29
30 # IPython imports
30 # IPython imports
31 from IPython.config.configurable import LoggingConfigurable
31 from IPython.config.configurable import LoggingConfigurable
32 from IPython.config import Config
32 from IPython.config import Config
33 from IPython.nbformat import current as nbformat
33 from IPython.nbformat import current as nbformat
34 from IPython.utils.traitlets import MetaHasTraits, DottedObjectName, Unicode, List, Dict, Any
34 from IPython.utils.traitlets import MetaHasTraits, DottedObjectName, Unicode, List, Dict, Any
35 from IPython.utils.importstring import import_item
35 from IPython.utils.importstring import import_item
36 from IPython.utils.text import indent
36 from IPython.utils.text import indent
37 from IPython.utils import py3compat
37 from IPython.utils import py3compat
38
38
39 from IPython.nbconvert import preprocessors as nbpreprocessors
39 from IPython.nbconvert import preprocessors as nbpreprocessors
40 from IPython.nbconvert import filters
40 from IPython.nbconvert import filters
41
41
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43 # Globals and constants
43 # Globals and constants
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45
45
46 #Jinja2 extensions to load.
46 #Jinja2 extensions to load.
47 JINJA_EXTENSIONS = ['jinja2.ext.loopcontrols']
47 JINJA_EXTENSIONS = ['jinja2.ext.loopcontrols']
48
48
49 default_filters = {
49 default_filters = {
50 'indent': indent,
50 'indent': indent,
51 'markdown2html': filters.markdown2html,
51 'markdown2html': filters.markdown2html,
52 'ansi2html': filters.ansi2html,
52 'ansi2html': filters.ansi2html,
53 'filter_data_type': filters.DataTypeFilter,
53 'filter_data_type': filters.DataTypeFilter,
54 'get_lines': filters.get_lines,
54 'get_lines': filters.get_lines,
55 'highlight2html': filters.highlight2html,
55 'highlight2html': filters.highlight2html,
56 'highlight2latex': filters.highlight2latex,
56 'highlight2latex': filters.highlight2latex,
57 'ipython2python': filters.ipython2python,
57 'ipython2python': filters.ipython2python,
58 'posix_path': filters.posix_path,
58 'posix_path': filters.posix_path,
59 'markdown2latex': filters.markdown2latex,
59 'markdown2latex': filters.markdown2latex,
60 'markdown2rst': filters.markdown2rst,
60 'markdown2rst': filters.markdown2rst,
61 'comment_lines': filters.comment_lines,
61 'comment_lines': filters.comment_lines,
62 'strip_ansi': filters.strip_ansi,
62 'strip_ansi': filters.strip_ansi,
63 'strip_dollars': filters.strip_dollars,
63 'strip_dollars': filters.strip_dollars,
64 'strip_files_prefix': filters.strip_files_prefix,
64 'strip_files_prefix': filters.strip_files_prefix,
65 'html2text' : filters.html2text,
65 'html2text' : filters.html2text,
66 'add_anchor': filters.add_anchor,
66 'add_anchor': filters.add_anchor,
67 'ansi2latex': filters.ansi2latex,
67 'ansi2latex': filters.ansi2latex,
68 'strip_math_space': filters.strip_math_space,
68 'strip_math_space': filters.strip_math_space,
69 'wrap_text': filters.wrap_text,
69 'wrap_text': filters.wrap_text,
70 'escape_latex': filters.escape_latex,
70 'escape_latex': filters.escape_latex,
71 'citation2latex': filters.citation2latex
71 'citation2latex': filters.citation2latex,
72 'path2url': filters.path2url,
72 }
73 }
73
74
74 #-----------------------------------------------------------------------------
75 #-----------------------------------------------------------------------------
75 # Class
76 # Class
76 #-----------------------------------------------------------------------------
77 #-----------------------------------------------------------------------------
77
78
78 class ResourcesDict(collections.defaultdict):
79 class ResourcesDict(collections.defaultdict):
79 def __missing__(self, key):
80 def __missing__(self, key):
80 return ''
81 return ''
81
82
82
83
83 class Exporter(LoggingConfigurable):
84 class Exporter(LoggingConfigurable):
84 """
85 """
85 Exports notebooks into other file formats. Uses Jinja 2 templating engine
86 Exports notebooks into other file formats. Uses Jinja 2 templating engine
86 to output new formats. Inherit from this class if you are creating a new
87 to output new formats. Inherit from this class if you are creating a new
87 template type along with new filters/preprocessors. If the filters/
88 template type along with new filters/preprocessors. If the filters/
88 preprocessors provided by default suffice, there is no need to inherit from
89 preprocessors provided by default suffice, there is no need to inherit from
89 this class. Instead, override the template_file and file_extension
90 this class. Instead, override the template_file and file_extension
90 traits via a config file.
91 traits via a config file.
91
92
92 {filters}
93 {filters}
93 """
94 """
94
95
95 # finish the docstring
96 # finish the docstring
96 __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys()))
97 __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys()))
97
98
98
99
99 template_file = Unicode(u'default',
100 template_file = Unicode(u'default',
100 config=True,
101 config=True,
101 help="Name of the template file to use")
102 help="Name of the template file to use")
102 def _template_file_changed(self, name, old, new):
103 def _template_file_changed(self, name, old, new):
103 if new=='default':
104 if new=='default':
104 self.template_file = self.default_template
105 self.template_file = self.default_template
105 else:
106 else:
106 self.template_file = new
107 self.template_file = new
107 self.template = None
108 self.template = None
108 self._load_template()
109 self._load_template()
109
110
110 default_template = Unicode(u'')
111 default_template = Unicode(u'')
111 template = Any()
112 template = Any()
112 environment = Any()
113 environment = Any()
113
114
114 file_extension = Unicode(
115 file_extension = Unicode(
115 'txt', config=True,
116 'txt', config=True,
116 help="Extension of the file that should be written to disk"
117 help="Extension of the file that should be written to disk"
117 )
118 )
118
119
119 template_path = List(['.'], config=True)
120 template_path = List(['.'], config=True)
120 def _template_path_changed(self, name, old, new):
121 def _template_path_changed(self, name, old, new):
121 self._load_template()
122 self._load_template()
122
123
123 default_template_path = Unicode(
124 default_template_path = Unicode(
124 os.path.join("..", "templates"),
125 os.path.join("..", "templates"),
125 help="Path where the template files are located.")
126 help="Path where the template files are located.")
126
127
127 template_skeleton_path = Unicode(
128 template_skeleton_path = Unicode(
128 os.path.join("..", "templates", "skeleton"),
129 os.path.join("..", "templates", "skeleton"),
129 help="Path where the template skeleton files are located.")
130 help="Path where the template skeleton files are located.")
130
131
131 #Jinja block definitions
132 #Jinja block definitions
132 jinja_comment_block_start = Unicode("", config=True)
133 jinja_comment_block_start = Unicode("", config=True)
133 jinja_comment_block_end = Unicode("", config=True)
134 jinja_comment_block_end = Unicode("", config=True)
134 jinja_variable_block_start = Unicode("", config=True)
135 jinja_variable_block_start = Unicode("", config=True)
135 jinja_variable_block_end = Unicode("", config=True)
136 jinja_variable_block_end = Unicode("", config=True)
136 jinja_logic_block_start = Unicode("", config=True)
137 jinja_logic_block_start = Unicode("", config=True)
137 jinja_logic_block_end = Unicode("", config=True)
138 jinja_logic_block_end = Unicode("", config=True)
138
139
139 #Extension that the template files use.
140 #Extension that the template files use.
140 template_extension = Unicode(".tpl", config=True)
141 template_extension = Unicode(".tpl", config=True)
141
142
142 #Configurability, allows the user to easily add filters and preprocessors.
143 #Configurability, allows the user to easily add filters and preprocessors.
143 preprocessors = List(config=True,
144 preprocessors = List(config=True,
144 help="""List of preprocessors, by name or namespace, to enable.""")
145 help="""List of preprocessors, by name or namespace, to enable.""")
145
146
146 filters = Dict(config=True,
147 filters = Dict(config=True,
147 help="""Dictionary of filters, by name and namespace, to add to the Jinja
148 help="""Dictionary of filters, by name and namespace, to add to the Jinja
148 environment.""")
149 environment.""")
149
150
150 default_preprocessors = List([nbpreprocessors.coalesce_streams,
151 default_preprocessors = List([nbpreprocessors.coalesce_streams,
151 nbpreprocessors.SVG2PDFPreprocessor,
152 nbpreprocessors.SVG2PDFPreprocessor,
152 nbpreprocessors.ExtractOutputPreprocessor,
153 nbpreprocessors.ExtractOutputPreprocessor,
153 nbpreprocessors.CSSHTMLHeaderPreprocessor,
154 nbpreprocessors.CSSHTMLHeaderPreprocessor,
154 nbpreprocessors.RevealHelpPreprocessor,
155 nbpreprocessors.RevealHelpPreprocessor,
155 nbpreprocessors.LatexPreprocessor,
156 nbpreprocessors.LatexPreprocessor,
156 nbpreprocessors.SphinxPreprocessor],
157 nbpreprocessors.SphinxPreprocessor],
157 config=True,
158 config=True,
158 help="""List of preprocessors available by default, by name, namespace,
159 help="""List of preprocessors available by default, by name, namespace,
159 instance, or type.""")
160 instance, or type.""")
160
161
161
162
162 def __init__(self, config=None, extra_loaders=None, **kw):
163 def __init__(self, config=None, extra_loaders=None, **kw):
163 """
164 """
164 Public constructor
165 Public constructor
165
166
166 Parameters
167 Parameters
167 ----------
168 ----------
168 config : config
169 config : config
169 User configuration instance.
170 User configuration instance.
170 extra_loaders : list[of Jinja Loaders]
171 extra_loaders : list[of Jinja Loaders]
171 ordered list of Jinja loader to find templates. Will be tried in order
172 ordered list of Jinja loader to find templates. Will be tried in order
172 before the default FileSystem ones.
173 before the default FileSystem ones.
173 template : str (optional, kw arg)
174 template : str (optional, kw arg)
174 Template to use when exporting.
175 Template to use when exporting.
175 """
176 """
176 if not config:
177 if not config:
177 config = self.default_config
178 config = self.default_config
178
179
179 super(Exporter, self).__init__(config=config, **kw)
180 super(Exporter, self).__init__(config=config, **kw)
180
181
181 #Init
182 #Init
182 self._init_template()
183 self._init_template()
183 self._init_environment(extra_loaders=extra_loaders)
184 self._init_environment(extra_loaders=extra_loaders)
184 self._init_preprocessors()
185 self._init_preprocessors()
185 self._init_filters()
186 self._init_filters()
186
187
187
188
188 @property
189 @property
189 def default_config(self):
190 def default_config(self):
190 return Config()
191 return Config()
191
192
192 def _config_changed(self, name, old, new):
193 def _config_changed(self, name, old, new):
193 """When setting config, make sure to start with our default_config"""
194 """When setting config, make sure to start with our default_config"""
194 c = self.default_config
195 c = self.default_config
195 if new:
196 if new:
196 c.merge(new)
197 c.merge(new)
197 if c != old:
198 if c != old:
198 self.config = c
199 self.config = c
199 super(Exporter, self)._config_changed(name, old, c)
200 super(Exporter, self)._config_changed(name, old, c)
200
201
201
202
202 def _load_template(self):
203 def _load_template(self):
203 """Load the Jinja template object from the template file
204 """Load the Jinja template object from the template file
204
205
205 This is a no-op if the template attribute is already defined,
206 This is a no-op if the template attribute is already defined,
206 or the Jinja environment is not setup yet.
207 or the Jinja environment is not setup yet.
207
208
208 This is triggered by various trait changes that would change the template.
209 This is triggered by various trait changes that would change the template.
209 """
210 """
210 if self.template is not None:
211 if self.template is not None:
211 return
212 return
212 # called too early, do nothing
213 # called too early, do nothing
213 if self.environment is None:
214 if self.environment is None:
214 return
215 return
215 # Try different template names during conversion. First try to load the
216 # Try different template names during conversion. First try to load the
216 # template by name with extension added, then try loading the template
217 # template by name with extension added, then try loading the template
217 # as if the name is explicitly specified, then try the name as a
218 # as if the name is explicitly specified, then try the name as a
218 # 'flavor', and lastly just try to load the template by module name.
219 # 'flavor', and lastly just try to load the template by module name.
219 module_name = self.__module__.rsplit('.', 1)[-1]
220 module_name = self.__module__.rsplit('.', 1)[-1]
220 try_names = []
221 try_names = []
221 if self.template_file:
222 if self.template_file:
222 try_names.extend([
223 try_names.extend([
223 self.template_file + self.template_extension,
224 self.template_file + self.template_extension,
224 self.template_file,
225 self.template_file,
225 module_name + '_' + self.template_file + self.template_extension,
226 module_name + '_' + self.template_file + self.template_extension,
226 ])
227 ])
227 try_names.append(module_name + self.template_extension)
228 try_names.append(module_name + self.template_extension)
228 for try_name in try_names:
229 for try_name in try_names:
229 self.log.debug("Attempting to load template %s", try_name)
230 self.log.debug("Attempting to load template %s", try_name)
230 try:
231 try:
231 self.template = self.environment.get_template(try_name)
232 self.template = self.environment.get_template(try_name)
232 except (TemplateNotFound, IOError):
233 except (TemplateNotFound, IOError):
233 pass
234 pass
234 except Exception as e:
235 except Exception as e:
235 self.log.warn("Unexpected exception loading template: %s", try_name, exc_info=True)
236 self.log.warn("Unexpected exception loading template: %s", try_name, exc_info=True)
236 else:
237 else:
237 self.log.info("Loaded template %s", try_name)
238 self.log.info("Loaded template %s", try_name)
238 break
239 break
239
240
240 def from_notebook_node(self, nb, resources=None, **kw):
241 def from_notebook_node(self, nb, resources=None, **kw):
241 """
242 """
242 Convert a notebook from a notebook node instance.
243 Convert a notebook from a notebook node instance.
243
244
244 Parameters
245 Parameters
245 ----------
246 ----------
246 nb : Notebook node
247 nb : Notebook node
247 resources : dict (**kw)
248 resources : dict (**kw)
248 of additional resources that can be accessed read/write by
249 of additional resources that can be accessed read/write by
249 preprocessors and filters.
250 preprocessors and filters.
250 """
251 """
251 nb_copy = copy.deepcopy(nb)
252 nb_copy = copy.deepcopy(nb)
252 resources = self._init_resources(resources)
253 resources = self._init_resources(resources)
253
254
254 # Preprocess
255 # Preprocess
255 nb_copy, resources = self._preprocess(nb_copy, resources)
256 nb_copy, resources = self._preprocess(nb_copy, resources)
256
257
257 self._load_template()
258 self._load_template()
258
259
259 if self.template is not None:
260 if self.template is not None:
260 output = self.template.render(nb=nb_copy, resources=resources)
261 output = self.template.render(nb=nb_copy, resources=resources)
261 else:
262 else:
262 raise IOError('template file "%s" could not be found' % self.template_file)
263 raise IOError('template file "%s" could not be found' % self.template_file)
263 return output, resources
264 return output, resources
264
265
265
266
266 def from_filename(self, filename, resources=None, **kw):
267 def from_filename(self, filename, resources=None, **kw):
267 """
268 """
268 Convert a notebook from a notebook file.
269 Convert a notebook from a notebook file.
269
270
270 Parameters
271 Parameters
271 ----------
272 ----------
272 filename : str
273 filename : str
273 Full filename of the notebook file to open and convert.
274 Full filename of the notebook file to open and convert.
274 """
275 """
275
276
276 #Pull the metadata from the filesystem.
277 #Pull the metadata from the filesystem.
277 if resources is None:
278 if resources is None:
278 resources = ResourcesDict()
279 resources = ResourcesDict()
279 if not 'metadata' in resources or resources['metadata'] == '':
280 if not 'metadata' in resources or resources['metadata'] == '':
280 resources['metadata'] = ResourcesDict()
281 resources['metadata'] = ResourcesDict()
281 basename = os.path.basename(filename)
282 basename = os.path.basename(filename)
282 notebook_name = basename[:basename.rfind('.')]
283 notebook_name = basename[:basename.rfind('.')]
283 resources['metadata']['name'] = notebook_name
284 resources['metadata']['name'] = notebook_name
284
285
285 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
286 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
286 resources['metadata']['modified_date'] = modified_date.strftime("%B %d, %Y")
287 resources['metadata']['modified_date'] = modified_date.strftime("%B %d, %Y")
287
288
288 with io.open(filename) as f:
289 with io.open(filename) as f:
289 return self.from_notebook_node(nbformat.read(f, 'json'), resources=resources,**kw)
290 return self.from_notebook_node(nbformat.read(f, 'json'), resources=resources,**kw)
290
291
291
292
292 def from_file(self, file_stream, resources=None, **kw):
293 def from_file(self, file_stream, resources=None, **kw):
293 """
294 """
294 Convert a notebook from a notebook file.
295 Convert a notebook from a notebook file.
295
296
296 Parameters
297 Parameters
297 ----------
298 ----------
298 file_stream : file-like object
299 file_stream : file-like object
299 Notebook file-like object to convert.
300 Notebook file-like object to convert.
300 """
301 """
301 return self.from_notebook_node(nbformat.read(file_stream, 'json'), resources=resources, **kw)
302 return self.from_notebook_node(nbformat.read(file_stream, 'json'), resources=resources, **kw)
302
303
303
304
304 def register_preprocessor(self, preprocessor, enabled=False):
305 def register_preprocessor(self, preprocessor, enabled=False):
305 """
306 """
306 Register a preprocessor.
307 Register a preprocessor.
307 Preprocessors are classes that act upon the notebook before it is
308 Preprocessors are classes that act upon the notebook before it is
308 passed into the Jinja templating engine. Preprocessors are also
309 passed into the Jinja templating engine. Preprocessors are also
309 capable of passing additional information to the Jinja
310 capable of passing additional information to the Jinja
310 templating engine.
311 templating engine.
311
312
312 Parameters
313 Parameters
313 ----------
314 ----------
314 preprocessor : preprocessor
315 preprocessor : preprocessor
315 """
316 """
316 if preprocessor is None:
317 if preprocessor is None:
317 raise TypeError('preprocessor')
318 raise TypeError('preprocessor')
318 isclass = isinstance(preprocessor, type)
319 isclass = isinstance(preprocessor, type)
319 constructed = not isclass
320 constructed = not isclass
320
321
321 #Handle preprocessor's registration based on it's type
322 #Handle preprocessor's registration based on it's type
322 if constructed and isinstance(preprocessor, py3compat.string_types):
323 if constructed and isinstance(preprocessor, py3compat.string_types):
323 #Preprocessor is a string, import the namespace and recursively call
324 #Preprocessor is a string, import the namespace and recursively call
324 #this register_preprocessor method
325 #this register_preprocessor method
325 preprocessor_cls = import_item(preprocessor)
326 preprocessor_cls = import_item(preprocessor)
326 return self.register_preprocessor(preprocessor_cls, enabled)
327 return self.register_preprocessor(preprocessor_cls, enabled)
327
328
328 if constructed and hasattr(preprocessor, '__call__'):
329 if constructed and hasattr(preprocessor, '__call__'):
329 #Preprocessor is a function, no need to construct it.
330 #Preprocessor is a function, no need to construct it.
330 #Register and return the preprocessor.
331 #Register and return the preprocessor.
331 if enabled:
332 if enabled:
332 preprocessor.enabled = True
333 preprocessor.enabled = True
333 self._preprocessors.append(preprocessor)
334 self._preprocessors.append(preprocessor)
334 return preprocessor
335 return preprocessor
335
336
336 elif isclass and isinstance(preprocessor, MetaHasTraits):
337 elif isclass and isinstance(preprocessor, MetaHasTraits):
337 #Preprocessor is configurable. Make sure to pass in new default for
338 #Preprocessor is configurable. Make sure to pass in new default for
338 #the enabled flag if one was specified.
339 #the enabled flag if one was specified.
339 self.register_preprocessor(preprocessor(parent=self), enabled)
340 self.register_preprocessor(preprocessor(parent=self), enabled)
340
341
341 elif isclass:
342 elif isclass:
342 #Preprocessor is not configurable, construct it
343 #Preprocessor is not configurable, construct it
343 self.register_preprocessor(preprocessor(), enabled)
344 self.register_preprocessor(preprocessor(), enabled)
344
345
345 else:
346 else:
346 #Preprocessor is an instance of something without a __call__
347 #Preprocessor is an instance of something without a __call__
347 #attribute.
348 #attribute.
348 raise TypeError('preprocessor')
349 raise TypeError('preprocessor')
349
350
350
351
351 def register_filter(self, name, jinja_filter):
352 def register_filter(self, name, jinja_filter):
352 """
353 """
353 Register a filter.
354 Register a filter.
354 A filter is a function that accepts and acts on one string.
355 A filter is a function that accepts and acts on one string.
355 The filters are accesible within the Jinja templating engine.
356 The filters are accesible within the Jinja templating engine.
356
357
357 Parameters
358 Parameters
358 ----------
359 ----------
359 name : str
360 name : str
360 name to give the filter in the Jinja engine
361 name to give the filter in the Jinja engine
361 filter : filter
362 filter : filter
362 """
363 """
363 if jinja_filter is None:
364 if jinja_filter is None:
364 raise TypeError('filter')
365 raise TypeError('filter')
365 isclass = isinstance(jinja_filter, type)
366 isclass = isinstance(jinja_filter, type)
366 constructed = not isclass
367 constructed = not isclass
367
368
368 #Handle filter's registration based on it's type
369 #Handle filter's registration based on it's type
369 if constructed and isinstance(jinja_filter, py3compat.string_types):
370 if constructed and isinstance(jinja_filter, py3compat.string_types):
370 #filter is a string, import the namespace and recursively call
371 #filter is a string, import the namespace and recursively call
371 #this register_filter method
372 #this register_filter method
372 filter_cls = import_item(jinja_filter)
373 filter_cls = import_item(jinja_filter)
373 return self.register_filter(name, filter_cls)
374 return self.register_filter(name, filter_cls)
374
375
375 if constructed and hasattr(jinja_filter, '__call__'):
376 if constructed and hasattr(jinja_filter, '__call__'):
376 #filter is a function, no need to construct it.
377 #filter is a function, no need to construct it.
377 self.environment.filters[name] = jinja_filter
378 self.environment.filters[name] = jinja_filter
378 return jinja_filter
379 return jinja_filter
379
380
380 elif isclass and isinstance(jinja_filter, MetaHasTraits):
381 elif isclass and isinstance(jinja_filter, MetaHasTraits):
381 #filter is configurable. Make sure to pass in new default for
382 #filter is configurable. Make sure to pass in new default for
382 #the enabled flag if one was specified.
383 #the enabled flag if one was specified.
383 filter_instance = jinja_filter(parent=self)
384 filter_instance = jinja_filter(parent=self)
384 self.register_filter(name, filter_instance )
385 self.register_filter(name, filter_instance )
385
386
386 elif isclass:
387 elif isclass:
387 #filter is not configurable, construct it
388 #filter is not configurable, construct it
388 filter_instance = jinja_filter()
389 filter_instance = jinja_filter()
389 self.register_filter(name, filter_instance)
390 self.register_filter(name, filter_instance)
390
391
391 else:
392 else:
392 #filter is an instance of something without a __call__
393 #filter is an instance of something without a __call__
393 #attribute.
394 #attribute.
394 raise TypeError('filter')
395 raise TypeError('filter')
395
396
396
397
397 def _init_template(self):
398 def _init_template(self):
398 """
399 """
399 Make sure a template name is specified. If one isn't specified, try to
400 Make sure a template name is specified. If one isn't specified, try to
400 build one from the information we know.
401 build one from the information we know.
401 """
402 """
402 self._template_file_changed('template_file', self.template_file, self.template_file)
403 self._template_file_changed('template_file', self.template_file, self.template_file)
403
404
404
405
405 def _init_environment(self, extra_loaders=None):
406 def _init_environment(self, extra_loaders=None):
406 """
407 """
407 Create the Jinja templating environment.
408 Create the Jinja templating environment.
408 """
409 """
409 here = os.path.dirname(os.path.realpath(__file__))
410 here = os.path.dirname(os.path.realpath(__file__))
410 loaders = []
411 loaders = []
411 if extra_loaders:
412 if extra_loaders:
412 loaders.extend(extra_loaders)
413 loaders.extend(extra_loaders)
413
414
414 paths = self.template_path
415 paths = self.template_path
415 paths.extend([os.path.join(here, self.default_template_path),
416 paths.extend([os.path.join(here, self.default_template_path),
416 os.path.join(here, self.template_skeleton_path)])
417 os.path.join(here, self.template_skeleton_path)])
417 loaders.append(FileSystemLoader(paths))
418 loaders.append(FileSystemLoader(paths))
418
419
419 self.environment = Environment(
420 self.environment = Environment(
420 loader= ChoiceLoader(loaders),
421 loader= ChoiceLoader(loaders),
421 extensions=JINJA_EXTENSIONS
422 extensions=JINJA_EXTENSIONS
422 )
423 )
423
424
424 #Set special Jinja2 syntax that will not conflict with latex.
425 #Set special Jinja2 syntax that will not conflict with latex.
425 if self.jinja_logic_block_start:
426 if self.jinja_logic_block_start:
426 self.environment.block_start_string = self.jinja_logic_block_start
427 self.environment.block_start_string = self.jinja_logic_block_start
427 if self.jinja_logic_block_end:
428 if self.jinja_logic_block_end:
428 self.environment.block_end_string = self.jinja_logic_block_end
429 self.environment.block_end_string = self.jinja_logic_block_end
429 if self.jinja_variable_block_start:
430 if self.jinja_variable_block_start:
430 self.environment.variable_start_string = self.jinja_variable_block_start
431 self.environment.variable_start_string = self.jinja_variable_block_start
431 if self.jinja_variable_block_end:
432 if self.jinja_variable_block_end:
432 self.environment.variable_end_string = self.jinja_variable_block_end
433 self.environment.variable_end_string = self.jinja_variable_block_end
433 if self.jinja_comment_block_start:
434 if self.jinja_comment_block_start:
434 self.environment.comment_start_string = self.jinja_comment_block_start
435 self.environment.comment_start_string = self.jinja_comment_block_start
435 if self.jinja_comment_block_end:
436 if self.jinja_comment_block_end:
436 self.environment.comment_end_string = self.jinja_comment_block_end
437 self.environment.comment_end_string = self.jinja_comment_block_end
437
438
438
439
439 def _init_preprocessors(self):
440 def _init_preprocessors(self):
440 """
441 """
441 Register all of the preprocessors needed for this exporter, disabled
442 Register all of the preprocessors needed for this exporter, disabled
442 unless specified explicitly.
443 unless specified explicitly.
443 """
444 """
444 self._preprocessors = []
445 self._preprocessors = []
445
446
446 #Load default preprocessors (not necessarly enabled by default).
447 #Load default preprocessors (not necessarly enabled by default).
447 if self.default_preprocessors:
448 if self.default_preprocessors:
448 for preprocessor in self.default_preprocessors:
449 for preprocessor in self.default_preprocessors:
449 self.register_preprocessor(preprocessor)
450 self.register_preprocessor(preprocessor)
450
451
451 #Load user preprocessors. Enable by default.
452 #Load user preprocessors. Enable by default.
452 if self.preprocessors:
453 if self.preprocessors:
453 for preprocessor in self.preprocessors:
454 for preprocessor in self.preprocessors:
454 self.register_preprocessor(preprocessor, enabled=True)
455 self.register_preprocessor(preprocessor, enabled=True)
455
456
456
457
457 def _init_filters(self):
458 def _init_filters(self):
458 """
459 """
459 Register all of the filters required for the exporter.
460 Register all of the filters required for the exporter.
460 """
461 """
461
462
462 #Add default filters to the Jinja2 environment
463 #Add default filters to the Jinja2 environment
463 for key, value in default_filters.items():
464 for key, value in default_filters.items():
464 self.register_filter(key, value)
465 self.register_filter(key, value)
465
466
466 #Load user filters. Overwrite existing filters if need be.
467 #Load user filters. Overwrite existing filters if need be.
467 if self.filters:
468 if self.filters:
468 for key, user_filter in self.filters.items():
469 for key, user_filter in self.filters.items():
469 self.register_filter(key, user_filter)
470 self.register_filter(key, user_filter)
470
471
471
472
472 def _init_resources(self, resources):
473 def _init_resources(self, resources):
473
474
474 #Make sure the resources dict is of ResourcesDict type.
475 #Make sure the resources dict is of ResourcesDict type.
475 if resources is None:
476 if resources is None:
476 resources = ResourcesDict()
477 resources = ResourcesDict()
477 if not isinstance(resources, ResourcesDict):
478 if not isinstance(resources, ResourcesDict):
478 new_resources = ResourcesDict()
479 new_resources = ResourcesDict()
479 new_resources.update(resources)
480 new_resources.update(resources)
480 resources = new_resources
481 resources = new_resources
481
482
482 #Make sure the metadata extension exists in resources
483 #Make sure the metadata extension exists in resources
483 if 'metadata' in resources:
484 if 'metadata' in resources:
484 if not isinstance(resources['metadata'], ResourcesDict):
485 if not isinstance(resources['metadata'], ResourcesDict):
485 resources['metadata'] = ResourcesDict(resources['metadata'])
486 resources['metadata'] = ResourcesDict(resources['metadata'])
486 else:
487 else:
487 resources['metadata'] = ResourcesDict()
488 resources['metadata'] = ResourcesDict()
488 if not resources['metadata']['name']:
489 if not resources['metadata']['name']:
489 resources['metadata']['name'] = 'Notebook'
490 resources['metadata']['name'] = 'Notebook'
490
491
491 #Set the output extension
492 #Set the output extension
492 resources['output_extension'] = self.file_extension
493 resources['output_extension'] = self.file_extension
493 return resources
494 return resources
494
495
495
496
496 def _preprocess(self, nb, resources):
497 def _preprocess(self, nb, resources):
497 """
498 """
498 Preprocess the notebook before passing it into the Jinja engine.
499 Preprocess the notebook before passing it into the Jinja engine.
499 To preprocess the notebook is to apply all of the
500 To preprocess the notebook is to apply all of the
500
501
501 Parameters
502 Parameters
502 ----------
503 ----------
503 nb : notebook node
504 nb : notebook node
504 notebook that is being exported.
505 notebook that is being exported.
505 resources : a dict of additional resources that
506 resources : a dict of additional resources that
506 can be accessed read/write by preprocessors
507 can be accessed read/write by preprocessors
507 and filters.
508 and filters.
508 """
509 """
509
510
510 # Do a copy.deepcopy first,
511 # Do a copy.deepcopy first,
511 # we are never safe enough with what the preprocessors could do.
512 # we are never safe enough with what the preprocessors could do.
512 nbc = copy.deepcopy(nb)
513 nbc = copy.deepcopy(nb)
513 resc = copy.deepcopy(resources)
514 resc = copy.deepcopy(resources)
514
515
515 #Run each preprocessor on the notebook. Carry the output along
516 #Run each preprocessor on the notebook. Carry the output along
516 #to each preprocessor
517 #to each preprocessor
517 for preprocessor in self._preprocessors:
518 for preprocessor in self._preprocessors:
518 nbc, resc = preprocessor(nbc, resc)
519 nbc, resc = preprocessor(nbc, resc)
519 return nbc, resc
520 return nbc, resc
@@ -1,31 +1,38 b''
1 """
1 """
2 Exporter that will export your ipynb to Markdown.
2 Exporter that will export your ipynb to Markdown.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 from IPython.config import Config
16 from IPython.utils.traitlets import Unicode
17 from IPython.utils.traitlets import Unicode
17
18
18 from .exporter import Exporter
19 from .exporter import Exporter
19
20
20 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
21 # Classes
22 # Classes
22 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
23
24
24 class MarkdownExporter(Exporter):
25 class MarkdownExporter(Exporter):
25 """
26 """
26 Exports to a markdown document (.md)
27 Exports to a markdown document (.md)
27 """
28 """
28
29
29 file_extension = Unicode(
30 file_extension = Unicode(
30 'md', config=True,
31 'md', config=True,
31 help="Extension of the file that should be written to disk")
32 help="Extension of the file that should be written to disk")
33
34 @property
35 def default_config(self):
36 c = Config({'ExtractOutputPreprocessor':{'enabled':True}})
37 c.merge(super(MarkdownExporter,self).default_config)
38 return c
@@ -1,183 +1,190 b''
1 # coding: utf-8
1 # coding: utf-8
2 """String filters.
2 """String filters.
3
3
4 Contains a collection of useful string manipulation filters for use in Jinja
4 Contains a collection of useful string manipulation filters for use in Jinja
5 templates.
5 templates.
6 """
6 """
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Copyright (c) 2013, the IPython Development Team.
8 # Copyright (c) 2013, the IPython Development Team.
9 #
9 #
10 # Distributed under the terms of the Modified BSD License.
10 # Distributed under the terms of the Modified BSD License.
11 #
11 #
12 # The full license is in the file COPYING.txt, distributed with this software.
12 # The full license is in the file COPYING.txt, distributed with this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import os
19 import os
20 import re
20 import re
21 import textwrap
21 import textwrap
22 from urllib2 import quote
22 from xml.etree import ElementTree
23 from xml.etree import ElementTree
23
24
24 from IPython.core.interactiveshell import InteractiveShell
25 from IPython.core.interactiveshell import InteractiveShell
25 from IPython.utils import py3compat
26 from IPython.utils import py3compat
26
27
27 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
28 # Functions
29 # Functions
29 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
30
31
31 __all__ = [
32 __all__ = [
32 'wrap_text',
33 'wrap_text',
33 'html2text',
34 'html2text',
34 'add_anchor',
35 'add_anchor',
35 'strip_dollars',
36 'strip_dollars',
36 'strip_files_prefix',
37 'strip_files_prefix',
37 'comment_lines',
38 'comment_lines',
38 'get_lines',
39 'get_lines',
39 'ipython2python',
40 'ipython2python',
40 'posix_path',
41 'posix_path',
42 'path2url',
41 ]
43 ]
42
44
43
45
44 def wrap_text(text, width=100):
46 def wrap_text(text, width=100):
45 """
47 """
46 Intelligently wrap text.
48 Intelligently wrap text.
47 Wrap text without breaking words if possible.
49 Wrap text without breaking words if possible.
48
50
49 Parameters
51 Parameters
50 ----------
52 ----------
51 text : str
53 text : str
52 Text to wrap.
54 Text to wrap.
53 width : int, optional
55 width : int, optional
54 Number of characters to wrap to, default 100.
56 Number of characters to wrap to, default 100.
55 """
57 """
56
58
57 split_text = text.split('\n')
59 split_text = text.split('\n')
58 wrp = map(lambda x:textwrap.wrap(x,width), split_text)
60 wrp = map(lambda x:textwrap.wrap(x,width), split_text)
59 wrpd = map('\n'.join, wrp)
61 wrpd = map('\n'.join, wrp)
60 return '\n'.join(wrpd)
62 return '\n'.join(wrpd)
61
63
62
64
63 def html2text(element):
65 def html2text(element):
64 """extract inner text from html
66 """extract inner text from html
65
67
66 Analog of jQuery's $(element).text()
68 Analog of jQuery's $(element).text()
67 """
69 """
68 if isinstance(element, py3compat.string_types):
70 if isinstance(element, py3compat.string_types):
69 element = ElementTree.fromstring(element)
71 element = ElementTree.fromstring(element)
70
72
71 text = element.text or ""
73 text = element.text or ""
72 for child in element:
74 for child in element:
73 text += html2text(child)
75 text += html2text(child)
74 text += (element.tail or "")
76 text += (element.tail or "")
75 return text
77 return text
76
78
77
79
78 def add_anchor(html):
80 def add_anchor(html):
79 """Add an anchor-link to an html header tag
81 """Add an anchor-link to an html header tag
80
82
81 For use in heading cells
83 For use in heading cells
82 """
84 """
83 h = ElementTree.fromstring(py3compat.cast_bytes_py2(html, encoding='utf-8'))
85 h = ElementTree.fromstring(py3compat.cast_bytes_py2(html, encoding='utf-8'))
84 link = html2text(h).replace(' ', '-')
86 link = html2text(h).replace(' ', '-')
85 h.set('id', link)
87 h.set('id', link)
86 a = ElementTree.Element("a", {"class" : "anchor-link", "href" : "#" + link})
88 a = ElementTree.Element("a", {"class" : "anchor-link", "href" : "#" + link})
87 a.text = u'ΒΆ'
89 a.text = u'ΒΆ'
88 h.append(a)
90 h.append(a)
89
91
90 # Known issue of Python3.x, ElementTree.tostring() returns a byte string
92 # Known issue of Python3.x, ElementTree.tostring() returns a byte string
91 # instead of a text string. See issue http://bugs.python.org/issue10942
93 # instead of a text string. See issue http://bugs.python.org/issue10942
92 # Workaround is to make sure the bytes are casted to a string.
94 # Workaround is to make sure the bytes are casted to a string.
93 return py3compat.decode(ElementTree.tostring(h), 'utf-8')
95 return py3compat.decode(ElementTree.tostring(h), 'utf-8')
94
96
95
97
96 def strip_dollars(text):
98 def strip_dollars(text):
97 """
99 """
98 Remove all dollar symbols from text
100 Remove all dollar symbols from text
99
101
100 Parameters
102 Parameters
101 ----------
103 ----------
102 text : str
104 text : str
103 Text to remove dollars from
105 Text to remove dollars from
104 """
106 """
105
107
106 return text.strip('$')
108 return text.strip('$')
107
109
108
110
109 files_url_pattern = re.compile(r'(src|href)\=([\'"]?)files/')
111 files_url_pattern = re.compile(r'(src|href)\=([\'"]?)files/')
110
112
111 def strip_files_prefix(text):
113 def strip_files_prefix(text):
112 """
114 """
113 Fix all fake URLs that start with `files/`,
115 Fix all fake URLs that start with `files/`,
114 stripping out the `files/` prefix.
116 stripping out the `files/` prefix.
115
117
116 Parameters
118 Parameters
117 ----------
119 ----------
118 text : str
120 text : str
119 Text in which to replace 'src="files/real...' with 'src="real...'
121 Text in which to replace 'src="files/real...' with 'src="real...'
120 """
122 """
121 return files_url_pattern.sub(r"\1=\2", text)
123 return files_url_pattern.sub(r"\1=\2", text)
122
124
123
125
124 def comment_lines(text, prefix='# '):
126 def comment_lines(text, prefix='# '):
125 """
127 """
126 Build a Python comment line from input text.
128 Build a Python comment line from input text.
127
129
128 Parameters
130 Parameters
129 ----------
131 ----------
130 text : str
132 text : str
131 Text to comment out.
133 Text to comment out.
132 prefix : str
134 prefix : str
133 Character to append to the start of each line.
135 Character to append to the start of each line.
134 """
136 """
135
137
136 #Replace line breaks with line breaks and comment symbols.
138 #Replace line breaks with line breaks and comment symbols.
137 #Also add a comment symbol at the beginning to comment out
139 #Also add a comment symbol at the beginning to comment out
138 #the first line.
140 #the first line.
139 return prefix + ('\n'+prefix).join(text.split('\n'))
141 return prefix + ('\n'+prefix).join(text.split('\n'))
140
142
141
143
142 def get_lines(text, start=None,end=None):
144 def get_lines(text, start=None,end=None):
143 """
145 """
144 Split the input text into separate lines and then return the
146 Split the input text into separate lines and then return the
145 lines that the caller is interested in.
147 lines that the caller is interested in.
146
148
147 Parameters
149 Parameters
148 ----------
150 ----------
149 text : str
151 text : str
150 Text to parse lines from.
152 Text to parse lines from.
151 start : int, optional
153 start : int, optional
152 First line to grab from.
154 First line to grab from.
153 end : int, optional
155 end : int, optional
154 Last line to grab from.
156 Last line to grab from.
155 """
157 """
156
158
157 # Split the input into lines.
159 # Split the input into lines.
158 lines = text.split("\n")
160 lines = text.split("\n")
159
161
160 # Return the right lines.
162 # Return the right lines.
161 return "\n".join(lines[start:end]) #re-join
163 return "\n".join(lines[start:end]) #re-join
162
164
163 def ipython2python(code):
165 def ipython2python(code):
164 """Transform IPython syntax to pure Python syntax
166 """Transform IPython syntax to pure Python syntax
165
167
166 Parameters
168 Parameters
167 ----------
169 ----------
168
170
169 code : str
171 code : str
170 IPython code, to be transformed to pure Python
172 IPython code, to be transformed to pure Python
171 """
173 """
172 shell = InteractiveShell.instance()
174 shell = InteractiveShell.instance()
173 return shell.input_transformer_manager.transform_cell(code)
175 return shell.input_transformer_manager.transform_cell(code)
174
176
175 def posix_path(path):
177 def posix_path(path):
176 """Turn a path into posix-style path/to/etc
178 """Turn a path into posix-style path/to/etc
177
179
178 Mainly for use in latex on Windows,
180 Mainly for use in latex on Windows,
179 where native Windows paths are not allowed.
181 where native Windows paths are not allowed.
180 """
182 """
181 if os.path.sep != '/':
183 if os.path.sep != '/':
182 return path.replace(os.path.sep, '/')
184 return path.replace(os.path.sep, '/')
183 return path
185 return path
186
187 def path2url(path):
188 """Turn a file path into a URL"""
189 parts = path.split(os.path.sep)
190 return '/'.join(quote(part) for part in parts)
@@ -1,72 +1,72 b''
1 {% extends 'display_priority.tpl' %}
1 {% extends 'display_priority.tpl' %}
2
2
3
3
4 {% block in_prompt %}
4 {% block in_prompt %}
5 {% endblock in_prompt %}
5 {% endblock in_prompt %}
6
6
7 {% block output_prompt %}
7 {% block output_prompt %}
8 {%- endblock output_prompt %}
8 {%- endblock output_prompt %}
9
9
10 {% block input %}
10 {% block input %}
11 {{ cell.input | indent(4)}}
11 {{ cell.input | indent(4)}}
12 {% endblock input %}
12 {% endblock input %}
13
13
14 {% block pyerr %}
14 {% block pyerr %}
15 {{ super() }}
15 {{ super() }}
16 {% endblock pyerr %}
16 {% endblock pyerr %}
17
17
18 {% block traceback_line %}
18 {% block traceback_line %}
19 {{ line | indent | strip_ansi }}
19 {{ line | indent | strip_ansi }}
20 {% endblock traceback_line %}
20 {% endblock traceback_line %}
21
21
22 {% block pyout %}
22 {% block pyout %}
23
23
24 {% block data_priority scoped %}
24 {% block data_priority scoped %}
25 {{ super() }}
25 {{ super() }}
26 {% endblock %}
26 {% endblock %}
27 {% endblock pyout %}
27 {% endblock pyout %}
28
28
29 {% block stream %}
29 {% block stream %}
30 {{ output.text | indent }}
30 {{ output.text | indent }}
31 {% endblock stream %}
31 {% endblock stream %}
32
32
33 {% block data_svg %}
33 {% block data_svg %}
34 [!image]({{ output.svg_filename }})
34 ![svg]({{ output.svg_filename | path2url }})
35 {% endblock data_svg %}
35 {% endblock data_svg %}
36
36
37 {% block data_png %}
37 {% block data_png %}
38 [!image]({{ output.png_filename }})
38 ![png]({{ output.png_filename | path2url }})
39 {% endblock data_png %}
39 {% endblock data_png %}
40
40
41 {% block data_jpg %}
41 {% block data_jpg %}
42 [!image]({{ output.jpg_filename }})
42 ![jpeg]({{ output.jpeg_filename | path2url }})
43 {% endblock data_jpg %}
43 {% endblock data_jpg %}
44
44
45 {% block data_latex %}
45 {% block data_latex %}
46 {{ output.latex }}
46 {{ output.latex }}
47 {% endblock data_latex %}
47 {% endblock data_latex %}
48
48
49 {% block data_html scoped %}
49 {% block data_html scoped %}
50 {{ output.html }}
50 {{ output.html }}
51 {% endblock data_html %}
51 {% endblock data_html %}
52
52
53 {% block data_text scoped %}
53 {% block data_text scoped %}
54 {{ output.text | indent }}
54 {{ output.text | indent }}
55 {% endblock data_text %}
55 {% endblock data_text %}
56
56
57 {% block markdowncell scoped %}
57 {% block markdowncell scoped %}
58 {{ cell.source | wrap_text(80) }}
58 {{ cell.source | wrap_text(80) }}
59 {% endblock markdowncell %}
59 {% endblock markdowncell %}
60
60
61
61
62 {% block headingcell scoped %}
62 {% block headingcell scoped %}
63 {{ '#' * cell.level }} {{ cell.source | replace('\n', ' ') }}
63 {{ '#' * cell.level }} {{ cell.source | replace('\n', ' ') }}
64 {% endblock headingcell %}
64 {% endblock headingcell %}
65
65
66 {% block rawcell scoped %}
66 {% block rawcell scoped %}
67 {{ cell.source }}
67 {{ cell.source }}
68 {% endblock rawcell %}
68 {% endblock rawcell %}
69
69
70 {% block unknowncell scoped %}
70 {% block unknowncell scoped %}
71 unknown type {{ cell.type }}
71 unknown type {{ cell.type }}
72 {% endblock unknowncell %} No newline at end of file
72 {% endblock unknowncell %}
General Comments 0
You need to be logged in to leave comments. Login now