Skip to content

Commit df53b2b

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

File tree

4 files changed

+140
-0
lines changed

4 files changed

+140
-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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Preprocessor that transforms markdown cells: Insert numbering in from of heading
3+
"""
4+
5+
import re
6+
7+
from nbconvert.preprocessors.base import Preprocessor
8+
9+
10+
class NumberedHeadingsPreprocessor(Preprocessor):
11+
"""Pre-processor that will rewrite markdown headings to include numberings."""
12+
13+
def __init__(self, *args, **kwargs):
14+
"""Init"""
15+
super().__init__(*args, **kwargs)
16+
self.current_numbering = [0]
17+
18+
def format_numbering(self):
19+
"""Return a string representation of the current numbering"""
20+
return ".".join(str(n) for n in self.current_numbering)
21+
22+
def _inc_current_numbering(self, level):
23+
"""Increase internal counter keeping track of numberings"""
24+
if level > len(self.current_numbering):
25+
self.current_numbering = self.current_numbering + [0] * (
26+
level - len(self.current_numbering)
27+
)
28+
elif level < len(self.current_numbering):
29+
self.current_numbering = self.current_numbering[:level]
30+
self.current_numbering[level - 1] += 1
31+
32+
def _transform_markdown_line(self, line, resources):
33+
"""Rewrites one markdown line, if needed"""
34+
if m := re.match(r"^(?P<level>#+) (?P<heading>.*)", line):
35+
level = len(m.group("level"))
36+
self._inc_current_numbering(level)
37+
old_heading = m.group("heading").strip()
38+
new_heading = self.format_numbering() + " " + old_heading
39+
return "#" * level + " " + new_heading
40+
41+
return line
42+
43+
def preprocess_cell(self, cell, resources, index):
44+
if cell["cell_type"] == "markdown":
45+
cell["source"] = "\n".join(
46+
self._transform_markdown_line(line, resources)
47+
for line in cell["source"].splitlines()
48+
)
49+
50+
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)