Remove comments
This commit is contained in:
1 parent
872175153a
commit
057b4be7d7
18 files changed
+66
-1350
No files matched your search
+42
-265
@@ -1,44 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile the eBible.org HTML export into the fragments the app serves.
|
||||
|
||||
One step, source to output. It reads a directory of eBible .htm files -- a flat
|
||||
set of per-chapter pages, ``JHN03.htm``, plus a site index -- and writes
|
||||
bible/GEN.html and so on, plus bible/index.html.
|
||||
|
||||
There used to be two scripts and a 15 MB bible.json between them. The JSON was
|
||||
worth having when the corpus was going to be queried; nothing queries it, so
|
||||
it was a second representation to keep in step for no one's benefit. The parse
|
||||
tree now lives for as long as one process run.
|
||||
|
||||
The two halves are still clearly separated below, because they are genuinely
|
||||
different jobs and the seam is where a second output format would attach:
|
||||
|
||||
READING eBible's generated markup is a flat sequence of
|
||||
``<div class='...'>`` blocks whose class is the USFM paragraph
|
||||
marker (``p``, ``q2``, ``s``, ...), with verse numbers, footnote
|
||||
markers and character styles as inline spans. This half turns
|
||||
that into a tree of blocks and inline nodes. Navigation and the
|
||||
copyright footer are dropped; footnotes are kept.
|
||||
|
||||
WRITING That tree becomes one semantic fragment per book: a <title> and
|
||||
one root element carrying data-template, not a document. The
|
||||
shell around it is assembled from the nunjucks templates in
|
||||
assets/templates/ by the service worker, 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 being
|
||||
regenerated.
|
||||
|
||||
This is the only build step in the project, and bible/ is all it owns.
|
||||
Its output is not committed: bible/ is in .gitignore, so a deploy runs this.
|
||||
Everything hand-written lives in assets/ and is served straight from there.
|
||||
|
||||
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>.
|
||||
|
||||
python3 utils/compile_bible.py source -o bible
|
||||
"""
|
||||
"""Compile the eBible.org HTML export into the fragments the app serves."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -50,70 +10,54 @@ import re
|
||||
import sys
|
||||
from html.parser import HTMLParser
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# READING -- eBible .htm into a tree of blocks and inline nodes
|
||||
# ==========================================================================
|
||||
|
||||
# Chapter files are a three-character book code plus a zero-padded chapter
|
||||
# number (three digits for Psalms).
|
||||
CHAPTER_RE = re.compile(r"^([0-9A-Z]{3})(\d{2,3})\.htm$")
|
||||
|
||||
# Links between chapters: "JOB05.htm#V13", "JHN03.htm", or a bare "#V7".
|
||||
HREF_RE = re.compile(r"^(?:([0-9A-Z]{3})(\d{2,3})\.htm)?(?:#V(\d+))?$")
|
||||
|
||||
# Block-level div classes -> semantic block type.
|
||||
BLOCK_TYPES = {
|
||||
"p": "paragraph", # normal prose paragraph
|
||||
"m": "paragraph", # continuation, no first-line indent
|
||||
"nb": "paragraph", # no break with previous
|
||||
"pi": "paragraph", # indented
|
||||
"mi": "paragraph", # indented, no first-line indent
|
||||
"pc": "paragraph", # centered
|
||||
"ip": "paragraph", # introduction paragraph
|
||||
"q": "line", # poetry line
|
||||
"p": "paragraph",
|
||||
"m": "paragraph",
|
||||
"nb": "paragraph",
|
||||
"pi": "paragraph",
|
||||
"mi": "paragraph",
|
||||
"pc": "paragraph",
|
||||
"ip": "paragraph",
|
||||
"q": "line",
|
||||
"q2": "line",
|
||||
"q3": "line",
|
||||
"qs": "line", # Selah
|
||||
"b": "break", # blank line between stanzas
|
||||
"d": "title", # descriptive title (psalm superscription, acrostic letter)
|
||||
"s": "heading", # section heading
|
||||
"ms": "heading", # major section heading ("BOOK 3")
|
||||
"is": "heading", # introduction heading
|
||||
"sp": "speaker", # speaker label (Song of Solomon)
|
||||
"li": "item", # list item
|
||||
"ili": "item", # introduction list item (glossary entries)
|
||||
"qs": "line",
|
||||
"b": "break",
|
||||
"d": "title",
|
||||
"s": "heading",
|
||||
"ms": "heading",
|
||||
"is": "heading",
|
||||
"sp": "speaker",
|
||||
"li": "item",
|
||||
"ili": "item",
|
||||
"mt": "booktitle",
|
||||
"mt2": "booktitle",
|
||||
"mt3": "booktitle",
|
||||
}
|
||||
|
||||
# Nesting depth for poetry lines and indented paragraphs.
|
||||
INDENT = {"q": 1, "q2": 2, "q3": 3, "qs": 3, "pi": 1, "mi": 1, "li": 1, "ili": 1}
|
||||
|
||||
# Heading / title rank.
|
||||
LEVEL = {"mt": 1, "mt2": 2, "mt3": 3, "ms": 1, "s": 2, "is": 2}
|
||||
|
||||
# Character-style spans -> inline node type. ``None`` means unwrap the span and
|
||||
# keep its children inline (``ft`` is just "footnote body text").
|
||||
INLINE = {
|
||||
"wj": "wj", # words of Jesus
|
||||
"k": "keyword", # glossary keyword
|
||||
"bk": "bookref", # book name cited in a note
|
||||
"fq": "fq", # the quoted words the footnote is about
|
||||
"fqa": "fqa", # an alternative rendering
|
||||
"fl": "fl", # footnote label
|
||||
"wj": "wj",
|
||||
"k": "keyword",
|
||||
"bk": "bookref",
|
||||
"fq": "fq",
|
||||
"fqa": "fqa",
|
||||
"fl": "fl",
|
||||
"fine": "fine",
|
||||
"xt": "xt", # cross-reference target
|
||||
"xt": "xt",
|
||||
"ft": None,
|
||||
}
|
||||
|
||||
# Divs whose entire subtree is navigation or licensing, not content.
|
||||
DROP_CLASSES = {"copyright", "tnav", "vnav", "popup", "toc", "toc1", "toc2"}
|
||||
|
||||
|
||||
def parse_href(href):
|
||||
"""Turn an intra-corpus link into a structured reference, or None."""
|
||||
m = HREF_RE.match(href or "")
|
||||
if not m or not any(m.groups()):
|
||||
return None
|
||||
@@ -126,18 +70,8 @@ def parse_href(href):
|
||||
ref["verse"] = int(verse)
|
||||
return ref or None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Whitespace
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def normalize(nodes):
|
||||
"""Collapse whitespace across the whole inline tree, in document order.
|
||||
|
||||
Runs of whitespace become a single space and spaces are not duplicated
|
||||
across element boundaries, so "word</span> <span>next" keeps exactly one.
|
||||
"""
|
||||
state = {"prev_space": True} # True at the start suppresses a leading space
|
||||
state = {"prev_space": True}
|
||||
|
||||
def walk(lst):
|
||||
out = []
|
||||
@@ -162,15 +96,7 @@ def normalize(nodes):
|
||||
rstrip_last(result)
|
||||
return result
|
||||
|
||||
|
||||
def rstrip_last(nodes):
|
||||
"""Drop the trailing space on a tree's final text node.
|
||||
|
||||
Only a genuinely trailing node is stripped. A space before a block's
|
||||
closing verse or note marker separates it from the preceding word and
|
||||
has to survive, so the walk stops at the last node rather than looking
|
||||
past markers for something to strip.
|
||||
"""
|
||||
if not nodes:
|
||||
return False
|
||||
node = nodes[-1]
|
||||
@@ -184,9 +110,7 @@ def rstrip_last(nodes):
|
||||
return rstrip_last(node["c"])
|
||||
return False
|
||||
|
||||
|
||||
def plain_text(nodes, skip=("noteref",)):
|
||||
"""Flatten an inline tree to readable text, dropping note markers."""
|
||||
parts = []
|
||||
for node in nodes:
|
||||
if node["t"] in skip:
|
||||
@@ -199,34 +123,20 @@ def plain_text(nodes, skip=("noteref",)):
|
||||
parts.append(plain_text(node["c"], skip))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Chapter parser
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
class ChapterParser(HTMLParser):
|
||||
"""Parse one chapter file into blocks + notes.
|
||||
|
||||
Content is appended to ``self.sink``, the inline-node list currently being
|
||||
filled. A stack of frames records what to restore (and what to finish) as
|
||||
each element closes; ``sink is None`` discards text, which is how
|
||||
navigation and the copyright footer are dropped.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(convert_charrefs=True)
|
||||
self.blocks = []
|
||||
self.notes = []
|
||||
self.titles = [] # mt/mt2/mt3, hoisted to the book
|
||||
self.titles = []
|
||||
self.chapter_label = None
|
||||
self.is_toc = False # a chapter-0 navigation page
|
||||
self.is_toc = False
|
||||
|
||||
self.in_main = False
|
||||
self.stack = []
|
||||
self.sink = None
|
||||
|
||||
# -- frame helpers ----------------------------------------------------
|
||||
|
||||
def _push(self, tag, sink, on_close=None, **extra):
|
||||
frame = {"tag": tag, "prev": self.sink, "close": on_close}
|
||||
frame.update(extra)
|
||||
@@ -238,8 +148,6 @@ class ChapterParser(HTMLParser):
|
||||
if self.sink is not None:
|
||||
self.sink.append(node)
|
||||
|
||||
# -- HTMLParser hooks -------------------------------------------------
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
attrs = dict(attrs)
|
||||
classes = (attrs.get("class") or "").split()
|
||||
@@ -251,7 +159,6 @@ class ChapterParser(HTMLParser):
|
||||
return
|
||||
|
||||
if not self.in_main:
|
||||
# Everything before <div class="main"> is <head> and top nav.
|
||||
if tag == "div" and "main" in classes:
|
||||
self.in_main = True
|
||||
self._push(tag, None)
|
||||
@@ -295,14 +202,11 @@ class ChapterParser(HTMLParser):
|
||||
return
|
||||
|
||||
if tag == "span" and cls == "notemark":
|
||||
# The note's own marker inside the footnote list.
|
||||
buf = []
|
||||
self._push(tag, buf, self._close_note_marker, nodes=buf)
|
||||
return
|
||||
|
||||
if tag == "a" and cls == "notemark":
|
||||
# An in-text marker linking down to the note; its nested
|
||||
# <span class="popup"> duplicates the note body and is dropped.
|
||||
node = {"t": "noteref", "id": (attrs.get("href") or "").lstrip("#")}
|
||||
self._emit(node)
|
||||
buf = []
|
||||
@@ -317,7 +221,7 @@ class ChapterParser(HTMLParser):
|
||||
if tag == "span" and cls in INLINE:
|
||||
kind = INLINE[cls]
|
||||
if kind is None:
|
||||
self._push(tag, self.sink) # unwrap
|
||||
self._push(tag, self.sink)
|
||||
else:
|
||||
node = {"t": kind, "c": []}
|
||||
self._emit(node)
|
||||
@@ -335,7 +239,6 @@ class ChapterParser(HTMLParser):
|
||||
self._push(tag, self.sink)
|
||||
return
|
||||
|
||||
# Anything else (nested spans, stray markup) is transparent.
|
||||
self._push(tag, self.sink)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
@@ -345,21 +248,19 @@ class ChapterParser(HTMLParser):
|
||||
if self.stack[i]["tag"] == tag:
|
||||
break
|
||||
else:
|
||||
return # unmatched close tag; ignore
|
||||
return
|
||||
while len(self.stack) > i:
|
||||
frame = self.stack.pop()
|
||||
self.sink = frame["prev"]
|
||||
if frame["close"]:
|
||||
frame["close"](frame)
|
||||
if tag == "div" and not self.stack:
|
||||
self.in_main = False # <div class="main"> closed
|
||||
self.in_main = False
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.sink is not None and data:
|
||||
self.sink.append({"t": "text", "v": data})
|
||||
|
||||
# -- frame close handlers ---------------------------------------------
|
||||
|
||||
def _close_chapter_label(self, frame):
|
||||
self.chapter_label = plain_text(frame["nodes"]).strip()
|
||||
|
||||
@@ -417,20 +318,13 @@ class ChapterParser(HTMLParser):
|
||||
f["note"]["verse"] = int(m.group(1))
|
||||
return
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Corpus
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
class IndexParser(HTMLParser):
|
||||
"""Read book order, display names and canon section from index.htm."""
|
||||
|
||||
# Link class on the site index marks which part of the canon a book is in.
|
||||
SECTIONS = {"oo": "ot", "nn": "nt", "aa": "deuterocanon", "xx": "other"}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(convert_charrefs=True)
|
||||
self.books = [] # [{code, name, section, order}]
|
||||
self.books = []
|
||||
self.title = None
|
||||
self._link = None
|
||||
self._in_title = False
|
||||
@@ -464,7 +358,6 @@ class IndexParser(HTMLParser):
|
||||
elif self._link is not None:
|
||||
self._link["name"] += data
|
||||
|
||||
|
||||
def parse_chapter(path):
|
||||
parser = ChapterParser()
|
||||
with open(path, encoding="utf-8-sig") as fh:
|
||||
@@ -472,7 +365,6 @@ def parse_chapter(path):
|
||||
parser.close()
|
||||
return parser
|
||||
|
||||
|
||||
def extract(source_dir):
|
||||
index_path = os.path.join(source_dir, "index.htm")
|
||||
index = IndexParser()
|
||||
@@ -481,7 +373,6 @@ def extract(source_dir):
|
||||
|
||||
meta = {b["code"]: b for b in index.books}
|
||||
|
||||
# Group chapter files by book code.
|
||||
by_book = {}
|
||||
for name in sorted(os.listdir(source_dir)):
|
||||
m = CHAPTER_RE.match(name)
|
||||
@@ -503,7 +394,6 @@ def extract(source_dir):
|
||||
for number, name in sorted(by_book[code]):
|
||||
parsed = parse_chapter(os.path.join(source_dir, name))
|
||||
if parsed.is_toc or not parsed.blocks:
|
||||
# Chapter-0 files are per-book tables of contents, not text.
|
||||
skipped.append(name)
|
||||
continue
|
||||
if parsed.titles and not book["titles"]:
|
||||
@@ -528,26 +418,17 @@ def extract(source_dir):
|
||||
"books": books,
|
||||
}, skipped
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# WRITING -- that tree into one HTML fragment per book
|
||||
# ==========================================================================
|
||||
|
||||
# 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
|
||||
"wj": ("span", "wj"),
|
||||
"keyword": ("dfn", "k"),
|
||||
"bookref": ("cite", "bk"),
|
||||
"fq": ("span", "fq"),
|
||||
"fqa": ("span", "fqa"),
|
||||
"fl": ("span", "fl"),
|
||||
"fine": ("span", "fine"),
|
||||
"xt": ("span", "xt"), # cross-reference target
|
||||
"xt": ("span", "xt"),
|
||||
}
|
||||
|
||||
# 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",
|
||||
@@ -557,19 +438,13 @@ BLOCK_TAGS = {
|
||||
"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")
|
||||
@@ -578,26 +453,12 @@ def ref_href(node, book, chapter):
|
||||
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.
|
||||
#
|
||||
# A control, said here rather than by a pass over the page once it
|
||||
# has landed. role is what announces it and what earns Reboot's
|
||||
# [role=button] pointer cursor, tabindex is what puts it in the tab
|
||||
# order, and the label is what a screen reader says in place of a
|
||||
# bare number. All three are true of every verse in the corpus and
|
||||
# none of them depends on anything only the browser knows, so
|
||||
# there is nothing here for a script to work out: app.js used to
|
||||
# set them on load, after every htmx swap and after every history
|
||||
# restore, walking a whole book each time. The click itself never
|
||||
# needed that pass -- it is delegated at #content in base.njk.
|
||||
label = esc(node["label"])
|
||||
out.append('<span class="v" id="%s" role="button" tabindex="0" '
|
||||
'aria-label="Verse %s, options">%s</span> '
|
||||
@@ -618,10 +479,9 @@ def render_inline(nodes, book, chapter, out):
|
||||
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
|
||||
else:
|
||||
render_inline(node.get("c", []), book, chapter, out)
|
||||
|
||||
|
||||
def render_block(block, book, chapter, out):
|
||||
btype = block["type"]
|
||||
tag = BLOCK_TAGS.get(btype, "p")
|
||||
@@ -635,23 +495,13 @@ def render_block(block, book, chapter, out):
|
||||
render_inline(block.get("content", []), book, chapter, out)
|
||||
out.append("</%s>\n" % tag)
|
||||
|
||||
|
||||
# Footnote callers arrive from the source as * † ‡ § ✡. The star is the only
|
||||
# one of them a browser may decide is an emoji: no serif font carries a glyph
|
||||
# for U+2721, so the fallback chain reaches the colour emoji font and drops a
|
||||
# purple tile into the middle of a verse. U+FE0E is the standardised way to ask
|
||||
# for the text glyph of the same character -- the symbol, not the emoji. There
|
||||
# is no separate text codepoint to switch to; this is the only lever.
|
||||
TEXT_PRESENTATION = "\ufe0e"
|
||||
EMOJI_CAPABLE = "\u2721"
|
||||
|
||||
|
||||
def caller(marker):
|
||||
"""A footnote marker, asked to render as text rather than as an emoji."""
|
||||
return "".join(c + TEXT_PRESENTATION if c in EMOJI_CAPABLE else c
|
||||
for c in (marker or "*"))
|
||||
|
||||
|
||||
def render_notes(notes, chapter, book, out):
|
||||
if not notes:
|
||||
return
|
||||
@@ -671,14 +521,13 @@ def render_notes(notes, chapter, book, 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
|
||||
group = None
|
||||
|
||||
def close_group():
|
||||
nonlocal group
|
||||
@@ -691,7 +540,7 @@ def render_chapter(chapter, book, out):
|
||||
for block in chapter["blocks"]:
|
||||
btype = block["type"]
|
||||
if btype == "break":
|
||||
close_group() # a blank line ends the stanza
|
||||
close_group()
|
||||
continue
|
||||
want = {"line": "poetry", "item": "list"}.get(btype)
|
||||
if want != group:
|
||||
@@ -707,41 +556,24 @@ def render_chapter(chapter, book, out):
|
||||
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 whose
|
||||
# data-template names the template that wraps it.
|
||||
out.append("<title>%s</title>\n" % esc(title))
|
||||
out.append('<article class="book" data-template="book" id="%s" data-book="%s" '
|
||||
'data-name="%s" data-section="%s">\n'
|
||||
% (code, code, esc(name), book["section"]))
|
||||
|
||||
# Header: the full multi-line title if the source had one.
|
||||
out.append(' <header class="book-header">\n')
|
||||
out.append(" <h1>")
|
||||
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('<span class="%s">%s</span>'
|
||||
% (part["style"], esc(part["text"]))
|
||||
for part in book["titles"]))
|
||||
else:
|
||||
out.append(esc(name))
|
||||
out.append("</h1>\n")
|
||||
# The title and nothing else. Both of the things that used to sit under it
|
||||
# are in the navbar now, where they are in reach from anywhere in the book
|
||||
# rather than only from the top of it: the chapter list became the
|
||||
# reference picker, which reads its chapters and verses straight off this
|
||||
# markup, and "All books" is what the navbar brand already does. In Psalms
|
||||
# the chapter list alone was a hundred and fifty links, carried in the
|
||||
# fragment and in every device's offline copy.
|
||||
out.append(" </header>\n")
|
||||
|
||||
out.append(' <div class="chapters">\n')
|
||||
@@ -758,7 +590,6 @@ def render_book(book, translation, out_dir):
|
||||
fh.write("".join(out))
|
||||
return path
|
||||
|
||||
|
||||
SECTION_LABELS = [
|
||||
("ot", "Old Testament"),
|
||||
("nt", "New Testament"),
|
||||
@@ -766,7 +597,6 @@ SECTION_LABELS = [
|
||||
("other", "Other"),
|
||||
]
|
||||
|
||||
|
||||
def render_index(books, translation, out_dir):
|
||||
out = ["<title>%s</title>\n" % esc(translation["name"]),
|
||||
'<div class="index" data-template="index">\n']
|
||||
@@ -789,48 +619,9 @@ def render_index(books, translation, out_dir):
|
||||
fh.write("".join(out))
|
||||
return path
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# WRITING -- the reference map
|
||||
# ==========================================================================
|
||||
|
||||
# bible/verses.json: every book's chapters, and every chapter's verse labels.
|
||||
#
|
||||
# The navbar's reference picker is built from this, by the service worker. It
|
||||
# used to be built in the browser by reading the book already on the page --
|
||||
# the chapters were the sections and a chapter's verses were the anchors inside
|
||||
# it -- which was free but only worked where there was a document to read. A
|
||||
# worker has no DOM, and the picker now lives in a shell the worker composes
|
||||
# before any document exists, so what the picker needs has to be a fact on
|
||||
# disk rather than something recovered from markup.
|
||||
#
|
||||
# Written here, from the same tree the fragments are rendered from, so the two
|
||||
# cannot disagree: every option this file produces names an anchor render_book
|
||||
# wrote, because both come from the same verse nodes.
|
||||
#
|
||||
# The encoding is compact on purpose. A chapter whose verses run 1..n is
|
||||
# written as the number n; only the eight in the corpus with a gap or a range
|
||||
# -- Sirach 26 ends "19-27", 28, 29 -- spell their labels out:
|
||||
#
|
||||
# {"GEN": [31, 25, 24, ...], "SIR": [..., ["1", ..., "19-27", "28", "29"]]}
|
||||
#
|
||||
# That is 6 KB for 38,000 verses against 180 KB of spelled-out labels, which is
|
||||
# the difference between a file the worker can parse every time it wakes and
|
||||
# one it would have to find somewhere to keep.
|
||||
#
|
||||
# Chapter numbers are not in the file at all: they are the array index plus
|
||||
# one. render_verses checks that, rather than assuming it.
|
||||
|
||||
VERSES_FILE = "verses.json"
|
||||
|
||||
|
||||
def chapter_verses(chapter):
|
||||
"""The verse labels of one chapter, in document order.
|
||||
|
||||
Blocks only. Footnotes hang inside the chapter section too, but a note's
|
||||
content never carries a verse node -- and if one ever did, it would be a
|
||||
citation rather than a place in the text.
|
||||
"""
|
||||
labels = []
|
||||
|
||||
def walk(nodes):
|
||||
@@ -844,20 +635,12 @@ def chapter_verses(chapter):
|
||||
walk(block.get("content", []))
|
||||
return labels
|
||||
|
||||
|
||||
def render_verses(books, out_dir):
|
||||
"""Write the reference map. Returns its path and the verses in it.
|
||||
|
||||
See the note above for the encoding.
|
||||
"""
|
||||
out = {}
|
||||
total = 0
|
||||
for book in books:
|
||||
numbers = [c["number"] for c in book["chapters"]]
|
||||
if numbers != list(range(1, len(numbers) + 1)):
|
||||
# The map addresses chapters by position, so a book that skips or
|
||||
# repeats one would silently mislabel every option after the gap.
|
||||
# Better to stop the build than to ship a picker that lies.
|
||||
raise RuntimeError("%s: chapters are not 1..%d (%s)"
|
||||
% (book["code"], len(numbers), numbers))
|
||||
chapters = []
|
||||
@@ -874,11 +657,6 @@ def render_verses(books, out_dir):
|
||||
fh.write("\n")
|
||||
return path, total
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# Entry point
|
||||
# ==========================================================================
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
@@ -912,6 +690,5 @@ def main(argv=None):
|
||||
print("wrote %s/" % args.out.rstrip("/"), file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user