feat(okf-hub): relocar tooling OKF do Hub para Dev/Scripts (regra: scripts fora do vault)
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user