Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add NumberedHeadingsPreprocessor #2187

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1634,6 +1634,7 @@ raw template
{%- endblock in_prompt -%}
"""


exporter_attr = AttrExporter()
output_attr, _ = exporter_attr.from_notebook_node(nb)
assert "raw template" in output_attr
Expand Down
2 changes: 2 additions & 0 deletions docs/source/api/preprocessors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Converting text

.. autoclass:: HighlightMagicsPreprocessor

.. autoclass:: NumberedHeadingsPreprocessor

Metadata and header control
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 2 additions & 0 deletions nbconvert/preprocessors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .extractoutput import ExtractOutputPreprocessor
from .highlightmagics import HighlightMagicsPreprocessor
from .latex import LatexPreprocessor
from .numbered_headings import NumberedHeadingsPreprocessor
from .regexremove import RegexRemovePreprocessor
from .svg2pdf import SVG2PDFPreprocessor
from .tagremove import TagRemovePreprocessor
Expand All @@ -30,6 +31,7 @@
"ExtractOutputPreprocessor",
"HighlightMagicsPreprocessor",
"LatexPreprocessor",
"NumberedHeadingsPreprocessor",
"RegexRemovePreprocessor",
"SVG2PDFPreprocessor",
"TagRemovePreprocessor",
Expand Down
51 changes: 51 additions & 0 deletions nbconvert/preprocessors/numbered_headings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
Preprocessor that transforms markdown cells: Insert numbering in from of heading
"""

import re

from nbconvert.preprocessors.base import Preprocessor


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

def __init__(self, *args, **kwargs):
"""Init"""
super().__init__(*args, **kwargs)
self.current_numbering = [0]

def format_numbering(self):
"""Return a string representation of the current numbering"""
return ".".join(str(n) for n in self.current_numbering)

def _inc_current_numbering(self, level):
"""Increase internal counter keeping track of numberings"""
if level > len(self.current_numbering):
self.current_numbering = self.current_numbering + [0] * (
level - len(self.current_numbering)
)
elif level < len(self.current_numbering):
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if I we have a code in markdown, e.g.:

This is how you create a heading in Markdown
```markdown
# heading
```

It feels like a more robust solution would be to use mistune API for parsing, see https://mistune.lepture.com/en/latest/api.html (markdown_mistune is already in use here, but it should fail nicely).

It is also possible to instruct pandoc to run with --number-sections (see https://www.youtube.com/watch?v=gDV63UMJR_U and https://pandoc.org/chunkedhtml-demo/8.3-headings.html and https://pandoc.org/MANUAL.html) but pandoc is neither the only nor even default markdown parser.

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
86 changes: 86 additions & 0 deletions tests/preprocessors/test_numbered_headings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
Module with tests for the Numbered Headings preprocessor.
"""

from nbformat import v4 as nbformat

from nbconvert.preprocessors.numbered_headings import NumberedHeadingsPreprocessor

from .base import PreprocessorTestsBase

MARKDOWN_1 = """
# Heading 1

## Sub-heading

some content
"""

MARKDOWN_1_POST = """
# 1 Heading 1

## 1.1 Sub-heading

some content
"""


MARKDOWN_2 = """

## Second sub-heading

# Another main heading

## Sub-heading


some more content

### Third heading
"""

MARKDOWN_2_POST = """

## 1.2 Second sub-heading

# 2 Another main heading

## 2.1 Sub-heading


some more content

### 2.1.1 Third heading
"""


class TestNumberedHeadings(PreprocessorTestsBase):
def build_notebook(self):
cells = [
nbformat.new_code_cell(source="$ e $", execution_count=1),
nbformat.new_markdown_cell(source=MARKDOWN_1),
nbformat.new_code_cell(source="$ e $", execution_count=1),
nbformat.new_markdown_cell(source=MARKDOWN_2),
]

return nbformat.new_notebook(cells=cells)

def build_preprocessor(self):
"""Make an instance of a preprocessor"""
preprocessor = NumberedHeadingsPreprocessor()
preprocessor.enabled = True
return preprocessor

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

def test_output(self):
"""Test the output of the NumberedHeadingsPreprocessor"""
nb = self.build_notebook()
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()
Loading