diff --git a/ADOPTION_GUIDE.md b/ADOPTION_GUIDE.md
deleted file mode 100644
index fc52ee9..0000000
--- a/ADOPTION_GUIDE.md
+++ /dev/null
@@ -1,152 +0,0 @@
-# Using mdxify
-
-## Quick Start
-
-### Run without installing (using uvx)
-```bash
-uvx mdxify --help
-```
-
-### Basic Commands
-
-**Generate docs for your package:**
-```bash
-mdxify --all --root-module mypackage
-```
-
-**Generate docs for specific modules only:**
-```bash
-mdxify mypackage.core mypackage.api mypackage.models
-```
-
-**Change output directory:**
-```bash
-mdxify --all --root-module mypackage --output-dir docs/python-sdk
-```
-(Default is `docs/python-sdk`)
-
-**Skip navigation file updates:**
-```bash
-mdxify --all --root-module mypackage --no-update-nav
-```
-
-## What It Does
-
-1. Reads your Python files using AST (doesn't import them)
-2. Extracts classes, functions, methods, and their docstrings
-3. Generates `.mdx` files with formatted documentation
-4. Optionally updates a `docs.json` navigation file
-
-## Output
-
-For a module like `mypackage.core.auth`, you get:
-- File: `docs/python-sdk/mypackage-core-auth.mdx`
-- Contains: All public classes, functions, methods with their signatures and docstrings
-- Formatted: MDX with proper escaping for type annotations
-
-That's it. Run it, get MDX files.
-
-## Navigation Updates
-
-If your documentation framework uses a `docs.json` file (like Mintlify), mdxify can automatically update your navigation. Add this placeholder where you want the API docs to appear:
-
-```json
-{
- "navigation": {
- "anchors": [
- {
- "anchor": "API Reference",
- "groups": [
- {
- "group": "Python API",
- "pages": [
- "api/overview",
- {"$mdxify": "generated"},
- "api/advanced"
- ]
- }
- ]
- }
- ]
- }
-}
-```
-
-mdxify will replace `{"$mdxify": "generated"}` with your module structure. Without this placeholder, you'll see a warning and need to manually add the generated files to your navigation.
-
-## GitHub Actions Example
-
-Create `.github/workflows/docs.yml`:
-
-```yaml
-name: Generate API Docs
-
-on:
- push:
- branches: [main]
- paths:
- - 'src/**/*.py'
- - 'pyproject.toml'
-
-jobs:
- generate-docs:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Install uv
- uses: astral-sh/setup-uv@v5
-
- - name: Generate API documentation
- run: uvx mdxify --all --root-module mypackage --output-dir docs/python-sdk
-
- - name: Commit changes
- uses: stefanzweifel/git-auto-commit-action@v4
- with:
- commit_message: 'docs: update API reference [skip ci]'
- file_pattern: 'docs/python-sdk/**/*.mdx'
-```
-
-## Example Output Structure
-
-For a package like:
-```
-mypackage/
-├── __init__.py
-├── core.py
-├── utils/
-│ ├── __init__.py
-│ ├── helpers.py
-│ └── validators.py
-└── models/
- ├── __init__.py
- └── base.py
-```
-
-You get:
-```
-docs/python-sdk/
-├── mypackage-__init__.mdx
-├── mypackage-core.mdx
-├── mypackage-utils-__init__.mdx
-├── mypackage-utils-helpers.mdx
-├── mypackage-utils-validators.mdx
-├── mypackage-models-__init__.mdx
-└── mypackage-models-base.mdx
-```
-
-## Local Mintlify Development
-
-If you're using Mintlify for documentation, you can preview locally:
-
-```bash
-cd docs && npx mint dev
-```
-
-Add these entries to your `.gitignore` for local Mintlify development:
-```
-# Mintlify local development
-docs/node_modules
-docs/package.json
-docs/package-lock.json
-```
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..4276389
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,32 @@
+# AGENTS.md
+
+Agent notes for working in this repository. The scope of this file is the entire repo.
+
+## Working Directory & Module Discovery
+
+- Always confirm `pwd` before running commands.
+- When testing mdxify on other projects, ensure modules are importable from CWD or run from the target project with:
+ - `uv run --with-editable /path/to/mdxify mdxify`
+- Prefer `uv run -m` over invoking Python directly.
+
+## UV Usage
+
+- Use `uv run` for scripts and tests.
+- Use `uvx mdxify@version` to test a specific PyPI version.
+
+## Testing Discipline
+
+- Run the full test suite: `uv run pytest -xvs`.
+- For navigation/output changes, validate against a real project (e.g., FastMCP).
+
+## Git & Releases
+
+- Run pre-commit before pushing: `uv run pre-commit run --all-files`.
+- Push to `main` before creating a release.
+- Create releases with `gh release create` (CI handles PyPI).
+
+## Focus
+
+- Don’t fixate on arbitrary details; align with user requirements.
+- Ask for clarification when intent isn’t clear.
+
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index 458ad2e..0000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# mdxify
-
-A tool to generate MDX API documentation for Python packages with automatic Mintlify navigation integration.
-
-## Project Overview
-
-**mdxify** converts Python docstrings into MDX format documentation. Key features:
-- Parses Python modules using AST (no imports required)
-- Generates hierarchical navigation structure
-- Integrates with Mintlify via placeholder replacement (`{"$mdxify": "generated"}`)
-- Formats Google-style docstrings using Griffe
-- Default output: `docs/python-sdk/`
-
-## Development Notes for Claude
-
-### Common Pitfalls to Avoid
-
-### Working Directory Management
-- **Always verify your current directory** with `pwd` before running commands
-- When testing mdxify on other projects, remember that Python needs to find the modules:
- - Either run from the target project's directory with `uv run --with-editable /path/to/mdxify mdxify`
- - Or ensure the target modules are importable from your current directory
-- Don't assume you're in the right directory - check first
-
-### UV Usage (It's 2025!)
-- **Always use `uv run`** instead of calling Python directly
-- For testing local changes: `uv run` will use the editable version of the package
-- For testing specific versions from PyPI: `uvx mdxify@version`
-- Use `uv run -m` not `python -m` to run modules
-
-### Testing Before Releasing
-- **Always test your changes** before cutting a release
-- Run the full test suite: `uv run pytest -xvs`, run a single test: `uv run pytest -xvs -k test_function_name`
-- For navigation/output changes, test on a real project (like FastMCP)
-- Don't push and release without verifying the changes work
-
-### Focus on What Matters
-- Don't get fixated on arbitrary implementation details (like "api-reference/" prefixes)
-- Focus on the actual requirements the user stated
-- Ask for clarification if the goal isn't clear rather than making assumptions
-
-### Git and Releases
-- **ALWAYS run pre-commit before pushing**: `uv run pre-commit run --all-files`
-- Or better: install pre-commit hooks: `uv run pre-commit install`
-- Commit messages should be clear and describe what changed
-- Always push to main before creating a release
-- Use `gh release create` for releases - GitHub Actions handles PyPI publishing
-
-## Project-Specific Notes
-
-### Navigation Structure
-- Top-level groups use fully qualified names (e.g., `fastmcp.client`)
-- Nested groups use simple names (e.g., `auth` under `fastmcp.client`)
-- Navigation entries are sorted: plain pages first, then groups, alphabetically
-
-### Default Paths
-- Default output directory is `docs/python-sdk` (not `docs/api`)
-- Navigation anchor is "SDK Reference" (not "API Reference")
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..57fa6c4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,47 @@
+# Contributing to mdxify
+
+Thanks for contributing! This guide outlines local development, testing, and release tips.
+
+## Local Development
+
+```bash
+# Install dependencies
+uv sync
+
+# Run tests
+uv run pytest -xvs
+
+# Type checking
+uv run ty check
+
+# Linting
+uv run ruff check src/ tests/
+
+# Pre-commit hooks
+uv run pre-commit install
+uv run pre-commit run --all-files
+```
+
+## Working Practices
+
+- Prefer small, focused PRs with clear titles and descriptions.
+- For navigation/output changes, test on a real project (e.g., FastMCP) where possible.
+- Ask for clarification if goals are ambiguous.
+
+## Releasing
+
+- Ensure tests pass and pre-commit is clean.
+- Push to `main` before creating a release.
+- Use `gh release create` (GitHub Actions handles PyPI publishing).
+
+## Notes on Navigation Structure
+
+- Top-level groups use fully-qualified names (e.g., `fastmcp.client`).
+- Nested groups use simple names (e.g., `auth`).
+- Entries are sorted: plain pages first, then groups alphabetically.
+
+## Default Paths
+
+- Default output directory: `docs/python-sdk`
+- Default navigation anchor: `SDK Reference`
+
diff --git a/README.md b/README.md
index df620ba..d4da9b5 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
# mdxify
-Generate MDX API documentation from Python modules with automatic navigation structure.
+Generate API documentation from Python modules with automatic navigation and source links. MDX is the default output today; Markdown support is planned.
## Projects Using mdxify
-mdxify is powering the API documentation for the following projects:
+mdxify powers the API docs for:
-- **[FastMCP](https://github.com/jlowin/fastmcp)** by Jeremiah Lowin - See it in action at [gofastmcp.com](https://gofastmcp.com/python-sdk)
-- **[Prefect](https://github.com/PrefectHQ/prefect)** - See the API reference at [docs.prefect.io](https://docs.prefect.io/v3/api-ref/python)
+- [FastMCP](https://github.com/jlowin/fastmcp) — live at https://gofastmcp.com/python-sdk
+- [Prefect](https://github.com/PrefectHQ/prefect) — API ref at https://docs.prefect.io/v3/api-ref/python
## Installation
@@ -15,129 +15,127 @@ mdxify is powering the API documentation for the following projects:
pip install mdxify
```
-## Usage
+## Quick Start
-Generate documentation for all modules in a package:
+
+Basic commands
+
+Generate docs for all modules in a package:
```bash
mdxify --all --root-module mypackage --output-dir docs/python-sdk
```
-Generate documentation for specific modules:
+Generate docs for specific modules:
```bash
mdxify mypackage.core mypackage.utils --output-dir docs/python-sdk
```
-Exclude internal modules from documentation:
+Exclude internal modules:
```bash
-mdxify --all --root-module mypackage --exclude mypackage.internal --exclude mypackage.tests
+mdxify --all --root-module mypackage \
+ --exclude mypackage.internal --exclude mypackage.tests
+```
+
+
+
+
+GitHub Actions example
+
+Create `.github/workflows/docs.yml`:
+
+```yaml
+name: Generate API Docs
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'src/**/*.py'
+ - 'pyproject.toml'
+
+jobs:
+ generate-docs:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+ - name: Generate API documentation
+ run: uvx mdxify --all --root-module mypackage --output-dir docs/python-sdk
+ - name: Commit changes
+ uses: stefanzweifel/git-auto-commit-action@v4
+ with:
+ commit_message: 'docs: update API reference [skip ci]'
+ file_pattern: 'docs/python-sdk/**/*.mdx'
```
-### Options
+
-- `modules`: Specific modules to document
-- `--all`: Generate documentation for all modules under the root module
-- `--root-module`: Root module to generate docs for (required when using --all)
-- `--output-dir`: Output directory for generated MDX files (default: docs/python-sdk)
-- `--update-nav/--no-update-nav`: Update docs.json navigation (default: True)
-- `--skip-empty-parents`: Skip parent modules that only contain boilerplate (default: False)
-- `--anchor-name` / `--navigation-key`: Name of the navigation anchor or group to update (default: 'SDK Reference')
-- `--exclude`: Module to exclude from documentation (can be specified multiple times). Excludes the module and all its submodules.
-- `--repo-url`: GitHub repository URL for source code links (e.g., https://github.com/owner/repo). If not provided, will attempt to detect from git remote.
-- `--branch`: Git branch name for source code links (default: main)
+## CLI Options
-### Navigation Updates
+- `modules`: Modules to document
+- `--all`: Generate docs for all modules under the root module
+- `--root-module`: Root module (required with `--all`)
+- `--output-dir`: Output directory (default: `docs/python-sdk`)
+- `--update-nav/--no-update-nav`: Update Mintlify `docs.json` (default: True)
+- `--skip-empty-parents`: Skip boilerplate parents in nav (default: False)
+- `--anchor-name` / `--navigation-key`: Anchor/group name to update (default: `SDK Reference`)
+- `--exclude`: Module(s) to exclude (repeatable, excludes submodules too)
+- `--repo-url`: GitHub repo for source links (auto-detected if omitted)
+- `--branch`: Git branch for source links (default: `main`)
-mdxify can automatically update your `docs.json` navigation by finding either anchors or groups:
+## Navigation Updates (Mintlify)
-1. **First run**: Add a placeholder in your `docs.json` using either format:
+mdxify can update `docs/docs.json` by finding either anchors or groups. Add a placeholder for the first run:
+
+Anchor format:
-**Anchor format (e.g., FastMCP):**
```json
{
"navigation": [
- {
- "anchor": "SDK Reference",
- "pages": [
- {"$mdxify": "generated"}
- ]
- }
+ { "anchor": "SDK Reference", "pages": [{ "$mdxify": "generated" }] }
]
}
```
-**Group format (e.g., Prefect):**
+Group format:
+
```json
{
"navigation": [
- {
- "group": "SDK Reference",
- "pages": [
- {"$mdxify": "generated"}
- ]
- }
+ { "group": "SDK Reference", "pages": [{ "$mdxify": "generated" }] }
]
}
```
-2. **Subsequent runs**: mdxify will find and update the existing anchor or group directly - no placeholder needed!
+Subsequent runs will update the existing anchor/group automatically. Configure the target with `--anchor-name` (alias `--navigation-key`).
-The `--anchor-name` parameter (or its alias `--navigation-key`) identifies which anchor or group to update.
+## Source Code Links
-### Source Code Links
-
-mdxify can automatically add links to source code on GitHub for all functions, classes, and methods:
+Add GitHub source links to functions/classes/methods:
```bash
# Auto-detect repository from git remote
mdxify --all --root-module mypackage
# Or specify repository explicitly
-mdxify --all --root-module mypackage --repo-url https://github.com/owner/repo --branch develop
+mdxify --all --root-module mypackage \
+ --repo-url https://github.com/owner/repo --branch develop
```
-This adds source links next to each function/class/method that link directly to the code on GitHub.
-
-#### Customizing Source Link Text
-
-You can customize the link text/symbol using the `MDXIFY_SOURCE_LINK_TEXT` environment variable:
-
-```bash
-# Use custom text
-export MDXIFY_SOURCE_LINK_TEXT="[src]"
-mdxify --all --root-module mypackage
-
-# Use emoji
-export MDXIFY_SOURCE_LINK_TEXT="🔗"
-mdxify --all --root-module mypackage
-
-# Use different Unicode symbol (default is "view on GitHub ↗")
-export MDXIFY_SOURCE_LINK_TEXT="⧉"
-mdxify --all --root-module mypackage
-```
+Customize link text via `MDXIFY_SOURCE_LINK_TEXT` if desired.
## Features
-- **Fast AST-based parsing** - No module imports required
-- **MDX output** - Compatible with modern documentation frameworks
-- **Automatic navigation** - Generates hierarchical navigation structure
-- **Google-style docstrings** - Formats docstrings using Griffe
-- **Smart filtering** - Excludes private modules and known test patterns
+- Fast AST-based parsing (no imports)
+- MDX output with safe escaping
+- Automatic hierarchical navigation (Mintlify)
+- Google-style docstrings via Griffe
+- Smart filtering of private modules
## Development
-```bash
-# Install development dependencies
-uv sync
-
-# Run tests
-uv run pytest
-
-# Run type checking
-uv run ty check
-
-# Run linting
-uv run ruff check src/ tests/
-```
\ No newline at end of file
+See `CONTRIBUTING.md` for local setup, testing, linting, and release guidance.
diff --git a/docs/mdxify-cli.mdx b/docs/mdxify-cli.mdx
index 5fe5c0e..f8c9c6a 100644
--- a/docs/mdxify-cli.mdx
+++ b/docs/mdxify-cli.mdx
@@ -10,10 +10,10 @@ CLI interface for mdxify.
## Functions
-### `remove_excluded_files`
+### `remove_excluded_files`
```python
-remove_excluded_files(output_dir: Path, exclude_patterns: list[str]) -> int
+remove_excluded_files(output_dir: Path, exclude_patterns: list[str], extension: str = 'mdx') -> int
```
@@ -22,7 +22,7 @@ Remove existing MDX files for excluded modules.
Returns the number of files removed.
-### `main`
+### `main`
```python
main()
@@ -30,7 +30,7 @@ main()
## Classes
-### `SimpleProgressBar`
+### `SimpleProgressBar`
A simple progress bar using only built-in Python.
@@ -38,7 +38,7 @@ A simple progress bar using only built-in Python.
**Methods:**
-#### `update`
+#### `update`
```python
update(self, n: int = 1)
@@ -47,7 +47,7 @@ update(self, n: int = 1)
Update progress by n steps.
-#### `finish`
+#### `finish`
```python
finish(self)
diff --git a/docs/mdxify-generator.mdx b/docs/mdxify-generator.mdx
index 7c5c060..90b1309 100644
--- a/docs/mdxify-generator.mdx
+++ b/docs/mdxify-generator.mdx
@@ -10,7 +10,7 @@ MDX documentation generation.
## Functions
-### `is_module_empty`
+### `is_module_empty`
```python
is_module_empty(module_path: Path) -> bool
@@ -20,10 +20,10 @@ is_module_empty(module_path: Path) -> bool
Check if a module documentation file indicates it's empty.
-### `generate_mdx`
+### `generate_mdx`
```python
-generate_mdx(module_info: dict[str, Any], output_file: Path, repo_url: Optional[str] = None, branch: str = 'main', root_module: Optional[str] = None) -> None
+generate_mdx(module_info: dict[str, Any], output_file: Path, repo_url: Optional[str] = None, branch: str = 'main', root_module: Optional[str] = None, renderer: Optional[Renderer] = None) -> None
```
diff --git a/docs/mdxify-renderers.mdx b/docs/mdxify-renderers.mdx
new file mode 100644
index 0000000..d374a81
--- /dev/null
+++ b/docs/mdxify-renderers.mdx
@@ -0,0 +1,73 @@
+---
+title: renderers
+sidebarTitle: renderers
+---
+
+# `mdxify.renderers`
+
+
+Renderers for different output formats (MDX, Markdown).
+
+## Functions
+
+### `get_renderer`
+
+```python
+get_renderer(name: str) -> Renderer
+```
+
+## Classes
+
+### `Renderer`
+
+**Methods:**
+
+#### `escape`
+
+```python
+escape(self, content: str) -> str
+```
+
+#### `header_with_source`
+
+```python
+header_with_source(self, header: str, source_link: Optional[str]) -> str
+```
+
+#### `frontmatter`
+
+```python
+frontmatter(self, title: str, sidebar_title: Optional[str] = None) -> list[str]
+```
+
+### `MDXRenderer`
+
+**Methods:**
+
+#### `escape`
+
+```python
+escape(self, content: str) -> str
+```
+
+#### `header_with_source`
+
+```python
+header_with_source(self, header: str, source_link: Optional[str]) -> str
+```
+
+### `MarkdownRenderer`
+
+**Methods:**
+
+#### `escape`
+
+```python
+escape(self, content: str) -> str
+```
+
+#### `header_with_source`
+
+```python
+header_with_source(self, header: str, source_link: Optional[str]) -> str
+```
diff --git a/src/mdxify/cli.py b/src/mdxify/cli.py
index 7f5319b..8e7cb40 100644
--- a/src/mdxify/cli.py
+++ b/src/mdxify/cli.py
@@ -13,6 +13,7 @@
from .generator import generate_mdx
from .navigation import update_docs_json
from .parser import parse_module_fast, parse_modules_with_inheritance
+from .renderers import get_renderer
from .source_links import detect_github_repo_url
@@ -78,7 +79,7 @@ def finish(self):
print() # New line after progress bar
-def remove_excluded_files(output_dir: Path, exclude_patterns: list[str]) -> int:
+def remove_excluded_files(output_dir: Path, exclude_patterns: list[str], extension: str = "mdx") -> int:
"""Remove existing MDX files for excluded modules.
Returns the number of files removed.
@@ -87,7 +88,7 @@ def remove_excluded_files(output_dir: Path, exclude_patterns: list[str]) -> int:
return 0
removed_count = 0
- for mdx_file in output_dir.glob("*.mdx"):
+ for mdx_file in output_dir.glob(f"*.{extension}"):
# Convert filename back to module name
stem = mdx_file.stem
if stem.endswith("-__init__"):
@@ -173,6 +174,12 @@ def main():
default="main",
help="Git branch name for source code links (default: main)",
)
+ parser.add_argument(
+ "--format",
+ choices=["mdx", "md"],
+ default="mdx",
+ help="Output format: 'mdx' (default) or 'md' (Markdown)",
+ )
parser.add_argument(
"--include-internal",
action="store_true",
@@ -237,6 +244,10 @@ def main():
# Remove duplicates
modules_to_process = sorted(set(modules_to_process))
+ # Select renderer early (used by cleanup for excludes)
+ renderer = get_renderer(args.format)
+ ext = renderer.file_extension
+
# Filter out excluded modules
if args.exclude:
excluded_count = 0
@@ -265,9 +276,9 @@ def main():
modules_to_process = filtered_modules
# Remove existing MDX files for excluded modules (declarative behavior)
- removed_count = remove_excluded_files(args.output_dir, args.exclude)
+ removed_count = remove_excluded_files(args.output_dir, args.exclude, extension=ext)
if removed_count > 0:
- print(f"Removed {removed_count} existing MDX files for excluded modules")
+ print(f"Removed {removed_count} existing {ext} files for excluded modules")
# Determine repository URL for source links
repo_url = args.repo_url
@@ -275,10 +286,15 @@ def main():
repo_url = detect_github_repo_url()
if repo_url and args.verbose:
print(f"Detected repository: {repo_url}")
+
+ # If using Markdown, disable nav updates (Mintlify is MDX-focused)
+ if renderer.file_extension == "md" and args.update_nav:
+ print("Warning: --format md selected; skipping navigation updates.")
+ args.update_nav = False
# Clean up existing MDX files when using --all (declarative behavior)
if args.all and args.output_dir.exists():
- existing_files = list(args.output_dir.glob("*.mdx"))
+ existing_files = list(args.output_dir.glob(f"*.{ext}"))
# Build a set of expected filenames for current modules
expected_files = set()
for module_name in modules_to_process:
@@ -289,9 +305,9 @@ def main():
for m in modules_to_process
)
if has_submodules:
- filename = f"{module_name.replace('.', '-')}-__init__.mdx"
+ filename = f"{module_name.replace('.', '-')}-__init__.{ext}"
else:
- filename = f"{module_name.replace('.', '-')}.mdx"
+ filename = f"{module_name.replace('.', '-')}.{ext}"
expected_files.add(filename)
# Remove files that shouldn't exist anymore
@@ -311,7 +327,7 @@ def main():
removed_count += 1
if removed_count > 0:
- print(f"Removed {removed_count} stale MDX files")
+ print(f"Removed {removed_count} stale {ext} files")
# Generate documentation
generated_modules = []
@@ -338,10 +354,10 @@ def main():
# If it has submodules, save it as __init__
if has_submodules:
output_file = (
- args.output_dir / f"{module_name.replace('.', '-')}-__init__.mdx"
+ args.output_dir / f"{module_name.replace('.', '-')}-__init__.{ext}"
)
else:
- output_file = args.output_dir / f"{module_name.replace('.', '-')}.mdx"
+ output_file = args.output_dir / f"{module_name.replace('.', '-')}.{ext}"
generate_mdx(
module_info,
@@ -349,6 +365,7 @@ def main():
repo_url=repo_url,
branch=args.branch,
root_module=args.root_module,
+ renderer=renderer,
)
generated_modules.append(module_name)
@@ -397,10 +414,10 @@ def process_module(module_data):
# If it has submodules, save it as __init__
if has_submodules:
output_file = (
- args.output_dir / f"{module_name.replace('.', '-')}-__init__.mdx"
+ args.output_dir / f"{module_name.replace('.', '-')}-__init__.{ext}"
)
else:
- output_file = args.output_dir / f"{module_name.replace('.', '-')}.mdx"
+ output_file = args.output_dir / f"{module_name.replace('.', '-')}.{ext}"
file_existed = output_file.exists()
generate_mdx(
@@ -409,6 +426,7 @@ def process_module(module_data):
repo_url=repo_url,
branch=args.branch,
root_module=args.root_module,
+ renderer=renderer,
)
module_time = time.time() - module_start
@@ -442,9 +460,9 @@ def process_module(module_data):
for m in modules_to_process
)
if has_submodules:
- output_file = args.output_dir / f"{module_name.replace('.', '-')}-__init__.mdx"
+ output_file = args.output_dir / f"{module_name.replace('.', '-')}-__init__.{ext}"
else:
- output_file = args.output_dir / f"{module_name.replace('.', '-')}.mdx"
+ output_file = args.output_dir / f"{module_name.replace('.', '-')}.{ext}"
if output_file.exists():
existing_files += 1
@@ -541,4 +559,4 @@ def process_module(module_data):
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/src/mdxify/generator.py b/src/mdxify/generator.py
index 74cde4b..44a20d0 100644
--- a/src/mdxify/generator.py
+++ b/src/mdxify/generator.py
@@ -3,8 +3,9 @@
from pathlib import Path
from typing import Any, Optional
-from .formatter import escape_mdx_content, format_docstring_with_griffe
-from .source_links import add_source_link_to_header, generate_source_link
+from .formatter import format_docstring_with_griffe
+from .renderers import MDXRenderer, Renderer
+from .source_links import generate_source_link
def is_module_empty(module_path: Path) -> bool:
@@ -25,6 +26,7 @@ def generate_mdx(
repo_url: Optional[str] = None,
branch: str = "main",
root_module: Optional[str] = None,
+ renderer: Optional[Renderer] = None,
) -> None:
"""Generate MDX documentation from module info.
@@ -35,20 +37,18 @@ def generate_mdx(
branch: Git branch name for source links
root_module: Root module name for finding relative paths
"""
- lines = []
+ if renderer is None:
+ renderer = MDXRenderer()
+ lines: list[str] = []
+
+ # Frontmatter
# Frontmatter
- lines.append("---")
- # If this is an __init__ file, use __init__ as the title
if output_file.stem.endswith("-__init__"):
- lines.append("title: __init__")
- lines.append("sidebarTitle: __init__")
+ lines.extend(renderer.frontmatter("__init__", "__init__"))
else:
module_name = module_info["name"].split(".")[-1]
- lines.append(f"title: {module_name}")
- lines.append(f"sidebarTitle: {module_name}")
- lines.append("---")
- lines.append("")
+ lines.extend(renderer.frontmatter(module_name, module_name))
# Module header
lines.append(f"# `{module_info['name']}`")
@@ -80,7 +80,7 @@ def generate_mdx(
if module_info["docstring"]:
lines.append("")
- lines.append(escape_mdx_content(module_info["docstring"]))
+ lines.append(renderer.escape(module_info["docstring"]))
lines.append("")
# Functions
@@ -101,7 +101,7 @@ def generate_mdx(
)
header = f"### `{func['name']}`"
- header_with_link = add_source_link_to_header(header, source_link)
+ header_with_link = renderer.header_with_source(header, source_link)
lines.append(header_with_link)
lines.append("")
lines.append("```python")
@@ -113,7 +113,7 @@ def generate_mdx(
lines.append("")
# Format docstring with Griffe
formatted_docstring = format_docstring_with_griffe(func["docstring"])
- lines.append(escape_mdx_content(formatted_docstring))
+ lines.append(renderer.escape(formatted_docstring))
lines.append("")
# Classes
@@ -134,7 +134,7 @@ def generate_mdx(
)
header = f"### `{cls['name']}`"
- header_with_link = add_source_link_to_header(header, source_link)
+ header_with_link = renderer.header_with_source(header, source_link)
lines.append(header_with_link)
lines.append("")
@@ -142,7 +142,7 @@ def generate_mdx(
lines.append("")
# Format docstring with Griffe
formatted_docstring = format_docstring_with_griffe(cls["docstring"])
- lines.append(escape_mdx_content(formatted_docstring))
+ lines.append(renderer.escape(formatted_docstring))
lines.append("")
if cls["methods"]:
@@ -169,7 +169,7 @@ def generate_mdx(
method_name = method["name"]
method_header = f"#### `{method_name}`"
- method_header_with_link = add_source_link_to_header(method_header, method_source_link)
+ method_header_with_link = renderer.header_with_source(method_header, method_source_link)
lines.append(method_header_with_link)
lines.append("")
lines.append("```python")
@@ -181,7 +181,7 @@ def generate_mdx(
formatted_docstring = format_docstring_with_griffe(
method["docstring"]
)
- lines.append(escape_mdx_content(formatted_docstring))
+ lines.append(renderer.escape(formatted_docstring))
lines.append("")
output_file.parent.mkdir(parents=True, exist_ok=True)
@@ -194,4 +194,4 @@ def generate_mdx(
if existing_content == new_content:
return # No changes needed
- output_file.write_text(new_content)
\ No newline at end of file
+ output_file.write_text(new_content)
diff --git a/src/mdxify/renderers.py b/src/mdxify/renderers.py
new file mode 100644
index 0000000..d848d97
--- /dev/null
+++ b/src/mdxify/renderers.py
@@ -0,0 +1,63 @@
+"""Renderers for different output formats (MDX, Markdown)."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Optional
+
+from .formatter import escape_mdx_content
+from .source_links import add_source_link_to_header
+
+
+@dataclass(frozen=True)
+class Renderer:
+ name: str
+ file_extension: str
+
+ def escape(self, content: str) -> str: # pragma: no cover - simple passthroughs
+ return content
+
+ def header_with_source(self, header: str, source_link: Optional[str]) -> str:
+ return header
+
+ def frontmatter(self, title: str, sidebar_title: Optional[str] = None) -> list[str]:
+ lines = ["---", f"title: {title}"]
+ if sidebar_title:
+ lines.append(f"sidebarTitle: {sidebar_title}")
+ lines.append("---")
+ lines.append("")
+ return lines
+
+
+class MDXRenderer(Renderer):
+ def __init__(self) -> None:
+ super().__init__(name="mdx", file_extension="mdx")
+
+ def escape(self, content: str) -> str:
+ return escape_mdx_content(content)
+
+ def header_with_source(self, header: str, source_link: Optional[str]) -> str:
+ return add_source_link_to_header(header, source_link)
+
+
+class MarkdownRenderer(Renderer):
+ def __init__(self) -> None:
+ super().__init__(name="md", file_extension="md")
+
+ def escape(self, content: str) -> str:
+ # Markdown needs minimal escaping; pass through content as-is.
+ return content
+
+ def header_with_source(self, header: str, source_link: Optional[str]) -> str:
+ if not source_link:
+ return header
+ # Append a plain markdown link after the header
+ return f"{header} [source]({source_link})"
+
+
+def get_renderer(name: str) -> Renderer:
+ name = (name or "mdx").lower()
+ if name == "md":
+ return MarkdownRenderer()
+ return MDXRenderer()
+