docuchango 0.1.0__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.
- docuchango/__init__.py +49 -0
- docuchango/__main__.py +10 -0
- docuchango/cli.py +286 -0
- docuchango/fixes/__init__.py +17 -0
- docuchango/fixes/broken_links.py +106 -0
- docuchango/fixes/code_blocks.py +174 -0
- docuchango/fixes/code_blocks_proper.py +103 -0
- docuchango/fixes/cross_plugin_links.py +61 -0
- docuchango/fixes/doc_links.py +142 -0
- docuchango/fixes/docs.py +185 -0
- docuchango/fixes/internal_links.py +149 -0
- docuchango/fixes/mdx_code_blocks.py +94 -0
- docuchango/fixes/mdx_syntax.py +153 -0
- docuchango/fixes/migration_syntax.py +98 -0
- docuchango/fixes/proto_imports.py +142 -0
- docuchango/schemas.py +358 -0
- docuchango/testing/__init__.py +26 -0
- docuchango/testing/assertions.py +100 -0
- docuchango/testing/cli.py +272 -0
- docuchango/testing/docker.py +219 -0
- docuchango/testing/fixtures.py +276 -0
- docuchango/testing/health.py +230 -0
- docuchango/validator.py +1200 -0
- docuchango-0.1.0.dist-info/METADATA +284 -0
- docuchango-0.1.0.dist-info/RECORD +28 -0
- docuchango-0.1.0.dist-info/WHEEL +4 -0
- docuchango-0.1.0.dist-info/entry_points.txt +4 -0
- docuchango-0.1.0.dist-info/licenses/LICENSE +373 -0
docuchango/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Docuchango - Docusaurus Validation and Repair Framework.
|
|
2
|
+
|
|
3
|
+
A comprehensive toolkit for Docusaurus documentation validation, testing, and repair.
|
|
4
|
+
Designed for opinionated micro-CMS documentation systems with human-agent collaboration.
|
|
5
|
+
|
|
6
|
+
This package provides:
|
|
7
|
+
- Documentation validation (frontmatter, links, formatting)
|
|
8
|
+
- Automated fixing of common documentation issues
|
|
9
|
+
- Testing utilities for documentation workflows
|
|
10
|
+
- CLI tools for all operations
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
>>> from docuchango.validator import DocValidator
|
|
14
|
+
>>> validator = DocValidator(repo_root=".")
|
|
15
|
+
>>> validator.scan_documents()
|
|
16
|
+
>>> validator.check_code_blocks()
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
__author__ = "Jacob Repp"
|
|
21
|
+
__email__ = "jacobrepp@gmail.com"
|
|
22
|
+
|
|
23
|
+
# Core validation exports
|
|
24
|
+
from docuchango.schemas import (
|
|
25
|
+
ADRFrontmatter,
|
|
26
|
+
GenericDocFrontmatter,
|
|
27
|
+
MemoFrontmatter,
|
|
28
|
+
RFCFrontmatter,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Testing framework exports
|
|
32
|
+
from docuchango.testing import AGFAssertions, AGFCLIRunner, CLIResult, HealthChecker
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Version info
|
|
36
|
+
"__version__",
|
|
37
|
+
"__author__",
|
|
38
|
+
"__email__",
|
|
39
|
+
# Schemas
|
|
40
|
+
"ADRFrontmatter",
|
|
41
|
+
"RFCFrontmatter",
|
|
42
|
+
"MemoFrontmatter",
|
|
43
|
+
"GenericDocFrontmatter",
|
|
44
|
+
# Testing
|
|
45
|
+
"AGFCLIRunner",
|
|
46
|
+
"CLIResult",
|
|
47
|
+
"AGFAssertions",
|
|
48
|
+
"HealthChecker",
|
|
49
|
+
]
|
docuchango/__main__.py
ADDED
docuchango/cli.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Docuchango CLI.
|
|
3
|
+
|
|
4
|
+
Docusaurus validation and repair framework for opinionated micro-CMS documentation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
@click.version_option(version="0.1.0")
|
|
18
|
+
def main():
|
|
19
|
+
"""Docuchango - Docusaurus validation and repair framework."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@main.command()
|
|
24
|
+
@click.option(
|
|
25
|
+
"--repo-root",
|
|
26
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
27
|
+
default=Path.cwd(),
|
|
28
|
+
help="Repository root directory (default: current directory)",
|
|
29
|
+
)
|
|
30
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
|
31
|
+
@click.option("--skip-build", is_flag=True, help="Skip Docusaurus build validation")
|
|
32
|
+
@click.option("--fix", is_flag=True, help="Auto-fix issues where possible")
|
|
33
|
+
def validate(
|
|
34
|
+
repo_root: Path,
|
|
35
|
+
verbose: bool,
|
|
36
|
+
skip_build: bool,
|
|
37
|
+
fix: bool,
|
|
38
|
+
):
|
|
39
|
+
"""Validate documentation files for correctness.
|
|
40
|
+
|
|
41
|
+
Validates markdown documents for:
|
|
42
|
+
- YAML frontmatter format and required fields
|
|
43
|
+
- Internal link reachability
|
|
44
|
+
- Markdown formatting issues
|
|
45
|
+
- Consistent ADR/RFC numbering
|
|
46
|
+
- MDX compilation compatibility
|
|
47
|
+
- Docusaurus build validation (unless --skip-build)
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
from docuchango.validator import DocValidator
|
|
51
|
+
except ImportError as e:
|
|
52
|
+
console.print(f"[red]Error importing validator: {e}[/red]")
|
|
53
|
+
sys.exit(2)
|
|
54
|
+
|
|
55
|
+
console.print("[bold blue]🔍 Validating Documentation[/bold blue]\n")
|
|
56
|
+
console.print(f"Repository root: {repo_root}")
|
|
57
|
+
console.print(f"Verbose: {verbose}")
|
|
58
|
+
console.print(f"Skip build: {skip_build}")
|
|
59
|
+
console.print(f"Auto-fix: {fix}\n")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
validator = DocValidator(repo_root=repo_root, verbose=verbose)
|
|
63
|
+
validator.scan_documents()
|
|
64
|
+
validator.check_code_blocks()
|
|
65
|
+
validator.check_formatting()
|
|
66
|
+
|
|
67
|
+
# Check if we should run build validation
|
|
68
|
+
if not skip_build:
|
|
69
|
+
# This would need to be implemented based on the validate_docs.py logic
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# Check for errors
|
|
73
|
+
has_errors = False
|
|
74
|
+
for doc in validator.documents:
|
|
75
|
+
if doc.errors:
|
|
76
|
+
has_errors = True
|
|
77
|
+
console.print(f"[red]✗[/red] {doc.file_path}")
|
|
78
|
+
for error in doc.errors:
|
|
79
|
+
console.print(f" [red]{error}[/red]")
|
|
80
|
+
|
|
81
|
+
if validator.errors:
|
|
82
|
+
has_errors = True
|
|
83
|
+
for error in validator.errors:
|
|
84
|
+
console.print(f"[red]{error}[/red]")
|
|
85
|
+
|
|
86
|
+
if has_errors:
|
|
87
|
+
console.print("\n[bold red]❌ Validation failed[/bold red]")
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
else:
|
|
90
|
+
console.print("\n[bold green]✅ All documents valid[/bold green]")
|
|
91
|
+
sys.exit(0)
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
console.print(f"[bold red]Error during validation: {e}[/bold red]")
|
|
95
|
+
if verbose:
|
|
96
|
+
import traceback
|
|
97
|
+
|
|
98
|
+
traceback.print_exc()
|
|
99
|
+
sys.exit(2)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@main.group()
|
|
103
|
+
def fix():
|
|
104
|
+
"""Fix documentation issues automatically."""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@fix.command("all")
|
|
109
|
+
@click.option(
|
|
110
|
+
"--repo-root",
|
|
111
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
112
|
+
default=Path.cwd(),
|
|
113
|
+
help="Repository root directory (default: current directory)",
|
|
114
|
+
)
|
|
115
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be fixed without making changes")
|
|
116
|
+
def fix_all(repo_root: Path, dry_run: bool):
|
|
117
|
+
"""Run all automatic fixes on documentation."""
|
|
118
|
+
|
|
119
|
+
console.print("[bold blue]🔧 Fixing Documentation Issues[/bold blue]\n")
|
|
120
|
+
if dry_run:
|
|
121
|
+
console.print("[yellow]DRY RUN - No changes will be made[/yellow]\n")
|
|
122
|
+
|
|
123
|
+
# This would call the fix functions
|
|
124
|
+
# For now, just show what would happen
|
|
125
|
+
console.print("Would fix:")
|
|
126
|
+
console.print(" • Trailing whitespace")
|
|
127
|
+
console.print(" • Code fence languages")
|
|
128
|
+
console.print(" • Blank lines before fences")
|
|
129
|
+
console.print(" • Missing frontmatter fields")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@fix.command("links")
|
|
133
|
+
@click.option(
|
|
134
|
+
"--repo-root",
|
|
135
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
136
|
+
default=Path.cwd(),
|
|
137
|
+
help="Repository root directory (default: current directory)",
|
|
138
|
+
)
|
|
139
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be fixed without making changes")
|
|
140
|
+
def fix_links(repo_root: Path, dry_run: bool):
|
|
141
|
+
"""Fix broken links in documentation."""
|
|
142
|
+
console.print("[bold blue]🔗 Fixing Broken Links[/bold blue]\n")
|
|
143
|
+
if dry_run:
|
|
144
|
+
console.print("[yellow]DRY RUN - No changes will be made[/yellow]\n")
|
|
145
|
+
|
|
146
|
+
# Import and run the fix_broken_links module
|
|
147
|
+
console.print("Fixing broken links...")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@fix.command("code-blocks")
|
|
151
|
+
@click.option(
|
|
152
|
+
"--repo-root",
|
|
153
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
154
|
+
default=Path.cwd(),
|
|
155
|
+
help="Repository root directory (default: current directory)",
|
|
156
|
+
)
|
|
157
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be fixed without making changes")
|
|
158
|
+
def fix_code_blocks(repo_root: Path, dry_run: bool):
|
|
159
|
+
"""Fix code block formatting issues."""
|
|
160
|
+
console.print("[bold blue]📝 Fixing Code Blocks[/bold blue]\n")
|
|
161
|
+
if dry_run:
|
|
162
|
+
console.print("[yellow]DRY RUN - No changes will be made[/yellow]\n")
|
|
163
|
+
|
|
164
|
+
console.print("Fixing code blocks...")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@main.group()
|
|
168
|
+
def test():
|
|
169
|
+
"""Testing utilities and helpers."""
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@test.command("health")
|
|
174
|
+
@click.option("--url", default="http://localhost:8080", help="Service URL to check")
|
|
175
|
+
@click.option("--timeout", default=30, help="Timeout in seconds")
|
|
176
|
+
def test_health(url: str, timeout: int):
|
|
177
|
+
"""Check service health."""
|
|
178
|
+
console.print(f"[bold blue]🏥 Checking Health: {url}[/bold blue]\n")
|
|
179
|
+
|
|
180
|
+
# Placeholder for health check implementation
|
|
181
|
+
# HealthChecker would be initialized and used here
|
|
182
|
+
console.print(f"[yellow]ℹ[/yellow] Health check not yet implemented for {url}")
|
|
183
|
+
console.print(f"[dim]Timeout: {timeout}s[/dim]")
|
|
184
|
+
console.print("[green]✓[/green] Placeholder completed")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@main.command()
|
|
188
|
+
@click.option(
|
|
189
|
+
"--guide",
|
|
190
|
+
type=click.Choice(["bootstrap", "agent", "best-practices"], case_sensitive=False),
|
|
191
|
+
default="bootstrap",
|
|
192
|
+
help="Which guide to display (default: bootstrap)",
|
|
193
|
+
)
|
|
194
|
+
@click.option(
|
|
195
|
+
"--output",
|
|
196
|
+
type=click.Path(path_type=Path),
|
|
197
|
+
help="Save guide to file instead of displaying",
|
|
198
|
+
)
|
|
199
|
+
def bootstrap(guide: str, output: Path | None):
|
|
200
|
+
"""Display or save docs-cms bootstrap and agent guides.
|
|
201
|
+
|
|
202
|
+
Provides quick access to documentation for setting up and using docs-cms:
|
|
203
|
+
|
|
204
|
+
- bootstrap: Step-by-step setup guide for docs-cms
|
|
205
|
+
- agent: Instructions for AI agents using docs-cms
|
|
206
|
+
- best-practices: Best practices for agent-CMS interaction
|
|
207
|
+
|
|
208
|
+
Examples:
|
|
209
|
+
|
|
210
|
+
\b
|
|
211
|
+
# Display bootstrap guide
|
|
212
|
+
docuchango bootstrap
|
|
213
|
+
|
|
214
|
+
\b
|
|
215
|
+
# Display agent guide
|
|
216
|
+
docuchango bootstrap --guide agent
|
|
217
|
+
|
|
218
|
+
\b
|
|
219
|
+
# Save bootstrap guide to file
|
|
220
|
+
docuchango bootstrap --output /path/to/BOOTSTRAP_GUIDE.md
|
|
221
|
+
"""
|
|
222
|
+
# Map guide names to file names
|
|
223
|
+
guide_files = {
|
|
224
|
+
"bootstrap": "BOOTSTRAP_GUIDE.md",
|
|
225
|
+
"agent": "AGENT_GUIDE.md",
|
|
226
|
+
"best-practices": "BEST_PRACTICES.md",
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
guide_file = guide_files[guide]
|
|
230
|
+
|
|
231
|
+
# Try to find the guide in the package
|
|
232
|
+
try:
|
|
233
|
+
# First, try to find it in the installed package
|
|
234
|
+
import importlib.resources as resources
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
# Python 3.9+
|
|
238
|
+
guide_path = resources.files("docuchango") / ".." / "docs" / guide_file
|
|
239
|
+
guide_content = guide_path.read_text()
|
|
240
|
+
except AttributeError:
|
|
241
|
+
# Python 3.8 fallback
|
|
242
|
+
with resources.path("docuchango", "__init__.py") as pkg_path:
|
|
243
|
+
docs_dir = pkg_path.parent.parent / "docs"
|
|
244
|
+
guide_path = docs_dir / guide_file
|
|
245
|
+
guide_content = guide_path.read_text()
|
|
246
|
+
|
|
247
|
+
except (FileNotFoundError, ModuleNotFoundError):
|
|
248
|
+
# Fallback: try relative to the script location
|
|
249
|
+
script_dir = Path(__file__).parent.parent
|
|
250
|
+
guide_path = script_dir / "docs" / guide_file
|
|
251
|
+
|
|
252
|
+
if not guide_path.exists():
|
|
253
|
+
from rich.console import Console as RichConsole
|
|
254
|
+
|
|
255
|
+
stderr_console = RichConsole(stderr=True)
|
|
256
|
+
stderr_console.print(f"[red]✗[/red] Guide not found: {guide_file}")
|
|
257
|
+
stderr_console.print(f"[dim]Searched in: {guide_path}[/dim]")
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
|
|
260
|
+
guide_content = guide_path.read_text()
|
|
261
|
+
|
|
262
|
+
# Output or display
|
|
263
|
+
if output:
|
|
264
|
+
output.write_text(guide_content)
|
|
265
|
+
console.print(f"[green]✓[/green] Saved {guide} guide to: {output}")
|
|
266
|
+
else:
|
|
267
|
+
# Display with rich markdown rendering
|
|
268
|
+
from rich.markdown import Markdown
|
|
269
|
+
|
|
270
|
+
console.print(Markdown(guide_content))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# Export the validate command as a separate entry point
|
|
274
|
+
def validate_main():
|
|
275
|
+
"""Entry point for agf-validate command."""
|
|
276
|
+
validate()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# Export the fix command as a separate entry point
|
|
280
|
+
def fix_main():
|
|
281
|
+
"""Entry point for agf-fix command."""
|
|
282
|
+
fix()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
main()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Documentation fix modules.
|
|
2
|
+
|
|
3
|
+
This package contains modules for automatically fixing common documentation issues:
|
|
4
|
+
- broken_links: Fix broken internal and cross-reference links
|
|
5
|
+
- code_blocks: Fix code block formatting and language tags
|
|
6
|
+
- docs: General documentation fixes (whitespace, frontmatter, etc.)
|
|
7
|
+
- internal_links: Fix internal link references
|
|
8
|
+
- mdx_syntax: Fix MDX syntax issues
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"broken_links",
|
|
13
|
+
"code_blocks",
|
|
14
|
+
"docs",
|
|
15
|
+
"internal_links",
|
|
16
|
+
"mdx_syntax",
|
|
17
|
+
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fix broken documentation links by converting full filenames to short-form IDs.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
uv run tooling/fix_broken_links.py [--dry-run]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Mapping of common broken link patterns to correct formats
|
|
13
|
+
LINK_FIXES = {
|
|
14
|
+
# RFCs - full filename to short ID
|
|
15
|
+
r"/rfc/rfc-(\d+)-[a-z-]+": r"/rfc/rfc-\1",
|
|
16
|
+
r"/prism-data-layer/rfc/rfc-(\d+)-[a-z-]+": r"/rfc/rfc-\1",
|
|
17
|
+
r"/prism-data-layer/rfc/RFC-(\d+)-[a-z-]+": r"/rfc/rfc-\1",
|
|
18
|
+
r"\.\/RFC-(\d+)-[a-z-]+": r"/rfc/rfc-\1",
|
|
19
|
+
# ADRs - full filename to short ID
|
|
20
|
+
r"/adr/adr-(\d+)-[a-z-]+": r"/adr/adr-\1",
|
|
21
|
+
r"/prism-data-layer/adr/adr-(\d+)-[a-z-]+": r"/adr/adr-\1",
|
|
22
|
+
# MEMOs - full filename to short ID
|
|
23
|
+
r"/memos/memo-(\d+)-[a-z-]+": r"/memos/memo-\1",
|
|
24
|
+
r"/prism-data-layer/memos/memo-(\d+)-[a-z-]+": r"/memos/memo-\1",
|
|
25
|
+
# Remove /prism-data-layer prefix from already short paths
|
|
26
|
+
r"/prism-data-layer/(adr/adr-\d+)": r"/\1",
|
|
27
|
+
r"/prism-data-layer/(rfc/rfc-\d+)": r"/\1",
|
|
28
|
+
r"/prism-data-layer/(memos/memo-\d+)": r"/\1",
|
|
29
|
+
r"/prism-data-layer/(prd)": r"/\1",
|
|
30
|
+
r"/prism-data-layer/(key-documents)": r"/\1",
|
|
31
|
+
r"/prism-data-layer/(netflix/[a-z\-]+)": r"/\1",
|
|
32
|
+
# Fix incorrectly converted RFC numbers (rfc-211 should be rfc-021)
|
|
33
|
+
r"/rfc/rfc-211([^0-9])": r"/rfc/rfc-021\1",
|
|
34
|
+
r"/rfc/rfc-211$": r"/rfc/rfc-021",
|
|
35
|
+
# Fix netflix links - add netflix- prefix to all document names
|
|
36
|
+
r"/netflix/abstractions\b": r"/netflix/netflix-abstractions",
|
|
37
|
+
r"/netflix/write-ahead-log\b": r"/netflix/netflix-write-ahead-log",
|
|
38
|
+
r"/netflix/scale\b": r"/netflix/netflix-scale",
|
|
39
|
+
r"/netflix/dual-write-migration\b": r"/netflix/netflix-dual-write-migration",
|
|
40
|
+
r"/netflix/data-evolve-migration\b": r"/netflix/netflix-data-evolve-migration",
|
|
41
|
+
r"/netflix/summary\b": r"/netflix/netflix-summary",
|
|
42
|
+
r"/netflix/key-use-cases\b": r"/netflix/netflix-key-use-cases",
|
|
43
|
+
r"/netflix/netflix-index\b": r"/netflix/netflix-index", # This one is already correct but keeping for completeness
|
|
44
|
+
r"/netflix/video1\b": r"/netflix/netflix-video1",
|
|
45
|
+
r"/netflix/video2\b": r"/netflix/netflix-video2",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def fix_links_in_file(file_path: Path, dry_run: bool = False) -> int:
|
|
50
|
+
"""Fix broken links in a single file."""
|
|
51
|
+
try:
|
|
52
|
+
content = file_path.read_text()
|
|
53
|
+
original_content = content
|
|
54
|
+
changes = 0
|
|
55
|
+
|
|
56
|
+
for pattern, replacement in LINK_FIXES.items():
|
|
57
|
+
new_content, count = re.subn(pattern, replacement, content)
|
|
58
|
+
if count > 0:
|
|
59
|
+
changes += count
|
|
60
|
+
content = new_content
|
|
61
|
+
|
|
62
|
+
if content != original_content:
|
|
63
|
+
if dry_run:
|
|
64
|
+
print(f"Would fix {changes} links in: {file_path}")
|
|
65
|
+
else:
|
|
66
|
+
file_path.write_text(content)
|
|
67
|
+
print(f"Fixed {changes} links in: {file_path}")
|
|
68
|
+
return changes
|
|
69
|
+
|
|
70
|
+
return 0
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print(f"Error processing {file_path}: {e}")
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main():
|
|
77
|
+
parser = argparse.ArgumentParser(description="Fix broken documentation links")
|
|
78
|
+
parser.add_argument("--dry-run", action="store_true", help="Show what would be changed without making changes")
|
|
79
|
+
args = parser.parse_args()
|
|
80
|
+
|
|
81
|
+
repo_root = Path(__file__).parent.parent
|
|
82
|
+
docs_cms = repo_root / "docs-cms"
|
|
83
|
+
docusaurus_docs = repo_root / "docusaurus" / "docs"
|
|
84
|
+
|
|
85
|
+
total_changes = 0
|
|
86
|
+
total_files = 0
|
|
87
|
+
|
|
88
|
+
# Process all markdown files in docs-cms
|
|
89
|
+
for md_file in docs_cms.rglob("*.md"):
|
|
90
|
+
changes = fix_links_in_file(md_file, args.dry_run)
|
|
91
|
+
if changes > 0:
|
|
92
|
+
total_changes += changes
|
|
93
|
+
total_files += 1
|
|
94
|
+
|
|
95
|
+
# Process all markdown files in docusaurus/docs
|
|
96
|
+
for md_file in docusaurus_docs.rglob("*.md"):
|
|
97
|
+
changes = fix_links_in_file(md_file, args.dry_run)
|
|
98
|
+
if changes > 0:
|
|
99
|
+
total_changes += changes
|
|
100
|
+
total_files += 1
|
|
101
|
+
|
|
102
|
+
print(f"\n{'[DRY RUN] ' if args.dry_run else ''}Fixed {total_changes} links in {total_files} files")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
main()
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Auto-fix code block formatting issues
|
|
3
|
+
|
|
4
|
+
Fixes:
|
|
5
|
+
1. Missing blank lines before opening code fences
|
|
6
|
+
2. Missing blank lines after closing code fences
|
|
7
|
+
3. Closing fences with extra text (```bash -> ```)
|
|
8
|
+
4. Opening fences without language (``` -> ```text)
|
|
9
|
+
5. Unclosed code blocks
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
uv run python -m tooling.fix_code_blocks
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fix_code_blocks(file_path: Path) -> tuple[bool, list[str]]:
|
|
21
|
+
"""Fix code block issues in a file"""
|
|
22
|
+
changes = []
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
content = file_path.read_text(encoding="utf-8")
|
|
26
|
+
lines = content.split("\n")
|
|
27
|
+
fixed_lines = []
|
|
28
|
+
|
|
29
|
+
in_code_block = False
|
|
30
|
+
in_frontmatter = False
|
|
31
|
+
frontmatter_count = 0
|
|
32
|
+
frontmatter_end_line = None
|
|
33
|
+
i = 0
|
|
34
|
+
|
|
35
|
+
while i < len(lines):
|
|
36
|
+
line = lines[i]
|
|
37
|
+
stripped = line.strip()
|
|
38
|
+
|
|
39
|
+
# Track frontmatter
|
|
40
|
+
if stripped == "---":
|
|
41
|
+
frontmatter_count += 1
|
|
42
|
+
if frontmatter_count == 1:
|
|
43
|
+
in_frontmatter = True
|
|
44
|
+
elif frontmatter_count == 2:
|
|
45
|
+
in_frontmatter = False
|
|
46
|
+
frontmatter_end_line = i
|
|
47
|
+
fixed_lines.append(line)
|
|
48
|
+
i += 1
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
# Skip frontmatter content
|
|
52
|
+
if in_frontmatter:
|
|
53
|
+
fixed_lines.append(line)
|
|
54
|
+
i += 1
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
# Check for code fence (match beginning of stripped line)
|
|
58
|
+
fence_match = re.match(r"^(`{3,})(.*)$", stripped)
|
|
59
|
+
if fence_match:
|
|
60
|
+
fence_backticks = fence_match.group(1)
|
|
61
|
+
remainder = fence_match.group(2).strip()
|
|
62
|
+
|
|
63
|
+
if not in_code_block:
|
|
64
|
+
# Opening fence
|
|
65
|
+
content_start = (frontmatter_end_line + 1) if frontmatter_end_line is not None else 0
|
|
66
|
+
is_after_frontmatter = frontmatter_end_line is not None and i == frontmatter_end_line + 1
|
|
67
|
+
is_document_start = i == content_start
|
|
68
|
+
|
|
69
|
+
# Check if previous line was blank
|
|
70
|
+
previous_line_blank = len(fixed_lines) == 0 or not fixed_lines[-1].strip()
|
|
71
|
+
|
|
72
|
+
# Add blank line before if needed
|
|
73
|
+
if not previous_line_blank and not is_after_frontmatter and not is_document_start:
|
|
74
|
+
fixed_lines.append("")
|
|
75
|
+
changes.append(f"Line {i + 1}: Added blank line before opening fence")
|
|
76
|
+
|
|
77
|
+
# Check if language is missing
|
|
78
|
+
if not remainder:
|
|
79
|
+
fixed_lines.append(f"{fence_backticks}text")
|
|
80
|
+
changes.append(f"Line {i + 1}: Added 'text' language to bare opening fence")
|
|
81
|
+
else:
|
|
82
|
+
fixed_lines.append(line)
|
|
83
|
+
|
|
84
|
+
in_code_block = True
|
|
85
|
+
else:
|
|
86
|
+
# Closing fence
|
|
87
|
+
if remainder:
|
|
88
|
+
# Remove extra text from closing fence
|
|
89
|
+
fixed_lines.append(fence_backticks)
|
|
90
|
+
changes.append(f"Line {i + 1}: Removed extra text from closing fence (```{remainder} -> ```)")
|
|
91
|
+
else:
|
|
92
|
+
fixed_lines.append(line)
|
|
93
|
+
|
|
94
|
+
in_code_block = False
|
|
95
|
+
|
|
96
|
+
# Check next line for blank line requirement
|
|
97
|
+
if i + 1 < len(lines):
|
|
98
|
+
next_line = lines[i + 1].strip()
|
|
99
|
+
if next_line: # Next line has content
|
|
100
|
+
# Insert blank line after closing fence
|
|
101
|
+
i += 1 # Move to next line
|
|
102
|
+
fixed_lines.append("") # Add blank line
|
|
103
|
+
changes.append(f"Line {i}: Added blank line after closing fence")
|
|
104
|
+
continue # Will process next line in next iteration
|
|
105
|
+
else:
|
|
106
|
+
# Regular line (not a code fence)
|
|
107
|
+
if not in_code_block:
|
|
108
|
+
fixed_lines.append(line)
|
|
109
|
+
else:
|
|
110
|
+
# Inside code block - preserve exactly
|
|
111
|
+
fixed_lines.append(line)
|
|
112
|
+
|
|
113
|
+
i += 1
|
|
114
|
+
|
|
115
|
+
# Check for unclosed code block
|
|
116
|
+
if in_code_block:
|
|
117
|
+
fixed_lines.append("```")
|
|
118
|
+
changes.append("End of file: Added closing fence for unclosed code block")
|
|
119
|
+
|
|
120
|
+
# Write back if changes were made
|
|
121
|
+
if changes:
|
|
122
|
+
new_content = "\n".join(fixed_lines)
|
|
123
|
+
file_path.write_text(new_content, encoding="utf-8")
|
|
124
|
+
return True, changes
|
|
125
|
+
|
|
126
|
+
return False, []
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f"Error processing {file_path}: {e}", file=sys.stderr)
|
|
130
|
+
return False, []
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main():
|
|
134
|
+
repo_root = Path(__file__).parent.parent
|
|
135
|
+
docs_cms = repo_root / "docs-cms"
|
|
136
|
+
|
|
137
|
+
print("🔧 Auto-fixing code block issues...\n")
|
|
138
|
+
|
|
139
|
+
total_files = 0
|
|
140
|
+
fixed_files = 0
|
|
141
|
+
total_changes = 0
|
|
142
|
+
|
|
143
|
+
# Process all markdown files
|
|
144
|
+
for md_file in docs_cms.rglob("*.md"):
|
|
145
|
+
# Skip README and index files
|
|
146
|
+
if md_file.name in ["README.md", "index.md"]:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
total_files += 1
|
|
150
|
+
modified, changes = fix_code_blocks(md_file)
|
|
151
|
+
|
|
152
|
+
if modified:
|
|
153
|
+
fixed_files += 1
|
|
154
|
+
total_changes += len(changes)
|
|
155
|
+
print(f"✓ {md_file.relative_to(repo_root)}")
|
|
156
|
+
for change in changes:
|
|
157
|
+
print(f" • {change}")
|
|
158
|
+
print()
|
|
159
|
+
|
|
160
|
+
print(f"\n{'=' * 80}")
|
|
161
|
+
print("📊 Summary:")
|
|
162
|
+
print(f" Files scanned: {total_files}")
|
|
163
|
+
print(f" Files fixed: {fixed_files}")
|
|
164
|
+
print(f" Total changes: {total_changes}")
|
|
165
|
+
print(f"{'=' * 80}\n")
|
|
166
|
+
|
|
167
|
+
if fixed_files > 0:
|
|
168
|
+
print("✅ Fixes applied! Run validation again to verify.")
|
|
169
|
+
else:
|
|
170
|
+
print("✅ No fixes needed.")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
main()
|