#!/usr/bin/env python3 """Render bible.json into one semantic HTML fragment per book. Reads the JSON produced by extract_bible.py and writes books/GEN.html and so on, plus an index. These are fragments, not documents: a and one root element. The document around them is assets/shell.html, which is static and the same for every page, so chrome changes never require running this. The markup carries structure and no presentation: every USFM distinction survives as a class or data attribute, so the whole corpus can be restyled from the stylesheet without regenerating it. This is the only build step left, and books/ is all it owns: everything hand-written lives in assets/ and is served straight from there. Run it when bible.json changes, which is to say almost never. Grouping worth knowing about: consecutive poetry lines are wrapped in a <div class="poetry"> that a blank-line block closes, so stanzas are real elements rather than spacing; consecutive list items become one <ul>. """ from __future__ import annotations import argparse import html import json import os import sys # Inline node type -> (tag, class). ``cite`` and ``dfn`` carry real meaning # here: a book name cited in a note, and a glossary term at its definition. INLINE_TAGS = { "wj": ("span", "wj"), # words of Jesus "keyword": ("dfn", "k"), # glossary headword "bookref": ("cite", "bk"), # book title cited in a note "fq": ("span", "fq"), # the words a footnote is about "fqa": ("span", "fqa"), # an alternative rendering "fl": ("span", "fl"), # footnote label "fine": ("span", "fine"), "xt": ("span", "xt"), # cross-reference target } # Block type -> tag. Headings sit at h3 so the outline never skips a level: # h1 book, h2 chapter, h3 section. BLOCK_TAGS = { "paragraph": "p", "line": "p", "heading": "h3", "title": "p", "speaker": "p", "item": "li", } def esc(text): return html.escape(text, quote=False) def verse_id(chapter, label): """Page-unique verse anchor, e.g. v23_1 (or v11_15-16 for a range).""" return "v%d_%s" % (chapter, label) def ref_href(node, book, chapter): """Link an intra-corpus reference; whole books are single pages.""" target_book = node.get("book") or book target_ch = node.get("chapter", chapter) verse = node.get("verse") base = "" if target_book == book else "%s.html" % target_book if verse: return "%s#%s" % (base, verse_id(target_ch, verse)) return "%s#ch%d" % (base, target_ch) def render_inline(nodes, book, chapter, out): for node in nodes: kind = node["t"] if kind == "text": out.append(esc(node["v"])) elif kind == "verse": # Trailing space keeps the number off the text before any CSS # lands; style .v with padding and it stays correct either way. out.append('<span class="v" id="%s">%s</span> ' % ( verse_id(chapter, node["label"]), esc(node["label"]))) elif kind == "noteref": n = node["id"].replace("FN", "") out.append('<a class="noteref" id="fnref%s_%s" href="#fn%s_%s">' "<sup>%s</sup></a>" % (chapter, n, chapter, n, esc(node.get("marker") or "*"))) elif kind == "br": out.append("<br>") elif kind == "ref": out.append('<a class="ref" href="%s">' % ref_href(node, book, chapter)) render_inline(node.get("c", []), book, chapter, out) out.append("</a>") elif kind in INLINE_TAGS: tag, cls = INLINE_TAGS[kind] out.append('<%s class="%s">' % (tag, cls)) render_inline(node.get("c", []), book, chapter, out) out.append("</%s>" % tag) else: # unknown node type: keep the words, drop the wrapper render_inline(node.get("c", []), book, chapter, out) def render_block(block, book, chapter, out): btype = block["type"] tag = BLOCK_TAGS.get(btype, "p") classes = [block["style"]] attrs = "" if "indent" in block: attrs += ' data-indent="%d"' % block["indent"] if btype == "line": classes.append("line") out.append(" <%s class=\"%s\"%s>" % (tag, " ".join(classes), attrs)) render_inline(block.get("content", []), book, chapter, out) out.append("</%s>\n" % tag) def render_notes(notes, chapter, book, out): if not notes: return out.append(' <section class="footnotes">\n') out.append(' <h4 class="footnotes-title">Notes</h4>\n') out.append(" <ol>\n") for note in notes: n = (note["id"] or "").replace("FN", "") out.append(' <li class="note" id="fn%s_%s" data-kind="%s">' % (chapter, n, note["kind"])) out.append('<span class="marker">%s</span> ' % esc(note.get("marker") or "*")) if note.get("ref"): out.append('<a class="backref" href="#fnref%s_%s">%s</a> ' % (chapter, n, esc(note["ref"]))) out.append('<span class="ft">') render_inline(note["content"], book, chapter, out) out.append("</span></li>\n") out.append(" </ol>\n </section>\n") def render_chapter(chapter, book, out): num = chapter["number"] out.append(' <section class="chapter" id="ch%d" data-chapter="%d">\n' % (num, num)) out.append(' <h2 class="chapter-number">%d</h2>\n' % num) group = None # "poetry" | "list" | None def close_group(): nonlocal group if group == "poetry": out.append(" </div>\n") elif group == "list": out.append(" </ul>\n") group = None for block in chapter["blocks"]: btype = block["type"] if btype == "break": close_group() # a blank line ends the stanza continue want = {"line": "poetry", "item": "list"}.get(btype) if want != group: close_group() if want == "poetry": out.append(' <div class="poetry">\n') elif want == "list": out.append(' <ul class="list">\n') group = want render_block(block, book, num, out) close_group() render_notes(chapter["notes"], num, book, out) out.append(" </section>\n") def render_book(book, translation, out_dir): code, name = book["code"], book["name"] out = [] title = "%s — %s" % (name, translation["name"]) # A fragment: the title the shell will use, then one root element. The # class on it is what identifies the kind of page to CSS. out.append("<title>%s\n" % esc(title)) out.append('
\n' % (code, code, esc(name), book["section"])) # Header: the full multi-line title if the source had one. out.append('
\n') out.append("

") if book["titles"]: # Separated by a space, not run together. The spans record what USFM # called them -- mt2 and mt3 were lines above and below the main title # -- but they are set as one line here, and without the space between # them Genesis reads "The First Book of Moses,Commonly CalledGenesis". # The markup still says what each part is, so stacking them again is a # stylesheet away. out.append(" ".join('%s' % (part["style"], esc(part["text"])) for part in book["titles"])) else: out.append(esc(name)) out.append("

\n") out.append(' \n") out.append('

All books

\n') out.append("
\n") out.append('
\n') for chapter in book["chapters"]: render_chapter(chapter, code, out) out.append("
\n") out.append(' \n' % (esc(translation["name"]), esc(translation["license"]))) out.append("
\n") path = os.path.join(out_dir, "%s.html" % code) with open(path, "w", encoding="utf-8") as fh: fh.write("".join(out)) return path SECTION_LABELS = [ ("ot", "Old Testament"), ("nt", "New Testament"), ("deuterocanon", "Deuterocanon"), ("other", "Other"), ] def render_index(books, translation, out_dir): out = ["%s\n" % esc(translation["name"]), '
\n'] for key, label in SECTION_LABELS: group = [b for b in books if b["section"] == key] if not group: continue out.append('
\n' % key) out.append("

%s

\n \n
\n") out.append(' \n' % (esc(translation["name"]), esc(translation["license"]))) out.append("
\n") path = os.path.join(out_dir, "index.html") with open(path, "w", encoding="utf-8") as fh: fh.write("".join(out)) return path def main(argv=None): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("-i", "--input", default="bible.json", help="JSON from extract_bible.py (default: bible.json)") ap.add_argument("-o", "--out", default="books", help="output directory (default: books)") args = ap.parse_args(argv) with open(args.input, encoding="utf-8") as fh: data = json.load(fh) os.makedirs(args.out, exist_ok=True) total = 0 for book in data["books"]: path = render_book(book, data["translation"], args.out) total += os.path.getsize(path) render_index(data["books"], data["translation"], args.out) print("books %d" % len(data["books"]), file=sys.stderr) print("html %.1f MB total, %.0f KB average" % (total / 1e6, total / len(data["books"]) / 1e3), file=sys.stderr) print("wrote %s/" % args.out, file=sys.stderr) return 0 if __name__ == "__main__": sys.exit(main())