summaryrefslogtreecommitdiff
path: root/internal/web
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 16:50:45 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 16:50:45 +0200
commit29e244c48222278c2f255ff2adf45e3af7c9f895 (patch)
treeafa11818de1d8115eee1777f7bfc70757f218ac7 /internal/web
parent102bf0c522c94d36f8363c38711621fe5ad18555 (diff)
downloadlectio-29e244c48222278c2f255ff2adf45e3af7c9f895.tar.gz
lectio-29e244c48222278c2f255ff2adf45e3af7c9f895.zip
export: internal/export (Markdown/Text/ReadingsPDF via pure-Go go-pdf/fpdf + embedded DejaVu Sans, covers pl/la/gr); cli --md/--pdf/--out; web /export + download links; v0.22.0
Diffstat (limited to 'internal/web')
-rw-r--r--internal/web/server.go44
-rw-r--r--internal/web/server_test.go12
-rw-r--r--internal/web/static/base.css4
-rw-r--r--internal/web/templates/index.html8
4 files changed, 68 insertions, 0 deletions
diff --git a/internal/web/server.go b/internal/web/server.go
index 006bbdb..45ab06f 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -25,6 +25,7 @@ import (
"github.com/lukaszkasprzak/lectio/internal/bible"
"github.com/lukaszkasprzak/lectio/internal/bookmarks"
"github.com/lukaszkasprzak/lectio/internal/config"
+ "github.com/lukaszkasprzak/lectio/internal/export"
"github.com/lukaszkasprzak/lectio/internal/i18n"
"github.com/lukaszkasprzak/lectio/internal/liturgy"
"github.com/lukaszkasprzak/lectio/internal/readings"
@@ -67,6 +68,7 @@ func NewServer(cfg config.Config) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { indexHandler(s.get())(w, r) })
mux.HandleFunc("GET /readings", func(w http.ResponseWriter, r *http.Request) { readingsHandler(s.get())(w, r) })
+ mux.HandleFunc("GET /export", func(w http.ResponseWriter, r *http.Request) { exportHandler(s.get())(w, r) })
mux.HandleFunc("GET /reader", func(w http.ResponseWriter, r *http.Request) { readerHandler(s.get(), s.table())(w, r) })
mux.HandleFunc("GET /theme.css", func(w http.ResponseWriter, r *http.Request) { themeCSSHandler(s.get())(w, r) })
mux.HandleFunc("GET /settings", settingsGet(s))
@@ -337,6 +339,48 @@ func readingsHandler(cfg config.Config) http.HandlerFunc {
}
}
+// exportHandler serves GET /export?fmt=txt|md|pdf: the current day's readings
+// as a downloadable file (Content-Disposition attachment), reusing the same
+// date/lectionary/all/version resolution as the reading pane.
+func exportHandler(cfg config.Config) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ date, lectionary, all, versions, _ := resolveQuery(cfg, r)
+ secs, dayInfo, loadVersions, err := loadSections(cfg, lectionary, date, all, versions)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if len(secs) == 0 {
+ http.Error(w, i18n.Get(cfg.UILanguage).NoReadingsDay, http.StatusNotFound)
+ return
+ }
+ version := cfg.DefaultVersion
+ if len(loadVersions) > 0 {
+ version = loadVersions[0]
+ }
+
+ switch r.URL.Query().Get("fmt") {
+ case "pdf":
+ data, perr := export.ReadingsPDF(date, dayInfo, secs, version, lectionary, cfg.UILanguage)
+ if perr != nil {
+ http.Error(w, perr.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("Content-Type", "application/pdf")
+ w.Header().Set("Content-Disposition", `attachment; filename="lectio-`+date+`.pdf"`)
+ w.Write(data)
+ case "md":
+ w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
+ w.Header().Set("Content-Disposition", `attachment; filename="lectio-`+date+`.md"`)
+ io.WriteString(w, export.Markdown(date, dayInfo, secs, version, lectionary, cfg.UILanguage))
+ default:
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.Header().Set("Content-Disposition", `attachment; filename="lectio-`+date+`.txt"`)
+ io.WriteString(w, export.Text(date, dayInfo, secs, version, lectionary, cfg.UILanguage))
+ }
+ }
+}
+
// renderOrError returns RenderReadings' fragment, or (on a readings.Load
// error, or no sections at all for the date) a small escaped error
// paragraph, localised via lang -- readings.Load errors are expected in
diff --git a/internal/web/server_test.go b/internal/web/server_test.go
index 0754b08..43508ba 100644
--- a/internal/web/server_test.go
+++ b/internal/web/server_test.go
@@ -633,3 +633,15 @@ func TestBookmarksFlow(t *testing.T) {
t.Errorf("tag filter dropped the bookmark")
}
}
+
+func TestExportNoReadings(t *testing.T) {
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
+ cfg := config.Default()
+ cfg.Offline = true // no network; uncached date -> no readings
+ srv := NewServer(cfg)
+ rec := httptest.NewRecorder()
+ srv.ServeHTTP(rec, httptest.NewRequest("GET", "/export?fmt=md&date=2099-01-01", nil))
+ if rec.Code != http.StatusNotFound && rec.Code != http.StatusInternalServerError {
+ t.Errorf("export with no readings status=%d (want 404/500)", rec.Code)
+ }
+}
diff --git a/internal/web/static/base.css b/internal/web/static/base.css
index 36f2f4d..d9d0e59 100644
--- a/internal/web/static/base.css
+++ b/internal/web/static/base.css
@@ -292,3 +292,7 @@ a {
.bookmark { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: baseline; }
.bookmark .tags { font-size: 0.85rem; }
.tag-filter { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-bottom: var(--space-2); font-family: var(--font-ui); }
+
+/* Export links (download the readings). */
+.export { font-family: var(--font-ui); font-size: 0.85rem; }
+.export a { margin: 0 0.15rem; }
diff --git a/internal/web/templates/index.html b/internal/web/templates/index.html
index d3416e9..cd30208 100644
--- a/internal/web/templates/index.html
+++ b/internal/web/templates/index.html
@@ -78,6 +78,14 @@
<label class="mono-toggle"><input type="checkbox" {{if .Mono}}checked{{end}}
onchange="document.body.classList.toggle('mono', this.checked)"> {{.L.Mono}}</label>
+ {{/* Download the current readings; the URL is built from the live control
+ values at click time so it always matches what's on screen. */}}
+ <span class="export">{{.L.Export}}:
+ <a href="#" onclick="location='/export?fmt=txt&'+new URLSearchParams(new FormData(document.getElementById('controls'))).toString();return false;">txt</a>
+ <a href="#" onclick="location='/export?fmt=md&'+new URLSearchParams(new FormData(document.getElementById('controls'))).toString();return false;">md</a>
+ <a href="#" onclick="location='/export?fmt=pdf&'+new URLSearchParams(new FormData(document.getElementById('controls'))).toString();return false;">pdf</a>
+ </span>
+
<div id="pane">{{.Reading}}</div>
</div>