Skip to content

Commit 4e870ae

Browse files
author
Mathias Millet
committed
feat: add NumberedHeadingsPreprocessor
1 parent e159962 commit 4e870ae

File tree

4 files changed

+133
-0
lines changed

4 files changed

+133
-0
lines changed

docs/source/api/preprocessors.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Converting text
3636

3737
.. autoclass:: HighlightMagicsPreprocessor
3838

39+
.. autoclass:: NumberedHeadingsPreprocessor
40+
3941
Metadata and header control
4042
~~~~~~~~~~~~~~~~~~~~~~~~~~~
4143

nbconvert/preprocessors/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .extractoutput import ExtractOutputPreprocessor
1414
from .highlightmagics import HighlightMagicsPreprocessor
1515
from .latex import LatexPreprocessor
16+
from .numbered_headings import NumberedHeadingsPreprocessor
1617
from .regexremove import RegexRemovePreprocessor
1718
from .svg2pdf import SVG2PDFPreprocessor
1819
from .tagremove import TagRemovePreprocessor
@@ -30,6 +31,7 @@
3031
"ExtractOutputPreprocessor",
3132
"HighlightMagicsPreprocessor",
3233
"LatexPreprocessor",
34+
"NumberedHeadingsPreprocessor",
3335
"RegexRemovePreprocessor",
3436
"SVG2PDFPreprocessor",
3537
"TagRemovePreprocessor",
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import re
2+
3+
from nbconvert.preprocessors.base import Preprocessor
4+
5+
6+
class NumberedHeadingsPreprocessor(Preprocessor):
7+
"""Pre-processor that will rewrite markdown headings to include numberings."""
8+
9+
def __init__(self, *args, **kwargs):
10+
super().__init__(*args, **kwargs)
11+
self.current_numbering = [0]
12+
13+
def format_numbering(self):
14+
"""Return a string representation of the current numbering"""
15+
return ".".join(str(n) for n in self.current_numbering)
16+
17+
def inc_current_numbering(self, level):
18+
if level > len(self.current_numbering):
19+
self.current_numbering = self.current_numbering + [0] * (
20+
level - len(self.current_numbering)
21+
)
22+
elif level < len(self.current_numbering):
23+
self.current_numbering = self.current_numbering[:level]
24+
self.current_numbering[level - 1] += 1
25+
26+
def transform_markdown_line(self, line, resources):
27+
if m := re.match(r"^(?P<level>#+) (?P<heading>.*)", line):
28+
level = len(m.group("level"))
29+
self.inc_current_numbering(level)
30+
old_heading = m.group("heading").strip()
31+
new_heading = self.format_numbering() + " " + old_heading
32+
return "#" * level + " " + new_heading
33+
34+
return line
35+
36+
def preprocess_cell(self, cell, resources, index):
37+
if cell["cell_type"] == "markdown":
38+
cell["source"] = "\n".join(
39+
self.transform_markdown_line(line, resources)
40+
for line in cell["source"].splitlines()
41+
)
42+
43+
return cell, resources
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""
2+
Module with tests for the Numbered Headings preprocessor.
3+
"""
4+
5+
from nbformat import v4 as nbformat
6+
7+
from nbconvert.preprocessors.numbered_headings import NumberedHeadingsPreprocessor
8+
9+
from .base import PreprocessorTestsBase
10+
11+
MARKDOWN_1 = """
12+
# Heading 1
13+
14+
## Sub-heading
15+
16+
some content
17+
"""
18+
19+
MARKDOWN_1_POST = """
20+
# 1 Heading 1
21+
22+
## 1.1 Sub-heading
23+
24+
some content
25+
"""
26+
27+
28+
MARKDOWN_2 = """
29+
30+
## Second sub-heading
31+
32+
# Another main heading
33+
34+
## Sub-heading
35+
36+
37+
some more content
38+
39+
### Third heading
40+
"""
41+
42+
MARKDOWN_2_POST = """
43+
44+
## 1.2 Second sub-heading
45+
46+
# 2 Another main heading
47+
48+
## 2.1 Sub-heading
49+
50+
51+
some more content
52+
53+
### 2.1.1 Third heading
54+
"""
55+
56+
57+
class TestNumberedHeadings(PreprocessorTestsBase):
58+
def build_notebook(self):
59+
cells = [
60+
nbformat.new_code_cell(source="$ e $", execution_count=1),
61+
nbformat.new_markdown_cell(source=MARKDOWN_1),
62+
nbformat.new_code_cell(source="$ e $", execution_count=1),
63+
nbformat.new_markdown_cell(source=MARKDOWN_2),
64+
]
65+
66+
return nbformat.new_notebook(cells=cells)
67+
68+
def build_preprocessor(self):
69+
"""Make an instance of a preprocessor"""
70+
preprocessor = NumberedHeadingsPreprocessor()
71+
preprocessor.enabled = True
72+
return preprocessor
73+
74+
def test_constructor(self):
75+
"""Can a ClearOutputPreprocessor be constructed?"""
76+
self.build_preprocessor()
77+
78+
def test_output(self):
79+
"""Test the output of the NumberedHeadingsPreprocessor"""
80+
nb = self.build_notebook()
81+
res = self.build_resources()
82+
preprocessor = self.build_preprocessor()
83+
nb, res = preprocessor(nb, res)
84+
print(nb.cells[1].source)
85+
assert nb.cells[1].source.strip() == MARKDOWN_1_POST.strip()
86+
assert nb.cells[3].source.strip() == MARKDOWN_2_POST.strip()

0 commit comments

Comments
 (0)