mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-08-04 01:58:18 +00:00
add basic documentation (#42)
* add basic documentation * remove leading slash
This commit is contained in:
parent
757a400a8a
commit
5c821d8591
12 changed files with 1435 additions and 71 deletions
410
docs/processor.py
Normal file
410
docs/processor.py
Normal file
|
@ -0,0 +1,410 @@
|
|||
# /// script
|
||||
# dependencies = [
|
||||
# "rich>=13.9.4",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
"""
|
||||
README.md processor using functional callbacks for processing steps.
|
||||
Uses rich for beautiful logging and progress display.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from difflib import Differ
|
||||
from functools import reduce
|
||||
from itertools import islice
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
from rich.panel import Panel
|
||||
from rich.progress import track
|
||||
|
||||
console = Console()
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(message)s",
|
||||
handlers=[RichHandler(rich_tracebacks=True, show_time=False)],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ProcessingFunc = Callable[[str], str]
|
||||
|
||||
|
||||
def compose(*functions: ProcessingFunc) -> ProcessingFunc:
|
||||
"""Compose multiple processing functions into a single function."""
|
||||
return reduce(lambda f, g: lambda x: g(f(x)), functions)
|
||||
|
||||
|
||||
def read_file(path: Path) -> str | None:
|
||||
"""Read content from a file."""
|
||||
try:
|
||||
content = path.read_text(encoding="utf-8")
|
||||
console.print(f"[green]✓[/green] Read {len(content)} bytes from {path}")
|
||||
return content
|
||||
except FileNotFoundError:
|
||||
console.print(f"[red]✗[/red] Input file not found: {path}")
|
||||
return None
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗[/red] Error reading input file: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def write_file(path: Path, content: str) -> bool:
|
||||
"""Write content to a file."""
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
console.print(f"[green]✓[/green] Wrote {len(content)} bytes to {path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗[/red] Error writing output file: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def preview_changes(original: str, processed: str, context_lines: int = 2) -> None:
|
||||
"""Show a preview of the changes made."""
|
||||
console.print("\n[yellow]Preview of changes:[/yellow]")
|
||||
|
||||
# Basic statistics
|
||||
orig_lines = original.count("\n")
|
||||
proc_lines = processed.count("\n")
|
||||
diff_lines = proc_lines - orig_lines
|
||||
|
||||
stats_panel = Panel(
|
||||
f"Original lines: {orig_lines}\n"
|
||||
f"Processed lines: {proc_lines}\n"
|
||||
f"Difference: {diff_lines:+d} lines",
|
||||
title="Statistics",
|
||||
border_style="blue",
|
||||
)
|
||||
console.print(stats_panel)
|
||||
|
||||
# Create diff
|
||||
differ = Differ()
|
||||
diff = list(differ.compare(original.splitlines(), processed.splitlines()))
|
||||
|
||||
# Find changed line groups with context
|
||||
changes = []
|
||||
current_group = []
|
||||
in_change = False
|
||||
last_change_line = -1
|
||||
|
||||
for i, line in enumerate(diff):
|
||||
if line.startswith("? "): # Skip hint lines
|
||||
continue
|
||||
|
||||
is_change = line.startswith(("- ", "+ "))
|
||||
if is_change:
|
||||
if not in_change: # Start of a new change group
|
||||
start = max(0, i - context_lines)
|
||||
# If we're close to previous group, connect them
|
||||
if start <= last_change_line + context_lines:
|
||||
start = last_change_line + 1
|
||||
else:
|
||||
if current_group:
|
||||
changes.append(current_group)
|
||||
current_group = []
|
||||
# Add previous context
|
||||
current_group.extend(
|
||||
l for l in diff[start:i] if not l.startswith("? ")
|
||||
)
|
||||
current_group.append(line)
|
||||
in_change = True
|
||||
last_change_line = i
|
||||
else:
|
||||
if in_change:
|
||||
# Add following context
|
||||
following_context = list(
|
||||
islice(
|
||||
(l for l in diff[i:] if not l.startswith("? ")), context_lines
|
||||
)
|
||||
)
|
||||
if following_context: # Only extend if we have context to add
|
||||
current_group.extend(following_context)
|
||||
in_change = False
|
||||
|
||||
if current_group:
|
||||
changes.append(current_group)
|
||||
|
||||
# Format and display the changes
|
||||
formatted_output = []
|
||||
for i, group in enumerate(changes):
|
||||
if i > 0:
|
||||
formatted_output.append(
|
||||
"[bright_black]⋮ skipped unchanged content ⋮[/bright_black]"
|
||||
)
|
||||
|
||||
# Track the last line to avoid duplicates
|
||||
last_line = None
|
||||
|
||||
for line in group:
|
||||
# Skip if this line is the same as the last one
|
||||
if line == last_line:
|
||||
continue
|
||||
|
||||
if line.startswith(" "): # unchanged
|
||||
formatted_output.append(f"[white]{line[2:]}[/white]")
|
||||
elif line.startswith("- "): # removed
|
||||
formatted_output.append(f"[red]━ {line[2:]}[/red]")
|
||||
elif line.startswith("+ "): # added
|
||||
formatted_output.append(f"[green]+ {line[2:]}[/green]")
|
||||
|
||||
last_line = line
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
"\n".join(formatted_output),
|
||||
title="Changes with Context",
|
||||
border_style="yellow",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def process_readme(
|
||||
input: str = "README.md",
|
||||
output: str = "docs/index.md",
|
||||
processors: list[ProcessingFunc] | None = None,
|
||||
preview: bool = True,
|
||||
) -> bool:
|
||||
"""
|
||||
Process README.md with given processing functions.
|
||||
|
||||
Args:
|
||||
input_path: Path to the input README.md file
|
||||
output_path: Path where the processed file will be saved
|
||||
processors: List of processing functions to apply
|
||||
preview: Whether to show a preview of changes
|
||||
|
||||
Returns:
|
||||
bool: True if processing was successful, False otherwise
|
||||
"""
|
||||
with console.status("[bold green]Processing README...") as status:
|
||||
input_path = Path(input)
|
||||
output_path = Path(output)
|
||||
|
||||
content = read_file(input_path)
|
||||
if content is None:
|
||||
return False
|
||||
|
||||
original_content = content
|
||||
|
||||
try:
|
||||
for proc in track(processors, description="Applying processors"):
|
||||
status.update(f"[bold green]Running {proc.__name__}...")
|
||||
content = proc(content)
|
||||
|
||||
if preview:
|
||||
preview_changes(original_content, content)
|
||||
|
||||
return write_file(output_path, content)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error during processing:[/red] {e}")
|
||||
return False
|
||||
|
||||
|
||||
def add_frontmatter(
|
||||
metadata: dict[str, str | int | float | bool | list | None],
|
||||
) -> ProcessingFunc:
|
||||
"""
|
||||
Add or update frontmatter from a dictionary of metadata.
|
||||
|
||||
Args:
|
||||
metadata: Dictionary of metadata to add to frontmatter
|
||||
|
||||
Returns:
|
||||
A processor function that adds/updates frontmatter
|
||||
|
||||
Example:
|
||||
Input:
|
||||
# Title
|
||||
Content here
|
||||
|
||||
Output:
|
||||
---
|
||||
title: My Page
|
||||
weight: 10
|
||||
hide:
|
||||
- navigation
|
||||
---
|
||||
|
||||
# Title
|
||||
Content here
|
||||
"""
|
||||
|
||||
def processor(content: str) -> str:
|
||||
# Remove existing frontmatter if present
|
||||
content_without_frontmatter = re.sub(
|
||||
r"^---\n.*?\n---\n", "", content, flags=re.DOTALL
|
||||
)
|
||||
|
||||
# Build the new frontmatter
|
||||
frontmatter_lines = ["---"]
|
||||
|
||||
for key, value in metadata.items():
|
||||
if isinstance(value, (str, int, float, bool)) or value is None:
|
||||
frontmatter_lines.append(f"{key}: {value}")
|
||||
elif isinstance(value, list):
|
||||
frontmatter_lines.append(f"{key}:")
|
||||
for item in value:
|
||||
frontmatter_lines.append(f" - {item}")
|
||||
# Could add more types (dict, etc.) as needed
|
||||
|
||||
frontmatter_lines.append("---\n\n")
|
||||
|
||||
return "\n".join(frontmatter_lines) + content_without_frontmatter
|
||||
|
||||
processor.__name__ = "add_frontmatter"
|
||||
return processor
|
||||
|
||||
|
||||
def convert_admonitions(content: str) -> str:
|
||||
"""
|
||||
Convert GitHub-style admonitions to Material for MkDocs-style admonitions.
|
||||
|
||||
Args:
|
||||
content: The markdown content to process
|
||||
|
||||
Returns:
|
||||
Processed content with converted admonitions
|
||||
|
||||
Example:
|
||||
Input:
|
||||
> [!NOTE]
|
||||
> Content here
|
||||
> More content
|
||||
|
||||
Output:
|
||||
!!! note
|
||||
|
||||
Content here
|
||||
More content
|
||||
"""
|
||||
# Mapping from GitHub admonition types to Material for MkDocs types
|
||||
ADMONITION_MAP = {
|
||||
"NOTE": "note",
|
||||
"TIP": "tip",
|
||||
"IMPORTANT": "important",
|
||||
"WARNING": "warning",
|
||||
"CAUTION": "warning",
|
||||
"ALERT": "danger",
|
||||
"DANGER": "danger",
|
||||
"INFO": "info",
|
||||
"TODO": "todo",
|
||||
"HINT": "tip",
|
||||
}
|
||||
|
||||
def process_match(match: re.Match[str]) -> str:
|
||||
# Get admonition type and map it, defaulting to note if unknown
|
||||
admonition_type = ADMONITION_MAP.get(match.group(1).upper(), "note")
|
||||
content_lines = match.group(2).rstrip().split("\n")
|
||||
|
||||
# Remove the leading '> ' from each line
|
||||
cleaned_lines = [line.lstrip("> ") for line in content_lines]
|
||||
|
||||
# Indent the content (4 spaces)
|
||||
indented_content = "\n".join(
|
||||
f" {line}" if line.strip() else "" for line in cleaned_lines
|
||||
)
|
||||
|
||||
# Preserve the exact number of trailing newlines from the original match
|
||||
trailing_newlines = len(match.group(2)) - len(match.group(2).rstrip("\n"))
|
||||
|
||||
return f"!!! {admonition_type}\n\n{indented_content}" + "\n" * trailing_newlines
|
||||
|
||||
# Match GitHub-style admonitions
|
||||
pattern = r"(?m)^>\s*\[!(.*?)\]\s*\n((?:>.*(?:\n|$))+)"
|
||||
|
||||
return re.sub(pattern, process_match, content)
|
||||
|
||||
|
||||
def convert_repo_links(repo_url: str) -> ProcessingFunc:
|
||||
"""
|
||||
Convert relative repository links to absolute URLs.
|
||||
|
||||
Args:
|
||||
repo_url: The base repository URL (e.g., 'https://github.com/username/repo')
|
||||
|
||||
Returns:
|
||||
A processor function that converts relative links to absolute URLs
|
||||
|
||||
Example:
|
||||
Input:
|
||||
See the [`LICENSE`](LICENSE) file for more information.
|
||||
Check the [Neovim](/docs/editors/neovim.md) guide.
|
||||
|
||||
Output:
|
||||
See the [`LICENSE`](https://github.com/username/repo/blob/main/LICENSE) file for more information.
|
||||
Check the [Neovim](editors/neovim.md) guide.
|
||||
"""
|
||||
|
||||
def processor(content: str) -> str:
|
||||
def replace_link(match: re.Match[str]) -> str:
|
||||
text = match.group(1)
|
||||
path = match.group(2)
|
||||
|
||||
# Skip anchor links
|
||||
if path.startswith("#"):
|
||||
return match.group(0)
|
||||
|
||||
# Skip already absolute URLs
|
||||
if path.startswith(("http://", "https://")):
|
||||
return match.group(0)
|
||||
|
||||
# Handle docs directory links
|
||||
if path.startswith(("/docs/", "docs/")):
|
||||
# Remove /docs/ or docs/ prefix and .md extension
|
||||
clean_path = path.removeprefix("/docs/").removeprefix("docs/")
|
||||
return f"[{text}]({clean_path})"
|
||||
|
||||
# Handle root-relative paths
|
||||
if path.startswith("/"):
|
||||
path = path.removeprefix("/")
|
||||
|
||||
# Remove ./ if present
|
||||
path = path.removeprefix("./")
|
||||
|
||||
# Construct the full URL for repository files
|
||||
full_url = f"{repo_url.rstrip('/')}/blob/main/{path}"
|
||||
return f"[{text}]({full_url})"
|
||||
|
||||
# Match markdown links: [text](url)
|
||||
pattern = r"\[((?:[^][]|\[[^]]*\])*)\]\(([^)]+)\)"
|
||||
return re.sub(pattern, replace_link, content)
|
||||
|
||||
processor.__name__ = "convert_repo_links"
|
||||
return processor
|
||||
|
||||
|
||||
def main():
|
||||
"""Example usage of the readme processor."""
|
||||
console.print("[bold blue]README Processor[/bold blue]")
|
||||
|
||||
processors = [
|
||||
add_frontmatter({"title": "Home"}),
|
||||
convert_admonitions,
|
||||
convert_repo_links(
|
||||
"https://github.com/joshuadavidthomas/django-language-server"
|
||||
),
|
||||
]
|
||||
|
||||
success = process_readme(
|
||||
input="README.md",
|
||||
output="docs/index.md",
|
||||
processors=processors,
|
||||
preview=True,
|
||||
)
|
||||
|
||||
if success:
|
||||
console.print("\n[green]✨ Processing completed successfully![/green]")
|
||||
else:
|
||||
console.print("\n[red]Processing failed![/red]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue