Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c0f597f
Add a function to extract the titles and levels of headings from the …
HaudinFlorence Sep 16, 2024
9a84d24
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 16, 2024
2f1ab0b
Updates jinja templates.
HaudinFlorence Sep 16, 2024
5c22927
Update jinja templates and add style to the table of contents.
HaudinFlorence Sep 17, 2024
1c7427a
Add links to be able to go to the relevant sections when clicking on …
HaudinFlorence Sep 17, 2024
c86d451
Place the css inline in index.html.j2.
HaudinFlorence Sep 17, 2024
c23d1eb
Remove index.css from the git index.
HaudinFlorence Sep 17, 2024
8780c56
Add the include_tableofcontents traitlet and update the jinja templat…
HaudinFlorence Sep 18, 2024
3bdca36
Add the logics to include a table of contents for the latex exporter.
HaudinFlorence Sep 19, 2024
b318e57
Update nbconvertapp with removing toc from alias and with adding toc …
HaudinFlorence Oct 4, 2024
7d4b572
Try to fix failing CI tests.
HaudinFlorence Oct 4, 2024
28913c9
Try to fix pre-commit test.
HaudinFlorence Oct 7, 2024
a804318
Try to fix failing linting test.
HaudinFlorence Oct 7, 2024
c70f4fc
Update markdown.mistune.py and add docstrings to get 100% as a score …
HaudinFlorence Oct 7, 2024
f175d45
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 7, 2024
3b4bc4d
Apply suggestions from code review
HaudinFlorence Oct 7, 2024
36e8889
Apply other suggestions from code review.
HaudinFlorence Oct 7, 2024
efc0133
Fix linting issues.
HaudinFlorence Oct 7, 2024
7eeac98
Try to fix failing linting test.
HaudinFlorence Oct 8, 2024
9554b7e
Try to remove None check condition.
HaudinFlorence Oct 8, 2024
df17ef2
Restore None check condition in HTML exporter instead of _init_resources
HaudinFlorence Oct 9, 2024
e6f9d9e
Change method extract_titles_from_markdown_input to extract_titles_fr…
HaudinFlorence Oct 14, 2024
c03c701
Update extract_titles_from_notebook_node to fix some parsing issues.
HaudinFlorence Oct 25, 2024
093b120
Try to fix failing tests.
HaudinFlorence Oct 25, 2024
338f071
Remove condition on headers of level 1.
HaudinFlorence Nov 4, 2024
5803af7
Update extract_titles_from_notebook_node.
HaudinFlorence Nov 4, 2024
d9ffc0c
Change the logics to get the headers using parsing of the markdown ce…
HaudinFlorence Nov 6, 2024
3211755
Add sysmetically an id to the headers to be sure to have a defined href.
HaudinFlorence Nov 7, 2024
3b45be0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 7, 2024
76c6273
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 7, 2025
d5eebbc
Restore using Plugin instead of MarkdownPlugin in markdown_mistune.py.
HaudinFlorence May 7, 2025
a132b7c
Try to fix failing linting tests.
HaudinFlorence May 7, 2025
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
8 changes: 7 additions & 1 deletion nbconvert/exporters/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
from nbformat import NotebookNode

from nbconvert.filters.highlight import Highlight2HTML
from nbconvert.filters.markdown_mistune import IPythonRenderer, MarkdownWithMath
from nbconvert.filters.markdown_mistune import (
IPythonRenderer,
MarkdownWithMath,
extract_titles_from_notebook_node,
)
from nbconvert.filters.widgetsdatatypefilter import WidgetsDataTypeFilter
from nbconvert.utils.iso639_1 import iso639_1

Expand Down Expand Up @@ -355,6 +359,7 @@ def resources_include_url(name):
return markupsafe.Markup(src)

resources = super()._init_resources(resources)

resources["theme"] = self.theme
resources["include_css"] = resources_include_css
resources["include_lab_theme"] = resources_include_lab_theme
Expand All @@ -370,4 +375,5 @@ def resources_include_url(name):
resources["should_sanitize_html"] = self.sanitize_html
resources["language_code"] = self.language_code
resources["should_not_encode_svg"] = self.skip_svg_encoding
resources["extract_titles_from_nodebook_node"] = extract_titles_from_notebook_node
return resources
4 changes: 4 additions & 0 deletions nbconvert/exporters/templateexporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ def default_config(self):
enable_async = Bool(False, help="Enable Jinja async template execution").tag(
affects_environment=True
)
include_tableofcontents = Bool(
False, allow_none=True, help="Enable to include a table of contents"
).tag(config=True, affects_template=True)

_last_template_file = ""
_raw_template_key = "<memory>"
Expand Down Expand Up @@ -684,4 +687,5 @@ def get_prefix_root_dirs(self):
def _init_resources(self, resources):
resources = super()._init_resources(resources)
resources["deprecated"] = deprecated
resources["include_tableofcontents"] = self.include_tableofcontents
return resources
47 changes: 40 additions & 7 deletions nbconvert/filters/markdown_mistune.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Iterable, Match, Optional, Protocol, Tuple

import bs4
import mistune
from nbformat import NotebookNode
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexer import Lexer
Expand Down Expand Up @@ -62,7 +64,7 @@ def __call__(self, markdown: "Markdown") -> None:
MISTUNE_V3_ATX = False

def import_plugin(name: str) -> "Plugin": # type: ignore[misc]
"""Simple implementation of Mistune V3's import_plugin for V2."""
"""Simple implementation of Mistune V3"s import_plugin for V2."""
return PLUGINS[name] # type: ignore[no-any-return]


Expand All @@ -71,7 +73,7 @@ class InvalidNotebook(Exception):


def _dotall(pattern: str) -> str:
"""Makes the '.' special character match any character inside the pattern, including a newline.
"""Makes the "." special character match any character inside the pattern, including a newline.

This is implemented with the inline flag `(?s:...)` and is equivalent to using `re.DOTALL`.
It is useful for LaTeX environments, where line breaks may be present.
Expand All @@ -86,7 +88,7 @@ class MathBlockParser(BlockParser):
order to avoid other block level rules splitting math sections apart.

It works by matching each multiline math environment as a single paragraph,
so that other rules don't think each section is its own paragraph. Inline
so that other rules don"t think each section is its own paragraph. Inline
is ignored here.
"""

Expand Down Expand Up @@ -214,7 +216,7 @@ class MathBlockParser(BlockParser): # type: ignore[no-redef]
re.DOTALL,
)

# Regex for header that doesn't require space after '#'
# Regex for header that doesn"t require space after "#"
AXT_HEADING = re.compile(r" {0,3}(#{1,6})(?!#+)(?: *\n+|([^\n]*?)(?:\n+|\s+?#+\s*\n+))")

# Multiline math must be searched before other rules
Expand Down Expand Up @@ -255,7 +257,7 @@ class MathInlineParser(InlineParser): # type: ignore[no-redef]

def parse_block_math_tex(self, m: Match[str], state: Any) -> Tuple[str, str]:
"""Parse block text math."""
# sometimes the Scanner keeps the final '$$', so we use the
# sometimes the Scanner keeps the final "$$", so we use the
# full matched string and remove the math markers
text = m.group(0)[2:-2]
return "block_math", text
Expand Down Expand Up @@ -450,7 +452,7 @@ def _html_embed_images(self, html: str) -> str:
parsed_html = bs4.BeautifulSoup(html, features="html.parser")
imgs: bs4.ResultSet[bs4.Tag] = parsed_html.find_all("img")

# Replace img tags's sources by base64 dataurls
# Replace img tags"s sources by base64 dataurls
for img in imgs:
src = img.attrs.get("src")
if src is None:
Expand All @@ -476,7 +478,7 @@ class MarkdownWithMath(Markdown):
"def_list",
)

def __init__(
def nb__(
self,
renderer: HTMLRenderer,
block: Optional[BlockParser] = None,
Expand Down Expand Up @@ -504,3 +506,34 @@ def render(self, source: str) -> str:
def markdown2html_mistune(source: str) -> str:
"""Convert a markdown string to HTML using mistune"""
return MarkdownWithMath(renderer=IPythonRenderer(escape=False)).render(source)


def extract_titles_from_notebook_node(nb: NotebookNode):
"""Create a Markdown parser with the HeadingExtractor renderer to collect all the headings of a notebook
The input argument is the notebooknode from which a single string with all the markdown content concatenated
The output is an array containing information about the headings such as their level, their text content, an identifier and a href that can be used in case of html converter.s"""

cells_html_collection = ""
for cell in nb.cells:
if cell.cell_type == "markdown":
markdown_source = cell.source
html_source = mistune.html(markdown_source) # convert all the markdown sources to html
if isinstance(html_source, str):
cells_html_collection += html_source + "\n"
elif isinstance(html_source, list):
rendered = "\n".join(str(item) for item in html_source)
cells_html_collection += rendered + "\n"

titles_array = []
html_collection = bs4.BeautifulSoup(cells_html_collection, "html.parser")
headings = html_collection.select("h1, h2, h3, h4, h5, h6")

# Iterate on all headings to get the necessary information on the various titles
for heading in headings:
text = heading.get_text().lstrip().rstrip()
level = int(heading.name[1])
header_id = text.replace(" ", "-")
heading["id"] = header_id
href = "#" + header_id
titles_array.append([str(heading), level, href])
return titles_array
5 changes: 5 additions & 0 deletions nbconvert/nbconvertapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ def validate(self, obj, value):
},
"""Whether the HTML in Markdown cells and cell outputs should be sanitized..""",
),
"toc": (
{"TemplateExporter": {"include_tableofcontents": True}},
"Generate a table of contents in the output (only compatible with HTML and Latex exporters)",
),
}
)

Expand Down Expand Up @@ -675,5 +679,6 @@ def _default_export_format(self):
# Main entry point
# -----------------------------------------------------------------------------


main = launch_new_instance = NbConvertApp.launch_instance
dejavu_main = DejavuApp.launch_instance
1 change: 1 addition & 0 deletions share/templates/lab/base.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{% from 'celltags.j2' import celltags %}
{% from 'cell_id_anchor.j2' import cell_id_anchor %}


{% block codecell %}
{%- if not cell.outputs -%}
{%- set no_output_class="jp-mod-noOutputs" -%}
Expand Down
78 changes: 78 additions & 0 deletions share/templates/lab/index.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,56 @@ a.anchor-link {
display: block;
}
}
/* Table of Contents for the html exporter */
.jp-RenderedHTMLTOC-Title {
font-family: var(--jp-content-font-family);
font-size: 14px;
margin: 16px 0;
padding-left: 64px;
font-weight: bold;
}

.jp-RenderedHTMLTOC-Item-h1 {
font-family: var(--jp-content-font-family);
font-size: 14px;
margin: 0;
padding-left: 88px;
}

.jp-RenderedHTMLTOC-Item-h2 {
font-family: var(--jp-content-font-family);
font-size: 12px;
margin: 4px;
padding-left: 112px;
}

.jp-RenderedHTMLTOC-Item-h3 {
font-family: var(--jp-content-font-family);
font-size:10px;
margin: 4px;
padding-left: 136px;
}

.jp-RenderedHTMLTOC-Item-h4 {
font-family: var(--jp-content-font-family);
font-size: 8px;
margin: 4px;
padding-left: 160px;
}

.jp-RenderedHTMLTOC-Item-h5 {
font-family: var(--jp-content-font-family);
font-size: 7px;
margin: 4px;
padding-left: 184px;
}

.jp-RenderedHTMLTOC-Item-h6 {
font-family: var(--jp-content-font-family);
font-size: 6px;
margin: 2px;
padding-left: 208px;
}
</style>

{% endblock notebook_css %}
Expand All @@ -126,6 +176,34 @@ a.anchor-link {
<body class="jp-Notebook" data-jp-theme-light="true" data-jp-theme-name="JupyterLab Light">
{% endif %}
<main>
{%- block tableofcontents -%}
{%- if resources.include_tableofcontents -%}
{%- set tableofcontents= resources.extract_titles_from_nodebook_node(nb) -%}
<div class="jp-RenderedHTMLTOC-Title">Table of contents</div>
{%- for item in tableofcontents -%}
{%- set (header, level, href) = item -%}
<div class="
{%- if level==1 -%}
jp-RenderedHTMLCommon jp-RenderedHTMLTOC-Item-h1
{%- elif level==2 -%}
jp-RenderedHTMLCommon jp-RenderedHTMLTOC-Item-h2
{%- elif level==3 -%}
jp-RenderedHTMLCommon jp-RenderedHTMLTOC-Item-h3
{%- elif level==4 -%}
jp-RenderedHTMLCommon jp-RenderedHTMLTOC-Item-h4
{%- elif level==5 -%}
jp-RenderedHTMLCommon jp-RenderedHTMLTOC-Item-h5
{%- elif level==6 -%}
jp-RenderedHTMLCommon jp-RenderedHTMLTOC-Item-h6
{%- endif -%}"
>
<a href={{href}}>
{{header | safe}}
</a>
</div>
{%- endfor -%}
{%- endif -%}
{% endblock tableofcontents %}
{%- endblock body_header -%}

{% block body_footer %}
Expand Down
6 changes: 6 additions & 0 deletions share/templates/latex/base.tex.j2
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,17 @@ override this.-=))
((* endblock header *))

((* block body *))

\begin{document}
((* block predoc *))
((* block maketitle *))\maketitle((* endblock maketitle *))
((* block abstract *))((* endblock abstract *))
((* endblock predoc *))
((* block tableofcontents *))
((* if resources.include_tableofcontents *))
\tableofcontents
((* endif *))
((* endblock tableofcontents *))

((( super() )))

Expand Down
Loading
Loading