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() +