2026.08 Prayers for Daily Life

This commit is contained in:
william 2026-09-22 11:10:49 -07:00
parent 2876cfb6e8
commit c3c112fbe6
74 changed files with 905 additions and 48 deletions

2
.gitignore vendored
View File

@ -7,6 +7,8 @@
*.out
*.fls
*.fdb_latexmk
*.zip
*.swp
*.tar

Binary file not shown.

View File

@ -0,0 +1,9 @@
title: "Stories from GrailHeart — Volume 2"
author: "William"
lang: en-US
toc: true
toc-title: "Contents"
rights: "Copyright © 2026 William"

View File

@ -0,0 +1,33 @@
#!/bin/bash
set -e
INPUT="$1"
if [ -z "$INPUT" ]; then
echo "Usage: $0 main.tex"
exit 1
fi
if [ ! -f "$INPUT" ]; then
echo "ERROR: File not found: $INPUT"
exit 1
fi
BASENAME="${INPUT%.tex}"
OUTPUT="${BASENAME}.epub"
echo "Converting:"
echo " $INPUT"
echo "to:"
echo " $OUTPUT"
pandoc \
--from=latex \
--to=epub3 \
--metadata-file=epub.yaml \
--resource-path=".:$(dirname "$INPUT")" \
"$INPUT" \
-o "$OUTPUT"
echo "Done: $OUTPUT"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

View File

@ -0,0 +1,9 @@
*How to use your template*
1. Open the PDF or PNG file for the Paperback Book Cover Template in your image editing software.
2. Create a new layer in your image editing software. This layer will serve as the design layer.
3. Design your cover in the design layer, using the template PDF or PNG file as the guide layer. The artwork should extend to the outside edge of the template's pink zone to ensure a white border will not exist within the printed work. Do not move the guide layer, as it is properly aligned for our printing specifications.
4. Ensure text and/or images that are intended to be read do not appear in the pink zones of the template.
5. The barcode area is indicated in yellow on the template. Do not place important images or text intended to be read in the barcode location. We suggest filling in this area with your background color or design. KDP will automatically generate a barcode representing your title’s ISBN when printing copies of your book.
6. Once your design is complete, you will need to turn off the guide layer so that it is not printed on your final product or rejected during the review process. If you are unable to turn off the guide layer, then you will need to format the artwork so that it completely covers the guide layer.
7. Flatten all layers, save the file as a press quality PDF with CMYK color profile, and upload the file via KDP.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
2026.08-PrayersForDailyLife/art/prayers101_qr.pdf Normal file → Executable file

Binary file not shown.

View File

@ -0,0 +1,680 @@
#!/usr/bin/env python3
"""
generate_epub.py
Converts the same category-coded .txt files used by generate_latex.py
(CODE-CCC.VV.txt, one occasion per file) directly into a single valid
EPUB3 file, suitable for upload to KDP as the Kindle edition.
This does NOT go through the LaTeX/PDF pipeline -- print-specific
formatting (drop caps, lettrine, fixed page breaks, hard \\ line breaks
mid-stanza) doesn't translate to Kindle's reflowable text, where the
reader controls font size and line spacing. Instead, this script parses
the .txt files directly (same parser shape as generate_latex.py) and
emits clean, semantic XHTML -- one file per category chapter, containing
all of that chapter's prayers -- with a proper EPUB3 nav document AND a
legacy NCX (older Kindle "Go To" menus still expect NCX; including both
costs nothing and avoids navigation complaints).
Within each prayer, stanza breaks (blank lines in the source) become
<p> boundaries; single line breaks WITHIN a stanza become <br/>, so the
poem's line structure is preserved without relying on print-only
line-break macros.
Usage:
python generate_epub.py txt output.epub \\
--title "Prayers for Daily Life" \\
--subtitle "101 Devotional Prayers for Every Occasion" \\
--author "George Marin" \\
--publisher "Zechariah Press" \\
--cover cover.jpg \\
--uuid urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
If --uuid isn't given, a random one is generated each run -- fine for
testing, but pick ONE fixed UUID for the real book and pass it every
time (this is the file's permanent identity; changing it between
uploads can confuse KDP/retailers into treating updates as a new book).
"""
import argparse
import html
import re
import sys
import uuid
import zipfile
from pathlib import Path
from categories import CATEGORIES, VALID_CODES, category_name
# --- Parsing (same shape as generate_latex.py) ------------------------------
HEADER_RE = re.compile(
r"^(?P<ref>##\s*\S+)[ \t]*\n"
r">(?!>)[ \t]*(?P<title>[^\n]*)\n"
r">>[ \t]*(?P<quote>[^\n]*)\n"
)
OCCASION_LINE_RE = re.compile(r"(?m)^Occasion:[ \t]*(?P<text>.+)$")
FILENAME_RE = re.compile(r"^(?P<code>[A-Z]+)-(?P<chapter>\d{3})\.(?P<verse>\d{2,3})(?:-[a-z])?\.txt$")
REF_RE = re.compile(r"^(?P<chapter>\d+):(?P<verse>\d+)")
def strip_comment_lines(text: str) -> str:
lines = text.split("\n")
out = []
in_block = False
for ln in lines:
stripped = ln.strip()
if not in_block and stripped == "%v":
in_block = True
continue
if in_block:
if stripped == "%^":
in_block = False
continue
if ln.lstrip().startswith("%"):
continue
out.append(ln)
return "\n".join(out)
def parse_ref(ref_line: str) -> str:
return ref_line.lstrip("#").strip()
def ref_sort_key(ref: str):
m = REF_RE.match(ref)
if not m:
return (9999, 9999)
return (int(m.group("chapter")), int(m.group("verse")))
def parse_file(text: str):
text = strip_comment_lines(text)
stripped = text.lstrip("\n")
m = HEADER_RE.match(stripped)
if not m:
return None
ref = parse_ref(m.group("ref"))
title = m.group("title").strip()
quote = m.group("quote").strip()
body = text[m.end():]
matches = list(OCCASION_LINE_RE.finditer(body))
if len(matches) != 1:
return None
mo = matches[0]
occasion = mo.group("text").strip()
prayer_text = body[mo.end():].strip()
if not prayer_text:
return None
return {"ref": ref, "title": title, "quote": quote, "occasion": occasion, "text": prayer_text}
_MINOR_WORDS = {
"a", "an", "and", "as", "at", "but", "by", "en", "for", "if", "in",
"nor", "of", "on", "or", "per", "the", "to", "v", "via", "vs",
}
def _title_case_word(word: str) -> str:
i = 0
while i < len(word) and not word[i].isalpha():
i += 1
if i >= len(word):
return word
return word[:i] + word[i].upper() + word[i + 1:].lower()
def title_case(text: str) -> str:
words = text.split(" ")
out = []
for idx, w in enumerate(words):
bare = re.sub(r"[^a-zA-Z']", "", w).lower()
if 0 < idx < len(words) - 1 and bare in _MINOR_WORDS:
out.append(w.lower())
else:
out.append(_title_case_word(w))
return " ".join(out)
# --- HTML rendering ----------------------------------------------------------
def esc(text: str) -> str:
return html.escape(text, quote=False)
def convert_latex_typography(text: str) -> str:
"""Converts this project's LaTeX-only typographic conventions into
real Unicode characters, since none of them mean anything to an
EPUB reader: "---" (em dash ligature) -> em dash; ``...'' (curly
quote ligature) -> real curly quotes."""
text = text.replace("---", "\u2014")
text = text.replace("``", "\u201c").replace("''", "\u201d")
return text
def render_foreword_from_latex(tex_text: str) -> str:
"""Parses an ACTUAL template_forward.tex file (not a separate plain-text
copy -- single source of truth, same file that feeds the print
pipeline) into foreword HTML. Handles this project's specific
foreword conventions:
- \\chapter*{...} and \\addcontentsline{...} lines are dropped (the
EPUB foreword gets its own <h1> here instead)
- \\medskip and \\bigskip mark paragraph breaks
- \\noindent is stripped
- wrapped lines within one paragraph are joined with spaces (the
LaTeX source hard-wraps for readability; those line breaks
aren't meaningful the way a prayer's stanza line breaks are)
- \\textit{...} becomes <em>...</em>
- "---" / ``...'' typography converted via convert_latex_typography()
"""
text = tex_text
text = re.sub(r"\\chapter\*\{[^}]*\}", "", text)
text = re.sub(r"\\addcontentsline\{[^}]*\}\{[^}]*\}\{[^}]*\}", "", text)
# \medskip and \bigskip both mark a paragraph break
chunks = re.split(r"\\(?:med|big)skip", text)
paragraphs = []
for chunk in chunks:
chunk = chunk.replace("\\noindent", "")
# collapse hard-wrapped source lines into one flowing paragraph
joined = " ".join(ln.strip() for ln in chunk.split("\n") if ln.strip())
if not joined:
continue
joined = convert_latex_typography(joined)
# \textit{...} -> <em>...</em>, done via sentinel markers so the
# em-tags themselves survive the HTML-escape step untouched
joined = re.sub(r"\\textit\{([^}]*)\}", lambda m: "\uE000" + m.group(1) + "\uE001", joined)
joined = esc(joined)
joined = joined.replace("\uE000", "<em>").replace("\uE001", "</em>")
paragraphs.append(f"<p>{joined}</p>")
return "\n".join(paragraphs)
def render_prayer_html(entry: dict, anchor_id: str) -> str:
"""One prayer as semantic XHTML: heading, verse blockquote, body
paragraphs (stanza = <p>, in-stanza line break = <br/>).
A dagger (&#8224;) follows the final line (always "Amen.") of every
prayer, matching the print edition's "Amen.\\enspace$\\dagger$"
convention.
See convert_latex_typography() -- the source files use LaTeX-only
ligature conventions for em dashes and curly quotes that need
converting to real Unicode characters outside the LaTeX pipeline."""
stanzas = [b.strip() for b in re.split(r"\n\s*\n", entry["text"].strip()) if b.strip()]
para_html = []
for i, stanza in enumerate(stanzas):
lines = [esc(convert_latex_typography(ln.strip())) for ln in stanza.split("\n") if ln.strip()]
joined = "<br/>\n".join(lines)
if i == len(stanzas) - 1:
joined += " &#8224;"
para_html.append(f"<p>{joined}</p>")
body = "\n".join(para_html)
title = esc(title_case(entry["occasion"]))
quote = esc(convert_latex_typography(entry["quote"]))
ref = esc(entry["ref"])
return f"""<section class="prayer" id="{anchor_id}">
<h3>{title}</h3>
<blockquote class="verse"><p>{quote} <span class="ref">(Ps.&#160;{ref})</span></p></blockquote>
{body}
</section>"""
def render_chapter_xhtml(code: str, entries: list, title_prefix: str, flourish_img: str | None = None):
"""Returns (html, toc_items) where toc_items is a list of
(anchor_id, prayer_title) pairs -- used by the caller to build the
nested chapter->prayer TOC (nav.xhtml, toc.ncx, AND the visible
in-book toc.xhtml page all share this same structure).
flourish_img, if given, is the filename (already placed under
OEBPS/images/) of a decorative divider shown under the chapter
title -- same graphic as the print edition's chapter openers."""
name = esc(category_name(code))
sections = []
toc_items = []
for i, e in enumerate(entries):
anchor = f"{code.lower()}-{i+1:03d}"
sections.append(render_prayer_html(e, anchor))
toc_items.append((anchor, title_case(e["occasion"])))
body = "\n\n".join(sections)
flourish_html = ""
if flourish_img:
flourish_html = f'<p class="flourish"><img src="../images/{esc(flourish_img)}" alt="" role="presentation"/></p>'
html_out = f"""<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="en">
<head>
<title>{name}</title>
<link rel="stylesheet" type="text/css" href="../css/style.css"/>
</head>
<body>
<h1>{name}</h1>
{flourish_html}
{body}
</body>
</html>"""
return html_out, toc_items
def render_simple_xhtml(title: str, body_html: str) -> str:
return f"""<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="en">
<head>
<title>{esc(title)}</title>
<link rel="stylesheet" type="text/css" href="../css/style.css"/>
</head>
<body>
{body_html}
</body>
</html>"""
DEFAULT_CSS = """
body { font-family: serif; margin: 1em; line-height: 1.4; }
h1 { text-align: center; margin-top: 1.5em; margin-bottom: 1em; page-break-before: always; }
h2 { text-align: center; }
h3 { margin-top: 2em; page-break-before: always; }
.prayer { margin-bottom: 2em; }
.verse { font-style: italic; margin: 1em 2em; }
.verse .ref { font-style: normal; }
.titlepage { text-align: center; margin-top: 3em; }
.titlepage h1 { page-break-before: avoid; font-size: 1.8em; margin-bottom: 0.2em; }
.titlepage .subtitle { font-size: 1.1em; margin-bottom: 2em; }
.titlepage .author { font-size: 1.2em; margin-top: 2em; }
.copyright { font-size: 0.85em; }
.foreword p { margin-bottom: 1em; }
.center { text-align: center; }
/* Chapter-opening divider graphic, sized relative to text width so it
scales with the reader's font size / screen, not a fixed pixel size. */
p.flourish { text-align: center; margin: 0.5em 0 1.5em 0; }
p.flourish img { width: 30%; max-width: 220px; height: auto; }
"""
# --- EPUB packaging -----------------------------------------------------------
CONTAINER_XML = """<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
"""
def _manifest_item_line(iid: str, href: str, mtype: str, is_nav: bool) -> str:
# Kept as a plain function (not inlined in the f-string below) because
# a backslash-escaped quote inside an f-string's {} expression part is
# only legal on Python 3.12+ (PEP 701) -- this needs to run on older
# Python too.
nav_attr = ' properties="nav"' if is_nav else ""
return f' <item id="{iid}" href="{href}" media-type="{mtype}"{nav_attr}/>'
def build_opf(book_uuid: str, title: str, subtitle: str, author: str, publisher: str,
manifest_items: list, spine_ids: list, cover_id: str | None,
toc_page_href: str | None) -> str:
meta_cover = f'<meta name="cover" content="{cover_id}"/>' if cover_id else ""
manifest = "\n".join(_manifest_item_line(iid, href, mtype, is_nav) for iid, href, mtype, is_nav in manifest_items)
spine = "\n".join(f' <itemref idref="{sid}"/>' for sid in spine_ids)
full_title = f"{title}: {subtitle}" if subtitle else title
# <guide> is EPUB2-era, but Kindle's conversion pipeline and older
# reading apps still use it to find the table of contents -- costs
# nothing to include alongside the EPUB3 nav doc.
guide = ""
if toc_page_href:
guide = f""" <guide>
<reference type="toc" title="Table of Contents" href="{toc_page_href}"/>
</guide>
"""
return f"""<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="BookId">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:identifier id="BookId">{esc(book_uuid)}</dc:identifier>
<dc:title>{esc(full_title)}</dc:title>
<dc:creator>{esc(author)}</dc:creator>
<dc:publisher>{esc(publisher)}</dc:publisher>
<dc:language>en</dc:language>
<meta property="dcterms:modified">2026-01-01T00:00:00Z</meta>
{meta_cover}
</metadata>
<manifest>
<item id="css" href="css/style.css" media-type="text/css"/>
{manifest}
</manifest>
<spine toc="ncx">
{spine}
</spine>
{guide}</package>
"""
def build_nav(title: str, toc_entries: list) -> str:
"""toc_entries: list of dicts {href, label, children: [(href, label), ...]}."""
items = []
for entry in toc_entries:
li = f' <li><a href="{entry["href"]}">{esc(entry["label"])}</a>'
if entry["children"]:
sub = "\n".join(f' <li><a href="{h}">{esc(l)}</a></li>' for h, l in entry["children"])
li += f'\n <ol>\n{sub}\n </ol>\n '
li += "</li>"
items.append(li)
items_str = "\n".join(items)
return f"""<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="en">
<head><title>{esc(title)}</title></head>
<body>
<nav epub:type="toc" id="toc">
<h1>Contents</h1>
<ol>
{items_str}
</ol>
</nav>
</body>
</html>"""
def render_psalm_index_xhtml(title: str, all_entries: list) -> str:
"""all_entries: list of (ref, href_with_anchor, prayer_title), already
sorted by ref_sort_key. Unlike the print index (ref -> page number,
meaningless in reflowable text with no fixed pages), each entry here
is a direct clickable link to that prayer -- arguably MORE useful
than the print version, since it's one tap instead of a page flip.
href_with_anchor is relative to OEBPS/text/ (where this file lives),
same convention as render_toc_page_xhtml."""
items = "\n".join(
f' <li><a href="{href}">{esc(ref)}</a> &#8212; {esc(prayer_title)}</li>'
for ref, href, prayer_title in all_entries
)
body = f'<div class="psalm-index"><h1>Index of Psalm References</h1><ol>\n{items}\n</ol></div>'
return render_simple_xhtml(title, body)
def render_toc_page_xhtml(title: str, toc_entries: list) -> str:
"""A real, visible in-book Contents page (unlike nav.xhtml, which
most reading apps treat as sidebar-menu metadata, not a page you
actually read through). This is what was missing -- KDP/Kindle
conversion and many EPUB readers expect an actual TOC page in the
reading order, not just the EPUB3 nav doc.
IMPORTANT: this file itself lives at OEBPS/text/toc.xhtml, alongside
the chapter files -- so unlike nav.xhtml (which lives at the OEBPS/
root and needs "text/..." prefixed hrefs), links here must be
relative to the text/ directory, i.e. with that prefix stripped."""
def strip_prefix(href: str) -> str:
return href[len("text/"):] if href.startswith("text/") else href
items = []
for entry in toc_entries:
href = strip_prefix(entry["href"])
li = f' <li><a href="{href}">{esc(entry["label"])}</a>'
if entry["children"]:
sub = "\n".join(f' <li><a href="{strip_prefix(h)}">{esc(l)}</a></li>' for h, l in entry["children"])
li += f'\n <ol>\n{sub}\n </ol>\n '
li += "</li>"
items.append(li)
items_str = "\n".join(items)
body = f'<div class="toc-page"><h1>Contents</h1><ol>\n{items_str}\n</ol></div>'
return render_simple_xhtml(title, body)
def build_ncx(book_uuid: str, title: str, toc_entries: list) -> str:
points = []
counter = [0]
def next_order():
counter[0] += 1
return counter[0]
for entry in toc_entries:
parent_order = next_order()
child_points = []
for h, l in entry["children"]:
child_order = next_order()
child_points.append(f""" <navPoint id="navPoint-{child_order}" playOrder="{child_order}">
<navLabel><text>{esc(l)}</text></navLabel>
<content src="{h}"/>
</navPoint>""")
child_str = "\n".join(child_points)
points.append(f""" <navPoint id="navPoint-{parent_order}" playOrder="{parent_order}">
<navLabel><text>{esc(entry['label'])}</text></navLabel>
<content src="{entry['href']}"/>
{child_str}
</navPoint>""")
points_str = "\n".join(points)
return f"""<?xml version="1.0" encoding="UTF-8"?>
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
<head>
<meta name="dtb:uid" content="{esc(book_uuid)}"/>
</head>
<docTitle><text>{esc(title)}</text></docTitle>
<navMap>
{points_str}
</navMap>
</ncx>
"""
def write_epub(out_path: Path, files: dict, image_paths: list):
"""files: dict of archive-path -> str content (text files) OR bytes.
image_paths: list of local Path objects to copy in under
OEBPS/images/ (cover, flourish, etc.) -- their manifest entries are
added by the caller; this just physically copies the bytes in.
mimetype MUST be first and MUST be stored (not deflated)."""
if out_path.exists():
out_path.unlink()
with zipfile.ZipFile(out_path, "w") as z:
z.writestr("mimetype", "application/epub+zip", compress_type=zipfile.ZIP_STORED)
for path, content in files.items():
data = content if isinstance(content, bytes) else content.encode("utf-8")
z.writestr(path, data, compress_type=zipfile.ZIP_DEFLATED)
for img_path in image_paths:
z.write(img_path, f"OEBPS/images/{img_path.name}", compress_type=zipfile.ZIP_DEFLATED)
# --- Main ---------------------------------------------------------------------
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("txt_dir", type=Path)
ap.add_argument("output_epub", type=Path)
ap.add_argument("--title", default="Prayers for Daily Life")
ap.add_argument("--subtitle", default="101 Devotional Prayers for Every Occasion")
ap.add_argument("--author", default="George Marin")
ap.add_argument("--publisher", default="Zechariah Press")
ap.add_argument("--cover", type=Path, default=None, help="Cover image (jpg/png). Strongly recommended for KDP.")
ap.add_argument("--flourish", type=Path, default=None, help="Optional decorative divider image (jpg/png) shown under each chapter title -- same graphic as the print edition's chapter openers, if you want it.")
ap.add_argument("--foreword", type=Path, default=None, help="Your actual template_forward.tex file -- parsed directly (paragraph breaks on \\medskip/\\bigskip, \\textit{} to italics, etc.), not a separate plain-text copy.")
ap.add_argument("--review-url", default=None, help="If given, adds a back-matter review-request page with a clickable link to this URL (NOT a QR code -- pointless in an ebook, since the reader's already on the device).")
ap.add_argument("--no-psalm-index", action="store_true", help="Omit the back-matter Index of Psalm References (on by default). Unlike the print version, this is a page of CLICKABLE LINKS, not page numbers -- reflowable text has no fixed pages to index.")
ap.add_argument("--uuid", default=None, help="Fixed book UUID (e.g. urn:uuid:...). Random if omitted -- fine for testing, but pin one for the real release.")
ap.add_argument("--category-order", default=None, help="Comma-separated category codes, e.g. WON,WWF,CRI,...")
args = ap.parse_args()
# Fail loudly and immediately if an optional file path was actually
# GIVEN but doesn't resolve -- silently skipping it (the old
# behavior) is exactly the kind of mistake that ships a KDP EPUB
# with no embedded cover and nobody notices until much later.
for flag_name, path in [
("--cover", args.cover),
("--flourish", args.flourish),
("--foreword", args.foreword),
]:
if path is not None and not path.exists():
sys.exit(f"{flag_name} was given as {path!s}, but that file doesn't exist. Fix the path or drop the flag.")
book_uuid = args.uuid or f"urn:uuid:{uuid.uuid4()}"
order = args.category_order.split(",") if args.category_order else list(CATEGORIES.keys())
for c in order:
if c not in VALID_CODES:
sys.exit(f"Unknown category code in --category-order: {c}")
txt_files = sorted(args.txt_dir.glob("*.txt"))
by_category: dict[str, list] = {c: [] for c in order}
skipped = []
for path in txt_files:
m = FILENAME_RE.match(path.name)
if not m or m.group("code") not in VALID_CODES:
continue
parsed = parse_file(path.read_text(encoding="utf-8"))
if parsed is None:
skipped.append(path.name)
continue
code = m.group("code")
if code not in by_category:
by_category[code] = []
by_category[code].append(parsed)
if skipped:
print(f"Skipped {len(skipped)} file(s) that didn't parse:")
for s in skipped:
print(f" {s}")
for code in by_category:
by_category[code].sort(key=lambda e: ref_sort_key(e["ref"]))
total = sum(len(v) for v in by_category.values())
print(f"Building EPUB from {total} entries across {sum(1 for v in by_category.values() if v)} categories.")
# --- Assemble files ---
files: dict[str, object] = {}
files["META-INF/container.xml"] = CONTAINER_XML
files["OEBPS/css/style.css"] = DEFAULT_CSS
manifest_items = [] # (id, href, media-type, is_nav)
spine_ids = []
toc_entries = [] # list of {href, label, children: [(href#anchor, label), ...]}
# Title page
title_html = f"""<div class="titlepage">
<h1>{esc(args.title)}</h1>
<p class="subtitle">{esc(args.subtitle)}</p>
<p class="author">{esc(args.author)}</p>
</div>"""
files["OEBPS/text/titlepage.xhtml"] = render_simple_xhtml(args.title, title_html)
manifest_items.append(("titlepage", "text/titlepage.xhtml", "application/xhtml+xml", False))
spine_ids.append("titlepage")
# Copyright page
copyright_html = f"""<div class="copyright">
<p>{esc(args.title)}: {esc(args.subtitle)}</p>
<p>Copyright &#169; 2026. All rights reserved.</p>
<p>Published by {esc(args.publisher)}</p>
</div>"""
files["OEBPS/text/copyright.xhtml"] = render_simple_xhtml("Copyright", copyright_html)
manifest_items.append(("copyright", "text/copyright.xhtml", "application/xhtml+xml", False))
spine_ids.append("copyright")
# Table of Contents -- a REAL visible page in the reading order, not just
# the EPUB3 nav.xhtml (which most reading apps only use for their own
# sidebar menu, not as an actual page). Placed here as a spine_ids
# placeholder; the id "toc-page" is inserted once its file is written
# below, once we know the full nested structure -- so reserve its
# position in the spine now, fill in content after the loop.
TOC_PAGE_POS = len(spine_ids)
spine_ids.append("toc-page") # placeholder, replaced below once content is built
# Foreword (optional) -- parsed directly from the real template_forward.tex
if args.foreword: # existence already validated above
fw_html = "<h1>Foreword</h1>\n" + render_foreword_from_latex(args.foreword.read_text(encoding="utf-8"))
files["OEBPS/text/foreword.xhtml"] = render_simple_xhtml("Foreword", f'<div class="foreword">{fw_html}</div>')
manifest_items.append(("foreword", "text/foreword.xhtml", "application/xhtml+xml", False))
spine_ids.append("foreword")
toc_entries.append({"href": "text/foreword.xhtml", "label": "Foreword", "children": []})
# One XHTML file per category chapter, plus its nested prayer-level
# TOC entries (anchor links within that chapter's file)
for code in order:
entries = by_category.get(code, [])
if not entries:
continue
chapter_html, prayer_toc_items = render_chapter_xhtml(
code, entries, args.title,
flourish_img=args.flourish.name if args.flourish else None, # existence already validated above
)
fname = f"{code.lower()}.xhtml"
files[f"OEBPS/text/{fname}"] = chapter_html
cid = f"chap-{code.lower()}"
manifest_items.append((cid, f"text/{fname}", "application/xhtml+xml", False))
spine_ids.append(cid)
children = [(f"text/{fname}#{anchor}", label) for anchor, label in prayer_toc_items]
toc_entries.append({"href": f"text/{fname}", "label": category_name(code), "children": children})
# Review request (optional) -- a real hyperlink, not a QR code; see
# --review-url help text for why a QR code doesn't make sense here.
if args.review_url:
review_html = f"""<div class="center">
<h2>Did this book meet you somewhere?</h2>
<p>If it did, a review helps the next reader find their way here too.</p>
<p><a href="{esc(args.review_url)}">{esc(args.review_url)}</a></p>
</div>"""
files["OEBPS/text/review.xhtml"] = render_simple_xhtml("A Request", review_html)
manifest_items.append(("review", "text/review.xhtml", "application/xhtml+xml", False))
spine_ids.append("review")
# Index of Psalm References (optional, on by default) -- flat list
# across ALL categories, sorted by verse reference, each entry
# linking directly to its prayer. Built here (after every chapter's
# anchors are known) rather than during the per-category loop above,
# since it needs the complete cross-category picture.
if not args.no_psalm_index:
all_entries = []
for code in order:
for i, e in enumerate(by_category.get(code, [])):
anchor = f"{code.lower()}-{i+1:03d}"
href = f"{code.lower()}.xhtml#{anchor}"
all_entries.append((e["ref"], href, title_case(e["occasion"])))
all_entries.sort(key=lambda t: ref_sort_key(t[0]))
files["OEBPS/text/psalm-index.xhtml"] = render_psalm_index_xhtml("Index of Psalm References", all_entries)
manifest_items.append(("psalm-index", "text/psalm-index.xhtml", "application/xhtml+xml", False))
spine_ids.append("psalm-index")
toc_entries.append({"href": "text/psalm-index.xhtml", "label": "Index of Psalm References", "children": []})
# Now that toc_entries is fully built, write the actual TOC page and
# fill in the spine placeholder reserved above.
files["OEBPS/text/toc.xhtml"] = render_toc_page_xhtml("Contents", toc_entries)
manifest_items.append(("toc-page", "text/toc.xhtml", "application/xhtml+xml", False))
spine_ids[TOC_PAGE_POS] = "toc-page"
# Cover
cover_id = None
image_paths = [] # local files to physically copy into OEBPS/images/
if args.cover: # existence already validated above
image_paths.append(args.cover)
media_type = "image/jpeg" if args.cover.suffix.lower() in (".jpg", ".jpeg") else "image/png"
cover_id = "cover-image"
manifest_items.append((cover_id, f"images/{args.cover.name}", media_type, False))
cover_xhtml = f'<div class="center"><img src="../images/{esc(args.cover.name)}" alt="Cover" style="max-width:100%;"/></div>'
files["OEBPS/text/cover.xhtml"] = render_simple_xhtml("Cover", cover_xhtml)
manifest_items.append(("cover-page", "text/cover.xhtml", "application/xhtml+xml", False))
spine_ids.insert(0, "cover-page")
if args.flourish: # existence already validated above
image_paths.append(args.flourish)
media_type = "image/jpeg" if args.flourish.suffix.lower() in (".jpg", ".jpeg") else "image/png"
manifest_items.append(("flourish-image", f"images/{args.flourish.name}", media_type, False))
# Nav + NCX -- both reference the SAME nested toc_entries as the
# visible toc.xhtml page above, so the reader-UI sidebar and the
# in-book Contents page always agree.
manifest_items.append(("nav", "nav.xhtml", "application/xhtml+xml", True))
manifest_items.append(("ncx", "toc.ncx", "application/x-dtbncx+xml", False))
files["OEBPS/nav.xhtml"] = build_nav(args.title, toc_entries)
files["OEBPS/toc.ncx"] = build_ncx(book_uuid, args.title, toc_entries)
files["OEBPS/content.opf"] = build_opf(
book_uuid, args.title, args.subtitle, args.author, args.publisher,
manifest_items, spine_ids, cover_id, toc_page_href="text/toc.xhtml",
)
write_epub(args.output_epub, files, image_paths)
print(f"Wrote {args.output_epub} ({total} prayers, {sum(1 for v in by_category.values() if v)} chapters)")
if __name__ == "__main__":
main()

View File

@ -147,7 +147,7 @@ No permission is required for its use.
\noindent Published by Zechariah Press
\noindent ISBN: 000-0-000000-00-0 % TODO: replace with the assigned ISBN before publication
\noindent ISBN: 979-8-1733570-08-3 % 000-0-000000-00-0
\medskip
\noindent First Edition

View File

@ -0,0 +1,10 @@
python3 generate_epub.py ../txt PrayersForDailyLife.epub \
--title "Prayers for Daily Life" \
--subtitle "101 Devotional Prayers for Every Occasion" \
--author "George Marin" \
--publisher "Zechariah Press" \
--cover ../art/BackSpineFront-Cover-2026.09.16-1230.jpg \
--foreword ./template-foreward.tex \
--review-url "https://zechariahpress.com/q/PrayersForDailyLife" \
--uuid "1460ac0b-4f40-4645-b12b-2c78aee89d34" \
--flourish ../art/o01.png

View File

@ -4,11 +4,12 @@ set -e
rm -f main.aux main.idx main.ilg main.ind main.ind main.log main.pdf main.toc $DEPOT/main.pdf
for p in CON CRI DRY GRA GUI LOS MIL REL TMP WON WWF
do
rm $p*.tex
rm -f $p*.tex
done
python3 generate_latex.py ../txt . --template template-prayer.tex --main-template template-main.tex
pdflatex main
makeindex main
pdflatex main
pdflatex main
cp main.pdf "$DEPOT/main-$(date +%Y.%m.%d-%H%M).pdf"
cp main.pdf "Prayers-$(date +%Y.%m.%d-%H%M).pdf"

View File

@ -4,24 +4,24 @@
\noindent I didn't set out to write a hundred and one prayers.
I set out to write the ones I needed and couldn't find ---
short enough to pray before the day got away,
plain enough to skip the churchier language.
plain enough to skip churchy language.
\medskip
\noindent It didn't take long to notice a pattern:
that every prayer
every prayer
rhymed with something a psalmist had already said three thousand years ago.
Weariness, wonder, gratitude, grief --- none of it is new.
\medskip
\noindent This book borrows their voice.
Each prayer opens with a verse from the Psalms and stays close to it,
but with present-day concerns --- a hospital wristband, a gurney, plastic bins after a storm.
but with present-day concerns --- a hospital wristband, plastic bins after a storm.
A prayer too general, ``Lord, help me in my trouble,'' rarely moves anyone;
it asks for nothing you can picture.
it asks for nothing you can wrap your hands around.
But one too specific runs a different risk: what if your trouble doesn't look like mine?
\medskip
\noindent So hold these loosely. Think of each one less as a script
\noindent So hold these loosely. Think of them less as a script
and more as an aperture --- a way of opening your imagination onto something
you already recognize. Let your words take over where mine leave off.
@ -33,7 +33,7 @@ since it's what most of us are used to.
\medskip
\noindent Wherever you are today --- weary, waiting, grateful, undone ---
my prayer is that you'll find something here that meets you there.
my prayer is that you'll find something here that meets you where you are.
\bigskip
\noindent \textit{George Marin}

View File

@ -147,7 +147,7 @@ No permission is required for its use.
\noindent Published by Zechariah Press
\noindent ISBN: 000-0-000000-00-0 % TODO: replace with the assigned ISBN before publication
\noindent ISBN: 979-8-1733570-08-3 % 000-0-000000-00-0
\medskip
\noindent First Edition

View File

@ -13,5 +13,10 @@ find their way here too.}
\vspace{1em}
{\small zechariahpress.com/reviews/PrayersForDailyLife}
\vspace{1.5em}
{\itshape \large Thank you.}
\end{center}
\vspace*{\fill}

View File

@ -3,7 +3,7 @@
> My soul longs, and even faints for the courts of the LORD
>> My soul longs, and even faints for the courts of the LORD; my heart and my flesh cry out for the living God.
Occasion: Restless after getting what you wanted
Occasion: Restless after getting what I wanted
Lord, I got the thing I asked for,
and nothing changed.

View File

@ -1,7 +1,7 @@
## 143:8
%% 4
> Your Lovingkindness in the Morning
>> Cause me to hear your lovingkindness in the morning, for I trust in you. Cause me to know the way in which I should walk, for I lift up my soul to you.
>> Cause me to hear your lovingkindness in the morning, for I trust in you.
Occasion: Praying first thing
@ -11,16 +11,12 @@ before I speak to anyone else,
Lord, let me speak with You.
I am here, though part of me is still waking,
and part of me has already begun
to carry the weight of the day.
Yesterday is not entirely gone.
Today is already asking things of me.
and today is already asking things of me.
Let Your kindness be the first thing I hear this morning.
Let Your lovingkindness be the first thing I hear this morning.
Teach me to bring You my soul
before I hand this day to habit,
hurry,
before I hand this day to habit, hurry,
or whatever shouts the loudest.
One thought follows another,
@ -30,16 +26,14 @@ where I am going
or who I am becoming along the way.
So make this first turning toward You plain.
A chair.
A breath.
A breath. A whisper.
A few honest words.
I don't need to sound wise ---
as if You didn't already know me.
I don't need the whole way mapped ---
one step is what You've promised.
Only show me how to walk faithfully
into the hours before me,
one step is all You've promised.
Only show me how to walk faithfully into the hours before me,
and give me grace enough
for the next thing.
@ -48,4 +42,4 @@ be first.
Before the screen lights up,
be first.
Amen.
Amen.

View File

@ -14,7 +14,7 @@ trying to secure tomorrow with my own hands.
One unanswered question becomes ten,
and soon I am living inside a future
that has not even arrived.
And likjely never will.
And likely never will.
I am tired of carrying it,
but I am afraid of what will happen if I let it go.

View File

@ -6,7 +6,6 @@
Occasion: When I'm grateful but don't quite know how to say it
Lord, thank You.
Saying that seems too small,
but it's what I have.
@ -43,4 +42,4 @@ these inadequate words will have to suffice:
Thank You.
Amen.
Amen.

View File

@ -6,7 +6,6 @@
Occasion: When I've had a good day
Lord, today was good.
Nothing spectacular.
No sea parted.
No mountain moved.
@ -42,4 +41,4 @@ I was here for it.
And I am glad.
And grateful.
Amen.
Amen.

View File

@ -3,7 +3,7 @@
> Teach Us To Count Our Days
>> So teach us to count our days, that we may gain a heart of wisdom.
Occasion: Choosing how to spend your time
Occasion: Choosing how to spend my time
There are too many tasks in front of me, Lord ---
the message to answer,

View File

@ -6,7 +6,6 @@
Occasion: Miscarriage
Lord, You knit together what I never got to hold.
I don't have a photograph.
I don't have a name everyone else got to use.
Just a due date I still know by heart,
@ -34,4 +33,4 @@ be someone You carry still.
Hold what I never got to hold.
Amen.
Amen.

View File

@ -39,3 +39,6 @@ one ordinary moment at a time.
Be the first guest in this house,
the one who never leaves,
the one who makes it a home.
Amen.

View File

@ -6,7 +6,6 @@
Occasion: When I need to forgive
Lord, I know I need to forgive.
I also know I cannot make the hurt disappear
by deciding that I should.

View File

@ -6,7 +6,6 @@
Occasion: When I need courage for an ordinary conversation
Lord, I need to say something.
It is not a speech.
No crowd is waiting.
The world will go on afterward.
@ -41,4 +40,4 @@ be acceptable to You.
Help me open my mouth
and begin.
Amen.
Amen.

View File

@ -6,7 +6,6 @@
Occasion: When I can't let something go
Lord, here it is again.
The memory.
The conversation.
The mistake.
@ -44,4 +43,4 @@ I am putting it down again.
And if I pick it up tomorrow,
I will bring it back again.
Amen.
Amen.

View File

@ -6,7 +6,6 @@
Occasion: When I don't want to do what needs doing
Lord, I know what needs doing.
I simply don't want to do it.
I'd rather postpone it,
@ -42,4 +41,4 @@ This is today's work.
Establish it.
Amen.
Amen.

View File

@ -6,7 +6,6 @@
Occasion: When I'm disappointed in myself
Lord, I expected better of myself.
I thought I would handle this differently.
I thought I had grown past this.
I thought by now I would be wiser,
@ -42,4 +41,4 @@ You have not mistaken me
for someone already finished.
Let's begin again.
Amen
Amen.

View File

@ -5,7 +5,7 @@
Occasion: Caring for the natural world
MMorning light on the leaves, Lord.
Morning light on the leaves, Lord.
Water running over stone.
The smell of rain on warm earth.
This world is precious,
@ -45,4 +45,4 @@ Keep me in that light,
attentive and humble,
careful with what is Yours.
Amen.
Amen.

View File

@ -6,10 +6,8 @@
Occasion: When I'm bone tired
Lord, I'm bone tired.
Nothing terrible has happened.
No crisis to solve.
I've simply reached the end
of what I have for today.

View File

@ -5,7 +5,6 @@
Occasion: When I can't stop worrying
Lord, my mind will not leave this alone.
I turn it over,
then turn it over again,
looking for something I missed,
@ -32,4 +31,4 @@ Still my soul beside You,
like a child who no longer needs to understand ---
all that matters is where home is.
Amen.
Amen.

View File

@ -0,0 +1,122 @@
Wonder & Thanksgiving
* Feeling small in a big world
* After a close call
* Thanking God for creation
* Caring for the natural world
* Wonder at how I'm made
* The stars on a clear night
* Wonder at absolutely everything
Weariness, Worry & Fear
* Waiting for the right season
* Trouble falling asleep
* Waking up worried
* Slow recovery from illness
* Fearing joy won't last
* Before speaking in public
* When I'm bone tired
* Fear of not having enough
* After losing a job
* When I have to wait
* When I can't stop worrying
* During a lonely season
Guidance \& Decisions
* Bringing dreams before God
* Making a big move
* Facing an important morning
* Seeking God before decisions
* When I reach for the wrong refuge
* Making the most of today
* Stress from too many decisions
* Choosing how to spend your time
* Taking one step at a time
* Needing moral clarity
Crisis \& Emergency
* Waiting for test results
* When panic takes over
* Before a big surgery
* Bad news from far away
* Bad news from the doctor
* Starting over after disaster
* After hearing bad news
* Facing the end of life
Loss
* A loss of something important
* Loss of a pet
* Grieving a broken family
* Loss of a child
* Miscarriage
* Loss of a friend
* Loss of parents
* Loss of a spouse
* Grieving a suicide
Milestones
* Starting a new job
* Moving into a new home
* Graduation
* Wedding
* The arrival of a new child
* The arrival of a new grandchild
* Retirement
Temptation \& Self-Examination
* Envy over someone else's success
* After a failure
* Letting go of control
* Asking forgiveness for hurtful words
* When I can't let something go
* When shame keeps me away
* When I don't want to do what needs doing
* Wanting wholehearted worship
* Confessing hidden wrongdoing
* When I'm disappointed in myself
* Wanting to break old habits
* When my motives are mixed
Spiritual Dryness \& Discontent
* A distracted heart
* Coming back after drifting
* When today feels wasted
* Restless after getting what you wanted
* Discouraged by slow growth
* Inner turmoil
* When God's goodness feels distant
* When gratitude feels hard
* Praying first thing
Relationships
* After being let down
* After hurting someone deeply
* When you have no one to talk to
* Teaching a child about God
* Children afraid of the dark
* When I need to forgive
* Building a marriage
* When I need courage for an ordinary conversation
Conflicts
* Joining in cruel jokes
* Stress over unfair treatment
* When I'm angry with someone I love
* Walking into a hard meeting
* Trouble that follows you home
* A church split healing
Gratitude
* Learning to trust God's provision
* Thanking God for answered prayer
* Coming out of a hard season
* Seeking contentment in God
* Giving thanks for enough
* Thankfulness for this morning
* Waking up grateful
* Singing with joy again
* Remembering past help
* Staying faithful in old age
* When blessings feel ordinary
* When I'm grateful but don't quite know how to say it
* When I've had a good day