code2docs 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code2docs/__init__.py +32 -0
- code2docs/__main__.py +6 -0
- code2docs/analyzers/__init__.py +13 -0
- code2docs/analyzers/dependency_scanner.py +159 -0
- code2docs/analyzers/docstring_extractor.py +111 -0
- code2docs/analyzers/endpoint_detector.py +116 -0
- code2docs/analyzers/project_scanner.py +45 -0
- code2docs/cli.py +226 -0
- code2docs/config.py +158 -0
- code2docs/formatters/__init__.py +7 -0
- code2docs/formatters/badges.py +52 -0
- code2docs/formatters/markdown.py +73 -0
- code2docs/formatters/toc.py +63 -0
- code2docs/generators/__init__.py +42 -0
- code2docs/generators/api_reference_gen.py +150 -0
- code2docs/generators/architecture_gen.py +192 -0
- code2docs/generators/changelog_gen.py +121 -0
- code2docs/generators/examples_gen.py +194 -0
- code2docs/generators/module_docs_gen.py +204 -0
- code2docs/generators/readme_gen.py +229 -0
- code2docs/sync/__init__.py +6 -0
- code2docs/sync/differ.py +125 -0
- code2docs/sync/updater.py +77 -0
- code2docs/sync/watcher.py +75 -0
- code2docs/templates/api_module.md.j2 +62 -0
- code2docs/templates/architecture.md.j2 +45 -0
- code2docs/templates/example_usage.py.j2 +12 -0
- code2docs/templates/index.md.j2 +31 -0
- code2docs/templates/readme.md.j2 +85 -0
- code2docs-0.1.1.dist-info/METADATA +228 -0
- code2docs-0.1.1.dist-info/RECORD +35 -0
- code2docs-0.1.1.dist-info/WHEEL +5 -0
- code2docs-0.1.1.dist-info/entry_points.txt +2 -0
- code2docs-0.1.1.dist-info/licenses/LICENSE +201 -0
- code2docs-0.1.1.dist-info/top_level.txt +1 -0
code2docs/cli.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""CLI interface for code2docs."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .config import Code2DocsConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group(invoke_without_command=True)
|
|
13
|
+
@click.argument("project_path", default=".", type=click.Path(exists=True))
|
|
14
|
+
@click.option("--config", "-c", "config_path", default=None, help="Path to code2docs.yaml")
|
|
15
|
+
@click.option("--readme-only", is_flag=True, help="Generate only README.md")
|
|
16
|
+
@click.option("--sections", "-s", default=None, help="Comma-separated sections to generate")
|
|
17
|
+
@click.option("--output", "-o", default=None, help="Output directory for docs")
|
|
18
|
+
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
|
19
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be generated without writing")
|
|
20
|
+
@click.pass_context
|
|
21
|
+
def main(ctx, project_path, config_path, readme_only, sections, output, verbose, dry_run):
|
|
22
|
+
"""code2docs — Auto-generate project documentation from source code.
|
|
23
|
+
|
|
24
|
+
Analyzes PROJECT_PATH using code2llm and generates human-readable documentation.
|
|
25
|
+
"""
|
|
26
|
+
if ctx.invoked_subcommand is not None:
|
|
27
|
+
ctx.ensure_object(dict)
|
|
28
|
+
ctx.obj["project_path"] = project_path
|
|
29
|
+
ctx.obj["config_path"] = config_path
|
|
30
|
+
ctx.obj["verbose"] = verbose
|
|
31
|
+
ctx.obj["dry_run"] = dry_run
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
# Default action: full generation
|
|
35
|
+
config = _load_config(project_path, config_path)
|
|
36
|
+
if verbose:
|
|
37
|
+
config.verbose = True
|
|
38
|
+
if output:
|
|
39
|
+
config.output = output
|
|
40
|
+
if sections:
|
|
41
|
+
config.readme.sections = [s.strip() for s in sections.split(",")]
|
|
42
|
+
|
|
43
|
+
_run_generate(project_path, config, readme_only=readme_only, dry_run=dry_run)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@main.command()
|
|
47
|
+
@click.argument("project_path", default=".", type=click.Path(exists=True))
|
|
48
|
+
@click.option("--config", "-c", "config_path", default=None, help="Path to code2docs.yaml")
|
|
49
|
+
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
|
50
|
+
@click.option("--dry-run", is_flag=True, help="Show what would change without writing")
|
|
51
|
+
def sync(project_path, config_path, verbose, dry_run):
|
|
52
|
+
"""Synchronize documentation with source code changes."""
|
|
53
|
+
config = _load_config(project_path, config_path)
|
|
54
|
+
if verbose:
|
|
55
|
+
config.verbose = True
|
|
56
|
+
|
|
57
|
+
_run_sync(project_path, config, dry_run=dry_run)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@main.command()
|
|
61
|
+
@click.argument("project_path", default=".", type=click.Path(exists=True))
|
|
62
|
+
@click.option("--config", "-c", "config_path", default=None, help="Path to code2docs.yaml")
|
|
63
|
+
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
|
64
|
+
def watch(project_path, config_path, verbose):
|
|
65
|
+
"""Watch for file changes and auto-regenerate docs."""
|
|
66
|
+
config = _load_config(project_path, config_path)
|
|
67
|
+
if verbose:
|
|
68
|
+
config.verbose = True
|
|
69
|
+
|
|
70
|
+
_run_watch(project_path, config)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@main.command()
|
|
74
|
+
@click.argument("project_path", default=".", type=click.Path(exists=True))
|
|
75
|
+
@click.option("--output", "-o", default="code2docs.yaml", help="Output config file path")
|
|
76
|
+
def init(project_path, output):
|
|
77
|
+
"""Initialize code2docs.yaml configuration file."""
|
|
78
|
+
project_path = Path(project_path).resolve()
|
|
79
|
+
config = Code2DocsConfig(
|
|
80
|
+
project_name=project_path.name,
|
|
81
|
+
source="./",
|
|
82
|
+
)
|
|
83
|
+
out_path = project_path / output
|
|
84
|
+
config.to_yaml(str(out_path))
|
|
85
|
+
click.echo(f"Created {out_path}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _load_config(project_path: str, config_path: Optional[str] = None) -> Code2DocsConfig:
|
|
89
|
+
"""Load configuration, auto-detecting code2docs.yaml if present."""
|
|
90
|
+
project = Path(project_path).resolve()
|
|
91
|
+
|
|
92
|
+
if config_path:
|
|
93
|
+
return Code2DocsConfig.from_yaml(config_path)
|
|
94
|
+
|
|
95
|
+
# Auto-detect
|
|
96
|
+
for name in ("code2docs.yaml", "code2docs.yml"):
|
|
97
|
+
candidate = project / name
|
|
98
|
+
if candidate.exists():
|
|
99
|
+
return Code2DocsConfig.from_yaml(str(candidate))
|
|
100
|
+
|
|
101
|
+
# Defaults
|
|
102
|
+
config = Code2DocsConfig(project_name=project.name, source="./")
|
|
103
|
+
return config
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _run_generate(project_path: str, config: Code2DocsConfig,
|
|
107
|
+
readme_only: bool = False, dry_run: bool = False):
|
|
108
|
+
"""Run full documentation generation."""
|
|
109
|
+
from .analyzers.project_scanner import ProjectScanner
|
|
110
|
+
from .generators.readme_gen import ReadmeGenerator
|
|
111
|
+
from .generators.api_reference_gen import ApiReferenceGenerator
|
|
112
|
+
from .generators.module_docs_gen import ModuleDocsGenerator
|
|
113
|
+
from .generators.examples_gen import ExamplesGenerator
|
|
114
|
+
from .generators.architecture_gen import ArchitectureGenerator
|
|
115
|
+
|
|
116
|
+
project = Path(project_path).resolve()
|
|
117
|
+
click.echo(f"📖 code2docs: analyzing {project.name}...")
|
|
118
|
+
|
|
119
|
+
# Step 1: Analyze
|
|
120
|
+
scanner = ProjectScanner(config)
|
|
121
|
+
result = scanner.analyze(str(project))
|
|
122
|
+
|
|
123
|
+
if config.verbose:
|
|
124
|
+
click.echo(f" Functions: {len(result.functions)}")
|
|
125
|
+
click.echo(f" Classes: {len(result.classes)}")
|
|
126
|
+
click.echo(f" Modules: {len(result.modules)}")
|
|
127
|
+
|
|
128
|
+
# Step 2: Generate README
|
|
129
|
+
readme_gen = ReadmeGenerator(config, result)
|
|
130
|
+
readme_content = readme_gen.generate()
|
|
131
|
+
|
|
132
|
+
if dry_run:
|
|
133
|
+
click.echo(f"\n--- README.md ({len(readme_content)} chars) ---")
|
|
134
|
+
click.echo(readme_content[:500] + "..." if len(readme_content) > 500 else readme_content)
|
|
135
|
+
else:
|
|
136
|
+
readme_path = project / config.readme_output
|
|
137
|
+
readme_gen.write(str(readme_path), readme_content)
|
|
138
|
+
click.echo(f" ✅ {readme_path.relative_to(project)}")
|
|
139
|
+
|
|
140
|
+
if readme_only:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
# Step 3: Generate docs/
|
|
144
|
+
docs_dir = project / config.output
|
|
145
|
+
docs_dir.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
|
|
147
|
+
if config.docs.api_reference:
|
|
148
|
+
api_gen = ApiReferenceGenerator(config, result)
|
|
149
|
+
files = api_gen.generate_all()
|
|
150
|
+
if not dry_run:
|
|
151
|
+
api_gen.write_all(str(docs_dir / "api"), files)
|
|
152
|
+
click.echo(f" ✅ docs/api/ ({len(files)} files)")
|
|
153
|
+
else:
|
|
154
|
+
click.echo(f" [dry-run] docs/api/ ({len(files)} files)")
|
|
155
|
+
|
|
156
|
+
if config.docs.module_docs:
|
|
157
|
+
mod_gen = ModuleDocsGenerator(config, result)
|
|
158
|
+
files = mod_gen.generate_all()
|
|
159
|
+
if not dry_run:
|
|
160
|
+
mod_gen.write_all(str(docs_dir / "modules"), files)
|
|
161
|
+
click.echo(f" ✅ docs/modules/ ({len(files)} files)")
|
|
162
|
+
else:
|
|
163
|
+
click.echo(f" [dry-run] docs/modules/ ({len(files)} files)")
|
|
164
|
+
|
|
165
|
+
if config.docs.architecture:
|
|
166
|
+
arch_gen = ArchitectureGenerator(config, result)
|
|
167
|
+
content = arch_gen.generate()
|
|
168
|
+
if not dry_run:
|
|
169
|
+
arch_path = docs_dir / "architecture.md"
|
|
170
|
+
arch_path.write_text(content, encoding="utf-8")
|
|
171
|
+
click.echo(f" ✅ docs/architecture.md")
|
|
172
|
+
else:
|
|
173
|
+
click.echo(f" [dry-run] docs/architecture.md")
|
|
174
|
+
|
|
175
|
+
# Step 4: Generate examples/
|
|
176
|
+
if config.examples.auto_generate:
|
|
177
|
+
ex_gen = ExamplesGenerator(config, result)
|
|
178
|
+
files = ex_gen.generate_all()
|
|
179
|
+
if not dry_run:
|
|
180
|
+
examples_dir = project / "examples"
|
|
181
|
+
ex_gen.write_all(str(examples_dir), files)
|
|
182
|
+
click.echo(f" ✅ examples/ ({len(files)} files)")
|
|
183
|
+
else:
|
|
184
|
+
click.echo(f" [dry-run] examples/ ({len(files)} files)")
|
|
185
|
+
|
|
186
|
+
click.echo("📖 Done!")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _run_sync(project_path: str, config: Code2DocsConfig, dry_run: bool = False):
|
|
190
|
+
"""Run sync — regenerate only changed documentation."""
|
|
191
|
+
from .sync.differ import Differ
|
|
192
|
+
from .sync.updater import Updater
|
|
193
|
+
|
|
194
|
+
project = Path(project_path).resolve()
|
|
195
|
+
click.echo(f"🔄 code2docs sync: {project.name}...")
|
|
196
|
+
|
|
197
|
+
differ = Differ(config)
|
|
198
|
+
changes = differ.detect_changes(str(project))
|
|
199
|
+
|
|
200
|
+
if not changes:
|
|
201
|
+
click.echo(" No changes detected.")
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
click.echo(f" Changes detected in {len(changes)} modules")
|
|
205
|
+
|
|
206
|
+
if dry_run:
|
|
207
|
+
for change in changes:
|
|
208
|
+
click.echo(f" - {change}")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
updater = Updater(config)
|
|
212
|
+
updater.apply(str(project), changes)
|
|
213
|
+
click.echo("🔄 Sync complete!")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _run_watch(project_path: str, config: Code2DocsConfig):
|
|
217
|
+
"""Run file watcher for auto-resync."""
|
|
218
|
+
try:
|
|
219
|
+
from .sync.watcher import start_watcher
|
|
220
|
+
except ImportError:
|
|
221
|
+
click.echo("Error: watchdog not installed. Install with: pip install code2docs[watch]")
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
|
|
224
|
+
project = Path(project_path).resolve()
|
|
225
|
+
click.echo(f"👁 Watching {project.name} for changes... (Ctrl+C to stop)")
|
|
226
|
+
start_watcher(str(project), config)
|
code2docs/config.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Configuration for code2docs documentation generation."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ReadmeConfig:
|
|
12
|
+
"""Configuration for README generation."""
|
|
13
|
+
sections: List[str] = field(default_factory=lambda: [
|
|
14
|
+
"overview", "install", "quickstart", "api", "structure", "endpoints",
|
|
15
|
+
])
|
|
16
|
+
badges: List[str] = field(default_factory=lambda: [
|
|
17
|
+
"version", "python", "coverage", "complexity",
|
|
18
|
+
])
|
|
19
|
+
sync_markers: bool = True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DocsConfig:
|
|
24
|
+
"""Configuration for docs/ generation."""
|
|
25
|
+
api_reference: bool = True
|
|
26
|
+
module_docs: bool = True
|
|
27
|
+
architecture: bool = True
|
|
28
|
+
changelog: bool = True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ExamplesConfig:
|
|
33
|
+
"""Configuration for examples/ generation."""
|
|
34
|
+
auto_generate: bool = True
|
|
35
|
+
from_entry_points: bool = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class SyncConfig:
|
|
40
|
+
"""Configuration for synchronization."""
|
|
41
|
+
strategy: str = "markers" # markers | full | git-diff
|
|
42
|
+
watch: bool = False
|
|
43
|
+
ignore: List[str] = field(default_factory=lambda: ["tests/", "__pycache__"])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Code2DocsConfig:
|
|
48
|
+
"""Main configuration for code2docs."""
|
|
49
|
+
project_name: str = ""
|
|
50
|
+
source: str = "./"
|
|
51
|
+
output: str = "./docs/"
|
|
52
|
+
readme_output: str = "./README.md"
|
|
53
|
+
|
|
54
|
+
readme: ReadmeConfig = field(default_factory=ReadmeConfig)
|
|
55
|
+
docs: DocsConfig = field(default_factory=DocsConfig)
|
|
56
|
+
examples: ExamplesConfig = field(default_factory=ExamplesConfig)
|
|
57
|
+
sync: SyncConfig = field(default_factory=SyncConfig)
|
|
58
|
+
|
|
59
|
+
# code2llm analysis options
|
|
60
|
+
verbose: bool = False
|
|
61
|
+
exclude_tests: bool = True
|
|
62
|
+
skip_private: bool = False
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_yaml(cls, path: str) -> "Code2DocsConfig":
|
|
66
|
+
"""Load configuration from code2docs.yaml."""
|
|
67
|
+
config_path = Path(path)
|
|
68
|
+
if not config_path.exists():
|
|
69
|
+
return cls()
|
|
70
|
+
|
|
71
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
72
|
+
data = yaml.safe_load(f) or {}
|
|
73
|
+
|
|
74
|
+
config = cls()
|
|
75
|
+
|
|
76
|
+
# Project-level settings
|
|
77
|
+
project = data.get("project", {})
|
|
78
|
+
config.project_name = project.get("name", "")
|
|
79
|
+
config.source = project.get("source", "./")
|
|
80
|
+
config.output = project.get("output", "./docs/")
|
|
81
|
+
config.readme_output = project.get("readme_output", "./README.md")
|
|
82
|
+
config.verbose = project.get("verbose", False)
|
|
83
|
+
config.exclude_tests = project.get("exclude_tests", True)
|
|
84
|
+
config.skip_private = project.get("skip_private", False)
|
|
85
|
+
|
|
86
|
+
# Readme config
|
|
87
|
+
readme_data = data.get("readme", {})
|
|
88
|
+
if readme_data:
|
|
89
|
+
config.readme = ReadmeConfig(
|
|
90
|
+
sections=readme_data.get("sections", config.readme.sections),
|
|
91
|
+
badges=readme_data.get("badges", config.readme.badges),
|
|
92
|
+
sync_markers=readme_data.get("sync_markers", True),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Docs config
|
|
96
|
+
docs_data = data.get("docs", {})
|
|
97
|
+
if docs_data:
|
|
98
|
+
config.docs = DocsConfig(
|
|
99
|
+
api_reference=docs_data.get("api_reference", True),
|
|
100
|
+
module_docs=docs_data.get("module_docs", True),
|
|
101
|
+
architecture=docs_data.get("architecture", True),
|
|
102
|
+
changelog=docs_data.get("changelog", True),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Examples config
|
|
106
|
+
examples_data = data.get("examples", {})
|
|
107
|
+
if examples_data:
|
|
108
|
+
config.examples = ExamplesConfig(
|
|
109
|
+
auto_generate=examples_data.get("auto_generate", True),
|
|
110
|
+
from_entry_points=examples_data.get("from_entry_points", True),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Sync config
|
|
114
|
+
sync_data = data.get("sync", {})
|
|
115
|
+
if sync_data:
|
|
116
|
+
config.sync = SyncConfig(
|
|
117
|
+
strategy=sync_data.get("strategy", "markers"),
|
|
118
|
+
watch=sync_data.get("watch", False),
|
|
119
|
+
ignore=sync_data.get("ignore", ["tests/", "__pycache__"]),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return config
|
|
123
|
+
|
|
124
|
+
def to_yaml(self, path: str) -> None:
|
|
125
|
+
"""Save configuration to YAML file."""
|
|
126
|
+
data = {
|
|
127
|
+
"project": {
|
|
128
|
+
"name": self.project_name,
|
|
129
|
+
"source": self.source,
|
|
130
|
+
"output": self.output,
|
|
131
|
+
"readme_output": self.readme_output,
|
|
132
|
+
"verbose": self.verbose,
|
|
133
|
+
"exclude_tests": self.exclude_tests,
|
|
134
|
+
"skip_private": self.skip_private,
|
|
135
|
+
},
|
|
136
|
+
"readme": {
|
|
137
|
+
"sections": self.readme.sections,
|
|
138
|
+
"badges": self.readme.badges,
|
|
139
|
+
"sync_markers": self.readme.sync_markers,
|
|
140
|
+
},
|
|
141
|
+
"docs": {
|
|
142
|
+
"api_reference": self.docs.api_reference,
|
|
143
|
+
"module_docs": self.docs.module_docs,
|
|
144
|
+
"architecture": self.docs.architecture,
|
|
145
|
+
"changelog": self.docs.changelog,
|
|
146
|
+
},
|
|
147
|
+
"examples": {
|
|
148
|
+
"auto_generate": self.examples.auto_generate,
|
|
149
|
+
"from_entry_points": self.examples.from_entry_points,
|
|
150
|
+
},
|
|
151
|
+
"sync": {
|
|
152
|
+
"strategy": self.sync.strategy,
|
|
153
|
+
"watch": self.sync.watch,
|
|
154
|
+
"ignore": self.sync.ignore,
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
158
|
+
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Badge generation using shields.io URLs."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def generate_badges(project_name: str, badge_types: List[str],
|
|
8
|
+
stats: Dict = None, deps=None) -> str:
|
|
9
|
+
"""Generate shields.io badge Markdown strings."""
|
|
10
|
+
stats = stats or {}
|
|
11
|
+
badges: List[str] = []
|
|
12
|
+
|
|
13
|
+
for badge_type in badge_types:
|
|
14
|
+
badge = _make_badge(badge_type, project_name, stats, deps)
|
|
15
|
+
if badge:
|
|
16
|
+
badges.append(badge)
|
|
17
|
+
|
|
18
|
+
return " ".join(badges)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _make_badge(badge_type: str, project_name: str,
|
|
22
|
+
stats: Dict, deps) -> Optional[str]:
|
|
23
|
+
"""Create a single badge Markdown string."""
|
|
24
|
+
name = quote(project_name)
|
|
25
|
+
|
|
26
|
+
if badge_type == "version":
|
|
27
|
+
return f""
|
|
28
|
+
|
|
29
|
+
elif badge_type == "python":
|
|
30
|
+
py_version = ""
|
|
31
|
+
if deps and hasattr(deps, "python_version"):
|
|
32
|
+
py_version = deps.python_version
|
|
33
|
+
py_version = py_version or ">=3.9"
|
|
34
|
+
py_safe = quote(py_version)
|
|
35
|
+
return f""
|
|
36
|
+
|
|
37
|
+
elif badge_type == "coverage":
|
|
38
|
+
return f""
|
|
39
|
+
|
|
40
|
+
elif badge_type == "complexity":
|
|
41
|
+
funcs = stats.get("functions_found", 0)
|
|
42
|
+
if funcs:
|
|
43
|
+
return f""
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
elif badge_type == "license":
|
|
47
|
+
return f""
|
|
48
|
+
|
|
49
|
+
elif badge_type == "docs":
|
|
50
|
+
return f""
|
|
51
|
+
|
|
52
|
+
return None
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Markdown formatting utilities."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MarkdownFormatter:
|
|
7
|
+
"""Helper for constructing Markdown documents."""
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.lines: List[str] = []
|
|
11
|
+
|
|
12
|
+
def heading(self, text: str, level: int = 1) -> "MarkdownFormatter":
|
|
13
|
+
"""Add a heading."""
|
|
14
|
+
self.lines.append(f"{'#' * level} {text}\n")
|
|
15
|
+
return self
|
|
16
|
+
|
|
17
|
+
def paragraph(self, text: str) -> "MarkdownFormatter":
|
|
18
|
+
"""Add a paragraph."""
|
|
19
|
+
self.lines.append(f"{text}\n")
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def blockquote(self, text: str) -> "MarkdownFormatter":
|
|
23
|
+
"""Add a blockquote."""
|
|
24
|
+
self.lines.append(f"> {text}\n")
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
def code_block(self, code: str, language: str = "") -> "MarkdownFormatter":
|
|
28
|
+
"""Add a fenced code block."""
|
|
29
|
+
self.lines.append(f"```{language}")
|
|
30
|
+
self.lines.append(code)
|
|
31
|
+
self.lines.append("```\n")
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def inline_code(self, text: str) -> str:
|
|
35
|
+
"""Return inline code string."""
|
|
36
|
+
return f"`{text}`"
|
|
37
|
+
|
|
38
|
+
def bold(self, text: str) -> str:
|
|
39
|
+
"""Return bold string."""
|
|
40
|
+
return f"**{text}**"
|
|
41
|
+
|
|
42
|
+
def link(self, text: str, url: str) -> str:
|
|
43
|
+
"""Return a Markdown link."""
|
|
44
|
+
return f"[{text}]({url})"
|
|
45
|
+
|
|
46
|
+
def list_item(self, text: str, indent: int = 0) -> "MarkdownFormatter":
|
|
47
|
+
"""Add a list item."""
|
|
48
|
+
prefix = " " * indent
|
|
49
|
+
self.lines.append(f"{prefix}- {text}")
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def table(self, headers: List[str], rows: List[List[str]]) -> "MarkdownFormatter":
|
|
53
|
+
"""Add a Markdown table."""
|
|
54
|
+
self.lines.append("| " + " | ".join(headers) + " |")
|
|
55
|
+
self.lines.append("| " + " | ".join("---" for _ in headers) + " |")
|
|
56
|
+
for row in rows:
|
|
57
|
+
self.lines.append("| " + " | ".join(str(c) for c in row) + " |")
|
|
58
|
+
self.lines.append("")
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def separator(self) -> "MarkdownFormatter":
|
|
62
|
+
"""Add a horizontal rule."""
|
|
63
|
+
self.lines.append("---\n")
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def blank(self) -> "MarkdownFormatter":
|
|
67
|
+
"""Add a blank line."""
|
|
68
|
+
self.lines.append("")
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def render(self) -> str:
|
|
72
|
+
"""Render accumulated Markdown to string."""
|
|
73
|
+
return "\n".join(self.lines)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Table of contents generator from Markdown headings."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def generate_toc(markdown_content: str, max_depth: int = 3) -> str:
|
|
8
|
+
"""Generate a table of contents from Markdown headings.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
markdown_content: Full Markdown document.
|
|
12
|
+
max_depth: Maximum heading level to include (1-6).
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
TOC as Markdown string.
|
|
16
|
+
"""
|
|
17
|
+
headings = extract_headings(markdown_content, max_depth)
|
|
18
|
+
if not headings:
|
|
19
|
+
return ""
|
|
20
|
+
|
|
21
|
+
lines = ["## Table of Contents\n"]
|
|
22
|
+
for level, text, anchor in headings:
|
|
23
|
+
indent = " " * (level - 1)
|
|
24
|
+
lines.append(f"{indent}- [{text}](#{anchor})")
|
|
25
|
+
|
|
26
|
+
lines.append("")
|
|
27
|
+
return "\n".join(lines)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extract_headings(content: str, max_depth: int = 3) -> List[Tuple[int, str, str]]:
|
|
31
|
+
"""Extract headings from Markdown content.
|
|
32
|
+
|
|
33
|
+
Returns list of (level, text, anchor) tuples.
|
|
34
|
+
"""
|
|
35
|
+
headings: List[Tuple[int, str, str]] = []
|
|
36
|
+
in_code_block = False
|
|
37
|
+
|
|
38
|
+
for line in content.splitlines():
|
|
39
|
+
# Skip code blocks
|
|
40
|
+
if line.strip().startswith("```"):
|
|
41
|
+
in_code_block = not in_code_block
|
|
42
|
+
continue
|
|
43
|
+
if in_code_block:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
match = re.match(r'^(#{1,6})\s+(.+)$', line)
|
|
47
|
+
if match:
|
|
48
|
+
level = len(match.group(1))
|
|
49
|
+
if level <= max_depth:
|
|
50
|
+
text = match.group(2).strip()
|
|
51
|
+
anchor = _slugify(text)
|
|
52
|
+
headings.append((level, text, anchor))
|
|
53
|
+
|
|
54
|
+
return headings
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _slugify(text: str) -> str:
|
|
58
|
+
"""Convert heading text to GitHub-compatible anchor slug."""
|
|
59
|
+
slug = text.lower()
|
|
60
|
+
slug = re.sub(r'[^\w\s-]', '', slug)
|
|
61
|
+
slug = re.sub(r'[\s]+', '-', slug)
|
|
62
|
+
slug = slug.strip('-')
|
|
63
|
+
return slug
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Documentation generators — produce Markdown, examples, and diagrams."""
|
|
2
|
+
|
|
3
|
+
from .readme_gen import ReadmeGenerator
|
|
4
|
+
from .api_reference_gen import ApiReferenceGenerator
|
|
5
|
+
from .module_docs_gen import ModuleDocsGenerator
|
|
6
|
+
from .examples_gen import ExamplesGenerator
|
|
7
|
+
from .architecture_gen import ArchitectureGenerator
|
|
8
|
+
from .changelog_gen import ChangelogGenerator
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ReadmeGenerator",
|
|
12
|
+
"ApiReferenceGenerator",
|
|
13
|
+
"ModuleDocsGenerator",
|
|
14
|
+
"ExamplesGenerator",
|
|
15
|
+
"ArchitectureGenerator",
|
|
16
|
+
"ChangelogGenerator",
|
|
17
|
+
"generate_docs",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_docs(project_path: str, config=None):
|
|
22
|
+
"""High-level function to generate all documentation."""
|
|
23
|
+
from ..analyzers.project_scanner import ProjectScanner
|
|
24
|
+
from ..config import Code2DocsConfig
|
|
25
|
+
|
|
26
|
+
config = config or Code2DocsConfig()
|
|
27
|
+
scanner = ProjectScanner(config)
|
|
28
|
+
result = scanner.analyze(project_path)
|
|
29
|
+
|
|
30
|
+
docs = {}
|
|
31
|
+
docs["readme"] = ReadmeGenerator(config, result).generate()
|
|
32
|
+
|
|
33
|
+
if config.docs.api_reference:
|
|
34
|
+
docs["api"] = ApiReferenceGenerator(config, result).generate_all()
|
|
35
|
+
|
|
36
|
+
if config.docs.module_docs:
|
|
37
|
+
docs["modules"] = ModuleDocsGenerator(config, result).generate_all()
|
|
38
|
+
|
|
39
|
+
if config.docs.architecture:
|
|
40
|
+
docs["architecture"] = ArchitectureGenerator(config, result).generate()
|
|
41
|
+
|
|
42
|
+
return docs
|