Skip to content

Commit 6ff5dbb

Browse files
author
Mathias Millet
committed
use mistune for numbered headings
1 parent 2141870 commit 6ff5dbb

File tree

3 files changed

+61
-22
lines changed

3 files changed

+61
-22
lines changed

nbconvert/exporters/exporter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class Exporter(LoggingConfigurable):
9797
"nbconvert.preprocessors.ExtractOutputPreprocessor",
9898
"nbconvert.preprocessors.ExtractAttachmentsPreprocessor",
9999
"nbconvert.preprocessors.ClearMetadataPreprocessor",
100+
"nbconvert.preprocessors.NumberedHeadingsPreprocessor",
100101
],
101102
help="""List of preprocessors available by default, by name, namespace,
102103
instance, or type.""",

nbconvert/preprocessors/numbered_headings.py

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,34 @@
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+
20+
WRONG_MISTUNE_VERSION_ERROR = "Error: NumberedHeadingsPreprocessor requires mistune >= 3"
21+
922

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

1326
def __init__(self, *args, **kwargs):
1427
"""Init"""
1528
super().__init__(*args, **kwargs)
29+
if not MISTUNE_V3:
30+
raise Exception(WRONG_MISTUNE_VERSION_ERROR)
31+
self.md_parser = mistune.create_markdown(renderer=None)
32+
self.md_renderer = MarkdownRenderer()
1633
self.current_numbering = [0]
1734

1835
def format_numbering(self):
@@ -29,23 +46,24 @@ def _inc_current_numbering(self, level):
2946
self.current_numbering = self.current_numbering[:level]
3047
self.current_numbering[level - 1] += 1
3148

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