#!/usr/bin/env python3 """ okf-convert-wikilinks.py — Fase 3: Converte [[wikilinks]] → [texto](path.md) nos index.md OKF §5: links bundle-relative para navegação entre conceitos Âmbito: apenas ficheiros index.md (navegação) Corpo de documentos (PROC, QR, etc.) mantém wikilinks — OKF tolera e Obsidian renderiza ambos. Uso: python3 okf-convert-wikilinks.py [--dry-run] [--dir /path/to/Hub] Criado: 28-06-2026 """ import os import re import sys from pathlib import Path HUB_DEFAULT = "/media/ealmeida/Dados/Hub" EXCLUDE_DIRS = {".stversions", "node_modules", ".git", ".obsidian", ".trash"} # Padrão wikilink: [[NomeFicheiro]] ou [[NomeFicheiro|Alias]] WIKILINK_RE = re.compile(r'\[\[([^\]|]+)(?:\|([^\]]+))?\]\]') def build_file_index(hub: Path) -> dict: """Constrói índice nome→path para resolução de wikilinks.""" index = {} # stem → Path relativo ao hub for root, dirs, files in os.walk(hub): dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS and not d.startswith(".")] for fname in files: if fname.endswith(".md"): fp = Path(root) / fname stem = fp.stem.lower() rel = fp.relative_to(hub) # Guardar o primeiro match (mais provável no vault activo) if stem not in index: index[stem] = rel # Também indexar o nome completo sem extensão full_name = fname.lower() if full_name not in index: index[full_name] = rel return index def resolve_wikilink(target: str, current_file: Path, file_index: dict, hub: Path) -> str: """Resolve [[target]] para um caminho relativo ao ficheiro actual.""" # Limpar o target (remover ^anchor, #heading, etc.) target_clean = re.split(r'[#^]', target)[0].strip() target_lower = target_clean.lower() target_with_ext = target_lower + ".md" if not target_lower.endswith(".md") else target_lower # Tentar resolver resolved = file_index.get(target_with_ext) or file_index.get(target_lower) if resolved: # Calcular path relativo a partir do directório do ficheiro actual try: rel_path = os.path.relpath(hub / resolved, current_file.parent) return rel_path.replace("\\", "/") except Exception: return str(resolved) return None def convert_wikilinks_in_file(filepath: Path, file_index: dict, hub: Path, dry_run: bool) -> dict: """Converte wikilinks no ficheiro. Retorna estatísticas.""" result = {"file": str(filepath.relative_to(hub)), "converted": 0, "unresolved": [], "action": "skip"} try: content = filepath.read_text(encoding="utf-8") except Exception as e: result["action"] = "error" result["error"] = str(e) return result if "[[" not in content: result["action"] = "no_wikilinks" return result def replace_wikilink(m): target = m.group(1) alias = m.group(2) display = alias if alias else target resolved_path = resolve_wikilink(target, filepath, file_index, hub) if resolved_path: result["converted"] += 1 return f"[{display}]({resolved_path})" else: # Manter como wikilink se não resolvível result["unresolved"].append(target) return m.group(0) new_content = WIKILINK_RE.sub(replace_wikilink, content) if new_content != content: result["action"] = "converted" if not dry_run: filepath.write_text(new_content, encoding="utf-8") else: result["action"] = "no_changes" return result def main(): dry_run = "--dry-run" in sys.argv hub = Path(HUB_DEFAULT) for arg in sys.argv[1:]: if arg.startswith("--dir="): hub = Path(arg[6:]) if not hub.exists(): print(f"ERRO: Hub não encontrado em {hub}", file=sys.stderr) sys.exit(1) print(f"{'[DRY-RUN] ' if dry_run else ''}A construir índice de ficheiros…") file_index = build_file_index(hub) print(f" {len(file_index)} ficheiros indexados") print(f"A converter wikilinks nos index.md…") total_converted = 0 total_unresolved = [] files_changed = 0 for root, dirs, files in os.walk(hub): dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS and not d.startswith(".")] for fname in files: if fname != "index.md": continue filepath = Path(root) / fname result = convert_wikilinks_in_file(filepath, file_index, hub, dry_run) if result["action"] == "converted": files_changed += 1 total_converted += result["converted"] total_unresolved.extend(result["unresolved"]) print(f" [OK] {result['file']}: {result['converted']} convertidos" + (f", {len(result['unresolved'])} não resolvidos" if result["unresolved"] else "")) elif result["action"] == "error": print(f" [ERRO] {result['file']}: {result.get('error')}") print(f"\n=== Resultado ===") print(f"Ficheiros alterados: {files_changed}") print(f"Wikilinks convertidos: {total_converted}") if total_unresolved: print(f"Não resolvidos ({len(total_unresolved)}): {', '.join(set(total_unresolved))[:200]}") if __name__ == "__main__": main()