closed
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()
def _close_block(self, frame):
block = frame["block"]
block["content"] = normalize(block["content"])
if block["type"] == "booktitle":
if block["content"]:
self.titles.append({
"style": block["style"],
"level": block.get("level"),
"text": plain_text(block["content"]),
})
return
if block["type"] == "break":
block.pop("content")
self.blocks.append(block)
return
if block["content"]:
self.blocks.append(block)
def _close_note(self, frame):
note = frame["note"]
note["content"] = normalize(note["content"])
note["text"] = plain_text(note["content"])
self.notes.append(note)
def _close_verse(self, frame):
label = plain_text(frame["nodes"]).replace(" ", " ").strip()
node = {"t": "verse", "label": label}
m = re.match(r"^(\d+)(?:[-–](\d+))?$", label)
if m:
node["n"] = int(m.group(1))
if m.group(2):
node["end"] = int(m.group(2))
self._emit(node)
def _close_note_marker(self, frame):
marker = plain_text(frame["nodes"]).strip()
for f in reversed(self.stack):
if "note" in f:
f["note"]["marker"] = marker
return
def _close_noteref(self, frame):
frame["node"]["marker"] = plain_text(frame["nodes"]).strip()
def _close_backref(self, frame):
text = plain_text(frame["nodes"]).strip()
for f in reversed(self.stack):
if "note" in f:
f["note"]["ref"] = text
m = re.search(r":(\d+)", text)
if m:
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.title = None
self._link = None
self._in_title = False
def handle_starttag(self, tag, attrs):
attrs = dict(attrs)
if tag == "title":
self._in_title = True
elif tag == "a":
m = CHAPTER_RE.match(os.path.basename(attrs.get("href") or ""))
section = self.SECTIONS.get((attrs.get("class") or "").strip())
if m and section:
self._link = {
"code": m.group(1),
"section": section,
"order": len(self.books) + 1,
"name": "",
}
def handle_endtag(self, tag):
if tag == "title":
self._in_title = False
elif tag == "a" and self._link:
self._link["name"] = self._link["name"].strip()
self.books.append(self._link)
self._link = None
def handle_data(self, data):
if self._in_title and self.title is None:
self.title = data.strip()
elif self._link is not None:
self._link["name"] += data
def verse_index(blocks):
"""Flat {verse label: text} map, for search and direct verse lookup.
Verse markers are inline, so a verse's text runs from its marker to the
next one and may span several blocks (a poetry couplet, for instance).
"""
verses = {}
current = None
parts = []
def flush():
if current is not None:
text = re.sub(r"\s+", " ", "".join(parts)).strip()
if text:
verses[current] = (verses.get(current, "") + " " + text).strip()
def walk(nodes):
nonlocal current
for node in nodes:
if node["t"] == "verse":
flush()
parts.clear()
current = node["label"]
elif node["t"] == "noteref":
continue
elif node["t"] == "text":
parts.append(node["v"])
elif node["t"] == "br":
parts.append(" ")
elif "c" in node:
walk(node["c"])
for block in blocks:
content = block.get("content")
if not content:
continue
walk(content)
parts.append(" ") # blocks do not run words together
flush()
return verses
def parse_chapter(path):
parser = ChapterParser()
with open(path, encoding="utf-8-sig") as fh:
parser.feed(fh.read())
parser.close()
return parser
def extract(source_dir, with_verse_text=True):
index_path = os.path.join(source_dir, "index.htm")
index = IndexParser()
with open(index_path, encoding="utf-8-sig") as fh:
index.feed(fh.read())
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)
if m:
by_book.setdefault(m.group(1), []).append((int(m.group(2)), name))
books = []
skipped = []
for code in sorted(by_book, key=lambda c: (meta.get(c, {}).get("order", 999), c)):
info = meta.get(code, {"code": code, "name": code, "section": "other",
"order": 999})
book = {
"code": code,
"name": info["name"],
"section": info["section"],
"titles": [],
"chapters": [],
}
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"]:
book["titles"] = parsed.titles
label = parsed.chapter_label
chapter = {
"number": int(label) if label and label.isdigit() else number,
"blocks": parsed.blocks,
"notes": parsed.notes,
}
if with_verse_text:
chapter["verses"] = verse_index(parsed.blocks)
book["chapters"].append(chapter)
if book["chapters"]:
books.append(book)
return {
"translation": {
"id": "engwebu",
"name": index.title or "World English Bible Updated",
"license": "Public Domain",
"source": "eBible.org",
},
"books": books,
}, skipped
def write_split(data, out_dir):
"""One JSON per chapter plus an index, for chapter-at-a-time fetching."""
os.makedirs(out_dir, exist_ok=True)
index = {"translation": data["translation"], "books": []}
for book in data["books"]:
book_dir = os.path.join(out_dir, book["code"])
os.makedirs(book_dir, exist_ok=True)
for chapter in book["chapters"]:
payload = dict(chapter)
payload["book"] = book["code"]
path = os.path.join(book_dir, "%d.json" % chapter["number"])
with open(path, "w", encoding="utf-8") as fh:
json.dump(payload, fh, ensure_ascii=False, separators=(",", ":"))
index["books"].append({
"code": book["code"],
"name": book["name"],
"section": book["section"],
"titles": book["titles"],
"chapters": [c["number"] for c in book["chapters"]],
})
with open(os.path.join(out_dir, "index.json"), "w", encoding="utf-8") as fh:
json.dump(index, fh, ensure_ascii=False, indent=2)
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("source", nargs="?", default="source",
help="directory of eBible .htm files (default: source)")
ap.add_argument("-o", "--output", default="bible.json",
help="output JSON file (default: bible.json)")
ap.add_argument("--split", metavar="DIR",
help="also write one JSON per chapter into DIR")
ap.add_argument("--no-verse-text", action="store_true",
help="omit the flat per-chapter verse text map")
ap.add_argument("--pretty", action="store_true",
help="indent the output JSON")
args = ap.parse_args(argv)
data, skipped = extract(args.source, with_verse_text=not args.no_verse_text)
dump = dict(indent=2) if args.pretty else dict(separators=(",", ":"))
with open(args.output, "w", encoding="utf-8") as fh:
json.dump(data, fh, ensure_ascii=False, **dump)
if args.split:
write_split(data, args.split)
chapters = sum(len(b["chapters"]) for b in data["books"])
notes = sum(len(c["notes"]) for b in data["books"] for c in b["chapters"])
blocks = sum(len(c["blocks"]) for b in data["books"] for c in b["chapters"])
size = os.path.getsize(args.output) / 1e6
print("books %d" % len(data["books"]), file=sys.stderr)
print("chapters %d" % chapters, file=sys.stderr)
print("blocks %d" % blocks, file=sys.stderr)
print("notes %d" % notes, file=sys.stderr)
print("skipped %d (contents pages)" % len(skipped), file=sys.stderr)
print("wrote %s (%.1f MB)" % (args.output, size), file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())