Skip to content

Commit

Permalink
Merge pull request #85 from daizutabi/80-add-toc-in-source-page
Browse files Browse the repository at this point in the history
Add toc in source page
  • Loading branch information
daizutabi authored Feb 11, 2024
2 parents 223b230 + 2f3c3ee commit aff5bd4
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 295 deletions.
186 changes: 119 additions & 67 deletions src/mkapi/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@

import re
from dataclasses import dataclass, field
from functools import partial
from typing import TYPE_CHECKING

import mkapi.markdown
import mkapi.renderers
from mkapi.globals import resolve_with_attribute
from mkapi.importlib import get_object
from mkapi.objects import Module, is_empty, iter_objects, iter_objects_with_depth
from mkapi.objects import Module, is_empty, iter_objects_with_depth
from mkapi.renderers import get_object_filter_for_source
from mkapi.utils import is_module_cache_dirty, split_filters

Expand All @@ -31,156 +30,208 @@ class Page:
markdown: str = field(default="", init=False)

def __post_init__(self) -> None:
# Delete in MkDocs v1.6. Switch to virtual files
if not self.path.exists():
if not self.path.parent.exists():
self.path.parent.mkdir(parents=True)
with self.path.open("w") as file:
file.write("")
file.write("") # Dummy content

self.set_markdown()
if self.kind in ["object", "source"]:
self.set_markdown()

def set_markdown(self) -> None:
"""Set markdown."""
if self.kind == "object":
self.source = create_object_markdown(self.name, self.path, self.filters)
elif self.kind == "source":
self.source = create_source_markdown(self.name, self.path, self.filters)
self.source = create_markdown(
self.name,
self.path,
self.filters,
is_source=self.kind == "source",
)

def convert_markdown(self, source: str, anchor: str) -> str:
"""Return converted markdown."""
if self.kind in ["object", "source"]: # noqa: SIM102
if self.kind in ["object", "source"]:
if self.markdown and not is_module_cache_dirty(self.name):
return self.markdown
if self.kind == "markdown":

elif self.kind == "markdown":
self.source = source

self.markdown = convert_markdown(self.source, self.path, anchor)
self.markdown = convert_markdown(
self.source,
self.path,
anchor,
is_source=self.kind == "source",
)
return self.markdown


object_paths: dict[str, Path] = {}
source_paths: dict[str, Path] = {}


def create_object_markdown(
def create_markdown(
name: str,
path: Path,
filters: list[str],
predicate: Callable[[str], bool] | None = None,
*,
is_source: bool = False,
) -> str:
"""Create object page for an object."""
if not (obj := get_object(name)):
return f"!!! failure\n\n {name!r} not found."
if not (obj := get_object(name)) or not isinstance(obj, Module):
return f"!!! failure\n\n module {name!r} not found.\n"

filters_str = "|" + "|".join(filters) if filters else ""
object_filter = ""

paths = source_paths if is_source else object_paths

markdowns = []
for child, depth in iter_objects_with_depth(obj, 2, member_only=True):
if is_empty(child):
continue
if predicate and not predicate(child.fullname):
continue
object_paths.setdefault(child.fullname, path)
paths.setdefault(child.fullname, path)

if is_source:
object_filter = get_object_filter_for_source(child, obj)
object_filter = f"|{object_filter}" if object_filter else ""

heading = "#" * (depth + 1)
markdown = f"{heading} ::: {child.fullname}{filters_str}\n"
markdown = f"{heading} ::: {child.fullname}{filters_str}{object_filter}\n"
markdowns.append(markdown)

return "\n".join(markdowns)


source_paths: dict[str, Path] = {}


def create_source_markdown(
name: str,
def convert_markdown(
markdown: str,
path: Path,
filters: list[str],
predicate: Callable[[str], bool] | None = None,
anchor: str,
*,
is_source: bool = False,
) -> str:
"""Create source page for a module."""
if not (obj := get_object(name)) or not isinstance(obj, Module):
return f"!!! failure\n\n module {name!r} not found.\n"

object_filters = []
for child in iter_objects(obj, 2):
if predicate and not predicate(child.fullname):
continue
if object_filter := get_object_filter_for_source(child, obj):
object_filters.append(object_filter)
source_paths.setdefault(child.fullname, path)

filters_str = "|" + "|".join([*filters, "source", *object_filters])
return f"# ::: {name}{filters_str}\n"
"""Return converted markdown."""
if is_source:
markdown = _replace_source(markdown)
else:
markdown = mkapi.markdown.sub(OBJECT_PATTERN, _replace_object, markdown)

paths = source_paths if is_source else object_paths

def convert_markdown(markdown: str, path: Path, anchor: str) -> str:
"""Return converted markdown."""
markdown = mkapi.markdown.sub(OBJECT_PATTERN, _replace_object, markdown)
def replace_link(match: re.Match) -> str:
return _replace_link(match, path.parent, paths, anchor)

replace_link = partial(_replace_link, directory=path.parent, anchor=anchor)
return mkapi.markdown.sub(LINK_PATTERN, replace_link, markdown)


OBJECT_PATTERN = re.compile(r"^(?P<heading>#*) *?::: (?P<name>.+?)$", re.M)


def _replace_object(match: re.Match) -> str:
def _get_level_name_filters(match: re.Match) -> tuple[str, int, list[str]]:
heading, name = match.group("heading"), match.group("name")
level = len(heading)
name, filters = split_filters(name)
return name, level, filters


def _replace_object(match: re.Match) -> str:
name, level, filters = _get_level_name_filters(match)

if not (obj := get_object(name)):
return f"!!! failure\n\n {name!r} not found."

return mkapi.renderers.render(obj, level, filters)
return mkapi.renderers.render(obj, level, filters, is_source=False)


def _replace_source(markdown: str) -> str:
module = None
filters = []
headings = []

for match in re.finditer(OBJECT_PATTERN, markdown):
name, level, object_filter = _get_level_name_filters(match)
if level == 1 and (obj := get_object(name)) and isinstance(obj, Module):
module = obj

# Move to renderer.py
if level >= 2:
# 'markdown="1"' for toc.
attr = f'class="mkapi-dummy-heading" id="{name}" markdown="1"'
name_ = name.replace("_", "\\_")
heading = f"<h{level} {attr}>{name_}</h{level}>"
headings.append(heading)
filters.extend(object_filter)

if not module:
return "!!! failure\n\n module not found."

source = mkapi.renderers.render(module, 1, filters, is_source=True)
return "\n".join([source, *headings])


LINK_PATTERN = re.compile(r"(?<!`)\[([^[\]\s]+?)\]\[([^[\]\s]+?)\]")


def _replace_link(match: re.Match, directory: Path, anchor: str = "source") -> str:
asname, fullname = match.groups()
def _replace_link(
match: re.Match,
directory: Path,
paths: dict[str, Path],
anchor: str,
) -> str:
name, fullname = match.groups()
fullname, filters = split_filters(fullname)

if fullname.startswith("__mkapi__.__source__."):
name = f"[{anchor}]"
return _replace_link_from_source(name, fullname[21:], directory)
paths = source_paths
return _replace_link_from_paths(name, fullname[21:], directory, paths) or ""

if fullname.startswith("__mkapi__.__object__."):
name = f"[{anchor}]"
paths = object_paths
return _replace_link_from_paths(name, fullname[21:], directory, paths) or ""

if "source" in filters:
return _replace_link_from_source(asname, fullname, directory) or asname
paths = source_paths

return _replace_link_from_object(asname, fullname, directory) or match.group()
return _replace_link_from_paths(name, fullname, directory, paths) or match.group()


def _replace_link_from_object(name: str, fullname: str, directory: Path) -> str:
if fullname.startswith("__mkapi__."):
from_mkapi = True
fullname = fullname[10:]
else:
from_mkapi = False

if fullname_ := resolve_with_attribute(fullname):
fullname = fullname_

if object_path := object_paths.get(fullname):
uri = object_path.relative_to(directory, walk_up=True).as_posix()
def _replace_link_from_paths(
name: str,
fullname: str,
directory: Path,
paths: dict[str, Path],
) -> str | None:
fullname, from_mkapi = _resolve_fullname(fullname)

if path := paths.get(fullname):
uri = path.relative_to(directory, walk_up=True).as_posix()
return f'[{name}]({uri}#{fullname} "{fullname}")'

if from_mkapi:
return f'<span class="mkapi-tooltip" title="{fullname}">{name}</span>'

return ""
return None


def _replace_link_from_source(name: str, fullname: str, directory: Path) -> str:
if source_path := source_paths.get(fullname):
uri = source_path.relative_to(directory, walk_up=True).as_posix()
return f'[{name}]({uri}#{fullname} "{fullname}")'
def _resolve_fullname(fullname: str) -> tuple[str, bool]:
if fullname.startswith("__mkapi__."):
from_mkapi = True
fullname = fullname[10:]
else:
from_mkapi = False

return ""
fullname = resolve_with_attribute(fullname) or fullname
return fullname, from_mkapi


SOURCE_LINK_PATTERN = re.compile(r"(<span[^<]+?)## __mkapi__\.(\S+?)(</span>)")
HEADING_PATTERN = re.compile(r"<h\d.+?mkapi-dummy-heading.+?</h\d>\n?")


def convert_source(html: str, path: Path, anchor: str) -> str:
Expand All @@ -201,4 +252,5 @@ def replace(match: re.Match) -> str:
return link
return f"{open_tag}{close_tag}{link}"

return SOURCE_LINK_PATTERN.sub(replace, html)
html = SOURCE_LINK_PATTERN.sub(replace, html)
return HEADING_PATTERN.sub("", html)
7 changes: 5 additions & 2 deletions src/mkapi/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ def on_page_markdown(self, markdown: str, page: MkDocsPage, **kwargs) -> str:
"""Convert Markdown source to intermediate version."""
uri = page.file.src_uri
page_ = self.pages[uri]
anchor = self.config.src_anchor
if page_.kind == "source":
anchor = self.config.docs_anchor
else:
anchor = self.config.src_anchor

try:
return page_.convert_markdown(markdown, anchor)
Expand All @@ -132,7 +135,7 @@ def on_page_content(
uri = page.file.src_uri
page_ = self.pages[uri]

if page_.kind == "object":
if page_.kind in ["object", "source"]:
_replace_toc(page.toc, self.toc_title)
if page_.kind == "source":
html = convert_source(html, page_.path, self.config.docs_anchor)
Expand Down
4 changes: 3 additions & 1 deletion src/mkapi/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def render(
obj: Module | Class | Function | Attribute,
level: int,
filters: list[str],
*,
is_source: bool = False,
) -> str:
"""Return a rendered Markdown."""
heading = f"h{level}" if level else "p"
Expand All @@ -53,7 +55,7 @@ def render(
"doc": obj.doc,
"filters": filters,
}
if isinstance(obj, Module) and "source" in filters:
if isinstance(obj, Module) and is_source:
return _render_source(obj, context, filters)
return _render_object(obj, context)

Expand Down
4 changes: 3 additions & 1 deletion src/mkapi/templates/source.jinja2
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<div class="mkapi-container" markdown="1">
<div class="mkapi-content" markdown="1">

{% if "bare" not in filters %}
{% if heading %}<{{ heading }} id="{{ obj.fullname }}" class="mkapi-heading" markdown="1">{% endif %}
<span class="mkapi-heading-name">{{ fullname|safe }}</span>
<span class="mkapi-docs-link">[docs][__mkapi__.__object__.{{ obj.fullname }}]</span>
{%- if heading %}</{{ heading }}>{% endif %}

<p class="mkapi-object" markdown="1">
Expand Down
5 changes: 0 additions & 5 deletions tests/test_importlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,3 @@ def test_iter_dataclass_parameters():
assert p[2].name == "text"
assert p[3].name == "items"
assert p[4].name == "kind"


def test_a(): # TODO: delete
module = load_module("schemdraw.elements.cables")
print(module)
1 change: 0 additions & 1 deletion tests/test_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,5 @@ def test_iter_merged_items_():

def test_create_admonition():
a = create_admonition("See Also", "a: b\nc: d")
print(a.text.str)
x = '!!! info "See Also"\n * [__mkapi__.a][]: b\n * [__mkapi__.c][]: d'
assert a.text.str == x
1 change: 0 additions & 1 deletion tests/test_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,6 @@ def rel(m: re.Match):
return f"xxx{name}xxx"

m = sub(pattern, rel, src)
print(m)
assert m.startswith("```\n# ::: a\n```\nxxx::: bxxx\nxxx::: cxxx\n```{.python")
assert m.endswith("output}\n::: d\n```\n\nxxx::: exxx\nf")

Expand Down
1 change: 0 additions & 1 deletion tests/test_nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,5 @@ def page_title(name: str, depth) -> str:

nav = yaml.safe_load(src)
update(nav, create_page, page_title=page_title)
print(nav)
assert "MKAPI.OBJECTS.0" in nav[1]
assert nav[1]["MKAPI.OBJECTS.0"] == "api1/mkapi.objects.f1.md"
Loading

0 comments on commit aff5bd4

Please sign in to comment.