#!/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( "{html.escape(ext)}
\n" if ext else "") + "{vacio}
" li = [] for it in items: s = sub(it) li.append(f"