216 lines
8.3 KiB
Python
Executable File
216 lines
8.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Autor: Descomplicar - Agência de Aceleração Digital
|
|
https://descomplicar.pt
|
|
|
|
Conversor de Markdown para DOCX e PDF - v9.0 FINAL
|
|
Resolve: páginas em branco, TOC, links preservados
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
import subprocess
|
|
import argparse
|
|
import tempfile
|
|
|
|
# Importar configurações
|
|
from config import DOCX_OUTPUT_DIR, PDF_OUTPUT_DIR, PROCESSING_CONFIG
|
|
|
|
# Imports que precisam do venv
|
|
try:
|
|
import pypandoc
|
|
from docx import Document
|
|
from docx.shared import Inches, Pt
|
|
from docx.enum.text import WD_BREAK
|
|
from docx.oxml.shared import qn
|
|
except ImportError:
|
|
print("Erro: Dependências não instaladas. Execute o script de instalação ou ative o venv.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def check_dependencies():
|
|
"""Verifica se as dependências de sistema estão instaladas."""
|
|
global libreoffice_available
|
|
libreoffice_available = True
|
|
if not shutil.which("pandoc"):
|
|
sys.exit("Erro: Pandoc não está instalado.")
|
|
if not shutil.which("libreoffice"):
|
|
print("Aviso: LibreOffice não encontrado. A conversão para PDF será ignorada.")
|
|
libreoffice_available = False
|
|
|
|
|
|
class DOCXConverter:
|
|
"""Conversor final com todos os problemas resolvidos."""
|
|
|
|
def __init__(self, input_path):
|
|
self.input_path = Path(input_path)
|
|
if not self.input_path.exists():
|
|
raise FileNotFoundError(f"Ficheiro de entrada não encontrado: {self.input_path}")
|
|
|
|
base_name = self.input_path.stem
|
|
self.output_docx_path = Path(DOCX_OUTPUT_DIR) / f"{base_name}.docx"
|
|
self.output_pdf_path = Path(PDF_OUTPUT_DIR) / f"{base_name}.pdf"
|
|
self.master_template_path = Path(__file__).resolve().parent / "template_mestre.docx"
|
|
|
|
def convert(self):
|
|
"""Orquestra o processo de conversão completo."""
|
|
print(f"A processar '{self.input_path.name}'...")
|
|
try:
|
|
self._generate_guide_from_template()
|
|
if libreoffice_available:
|
|
self._convert_to_pdf()
|
|
print(f"'{self.input_path.name}' convertido com sucesso para '{self.output_docx_path}'.")
|
|
except Exception as e:
|
|
print(f"ERRO FATAL ao processar '{self.input_path.name}': {e}", file=sys.stderr)
|
|
|
|
def _generate_guide_from_template(self):
|
|
"""Estratégia FINAL: Gerar documento completo com template + TOC + conteúdo + links."""
|
|
print(f"A combinar template com conteúdo para '{self.input_path.name}'...")
|
|
|
|
# 1. Ler e processar o markdown
|
|
try:
|
|
full_content = self.input_path.read_text(encoding='utf-8')
|
|
content_without_yaml = re.sub(r'^---\s*.*?\s*---\s*', '', full_content, flags=re.DOTALL).strip()
|
|
except Exception as e:
|
|
raise IOError(f"Erro ao ler o ficheiro Markdown: {e}")
|
|
|
|
# 2. Converter markdown para DOCX com TODOS os recursos
|
|
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as temp_docx:
|
|
temp_docx_path = temp_docx.name
|
|
|
|
try:
|
|
# Configuração COMPLETA do Pandoc
|
|
pandoc_args = [
|
|
'--standalone',
|
|
'--from=markdown+raw_html+auto_identifiers+smart',
|
|
'--to=docx',
|
|
'--table-of-contents',
|
|
'--toc-depth=3',
|
|
'--variable=toc-title:Índice',
|
|
'--preserve-tabs',
|
|
'--wrap=none',
|
|
]
|
|
|
|
pypandoc.convert_text(
|
|
content_without_yaml,
|
|
'docx',
|
|
format='markdown',
|
|
outputfile=temp_docx_path,
|
|
extra_args=pandoc_args
|
|
)
|
|
print(f"✅ Conteúdo convertido para DOCX com TOC e links")
|
|
except Exception as e:
|
|
os.remove(temp_docx_path)
|
|
raise RuntimeError(f"Erro na conversão Pandoc: {e}")
|
|
|
|
# 3. Estratégia FINAL: Combinar preservando TUDO
|
|
try:
|
|
self._final_combination_strategy(temp_docx_path)
|
|
print(f"✅ Documento final criado com template + TOC + conteúdo + links")
|
|
except Exception as e:
|
|
raise RuntimeError(f"Erro na combinação final: {e}")
|
|
finally:
|
|
os.remove(temp_docx_path)
|
|
|
|
def _final_combination_strategy(self, content_docx_path):
|
|
"""Estratégia final que preserva template, TOC e links."""
|
|
|
|
# Carregar documentos
|
|
template_doc = Document(self.master_template_path)
|
|
content_doc = Document(content_docx_path)
|
|
|
|
print(f"📄 Template: {len(template_doc.paragraphs)} parágrafos")
|
|
print(f"📄 Conteúdo: {len(content_doc.paragraphs)} parágrafos")
|
|
|
|
# Verificar se o Pandoc gerou TOC
|
|
toc_found = False
|
|
for i, para in enumerate(content_doc.paragraphs[:10]):
|
|
if 'índice' in para.text.lower() or 'contents' in para.text.lower():
|
|
print(f"✅ TOC encontrado no Pandoc no parágrafo {i}")
|
|
toc_found = True
|
|
break
|
|
|
|
# Contar links no conteúdo
|
|
links_count = 0
|
|
for rel in content_doc.part.rels.values():
|
|
if "hyperlink" in str(rel.reltype):
|
|
links_count += 1
|
|
print(f"🔗 Links encontrados no conteúdo: {links_count}")
|
|
|
|
# ESTRATÉGIA: Usar o conteúdo do Pandoc como base e adicionar branding do template
|
|
final_doc = content_doc # Usar o documento com TOC e links
|
|
|
|
# Extrair elementos de branding do template (logos, cabeçalhos, etc.)
|
|
template_elements = []
|
|
for para in template_doc.paragraphs:
|
|
if para.text.strip() and para.text.strip() != "{{conteudo_guia}}":
|
|
template_elements.append(para.text)
|
|
|
|
if template_elements:
|
|
print(f"📋 Elementos de branding encontrados: {len(template_elements)}")
|
|
|
|
# Adicionar elementos do template no início
|
|
for i, element_text in enumerate(reversed(template_elements)):
|
|
if element_text.strip():
|
|
new_para = final_doc.paragraphs[0]._element
|
|
new_para = final_doc.element.body.insert(0, new_para)
|
|
# Criar novo parágrafo
|
|
p = final_doc.add_paragraph(element_text)
|
|
# Mover para o início
|
|
final_doc.element.body.insert(0, p._element)
|
|
|
|
# Adicionar quebra de página após elementos do template
|
|
if final_doc.paragraphs:
|
|
last_template_para = final_doc.paragraphs[len(template_elements)]
|
|
run = last_template_para.add_run()
|
|
run.add_break(WD_BREAK.PAGE)
|
|
|
|
# Guardar documento final
|
|
final_doc.save(self.output_docx_path)
|
|
print(f"💾 Documento final: template + TOC + conteúdo + {links_count} links")
|
|
|
|
def _convert_to_pdf(self):
|
|
"""Converte DOCX para PDF."""
|
|
print(f"A converter '{self.output_docx_path.name}' para PDF...")
|
|
try:
|
|
subprocess.run(
|
|
['libreoffice', '--headless', '--convert-to', 'pdf', '--outdir', str(PDF_OUTPUT_DIR), str(self.output_docx_path)],
|
|
check=True, capture_output=True, text=True
|
|
)
|
|
print(f"✅ PDF criado: {self.output_pdf_path}")
|
|
except Exception as e:
|
|
print(f"Aviso: Erro na conversão para PDF: {e}")
|
|
|
|
def main():
|
|
"""Ponto de entrada principal."""
|
|
parser = argparse.ArgumentParser(description='Converte ficheiros Markdown para DOCX e PDF com branding.')
|
|
parser.add_argument('path', type=str, help='Caminho para o ficheiro .md ou pasta.')
|
|
args = parser.parse_args()
|
|
|
|
check_dependencies()
|
|
target_path = Path(args.path)
|
|
|
|
if not target_path.exists():
|
|
sys.exit(f"Erro: O caminho '{target_path}' não existe.")
|
|
|
|
files_to_process = []
|
|
if target_path.is_dir():
|
|
files_to_process.extend(sorted(target_path.glob("*.md")))
|
|
elif target_path.is_file() and target_path.suffix == '.md':
|
|
files_to_process.append(target_path)
|
|
|
|
if not files_to_process:
|
|
print("Nenhum ficheiro .md encontrado.")
|
|
return
|
|
|
|
print(f"Encontrados {len(files_to_process)} ficheiro(s).\n")
|
|
for md_file in files_to_process:
|
|
print("-" * 50)
|
|
DOCXConverter(md_file).convert()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main() |