Skip to content
Permalink
Browse files

fix: Shift Markdown headings according to the current `heading_level`

Fixes #192
  • Loading branch information
oprypin authored and pawamoy committed Dec 16, 2020
1 parent af24bc2 commit 13f41aec5a95c82c1229baa4ac3caf4abb2add51
@@ -16,9 +16,12 @@
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Dict, Optional, Sequence
from xml.etree.ElementTree import Element # noqa: S405 (we choose to trust the XML input)

from jinja2 import Environment, FileSystemLoader
from markdown import Markdown
from markdown.extensions import Extension
from markdown.treeprocessors import Treeprocessor
from markupsafe import Markup
from pymdownx.highlight import Highlight

@@ -125,20 +128,23 @@ def do_any(seq: Sequence, attribute: str = None) -> bool:
return any(_[attribute] for _ in seq)


def do_convert_markdown(md: Markdown, text: str) -> Markup:
def do_convert_markdown(md: Markdown, text: str, heading_level: int) -> Markup:
"""
Render Markdown text; for use inside templates.
Arguments:
md: A `markdown.Markdown` instance.
text: The text to convert.
heading_level: The base heading level to start all Markdown headings from.
Returns:
An HTML string.
"""
md.treeprocessors["mkdocstrings_headings"].shift_by = heading_level
try:
return Markup(md.convert(text))
finally:
md.treeprocessors["mkdocstrings_headings"].shift_by = 0
md.reset()


@@ -215,7 +221,7 @@ def update_env(self, md: Markdown, config: dict) -> None:
of [mkdocstrings.plugin.MkdocstringsPlugin.on_config][] to see what's in this dictionary.
"""
# Re-instantiate md: see https://github.com/tomchristie/mkautodoc/issues/14
md = Markdown(extensions=config["mdx"], extension_configs=config["mdx_configs"])
md = Markdown(extensions=config["mdx"] + [ShiftHeadingsExtension()], extension_configs=config["mdx_configs"])

self.env.filters["convert_markdown"] = functools.partial(do_convert_markdown, md)

@@ -361,3 +367,37 @@ def teardown(self):
for handler in self._handlers.values():
handler.collector.teardown()
self._handlers.clear()


class _HeadingShiftingTreeprocessor(Treeprocessor):
def __init__(self, md, shift_by: int):
super().__init__(md)
self.shift_by = shift_by

def run(self, root: Element):
if not self.shift_by:
return
for el in root.iter():
match = re.fullmatch(r"([Hh])([1-6])", el.tag)
if match:
level = int(match[2]) + self.shift_by
level = max(1, min(level, 6))
el.tag = f"{match[1]}{level}"


class ShiftHeadingsExtension(Extension):
"""Shift levels of all Markdown headings according to the configured base level."""

treeprocessor_priority = 12

def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent method's name)
"""
Register the extension, with a treeprocessor under the name 'mkdocstrings_headings'.
Arguments:
md: A `markdown.Markdown` instance.
"""
md.registerExtension(self)
md.treeprocessors.register(
_HeadingShiftingTreeprocessor(md, 0), "mkdocstrings_headings", self.treeprocessor_priority
)
@@ -13,7 +13,7 @@
<tr>
<td><code>{{ attribute.name }}</code></td>
<td><code>{{ attribute.annotation }}</code></td>
<td>{{ attribute.description|convert_markdown }}</td>
<td>{{ attribute.description|convert_markdown(heading_level) }}</td>
</tr>
{% endfor %}
</tbody>
@@ -2,7 +2,7 @@
{% if docstring_sections %}
{% for section in docstring_sections %}
{% if section.type == "markdown" %}
{{ section.value|convert_markdown }}
{{ section.value|convert_markdown(heading_level) }}
{% elif section.type == "attributes" %}
{% with attributes = section.value %}
{% include "attributes.html" with context %}
@@ -2,7 +2,7 @@
<p><strong>Examples:</strong></p>
{% for section_type, sub_section in examples %}
{% if section_type == "markdown" %}
{{ sub_section|convert_markdown }}
{{ sub_section|convert_markdown(heading_level) }}
{% elif section_type == "examples" %}
{{ sub_section|highlight(language="python", line_nums=False) }}
{% endif %}
@@ -11,7 +11,7 @@
{% for exception in exceptions %}
<tr>
<td><code>{{ exception.annotation }}</code></td>
<td>{{ exception.description|convert_markdown }}</td>
<td>{{ exception.description|convert_markdown(heading_level) }}</td>
</tr>
{% endfor %}
</tbody>
@@ -14,7 +14,7 @@
<tr>
<td><code>{{ parameter.name }}</code></td>
<td><code>{{ parameter.annotation }}</code></td>
<td>{{ parameter.description|convert_markdown }}</td>
<td>{{ parameter.description|convert_markdown(heading_level) }}</td>
<td>{% if parameter.default %}<code>{{ parameter.default }}</code>{% else %}<em>required</em>{% endif %}</td>
</tr>
{% endfor %}
@@ -10,7 +10,7 @@
<tbody>
<tr>
<td><code>{{ return.annotation }}</code></td>
<td>{{ return.description|convert_markdown }}</td>
<td>{{ return.description|convert_markdown(heading_level) }}</td>
</tr>
</tbody>
</table>
@@ -10,7 +10,7 @@
<td class="field-body">
<ul class="first simple">
{% for exception in exceptions %}
<li>{{ ("`" + exception.annotation + "` – " + exception.description)|convert_markdown }}</li>
<li>{{ ("`" + exception.annotation + "` – " + exception.description)|convert_markdown(heading_level) }}</li>
{% endfor %}
</ul>
</td>
@@ -10,7 +10,7 @@
<td class="field-body">
<ul class="first simple">
{% for parameter in parameters %}
<li>{{ ("**" + parameter.name + "** (`" + parameter.annotation + "`) – " + parameter.description)|convert_markdown }}</li>
<li>{{ ("**" + parameter.name + "** (`" + parameter.annotation + "`) – " + parameter.description)|convert_markdown(heading_level) }}</li>
{% endfor %}
</ul>
</td>
@@ -9,7 +9,7 @@
<th class="field-name">Returns:</th>
<td class="field-body">
<ul class="first simple">
<li>{{ ("`" + return.annotation + "` – " + return.description)|convert_markdown }}</li>
<li>{{ ("`" + return.annotation + "` – " + return.description)|convert_markdown(heading_level) }}</li>
</ul>
</td>
</tr>
@@ -0,0 +1,8 @@
"""
Foo
===
### Bar
###### Baz
"""
@@ -41,14 +41,26 @@ def test_multiple_footnotes():
::: tests.fixtures.footnotes.func_c
[^aaa]: Top footnote
"""
)
""",
),
)
assert output.count("Footnote A") == 1
assert output.count("Footnote B") == 1
assert output.count("Top footnote") == 1


def test_markdown_heading_level():
"""Assert that Markdown headings' level doesn't exceed heading_level."""
config = dict(_DEFAULT_CONFIG)
config["mdx"].append(MkdocstringsExtension(config, Handlers(config)))
md = Markdown(extensions=config["mdx"])

output = md.convert("::: tests.fixtures.headings\n rendering:\n show_root_heading: true")
assert "<h3>Foo</h3>" in output
assert "<h5>Bar</h5>" in output
assert "<h6>Baz</h6>" in output


def test_reference_inside_autodoc():
"""Assert cross-reference Markdown extension works correctly."""
config = dict(_DEFAULT_CONFIG)

0 comments on commit 13f41ae

Please sign in to comment.