Source code for ipypublish.preprocessors.slides_from_markdown

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 is_header(line, max_level): """if max_level is 0 assumes all headers ok Examples -------- >>> is_header("abc",0) False >>> is_header("#",0) False >>> is_header("# title",0) True >>> is_header("### title",3) True >>> is_header("### title",2) False """ if max_level: return len(re.findall("^#{{1,{0}}} .+".format(max_level), line)) > 0 else: return len(re.findall("^#+ .+", line)) > 0
[docs]def header_level(line): """ Examples -------- >>> header_level('# title') 1 >>> header_level('### title') 3 """ i = 0 title = line + "e" while title[0] == "#": i += 1 title = title[1:] return i
[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