aboutsummaryrefslogtreecommitdiff
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
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
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--internal/cli/cli.go62
-rw-r--r--internal/config/config.go2
-rw-r--r--internal/export/export.go128
-rw-r--r--internal/export/export_test.go41
-rw-r--r--internal/export/fonts/DejaVuSans-Bold.ttfbin0 -> 708920 bytes
-rw-r--r--internal/export/fonts/DejaVuSans.ttfbin0 -> 759720 bytes
-rw-r--r--internal/i18n/i18n.go5
-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
13 files changed, 308 insertions, 1 deletions
diff --git a/go.mod b/go.mod
index 94f5ab2..f80d413 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.24.0
require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
+ github.com/go-pdf/fpdf v0.9.0
github.com/pelletier/go-toml/v2 v2.4.2
)
diff --git a/go.sum b/go.sum
index cd63509..d57ae4f 100644
--- a/go.sum
+++ b/go.sum
@@ -14,6 +14,8 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
+github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index 04defbf..88d3a30 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -16,6 +16,7 @@ import (
"github.com/lukaszkasprzak/lectio/internal/bible"
"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"
@@ -49,6 +50,9 @@ Flags:
--rand, --rand-v print a random verse and exit (uses default_version or -b VER;
bt has no corpus, so it falls back to a corpus version)
--rand-ch print a random chapter and exit
+ --md export the readings as Markdown (to --out or stdout)
+ --pdf export the readings as PDF (to --out or stdout)
+ --out FILE write --md/--pdf output to FILE
-v, --version print the version and exit
-h, --help this help
@@ -107,6 +111,8 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
var all, raw, refresh, offline, update, clean, pagerFlag, noPager, citation, week bool
var randV, randCh bool
+ var expMD, expPDF bool
+ var output string
var bibleVer, compareList, lectionary, lang string
var width int
var list bool
@@ -149,6 +155,10 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
fs.BoolVar(&randV, "rand", false, "print a random verse from the corpus and exit")
fs.BoolVar(&randV, "rand-v", false, "print a random verse from the corpus and exit")
fs.BoolVar(&randCh, "rand-ch", false, "print a random chapter from the corpus and exit")
+ fs.BoolVar(&expMD, "md", false, "export the readings as Markdown")
+ fs.BoolVar(&expPDF, "pdf", false, "export the readings as PDF")
+ fs.StringVar(&output, "out", "", "write --md/--pdf output to FILE (default: stdout)")
+ fs.StringVar(&output, "output", "", "write --md/--pdf output to FILE (default: stdout)")
if err := fs.Parse(rest); err != nil {
return 2
@@ -208,6 +218,18 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
}
effAll := all || cfg.All
+
+ if expMD || expPDF {
+ ver := bibleVer
+ if ver == "" {
+ ver = cfg.DefaultVersion
+ }
+ if !config.ValidVersion(ver) {
+ fmt.Fprintf(stderr, "lectio: unknown version %q (want one of bt, wuj, vul, grb, drb)\n", ver)
+ return 2
+ }
+ return runExport(cfg, date, ver, effAll, refresh, expPDF, output, stdout, stderr)
+ }
effWidth := width
if effWidth == 0 {
effWidth = cfg.Width
@@ -332,6 +354,46 @@ func dialectCitation(cfg config.Config, tbl *bible.BookTable, sec liturgy.Sectio
return tbl.FormatRef(cfg.SiglaLang(), canonical)
}
+// runExport handles --md/--pdf: load the day's readings and write them as
+// Markdown or PDF to --out FILE (or stdout). Honors date/lectionary/all/offline
+// and the version (bt is fine -- it is the niedziela text -- but is swapped for
+// traditional/offline via EffectiveVersions).
+func runExport(cfg config.Config, date, version string, all, refresh, asPDF bool, output string, stdout, stderr io.Writer) int {
+ secs, info, err := readings.Load(cfg, readings.Options{Date: date, Refresh: refresh, Offline: cfg.Offline, All: all})
+ if err != nil {
+ fmt.Fprintln(stderr, "lectio:", err)
+ return 1
+ }
+ if len(secs) == 0 {
+ fmt.Fprintln(stderr, "lectio: no readings found for", date)
+ return 1
+ }
+ if vs := render.EffectiveVersions([]string{version}, cfg.Lectionary, cfg.Offline); len(vs) > 0 {
+ version = vs[0]
+ }
+
+ var data []byte
+ if asPDF {
+ data, err = export.ReadingsPDF(date, info, secs, version, cfg.Lectionary, cfg.UILanguage)
+ if err != nil {
+ fmt.Fprintln(stderr, "lectio:", err)
+ return 1
+ }
+ } else {
+ data = []byte(export.Markdown(date, info, secs, version, cfg.Lectionary, cfg.UILanguage))
+ }
+
+ if output != "" {
+ if err := os.WriteFile(output, data, 0o644); err != nil {
+ fmt.Fprintln(stderr, "lectio:", err)
+ return 1
+ }
+ return 0
+ }
+ _, _ = stdout.Write(data)
+ return 0
+}
+
// runCitation handles --citation: fetch the day's gospel (gospel-only) and
// print just its scripture reference in the configured sigla dialect (see
// dialectCitation), for scripts/cron/prompt use. Honors the resolved cfg
diff --git a/internal/config/config.go b/internal/config/config.go
index 8847678..0ddbaa1 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -25,7 +25,7 @@ var seedTOML []byte
// Version is lectio's release version, shared by every binary's
// -v/--version output (lectio, lectio-ui, lectio-web).
-const Version = "0.21.0"
+const Version = "0.22.0"
// validVersions are the five scripture versions lectio understands.
var validVersions = map[string]bool{
diff --git a/internal/export/export.go b/internal/export/export.go
new file mode 100644
index 0000000..96491db
--- /dev/null
+++ b/internal/export/export.go
@@ -0,0 +1,128 @@
+// Package export renders a day's readings (or a passage) to shareable formats:
+// plain text, Markdown, and PDF -- plus a printable A4 month calendar. The PDF
+// uses an embedded DejaVu Sans (regular + bold) so Polish, Latin and Greek all
+// render; it is pure Go (no cgo), keeping the single-binary cross-compile.
+package export
+
+import (
+ "bytes"
+ _ "embed"
+ "fmt"
+ "strings"
+
+ "github.com/go-pdf/fpdf"
+
+ "github.com/lukaszkasprzak/lectio/internal/i18n"
+ "github.com/lukaszkasprzak/lectio/internal/liturgy"
+ "github.com/lukaszkasprzak/lectio/internal/render"
+)
+
+// versionLabel is the localized name of a version (i18n), or the bare code.
+func versionLabel(version, lang string) string {
+ if l, ok := i18n.Get(lang).Version[version]; ok {
+ return l
+ }
+ return version
+}
+
+// title is the document heading: the celebration name if the source carries
+// one, else a plain "readings for DATE" style line.
+func title(date string, info liturgy.DayInfo) string {
+ if info.Name != "" {
+ return info.Name
+ }
+ return date
+}
+
+// Text renders a day's readings as plain UTF-8 text: a title, the day-info,
+// then each section's localized heading and its version blocks.
+func Text(date string, info liturgy.DayInfo, secs []liturgy.Section, version, lectionary, lang string) string {
+ var b strings.Builder
+ fmt.Fprintf(&b, "%s\n", title(date, info))
+ if info.Season != "" {
+ fmt.Fprintf(&b, "%s\n", info.Season)
+ }
+ fmt.Fprintf(&b, "%s · %s\n\n", date, versionLabel(version, lang))
+ for _, sec := range secs {
+ fmt.Fprintf(&b, "%s\n\n", render.HeadingWithRef(sec, lang))
+ _, blocks := render.GatherVersion(version, sec, lectionary, lang)
+ for _, blk := range blocks {
+ fmt.Fprintf(&b, "%s\n", blk)
+ }
+ b.WriteString("\n")
+ }
+ return b.String()
+}
+
+// Markdown renders a day's readings as Markdown (# title, ## section headings,
+// verse/paragraph blocks).
+func Markdown(date string, info liturgy.DayInfo, secs []liturgy.Section, version, lectionary, lang string) string {
+ var b strings.Builder
+ fmt.Fprintf(&b, "# %s\n\n", title(date, info))
+ if info.Season != "" {
+ fmt.Fprintf(&b, "*%s*\n\n", info.Season)
+ }
+ fmt.Fprintf(&b, "**%s · %s**\n\n", date, versionLabel(version, lang))
+ for _, sec := range secs {
+ fmt.Fprintf(&b, "## %s\n\n", render.HeadingWithRef(sec, lang))
+ _, blocks := render.GatherVersion(version, sec, lectionary, lang)
+ for _, blk := range blocks {
+ fmt.Fprintf(&b, "%s\n\n", blk)
+ }
+ }
+ return b.String()
+}
+
+// ReadingsPDF renders a day's readings as a clean printable PDF.
+func ReadingsPDF(date string, info liturgy.DayInfo, secs []liturgy.Section, version, lectionary, lang string) ([]byte, error) {
+ pdf := newPDF()
+ pdf.AddPage()
+
+ pdf.SetFont(fontFamily, "B", 18)
+ pdf.MultiCell(0, 9, title(date, info), "", "L", false)
+ if info.Season != "" {
+ pdf.SetFont(fontFamily, "", 10)
+ pdf.MultiCell(0, 6, info.Season, "", "L", false)
+ }
+ pdf.SetFont(fontFamily, "", 10)
+ pdf.MultiCell(0, 6, date+" · "+versionLabel(version, lang), "", "L", false)
+ pdf.Ln(3)
+
+ for _, sec := range secs {
+ pdf.Ln(2)
+ pdf.SetFont(fontFamily, "B", 13)
+ pdf.MultiCell(0, 7, render.HeadingWithRef(sec, lang), "", "L", false)
+ pdf.Ln(1)
+ pdf.SetFont(fontFamily, "", 11)
+ _, blocks := render.GatherVersion(version, sec, lectionary, lang)
+ for _, blk := range blocks {
+ pdf.MultiCell(0, 6, blk, "", "L", false)
+ pdf.Ln(1)
+ }
+ }
+
+ var buf bytes.Buffer
+ if err := pdf.Output(&buf); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+//go:embed fonts/DejaVuSans.ttf
+var fontRegular []byte
+
+//go:embed fonts/DejaVuSans-Bold.ttf
+var fontBold []byte
+
+const fontFamily = "dejavu"
+
+// newPDF returns an A4 portrait document with the embedded Unicode font
+// registered (regular + bold) and sensible margins/auto page break.
+func newPDF() *fpdf.Fpdf {
+ pdf := fpdf.New("P", "mm", "A4", "")
+ pdf.AddUTF8FontFromBytes(fontFamily, "", fontRegular)
+ pdf.AddUTF8FontFromBytes(fontFamily, "B", fontBold)
+ pdf.SetMargins(18, 16, 18)
+ pdf.SetAutoPageBreak(true, 16)
+ return pdf
+}
diff --git a/internal/export/export_test.go b/internal/export/export_test.go
new file mode 100644
index 0000000..7f1c68d
--- /dev/null
+++ b/internal/export/export_test.go
@@ -0,0 +1,41 @@
+package export
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+
+ "github.com/lukaszkasprzak/lectio/internal/liturgy"
+)
+
+func sampleSecs() []liturgy.Section {
+ return []liturgy.Section{{
+ Heading: "Ewangelia (Mt 13, 1-9)",
+ Citation: "Mt 13, 1-9",
+ PartID: "ewangelia",
+ Paragraphs: [][]string{{"Owego dnia Jezus wyszedł z domu i usiadł nad jeziorem."}},
+ }}
+}
+
+func TestMarkdownAndText(t *testing.T) {
+ info := liturgy.DayInfo{Name: "Środa XV tygodnia", Colour: "green"}
+ md := Markdown("2026-07-22", info, sampleSecs(), "bt", "new", "en")
+ if !strings.Contains(md, "# Środa") || !strings.Contains(md, "## Gospel") || !strings.Contains(md, "Jezus wyszedł") {
+ t.Errorf("markdown:\n%s", md)
+ }
+ txt := Text("2026-07-22", info, sampleSecs(), "bt", "new", "en")
+ if !strings.Contains(txt, "Jezus wyszedł") || !strings.Contains(txt, "Gospel") {
+ t.Errorf("text:\n%s", txt)
+ }
+}
+
+func TestReadingsPDF(t *testing.T) {
+ info := liturgy.DayInfo{Name: "Środa XV tygodnia"}
+ pdf, err := ReadingsPDF("2026-07-22", info, sampleSecs(), "bt", "new", "en")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(pdf) < 1000 || !bytes.HasPrefix(pdf, []byte("%PDF")) {
+ t.Errorf("bad pdf len=%d", len(pdf))
+ }
+}
diff --git a/internal/export/fonts/DejaVuSans-Bold.ttf b/internal/export/fonts/DejaVuSans-Bold.ttf
new file mode 100644
index 0000000..0a7fb3f
--- /dev/null
+++ b/internal/export/fonts/DejaVuSans-Bold.ttf
Binary files differ
diff --git a/internal/export/fonts/DejaVuSans.ttf b/internal/export/fonts/DejaVuSans.ttf
new file mode 100644
index 0000000..1728e9e
--- /dev/null
+++ b/internal/export/fonts/DejaVuSans.ttf
Binary files differ
diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go
index ed03548..cf1f550 100644
--- a/internal/i18n/i18n.go
+++ b/internal/i18n/i18n.go
@@ -53,6 +53,9 @@ type UI struct {
// Web reader (lectio-web /reader) control labels.
NavReader, WebBook, WebChapter string
+ // Export is the web daily-page download label (txt/md/pdf links).
+ Export string
+
// NoReadingsDay is the web error fragment shown when readings.Load
// returns no sections at all for the requested date.
NoReadingsDay string
@@ -131,6 +134,7 @@ var enUI = UI{
NavReader: "reader",
WebBook: "book",
WebChapter: "chapter",
+ Export: "download",
NoReadingsDay: "no readings for this day",
NoVersion: "(not in %s)",
NoVersionPartial: "(not in %s: %s)",
@@ -193,6 +197,7 @@ var plUI = UI{
NavReader: "czytnik",
WebBook: "księga",
WebChapter: "rozdział",
+ Export: "pobierz",
NoReadingsDay: "brak czytań na ten dzień",
NoVersion: "(brak w „%s”)",
NoVersionPartial: "(brak w „%s”: %s)",
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>