From 29e244c48222278c2f255ff2adf45e3af7c9f895 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 24 Jul 2026 16:50:45 +0200 Subject: 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 --- internal/web/server.go | 44 +++++++++++++++++++++++++++++++++++++++ internal/web/server_test.go | 12 +++++++++++ internal/web/static/base.css | 4 ++++ internal/web/templates/index.html | 8 +++++++ 4 files changed, 68 insertions(+) (limited to 'internal/web') 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 @@ + {{/* Download the current readings; the URL is built from the live control + values at click time so it always matches what's on screen. */}} + {{.L.Export}}: + txt + md + pdf + +
{{.Reading}}
-- cgit v1.3