import copy
import logging
import re
import traitlets as traits
from nbconvert.preprocessors import Preprocessor
from nbformat.notebooknode import NotebookNode
[docs]class FinalCells(object):
""" a class that stores cells
"""
def __init__(self, header_slide):
self.cells = []
if header_slide:
self.horizontalbreak_after = "horizontalbreak_after_plusvertical"
else:
self.horizontalbreak_after = "horizontalbreak_after"
[docs] def mkdcell(self, source, metadata, slidetype):
meta = copy.deepcopy(metadata)
meta.ipyslides = slidetype
self.append(
NotebookNode(
{"cell_type": "markdown", "source": "\n".join(source), "metadata": meta}
)
)
[docs] def append(self, cell):
last = self.last()
if not last:
pass
elif cell.metadata.ipyslides == "verticalbreak_after":
pass # last.metadata.ipyslides = 'verticalbreak_above'
elif cell.metadata.ipyslides == self.horizontalbreak_after:
# if last.metadata.ipyslides == 'before_header':
# last.metadata.ipyslides == 'between_headers'
if not last.metadata.ipyslides == self.horizontalbreak_after:
last.metadata.ipyslides = "horizontalbreak_before"
else:
last.metadata.ipyslides = "horizontalbreak_after_novertical"
self.cells.append(cell)
[docs] def first(self):
for cell in self.cells:
if cell.metadata.ipyslides not in ["skip", "notes"]:
return cell
return False
[docs] def last(self):
for cell in reversed(self.cells):
if cell.metadata.ipyslides not in ["skip", "notes"]:
return cell
return False
[docs] def finalize(self):
if not self.first():
return False
if self.first().metadata.ipyslides == "normal":
self.first().metadata.ipyslides = "first_cell"
if self.last().metadata.ipyslides == "normal":
self.last().metadata.ipyslides = "last_cell"
return True
[docs]def number_title(line, current_levels):
"""
Examples
--------
>>> number_title("# title",[])
('# 1. title', [1])
>>> number_title("## title",[])
('## 1.1. title', [1, 1])
>>> number_title("# title",[1,1])
('# 2. title', [2])
>>> number_title("## title",[2,1])
('## 2.2. title', [2, 2])
>>> number_title("### title a#bc",[2])
('### 2.1.1. title a#bc', [2, 1, 1])
>>> number_title("### title a#bc",[2,1,2,3])
('### 2.1.3. title a#bc', [2, 1, 3])
"""
level = header_level(line)
if not level > 0:
raise ValueError("level must be > 0: {}".format(level))
if len(current_levels) < level:
while len(current_levels) < level:
current_levels.append(1)
else:
current_levels = current_levels[:level]
current_levels[-1] += 1
hashes, title = line.split(" ", 1)
numbers = ".".join([str(i) for i in current_levels]) + "."
new = " ".join([hashes, numbers, title])
return new, current_levels
[docs]class MarkdownSlides(Preprocessor):
""" a preprocessor to setup the notebook as an ipyslideshow,
according to a set of rules
- markdown cells containaing # headers are broken into individual cells
- any cells where ipub.ignore=True is set to 'skip'
- any code cells with no other ipub tags are set to 'skip'
- any header level >= column_level starts a new column
- else, any header level >= row_level starts a new row
- if max_cells is not 0, then breaks to a new row after <max_cells> cells
"""
column_level = traits.Integer(
1, min=0, help="maximum header level for new columns (0 indicates no maximum)"
).tag(config=True)
row_level = traits.Integer(
0, min=0, help="maximum header level for new rows (0 indicates no maximum)"
).tag(config=True)
header_slide = traits.Bool(
False,
help=("if True, make the first header in a " "column appear on its own slide"),
).tag(config=True)
max_cells = traits.Integer(
0, min=0, help="maximum number of nb cells per slide (0 indicates no maximum)"
).tag(config=True)
autonumbering = traits.Bool(
False, help="append section numbering to titles, e.g. 1.1.1 Title"
).tag(config=True)
[docs] def preprocess(self, nb, resources):
logging.info("creating slides based on markdown and existing slide tags")
latexdoc_tags = ["code", "error", "table", "equation", "figure", "text"]
# break up titles
cells_in_slide = 0
final_cells = FinalCells(self.header_slide)
header_levels = []
try:
base_numbering = nb.metadata.toc.base_numbering
header_levels = list(map(lambda x: int(x), base_numbering.split(".")))
header_levels[0] -= 1
logging.debug("base_numbering = " + base_numbering)
logging.debug("header_levels = " + str(header_levels))
except ValueError:
logging.warning("Invalid toc.base_numbering in notebook metadata")
except AttributeError:
logging.debug("No toc.base_numbering in notebook metadata; starting at 1")
for i, cell in enumerate(nb.cells):
# Make sure every cell has an ipub meta tag
cell.metadata.ipub = cell.metadata.get("ipub", NotebookNode())
if cell.metadata.ipub.get("ignore", False):
cell.metadata.ipyslides = "skip"
final_cells.append(cell)
continue
if cell.metadata.ipub.get("slide", False) == "notes":
cell.metadata.ipyslides = "notes"
final_cells.append(cell)
continue
if not cell.cell_type == "markdown":
# TODO this doesn't test if the data is actually available
# to be output
if not any(
[cell.metadata.ipub.get(typ, False) for typ in latexdoc_tags]
):
cell.metadata.ipyslides = "skip"
final_cells.append(cell)
continue
if cells_in_slide > self.max_cells and self.max_cells:
cell.metadata.ipyslides = "verticalbreak_after"
cells_in_slide = 1
elif cell.metadata.ipub.get("slide", False) == "new":
cell.metadata.ipyslides = "verticalbreak_after"
cells_in_slide = 1
else:
cell.metadata.ipyslides = "normal"
cells_in_slide += 1
final_cells.append(cell)
continue
nonheader_lines = []
for line in cell.source.split("\n"):
if is_header(line, 0) and self.autonumbering:
line, header_levels = number_title(line, header_levels[:])
if is_header(line, self.column_level):
if nonheader_lines and cell.metadata.ipub.get("slide", False):
if (
cells_in_slide > self.max_cells and self.max_cells
) or cell.metadata.ipub.slide == "new":
final_cells.mkdcell(
nonheader_lines, cell.metadata, "verticalbreak_after"
)
cells_in_slide = 1
else:
cells_in_slide += 1
final_cells.mkdcell(
nonheader_lines, cell.metadata, "normal"
)
# current_lines = []
if self.header_slide:
final_cells.mkdcell(
[line], cell.metadata, "horizontalbreak_after_plusvertical"
)
else:
final_cells.mkdcell(
[line], cell.metadata, "horizontalbreak_after"
)
cells_in_slide = 1
elif is_header(line, self.row_level):
if nonheader_lines and cell.metadata.ipub.get("slide", False):
if (
cells_in_slide > self.max_cells and self.max_cells
) or cell.metadata.ipub.slide == "new":
final_cells.mkdcell(
nonheader_lines, cell.metadata, "verticalbreak_after"
)
cells_in_slide = 1
else:
cells_in_slide += 1
final_cells.mkdcell(
nonheader_lines, cell.metadata, "normal"
)
# current_lines = []
final_cells.mkdcell([line], cell.metadata, "verticalbreak_after")
cells_in_slide = 1
else:
nonheader_lines.append(line)
if nonheader_lines and cell.metadata.ipub.get("slide", False):
if (
cells_in_slide > self.max_cells and self.max_cells
) or cell.metadata.ipub.slide == "new":
final_cells.mkdcell(
nonheader_lines, cell.metadata, "verticalbreak_after"
)
cells_in_slide = 1
else:
cells_in_slide += 1
final_cells.mkdcell(nonheader_lines, cell.metadata, "normal")
if not final_cells.finalize():
logging.warning("no cells available for slideshow")
nb.cells = final_cells.cells
return nb, resources