Files
stack/tests/rex/comments/test_epub_text.py
kert 71227e879d feat(pfs): CPT guideline facts from the licensed books (R69), stance-v5 coding event from facts, link relevance by kind, home-and-residence pair phrases (R74)
pfs guideline-facts writes pfs.code_guideline_fact: selection basis, MDM level,
time thresholds, deletions between editions, copy-guarded scope points and the
Feb 2021 Panel action, each cited by (edition, page). stance-v5 states the CPT
change in the coding event and drops keyword-only coding events; v5 ships on
the golden set (recall 14/27, direction 11/14 vs v4 13/27, 10/13).
2026-11-12 17:19:09 -05:00

178 lines
6.8 KiB
Python

"""rex.comments.epub_text — shared EPUB container/spine resolution."""
from __future__ import annotations
import zipfile
from pathlib import Path
from rex.comments.epub_text import content_root, epub_pages, extract_text, spine_paths
_CONTAINER_XML = (
'<?xml version="1.0"?>\n'
'<container version="1.0" '
'xmlns="urn:oasis:names:tc:opendocument:xmlns:container">\n'
"<rootfiles>\n"
'<rootfile full-path="OEBPS/content.opf" '
'media-type="application/oebps-package+xml"/>\n'
"</rootfiles>\n</container>"
)
def _opf(spine_idrefs: list[str]) -> str:
spine = "\n".join(f'<itemref idref="{i}"/>' for i in spine_idrefs)
return (
'<?xml version="1.0"?>\n'
'<package xmlns="http://www.idpf.org/2007/opf" version="3.0">\n'
"<manifest>\n"
'<item id="a" href="a.xhtml" media-type="application/xhtml+xml"/>\n'
'<item id="b" href="b.xhtml" media-type="application/xhtml+xml"/>\n'
"</manifest>\n"
f"<spine>\n{spine}\n</spine>\n"
"</package>"
)
def _build_epub_with_container(path: Path, *, spine_order: list[str]) -> None:
with zipfile.ZipFile(path, "w") as zf:
zf.writestr("mimetype", "application/epub+zip")
zf.writestr("META-INF/container.xml", _CONTAINER_XML)
zf.writestr("OEBPS/content.opf", _opf(spine_order))
zf.writestr("OEBPS/a.xhtml", "<html><body><p>Section A text.</p></body></html>")
zf.writestr("OEBPS/b.xhtml", "<html><body><p>Section B text.</p></body></html>")
def _build_epub_without_container(path: Path) -> None:
with zipfile.ZipFile(path, "w") as zf:
zf.writestr("mimetype", "application/epub+zip")
# Deliberately named so name-sort order differs from any
# plausible spine order — proves the fallback really is name-sort.
zf.writestr("OPS/z_first.xhtml", "<html><body><p>Z content.</p></body></html>")
zf.writestr("OPS/a_second.xhtml", "<html><body><p>A content.</p></body></html>")
class TestContentRoot:
def test_resolves_via_container_xml(self, tmp_path):
path = tmp_path / "book.epub"
_build_epub_with_container(path, spine_order=["a", "b"])
with zipfile.ZipFile(path) as zf:
assert content_root(zf) == "OEBPS/"
def test_falls_back_to_ops_without_container_xml(self, tmp_path):
path = tmp_path / "book.epub"
_build_epub_without_container(path)
with zipfile.ZipFile(path) as zf:
assert content_root(zf) == "OPS/"
class TestSpinePaths:
def test_resolves_spine_order_from_the_opf(self, tmp_path):
path = tmp_path / "book.epub"
_build_epub_with_container(path, spine_order=["b", "a"])
with zipfile.ZipFile(path) as zf:
assert spine_paths(zf) == ["OEBPS/b.xhtml", "OEBPS/a.xhtml"]
def test_none_without_container_xml(self, tmp_path):
path = tmp_path / "book.epub"
_build_epub_without_container(path)
with zipfile.ZipFile(path) as zf:
assert spine_paths(zf) is None
class TestExtractText:
def test_extracts_in_spine_order_not_alphabetical(self, tmp_path):
path = tmp_path / "book.epub"
# Spine deliberately reversed from alphabetical (a, b) name order.
_build_epub_with_container(path, spine_order=["b", "a"])
text = extract_text(path)
assert text.index("Section B text") < text.index("Section A text")
def test_falls_back_to_sorted_names_without_container_xml(self, tmp_path):
path = tmp_path / "book.epub"
_build_epub_without_container(path)
text = extract_text(path)
# a_second.xhtml sorts before z_first.xhtml by name.
assert text.index("A content") < text.index("Z content")
def test_strips_tags_and_unescapes_entities(self, tmp_path):
path = tmp_path / "book.epub"
with zipfile.ZipFile(path, "w") as zf:
zf.writestr("mimetype", "application/epub+zip")
zf.writestr(
"OPS/a.xhtml",
"<html><body><p>CPT&#174; codes &amp; values</p></body></html>",
)
text = extract_text(path)
assert "<p>" not in text
assert "CPT® codes & values" in text
def _paged_epub(path: Path, a: str, b: str, *, container: bool = True) -> None:
with zipfile.ZipFile(path, "w") as zf:
zf.writestr("mimetype", "application/epub+zip")
if container:
zf.writestr("META-INF/container.xml", _CONTAINER_XML)
zf.writestr("OEBPS/content.opf", _opf(["a", "b"]))
zf.writestr("OEBPS/a.xhtml", a)
zf.writestr("OEBPS/b.xhtml", b)
else:
zf.writestr("OPS/a.xhtml", a)
zf.writestr("OPS/b.xhtml", b)
def test_epub_pages_split_on_page_spans(tmp_path):
path = tmp_path / "book.epub"
_paged_epub(
path,
'<html><body><p>Front matter.</p><span id="page_ix"/><p>Nine text.</p>'
'<span id="page_12"></span><p>Twelve one.</p><p>Twelve two.</p></body></html>',
'<html><body><p>Twelve continues.</p><span id="page_13"/>'
'<span id="page_14"/><p>Fourteen.</p></body></html>',
)
assert epub_pages(path) == [
("", "Front matter."),
("ix", "Nine text."),
("12", "Twelve one.\nTwelve two.\n\nTwelve continues."),
("14", "Fourteen."),
]
def test_epub_pages_split_on_page_anchors(tmp_path):
"""The 2022 CPT Professional EPUB marks pages with ``<a id="page_N"/>``."""
path = tmp_path / "book.epub"
_paged_epub(
path,
'<html><body><div><a id="page_5"/><b>Title</b></div><p>Five.</p>'
'<a id="page_6"></a><p>Six.</p><a href="x.xhtml#page_7">7</a></body></html>',
"<html><body><p>Six more.</p></body></html>",
)
assert [label for label, _ in epub_pages(path)] == ["5", "6"]
def test_epub_pages_without_container_sorts_members(tmp_path):
path = tmp_path / "book.epub"
_paged_epub(
path,
'<html><body><span id="page_1"/><p>One.</p></body></html>',
'<html><body><span id="page_2"/><p>Two.</p></body></html>',
container=False,
)
assert epub_pages(path) == [("1", "One."), ("2", "Two.")]
def test_epub_pages_skips_unreadable_spine_member(tmp_path):
path = tmp_path / "book.epub"
with zipfile.ZipFile(path, "w") as zf:
zf.writestr("META-INF/container.xml", _CONTAINER_XML)
zf.writestr("OEBPS/content.opf", _opf(["a", "b"])) # b.xhtml never written
zf.writestr("OEBPS/a.xhtml", "<html><body><p>Only A.</p></body></html>")
assert epub_pages(path) == [("", "Only A.")]
def test_extract_text_skips_unreadable_spine_member(tmp_path):
path = tmp_path / "book.epub"
with zipfile.ZipFile(path, "w") as zf:
zf.writestr("META-INF/container.xml", _CONTAINER_XML)
zf.writestr("OEBPS/content.opf", _opf(["a", "b"]))
zf.writestr("OEBPS/a.xhtml", "<html><body><p>Only A.</p></body></html>")
assert extract_text(path) == "Only A."