diff --git a/.travis.yml b/.travis.yml index 9cb9c6768..34d144cf5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ install: - pip install check-manifest - python -m ipykernel.kernelspec --user script: - - check-manifest + - check-manifest --ignore "share/**" # cd so we test the install, not the repo - cd `mktemp -d` - py.test --cov nbconvert -v --pyargs nbconvert diff --git a/nbconvert/exporters/asciidoc.py b/nbconvert/exporters/asciidoc.py index 84320d9ef..ea128c487 100644 --- a/nbconvert/exporters/asciidoc.py +++ b/nbconvert/exporters/asciidoc.py @@ -18,8 +18,8 @@ class ASCIIDocExporter(TemplateExporter): def _file_extension_default(self): return '.asciidoc' - @default('template_file') - def _template_file_default(self): + @default('template_name') + def _template_name_default(self): return 'asciidoc' output_mimetype = 'text/asciidoc' diff --git a/nbconvert/exporters/exporter.py b/nbconvert/exporters/exporter.py index ffd6af4de..e6160cf9b 100644 --- a/nbconvert/exporters/exporter.py +++ b/nbconvert/exporters/exporter.py @@ -52,7 +52,7 @@ class Exporter(LoggingConfigurable): accompanying resources dict. """ - file_extension = FilenameExtension('.txt', + file_extension = FilenameExtension( help="Extension of the file that should be written to disk" ).tag(config=True) diff --git a/nbconvert/exporters/html.py b/nbconvert/exporters/html.py index bf2b3c005..b8689d0de 100644 --- a/nbconvert/exporters/html.py +++ b/nbconvert/exporters/html.py @@ -5,11 +5,14 @@ # Distributed under the terms of the Modified BSD License. import os +import mimetypes +import base64 from traitlets import default, Unicode from traitlets.config import Config from jupyter_core.paths import jupyter_path from jinja2 import contextfilter +import jinja2 from nbconvert.filters.highlight import Highlight2HTML from nbconvert.filters.markdown_mistune import IPythonRenderer, MarkdownWithMath @@ -33,17 +36,18 @@ class HTMLExporter(TemplateExporter): def _file_extension_default(self): return '.html' - @default('default_template_path') - def _default_template_path_default(self): - return os.path.join("..", "templates", "html") + @default('template_name') + def _template_name_default(self): + return 'classic' @default('template_data_paths') def _template_data_paths_default(self): return jupyter_path("nbconvert", "templates", "html") - @default('template_file') - def _template_file_default(self): - return 'full.tpl' + + theme = Unicode('light', + help='Template specific theme(e.g. the JupyterLab CSS theme for the lab template)' + ).tag(config=True) output_mimetype = 'text/html' @@ -93,3 +97,30 @@ def from_notebook_node(self, nb, resources=None, **kw): highlight_code = self.filters.get('highlight_code', Highlight2HTML(pygments_lexer=lexer, parent=self)) self.register_filter('highlight_code', highlight_code) return super(HTMLExporter, self).from_notebook_node(nb, resources, **kw) + + def _init_resources(self, resources): + def resources_include_css(name): + env = self.environment + code = """""" % (env.loader.get_source(env, name)[0]) + return jinja2.Markup(code) + + def resources_include_js(name): + env = self.environment + code = """""" % (env.loader.get_source(env, name)[0]) + return jinja2.Markup(code) + + def resources_include_url(name): + env = self.environment + mime_type, encoding = mimetypes.guess_type(name) + data = env.loader.get_source(env, name)[0] + data = base64.b64encode(data.encode('utf8')) + data = data.replace(b'\n', b'').decode('ascii') + print(type(data), data) + src = 'data:{mime_type};base64,{data}'.format(mime_type=mime_type, data=data) + return jinja2.Markup(src) + resources = super(HTMLExporter, self)._init_resources(resources) + resources['theme'] = self.theme + resources['include_css'] = resources_include_css + resources['include_js'] = resources_include_js + resources['include_url'] = resources_include_url + return resources diff --git a/nbconvert/exporters/latex.py b/nbconvert/exporters/latex.py index b9f82eb4e..723b22d45 100644 --- a/nbconvert/exporters/latex.py +++ b/nbconvert/exporters/latex.py @@ -27,25 +27,21 @@ class LatexExporter(TemplateExporter): def _file_extension_default(self): return '.tex' - @default('template_file') - def _template_file_default(self): - return 'article.tplx' - - # Latex constants - @default('default_template_path') - def _default_template_path_default(self): - return os.path.join("..", "templates", "latex") - - @default('template_skeleton_path') - def _template_skeleton_path_default(self): - return os.path.join("..", "templates", "latex", "skeleton") @default('template_data_paths') def _template_data_paths_default(self): return jupyter_path("nbconvert", "templates", "latex") - + + @default('template_extension') + def _template_extension_default(self): + return '.tex' + + @default('template_name') + def _template_name_default(self): + return 'latex' + #Extension that the template files use. - template_extension = Unicode(".tplx").tag(config=True) + template_extension = Unicode(".tex").tag(config=True) output_mimetype = 'text/latex' diff --git a/nbconvert/exporters/markdown.py b/nbconvert/exporters/markdown.py index b810d3181..24d357f95 100644 --- a/nbconvert/exporters/markdown.py +++ b/nbconvert/exporters/markdown.py @@ -19,9 +19,9 @@ class MarkdownExporter(TemplateExporter): def _file_extension_default(self): return '.md' - @default('template_file') - def _template_file_default(self): - return 'markdown.tpl' + @default('template_name') + def _template_name_default(self): + return 'markdown' output_mimetype = 'text/markdown' diff --git a/nbconvert/exporters/python.py b/nbconvert/exporters/python.py index 642003632..aa833c813 100644 --- a/nbconvert/exporters/python.py +++ b/nbconvert/exporters/python.py @@ -16,8 +16,8 @@ class PythonExporter(TemplateExporter): def _file_extension_default(self): return '.py' - @default('template_file') - def _template_file_default(self): - return 'python.tpl' + @default('template_name') + def _template_name_default(self): + return 'python' output_mimetype = 'text/x-python' diff --git a/nbconvert/exporters/rst.py b/nbconvert/exporters/rst.py index 568144ba9..b27b9535c 100644 --- a/nbconvert/exporters/rst.py +++ b/nbconvert/exporters/rst.py @@ -18,9 +18,9 @@ class RSTExporter(TemplateExporter): def _file_extension_default(self): return '.rst' - @default('template_file') - def _template_file_default(self): - return 'rst.tpl' + @default('template_name') + def _template_name_default(self): + return 'rst' output_mimetype = 'text/restructuredtext' export_from_notebook = "reST" diff --git a/nbconvert/exporters/script.py b/nbconvert/exporters/script.py index 0875df60f..c9986289d 100644 --- a/nbconvert/exporters/script.py +++ b/nbconvert/exporters/script.py @@ -18,7 +18,11 @@ class ScriptExporter(TemplateExporter): @default('template_file') def _template_file_default(self): - return 'script.tpl' + return 'script.j2' + + @default('template_name') + def _template_name_default(self): + return 'script' def _get_language_exporter(self, lang_name): """Find an exporter for the language name from notebook metadata. diff --git a/nbconvert/exporters/slides.py b/nbconvert/exporters/slides.py index 500eef854..f1f35ded2 100644 --- a/nbconvert/exporters/slides.py +++ b/nbconvert/exporters/slides.py @@ -77,6 +77,14 @@ class SlidesExporter(HTMLExporter): export_from_notebook = "Reveal.js slides" + @default('template_name') + def _template_name_default(self): + return 'reveal' + + template_name = Unicode('reveal', + help="Name of the template to use" + ).tag(config=True, affects_template=True) + reveal_url_prefix = Unicode( help="""The URL prefix for reveal.js (version 3.x). This defaults to the reveal CDN, but can be any url pointing to a copy @@ -160,9 +168,9 @@ def _reveal_url_prefix_default(self): def _file_extension_default(self): return '.slides.html' - @default('template_file') - def _template_file_default(self): - return 'slides_reveal.tpl' + @default('template_extension') + def _template_extension_default(self): + return '.html' output_mimetype = 'text/html' diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 834480ad7..938004363 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -11,6 +11,7 @@ import uuid import json +from jupyter_core.paths import jupyter_path from traitlets import HasTraits, Unicode, List, Dict, Bool, default, observe from traitlets.config import Config from traitlets.utils.importstring import import_item @@ -28,6 +29,10 @@ # Jinja2 extensions to load. JINJA_EXTENSIONS = ['jinja2.ext.loopcontrols'] +ROOT = os.path.dirname(__file__) +DEV_MODE = os.path.exists(os.path.join(ROOT, '../../setup.py')) and os.path.exists(os.path.join(ROOT, '../../share')) + + default_filters = { 'indent': filters.indent, 'markdown2html': filters.markdown2html, @@ -61,6 +66,29 @@ 'strip_trailing_newline': filters.strip_trailing_newline, } + +# copy of https://github.com/jupyter/jupyter_server/blob/b62458a7f5ad6b5246d2f142258dedaa409de5d9/jupyter_server/config_manager.py#L19 +def recursive_update(target, new): + """Recursively update one dictionary using another. + None values will delete their keys. + """ + for k, v in new.items(): + if isinstance(v, dict): + if k not in target: + target[k] = {} + recursive_update(target[k], v) + if not target[k]: + # Prune empty subdicts + del target[k] + + elif v is None: + target.pop(k, None) + + else: + target[k] = v + return target # return for convenience + + class ExtensionTolerantLoader(BaseLoader): """A template loader which optionally adds a given extension when searching. @@ -139,7 +167,10 @@ def default_config(self): c.merge(super(TemplateExporter, self).default_config) return c - template_file = Unicode( + template_name = Unicode(help="Name of the template to use" + ).tag(config=True, affects_template=True) + + template_file = Unicode(None, allow_none=True, help="Name of the template file to use" ).tag(config=True, affects_template=True) @@ -165,27 +196,20 @@ def _template_file_changed(self, change): @default('template_file') def _template_file_default(self): - return self.default_template + if self.template_extension: + return 'index' + self.template_extension @observe('raw_template') def _raw_template_changed(self, change): if not change['new']: - self.template_file = self.default_template or self._last_template_file + self.template_file = self._last_template_file self._invalidate_template_cache() - default_template = Unicode(u'').tag(affects_template=True) - template_path = List(['.']).tag(config=True, affects_environment=True) - default_template_path = Unicode( - os.path.join("..", "templates"), - help="Path where the template files are located." - ).tag(affects_environment=True) + #Extension that the template files use. + template_extension = Unicode().tag(config=True, affects_environment=True) - template_skeleton_path = Unicode( - os.path.join("..", "templates", "skeleton"), - help="Path where the template skeleton files are located.", - ).tag(affects_environment=True) template_data_paths = List( jupyter_path('nbconvert','templates'), @@ -194,6 +218,14 @@ def _raw_template_changed(self, change): #Extension that the template files use. template_extension = Unicode(".tpl").tag(config=True, affects_environment=True) + @default('template_extension') + def _template_extension_default(self): + return self.file_extension + + @default('template_file') + def _template_file_default(self): + if self.template_extension: + return 'index' + self.template_extension exclude_input = Bool(False, help = "This allows you to exclude code cell inputs from all templates if set to True." @@ -398,19 +430,8 @@ def _create_environment(self): """ Create the Jinja templating environment. """ - here = os.path.dirname(os.path.realpath(__file__)) - - additional_paths = self.template_data_paths - for path in additional_paths: - try: - ensure_dir_exists(path, mode=0o700) - except OSError: - pass - - paths = self.template_path + \ - additional_paths + \ - [os.path.join(here, self.default_template_path), - os.path.join(here, self.template_skeleton_path)] + paths = self.get_template_paths() + self.log.info('template paths:\n\t%s', '\n\t'.join(paths)) loaders = self.extra_loaders + [ ExtensionTolerantLoader(FileSystemLoader(paths), self.template_extension), @@ -433,3 +454,74 @@ def _create_environment(self): self._register_filter(environment, key, user_filter) return environment + + def get_template_paths(self, prune=False, root_dirs=None): + full_paths = [] + paths = list(self.template_path) + root_dirs = self.get_prefix_root_dirs() + template_names = self.get_template_names() + for template_name in template_names: + for root_dir in root_dirs: + base_dir = os.path.join(root_dir, 'nbconvert', 'templates') + path = os.path.join(base_dir, template_name) + if not prune or os.path.exists(path): + paths.append(path) + + for root_dir in root_dirs: + # we include root_dir for when we want to be very explicit, e.g. + # {% extends 'nbconvert/templates/classic/base.html' %} + paths.append(root_dir) + # we include base_dir for when we want to be explicit, but less than root_dir, e.g. + # {% extends 'classic/base.html' %} + base_dir = os.path.join(root_dir, 'nbconvert', 'templates') + paths.append(base_dir) + + additional_paths = self.template_data_paths + for path in additional_paths: + try: + ensure_dir_exists(path, mode=0o700) + except OSError: + pass + + + return additional_paths + paths + + def get_template_names(self): + # finds a list of template name where each successive template name is the base template + template_names = [] + root_dirs = self.get_prefix_root_dirs() + template_name = self.template_name + merged_conf = {} # the configuration once all conf files are merged + while template_name is not None: + template_names.append(template_name) + conf = {} + found_at_least_one = False + for root_dir in root_dirs: + template_dir = os.path.join(root_dir, 'nbconvert', 'templates', template_name) + if os.path.exists(template_dir): + found_at_least_one = True + conf_file = os.path.join(template_dir, 'conf.json') + if os.path.exists(conf_file): + with open(conf_file) as f: + conf = recursive_update(json.load(f), conf) + if not found_at_least_one: + paths = "\n\t".join(root_dirs) + raise ValueError('No template sub-directory with name %r found in the following paths:\n\t%s' % (template_name, paths)) + merged_conf = recursive_update(dict(conf), merged_conf) + template_name = conf.get('base_template') + conf = merged_conf + mimetypes = [mimetype for mimetype, enabled in conf.get('mimetypes', {}).items() if enabled] + if self.output_mimetype and self.output_mimetype not in mimetypes: + supported_mimetypes = '\n\t'.join(mimetypes) + raise ValueError('Unsupported mimetype %r for template %r, mimetypes supported are: \n\t%s' %\ + (self.output_mimetype, self.template_name, supported_mimetypes)) + return template_names + + def get_prefix_root_dirs(self): + # We look at the usual jupyter locations, and for development purposes also + # relative to the package directory (first entry, meaning with highest precedence) + root_dirs = [] + if DEV_MODE: + root_dirs.append(os.path.abspath(os.path.join(ROOT, '..', '..', 'share', 'jupyter'))) + root_dirs.extend(jupyter_path()) + return root_dirs diff --git a/nbconvert/exporters/tests/test_html.py b/nbconvert/exporters/tests/test_html.py index 507a713a1..ef1137bd0 100644 --- a/nbconvert/exporters/tests/test_html.py +++ b/nbconvert/exporters/tests/test_html.py @@ -33,26 +33,26 @@ def test_export(self): assert len(output) > 0 - def test_export_basic(self): + def test_export_classic(self): """ - Can a HTMLExporter export using the 'basic' template? + Can a HTMLExporter export using the 'classic' template? """ - (output, resources) = HTMLExporter(template_file='basic').from_filename(self._get_notebook()) + (output, resources) = HTMLExporter(template_name='classic').from_filename(self._get_notebook()) assert len(output) > 0 - def test_export_full(self): + def test_export_notebook(self): """ - Can a HTMLExporter export using the 'full' template? + Can a HTMLExporter export using the 'lab' template? """ - (output, resources) = HTMLExporter(template_file='full').from_filename(self._get_notebook()) + (output, resources) = HTMLExporter(template_name='lab').from_filename(self._get_notebook()) assert len(output) > 0 def test_prompt_number(self): """ Does HTMLExporter properly format input and output prompts? """ - (output, resources) = HTMLExporter(template_file='full').from_filename( + (output, resources) = HTMLExporter(template_name='lab').from_filename( self._get_notebook(nb_name="prompt_numbers.ipynb")) in_regex = r"In \[(.*)\]:" out_regex = r"Out\[(.*)\]:" @@ -74,7 +74,7 @@ def test_prompt_number(self): } } ) - exporter = HTMLExporter(config=no_prompt_conf, template_file='full') + exporter = HTMLExporter(config=no_prompt_conf, template_name='lab') (output, resources) = exporter.from_filename( self._get_notebook(nb_name="prompt_numbers.ipynb")) in_regex = r"In \[(.*)\]:" @@ -85,9 +85,9 @@ def test_prompt_number(self): def test_png_metadata(self): """ - Does HTMLExporter with the 'basic' template treat pngs with width/height metadata correctly? + Does HTMLExporter with the 'classic' template treat pngs with width/height metadata correctly? """ - (output, resources) = HTMLExporter(template_file='basic').from_filename( + (output, resources) = HTMLExporter(template_name='classic').from_filename( self._get_notebook(nb_name="pngmetadata.ipynb")) check_for_png = re.compile(r']*?)>') result = check_for_png.search(output) @@ -108,11 +108,11 @@ def test_javascript_output(self): ) ] ) - (output, resources) = HTMLExporter(template_file='basic').from_notebook_node(nb) + (output, resources) = HTMLExporter(template_name='classic').from_notebook_node(nb) self.assertIn('javascript_output', output) def test_attachments(self): - (output, resources) = HTMLExporter(template_file='basic').from_file( + (output, resources) = HTMLExporter(template_name='classic').from_file( self._get_notebook(nb_name='attachment.ipynb') ) check_for_png = re.compile(r']*?)>') @@ -137,6 +137,6 @@ def custom_highlight_code(source, language="python", metadata=None): filters = { "highlight_code": custom_highlight_code } - (output, resources) = HTMLExporter(template_file='basic', filters=filters).from_notebook_node(nb) + (output, resources) = HTMLExporter(template_name='classic', filters=filters).from_notebook_node(nb) self.assertTrue("ADDED_TEXT" in output) diff --git a/nbconvert/exporters/tests/test_latex.py b/nbconvert/exporters/tests/test_latex.py index 63e97d832..b9f5ab64c 100644 --- a/nbconvert/exporters/tests/test_latex.py +++ b/nbconvert/exporters/tests/test_latex.py @@ -53,23 +53,6 @@ def test_export_book(self): assert len(output) > 0 - @onlyif_cmds_exist('pandoc') - def test_export_basic(self): - """ - Can a LatexExporter export using 'article' template? - """ - (output, resources) = LatexExporter(template_file='article').from_filename(self._get_notebook()) - assert len(output) > 0 - - - @onlyif_cmds_exist('pandoc') - def test_export_article(self): - """ - Can a LatexExporter export using 'article' template? - """ - (output, resources) = LatexExporter(template_file='article').from_filename(self._get_notebook()) - assert len(output) > 0 - @onlyif_cmds_exist('pandoc') def test_very_long_cells(self): """ @@ -104,7 +87,7 @@ def test_very_long_cells(self): with open(nbfile, 'w') as f: write(nb, f, 4) - (output, resources) = LatexExporter(template_file='article').from_filename(nbfile) + (output, resources) = LatexExporter().from_filename(nbfile) assert len(output) > 0 @onlyif_cmds_exist('pandoc') @@ -133,7 +116,7 @@ def test_prompt_number_color_ipython(self): """ my_loader_tplx = DictLoader({'my_template': """ - ((* extends 'style_ipython.tplx' *)) + ((* extends 'style_ipython.tex' *)) ((* block docclass *)) \documentclass[11pt]{article} @@ -184,7 +167,7 @@ def test_in_memory_template_tplx(self): # Loads in an in memory latex template (.tplx) using jinja2.DictLoader # creates a class that uses this template with the template_file argument # converts an empty notebook using this mechanism - my_loader_tplx = DictLoader({'my_template': "{%- extends 'article.tplx' -%}"}) + my_loader_tplx = DictLoader({'my_template': "{%- extends 'index' -%}"}) class MyExporter(LatexExporter): template_file = 'my_template' diff --git a/nbconvert/exporters/tests/test_slides.py b/nbconvert/exporters/tests/test_slides.py index f7873c159..5890d99d0 100644 --- a/nbconvert/exporters/tests/test_slides.py +++ b/nbconvert/exporters/tests/test_slides.py @@ -33,7 +33,7 @@ def test_export_reveal(self): """ Can a SlidesExporter export using the 'reveal' template? """ - (output, resources) = SlidesExporter(template_file='slides_reveal').from_filename(self._get_notebook()) + (output, resources) = SlidesExporter().from_filename(self._get_notebook()) assert len(output) > 0 def build_notebook(self): diff --git a/nbconvert/exporters/tests/test_templateexporter.py b/nbconvert/exporters/tests/test_templateexporter.py index 0a57b6ebe..2c0130cb9 100644 --- a/nbconvert/exporters/tests/test_templateexporter.py +++ b/nbconvert/exporters/tests/test_templateexporter.py @@ -22,7 +22,7 @@ import pytest -raw_template = """{%- extends 'rst.tpl' -%} +raw_template = """{%- extends 'index.rst' -%} {%- block in_prompt -%} blah {%- endblock in_prompt -%} @@ -140,7 +140,7 @@ def test_raw_template_attr(self): class AttrExporter(TemplateExporter): raw_template = raw_template - exporter_attr = AttrExporter() + exporter_attr = AttrExporter(template_name='rst') output_attr, _ = exporter_attr.from_notebook_node(nb) assert "blah" in output_attr @@ -163,7 +163,7 @@ def __init__(self, *args, **kwargs): output_init, _ = exporter_init.from_notebook_node(nb) assert "blah" in output_init exporter_init.raw_template = '' - assert exporter_init.template_file == "rst.tpl" + assert exporter_init.template_file == "index.rst" output_init, _ = exporter_init.from_notebook_node(nb) assert "blah" not in output_init @@ -178,19 +178,19 @@ def test_raw_template_dynamic_attr(self): nb.cells.append(v4.new_code_cell("some_text")) class AttrDynamicExporter(TemplateExporter): - @default('template_file') + @default('default_template_file') def _template_file_default(self): - return "rst.tpl" + return "index.rst" @default('raw_template') def _raw_template_default(self): return raw_template - exporter_attr_dynamic = AttrDynamicExporter() + exporter_attr_dynamic = AttrDynamicExporter(template_name='rst') output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) assert "blah" in output_attr_dynamic exporter_attr_dynamic.raw_template = '' - assert exporter_attr_dynamic.template_file == "rst.tpl" + assert exporter_attr_dynamic.template_file == "index.rst" output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) assert "blah" not in output_attr_dynamic @@ -209,15 +209,15 @@ class AttrDynamicExporter(TemplateExporter): def _raw_template_default(self): return raw_template - @default('template_file') + @default('default_template_file') def _template_file_default(self): - return "rst.tpl" + return 'index.rst' - exporter_attr_dynamic = AttrDynamicExporter() + exporter_attr_dynamic = AttrDynamicExporter(template_name='rst') output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) assert "blah" in output_attr_dynamic exporter_attr_dynamic.raw_template = '' - assert exporter_attr_dynamic.template_file == "rst.tpl" + assert exporter_attr_dynamic.template_file == 'index.rst' output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) assert "blah" not in output_attr_dynamic @@ -229,7 +229,7 @@ def test_raw_template_constructor(self): nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) - output_constructor, _ = TemplateExporter( + output_constructor, _ = TemplateExporter(template_name='rst', raw_template=raw_template).from_notebook_node(nb) assert "blah" in output_constructor @@ -239,7 +239,7 @@ def test_raw_template_assignment(self): """ nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) - exporter_assign = TemplateExporter() + exporter_assign = TemplateExporter(template_name='rst') exporter_assign.raw_template = raw_template output_assign, _ = exporter_assign.from_notebook_node(nb) assert "blah" in output_assign @@ -250,7 +250,7 @@ def test_raw_template_reassignment(self): """ nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) - exporter_reassign = TemplateExporter() + exporter_reassign = TemplateExporter(template_name='rst') exporter_reassign.raw_template = raw_template output_reassign, _ = exporter_reassign.from_notebook_node(nb) assert "blah" in output_reassign @@ -270,7 +270,7 @@ def test_raw_template_deassignment(self): output_deassign, _ = exporter_deassign.from_notebook_node(nb) assert "blah" in output_deassign exporter_deassign.raw_template = '' - assert exporter_deassign.template_file == 'rst.tpl' + assert exporter_deassign.template_file == 'index.rst' output_deassign, _ = exporter_deassign.from_notebook_node(nb) assert "blah" not in output_deassign @@ -289,7 +289,7 @@ def test_raw_template_dereassignment(self): output_dereassign, _ = exporter_dereassign.from_notebook_node(nb) assert "baz" in output_dereassign exporter_dereassign.raw_template = '' - assert exporter_dereassign.template_file == 'rst.tpl' + assert exporter_dereassign.template_file == 'index.rst' output_dereassign, _ = exporter_dereassign.from_notebook_node(nb) assert "blah" not in output_dereassign @@ -317,8 +317,8 @@ def test_exclude_code_cell(self): } } c_no_io = Config(no_io) - exporter_no_io = TemplateExporter(config=c_no_io) - exporter_no_io.template_file = 'markdown' + exporter_no_io = TemplateExporter(config=c_no_io, template_name='markdown') + exporter_no_io.template_file = 'index.md' nb_no_io, resources_no_io = exporter_no_io.from_filename(self._get_notebook()) assert not resources_no_io['global_content_filter']['include_input'] @@ -335,8 +335,8 @@ def test_exclude_code_cell(self): } } c_no_code = Config(no_code) - exporter_no_code = TemplateExporter(config=c_no_code) - exporter_no_code.template_file = 'markdown' + exporter_no_code = TemplateExporter(config=c_no_code, template_name='markdown') + exporter_no_code.template_file = 'index.md' nb_no_code, resources_no_code = exporter_no_code.from_filename(self._get_notebook()) assert not resources_no_code['global_content_filter']['include_code'] @@ -375,8 +375,8 @@ def test_exclude_markdown(self): } c_no_md = Config(no_md) - exporter_no_md = TemplateExporter(config=c_no_md) - exporter_no_md.template_file = 'python' + exporter_no_md = TemplateExporter(config=c_no_md, template_name='python') + exporter_no_md.template_file = 'index.py' nb_no_md, resources_no_md = exporter_no_md.from_filename(self._get_notebook()) assert not resources_no_md['global_content_filter']['include_markdown'] @@ -423,5 +423,6 @@ def _make_exporter(self, config=None): exporter = TemplateExporter(config=config) if not exporter.template_file: # give it a default if not specified - exporter.template_file = 'python' + exporter.template_name = 'python' + exporter.template_file = 'index.py' return exporter diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index 0a6a7f865..1860552c5 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -51,7 +51,8 @@ def validate(self, obj, value): nbconvert_aliases.update(base_aliases) nbconvert_aliases.update({ 'to' : 'NbConvertApp.export_format', - 'template' : 'TemplateExporter.template_file', + 'template' : 'TemplateExporter.template_name', + 'template-file' : 'TemplateExporter.template_file', 'writer' : 'NbConvertApp.writer_class', 'post': 'NbConvertApp.postprocessor_class', 'output': 'NbConvertApp.output_base', @@ -191,7 +192,7 @@ def _classes_default(self): 'base', 'article' and 'report'. HTML includes 'basic' and 'full'. You can specify the flavor of the format used. - > jupyter nbconvert --to html --template basic mynotebook.ipynb + > jupyter nbconvert --to html --template lab mynotebook.ipynb You can also pipe the output to stdout, rather than a file diff --git a/nbconvert/preprocessors/csshtmlheader.py b/nbconvert/preprocessors/csshtmlheader.py index 191a2ea6c..0fa7b9bb8 100755 --- a/nbconvert/preprocessors/csshtmlheader.py +++ b/nbconvert/preprocessors/csshtmlheader.py @@ -9,9 +9,11 @@ import hashlib import nbconvert.resources -from traitlets import Unicode -from .base import Preprocessor +from traitlets import Unicode, Union, Type +from pygments.style import Style +from jupyterlab_pygments import JupyterStyle +from .base import Preprocessor try: from notebook import DEFAULT_STATIC_FILES_PATH @@ -28,8 +30,9 @@ class CSSHTMLHeaderPreprocessor(Preprocessor): help="CSS highlight class identifier" ).tag(config=True) - style = Unicode('default', - help='Name of the pygments style to use' + style = Union([Unicode('default'), Type(klass=Style)], + help='Name of the pygments style to use', + default_value=JupyterStyle ).tag(config=True) def __init__(self, *pargs, **kwargs): @@ -40,9 +43,9 @@ def preprocess(self, nb, resources): """Fetch and add CSS to the resource dictionary Fetch CSS from IPython and Pygments to add at the beginning - of the html files. Add this css in resources in the + of the html files. Add this css in resources in the "inlining.css" key - + Parameters ---------- nb : NotebookNode @@ -56,24 +59,13 @@ def preprocess(self, nb, resources): return nb, resources def _generate_header(self, resources): - """ - Fills self.header with lines of CSS extracted from IPython + """ + Fills self.header with lines of CSS extracted from IPython and Pygments. """ from pygments.formatters import HtmlFormatter header = [] - - # Construct path to Jupyter CSS - sheet_filename = os.path.join( - os.path.dirname(nbconvert.resources.__file__), - 'style.min.css', - ) - - # Load style CSS file. - with io.open(sheet_filename, encoding='utf-8') as f: - header.append(f.read()) - - # Add pygments CSS + formatter = HtmlFormatter(style=self.style) pygments_css = formatter.get_style_defs(self.highlight_class) header.append(pygments_css) diff --git a/nbconvert/tests/fake_exporters.py b/nbconvert/tests/fake_exporters.py index bf75cca51..904665813 100644 --- a/nbconvert/tests/fake_exporters.py +++ b/nbconvert/tests/fake_exporters.py @@ -19,3 +19,7 @@ def _file_extension_default(self): The new file extension is `.test_ext` """ return '.test_ext' + + @default('template_extension') + def _template_extension_default(self): + return '.html' diff --git a/nbconvert/tests/test_nbconvertapp.py b/nbconvert/tests/test_nbconvertapp.py index 48b876b59..b6b5590f0 100644 --- a/nbconvert/tests/test_nbconvertapp.py +++ b/nbconvert/tests/test_nbconvertapp.py @@ -91,27 +91,27 @@ def test_explicit(self): assert os.path.isfile('notebook2.py') def test_absolute_template_file(self): - """--template '/path/to/template.tpl'""" + """--template-file '/path/to/template.tpl'""" with self.create_temp_cwd(['notebook*.ipynb']), tempdir.TemporaryDirectory() as td: template = os.path.join(td, 'mytemplate.tpl') test_output = 'success!' with open(template, 'w') as f: f.write(test_output) - self.nbconvert('--log-level 0 notebook2 --template %s' % template) + self.nbconvert('--log-level 0 notebook2 --template-file %s' % template) assert os.path.isfile('notebook2.html') with open('notebook2.html') as f: text = f.read() assert text == test_output def test_relative_template_file(self): - """Test --template 'relative/path.tpl'""" + """Test --template-file 'relative/path.tpl'""" with self.create_temp_cwd(['notebook*.ipynb']): os.mkdir('relative') template = os.path.join('relative', 'path.tpl') test_output = 'success!' with open(template, 'w') as f: f.write(test_output) - self.nbconvert('--log-level 0 notebook2 --template %s' % template) + self.nbconvert('--log-level 0 notebook2 --template-file %s' % template) assert os.path.isfile('notebook2.html') with open('notebook2.html') as f: text = f.read() @@ -171,7 +171,7 @@ def test_png_base64_html_ok(self): """Is embedded png data well formed in HTML?""" with self.create_temp_cwd(['notebook2.ipynb']): self.nbconvert('--log-level 0 --to HTML ' - 'notebook2.ipynb --template full') + 'notebook2.ipynb --template lab') assert os.path.isfile('notebook2.html') with open('notebook2.html') as f: assert "data:image/png;base64,b'" not in f.read() diff --git a/setup.py b/setup.py index 1cf78b1bb..2dcf91a1a 100644 --- a/setup.py +++ b/setup.py @@ -65,13 +65,27 @@ ], } - notebook_css_version = '5.4.0' -css_url = "https://cdn.jupyter.org/notebook/%s/style/style.min.css" % notebook_css_version +notebook_css_url = "https://cdn.jupyter.org/notebook/%s/style/style.min.css" % notebook_css_version + + +jupyterlab_css_version = '0.1.0' +jupyterlab_css_url = "https://unpkg.com/@jupyterlab/nbconvert-css@%s/style/index.css" % jupyterlab_css_version + +jupyterlab_theme_light_version = '0.19.1' +jupyterlab_theme_light_url = "https://unpkg.com/@jupyterlab/theme-light-extension@%s/static/embed.css" % jupyterlab_theme_light_version + +jupyterlab_theme_dark_version = '0.19.1' +jupyterlab_theme_dark_url = "https://unpkg.com/@jupyterlab/theme-dark-extension@%s/static/embed.css" % jupyterlab_theme_dark_version + +template_css_urls = { + 'lab': [(jupyterlab_css_url, 'index.css'), (jupyterlab_theme_light_url, 'theme-light.css'), (jupyterlab_theme_dark_url, 'theme-dark.css')], + 'classic': [(notebook_css_url, 'style.css')] +} class FetchCSS(Command): - description = "Fetch Notebook CSS from Jupyter CDN" + description = "Fetch CSS from CDN" user_options = [] def initialize_options(self): pass @@ -79,9 +93,9 @@ def initialize_options(self): def finalize_options(self): pass - def _download(self): + def _download(self, url): try: - return urlopen(css_url).read() + return urlopen(url).read() except Exception as e: if 'ssl' in str(e).lower(): try: @@ -91,39 +105,48 @@ def _download(self): raise e else: print("Failed, trying again with PycURL to avoid outdated SSL.", file=sys.stderr) - return self._download_pycurl() + return self._download_pycurl(url) raise e - def _download_pycurl(self): + def _download_pycurl(self, url): """Download CSS with pycurl, in case of old SSL (e.g. Python < 2.7.9).""" import pycurl c = pycurl.Curl() - c.setopt(c.URL, css_url) + c.setopt(c.URL, url) buf = BytesIO() c.setopt(c.WRITEDATA, buf) c.perform() return buf.getvalue() def run(self): - dest = os.path.join('nbconvert', 'resources', 'style.min.css') - if not os.path.exists('.git') and os.path.exists(dest): - # not running from git, nothing to do - return - print("Downloading CSS: %s" % css_url) - try: - css = self._download() - except Exception as e: - msg = "Failed to download css from %s: %s" % (css_url, e) - print(msg, file=sys.stderr) - if os.path.exists(dest): - print("Already have CSS: %s, moving on." % dest) - else: - raise OSError("Need Notebook CSS to proceed: %s" % dest) - return - - with open(dest, 'wb') as f: - f.write(css) - print("Downloaded Notebook CSS to %s" % dest) + for template_name, resources in template_css_urls.items(): + for url, filename in resources: + directory = os.path.join('share', 'jupyter', 'nbconvert', 'templates', template_name, 'static') + dest = os.path.join(directory, filename) + if not os.path.exists(directory): + os.makedirs(directory) + if not os.path.exists('.git') and os.path.exists(dest): + # not running from git, nothing to do + return + print("Downloading CSS: %s" % url) + try: + css = self._download(url) + except Exception as e: + msg = "Failed to download css from %s: %s" % (url, e) + print(msg, file=sys.stderr) + if os.path.exists(dest): + print("Already have CSS: %s, moving on." % dest) + else: + raise OSError("Need CSS to proceed.") + return + + with open(dest, 'wb') as f: + f.write(css) + print("Downloaded Notebook CSS to %s" % dest) + + # update package data in case this created new files + self.distribution.data_files = get_data_files() + update_package_data(self.distribution) cmdclass = {'css': FetchCSS} @@ -160,6 +183,24 @@ def run(self): with io.open(pjoin(here, 'README.md'), encoding='utf-8') as f: long_description = f.read() + +def update_package_data(distribution): + """update package_data to catch changes during setup""" + build_py = distribution.get_command_obj('build_py') + # distribution.package_data = find_package_data() + # re-init build_py options which load package_data + build_py.finalize_options() + + +def get_data_files(): + # Add all the templates + data_files = [] + for (dirpath, dirnames, filenames) in os.walk('share/jupyter/nbconvert/templates/'): + if filenames: + data_files.append((dirpath, [os.path.join(dirpath, filename) for filename in filenames])) + return data_files + + setup_args = dict( name = name, description = "Converting Jupyter Notebooks", @@ -168,6 +209,7 @@ def run(self): packages = packages, long_description= long_description, package_data = package_data, + data_files = get_data_files(), cmdclass = cmdclass, python_requires = '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', author = 'Jupyter Development Team', @@ -199,7 +241,8 @@ def run(self): setup_args['install_requires'] = [ 'mistune>=0.8.1,<2', 'jinja2>=2.4', - 'pygments', + 'pygments>=2.4.1', + 'jupyterlab_pygments', 'traitlets>=4.2', 'jupyter_core', 'nbformat>=4.4', diff --git a/share/jupyter/nbconvert/templates/asciidoc/conf.json b/share/jupyter/nbconvert/templates/asciidoc/conf.json new file mode 100644 index 000000000..37cdfa7bc --- /dev/null +++ b/share/jupyter/nbconvert/templates/asciidoc/conf.json @@ -0,0 +1,6 @@ +{ + "base_template": "base", + "mimetypes": { + "text/asciidoc": true + } +} \ No newline at end of file diff --git a/nbconvert/templates/asciidoc.tpl b/share/jupyter/nbconvert/templates/asciidoc/index.asciidoc similarity index 98% rename from nbconvert/templates/asciidoc.tpl rename to share/jupyter/nbconvert/templates/asciidoc/index.asciidoc index 8f32c4a6e..1512a2daa 100644 --- a/nbconvert/templates/asciidoc.tpl +++ b/share/jupyter/nbconvert/templates/asciidoc/index.asciidoc @@ -1,4 +1,4 @@ -{% extends 'display_priority.tpl' %} +{% extends 'display_priority.j2' %} {% block input %} diff --git a/nbconvert/templates/html/basic.tpl b/share/jupyter/nbconvert/templates/base/base.html similarity index 98% rename from nbconvert/templates/html/basic.tpl rename to share/jupyter/nbconvert/templates/base/base.html index 110e86423..e9bd3eb54 100644 --- a/nbconvert/templates/html/basic.tpl +++ b/share/jupyter/nbconvert/templates/base/base.html @@ -1,5 +1,5 @@ -{%- extends 'display_priority.tpl' -%} -{% from 'celltags.tpl' import celltags %} +{%- extends 'display_priority.j2' -%} +{% from 'celltags.j2' import celltags %} {% block codecell %}