Skip to content

Commit 9f8f427

Browse files
author
Mathias Millet
committed
use mistune for numbered headings
1 parent 2141870 commit 9f8f427

File tree

2 files changed

+61
-22
lines changed

2 files changed

+61
-22
lines changed

nbconvert/preprocessors/numbered_headings.py

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,33 @@
22
Preprocessor that transforms markdown cells: Insert numbering in from of heading
33
"""
44

5-
import re
5+
from traitlets.log import get_logger
66

77
from nbconvert.preprocessors.base import Preprocessor
88

9+
logger = get_logger()
10+
11+
try: # for Mistune >= 3.0
12+
import mistune
13+
from mistune.core import BlockState
14+
from mistune.renderers.markdown import MarkdownRenderer
15+
16+
MISTUNE_V3 = True
17+
except ImportError: # for Mistune >= 2.0
18+
MISTUNE_V3 = False
19+
920

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

1324
def __init__(self, *args, **kwargs):
1425
"""Init"""
1526
super().__init__(*args, **kwargs)
27+
if not MISTUNE_V3:
28+
logger.error("NumberedHeadingsPreprocessor requires mistune >= 3")
29+
return
30+
self.md_parser = mistune.create_markdown(renderer=None)
31+
self.md_renderer = MarkdownRenderer()
1632
self.current_numbering = [0]
1733

1834
def format_numbering(self):
@@ -29,23 +45,26 @@ def _inc_current_numbering(self, level):
2945
self.current_numbering = self.current_numbering[:level]
3046
self.current_numbering[level - 1] += 1
3147

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-
4348
def preprocess_cell(self, cell, resources, index):
4449
"""Rewrites all the headings in the cell if it is markdown"""
45-
if cell["cell_type"] == "markdown":
46-
cell["source"] = "\n".join(
47-
self._transform_markdown_line(line, resources)
48-
for line in cell["source"].splitlines()
49-
)
50-
51-
return cell, resources
50+
if not MISTUNE_V3:
51+
return cell, resources
52+
if cell["cell_type"] != "markdown":
53+
return cell, resources
54+
try:
55+
md_ast = self.md_parser(cell["source"])
56+
assert not isinstance(md_ast, str) # type guard ; str is not returned by ast parser
57+
for element in md_ast:
58+
if element["type"] == "heading":
59+
level = element["attrs"]["level"]
60+
self._inc_current_numbering(level)
61+
if len(element["children"]) > 0:
62+
child = element["children"][0]
63+
if child["type"] == "text":
64+
child["raw"] = self.format_numbering() + " " + child["raw"]
65+
new_source = self.md_renderer(md_ast, BlockState())
66+
cell["source"] = new_source
67+
return cell, resources
68+
except Exception:
69+
logger.warning("Failed processing cell headings", exc_info=True)
70+
return cell, resources

tests/preprocessors/test_numbered_headings.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,31 @@
4747
4848
## 2.1 Sub-heading
4949
50-
5150
some more content
5251
5352
### 2.1.1 Third heading
5453
"""
5554

55+
MARKDOWN_3 = """
56+
# HEADING
57+
58+
```
59+
# this is not a heading
60+
61+
## this neither
62+
```
63+
"""
64+
65+
MARKDOWN_3_POST = """
66+
# 3 HEADING
67+
68+
```
69+
# this is not a heading
70+
71+
## this neither
72+
```
73+
"""
74+
5675

5776
class TestNumberedHeadings(PreprocessorTestsBase):
5877
def build_notebook(self):
@@ -61,6 +80,7 @@ def build_notebook(self):
6180
nbformat.new_markdown_cell(source=MARKDOWN_1),
6281
nbformat.new_code_cell(source="$ e $", execution_count=1),
6382
nbformat.new_markdown_cell(source=MARKDOWN_2),
83+
nbformat.new_markdown_cell(source=MARKDOWN_3),
6484
]
6585

6686
return nbformat.new_notebook(cells=cells)
@@ -72,7 +92,7 @@ def build_preprocessor(self):
7292
return preprocessor
7393

7494
def test_constructor(self):
75-
"""Can a ClearOutputPreprocessor be constructed?"""
95+
"""Can a NumberedHeadingsPreprocessor be constructed?"""
7696
self.build_preprocessor()
7797

7898
def test_output(self):
@@ -81,6 +101,6 @@ def test_output(self):
81101
res = self.build_resources()
82102
preprocessor = self.build_preprocessor()
83103
nb, res = preprocessor(nb, res)
84-
print(nb.cells[1].source)
85104
assert nb.cells[1].source.strip() == MARKDOWN_1_POST.strip()
86105
assert nb.cells[3].source.strip() == MARKDOWN_2_POST.strip()
106+
assert nb.cells[4].source.strip() == MARKDOWN_3_POST.strip()

0 commit comments

Comments
 (0)