Skip to content

Commit 0846f33

Browse files
Merge pull request #5 from fern-api/navigation
feat: Navigation
2 parents 14f3d46 + ab2c10d commit 0846f33

File tree

4 files changed

+111
-73
lines changed

4 files changed

+111
-73
lines changed

README.md

Lines changed: 13 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,28 @@
1-
# sphinx-autodoc-fern
1+
# sphinx-autodoc2-fern
22

3-
`sphinx-autodoc-fern` is a Sphinx extension that automatically generates API documentation for your Python packages with **Fern documentation format support**.
3+
Generate Fern-compatible API documentation from Python packages using static analysis.
44

5-
This is a fork of [`sphinx-autodoc2`](https://github.com/sphinx-extensions2/sphinx-autodoc2) that adds powerful [Fern](https://buildwithfern.com) documentation generation capabilities.
5+
Fork of [`sphinx-autodoc2`](https://github.com/sphinx-extensions2/sphinx-autodoc2) with [Fern](https://buildwithfern.com) output support.
66

7-
## 🆕 New Fern Features
8-
9-
- **Fern-compatible Markdown output** - Generate documentation that works seamlessly with Fern
10-
- **Enhanced parameter formatting** - Uses Fern's ParamField components for better API documentation
11-
- **Smart callout handling** - Automatically converts NOTE: and WARNING: to Fern components
12-
- **Beautiful tables** - Improved formatting for classes, functions, and module contents
13-
14-
Static analysis of Python code
15-
16-
: There is no need to install your package to generate the documentation, and `sphinx-autodoc2` will correctly handle `if TYPE_CHECKING` blocks and other typing only features.
17-
: You can even document packages from outside the project (via `git clone`)!
18-
19-
Optimized for rebuilds
20-
21-
: Analysis of packages and file rendering are cached, so you can use `sphinx-autodoc2` in your development workflow.
22-
23-
Support for `__all__`
24-
25-
: `sphinx-autodoc2` can follow `__all__` variable, to only document the public API.
26-
27-
Support for both `rst` and `md` docstrings
28-
29-
: `sphinx-autodoc2` supports both `rst` and `md` ([MyST](https://myst-parser.readthedocs.io)) docstrings, which can be mixed within the same project.
30-
31-
Highly configurable
32-
33-
: `sphinx-autodoc-fern` is highly configurable, with many options to control the analysis and output of the documentation.
34-
35-
Fern Documentation Format Support
36-
37-
: Generate beautiful Fern-compatible documentation with enhanced formatting for parameters, callouts, and API references.
38-
39-
Decoupled analysis and rendering
40-
41-
: The analysis and rendering of the documentation are decoupled, and not dependent on Sphinx.
42-
: This means that you can use `sphinx-autodoc-fern` to generate documentation outside of Sphinx (see the `autodoc2` command line tool).
43-
44-
## 🚀 Quick Start with Fern
7+
## Installation
458

469
```bash
47-
pip install sphinx-autodoc-fern
10+
pip install sphinx-autodoc2-fern
4811
```
4912

50-
To use the Fern renderer:
51-
52-
```python
53-
from autodoc2.render.fern_ import FernRenderer
13+
## Usage
5414

55-
renderer = FernRenderer()
56-
# Generate Fern-compatible documentation
57-
```
58-
59-
Or use with the CLI:
15+
Generate Fern-compatible markdown documentation:
6016

6117
```bash
62-
autodoc2 --renderer fern your_package/
18+
autodoc2 --renderer fern /path/to/your/package
6319
```
6420

21+
This creates:
22+
- Markdown files with Fern-compatible frontmatter and slugs
23+
- `navigation.yml` for Fern docs structure
24+
- Tables with proper linking and descriptions
25+
6526
## Acknowledgments
6627

6728
This project is a fork of the excellent [`sphinx-autodoc2`](https://github.com/sphinx-extensions2/sphinx-autodoc2) by Chris Sewell. All credit for the core functionality goes to the original project.

src/autodoc2/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This is a fork of sphinx-autodoc2 with added support for Fern documentation format.
44
"""
55

6-
__version__ = "0.1.0"
6+
__version__ = "0.1.1"
77

88

99
def setup(app): # type: ignore

src/autodoc2/cli.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,13 @@ def _warn(msg: str, type_: WarningSubtypes) -> None:
276276
content = "\n".join(
277277
render_class(db, config, warn=_warn).render_item(mod_name)
278278
)
279-
out_path = output / (mod_name + render_class.EXTENSION)
279+
280+
# Use hyphens in filenames for Fern renderer to match slugs
281+
if renderer == "fern":
282+
filename = mod_name.replace('.', '-').replace('_', '-')
283+
out_path = output / (filename + render_class.EXTENSION)
284+
else:
285+
out_path = output / (mod_name + render_class.EXTENSION)
280286
paths.append(out_path)
281287
if out_path.exists() and out_path.read_text("utf8") == content:
282288
# Don't write the file if it hasn't changed

src/autodoc2/render/fern_.py

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def render_item(self, full_name: str) -> t.Iterable[str]:
3131
if type_ in ("package", "module"):
3232
yield "---"
3333
yield "layout: overview"
34+
slug = self._generate_slug(full_name)
35+
yield f"slug: {slug}"
3436
yield "---"
3537
yield ""
3638

@@ -137,35 +139,25 @@ def render_package(self, item: ItemData) -> t.Iterable[str]:
137139
yield ""
138140
for child in children_by_type["package"]:
139141
name = child["full_name"].split(".")[-1]
140-
# Create simple relative link using just the child name
141-
link_path = name
142+
# Create slug-based link using full dotted name
143+
slug = self._generate_slug(child["full_name"])
142144
doc_summary = child.get('doc', '').split('\n')[0][:80] if child.get('doc') else ""
143145
if len(child.get('doc', '')) > 80:
144146
doc_summary += "..."
145-
yield f"- **[`{name}`]({link_path})** - {doc_summary}" if doc_summary else f"- **[`{name}`]({link_path})**"
147+
yield f"- **[`{name}`]({slug})** - {doc_summary}" if doc_summary else f"- **[`{name}`]({slug})**"
146148
yield ""
147149

148150
if has_submodules:
149151
yield "## Submodules"
150152
yield ""
151153
for child in children_by_type["module"]:
152154
name = child["full_name"].split(".")[-1]
153-
# Replace underscores with dashes in display text for better readability
154-
display_name = name.replace('_', '-')
155-
156-
# Create contextual link based on current page
157-
current_parts = item["full_name"].split(".")
158-
if len(current_parts) == 1:
159-
# On root page - use simple name
160-
link_path = display_name
161-
else:
162-
# On subpackage page - use full filename (convert dots and underscores to dashes)
163-
link_path = child["full_name"].replace('.', '-').replace('_', '-')
164-
155+
# Create slug-based link using full dotted name
156+
slug = self._generate_slug(child["full_name"])
165157
doc_summary = child.get('doc', '').split('\n')[0][:80] if child.get('doc') else ""
166158
if len(child.get('doc', '')) > 80:
167159
doc_summary += "..."
168-
yield f"- **[`{display_name}`]({link_path})** - {doc_summary}" if doc_summary else f"- **[`{display_name}`]({link_path})**"
160+
yield f"- **[`{name}`]({slug})** - {doc_summary}" if doc_summary else f"- **[`{name}`]({slug})**"
169161
yield ""
170162

171163
# Show Module Contents summary if we have actual content (not just submodules)
@@ -530,10 +522,11 @@ def _format_docstring_sections(self, docstring: str, item: ItemData | None = Non
530522

531523
# Parameters section
532524
if sections["parameters"]:
533-
yield "**Parameters:**"
534-
yield ""
535525
# If we have function item data, use ParamField components
536526
if item and item.get("args"):
527+
yield "**Parameters:**"
528+
yield ""
529+
537530
# Build parameter info map from function signature
538531
param_info = {}
539532
for prefix, name, annotation, default in item["args"]:
@@ -711,4 +704,82 @@ def replace_tag(match):
711704
escaped_tag = tag.replace('{', '\\{').replace('}', '\\}')
712705
escaped_content = escaped_content.replace(placeholder, f'`{escaped_tag}`')
713706

714-
return escaped_content
707+
return escaped_content
708+
709+
def _generate_slug(self, full_name: str) -> str:
710+
"""Generate slug from full dotted name: nemo_curator.utils.grouping → nemo-curator-utils-grouping"""
711+
return full_name.replace('.', '-').replace('_', '-')
712+
713+
def generate_navigation_yaml(self) -> str:
714+
"""Generate navigation YAML for Fern docs.yml following sphinx autodoc2 toctree logic."""
715+
import yaml
716+
717+
# Find root packages (no dots in name)
718+
root_packages = []
719+
for item in self._db.get_by_type("package"):
720+
full_name = item["full_name"]
721+
if "." not in full_name: # Root packages only
722+
root_packages.append(item)
723+
724+
if not root_packages:
725+
return ""
726+
727+
# Build navigation structure recursively
728+
nav_contents = []
729+
for root_pkg in sorted(root_packages, key=lambda x: x["full_name"]):
730+
nav_item = self._build_nav_item_recursive(root_pkg)
731+
if nav_item:
732+
nav_contents.append(nav_item)
733+
734+
# Create the final navigation structure
735+
navigation = {
736+
"navigation": [
737+
{
738+
"section": "API Reference",
739+
"skip-slug": True,
740+
"contents": nav_contents
741+
}
742+
]
743+
}
744+
745+
return yaml.dump(navigation, default_flow_style=False, sort_keys=False, allow_unicode=True)
746+
747+
def _build_nav_item_recursive(self, item: ItemData) -> dict[str, t.Any] | None:
748+
"""Build navigation item recursively following sphinx autodoc2 toctree logic."""
749+
full_name = item["full_name"]
750+
slug = self._generate_slug(full_name)
751+
752+
# Get children (same logic as sphinx toctrees)
753+
subpackages = list(self.get_children(item, {"package"}))
754+
submodules = list(self.get_children(item, {"module"}))
755+
756+
if subpackages or submodules:
757+
# This has children - make it a section with skip-slug
758+
section_item = {
759+
"section": full_name.split(".")[-1], # Use short name for section
760+
"skip-slug": True,
761+
"path": f"{slug}.md",
762+
"contents": []
763+
}
764+
765+
# Add subpackages recursively (maxdepth: 3 like sphinx)
766+
for child in sorted(subpackages, key=lambda x: x["full_name"]):
767+
child_nav = self._build_nav_item_recursive(child)
768+
if child_nav:
769+
section_item["contents"].append(child_nav)
770+
771+
# Add submodules as pages (maxdepth: 1 like sphinx)
772+
for child in sorted(submodules, key=lambda x: x["full_name"]):
773+
child_slug = self._generate_slug(child["full_name"])
774+
section_item["contents"].append({
775+
"page": child["full_name"].split(".")[-1], # Use short name
776+
"path": f"{child_slug}.md"
777+
})
778+
779+
return section_item
780+
else:
781+
# Leaf item - just a page
782+
return {
783+
"page": full_name.split(".")[-1], # Use short name
784+
"path": f"{slug}.md"
785+
}

0 commit comments

Comments
 (0)