docling-processor / processors /markdown_formatter.py
Gabriel Ramos
feat: Docling Document Processor - Gradio + ZeroGPU
780413d
"""
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