commit 6a943df87fa669cbd0d473dc826e5ec3882bd89b
parent 380f7dbbd0bb9535c19ef75dac51a0df81762bb4
Author: lou woell <lou@repetitions.de>
Date: Sat, 29 Aug 2026 20:00:48 +0200
add html generator
Diffstat:
12 files changed, 462 insertions(+), 1 deletion(-)
diff --git a/assets/style.css b/assets/style.css
@@ -0,0 +1,53 @@
+body {
+ margin: auto;
+ max-width: 56em;
+ padding: 0 2rem 0;
+}
+
+.headline {
+
+ margin-bottom: 0.1rem;
+
+ .anchor {
+ display: none;
+ color: grey;
+ text-decoration: none;
+ }
+
+ &:hover {
+
+ .anchor {
+ display: inline-block;
+ }
+
+ }
+
+}
+
+.refs,
+.backlinks {
+ margin: 0;
+ font-size: 10pt;
+}
+
+.node {
+ margin-top: 0.3rem;
+ margin-bottom: 0.3rem;
+}
+
+h3 {
+ font-size: 15pt;
+}
+h4,h5,h6 {
+ font-size: 13pt;
+}
+
+@media (min-width: 90em) {
+ .sidebar {
+ display: block;
+ position: fixed;
+ top: 4.5rem;
+ right: calc(50% + 31.5rem);
+ width: 12.5rem;
+ }
+}
diff --git a/data/de_auerbach/types.json b/data/de_auerbach/types.json
@@ -0,0 +1,22 @@
+{
+ "ethica": "Ethik",
+ "pars": "Teil",
+ "definitiones": "Definitionen",
+ "definitio": "Definition",
+ "explicatio": "Erläuterung",
+ "axiomata": "Axiome",
+ "axioma": "Axiom",
+ "propositio": "Lehrsatz",
+ "demonstratio": "Beweis",
+ "corollarium": "Folgesatz",
+ "aliter": "Anderer Beweis",
+ "scholium": "Anmerkung",
+ "appendix": "Anhang",
+ "praefatio": "Vorrede",
+ "lemma": "Lehnsatz",
+ "postulatum": "Postulat",
+ "postulata": "Postulate",
+ "affectuum definitiones": "Definitionen der Affekte",
+ "affectuum generalis definitio": "Allgemeine Definition der Affekte",
+ "caput": "§"
+}
diff --git a/data/de_stern/types.json b/data/de_stern/types.json
@@ -0,0 +1,22 @@
+{
+ "ethica": "Ethik",
+ "pars": "Teil",
+ "definitiones": "Definitionen",
+ "definitio": "Definition",
+ "explicatio": "Erläuterung",
+ "axiomata": "Axiome",
+ "axioma": "Axiom",
+ "propositio": "Lehrsatz",
+ "demonstratio": "Beweis",
+ "corollarium": "Zusatz",
+ "aliter": "Anderer Beweis",
+ "scholium": "Anmerkung",
+ "appendix": "Anhang",
+ "praefatio": "Vorwort",
+ "lemma": "Hilfssatz",
+ "postulatum": "Postulat",
+ "postulata": "Postulate",
+ "affectuum definitiones": "Definitionen der Affekte",
+ "affectuum generalis definitio": "Allgemeine Definition der Affekte",
+ "caput": "§"
+}
diff --git a/data/en_elwes/types.json b/data/en_elwes/types.json
@@ -0,0 +1,22 @@
+{
+ "ethica": "Ethics",
+ "pars": "Part",
+ "definitiones": "Definitions",
+ "definitio": "Definition",
+ "explicatio": "Explanation",
+ "axiomata": "Axioms",
+ "axioma": "Axiom",
+ "propositio": "Proposition",
+ "demonstratio": "Proof",
+ "corollarium": "Corollary",
+ "aliter": "Another Proof",
+ "scholium": "Note",
+ "appendix": "Appendix",
+ "praefatio": "Preface",
+ "lemma": "Lemma",
+ "postulatum": "Postulate",
+ "postulata": "Postulates",
+ "affectuum definitiones": "Definitions of the Emotions",
+ "affectuum generalis definitio": "General Definition of the Emotions",
+ "caput": "Paragraph"
+}
diff --git a/data/fr_appuhn/types.json b/data/fr_appuhn/types.json
@@ -0,0 +1,22 @@
+{
+ "ethica": "Éthique",
+ "pars": "Partie",
+ "definitiones": "Définitions",
+ "definitio": "Définition",
+ "explicatio": "Explication",
+ "axiomata": "Axiomes",
+ "axioma": "Axiome",
+ "propositio": "Proposition",
+ "demonstratio": "Démonstration",
+ "corollarium": "Corollaire",
+ "aliter": "Autre Démonstration",
+ "scholium": "Scolie",
+ "appendix": "Appendice",
+ "praefatio": "Préface",
+ "lemma": "Lemme",
+ "postulatum": "Postulat",
+ "postulata": "Postulats",
+ "affectuum definitiones": "Définitions des Affections",
+ "affectuum generalis definitio": "Définition Générale des Affections",
+ "caput": "Chapitre"
+}
diff --git a/data/fr_saisset/types.json b/data/fr_saisset/types.json
@@ -0,0 +1,22 @@
+{
+ "ethica": "Éthique",
+ "pars": "Partie",
+ "definitiones": "Définitions",
+ "definitio": "Définition",
+ "explicatio": "Explication",
+ "axiomata": "Axiomes",
+ "axioma": "Axiome",
+ "propositio": "Proposition",
+ "demonstratio": "Démonstration",
+ "corollarium": "Corollaire",
+ "aliter": "Autre Démonstration",
+ "scholium": "Scholie",
+ "appendix": "Appendice",
+ "praefatio": "Préface",
+ "lemma": "Lemme",
+ "postulatum": "Postulat",
+ "postulata": "Postulats",
+ "affectuum definitiones": "Définition des Passions",
+ "affectuum generalis definitio": "Définition Générale des Passions",
+ "caput": "Chapitre"
+}
diff --git a/data/la_spinoza/types.json b/data/la_spinoza/types.json
@@ -0,0 +1,22 @@
+{
+ "ethica": "Ethica",
+ "pars": "Pars",
+ "definitiones": "definitiones",
+ "definitio": "definitio",
+ "explicatio": "explicatio",
+ "axiomata": "axiomata",
+ "axioma": "axioma",
+ "propositio": "propositio",
+ "demonstratio": "demonstratio",
+ "corollarium": "corollarium",
+ "aliter": "aliter",
+ "scholium": "scholium",
+ "appendix": "appendix",
+ "praefatio": "praefatio",
+ "lemma": "lemma",
+ "postulatum": "postulatum",
+ "postulata": "postulata",
+ "affectuum definitiones": "affectuum definitiones",
+ "affectuum generalis definitio": "affectuum generalis definitio",
+ "caput": "caput"
+}
diff --git a/data/meta.json b/data/meta.json
@@ -8,7 +8,7 @@
"4",
"5"
],
- "type": "toplevel"
+ "type": "ethica"
},
"1": {
"id": "1",
diff --git a/html/ethics.chtml b/html/ethics.chtml
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title><c:s>(format nil "~A: ~A" (gethash "ethica" types) name)</c:s></title>
+ <meta charset="utf-8">
+ <link rel="stylesheet" href="assets/style.css">
+ </head>
+ <body>
+ <nav>
+ <span class="nav-links">
+ <a href="/">index</a>
+ </span>
+ <hr>
+ </nav>
+ <div class="container">
+ <c:h>
+ (make-node tree vkey types)
+ </c:h>
+ </div>
+ </body>
+</html>
diff --git a/html/index.chtml b/html/index.chtml
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <link rel="stylesheet" href="assets/style.css">
+ </head>
+ <body>
+ <h1>ethica</h1>
+ <div>
+ <h2>Available Translations</h2>
+ <ol iterate="versions">
+ <li>
+ <c:let file="file" name="name">
+ <a href="./{file}"
+ fill="href *"
+ lquery="(text name)"/>
+ </c:let>
+ </li>
+ </ol>
+ </div>
+ <div>
+ <h2>What is this?</h2>
+ <p>
+ There are already a number of hypertext versions of Spinozas Ethics available on the internet. In particular <a href="https://ethicadb.org">ethicadb.org</a> was an ambitioned attempt at collecting as many translations of the text as possible, making them easily cross-referencable. Sadly ethicadb has been broken for a while and since neither it's corpus nor the code of the website is public, the work that went into its creation is probably lost.
+ </p>
+ <p>
+ This site is a reaction to this situation. My goal is to create a successor to ethicaDB that can continue to be useful to people whether or not I'm around to care for and maintain it or not. To that end I'm providing with this site all the data and code needed to reproduce this site <em>or similar projects</em>, in the hope that when this site inevitably goes offline someone will be able to pick up where I left off without needing to do all the work of collecting, formatting and organizing the texts again.
+ </p>
+ <div>
+ <h3>Helping</h3>
+ <p>
+ There's lots left to do. There's still a a lot of formatting issues in the corpus that are easy to recitify but a lot of work to find. Similarly it would be nice to have inline links for all references in the text. And finally of course there's always more translations to add. If you want to help out, feel free to reach out via <a href="mailto:lou+ethica@repetitions.de">mail.</a>
+ </p>
+ </div>
+ </div>
+ <div>
+ <h2>Other Online Spinoza Resources</h2>
+ <div>
+ <h4>Latin</h4>
+ <ul>
+ <li><a href="http://baptiste.meles.free.fr/spinozabase/index.html">http://baptiste.meles.free.fr/spinozabase/index.html</a></li>
+ </ul>
+ </div>
+ <div>
+ <h4>French</h4>
+ <ul>
+ <li><a href="https://spinozaetnous.org">https://spinozaetnous.org</a></li>
+ <li><a href="https://ethicadb.org/">https://ethicadb.org/</a></li>
+ <li><a href="https://spinoza.fr">https://spinoza.fr</a></li>
+ <li><a href="https://fr.wikisource.org/wiki/%C3%89thique_(Spinoza)">https://fr.wikisource.org/wiki/Éthique_(Spinoza)</a></li>
+ </ul>
+ </div>
+ <div>
+ <h4> English </h4>
+ <ul>
+ <li><a href="https://capone.mtsu.edu/rbombard/RB/Spinoza/ethica-front.html">https://capone.mtsu.edu/rbombard/RB/Spinoza/ethica-front.html</a></li>
+ <li><a href="https://www.gutenberg.org/files/3800/3800-h/3800-h.htm">https://www.gutenberg.org/files/3800/3800-h/3800-h.htm</a></li>
+ <li><a href="https://ethica.bc.edu/">https://ethica.bc.edu/</a></li>
+ </ul>
+ </div>
+ <div>
+ <h4>German</h4>
+ <ul>
+ <li><a href="http://www.zeno.org/nid/20009272534">http://www.zeno.org/nid/20009272534</a></li>
+ </ul>
+ </div>
+ </div>
+ <div>
+ <h2>Credits & Licenses</h2>
+ <p>
+ The text of Spinozas Ethics and of all the translations provided on this site is in the public domain.
+ </p>
+ <p>
+ The Meta data used to generate the hyperlinks between sections of the text where taken from
+ <a href="http://baptiste.meles.free.fr/spinozabase/index.html">Baptiste Mélès' Latin hypertext version of the ethics</a>, licensed under the Gnu General Public License (<a href="https://archive.softwareheritage.org/browse/directory/7a99eb215c838ddd7842bba8bb9679120f5161cd/?origin_url=https://github.com/BapMel/spinozabase&revision=8b3945fc34f32b1ee7b96396b1247ec56fc16976&snapshot=f9b06f499471d09d15e01b74db11ba9fdec7b72f">src</a>).
+ </p>
+ <p>
+ Additional Credits for translations and editions are provided with the corresponding versions.
+ </p>
+ </div>
+ </body>
+</html>
diff --git a/html/node.chtml b/html/node.chtml
@@ -0,0 +1,48 @@
+<c:let vkey="vkey"
+ node="node"
+ level="level"
+ types="types"
+ type="(gethash "type" node)"
+ refs="(gethash "ref" node)"
+ backlinks="(gethash :links node)"
+ id="(gethash "id" node)">
+ <div class="node"
+ lquery="(attr :id id) (add-class type)">
+ <c:expand class="headline" lquery="(attr :as (format nil "h~a" level))">
+ <c:s>(format nil "~a ~a" (gethash type types) (or (gethash "number" node) ""))</c:s>
+ <a class="anchor" href="#{id}" fill="href *">#<c:s>id</c:s></a>
+ </c:expand>
+ <c:when test="backlinks">
+ <div class="backlinks">
+ <label>Referenced by:</label>
+ <c:splice iterate="backlinks">
+ <c:let target="*">
+ <a href="#{target}"
+ fill="href *"
+ lquery="(text target)">
+ </a>
+ </c:let>
+ </c:splice>
+ </div>
+ </c:when>
+ <div class="node-body">
+ <c:h>(md:parse (or (gethash vkey node) ""))</c:h>
+ </div>
+ <c:when test="refs">
+ <div class="refs" >
+ <label>References:</label>
+ <c:splice iterate="refs">
+ <c:let target="*">
+ <a href="#{target}"
+ fill="href *"
+ lquery="(text target)">
+ </a>
+ </c:let>
+ </c:splice>
+ </div>
+ </c:when>
+ <div class="node-children" iterate="(gethash :child-nodes node)">
+ <c:h>(make-node * (** :vkey) (** :types) (+ (** :level) 1) )</c:h>
+ </div>
+ </div>
+</c:let>
diff --git a/lisp/ethics.lisp b/lisp/ethics.lisp
@@ -0,0 +1,125 @@
+(asdf:defsystem :lou.spinoza.ethics
+ :depends-on (#:plump
+ #:clip
+ #:markdown.cl
+ #:com.inuoe.jzon))
+
+(ql:quickload :lou.spinoza.ethics)
+
+(defpackage ethics
+ (:use :cl)
+ (:local-nicknames
+ (:md :markdown)
+ (:jzon :com.inuoe.jzon)))
+
+(in-package :ethics)
+
+;; --- Importing Data
+
+(defparameter config
+ '(:build-dir #p"../build/"
+ :meta-data #p"../data/meta.json"
+ :versions
+ ((:key :stern
+ :name "German (Stern)"
+ :file "de_stern.html"
+ :data #p"../data/de_stern/")
+ (:key :auerbach
+ :name "German (Auerbach)"
+ :file "de_auerbach.html"
+ :data #p"../data/de_auerbach/")
+ (:key :latin
+ :name "Latin (orig.)"
+ :file "latin.html"
+ :data #p"../data/la_spinoza/")
+ (:key :saisset
+ :name "French (Saisset)"
+ :file "fr_saisset.html"
+ :data #p"../data/fr_saisset/")
+ (:key :appuhn
+ :name "French (Appuhn)"
+ :file "fr_appuhn.html"
+ :data #p"../data/fr_appuhn/")
+ (:key :elwes
+ :name "English (Elwes)"
+ :file "english.html"
+ :data #p"../data/en_elwes/"))))
+
+(defun read-version (tbl version)
+ (loop :for path :in (uiop:directory-files (getf version :data))
+ :as type := (pathname-type path)
+ :when (string= type "md")
+ :do (setf (gethash (getf version :key)
+ (gethash (pathname-name path) tbl))
+ (uiop:read-file-string path))
+ :when (string= type "json")
+ :do (setf (gethash (getf version :key) tbl)
+ (jzon:parse path))))
+
+(defun get-nodes (tbl nodes)
+ (when nodes
+ (loop :with res := (make-array (length nodes) :fill-pointer 0)
+ :for key :across nodes
+ :as node := (gethash key tbl)
+ :do (vector-push node res)
+ :finally (return res))))
+
+(defun make-links (tbl id nodes)
+ (when nodes
+ (loop :for node :across nodes
+ :do (push id (gethash :links (gethash node tbl))))))
+
+(defun build-tree (tbl)
+ (loop :for node :being :each :hash-value :in tbl
+ :as children := (gethash "children" node)
+ :do
+ (make-links tbl (gethash "id" node) (gethash "ref" node))
+ (setf (gethash :child-nodes node)
+ (get-nodes tbl children))
+ :finally (return (gethash "ethica" tbl))))
+
+(defun import-ethics (config)
+ (let ((tbl (jzon:parse (getf config :meta-data))))
+ (loop :for v :in (getf config :versions)
+ :do (read-version tbl v))
+ (build-tree tbl)
+ tbl))
+
+;; ---
+
+(defun process-to-file (template outpath &rest bindings)
+ (with-open-file (out outpath
+ :direction :output
+ :external-format :utf8
+ :if-exists :supersede
+ :if-does-not-exist :create)
+ (plump:serialize
+ (apply #'clip:process
+ template
+ bindings)
+ out)))
+
+(defun make-node (node vkey types &optional (level 1))
+ (clip:process
+ #p"../html/node.chtml"
+ :node node
+ :level level
+ :vkey vkey
+ :types types))
+
+(defun write-ethics ()
+ (let ((ethica (import-ethics config))
+ (versions (getf config :versions)))
+ (process-to-file #p"../html/index.chtml"
+ #p"../build/index.html"
+ :versions versions)
+ (dolist (version versions)
+ (let ((key (getf version :key)))
+ (process-to-file
+ #p"../html/ethics.chtml"
+ (merge-pathnames (getf version :file)
+ (getf config :build-dir))
+ :tree (gethash "ethica" ethica)
+ :name (getf version :name)
+ :vkey key
+ :types (gethash key ethica))))))