feat(bib): :pincite: FR locators (fr_para/fr_anchor) resolve to jump URLs (closes #636)
This commit is contained in:
@@ -67,6 +67,22 @@ class Pincite(BaseModel):
|
|||||||
locator_type: str = ""
|
locator_type: str = ""
|
||||||
note: str = ""
|
note: str = ""
|
||||||
|
|
||||||
|
def jump_url(self, store: Store) -> str:
|
||||||
|
"""Deep federalregister.gov link for FR-typed locators (#636).
|
||||||
|
|
||||||
|
Returns ``""`` for non-FR locators and whenever resolution
|
||||||
|
fails (no anchor map grabbed, page out of range, …) — graph
|
||||||
|
and docs rendering must degrade to no-URL, never raise.
|
||||||
|
"""
|
||||||
|
if self.locator_type not in ("fr", "fr_para", "fr_anchor"):
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
from bib import frlink
|
||||||
|
|
||||||
|
return frlink.resolve(self.locator, store=store, item_key=self.item_key).url
|
||||||
|
except Exception: # noqa: BLE001 — degrade to no-URL by contract
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CitationNode:
|
class CitationNode:
|
||||||
@@ -87,6 +103,7 @@ class CitationEdge:
|
|||||||
locator: str = ""
|
locator: str = ""
|
||||||
locator_type: str = ""
|
locator_type: str = ""
|
||||||
note: str = ""
|
note: str = ""
|
||||||
|
jump_url: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -153,11 +170,20 @@ def _classify_locator(locator: str) -> str:
|
|||||||
if not locator:
|
if not locator:
|
||||||
return ""
|
return ""
|
||||||
loc = locator.strip()
|
loc = locator.strip()
|
||||||
|
if re.match(r"^p-\d+$", loc):
|
||||||
|
# A raw federalregister.gov paragraph anchor (#636) — checked
|
||||||
|
# before "p." so it never reads as a page locator.
|
||||||
|
return "fr_anchor"
|
||||||
if loc.lower().startswith(("p.", "pp.")):
|
if loc.lower().startswith(("p.", "pp.")):
|
||||||
return "page"
|
return "page"
|
||||||
if "CFR" in loc or "cfr" in loc:
|
if "CFR" in loc or "cfr" in loc:
|
||||||
return "cfr"
|
return "cfr"
|
||||||
if re.match(r"\d+\s+FR\s+\d+", loc, re.IGNORECASE):
|
m = re.match(r"\d+\s+FR\s+\d+", loc, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
# "91 FR 44218 ¶3" (or "para 3"/"p.3") pins a paragraph on the
|
||||||
|
# cited page; a bare cite stays the backward-compatible "fr".
|
||||||
|
if re.search(r"(?:¶|\bpara\.?\s*|\bp\.\s*)\d+", loc[m.end() :]):
|
||||||
|
return "fr_para"
|
||||||
return "fr"
|
return "fr"
|
||||||
if loc.lower().startswith(("ch.", "ch ")):
|
if loc.lower().startswith(("ch.", "ch ")):
|
||||||
return "chapter"
|
return "chapter"
|
||||||
@@ -435,6 +461,7 @@ def build_citation_graph(store: Store) -> CitationGraph:
|
|||||||
locator=p.locator,
|
locator=p.locator,
|
||||||
locator_type=p.locator_type,
|
locator_type=p.locator_type,
|
||||||
note=p.note,
|
note=p.note,
|
||||||
|
jump_url=p.jump_url(store),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -495,3 +495,90 @@ class TestUpsertPincitesTagFailure:
|
|||||||
[Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.1")],
|
[Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.1")],
|
||||||
)
|
)
|
||||||
assert count == 1
|
assert count == 1
|
||||||
|
|
||||||
|
|
||||||
|
# ── FR locators + jump_url (#636) ───────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestFrLocators:
|
||||||
|
def test_plain_fr_cite_keeps_fr_type(self) -> None:
|
||||||
|
assert _classify_locator("91 FR 44218") == "fr"
|
||||||
|
|
||||||
|
def test_fr_para_locator(self) -> None:
|
||||||
|
assert _classify_locator("91 FR 44218 ¶3") == "fr_para"
|
||||||
|
assert _classify_locator("91 FR 44218, para 3") == "fr_para"
|
||||||
|
assert _classify_locator("91 FR 44218 p.3") == "fr_para"
|
||||||
|
|
||||||
|
def test_fr_anchor_locator(self) -> None:
|
||||||
|
assert _classify_locator("p-3600") == "fr_anchor"
|
||||||
|
|
||||||
|
def test_existing_types_unchanged(self) -> None:
|
||||||
|
assert _classify_locator("p.14") == "page"
|
||||||
|
assert _classify_locator("§3.2") == "section"
|
||||||
|
assert _classify_locator("42 CFR 425.400") == "cfr"
|
||||||
|
|
||||||
|
|
||||||
|
class TestJumpUrl:
|
||||||
|
def _frlink_store(self):
|
||||||
|
from bib import frlink
|
||||||
|
from tests.bib.test_frlink import DOC_META, FIXTURE_HTML, _store_with_rule
|
||||||
|
|
||||||
|
s, key = _store_with_rule()
|
||||||
|
frlink.grab(s, key, fetch=lambda _doc: (DOC_META, FIXTURE_HTML))
|
||||||
|
return s, key
|
||||||
|
|
||||||
|
def test_jump_url_resolves_fr_para(self) -> None:
|
||||||
|
s, key = self._frlink_store()
|
||||||
|
p = Pincite(
|
||||||
|
fn_path="mod.fn",
|
||||||
|
item_key=key,
|
||||||
|
locator="91 FR 100 ¶2",
|
||||||
|
locator_type="fr_para",
|
||||||
|
)
|
||||||
|
assert p.jump_url(s) == "https://example.test/doc#p-2"
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
def test_jump_url_resolves_plain_fr(self) -> None:
|
||||||
|
s, key = self._frlink_store()
|
||||||
|
p = Pincite(
|
||||||
|
fn_path="mod.fn", item_key=key, locator="91 FR 100", locator_type="fr"
|
||||||
|
)
|
||||||
|
assert p.jump_url(s) == "https://example.test/doc#page-100"
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
def test_jump_url_empty_when_unresolvable(self) -> None:
|
||||||
|
s, key = self._frlink_store()
|
||||||
|
# non-FR locator → no URL; missing anchor map → no URL, no raise
|
||||||
|
p = Pincite(
|
||||||
|
fn_path="mod.fn", item_key=key, locator="§3.2", locator_type="section"
|
||||||
|
)
|
||||||
|
assert p.jump_url(s) == ""
|
||||||
|
p2 = Pincite(
|
||||||
|
fn_path="mod.fn", item_key=key, locator="91 FR 999", locator_type="fr"
|
||||||
|
)
|
||||||
|
assert p2.jump_url(s) == ""
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
|
class TestGraphJumpUrl:
|
||||||
|
def test_edges_carry_jump_url(self) -> None:
|
||||||
|
from bib import frlink
|
||||||
|
from tests.bib.test_frlink import DOC_META, FIXTURE_HTML, _store_with_rule
|
||||||
|
|
||||||
|
s, key = _store_with_rule()
|
||||||
|
frlink.grab(s, key, fetch=lambda _doc: (DOC_META, FIXTURE_HTML))
|
||||||
|
upsert_pincites(
|
||||||
|
s,
|
||||||
|
[
|
||||||
|
Pincite(
|
||||||
|
fn_path="mod.fn",
|
||||||
|
item_key=key,
|
||||||
|
locator="91 FR 100 ¶2",
|
||||||
|
locator_type="fr_para",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
g = build_citation_graph(s)
|
||||||
|
edge = next(e for e in g.edges if e.source == "mod.fn")
|
||||||
|
assert edge.jump_url == "https://example.test/doc#p-2"
|
||||||
|
s.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user