diff --git a/doc2man/generators/markdown.py b/doc2man/generators/markdown.py new file mode 100644 index 0000000..e433ba7 --- /dev/null +++ b/doc2man/generators/markdown.py @@ -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