155 lines
5.3 KiB
Python
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()
|