Add generators: man, markdown, and HTML generators with validators
This commit is contained in:
134
doc2man/generators/markdown.py
Normal file
134
doc2man/generators/markdown.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
"""Markdown generator for Doc2Man."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader, TemplateSyntaxError
|
||||||
|
|
||||||
|
|
||||||
|
def generate_markdown(
|
||||||
|
parsed_data: List[Dict[str, Any]],
|
||||||
|
output_path: Path,
|
||||||
|
template_path: Optional[Path] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Generate Markdown documentation from parsed data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parsed_data: List of parsed documentation dictionaries.
|
||||||
|
output_path: Path to write the markdown file.
|
||||||
|
template_path: Optional custom template file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The generated Markdown content.
|
||||||
|
"""
|
||||||
|
env = Environment(
|
||||||
|
loader=FileSystemLoader(str(template_path.parent) if template_path else get_template_dir()),
|
||||||
|
autoescape=True,
|
||||||
|
)
|
||||||
|
env.filters['first_line'] = first_line_filter
|
||||||
|
|
||||||
|
try:
|
||||||
|
if template_path:
|
||||||
|
template = env.from_string(template_path.read_text())
|
||||||
|
else:
|
||||||
|
template = env.get_template("markdown.j2")
|
||||||
|
except TemplateSyntaxError as e:
|
||||||
|
raise ValueError(f"Template syntax error: {e}")
|
||||||
|
|
||||||
|
md_content = template.render(
|
||||||
|
data=parsed_data,
|
||||||
|
title=get_md_title(parsed_data),
|
||||||
|
)
|
||||||
|
|
||||||
|
if output_path:
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path.write_text(md_content, encoding="utf-8")
|
||||||
|
|
||||||
|
return md_content
|
||||||
|
|
||||||
|
|
||||||
|
def first_line_filter(text: str) -> str:
|
||||||
|
"""Get the first line of text."""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
lines = text.split('\n')
|
||||||
|
return lines[0] if lines else ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_dir() -> Path:
|
||||||
|
"""Get the directory containing templates."""
|
||||||
|
return Path(__file__).parent.parent / "templates"
|
||||||
|
|
||||||
|
|
||||||
|
def get_md_title(parsed_data: List[Dict[str, Any]]) -> str:
|
||||||
|
"""Extract a title for the Markdown document."""
|
||||||
|
for item in parsed_data:
|
||||||
|
data = item.get("data", {})
|
||||||
|
if data.get("title"):
|
||||||
|
return data["title"]
|
||||||
|
if data.get("functions"):
|
||||||
|
for func in data["functions"]:
|
||||||
|
if func.get("name"):
|
||||||
|
return func["name"]
|
||||||
|
return "Documentation"
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownValidator:
|
||||||
|
"""Validator for Markdown format."""
|
||||||
|
|
||||||
|
VALID_HEADERS = {"#", "##", "###", "####", "#####", "######"}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(content: str) -> List[str]:
|
||||||
|
"""Validate Markdown content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The Markdown content to validate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of validation warnings.
|
||||||
|
"""
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
lines = content.split("\n")
|
||||||
|
has_header = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.lstrip()
|
||||||
|
if stripped.startswith("#"):
|
||||||
|
has_header = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not has_header:
|
||||||
|
warnings.append("No markdown header found (document should start with #)")
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_code_blocks(content: str) -> List[str]:
|
||||||
|
"""Validate code blocks in Markdown.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The Markdown content to validate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of validation errors.
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
lines = content.split("\n")
|
||||||
|
in_code_block = False
|
||||||
|
code_language = None
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.strip().startswith("```"):
|
||||||
|
if not in_code_block:
|
||||||
|
in_code_block = True
|
||||||
|
code_language = line.strip()[3:].strip() or None
|
||||||
|
else:
|
||||||
|
in_code_block = False
|
||||||
|
code_language = None
|
||||||
|
elif in_code_block and line.strip() and not line.startswith(" ") and not line.startswith("\t"):
|
||||||
|
if code_language is None:
|
||||||
|
errors.append(f"Line {i+1}: Code block content should be indented or language should be specified")
|
||||||
|
|
||||||
|
return errors
|
||||||
Reference in New Issue
Block a user