code2docs 3.0.29__tar.gz → 3.0.33__tar.gz
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-3.0.29 → code2docs-3.0.33}/PKG-INFO +8 -8
- {code2docs-3.0.29 → code2docs-3.0.33}/README.md +7 -7
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/__init__.py +1 -1
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/analyzers/__init__.py +10 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/analyzers/dependency_scanner.py +26 -1
- code2docs-3.0.33/code2docs/analyzers/markdown_validator.py +194 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/cli.py +30 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/config.py +1 -1
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/formatters/badges.py +20 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/examples_gen.py +6 -34
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/readme_gen.py +44 -8
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/templates/readme.md.j2 +108 -27
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs.egg-info/PKG-INFO +8 -8
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs.egg-info/SOURCES.txt +2 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/pyproject.toml +1 -1
- code2docs-3.0.33/tests/test_markdown_validator.py +75 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/LICENSE +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/__main__.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/analyzers/docstring_extractor.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/analyzers/endpoint_detector.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/analyzers/project_scanner.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/base.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/examples/advanced_usage.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/examples/quickstart.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/formatters/__init__.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/formatters/markdown.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/formatters/toc.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/__init__.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/_registry_adapters.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/_source_links.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/api_changelog_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/api_reference_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/architecture_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/changelog_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/code2llm_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/config_docs_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/contributing_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/coverage_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/depgraph_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/getting_started_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/mkdocs_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/module_docs_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/generators/org_readme_gen.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/llm_helper.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/registry.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/sync/__init__.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/sync/differ.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/sync/updater.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/sync/watcher.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/templates/api_module.md.j2 +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/templates/architecture.md.j2 +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/templates/example_usage.py.j2 +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/templates/index.md.j2 +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs/templates/module_doc.md.j2 +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs.egg-info/dependency_links.txt +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs.egg-info/entry_points.txt +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs.egg-info/requires.txt +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/code2docs.egg-info/top_level.txt +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/setup.cfg +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/tests/test_analyzers.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/tests/test_cli.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/tests/test_code2docs.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/tests/test_config.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/tests/test_formatters.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/tests/test_generators.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/tests/test_llm_helper.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/tests/test_registry.py +0 -0
- {code2docs-3.0.29 → code2docs-3.0.33}/tests/test_sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code2docs
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.33
|
|
4
4
|
Summary: Auto-generate and sync project documentation from source code analysis
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -53,17 +53,17 @@ Dynamic: license-file
|
|
|
53
53
|
|
|
54
54
|
## AI Cost Tracking
|
|
55
55
|
|
|
56
|
-
     
|
|
57
|
+
  
|
|
58
58
|
|
|
59
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
60
|
-
- 👤 **Human dev:** ~$
|
|
59
|
+
- 🤖 **LLM usage:** $7.5000 (73 commits)
|
|
60
|
+
- 👤 **Human dev:** ~$2534 (25.3h @ $100/h, 30min dedup)
|
|
61
61
|
|
|
62
|
-
Generated on 2026-04-
|
|
62
|
+
Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
|
66
|
-
  
|
|
67
67
|
|
|
68
68
|
> Auto-generate and sync project documentation from source code analysis.
|
|
69
69
|
|
|
@@ -200,7 +200,7 @@ code2docs can update only specific sections of an existing README using markers:
|
|
|
200
200
|
```markdown
|
|
201
201
|
<!-- code2docs:start --># code2docs
|
|
202
202
|
|
|
203
|
-
   
|
|
204
204
|
> **276** functions | **57** classes | **51** files | CC̄ = 3.8
|
|
205
205
|
|
|
206
206
|
> Auto-generated project documentation from source code analysis.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
## AI Cost Tracking
|
|
2
2
|
|
|
3
|
-
     
|
|
4
|
+
  
|
|
5
5
|
|
|
6
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
7
|
-
- 👤 **Human dev:** ~$
|
|
6
|
+
- 🤖 **LLM usage:** $7.5000 (73 commits)
|
|
7
|
+
- 👤 **Human dev:** ~$2534 (25.3h @ $100/h, 30min dedup)
|
|
8
8
|
|
|
9
|
-
Generated on 2026-04-
|
|
9
|
+
Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
  
|
|
14
14
|
|
|
15
15
|
> Auto-generate and sync project documentation from source code analysis.
|
|
16
16
|
|
|
@@ -147,7 +147,7 @@ code2docs can update only specific sections of an existing README using markers:
|
|
|
147
147
|
```markdown
|
|
148
148
|
<!-- code2docs:start --># code2docs
|
|
149
149
|
|
|
150
|
-
   
|
|
151
151
|
> **276** functions | **57** classes | **51** files | CC̄ = 3.8
|
|
152
152
|
|
|
153
153
|
> Auto-generated project documentation from source code analysis.
|
|
@@ -5,7 +5,7 @@ Uses code2llm's AnalysisResult to produce human-readable documentation:
|
|
|
5
5
|
README.md, API references, module docs, examples, and architecture diagrams.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = '3.0.
|
|
8
|
+
__version__ = '3.0.33'
|
|
9
9
|
__author__ = 'Tom Sapletta'
|
|
10
10
|
__all__ = ['Code2DocsConfig', 'generate_readme', 'generate_docs', 'analyze_and_document']
|
|
11
11
|
|
|
@@ -4,10 +4,20 @@ from .project_scanner import ProjectScanner
|
|
|
4
4
|
from .endpoint_detector import EndpointDetector
|
|
5
5
|
from .docstring_extractor import DocstringExtractor
|
|
6
6
|
from .dependency_scanner import DependencyScanner
|
|
7
|
+
from .markdown_validator import (
|
|
8
|
+
MarkdownIssue,
|
|
9
|
+
ValidationReport,
|
|
10
|
+
validate_markdown_file,
|
|
11
|
+
validate_markdown_tree,
|
|
12
|
+
)
|
|
7
13
|
|
|
8
14
|
__all__ = [
|
|
9
15
|
"ProjectScanner",
|
|
10
16
|
"EndpointDetector",
|
|
11
17
|
"DocstringExtractor",
|
|
12
18
|
"DependencyScanner",
|
|
19
|
+
"MarkdownIssue",
|
|
20
|
+
"ValidationReport",
|
|
21
|
+
"validate_markdown_file",
|
|
22
|
+
"validate_markdown_tree",
|
|
13
23
|
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Scan project dependencies from requirements.txt, pyproject.toml, setup.py, package.json, Cargo.toml, go.mod."""
|
|
1
|
+
"""Scan project dependencies from requirements.txt, pyproject.toml, setup.py, package.json, composer.json, Cargo.toml, go.mod."""
|
|
2
2
|
import re
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from pathlib import Path
|
|
@@ -50,6 +50,11 @@ class DependencyScanner:
|
|
|
50
50
|
if tsconfig.exists():
|
|
51
51
|
deps.language = 'typescript'
|
|
52
52
|
return deps
|
|
53
|
+
composer_json = project / 'composer.json'
|
|
54
|
+
if composer_json.exists():
|
|
55
|
+
deps = self._parse_composer_json(composer_json)
|
|
56
|
+
deps.source_file = 'composer.json'
|
|
57
|
+
return deps
|
|
53
58
|
cargo_toml = project / 'Cargo.toml'
|
|
54
59
|
if cargo_toml.exists():
|
|
55
60
|
deps = self._parse_cargo_toml(cargo_toml)
|
|
@@ -171,6 +176,26 @@ class DependencyScanner:
|
|
|
171
176
|
deps.keywords = data.get('keywords', [])
|
|
172
177
|
return deps
|
|
173
178
|
|
|
179
|
+
def _parse_composer_json(self, path: Path) -> ProjectDependencies:
|
|
180
|
+
"""Parse composer.json for PHP dependencies."""
|
|
181
|
+
import json
|
|
182
|
+
deps = ProjectDependencies(language='php')
|
|
183
|
+
try:
|
|
184
|
+
data = json.loads(path.read_text(encoding='utf-8'))
|
|
185
|
+
except (json.JSONDecodeError, OSError):
|
|
186
|
+
return deps
|
|
187
|
+
deps.version = data.get('version', '')
|
|
188
|
+
deps.keywords = data.get('keywords', []) or []
|
|
189
|
+
for dep_name, ver in (data.get('require', {}) or {}).items():
|
|
190
|
+
if dep_name.lower() == 'php':
|
|
191
|
+
deps.runtime_version = ver
|
|
192
|
+
continue
|
|
193
|
+
deps.dependencies.append(DependencyInfo(name=dep_name, version_spec=ver))
|
|
194
|
+
for dep_name, ver in (data.get('require-dev', {}) or {}).items():
|
|
195
|
+
deps.dev_dependencies.append(DependencyInfo(name=dep_name, version_spec=ver, group='dev'))
|
|
196
|
+
deps.install_command = 'composer install'
|
|
197
|
+
return deps
|
|
198
|
+
|
|
174
199
|
def _parse_cargo_toml(self, path: Path) -> ProjectDependencies:
|
|
175
200
|
"""Parse Cargo.toml for Rust dependencies."""
|
|
176
201
|
deps = ProjectDependencies(language='rust')
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Validate generated markdown: checks local links/images, table shape, duplicate headings."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterable, List, Optional
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
_LINK_RE = re.compile(r'!?\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)')
|
|
11
|
+
_HEADING_RE = re.compile(r'^(#{1,6})\s+(.+?)\s*#*\s*$')
|
|
12
|
+
_FENCE_RE = re.compile(r'^(```|~~~)')
|
|
13
|
+
_TABLE_SEP_RE = re.compile(r'^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class MarkdownIssue:
|
|
18
|
+
"""A single validation issue in a markdown file."""
|
|
19
|
+
file: str
|
|
20
|
+
line: int
|
|
21
|
+
kind: str
|
|
22
|
+
message: str
|
|
23
|
+
|
|
24
|
+
def __str__(self) -> str:
|
|
25
|
+
return f'{self.file}:{self.line} [{self.kind}] {self.message}'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ValidationReport:
|
|
30
|
+
"""Aggregate result of markdown validation."""
|
|
31
|
+
issues: List[MarkdownIssue] = field(default_factory=list)
|
|
32
|
+
files_checked: int = 0
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def ok(self) -> bool:
|
|
36
|
+
return not self.issues
|
|
37
|
+
|
|
38
|
+
def by_kind(self, kind: str) -> List[MarkdownIssue]:
|
|
39
|
+
return [i for i in self.issues if i.kind == kind]
|
|
40
|
+
|
|
41
|
+
def summary(self) -> str:
|
|
42
|
+
if self.ok:
|
|
43
|
+
return f'✅ {self.files_checked} file(s) checked, no issues'
|
|
44
|
+
kinds: dict = {}
|
|
45
|
+
for issue in self.issues:
|
|
46
|
+
kinds[issue.kind] = kinds.get(issue.kind, 0) + 1
|
|
47
|
+
parts = ', '.join(f'{k}={v}' for k, v in sorted(kinds.items()))
|
|
48
|
+
return f'❌ {len(self.issues)} issue(s) across {self.files_checked} file(s): {parts}'
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_markdown_file(md_path: Path, project_root: Optional[Path] = None) -> List[MarkdownIssue]:
|
|
52
|
+
"""Validate a single markdown file.
|
|
53
|
+
|
|
54
|
+
Checks:
|
|
55
|
+
- Local links/images resolve to existing files (skips URLs and in-page anchors).
|
|
56
|
+
- Tables have consistent column count.
|
|
57
|
+
- Duplicate headings at the same level are flagged.
|
|
58
|
+
"""
|
|
59
|
+
md_path = Path(md_path)
|
|
60
|
+
if not md_path.exists():
|
|
61
|
+
return [MarkdownIssue(str(md_path), 0, 'missing', 'file does not exist')]
|
|
62
|
+
root = Path(project_root) if project_root else md_path.parent
|
|
63
|
+
try:
|
|
64
|
+
lines = md_path.read_text(encoding='utf-8').splitlines()
|
|
65
|
+
except OSError as exc:
|
|
66
|
+
return [MarkdownIssue(str(md_path), 0, 'read_error', str(exc))]
|
|
67
|
+
issues: List[MarkdownIssue] = []
|
|
68
|
+
issues.extend(_check_links(md_path, root, lines))
|
|
69
|
+
issues.extend(_check_tables(md_path, lines))
|
|
70
|
+
issues.extend(_check_headings(md_path, lines))
|
|
71
|
+
return issues
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def validate_markdown_tree(root: Path, patterns: Iterable[str] = ('*.md',),
|
|
75
|
+
ignore: Iterable[str] = ('node_modules', 'venv', '.venv',
|
|
76
|
+
'vendor', '.git', '__pycache__')) -> ValidationReport:
|
|
77
|
+
"""Validate every markdown file under ``root`` matching ``patterns``."""
|
|
78
|
+
root = Path(root)
|
|
79
|
+
ignore_set = set(ignore)
|
|
80
|
+
report = ValidationReport()
|
|
81
|
+
seen: set = set()
|
|
82
|
+
for pattern in patterns:
|
|
83
|
+
for md_file in root.rglob(pattern):
|
|
84
|
+
if any(part in ignore_set for part in md_file.parts):
|
|
85
|
+
continue
|
|
86
|
+
if md_file in seen:
|
|
87
|
+
continue
|
|
88
|
+
seen.add(md_file)
|
|
89
|
+
report.files_checked += 1
|
|
90
|
+
report.issues.extend(validate_markdown_file(md_file, project_root=root))
|
|
91
|
+
return report
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _strip_code_fences(lines: List[str]) -> List[bool]:
|
|
95
|
+
"""Return a per-line boolean: True if the line is inside a fenced code block."""
|
|
96
|
+
inside = False
|
|
97
|
+
mask: List[bool] = []
|
|
98
|
+
for line in lines:
|
|
99
|
+
if _FENCE_RE.match(line.strip()):
|
|
100
|
+
mask.append(True)
|
|
101
|
+
inside = not inside
|
|
102
|
+
continue
|
|
103
|
+
mask.append(inside)
|
|
104
|
+
return mask
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _check_links(md_path: Path, root: Path, lines: List[str]) -> List[MarkdownIssue]:
|
|
108
|
+
issues: List[MarkdownIssue] = []
|
|
109
|
+
in_code = _strip_code_fences(lines)
|
|
110
|
+
for lineno, line in enumerate(lines, 1):
|
|
111
|
+
if in_code[lineno - 1]:
|
|
112
|
+
continue
|
|
113
|
+
for match in _LINK_RE.finditer(line):
|
|
114
|
+
target = match.group(2).strip()
|
|
115
|
+
if not target or target.startswith('#'):
|
|
116
|
+
continue
|
|
117
|
+
parsed = urlparse(target)
|
|
118
|
+
if parsed.scheme in ('http', 'https', 'mailto', 'ftp', 'tel', 'data'):
|
|
119
|
+
continue
|
|
120
|
+
if parsed.scheme and parsed.scheme not in ('file',):
|
|
121
|
+
continue
|
|
122
|
+
rel = parsed.path
|
|
123
|
+
if not rel:
|
|
124
|
+
continue
|
|
125
|
+
if rel.startswith('/'):
|
|
126
|
+
resolved = root / rel.lstrip('/')
|
|
127
|
+
else:
|
|
128
|
+
resolved = (md_path.parent / rel).resolve()
|
|
129
|
+
if not resolved.exists():
|
|
130
|
+
issues.append(MarkdownIssue(
|
|
131
|
+
str(md_path), lineno, 'broken_link',
|
|
132
|
+
f'link target not found: {target}'))
|
|
133
|
+
return issues
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _check_tables(md_path: Path, lines: List[str]) -> List[MarkdownIssue]:
|
|
137
|
+
issues: List[MarkdownIssue] = []
|
|
138
|
+
in_code = _strip_code_fences(lines)
|
|
139
|
+
i = 0
|
|
140
|
+
while i < len(lines):
|
|
141
|
+
if in_code[i]:
|
|
142
|
+
i += 1
|
|
143
|
+
continue
|
|
144
|
+
line = lines[i]
|
|
145
|
+
if '|' in line and i + 1 < len(lines) and _TABLE_SEP_RE.match(lines[i + 1]):
|
|
146
|
+
header_cols = _count_cells(line)
|
|
147
|
+
sep_cols = _count_cells(lines[i + 1])
|
|
148
|
+
if header_cols != sep_cols:
|
|
149
|
+
issues.append(MarkdownIssue(
|
|
150
|
+
str(md_path), i + 2, 'table_shape',
|
|
151
|
+
f'separator has {sep_cols} cells, header has {header_cols}'))
|
|
152
|
+
j = i + 2
|
|
153
|
+
while j < len(lines) and not in_code[j] and lines[j].strip() and '|' in lines[j]:
|
|
154
|
+
cells = _count_cells(lines[j])
|
|
155
|
+
if cells != header_cols:
|
|
156
|
+
issues.append(MarkdownIssue(
|
|
157
|
+
str(md_path), j + 1, 'table_shape',
|
|
158
|
+
f'row has {cells} cells, expected {header_cols}'))
|
|
159
|
+
j += 1
|
|
160
|
+
i = j
|
|
161
|
+
else:
|
|
162
|
+
i += 1
|
|
163
|
+
return issues
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _count_cells(row: str) -> int:
|
|
167
|
+
stripped = row.strip()
|
|
168
|
+
if stripped.startswith('|'):
|
|
169
|
+
stripped = stripped[1:]
|
|
170
|
+
if stripped.endswith('|'):
|
|
171
|
+
stripped = stripped[:-1]
|
|
172
|
+
return len(stripped.split('|'))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _check_headings(md_path: Path, lines: List[str]) -> List[MarkdownIssue]:
|
|
176
|
+
issues: List[MarkdownIssue] = []
|
|
177
|
+
in_code = _strip_code_fences(lines)
|
|
178
|
+
seen: dict = {}
|
|
179
|
+
for lineno, line in enumerate(lines, 1):
|
|
180
|
+
if in_code[lineno - 1]:
|
|
181
|
+
continue
|
|
182
|
+
match = _HEADING_RE.match(line)
|
|
183
|
+
if not match:
|
|
184
|
+
continue
|
|
185
|
+
level = len(match.group(1))
|
|
186
|
+
title = match.group(2).strip().lower()
|
|
187
|
+
key = (level, title)
|
|
188
|
+
if key in seen:
|
|
189
|
+
issues.append(MarkdownIssue(
|
|
190
|
+
str(md_path), lineno, 'duplicate_heading',
|
|
191
|
+
f'H{level} "{match.group(2).strip()}" already at line {seen[key]}'))
|
|
192
|
+
else:
|
|
193
|
+
seen[key] = lineno
|
|
194
|
+
return issues
|
|
@@ -93,6 +93,36 @@ def check(project_path, config_path, target) -> None:
|
|
|
93
93
|
config = _load_config(project_path, config_path)
|
|
94
94
|
_run_check(project_path, config, target)
|
|
95
95
|
|
|
96
|
+
@main.command()
|
|
97
|
+
@click.argument('project_path', default='.', type=click.Path(exists=True))
|
|
98
|
+
@click.option('--pattern', multiple=True, default=('*.md',), show_default=True,
|
|
99
|
+
help='Glob pattern(s) of markdown files to validate.')
|
|
100
|
+
@click.option('--strict', is_flag=True, help='Exit with non-zero status if any issues are found.')
|
|
101
|
+
def validate(project_path, pattern, strict) -> None:
|
|
102
|
+
"""Validate generated markdown: broken links, table shape, duplicate headings."""
|
|
103
|
+
from .analyzers.markdown_validator import validate_markdown_tree
|
|
104
|
+
project = Path(project_path).resolve()
|
|
105
|
+
console.print(f'[bold blue]🔎 code2docs validate:[/] {project.name}')
|
|
106
|
+
report = validate_markdown_tree(project, patterns=pattern)
|
|
107
|
+
if report.ok:
|
|
108
|
+
console.print(f'[green]{report.summary()}[/]')
|
|
109
|
+
return
|
|
110
|
+
table = Table(title='Markdown Issues', show_lines=False)
|
|
111
|
+
table.add_column('File', style='cyan', overflow='fold')
|
|
112
|
+
table.add_column('Line', justify='right')
|
|
113
|
+
table.add_column('Kind', style='magenta')
|
|
114
|
+
table.add_column('Message', style='dim', overflow='fold')
|
|
115
|
+
for issue in report.issues:
|
|
116
|
+
try:
|
|
117
|
+
rel = Path(issue.file).relative_to(project)
|
|
118
|
+
except ValueError:
|
|
119
|
+
rel = Path(issue.file)
|
|
120
|
+
table.add_row(str(rel), str(issue.line), issue.kind, issue.message)
|
|
121
|
+
console.print(table)
|
|
122
|
+
console.print(f'[yellow]{report.summary()}[/]')
|
|
123
|
+
if strict:
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
|
|
96
126
|
@main.command()
|
|
97
127
|
@click.argument('project_path', default='.', type=click.Path(exists=True))
|
|
98
128
|
@click.option('--config', '-c', 'config_path', default=None, help='Path to code2docs.yaml')
|
|
@@ -14,7 +14,7 @@ except ImportError:
|
|
|
14
14
|
@dataclass
|
|
15
15
|
class ReadmeConfig:
|
|
16
16
|
"""Configuration for README generation."""
|
|
17
|
-
sections: List[str] = field(default_factory=lambda: ['overview', 'install', 'quickstart', '
|
|
17
|
+
sections: List[str] = field(default_factory=lambda: ['overview', 'install', 'quickstart', 'architecture', 'api', 'structure', 'requirements', 'endpoints', 'contributing', 'docs_nav'])
|
|
18
18
|
badges: List[str] = field(default_factory=lambda: ['version', 'python', 'coverage', 'complexity'])
|
|
19
19
|
sync_markers: bool = True
|
|
20
20
|
|
|
@@ -27,6 +27,9 @@ def _make_badge(badge_type: str, project_name: str,
|
|
|
27
27
|
return f""
|
|
28
28
|
|
|
29
29
|
elif badge_type == "python":
|
|
30
|
+
lang = getattr(deps, "language", "python") if deps else "python"
|
|
31
|
+
if lang and lang != "python":
|
|
32
|
+
return _runtime_badge(lang, deps)
|
|
30
33
|
py_version = ""
|
|
31
34
|
if deps and hasattr(deps, "python_version"):
|
|
32
35
|
py_version = deps.python_version
|
|
@@ -50,3 +53,20 @@ def _make_badge(badge_type: str, project_name: str,
|
|
|
50
53
|
return f""
|
|
51
54
|
|
|
52
55
|
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_RUNTIME_LABELS = {
|
|
59
|
+
"php": ("php", "777BB4"),
|
|
60
|
+
"javascript": ("node", "339933"),
|
|
61
|
+
"typescript": ("typescript", "3178C6"),
|
|
62
|
+
"rust": ("rust", "B7410E"),
|
|
63
|
+
"go": ("go", "00ADD8"),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _runtime_badge(lang: str, deps) -> Optional[str]:
|
|
68
|
+
"""Generate a runtime badge for non-Python languages."""
|
|
69
|
+
label, color = _RUNTIME_LABELS.get(lang, (lang, "blue"))
|
|
70
|
+
version = getattr(deps, "runtime_version", "") if deps else ""
|
|
71
|
+
version = version or "any"
|
|
72
|
+
return f"}-{quote(version)}-{color})"
|
|
@@ -1,33 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
PORT_4 = 4
|
|
3
|
-
CONSTANT_5 = 5
|
|
4
|
-
CONSTANT_50 = 50
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
PORT_4 = PORT_4
|
|
8
|
-
CONSTANT_5 = CONSTANT_5
|
|
9
|
-
CONSTANT_50 = CONSTANT_50
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
PORT_4 = PORT_4
|
|
13
|
-
CONSTANT_5 = CONSTANT_5
|
|
14
|
-
CONSTANT_50 = CONSTANT_50
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
PORT_4 = PORT_4
|
|
18
|
-
CONSTANT_5 = CONSTANT_5
|
|
19
|
-
CONSTANT_50 = CONSTANT_50
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
PORT_4 = PORT_4
|
|
24
|
-
CONSTANT_5 = CONSTANT_5
|
|
25
|
-
CONSTANT_50 = CONSTANT_50
|
|
26
|
-
|
|
27
|
-
PORT_4 = PORT_4
|
|
28
|
-
CONSTANT_5 = CONSTANT_5
|
|
29
|
-
CONSTANT_50 = CONSTANT_50
|
|
30
|
-
|
|
31
1
|
"""Auto-generate usage examples from public signatures and entry points."""
|
|
32
2
|
|
|
33
3
|
from pathlib import Path
|
|
@@ -37,10 +7,12 @@ from code2llm.api import AnalysisResult, FunctionInfo, ClassInfo
|
|
|
37
7
|
|
|
38
8
|
from ..config import Code2DocsConfig
|
|
39
9
|
|
|
40
|
-
|
|
10
|
+
PORT_4 = 4
|
|
11
|
+
CONSTANT_5 = 5
|
|
12
|
+
CONSTANT_50 = 50
|
|
41
13
|
|
|
42
|
-
|
|
43
|
-
|
|
14
|
+
# Default type hints → example values
|
|
15
|
+
_TYPE_EXAMPLES = {
|
|
44
16
|
"str": '"./my-project"',
|
|
45
17
|
"Path": 'Path("./my-project")',
|
|
46
18
|
"int": "10",
|
|
@@ -55,7 +27,7 @@ if __name__ == "__main__":
|
|
|
55
27
|
}
|
|
56
28
|
|
|
57
29
|
# Arg name → realistic example value
|
|
58
|
-
|
|
30
|
+
_ARG_EXAMPLES = {
|
|
59
31
|
"project_path": '"./my-project"',
|
|
60
32
|
"path": '"./my-project"',
|
|
61
33
|
"source": '"./src"',
|
|
@@ -57,7 +57,38 @@ class ReadmeGenerator:
|
|
|
57
57
|
project_description = self._generate_description(project_name, entry_points)
|
|
58
58
|
metadata = self._extract_project_metadata()
|
|
59
59
|
extras = self._extract_extras()
|
|
60
|
-
|
|
60
|
+
lang = getattr(deps, 'language', 'python') or 'python'
|
|
61
|
+
docs_nav_items, generated_files = self._collect_existing_docs()
|
|
62
|
+
has_contributing = (Path(self.result.project_path) / 'CONTRIBUTING.md').exists()
|
|
63
|
+
return {'docs_nav_items': docs_nav_items, 'generated_files': generated_files, 'has_contributing': has_contributing, 'project_name': project_name, 'project_path': self.result.project_path, 'project_description': project_description, 'badges': generate_badges(project_name, self.config.readme.badges, stats, deps), 'stats': stats, 'avg_complexity': avg_complexity, 'dependencies': deps, 'language': lang, 'endpoints': endpoints, 'public_functions': public_functions, 'public_classes': public_classes, 'entry_points': entry_points, 'module_tree': module_tree, 'modules': self.result.modules, 'sync_markers': self.config.readme.sync_markers, 'author': metadata.get('author', ''), 'license': metadata.get('license', ''), 'license_file': metadata.get('license_file', ''), 'contributors': metadata.get('contributors', []), 'repo_url': self.config.repo_url, 'version': metadata.get('version', '0.1.0'), 'extras': extras}
|
|
64
|
+
|
|
65
|
+
_DOC_FILE_SPECS = [
|
|
66
|
+
('docs/getting-started.md', 'Getting Started', '🚀', 'Quick start guide'),
|
|
67
|
+
('docs/api.md', 'API Reference', '📚', 'Complete API documentation'),
|
|
68
|
+
('docs/modules.md', 'Module Reference', '📦', 'Module reference with metrics'),
|
|
69
|
+
('docs/architecture.md', 'Architecture', '🏛️', 'Architecture with diagrams'),
|
|
70
|
+
('docs/dependency-graph.md', 'Dependency Graph', '🔗', 'Module dependency graphs'),
|
|
71
|
+
('docs/coverage.md', 'Coverage', '📊', 'Docstring coverage report'),
|
|
72
|
+
('docs/configuration.md', 'Configuration', '🔧', 'Configuration reference'),
|
|
73
|
+
('docs/api-changelog.md', 'API Changelog', '📝', 'API change tracking'),
|
|
74
|
+
('CONTRIBUTING.md', 'Contributing', '🤝', 'Contribution guidelines'),
|
|
75
|
+
('examples', 'Examples', '💡', 'Usage examples and code samples'),
|
|
76
|
+
('mkdocs.yml', 'MkDocs Config', '⚙️', 'MkDocs site configuration'),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
def _collect_existing_docs(self) -> tuple:
|
|
80
|
+
"""Return (docs_nav_items, generated_files) only for files/dirs that exist."""
|
|
81
|
+
project = Path(self.result.project_path)
|
|
82
|
+
nav_items = []
|
|
83
|
+
generated = []
|
|
84
|
+
for rel_path, title, icon, description in self._DOC_FILE_SPECS:
|
|
85
|
+
target = project / rel_path
|
|
86
|
+
if not target.exists():
|
|
87
|
+
continue
|
|
88
|
+
link = f'./{rel_path}'
|
|
89
|
+
nav_items.append({'title': title, 'icon': icon, 'path': link, 'description': description})
|
|
90
|
+
generated.append({'output': rel_path, 'description': description, 'link': link})
|
|
91
|
+
return nav_items, generated
|
|
61
92
|
|
|
62
93
|
def _calc_avg_complexity(self) -> float:
|
|
63
94
|
"""Calculate average cyclomatic complexity."""
|
|
@@ -179,14 +210,19 @@ class ReadmeGenerator:
|
|
|
179
210
|
pass
|
|
180
211
|
|
|
181
212
|
def _detect_license(self, metadata: Dict) -> None:
|
|
182
|
-
"""Detect license type from LICENSE files.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
213
|
+
"""Detect license type from LICENSE files.
|
|
214
|
+
|
|
215
|
+
Only sets ``license_file`` (used as a link target) when the file lives
|
|
216
|
+
in the project root, so README links resolve correctly.
|
|
217
|
+
"""
|
|
218
|
+
project_root = Path(self.result.project_path)
|
|
219
|
+
in_project = [project_root / lf for lf in ['LICENSE', 'LICENSE.txt', 'LICENSE.md', 'COPYING']]
|
|
220
|
+
parent = project_root.parent
|
|
221
|
+
in_parent = [parent / lf for lf in ['LICENSE', 'LICENSE.txt', 'LICENSE.md', 'COPYING']] if parent != project_root else []
|
|
222
|
+
for license_path in in_project + in_parent:
|
|
188
223
|
if license_path.exists():
|
|
189
|
-
|
|
224
|
+
if license_path.parent == project_root:
|
|
225
|
+
metadata['license_file'] = license_path.name
|
|
190
226
|
if not metadata['license']:
|
|
191
227
|
try:
|
|
192
228
|
content = license_path.read_text(encoding='utf-8').lower()
|
|
@@ -23,6 +23,50 @@
|
|
|
23
23
|
{% if "install" in sections %}
|
|
24
24
|
## Installation
|
|
25
25
|
|
|
26
|
+
{% if language == 'php' %}
|
|
27
|
+
### Requirements
|
|
28
|
+
|
|
29
|
+
{% if dependencies and dependencies.runtime_version %}- PHP {{ dependencies.runtime_version }}
|
|
30
|
+
{% else %}- PHP 8.0+
|
|
31
|
+
{% endif %}- [Composer](https://getcomposer.org/)
|
|
32
|
+
|
|
33
|
+
### From Source
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone {{ repo_url or '<repository-url>' }}
|
|
37
|
+
cd {{ project_name }}
|
|
38
|
+
composer install
|
|
39
|
+
```
|
|
40
|
+
{% elif language in ('javascript', 'typescript') %}
|
|
41
|
+
### Requirements
|
|
42
|
+
|
|
43
|
+
{% if dependencies and dependencies.runtime_version %}- Node.js {{ dependencies.runtime_version }}
|
|
44
|
+
{% else %}- Node.js 18+
|
|
45
|
+
{% endif %}
|
|
46
|
+
### From Source
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone {{ repo_url or '<repository-url>' }}
|
|
50
|
+
cd {{ project_name }}
|
|
51
|
+
{{ dependencies.install_command if dependencies and dependencies.install_command else 'npm install' }}
|
|
52
|
+
```
|
|
53
|
+
{% elif language == 'rust' %}
|
|
54
|
+
### From Source
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone {{ repo_url or '<repository-url>' }}
|
|
58
|
+
cd {{ project_name }}
|
|
59
|
+
cargo build --release
|
|
60
|
+
```
|
|
61
|
+
{% elif language == 'go' %}
|
|
62
|
+
### From Source
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone {{ repo_url or '<repository-url>' }}
|
|
66
|
+
cd {{ project_name }}
|
|
67
|
+
go build ./...
|
|
68
|
+
```
|
|
69
|
+
{% else %}
|
|
26
70
|
### From PyPI
|
|
27
71
|
|
|
28
72
|
```bash
|
|
@@ -46,10 +90,37 @@ pip install -e .
|
|
|
46
90
|
```
|
|
47
91
|
{% endif %}
|
|
48
92
|
{% endif %}
|
|
93
|
+
{% endif %}
|
|
49
94
|
|
|
50
95
|
{% if "quickstart" in sections %}
|
|
51
96
|
## Quick Start
|
|
52
97
|
|
|
98
|
+
{% if language == 'php' %}
|
|
99
|
+
Serve the project with your preferred PHP runtime (built-in server shown for local development):
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
php -S localhost:8000
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Or with Docker Compose if a `docker-compose.yml` is provided:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
docker compose up
|
|
109
|
+
```
|
|
110
|
+
{% elif language in ('javascript', 'typescript') %}
|
|
111
|
+
```bash
|
|
112
|
+
{% if dependencies and dependencies.install_command %}{{ dependencies.install_command }}
|
|
113
|
+
{% endif %}npm start
|
|
114
|
+
```
|
|
115
|
+
{% elif language == 'rust' %}
|
|
116
|
+
```bash
|
|
117
|
+
cargo run
|
|
118
|
+
```
|
|
119
|
+
{% elif language == 'go' %}
|
|
120
|
+
```bash
|
|
121
|
+
go run .
|
|
122
|
+
```
|
|
123
|
+
{% else %}
|
|
53
124
|
### CLI Usage
|
|
54
125
|
|
|
55
126
|
```bash
|
|
@@ -82,6 +153,7 @@ config = Code2DocsConfig(project_name="mylib", verbose=True)
|
|
|
82
153
|
docs = generate_docs("./my-project", config=config)
|
|
83
154
|
```
|
|
84
155
|
{% endif %}
|
|
156
|
+
{% endif %}
|
|
85
157
|
|
|
86
158
|
{% if "generated_output" in sections %}
|
|
87
159
|
## Generated Output
|
|
@@ -171,8 +243,12 @@ Content outside the markers is preserved when regenerating. Enable this with `sy
|
|
|
171
243
|
|
|
172
244
|
```
|
|
173
245
|
{{ project_name }}/
|
|
174
|
-
{% for mod_name, mod in modules.items()|list
|
|
175
|
-
{%
|
|
246
|
+
{% for mod_name, mod in modules.items()|list -%}
|
|
247
|
+
{% if not mod_name.startswith('_') -%}
|
|
248
|
+
{% set parts = mod_name.split('.') -%}
|
|
249
|
+
{{ ' ' * (parts|length - 1) }}├── {{ parts[-1] }}{% if mod.is_package %}/{% endif %}{{ "\n" -}}
|
|
250
|
+
{% endif -%}
|
|
251
|
+
{% endfor -%}
|
|
176
252
|
```
|
|
177
253
|
{% endif %}
|
|
178
254
|
|
|
@@ -240,7 +316,7 @@ Content outside the markers is preserved when regenerating. Enable this with `sy
|
|
|
240
316
|
{% endfor %}
|
|
241
317
|
{% endif %}
|
|
242
318
|
|
|
243
|
-
We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
|
|
319
|
+
{% if has_contributing %}We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.{% else %}We welcome contributions! Open an issue or pull request to get started.{% endif %}
|
|
244
320
|
|
|
245
321
|
### Development Setup
|
|
246
322
|
|
|
@@ -249,46 +325,51 @@ We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for gu
|
|
|
249
325
|
git clone {{ repo_url or '<repository-url>' }}
|
|
250
326
|
cd {{ project_name }}
|
|
251
327
|
|
|
328
|
+
{% if language == 'php' %}
|
|
329
|
+
# Install dependencies
|
|
330
|
+
composer install
|
|
331
|
+
|
|
332
|
+
# Run tests
|
|
333
|
+
vendor/bin/phpunit
|
|
334
|
+
{% elif language in ('javascript', 'typescript') %}
|
|
335
|
+
# Install dependencies
|
|
336
|
+
{{ dependencies.install_command if dependencies and dependencies.install_command else 'npm install' }}
|
|
337
|
+
|
|
338
|
+
# Run tests
|
|
339
|
+
npm test
|
|
340
|
+
{% elif language == 'rust' %}
|
|
341
|
+
# Run tests
|
|
342
|
+
cargo test
|
|
343
|
+
{% elif language == 'go' %}
|
|
344
|
+
# Run tests
|
|
345
|
+
go test ./...
|
|
346
|
+
{% else %}
|
|
252
347
|
# Install in development mode
|
|
253
348
|
pip install -e ".[dev]"
|
|
254
349
|
|
|
255
350
|
# Run tests
|
|
256
351
|
pytest
|
|
352
|
+
{% endif %}
|
|
257
353
|
```
|
|
258
354
|
{% endif %}
|
|
259
355
|
|
|
260
|
-
{% if "docs_nav" in sections %}
|
|
356
|
+
{% if "docs_nav" in sections and docs_nav_items %}
|
|
261
357
|
## Documentation
|
|
262
358
|
|
|
263
|
-
{%
|
|
264
|
-
-
|
|
265
|
-
|
|
266
|
-
- 📚 [API Reference]({{ repo_url }}/blob/main/docs/api.md) — Complete API documentation
|
|
267
|
-
- 🔧 [Configuration]({{ repo_url }}/blob/main/docs/configuration.md) — Configuration options
|
|
268
|
-
{% else %}
|
|
269
|
-
- 📖 [Full Documentation](./docs) — API reference, module docs, architecture
|
|
270
|
-
- 🚀 [Getting Started](./docs/getting-started.md) — Quick start guide
|
|
271
|
-
- 📚 [API Reference](./docs/api.md) — Complete API documentation
|
|
272
|
-
- 🔧 [Configuration](./docs/configuration.md) — Configuration options
|
|
273
|
-
{% endif %}
|
|
274
|
-
- 💡 [Examples](./examples) — Usage examples and code samples
|
|
359
|
+
{% for item in docs_nav_items %}
|
|
360
|
+
- {{ item.icon }} [{{ item.title }}]({{ item.path }}) — {{ item.description }}
|
|
361
|
+
{% endfor %}
|
|
275
362
|
|
|
363
|
+
{% if generated_files %}
|
|
276
364
|
### Generated Files
|
|
277
365
|
|
|
278
366
|
| Output | Description | Link |
|
|
279
367
|
|--------|-------------|------|
|
|
280
368
|
| `README.md` | Project overview (this file) | — |
|
|
281
|
-
|
|
282
|
-
| `
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
| `docs/coverage.md` | Docstring coverage report | [View](./docs/coverage.md) |
|
|
286
|
-
| `docs/getting-started.md` | Getting started guide | [View](./docs/getting-started.md) |
|
|
287
|
-
| `docs/configuration.md` | Configuration reference | [View](./docs/configuration.md) |
|
|
288
|
-
| `docs/api-changelog.md` | API change tracking | [View](./docs/api-changelog.md) |
|
|
289
|
-
| `CONTRIBUTING.md` | Contribution guidelines | [View](./CONTRIBUTING.md) |
|
|
290
|
-
| `examples/` | Usage examples | [Browse](./examples) |
|
|
291
|
-
| `mkdocs.yml` | MkDocs configuration | — |
|
|
369
|
+
{% for file in generated_files %}
|
|
370
|
+
| `{{ file.output }}` | {{ file.description }} | [View]({{ file.link }}) |
|
|
371
|
+
{% endfor %}
|
|
372
|
+
{% endif %}
|
|
292
373
|
{% endif %}
|
|
293
374
|
|
|
294
375
|
{% if sync_markers %}<!-- code2docs:end -->{% endif %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code2docs
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.33
|
|
4
4
|
Summary: Auto-generate and sync project documentation from source code analysis
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -53,17 +53,17 @@ Dynamic: license-file
|
|
|
53
53
|
|
|
54
54
|
## AI Cost Tracking
|
|
55
55
|
|
|
56
|
-
     
|
|
57
|
+
  
|
|
58
58
|
|
|
59
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
60
|
-
- 👤 **Human dev:** ~$
|
|
59
|
+
- 🤖 **LLM usage:** $7.5000 (73 commits)
|
|
60
|
+
- 👤 **Human dev:** ~$2534 (25.3h @ $100/h, 30min dedup)
|
|
61
61
|
|
|
62
|
-
Generated on 2026-04-
|
|
62
|
+
Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
|
66
|
-
  
|
|
67
67
|
|
|
68
68
|
> Auto-generate and sync project documentation from source code analysis.
|
|
69
69
|
|
|
@@ -200,7 +200,7 @@ code2docs can update only specific sections of an existing README using markers:
|
|
|
200
200
|
```markdown
|
|
201
201
|
<!-- code2docs:start --># code2docs
|
|
202
202
|
|
|
203
|
-
   
|
|
204
204
|
> **276** functions | **57** classes | **51** files | CC̄ = 3.8
|
|
205
205
|
|
|
206
206
|
> Auto-generated project documentation from source code analysis.
|
|
@@ -18,6 +18,7 @@ code2docs/analyzers/__init__.py
|
|
|
18
18
|
code2docs/analyzers/dependency_scanner.py
|
|
19
19
|
code2docs/analyzers/docstring_extractor.py
|
|
20
20
|
code2docs/analyzers/endpoint_detector.py
|
|
21
|
+
code2docs/analyzers/markdown_validator.py
|
|
21
22
|
code2docs/analyzers/project_scanner.py
|
|
22
23
|
code2docs/examples/advanced_usage.py
|
|
23
24
|
code2docs/examples/quickstart.py
|
|
@@ -60,5 +61,6 @@ tests/test_config.py
|
|
|
60
61
|
tests/test_formatters.py
|
|
61
62
|
tests/test_generators.py
|
|
62
63
|
tests/test_llm_helper.py
|
|
64
|
+
tests/test_markdown_validator.py
|
|
63
65
|
tests/test_registry.py
|
|
64
66
|
tests/test_sync.py
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Tests for markdown validator."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from code2docs.analyzers.markdown_validator import (
|
|
5
|
+
validate_markdown_file,
|
|
6
|
+
validate_markdown_tree,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_validate_broken_link(tmp_path: Path) -> None:
|
|
11
|
+
md = tmp_path / "README.md"
|
|
12
|
+
md.write_text(
|
|
13
|
+
"# Title\n\n"
|
|
14
|
+
"[missing](./does-not-exist.md)\n"
|
|
15
|
+
"[existing](./present.md)\n",
|
|
16
|
+
encoding="utf-8",
|
|
17
|
+
)
|
|
18
|
+
(tmp_path / "present.md").write_text("# Present", encoding="utf-8")
|
|
19
|
+
issues = validate_markdown_file(md, project_root=tmp_path)
|
|
20
|
+
kinds = {i.kind for i in issues}
|
|
21
|
+
assert "broken_link" in kinds
|
|
22
|
+
assert any("does-not-exist" in i.message for i in issues)
|
|
23
|
+
assert not any("present.md" in i.message for i in issues)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_validate_ignores_http_and_anchors(tmp_path: Path) -> None:
|
|
27
|
+
md = tmp_path / "a.md"
|
|
28
|
+
md.write_text(
|
|
29
|
+
"[web](https://example.com)\n"
|
|
30
|
+
"[anchor](#section)\n"
|
|
31
|
+
"[mail](mailto:x@example.com)\n",
|
|
32
|
+
encoding="utf-8",
|
|
33
|
+
)
|
|
34
|
+
assert validate_markdown_file(md, project_root=tmp_path) == []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_validate_ignores_links_in_code_fences(tmp_path: Path) -> None:
|
|
38
|
+
md = tmp_path / "a.md"
|
|
39
|
+
md.write_text(
|
|
40
|
+
"```\n[missing](./nope.md)\n```\n",
|
|
41
|
+
encoding="utf-8",
|
|
42
|
+
)
|
|
43
|
+
assert validate_markdown_file(md, project_root=tmp_path) == []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_validate_detects_table_shape(tmp_path: Path) -> None:
|
|
47
|
+
md = tmp_path / "a.md"
|
|
48
|
+
md.write_text(
|
|
49
|
+
"| a | b | c |\n"
|
|
50
|
+
"|---|---|---|\n"
|
|
51
|
+
"| 1 | 2 |\n",
|
|
52
|
+
encoding="utf-8",
|
|
53
|
+
)
|
|
54
|
+
issues = validate_markdown_file(md, project_root=tmp_path)
|
|
55
|
+
assert any(i.kind == "table_shape" for i in issues)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_validate_detects_duplicate_headings(tmp_path: Path) -> None:
|
|
59
|
+
md = tmp_path / "a.md"
|
|
60
|
+
md.write_text(
|
|
61
|
+
"## Intro\n\n## Intro\n",
|
|
62
|
+
encoding="utf-8",
|
|
63
|
+
)
|
|
64
|
+
issues = validate_markdown_file(md, project_root=tmp_path)
|
|
65
|
+
assert any(i.kind == "duplicate_heading" for i in issues)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_validate_tree_skips_ignored_dirs(tmp_path: Path) -> None:
|
|
69
|
+
(tmp_path / "good.md").write_text("# ok\n", encoding="utf-8")
|
|
70
|
+
vendor = tmp_path / "vendor"
|
|
71
|
+
vendor.mkdir()
|
|
72
|
+
(vendor / "bad.md").write_text("[x](./missing.md)\n", encoding="utf-8")
|
|
73
|
+
report = validate_markdown_tree(tmp_path)
|
|
74
|
+
assert report.files_checked == 1
|
|
75
|
+
assert report.ok
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|