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).
178 lines
6.8 KiB
Python
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® codes & 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."
|