#!/usr/bin/env python3 """Escribe en la web lo que sale de tools/extrae.js. Lee el JSON por stdin y, dentro de SITE: - rellena en index.html los bloques entre y para X = comunicados, comisiones, debates - crea comunicados/AAAA-MM-DD-xxxxxxxx.html por comunicado y borra los que ya no están (borrados en OASIS o de alguien que ya no publica) Todo el texto que viene de SSB se escapa. Escritura atómica: nginx nunca sirve un fichero a medias. node tools/extrae.js | python3 tools/genera.py /var/www/acampada26s.net """ import hashlib import html import json import os import re import sys import tempfile from datetime import datetime from urllib.parse import quote from zoneinfo import ZoneInfo TZ = ZoneInfo("Europe/Madrid") ETIQUETA = re.compile(r"#comunicado26s\b", re.I) IMG = re.compile(r"!\[[^\]]*\]\((&[A-Za-z0-9+/=]+\.sha256)\)") ENLACE = re.compile(r"\[([^\]]+)\]\((https?://[^\s)]+)\)") NOMBRE = re.compile(r"^\d{4}-\d{2}-\d{2}-[0-9a-f]{8}\.html$") EXTRACTO = 240 def fecha(ts): return datetime.fromtimestamp(ts / 1000, tz=TZ) def blob(ref): return "/c/blob/" + quote(ref, safe="") def plano(texto): """Texto sin markdown de imágenes/enlaces ni la etiqueta, en una línea.""" texto = IMG.sub("", texto) texto = ENLACE.sub(r"\1", texto) texto = ETIQUETA.sub("", texto) return " ".join(texto.split()) def partir(texto): """Primera línea con contenido = título; el resto = cuerpo.""" lineas = ETIQUETA.sub("", texto).replace("\r", "").split("\n") for i, l in enumerate(lineas): t = plano(l).lstrip("# ").strip() if t: return t[:120], "\n".join(lineas[i + 1:]) return "Comunicado", "" def cuerpo(texto): """Markdown mínimo: párrafos, saltos, imágenes del pub y enlaces http(s).""" out = [] for parrafo in re.split(r"\n\s*\n", texto.replace("\r", "").strip()): if not parrafo.strip(): continue trozos, pos = [], 0 for m in re.finditer(f"{IMG.pattern}|{ENLACE.pattern}", parrafo): trozos.append(html.escape(parrafo[pos:m.start()])) if m.group(1): trozos.append(f'') else: trozos.append(f'' f"{html.escape(m.group(2))}") pos = m.end() trozos.append(html.escape(parrafo[pos:])) p = "".join(trozos).strip().replace("\n", "
\n") out.append(f"

{p}

") return "\n".join(out) def nombre(c): h = hashlib.sha256(c["key"].encode()).hexdigest()[:8] return f"{fecha(c['ts']):%Y-%m-%d}-{h}.html" def cambia(doc, clave, contenido): ini, fin = f"", f"" i, j = doc.find(ini), doc.find(fin) if i < 0 or j < i: sys.exit(f"faltan las marcas {clave}:inicio / {clave}:fin") return doc[:i + len(ini)] + "\n" + contenido + "\n " + doc[j:] def escribe(ruta, texto): try: with open(ruta, encoding="utf-8") as f: if f.read() == texto: return except FileNotFoundError: pass fd, tmp = tempfile.mkstemp(dir=os.path.dirname(ruta), prefix=".tmp.") try: with os.fdopen(fd, "w", encoding="utf-8") as f: f.write(texto) os.chmod(tmp, 0o644) os.replace(tmp, ruta) except BaseException: os.unlink(tmp) raise def lista_comunicados(cs): if not cs: return "

Todavía no hay comunicados.

" li = [] for c in cs: t = fecha(c["ts"]) titulo, resto = partir(c["texto"]) ext = plano(resto) if len(ext) > EXTRACTO: ext = ext[:EXTRACTO].rsplit(" ", 1)[0] + "…" li.append( "
  • \n" f' \n' f'

    {html.escape(titulo)}

    \n' + (f"

    {html.escape(ext)}

    \n" if ext else "") + "
  • ") return '
      \n' + "\n".join(li) + "\n
    " def lista_simple(items, vacio, sub): if not items: return f"

    {vacio}

    " li = [] for it in items: s = sub(it) li.append(f"
  • {html.escape(it['titulo'])}" + (f" · {html.escape(s)}" if s else "") + "
  • ") return ' " def pagina(plantilla, c): t = fecha(c["ts"]) titulo, resto = partir(c["texto"]) art = ( "
    \n" f"

    {html.escape(titulo)}

    \n" f'

    \n' f"{cuerpo(resto)}\n" "
    ") doc = cambia(plantilla, "articulo", " " + art) return doc.replace("Comunicado · Acampada 26S", f"{html.escape(titulo)} · Acampada 26S") def main(): if len(sys.argv) != 2: sys.exit("uso: node tools/extrae.js | genera.py SITE") site = sys.argv[1] datos = json.load(sys.stdin) cs = sorted(datos.get("comunicados", []), key=lambda c: c["ts"], reverse=True) com = sorted(datos.get("comisiones", []), key=lambda c: c["titulo"].lower()) deb = sorted(datos.get("debates", []), key=lambda c: c["ts"], reverse=True) idx = os.path.join(site, "index.html") with open(idx, encoding="utf-8") as f: doc = f.read() doc = cambia(doc, "comunicados", lista_comunicados(cs)) doc = cambia(doc, "comisiones", lista_simple( com, "Las comisiones se crean como tribus dentro de OASIS.", lambda c: plano(c["texto"])[:EXTRACTO])) doc = cambia(doc, "debates", lista_simple( deb, "Los debates se abren como foros dentro de OASIS.", lambda d: d["categoria"])) escribe(idx, doc) dcom = os.path.join(site, "comunicados") os.makedirs(dcom, exist_ok=True) with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "plantilla.html"), encoding="utf-8") as f: plantilla = f.read() quedan = set() for c in cs: n = nombre(c) quedan.add(n) escribe(os.path.join(dcom, n), pagina(plantilla, c)) for n in os.listdir(dcom): if NOMBRE.match(n) and n not in quedan: os.unlink(os.path.join(dcom, n)) if __name__ == "__main__": main()