Skip to content

Commit

Permalink
use mistune for numbered headings
Browse files Browse the repository at this point in the history
  • Loading branch information
Mathias Millet committed Feb 9, 2025
1 parent 2141870 commit 232d78c
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 23 deletions.
1 change: 1 addition & 0 deletions nbconvert/exporters/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class Exporter(LoggingConfigurable):
"nbconvert.preprocessors.ExtractOutputPreprocessor",
"nbconvert.preprocessors.ExtractAttachmentsPreprocessor",
"nbconvert.preprocessors.ClearMetadataPreprocessor",
"nbconvert.preprocessors.NumberedHeadingsPreprocessor",
],
help="""List of preprocessors available by default, by name, namespace,
instance, or type.""",
Expand Down
54 changes: 34 additions & 20 deletions nbconvert/preprocessors/numbered_headings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,30 @@
Preprocessor that transforms markdown cells: Insert numbering in from of heading
"""

import re

from nbconvert.preprocessors.base import Preprocessor

try: # for Mistune >= 3.0
import mistune
from mistune.core import BlockState
from mistune.renderers.markdown import MarkdownRenderer

MISTUNE_V3 = True
except ImportError: # for Mistune >= 2.0
MISTUNE_V3 = False

WRONG_MISTUNE_VERSION_ERROR = "Error: NumberedHeadingsPreprocessor requires mistune >= 3"


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

def __init__(self, *args, **kwargs):
"""Init"""
super().__init__(*args, **kwargs)
if not MISTUNE_V3:
raise Exception(WRONG_MISTUNE_VERSION_ERROR)
self.md_parser = mistune.create_markdown(renderer=None)
self.md_renderer = MarkdownRenderer()
self.current_numbering = [0]

def format_numbering(self):
Expand All @@ -29,23 +42,24 @@ def _inc_current_numbering(self, level):
self.current_numbering = self.current_numbering[:level]
self.current_numbering[level - 1] += 1

def _transform_markdown_line(self, line, resources):
"""Rewrites one markdown line, if needed"""
if m := re.match(r"^(?P<level>#+) (?P<heading>.*)", line):
level = len(m.group("level"))
self._inc_current_numbering(level)
old_heading = m.group("heading").strip()
new_heading = self.format_numbering() + " " + old_heading
return "#" * level + " " + new_heading

return line

def preprocess_cell(self, cell, resources, index):
"""Rewrites all the headings in the cell if it is markdown"""
if cell["cell_type"] == "markdown":
cell["source"] = "\n".join(
self._transform_markdown_line(line, resources)
for line in cell["source"].splitlines()
)

return cell, resources
if cell["cell_type"] != "markdown":
return cell, resources
try:
md_ast = self.md_parser(cell["source"])
assert not isinstance(md_ast, str) # type guard ; str is not returned by ast parser
for element in md_ast:
if element["type"] == "heading":
level = element["attrs"]["level"]
self._inc_current_numbering(level)
if len(element["children"]) > 0:
child = element["children"][0]
if child["type"] == "text":
child["raw"] = self.format_numbering() + " " + child["raw"]
new_source = self.md_renderer(md_ast, BlockState())
cell["source"] = new_source
return cell, resources
except Exception:
self.log.warning("Failed processing cell headings", exc_info=True)
return cell, resources
26 changes: 23 additions & 3 deletions tests/preprocessors/test_numbered_headings.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,31 @@
## 2.1 Sub-heading
some more content
### 2.1.1 Third heading
"""

MARKDOWN_3 = """
# HEADING
```
# this is not a heading
## this neither
```
"""

MARKDOWN_3_POST = """
# 3 HEADING
```
# this is not a heading
## this neither
```
"""


class TestNumberedHeadings(PreprocessorTestsBase):
def build_notebook(self):
Expand All @@ -61,6 +80,7 @@ def build_notebook(self):
nbformat.new_markdown_cell(source=MARKDOWN_1),
nbformat.new_code_cell(source="$ e $", execution_count=1),
nbformat.new_markdown_cell(source=MARKDOWN_2),
nbformat.new_markdown_cell(source=MARKDOWN_3),
]

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

def test_constructor(self):
"""Can a ClearOutputPreprocessor be constructed?"""
"""Can a NumberedHeadingsPreprocessor be constructed?"""
self.build_preprocessor()

def test_output(self):
Expand All @@ -81,6 +101,6 @@ def test_output(self):
res = self.build_resources()
preprocessor = self.build_preprocessor()
nb, res = preprocessor(nb, res)
print(nb.cells[1].source)
assert nb.cells[1].source.strip() == MARKDOWN_1_POST.strip()
assert nb.cells[3].source.strip() == MARKDOWN_2_POST.strip()
assert nb.cells[4].source.strip() == MARKDOWN_3_POST.strip()

0 comments on commit 232d78c

Please sign in to comment.