Contenido desde OASIS y colectivos
Comunicados, comisiones y debates salen del pub: los publica quien es miembro de la tribu de la acampada y además sigue la cuenta de la acampada. Directo enlaza al HUB. Lista de centros sociales y colectivos de Madrid con enlaces comprobados.
This commit is contained in:
parent
f78773bb87
commit
cb551e2a3c
13 changed files with 505 additions and 261 deletions
193
tools/genera.py
Executable file
193
tools/genera.py
Executable file
|
|
@ -0,0 +1,193 @@
|
|||
#!/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 <!-- X:inicio --> y <!-- X:fin -->
|
||||
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'<img src="{html.escape(blob(m.group(1)))}" alt="" loading="lazy">')
|
||||
else:
|
||||
trozos.append(f'<a href="{html.escape(m.group(3))}" rel="noopener noreferrer">'
|
||||
f"{html.escape(m.group(2))}</a>")
|
||||
pos = m.end()
|
||||
trozos.append(html.escape(parrafo[pos:]))
|
||||
p = "".join(trozos).strip().replace("\n", "<br>\n")
|
||||
out.append(f" <p>{p}</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"<!-- {clave}:inicio -->", f"<!-- {clave}:fin -->"
|
||||
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 " <p>Todavía no hay comunicados.</p>"
|
||||
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(
|
||||
" <li>\n"
|
||||
f' <time datetime="{t:%Y-%m-%d}">{t:%d.%m.%Y}</time>\n'
|
||||
f' <h3><a href="/comunicados/{nombre(c)}">{html.escape(titulo)}</a></h3>\n'
|
||||
+ (f" <p>{html.escape(ext)}</p>\n" if ext else "")
|
||||
+ " </li>")
|
||||
return ' <ol class="lista">\n' + "\n".join(li) + "\n </ol>"
|
||||
|
||||
|
||||
def lista_simple(items, vacio, sub):
|
||||
if not items:
|
||||
return f" <p>{vacio}</p>"
|
||||
li = []
|
||||
for it in items:
|
||||
s = sub(it)
|
||||
li.append(f" <li><strong>{html.escape(it['titulo'])}</strong>"
|
||||
+ (f" · {html.escape(s)}" if s else "") + "</li>")
|
||||
return ' <ul class="lista">\n' + "\n".join(li) + "\n </ul>"
|
||||
|
||||
|
||||
def pagina(plantilla, c):
|
||||
t = fecha(c["ts"])
|
||||
titulo, resto = partir(c["texto"])
|
||||
art = (
|
||||
"<article>\n"
|
||||
f" <h1>{html.escape(titulo)}</h1>\n"
|
||||
f' <p><time datetime="{t.isoformat(timespec="minutes")}">{t:%d.%m.%Y %H:%M}</time></p>\n'
|
||||
f"{cuerpo(resto)}\n"
|
||||
" </article>")
|
||||
doc = cambia(plantilla, "articulo", " " + art)
|
||||
return doc.replace("<title>Comunicado · Acampada 26S</title>",
|
||||
f"<title>{html.escape(titulo)} · Acampada 26S</title>")
|
||||
|
||||
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue