Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add NumberedHeadingsPreprocessor #2187

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/source/api/preprocessors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Converting text

.. autoclass:: HighlightMagicsPreprocessor

.. autoclass:: NumberedHeadingsPreprocessor

Metadata and header control
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 2 additions & 0 deletions nbconvert/preprocessors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .extractoutput import ExtractOutputPreprocessor
from .highlightmagics import HighlightMagicsPreprocessor
from .latex import LatexPreprocessor
from .numbered_headings import NumberedHeadingsPreprocessor
from .regexremove import RegexRemovePreprocessor
from .svg2pdf import SVG2PDFPreprocessor
from .tagremove import TagRemovePreprocessor
Expand All @@ -30,6 +31,7 @@
"ExtractOutputPreprocessor",
"HighlightMagicsPreprocessor",
"LatexPreprocessor",
"NumberedHeadingsPreprocessor",
"RegexRemovePreprocessor",
"SVG2PDFPreprocessor",
"TagRemovePreprocessor",
Expand Down
51 changes: 51 additions & 0 deletions nbconvert/preprocessors/numbered_headings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
Preprocessor that transforms markdown cells: Insert numbering in from of heading
"""

import re

from nbconvert.preprocessors.base import Preprocessor


class NumberedHeadingsPreprocessor(Preprocessor):
"""Pre-processor that will rewrite markdown headings to include numberings."""

def __init__(self, *args, **kwargs):
"""Init"""
super().__init__(*args, **kwargs)
self.current_numbering = [0]

def format_numbering(self):
"""Return a string representation of the current numbering"""
return ".".join(str(n) for n in self.current_numbering)

def _inc_current_numbering(self, level):
"""Increase internal counter keeping track of numberings"""
if level > len(self.current_numbering):
self.current_numbering = self.current_numbering + [0] * (
level - len(self.current_numbering)
)
elif level < len(self.current_numbering):
self.current_numbering = self.current_numbering[:level]
self.current_numbering[level - 1] += 1

def _transform_markdown_line(self, line, resources):
"""Rewrites one markdown line, if needed"""
if m := re.match(r"^(?P<level>#+) (?P<heading>.*)", line):
level = len(m.group("level"))
self._inc_current_numbering(level)
old_heading = m.group("heading").strip()
new_heading = self.format_numbering() + " " + old_heading
return "#" * level + " " + new_heading

return line

def preprocess_cell(self, cell, resources, index):
"""Rewrites all the headings in the cell if it is markdown"""
if cell["cell_type"] == "markdown":
cell["source"] = "\n".join(
self._transform_markdown_line(line, resources)
for line in cell["source"].splitlines()
)

return cell, resources
86 changes: 86 additions & 0 deletions tests/preprocessors/test_numbered_headings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
Module with tests for the Numbered Headings preprocessor.
"""

from nbformat import v4 as nbformat

from nbconvert.preprocessors.numbered_headings import NumberedHeadingsPreprocessor

from .base import PreprocessorTestsBase

MARKDOWN_1 = """
# Heading 1

## Sub-heading

some content
"""

MARKDOWN_1_POST = """
# 1 Heading 1

## 1.1 Sub-heading

some content
"""


MARKDOWN_2 = """

## Second sub-heading

# Another main heading

## Sub-heading


some more content

### Third heading
"""

MARKDOWN_2_POST = """

## 1.2 Second sub-heading

# 2 Another main heading

## 2.1 Sub-heading


some more content

### 2.1.1 Third heading
"""


class TestNumberedHeadings(PreprocessorTestsBase):
def build_notebook(self):
cells = [
nbformat.new_code_cell(source="$ e $", execution_count=1),
nbformat.new_markdown_cell(source=MARKDOWN_1),
nbformat.new_code_cell(source="$ e $", execution_count=1),
nbformat.new_markdown_cell(source=MARKDOWN_2),
]

return nbformat.new_notebook(cells=cells)

def build_preprocessor(self):
"""Make an instance of a preprocessor"""
preprocessor = NumberedHeadingsPreprocessor()
preprocessor.enabled = True
return preprocessor

def test_constructor(self):
"""Can a ClearOutputPreprocessor be constructed?"""
self.build_preprocessor()

def test_output(self):
"""Test the output of the NumberedHeadingsPreprocessor"""
nb = self.build_notebook()
res = self.build_resources()
preprocessor = self.build_preprocessor()
nb, res = preprocessor(nb, res)
print(nb.cells[1].source)
assert nb.cells[1].source.strip() == MARKDOWN_1_POST.strip()
assert nb.cells[3].source.strip() == MARKDOWN_2_POST.strip()
Loading