Seed docs/legal-archive with Terms / Privacy / Imprint snapshots

Adds a simple legal-archive folder that preserves the full text of the
Terms, Privacy Policy, and Impressum at each version, plus a small
renderer script so future snapshots are mechanical:

    python3 scripts/render-legal.py terms > docs/legal-archive/terms-YYYY-MM-DD.md

Why:
- Terms are per-user contract. users.terms_version maps each user to a
  file in this folder so we can always tell what text they agreed to.
- Privacy is informational but GDPR Art. 13/14 still expects us to be
  able to say what the policy disclosed on a given date.
- Impressum archived for symmetry; cheap.

How it works:
- scripts/render-legal.py reads the TSX source, resolves operator.*
  placeholders from operator.ts and TERMS_VERSION / PRIVACY_LAST_UPDATED
  from legal.ts, strips JSX tags, and prints clean markdown to stdout.
- A README in the folder explains when to bump and when to add a
  snapshot (material change only, not typo fixes).

Seeded entries:
- terms-2026-04-19.md
- privacy-2026-04-19.md
- imprint-2026-04-19.md

No automation / CI check yet — trust-based process documented in the
folder's README. Revisit if discipline slips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-19 07:51:41 +02:00
parent c6c5ad3765
commit d52828543a
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
5 changed files with 773 additions and 0 deletions

View file

@ -0,0 +1,53 @@
# Legal archive
Frozen snapshots of the Terms of Service, Privacy Policy, and Impressum at
each version. Kept so we can always answer "what did the Terms / Privacy
text say on a given date" without digging through git blame.
## Why
- **Terms**: users accept a specific version at registration
(`users.terms_accepted_at` + `users.terms_version`). The version string
is the date in this folder — so there is always a file here matching
every value that exists in that column.
- **Privacy**: users don't "accept" a privacy policy, but GDPR Art. 13/14
requires us to tell users how their data is processed *at the time* it's
processed. If a regulator or user asks what the policy said on
YYYY-MM-DD, this folder answers.
- **Impressum**: less legally critical, but free to snapshot for symmetry.
## When to add a file
Whenever any of these three texts change materially, run the snapshot step
as part of the change:
1. Bump `TERMS_VERSION` / `PRIVACY_LAST_UPDATED` in
[`apps/journal/app/lib/legal.ts`](../../apps/journal/app/lib/legal.ts)
to today's date.
2. Re-render the affected page(s) into this folder as
`<doc>-YYYY-MM-DD.md`.
3. Commit the bump **and** the new snapshot in the same PR.
A trivial wording tweak or typo fix does not need a new snapshot; only
changes that affect meaning / behaviour / purposes / third parties / legal
basis / retention / etc.
## File naming
`<doc>-YYYY-MM-DD.md` where `<doc>` is one of `terms`, `privacy`,
`imprint`, and the date matches the `Last updated` line in the
corresponding legal page on the day the snapshot was taken.
## How to render
The snapshots are plain markdown extracted from the TSX source by
stripping JSX tags / attributes and resolving the `operator.*`
placeholders. Same approach as the pbcopy export used during legal
reviews. One-liner (from repo root):
```bash
python3 scripts/render-legal.py <doc> > docs/legal-archive/<doc>-YYYY-MM-DD.md
```
(If `scripts/render-legal.py` doesn't exist yet, the extraction logic is
in the commit that seeded this directory — see PR history.)

View file

@ -0,0 +1,83 @@
# Impressum — trails.cool
Impressum / Legal Notice
Die deutsche Fassung ist maßgeblich. / The German version is authoritative.
Anbieter nach § 5 TMG
Ullrich Schäfer
Mehringdamm 87
10965 Berlin
Germany
Kontakt
E-Mail:
legal@trails.cool
Inhaltlich verantwortlich nach § 18 Abs. 2 MStV
Ullrich Schäfer
Mehringdamm 87
10965 Berlin
Streitbeilegung
Die Europäische Kommission stellt eine Plattform zur
Online-Streitbeilegung (OS) bereit:
https://ec.europa.eu/consumers/odr
.
Wir sind nicht bereit oder verpflichtet, an
Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle
teilzunehmen.
Alpha-Status
trails.cool befindet sich in aktiver Entwicklung (Alpha). Inhalte,
Funktionen und Daten können sich jederzeit ändern oder entfallen.
English version
Service provider (§ 5 TMG)
Ullrich Schäfer
Mehringdamm 87
10965 Berlin
Germany
Contact
Email:
legal@trails.cool
Content responsibility (§ 18 (2) MStV)
Ullrich Schäfer, address as above.
Dispute resolution
The European Commission provides an online dispute resolution platform at
https://ec.europa.eu/consumers/odr
. We are not willing or obliged to participate in dispute resolution
proceedings before a consumer arbitration board.
Alpha status
trails.cool is under active development (alpha). Features and data
may change or be removed at any time.

View file

@ -0,0 +1,358 @@
# Privacy Policy — trails.cool
Datenschutzerklärung / Privacy Policy
Stand / Last updated: 2026-04-19. Die deutsche Fassung ist maßgeblich.
The German version is authoritative; English summaries follow each
section.
1. Verantwortlicher
Verantwortlich für die Datenverarbeitung im Sinne der DSGVO ist:
Ullrich Schäfer
Mehringdamm 87
10965 Berlin
Germany
E-Mail:
legal@trails.cool
English.
Data controller under GDPR is the party named
above. Contact for any data-protection matter: legal@trails.cool.
2. Erhobene Daten und Zwecke
Wir verarbeiten nur die Daten, die für den Betrieb der jeweiligen
Funktion erforderlich sind. trails.cool besteht aus zwei Teilen:
dem
Journal
(trails.cool, mit Konto) und dem
Planner
(planner.trails.cool, anonym).
Kontodaten (Journal):
E-Mail-Adresse,
Benutzername, Anzeigename, Passkey-Public-Key. Zweck:
Kontoverwaltung und Anmeldung.
Nutzerinhalte (Journal):
Routen (GPX-Daten,
Geometrie, Titel, Beschreibung) und Aktivitäten (Titel,
Beschreibung, Datum, Verknüpfung zu Routen). Zweck: Speicherung
und Anzeige innerhalb des Dienstes.
Anmeldedaten (Journal):
kurzlebige Magic-Link-Token
(zur E-Mail-basierten Anmeldung). Zweck: Authentifizierung.
Sitzungscookie (Journal):
eine zufällige
Sitzungs-ID nach dem Einloggen. Zweck: Authentifizierung während
der Sitzung.
Planner-Sitzungsdaten:
anonyme Sitzungs-ID,
kollaborativer Zustand (Wegpunkte, Notizen). Keine Zuordnung zu
einer Person. Zweck: gemeinsames Planen von Routen.
Server-Logfiles:
IP-Adresse, Zeitstempel,
HTTP-Methode, Pfad, Statuscode, User-Agent. Zweck: Sicherheit,
Betrieb, Fehlersuche. Details siehe Abschnitt 4.
Fehlerdaten (Sentry):
Stacktraces,
Fehlermeldungen, Browser-/OS-Information aus dem User-Agent,
Performance-Metriken. Bei eingeloggten Journal-Nutzer:innen
zusätzlich die Nutzer-ID (keine E-Mail, kein Benutzername).
IP-Adressen werden nicht aktiv gespeichert, ebenso keine Cookies
und keine Formulareingaben.
English.
The Journal stores only what you provide (account
details and your own routes/activities) plus short-lived auth
artefacts. The Planner is anonymous and holds only ephemeral
session state. Server logs and Sentry error data are covered
separately below.
3. Rechtsgrundlagen
Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung):
Kontoführung, Anmeldung per Passkey oder Magic Link, Speicherung
und Anzeige der von Ihnen erstellten Routen und Aktivitäten,
Versand notwendiger Transaktions-E-Mails, Bereitstellung anonymer
Planner-Sitzungen. Ein Konto sowie die Bestätigung der
Nutzungsbedingungen sind Bestandteil dieses Nutzungsvertrags
sie sind
keine
Einwilligung im Sinne von Art. 6 Abs. 1
lit. a DSGVO.
Art. 6 Abs. 1 lit. f DSGVO (berechtigte Interessen):
kurzzeitige Server-Logfiles zur Sicherung des Betriebs und zur
Missbrauchsabwehr, Rate-Limiting, Fehlermonitoring über Sentry
zur Sicherstellung der Funktionsfähigkeit des Dienstes.
English.
Contract (Art. 6(1)(b)) covers everything
account- and content-related; legitimate interests (Art. 6(1)(f))
cover short-lived server logs, rate-limiting, and error monitoring.
We do
not
rely on consent for any of this.
4. Server-Logfiles
Beim Aufruf der Dienste werden automatisch technische Informationen
protokolliert:
IP-Adresse
Zeitstempel
HTTP-Methode, Pfad, Statuscode
User-Agent (Browser, Betriebssystem)
Zweck:
Sicherheit, Betrieb und Fehlersuche.
Rechtsgrundlage:
Art. 6 Abs. 1 lit. f DSGVO.
Speicherdauer:
maximal 14 Tage, danach automatische
Löschung. Eine Zusammenführung dieser Daten mit anderen Datenquellen
findet nicht statt.
English.
HTTP requests to our servers are logged (IP,
timestamp, method, path, status, user-agent) for up to 14 days for
operational and security purposes under Art. 6(1)(f), then deleted.
5. Speicherdauer
Konto und zugehörige Inhalte: bis zur Löschung durch Sie
Planner-Sitzungen: automatische Löschung nach 7 Tagen Inaktivität
Magic-Link-Token: 15 Minuten
Server-Logfiles: maximal 14 Tage
Fehlerdaten (Sentry): 90 Tage
Hinweis Alpha: Während der Alpha-Phase behält sich der Betreiber
ausdrücklich vor, die Datenbank zurückzusetzen oder einzelne
Datensätze zu löschen. Dies kann zu Datenverlust führen, bevor Sie
eine Löschung veranlassen. Details dazu in den Nutzungsbedingungen.
English.
Account and content kept until you delete them.
Ephemeral data (sessions, magic-link tokens, logs, Sentry events)
deleted automatically on the schedules above. Alpha caveat: the
operator may reset the database or delete individual records
during alpha, which can cause data loss before you request
deletion. See the Terms of Service.
6. Empfänger und Drittanbieter
Wir geben personenbezogene Daten nur an die unten genannten
Auftragsverarbeiter und Dritten weiter, und auch dort nur im
jeweils notwendigen Umfang.
Sentry
(Functional Software Inc.) Fehler- und
Performance-Monitoring. Was übermittelt wird: Stacktraces,
Fehlertext, Browser-/OS-Informationen aus dem User-Agent,
Performance-Daten; bei eingeloggten Journal-Nutzer:innen
zusätzlich die Nutzer-ID. IP-Adressen werden nicht aktiv
gespeichert (
sendDefaultPii
ist deaktiviert), ebenso
keine Cookies und keine vollständigen HTTP-Header. Keine
Session-Replays. Sentry agiert als externer Dienstleister
(Auftragsverarbeiter) für die Fehlerbehandlung. Da Sentry ein
Anbieter mit Sitz in den USA ist, kann im Einzelfall eine
Übermittlung personenbezogener Daten in ein Drittland im Sinne
der Art. 44 ff. DSGVO stattfinden; wir stützen uns hierbei auf
die von Sentry bereitgestellten
Standardvertragsklauseln.
OpenStreetMap
Kartenkacheln werden beim Anzeigen
der Karten direkt vom Browser von OSM-Tile-Servern geladen. Dabei
werden
IP-Adresse und User-Agent
an OSM übertragen.
Dies ist notwendig, um überhaupt eine Karte darzustellen; es gilt
die
OSM Foundation Privacy Policy
.
Overpass API
(POI-Daten) POI-Abfragen laufen
serverseitig über unsere eigene Route
/api/overpass
.
Der Upstream-Dienst sieht nur unsere Server-IP, nicht die Ihrer
Nutzer:innen. Aktueller Upstream:
overpass.private.coffee
, der ohne Query-Logs arbeitet. Eine selbst gehostete Instanz ist
geplant.
BRouter
Routenberechnung läuft auf einer von uns
selbst gehosteten Instanz. Keine Weitergabe an Dritte.
E-Mail-Versand
Transaktions-E-Mails (Magic
Link, Willkommensnachricht) werden über einen konfigurierten
externen SMTP-Dienst versendet. Dabei wird die E-Mail-Adresse
der Empfänger:in an diesen Dienst übergeben. Selbst-betriebene
Instanzen konfigurieren ihren eigenen Mailserver.
Hosting
Die Dienste werden in Rechenzentren
innerhalb der EU betrieben. Ein Auftragsverarbeitungsvertrag mit
dem Hoster besteht.
English.
Third parties and what they receive: Sentry (error
details, no IPs/cookies); OpenStreetMap tile servers
(your IP and user-agent, directly from your browser, to load map
tiles); Overpass (via our server-side proxy, so upstream only sees
our server); BRouter (self-hosted, no third party involved); SMTP
provider (your email address for magic link / welcome mail);
hosting provider in the EU under a DPA.
7. Ihre Rechte
Als betroffene Person stehen Ihnen die folgenden Rechte zu:
Auskunft (Art. 15 DSGVO)
Berichtigung (Art. 16 DSGVO)
Löschung (Art. 17 DSGVO)
Einschränkung der Verarbeitung (Art. 18 DSGVO)
Datenübertragbarkeit (Art. 20 DSGVO)
Widerspruch gegen Verarbeitung auf Basis berechtigter Interessen (Art. 21 DSGVO)
Zur Ausübung dieser Rechte genügt eine formlose E-Mail an
legal@trails.cool
. Einen vollständigen Export Ihrer Routen und Aktivitäten können Sie
jederzeit direkt in den Kontoeinstellungen herunterladen (GPX bzw.
JSON). Ihr Konto samt Inhalten können Sie dort ebenfalls selbst
löschen.
English.
You have the standard GDPR rights (access,
rectification, erasure, restriction, portability, objection). Email
us to exercise them. Data exports and account deletion are also
available directly in your account settings.
8. Beschwerderecht
Sie haben das Recht, sich bei einer Datenschutzaufsichtsbehörde zu
beschweren. Für uns zuständig ist:
Berliner Beauftragte für Datenschutz und Informationsfreiheit
Friedrichstr. 219
10969 Berlin
www.datenschutz-berlin.de
English.
You have the right to lodge a complaint with a
supervisory authority; ours is the Berlin data-protection
commissioner (address above).
Privacy Manifest
A plain-language summary of what we do, for readers who prefer
behaviour over paragraphs. Binding text is above.
Planner (planner.trails.cool)
Anonymous by design. No account, no identifier, nothing persisted
past the session.
No cookies, no localStorage, no sessionStorage
No browser-side error tracking (Sentry is not loaded)
Session data is deleted automatically after 7 days of inactivity
Journal (trails.cool)
Holds only what you put in. Exportable any time, deletable any time.
Account: email, username, display name, passkey public key
Routes: GPX, geometry, title, description
Activities: title, description, date, linked route
GPX / JSON export available per object and overall
Sentry
Journal server: always on
Journal browser: only after login, torn down on logout
Planner: server only; the browser never loads Sentry
IPs not actively stored, no cookies, no full headers (
sendDefaultPii: false
)
No replays (replay integration not installed, sample rates 0)
User ID only (no email/username) on Journal-logged-in events
What we don't do
Sell data
Show ads
Build user profiles
Run tracking pixels, analytics, or A/B tests
Security
Auth via passkey (WebAuthn) or magic link — no passwords stored
HTTPS + HSTS preload on all origins; session cookies httpOnly + Secure
CSP, X-Frame-Options, X-Content-Type-Options on every response
Containers run non-root; host firewall restricts to HTTP/HTTPS/SSH
Gitleaks + dependency audit on every PR
Vulnerability reports:
SECURITY.md

View file

@ -0,0 +1,157 @@
# Terms of Service — trails.cool
Nutzungsbedingungen / Terms of Service
Stand / Last updated: 2026-04-19 • Alpha — subject to change. Die
deutsche Fassung ist maßgeblich. The German version is authoritative;
English summaries follow each section.
1. Gegenstand und Alpha-Status
trails.cool ist ein experimenteller, derzeit kostenloser Dienst zur Planung
und Dokumentation von Outdoor-Aktivitäten. Der Dienst befindet sich
in aktiver Entwicklung (Alpha). Funktionen, Schnittstellen und
gespeicherte Daten können jederzeit ohne Vorankündigung geändert,
unterbrochen, gelöscht oder eingestellt werden. Eine
Verfügbarkeitsgarantie besteht nicht.
English.
trails.cool is an experimental service,
currently free of charge, in
active alpha development. Features, data, and availability can
change at any time without notice. No availability is guaranteed.
2. Mindestalter
Die Nutzung eines Journal-Kontos ist erst ab einem Alter von 16
Jahren zulässig. Anonyme Nutzung des Planners unterliegt keiner
Altersbeschränkung.
English.
A Journal account is available only to users
aged 16 or older. Anonymous use of the Planner has no age
requirement.
3. Dienstverfügbarkeit
Der Betreiber kann den Dienst jederzeit ändern, einschränken,
unterbrechen oder einstellen, auch einzelne Funktionen oder
einzelne Konten. Wartungs- und Ausfallzeiten sind möglich.
English.
The operator may modify, limit, interrupt, or
discontinue the service (or parts of it, or individual accounts)
at any time. Downtime and maintenance windows may occur.
4. Zurücksetzen der Datenbank
Während der Alpha-Phase behält sich der Betreiber ausdrücklich das
Recht vor, die gesamte Datenbank zurückzusetzen oder einzelne
Datensätze ohne vorherige Benachrichtigung zu löschen.
Nutzer:innen sollten wichtige Routen und Aktivitäten selbstständig
über die Export-Funktion (GPX bzw. JSON) sichern.
English.
During alpha, the operator may reset the entire
database or delete individual records without notice. Back up
anything important via the GPX / JSON export.
5. Eigenverantwortung für Daten
Nutzer:innen sind für die Sicherung ihrer Inhalte selbst
verantwortlich. GPX- und JSON-Exporte sind im Journal jederzeit
für jede Route und Aktivität verfügbar. Der Betreiber schuldet
keine Datenwiederherstellung.
English.
You are responsible for backing up your own
content. Exports are always available in the Journal; the
operator owes no data recovery.
6. Inhalte und Nutzungsrechte
Die von Ihnen eingestellten Inhalte (Routen, Aktivitäten,
Beschreibungen, GPX-Dateien) bleiben Ihr Eigentum. Sie räumen dem
Betreiber lediglich das einfache, nicht übertragbare Recht ein,
diese Inhalte zum Zweck des Betriebs des Dienstes zu speichern,
zu verarbeiten und Ihnen wieder anzuzeigen. Eine darüber
hinausgehende Nutzung, Veröffentlichung oder Weitergabe findet
nicht statt.
English.
You retain ownership of everything you upload.
You grant the operator a limited, non-transferable licence to
store, process, and display your content solely for operating the
service; no other use, publication, or sharing.
7. Nutzungsregeln
Keine Verbreitung rechtswidriger Inhalte
Keine missbräuchliche Nutzung (Spam, hochfrequente automatisierte
Abfragen, Denial-of-Service)
Kein Umgehen technischer Schutzmaßnahmen
Kein massenhaftes automatisiertes Auslesen von Daten
English.
No illegal content, no abuse (spam, flooding,
DoS), no circumvention of technical protections, no bulk
automated data extraction.
8. Kontobeendigung
Sie können Ihr Konto jederzeit über die Einstellungen löschen;
damit werden auch die zugehörigen Inhalte gelöscht. Der Betreiber
kann Konten bei Verstößen gegen diese Bedingungen nach
angemessener Interessenabwägung sperren oder löschen.
English.
Delete your account (and its content) any time
via settings. The operator may suspend or delete accounts that
violate these terms, balancing interests reasonably.
9. Haftung
Der Dienst wird ohne Gewährleistung bereitgestellt. Der Betreiber
haftet uneingeschränkt bei Vorsatz und grober Fahrlässigkeit sowie
bei Verletzung von Leben, Körper oder Gesundheit. Im Übrigen ist
die Haftung für leicht fahrlässige Pflichtverletzungen auf den
vertragstypischen, vorhersehbaren Schaden bei Verletzung
wesentlicher Vertragspflichten (Kardinalpflichten) begrenzt. Eine
darüber hinausgehende Haftung insbesondere für Datenverlust
ist ausgeschlossen, soweit gesetzlich zulässig.
English.
No warranty. Unlimited liability for intent,
gross negligence, and injury to life, body, or health. For
slight negligence, liability is limited to foreseeable damages
arising from breach of material contractual duties. Liability
beyond that — in particular for data loss — is excluded to the
extent permitted by law.
10. Änderungen der Bedingungen
Diese Nutzungsbedingungen können sich während der Alpha-Phase
ändern. Bei wesentlichen Änderungen werden registrierte
Nutzer:innen per E-Mail informiert und müssen die neuen
Bedingungen erneut bestätigen.
English.
These terms may change during alpha. Registered
users will be emailed about material changes and prompted to
re-accept.
11. Anwendbares Recht
Es gilt deutsches Recht unter Ausschluss des UN-Kaufrechts. Für
Verbraucher:innen mit gewöhnlichem Aufenthalt in einem Mitgliedstaat
der Europäischen Union bleibt der zwingende Schutz des Rechts ihres
Aufenthaltslandes unberührt.
English.
German law applies, excluding the UN Convention
on Contracts for the International Sale of Goods. Consumers
habitually residing in an EU member state retain the mandatory
protection of their national law.

122
scripts/render-legal.py Executable file
View file

@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""
Render a trails.cool legal page (Terms / Privacy / Impressum) from its TSX
source into plain markdown suitable for `docs/legal-archive/`.
Usage (from repo root):
python3 scripts/render-legal.py terms > docs/legal-archive/terms-YYYY-MM-DD.md
python3 scripts/render-legal.py privacy > docs/legal-archive/privacy-YYYY-MM-DD.md
python3 scripts/render-legal.py imprint > docs/legal-archive/imprint-YYYY-MM-DD.md
Strips JSX tags + attributes and resolves the `operator.*` placeholders
from `apps/journal/app/lib/operator.ts` so the output is a clean
human-readable snapshot.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
OPERATOR_FILE = REPO_ROOT / "apps/journal/app/lib/operator.ts"
LEGAL_FILE = REPO_ROOT / "apps/journal/app/lib/legal.ts"
PAGES = {
"terms": REPO_ROOT / "apps/journal/app/routes/legal.terms.tsx",
"privacy": REPO_ROOT / "apps/journal/app/routes/legal.privacy.tsx",
"imprint": REPO_ROOT / "apps/journal/app/routes/legal.imprint.tsx",
}
TITLES = {
"terms": "Terms of Service — trails.cool",
"privacy": "Privacy Policy — trails.cool",
"imprint": "Impressum — trails.cool",
}
def load_operator() -> dict[str, str]:
src = OPERATOR_FILE.read_text()
fields = ("name", "street", "postalCode", "city", "country", "email", "responsiblePerson")
return {
f: m.group(1) if (m := re.search(rf'{f}:\s*"([^"]+)"', src)) else ""
for f in fields
}
def load_legal_constants() -> dict[str, str]:
"""Exported top-level `export const NAME = "value";` from legal.ts."""
src = LEGAL_FILE.read_text()
return dict(re.findall(r'export const (\w+)\s*=\s*"([^"]+)"', src))
def render(src: str, operator: dict[str, str], legal: dict[str, str]) -> str:
# Resolve operator placeholders
src = re.sub(r'\{operator\.address\.(\w+)\}', lambda m: operator.get(m.group(1), ""), src)
src = re.sub(r'\{operator\.(\w+)\}', lambda m: operator.get(m.group(1), ""), src)
# Resolve legal.ts constants (TERMS_VERSION, PRIVACY_LAST_UPDATED, …)
for name, value in legal.items():
src = re.sub(r'\{' + re.escape(name) + r'\}', value, src)
# Unwrap template literals and explicit single-space expressions
src = re.sub(r'\{`(.*?)`\}', r'\1', src)
src = re.sub(r'\{"\s*"\}', ' ', src)
# Strip common JSX attributes we don't want in prose
src = re.sub(r'\s(?:className|href|target|rel|id|name|content|htmlFor)="[^"]*"', '', src)
# Drop any remaining {expr} blocks (unresolved expressions)
src = re.sub(r'\{[^{}]*\}', '', src)
# Replace tags with newlines to preserve paragraph structure
src = re.sub(r'<[^>]+>', '\n', src)
# HTML entity decode (minimal)
for entity, replacement in (
("&apos;", "'"),
("&amp;", "&"),
("&lt;", "<"),
("&gt;", ">"),
("&quot;", '"'),
("&nbsp;", " "),
):
src = src.replace(entity, replacement)
# Collapse blank lines
lines = [ln.rstrip() for ln in src.splitlines()]
out: list[str] = []
prev_blank = False
for ln in lines:
if ln.strip() == "":
if not prev_blank and out:
out.append("")
prev_blank = True
else:
out.append(ln.strip())
prev_blank = False
return "\n".join(out).strip()
def main() -> int:
if len(sys.argv) != 2 or sys.argv[1] not in PAGES:
sys.stderr.write(f"Usage: {sys.argv[0]} {{{'|'.join(PAGES)}}}\n")
return 2
page = sys.argv[1]
operator = load_operator()
legal = load_legal_constants()
src = PAGES[page].read_text()
# Slice out the JSX tree: everything between "return (" and the closing ");"
src = re.split(r'return \(\s*\n', src, maxsplit=1)[-1]
src = re.sub(r'\);\s*\}\s*$', '', src.rstrip())
body = render(src, operator, legal)
print(f"# {TITLES[page]}\n\n{body}")
return 0
if __name__ == "__main__":
raise SystemExit(main())