Jupyter Notebook导出幻灯片(nbconvert 6以上版本)

引言

关于如何将Juypter Notebook导出为幻灯片(基于reveal.js),之前曾经写过一篇(文章)[customize-ipynb-slides-reveal-js/]。但是前几天整理资料的时候发现,随着nbconvert升级到6以上的版本之后,由于其模板系统发生了破坏性改动(breaking changes),之前那篇文章的方法已经不再适用。因此,针对nbconvert6以上的版本做了些调整,与之前相同的东西不再赘述。

模板系统改动

nbconvert6引入了新的版本系统,主要变动包括:

  • 模板的后缀名从tpl改为了j2,但是实际还是jinja2语法;

  • 自带模板的路径安装在installation prefix>/share/jupyter/nbconvert

  • 在定义模板时,需要有一个conf.json位于模板的根目录下,其作用为标明:

    • 所继承的基本模板
    • 模板的mimetypes
    • 适用该模板时要在exporter中注册的预处理器类(preprocessors)
  • 采用Reveal.js 4.x 版本,并且可以用HTML exporter导出:

    1
    jupyter nbconvert <path-to-notebook> --to html --template reveal

    注:目前--to slides仍然适用。

自定义模板

假设我们自定义模板的名称为my_slides_template,其目录结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
.
|-- base.html.j2
|-- conf.json
|-- index.html.j2
`-- static
|-- custom_reveal.css
|-- plugin
| `-- customcontrols
| |-- plugin.js
| `-- style.css
`-- zenburn.css

3 directories, 7 files

我首先复制了nbconvert自带的reveal模板,然后在此基础上进行修改。

Cell的元数据修改

通过为Juypter Notebook的Cell增加自定义的元数据(metadata),从而控制其导出时的class属性。

这一部分通过修改base.html.j2实现。

1
2
3
4
5
6
7
8
9
{%- if cell.metadata.get('fragment_start', False) -%}
{# 控制幻灯片是否可见 #}
<div class="fragment {{'current-visible' if cell.metadata.get('current_visible', False)}}">
{%- endif -%}

{# 首页幻灯片 #}
{%- if cell.metadata.get('homepage', False) -%}
<div class="homepage">
{%- endif -%}

对模板主入口的修改

index.html.j2是模板的主入口,也就是exporter通过读取index.html.j2,然后进行解析和导出。

导入外部文件

主要是样式表:

1
2
3
4
5
<!-- General and theme style sheets -->
<link rel="stylesheet" href="{{ reveal_url_prefix }}/dist/reveal.css">
<link rel="stylesheet" href="{{ reveal_url_prefix }}/dist/theme/{{reveal_theme}}.css" id="theme">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js-plugins/menu/font-awesome/css/fontawesome.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css" integrity="sha512-MV7K8+y+gLIBoVD59lQIYicR65iaqukzvf/nwasF0nqhPay5w/9lJmVM2hMDcnK1OnMGCdVK+iQrJ7lzPJQd1w==" crossorigin="anonymous" referrerpolicy="no-referrer" />

导入静态文件

导入static文件夹中的文件:

1
2
3
4
{{ resources.include_css("static/custom_reveal.css") }}
{{ resources.include_css("static/zenburn.css") }}
{{ resources.include_css("static/plugin/customcontrols/style.css") }}
{{ resources.include_js("static/plugin/customcontrols/plugin.js") }}

添加页眉和页脚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{%- block body_header -%}
{% if resources.theme == 'dark' %}
<body class="jp-Notebook" data-jp-theme-light="false" data-jp-theme-name="JupyterLab Dark">
{% else %}
<body class="jp-Notebook" data-jp-theme-light="true" data-jp-theme-name="JupyterLab Light">
{% endif %}
{# 页眉 #}
<div class="headbar">
{{nb.metadata.get('headbar', '')}}
</div>
{# 页脚 #}
<div class="footer">
Copyright &copy;2022 <a href="https://www.northfar.net">Zhang Tongshuai</a>
</div>
<div class="reveal">
<div class="slides">
{%- endblock body_header -%}

设置Reveal参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function(Reveal, RevealNotes){
// 插件需要
window.Reveal = Reveal
// Full list of configuration options available here: https://github.com/hakimel/reveal.js#configuration
Reveal.initialize({
width: 1600,
height: 1000,
margin: 0.01,
controls: true,
progress: true,
history: true,
mouseWheel: true,
slideNumber: 'c/t',
transition: "{{reveal_transition}}",
customcontrols: {
controls: [
{
id: 'toggle-overview',
title: 'Toggle overview (O)',
icon: '<i class="fa fa-th"></i>', // 字体文件通过外部文件引入
action: 'Reveal.toggleOverview();'
}
]
},
// 注意:插件所需要的文件已经在静态文件中引入
plugins: [RevealNotes, RevealCustomControls]
});

var update = function(event){
if(MathJax.Hub.getAllJax(Reveal.getCurrentSlide())){
MathJax.Hub.Rerender(Reveal.getCurrentSlide());
}
};
}

结论

以上就是在nbconvert6.x下自定义Reveal模板的主要调整。

参考资料

  1. https://blog.jupyter.org/the-templating-system-of-nbconvert-6-47ea781eacd2
  2. https://nbconvert.readthedocs.io/en/latest/changelog.html
  3. https://github.com/jupyter/nbconvert/issues/1369
  4. https://nbconvert.readthedocs.io/en/latest/customizing.html