Skip to content

Commit 45553a1

Browse files
author
Mathias Millet
committed
use mistune for numbered headings
1 parent 2141870 commit 45553a1

File tree

2 files changed

+53
-20
lines changed

2 files changed

+53
-20
lines changed

nbconvert/preprocessors/numbered_headings.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,28 @@
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+
except ImportError: # for Mistune >= 2.0
16+
logger.error("NumberedHeadingsPreprocessor requires mistune >= 3")
17+
918

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

1322
def __init__(self, *args, **kwargs):
1423
"""Init"""
1524
super().__init__(*args, **kwargs)
25+
self.md_parser = mistune.create_markdown(renderer=None)
26+
self.md_renderer = MarkdownRenderer()
1627
self.current_numbering = [0]
1728

1829
def format_numbering(self):
@@ -29,23 +40,24 @@ def _inc_current_numbering(self, level):
2940
self.current_numbering = self.current_numbering[:level]
3041
self.current_numbering[level - 1] += 1
3142

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

tests/preprocessors/test_numbered_headings.py

Lines changed: 22 additions & 1 deletion
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)
@@ -84,3 +104,4 @@ def test_output(self):
84104
print(nb.cells[1].source)
85105
assert nb.cells[1].source.strip() == MARKDOWN_1_POST.strip()
86106
assert nb.cells[3].source.strip() == MARKDOWN_2_POST.strip()
107+
assert nb.cells[4].source.strip() == MARKDOWN_3_POST.strip()

0 commit comments

Comments
 (0)