Files
scripts/okf-hub/okf-convert-wikilinks.py
T

155 lines
5.3 KiB
Python

#!/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()