""" Formatador de saída Markdown. Este módulo contém funções e classes para formatar documentos processados em formato Markdown. """ from datetime import datetime from pathlib import Path from typing import Any, Optional from utils.logger import get_logger # Logger para este módulo logger = get_logger(__name__) def format_to_markdown( processed_data: dict[str, Any], include_metadata_header: bool = True, include_tables: bool = True ) -> str: """ Formata dados processados em Markdown. Args: processed_data: Dados retornados pelo DoclingProcessor. include_metadata_header: Se deve incluir cabeçalho com metadados. include_tables: Se deve incluir tabelas formatadas. Returns: String Markdown formatada. """ document = processed_data.get("document") metadata = processed_data.get("metadata", {}) tables = processed_data.get("tables", []) language = processed_data.get("language", "desconhecido") sections = [] # Cabeçalho com metadados if include_metadata_header: header = _format_metadata_header(metadata, language) if header: sections.append(header) # Conteúdo principal do documento if document: try: if hasattr(document, "export_to_markdown"): content = document.export_to_markdown() if content: sections.append(content) except Exception as e: logger.warning(f"Erro ao exportar Markdown: {e}") sections.append(f"> ⚠️ Erro ao exportar conteúdo: {e}") # Tabelas (se não foram incluídas no export padrão) if include_tables and tables: tables_section = _format_tables_section(tables) if tables_section: sections.append(tables_section) return "\n\n---\n\n".join(sections) def _format_metadata_header( metadata: dict[str, Any], language: str ) -> str: """ Formata cabeçalho com metadados. Args: metadata: Dicionário de metadados. language: Código do idioma. Returns: String Markdown com metadados. """ lines = [] # Título titulo = metadata.get("titulo", metadata.get("nome_arquivo", "Documento")) lines.append(f"# {titulo}") lines.append("") # Metadados como lista meta_items = [] if metadata.get("autor"): meta_items.append(f"**Autor:** {metadata['autor']}") if metadata.get("data_criacao"): meta_items.append(f"**Data de criação:** {metadata['data_criacao']}") if language and language not in ("desconhecido", "erro", "nao_detectado"): lang_names = { "pt": "Português", "en": "Inglês", "es": "Espanhol", "fr": "Francês", "de": "Alemão", "it": "Italiano", } lang_name = lang_names.get(language, language.upper()) meta_items.append(f"**Idioma:** {lang_name}") if metadata.get("num_paginas"): meta_items.append(f"**Páginas:** {metadata['num_paginas']}") if metadata.get("num_tabelas"): meta_items.append(f"**Tabelas:** {metadata['num_tabelas']}") if metadata.get("num_imagens"): meta_items.append(f"**Imagens:** {metadata['num_imagens']}") if meta_items: lines.extend(meta_items) lines.append("") return "\n".join(lines) def _format_tables_section(tables: list[dict[str, Any]]) -> str: """ Formata seção de tabelas. Args: tables: Lista de tabelas extraídas. Returns: String Markdown com tabelas. """ if not tables: return "" lines = ["## Tabelas Extraídas", ""] for table in tables: index = table.get("indice", 0) lines.append(f"### Tabela {index}") lines.append("") # Se tem dados como dict/list, formata como tabela MD if table.get("dados"): md_table = _dict_to_markdown_table(table["dados"]) lines.append(md_table) elif table.get("markdown"): lines.append(table["markdown"]) elif table.get("texto"): lines.append(f"```\n{table['texto']}\n```") else: lines.append("*Dados da tabela não disponíveis*") lines.append("") return "\n".join(lines) def _dict_to_markdown_table(data: list[dict[str, Any]]) -> str: """ Converte lista de dicionários em tabela Markdown. Args: data: Lista de dicionários (cada dict = uma linha). Returns: String com tabela em formato Markdown pipe. """ if not data: return "*Tabela vazia*" # Pega colunas do primeiro item headers = list(data[0].keys()) lines = [] # Cabeçalho header_line = "| " + " | ".join(str(h) for h in headers) + " |" lines.append(header_line) # Separador separator = "| " + " | ".join("---" for _ in headers) + " |" lines.append(separator) # Dados for row in data: values = [] for h in headers: value = row.get(h, "") # Escapa pipes no conteúdo value = str(value).replace("|", "\\|") # Remove quebras de linha value = value.replace("\n", " ") values.append(value) row_line = "| " + " | ".join(values) + " |" lines.append(row_line) return "\n".join(lines) class MarkdownFormatter: """ Classe para formatação Markdown com configurações personalizadas. Permite manter configurações consistentes entre múltiplas formatações. """ def __init__( self, include_metadata_header: bool = True, include_tables: bool = True, include_toc: bool = False, max_heading_level: int = 6 ): """ Inicializa o formatador Markdown. Args: include_metadata_header: Se deve incluir cabeçalho com metadados. include_tables: Se deve incluir tabelas extraídas. include_toc: Se deve incluir sumário (Table of Contents). max_heading_level: Nível máximo de heading a usar. """ self.include_metadata_header = include_metadata_header self.include_tables = include_tables self.include_toc = include_toc self.max_heading_level = max_heading_level def format(self, processed_data: dict[str, Any]) -> str: """ Formata dados processados em Markdown. Args: processed_data: Dados do DoclingProcessor. Returns: String Markdown formatada. """ content = format_to_markdown( processed_data, include_metadata_header=self.include_metadata_header, include_tables=self.include_tables ) if self.include_toc: toc = self._generate_toc(content) if toc: content = f"{toc}\n\n---\n\n{content}" return content def _generate_toc(self, content: str) -> str: """ Gera sumário (Table of Contents) do conteúdo. Args: content: Conteúdo Markdown. Returns: String com sumário em Markdown. """ import re lines = [] lines.append("## Sumário") lines.append("") # Encontra headings heading_pattern = r"^(#{1,6})\s+(.+)$" for line in content.split("\n"): match = re.match(heading_pattern, line) if match: level = len(match.group(1)) title = match.group(2) if level <= self.max_heading_level: # Cria link anchor = self._slugify(title) indent = " " * (level - 1) lines.append(f"{indent}- [{title}](#{anchor})") return "\n".join(lines) if len(lines) > 2 else "" def _slugify(self, text: str) -> str: """ Converte texto em slug para anchor. Args: text: Texto a converter. Returns: Slug do texto. """ import re # Converte para lowercase slug = text.lower() # Remove caracteres especiais slug = re.sub(r"[^\w\s-]", "", slug) # Substitui espaços por hífens slug = re.sub(r"\s+", "-", slug) return slug def save_markdown( content: str, output_path: str | Path, encoding: str = "utf-8" ) -> Path: """ Salva conteúdo Markdown em arquivo. Args: content: String Markdown. output_path: Caminho do arquivo de saída. encoding: Encoding do arquivo. Returns: Path para o arquivo salvo. """ output_path = Path(output_path) output_path.write_text(content, encoding=encoding) logger.debug(f"Markdown salvo: {output_path}") return output_path