1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
"""Builds the self-contained HTML page from the sessions.
The result needs neither network nor server: the data and the transcripts are
embedded inside and it opens with a double click.
"""
import json
from importlib import resources
MARKER = "__DATA__"
TEMPLATE_NAME = "template.html"
class TemplateError(Exception):
"""The template does not have the shape the generator expects."""
def template_text(path=None):
if path:
with open(path, encoding="utf-8") as f:
return f.read()
return (resources.files(__package__) / TEMPLATE_NAME).read_text(encoding="utf-8")
def build_payload(sessions, memories=None):
"""Structure that travels embedded in the page.
An object rather than a list because the page shows two different things:
sessions and project memories.
"""
return {"s": list(sessions), "m": list(memories or ())}
def encode_payload(payload):
"""Serializes the payload to put it inside a <script type=application/json>.
The HTML parser cuts that block at the first "</script", and transcripts
contain HTML. "</" is escaped as "<\\/", which is a valid JSON escape:
JSON.parse returns it intact.
"""
raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
return raw.replace("</", "<\\/")
def render(records, memories=None, template=None):
"""Returns the full HTML with the data already embedded."""
html = template if template is not None else template_text()
# The result cannot be checked by looking for the marker: these same
# sessions include conversations about this script, so the payload
# contains it as text. Validate the template before substituting.
if html.count(MARKER) != 1:
raise TemplateError(
f"el template debe tener exactamente un {MARKER} "
f"(encontrados: {html.count(MARKER)})")
return html.replace(MARKER, encode_payload(build_payload(records, memories)))
def write(records, out_path, memories=None, template=None):
"""Writes the page and returns a summary of what went into it."""
html = render(records, memories=memories, template=template)
with open(out_path, "w", encoding="utf-8") as f:
f.write(html)
return summary(records, memories)
def summary(records, memories=None):
return {
"sesiones": len(records),
"proyectos": len({r["p"] for r in records}),
"mensajes": sum(r["u"] for r in records),
"bloques": sum(len(r["c"]) for r in records),
"memorias": len(memories or ()),
}
|