--site es --link "A=slug-a" --link "B=slug-b" --apply
Sin --apply no escribe: imprime lo que haría con el contexto de cada inserción.
"""
import argparse
import copy
import datetime
import json
import os
import re
import subprocess
import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
SITES = {
"en": {"cli": "ghst-en", "base": "https://www.theexclusionzone.com"},
"es": {"cli": "ghst-es", "base": "https://zonadeexclusion.com"},
}
BDIR = os.path.expanduser("~/link-batch-backup")
# Esquema exacto de los nodos lexical nativos, copiado de un enlace real del
# corpus. No inventar campos: Ghost valida y un nodo mal formado se pierde.
TEXT_NODE = {"detail": 0, "format": 0, "mode": "normal", "style": "",
"text": "", "type": "extended-text", "version": 1}
LINK_NODE = {"children": [], "direction": "ltr", "format": "", "indent": 0,
"type": "link", "version": 1, "rel": None, "target": None,
"title": None, "url": ""}
def sh(cmd, timeout=240):
return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
def fetch_corpus(cli):
"""Corpus completo (published + scheduled). A fichero: un PIPE se trunca."""
fd, path = tempfile.mkstemp(suffix=".json")
os.close(fd)
try:
sh(f"{cli} post list --limit 100 --formats lexical,mobiledoc,html --json > {path}")
with open(path) as f:
d = json.load(f)
return d.get("posts", d) if isinstance(d, dict) else d
finally:
os.unlink(path)
# ---------- detección de formato ----------
def body_format(post):
lex = post.get("lexical")
if lex:
kids = json.loads(lex)["root"]["children"]
if len(kids) == 1 and kids[0].get("type") == "html":
return "lexical-html"
return "lexical-nativo"
if post.get("mobiledoc"):
return "mobiledoc"
return None
# ---------- inserción en HTML (vale para lexical-html y mobiledoc) ----------
def link_in_html(html, anchor, url):
"""Primer con el anchor FUERA de cualquier . -> (html, ok, contexto)"""
hecho = [False]
ctx = [""]
def en_parrafo(m):
if hecho[0]:
return m.group(0)
trozos = re.split(r"()", m.group(2), flags=re.S)
for i, tr in enumerate(trozos):
if tr.startswith("{anchor}' + tr[mm.end():]
a, b = max(0, mm.start() - 55), mm.end() + 55
ctx[0] = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", "", tr[a:b]))
hecho[0] = True
break
return m.group(1) + "".join(trozos) + m.group(3)
out = re.sub(r"(
)(.*?)(
)", en_parrafo, html, flags=re.S)
return out, hecho[0], ctx[0]
# ---------- inserción en lexical nativo ----------
def link_in_lexical_nodes(root_children, anchor, url):
"""Parte el nodo de texto en (antes | link | después). Solo mira los hijos
DIRECTOS del párrafo, así que un texto que ya viva dentro de un `link`
nunca se toca — que es justo lo que evita el anidamiento."""
for parrafo in root_children:
if parrafo.get("type") != "paragraph":
continue
hijos = parrafo.get("children") or []
for i, nodo in enumerate(hijos):
if nodo.get("type") != "extended-text":
continue
txt = nodo.get("text") or ""
mm = re.search(r"(?]*>(?:(?!