diff --git a/.changelog/_unreleased.toml b/.changelog/_unreleased.toml index 16a7b2dd..e22041d9 100644 --- a/.changelog/_unreleased.toml +++ b/.changelog/_unreleased.toml @@ -4,3 +4,4 @@ type = "feature" description = "Add a pre-commit hook for pydoc-markdown." author = "@RomainTT" pr = "https://github.com/NiklasRosenstein/pydoc-markdown/pull/298" + diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 04f865c4..1a68f50b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -16,9 +16,11 @@ If you want to talk about a potential contribution before investing any time, pl ## Changelog entries -Pydoc-Markdown uses [Slam][] to manage changelogs. You should use the Slam CLI to add a new changelog entry, otherwise +Pydoc-Markdown uses [Slap][] to manage changelogs. You should use the Slap CLI to add a new changelog entry, otherwise you need to manually generate a UUID-4. $ slap changelog add -t -d [--issue ] After you create the pull request, GitHub Actions will take care of injecting the PR URL into the changelog entry. + +For a full list of accepted changelog types see [here](https://niklasrosenstein.github.io/slap/commands/changelog/) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index bd0c9a2b..c5c64396 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ keywords = ["documentation", "docs", "generator", "markdown", "pydoc"] Homepage = "https://github.com/NiklasRosenstein/pydoc-markdown" [tool.poetry.dependencies] -python = "^3.7" +python = "^3.8" click = ">=7.1,<9.0" "databind.core" = "^4.4.0" "databind.json" = "^4.4.0" @@ -83,6 +83,7 @@ hugo = "pydoc_markdown.contrib.renderers.hugo:HugoRenderer" markdown = "pydoc_markdown.contrib.renderers.markdown:MarkdownRenderer" mkdocs = "pydoc_markdown.contrib.renderers.mkdocs:MkdocsRenderer" docusaurus = "pydoc_markdown.contrib.renderers.docusaurus:DocusaurusRenderer" +nextra = "pydoc_markdown.contrib.renderers.nextra:NextraRenderer" jinja2 = "pydoc_markdown.contrib.renderers.jinja2:Jinja2Renderer" [tool.poetry.plugins."pydoc_markdown.interfaces.SourceLinker"] diff --git a/src/pydoc_markdown/contrib/renderers/docusaurus.py b/src/pydoc_markdown/contrib/renderers/docusaurus.py index e70a03e4..cf6d854d 100644 --- a/src/pydoc_markdown/contrib/renderers/docusaurus.py +++ b/src/pydoc_markdown/contrib/renderers/docusaurus.py @@ -122,7 +122,6 @@ def _render_side_bar_config(self, module_tree: t.Dict[t.Text, t.Any]) -> None: "label": self.sidebar_top_level_label, } self._build_sidebar_tree(sidebar, module_tree) - if sidebar.get("items"): if self.sidebar_top_level_module_label: sidebar["items"][0]["label"] = self.sidebar_top_level_module_label diff --git a/src/pydoc_markdown/contrib/renderers/nextra.py b/src/pydoc_markdown/contrib/renderers/nextra.py new file mode 100644 index 00000000..05770997 --- /dev/null +++ b/src/pydoc_markdown/contrib/renderers/nextra.py @@ -0,0 +1,109 @@ +# -*- coding: utf8 -*- + +import dataclasses +import json +import logging +import os +import typing as t +from pathlib import Path + +import docspec +import typing_extensions as te +from databind.core import DeserializeAs + +from pydoc_markdown.contrib.renderers.markdown import MarkdownRenderer +from pydoc_markdown.interfaces import Context, Renderer + +logger = logging.getLogger(__name__) + + +@dataclasses.dataclass +class CustomizedMarkdownRenderer(MarkdownRenderer): + """We override some defaults in this subclass.""" + + #: Disabled because Docusaurus supports this automatically. + insert_header_anchors: bool = False + + #: Escape html in docstring, otherwise it could lead to invalid html. + escape_html_in_docstring: bool = True + + #: Conforms to Docusaurus header format. + render_module_header_template: str = ( + "---\n" "sidebar_label: {relative_module_name}\n" "title: {module_name}\n" "---\n\n" + ) + + add_module_prefix: bool = False + use_fixed_header_levels: bool = False + + +@dataclasses.dataclass +class NextraRenderer(Renderer): + """ + Produces Markdown files for use in a Nextra docs website. + It creates files in a fixed layout that reflects the structure of the documented packages. + The files will be rendered into the directory specified with the #docs_base_path option. + + ### Options + """ + + #: The #MarkdownRenderer configuration. + markdown: te.Annotated[MarkdownRenderer, DeserializeAs(CustomizedMarkdownRenderer)] = dataclasses.field( + default_factory=CustomizedMarkdownRenderer + ) + + #: The path where the Nextra docs content is. Defaults "docs" folder. + docs_base_path: str = "docs" + + #: The output path inside the docs_base_path folder, used to output the + #: module reference. + relative_output_path: str = "pages" + + #: The sidebar path inside the docs_base_path folder, used to output the + #: sidebar for the module reference. + relative_sidebar_path: str = "sidebar.json" + + #: The top-level label in the sidebar. Default to 'Reference'. Can be set to null to + #: remove the sidebar top-level all together. This option assumes that there is only one top-level module. + sidebar_top_level_label: t.Optional[str] = "Reference" + + #: The top-level module label in the sidebar. Default to null, meaning that the actual + #: module name will be used. This option assumes that there is only one top-level module. + sidebar_top_level_module_label: t.Optional[str] = None + + def init(self, context: Context) -> None: + self.markdown.init(context) + + def render(self, modules: t.List[docspec.Module]) -> None: + module_tree: t.Dict[str, t.Any] = {"children": {}, "edges": []} + output_path = Path(self.docs_base_path) / self.relative_output_path + for module in modules: + filepath = output_path + + module_parts = module.name.split(".") + if module.location.filename.endswith("__init__.py"): + module_parts.append("__init__") + + relative_module_tree = module_tree + intermediary_module = [] + + for module_part in module_parts[:-1]: + # update the module tree + intermediary_module.append(module_part) + intermediary_module_name = ".".join(intermediary_module) + relative_module_tree["children"].setdefault(intermediary_module_name, {"children": {}, "edges": []}) + relative_module_tree = relative_module_tree["children"][intermediary_module_name] + + # descend to the file + filepath = filepath / module_part + + # create intermediary missing directories and get the full path + filepath.mkdir(parents=True, exist_ok=True) + filepath = filepath / f"{module_parts[-1]}.md" + + with filepath.open("w", encoding=self.markdown.encoding) as fp: + logger.info("Render file %s", filepath) + self.markdown.render_single_page(fp, [module]) + + # only update the relative module tree if the file is not empty + relative_module_tree["edges"].append(os.path.splitext(str(filepath.relative_to(self.docs_base_path)))[0]) + diff --git a/src/pydoc_markdown/main.py b/src/pydoc_markdown/main.py index 1144b708..0a8ba126 100644 --- a/src/pydoc_markdown/main.py +++ b/src/pydoc_markdown/main.py @@ -330,6 +330,7 @@ def cli( "mkdocs": static.DEFAULT_MKDOCS_CONFIG, "hugo": static.DEFAULT_HUGO_CONFIG, "docusaurus": static.DEFAULT_DOCUSAURUS_CONFIG, + "nextra": static.DEFAULT_NEXTRA_CONFIG } with open(filename, "w") as fp: fp.write(source[bootstrap]) diff --git a/src/pydoc_markdown/static.py b/src/pydoc_markdown/static.py index 73da556f..eba99fa1 100644 --- a/src/pydoc_markdown/static.py +++ b/src/pydoc_markdown/static.py @@ -84,6 +84,7 @@ contents: [ my_project, my_project.* ] """.lstrip() +#: Default configuration for Docusaurus to use Pydoc-Markdown. DEFAULT_DOCUSAURUS_CONFIG = """ loaders: - type: python @@ -100,6 +101,21 @@ sidebar_top_level_label: 'Reference' """.lstrip() +#: Default configuration for Nextra to use Pydoc-Markdown. +DEFAULT_NEXTRA_CONFIG = """ +loaders: + - type: python +processors: + - type: filter + skip_empty_modules: true + - type: smart + - type: crossref +renderer: + type: nextra + docs_base_path: docs + relative_output_path: pages +""".lstrip() + #: Default configuration for Read the Docs to use Pydoc-Markdown. READTHEDOCS_FILES = {