diff --git a/CHANGES.md b/CHANGES.md index 0b6fcb17..954bd931 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,10 @@ Fixes: - Fix 2 `TypeError`s raised in `get_citations(markup_text=...)` when `clean_steps` was omitted or lacked `"html"`. The documented fallback that prepends the `html` step was both unreachable and broken. +- Keep the pin cite on an `Id.` citation that refers to a section, like + `Id. § 1985`. The section mark is its own token and used to stop the pin + cite scan, so the pin cite was lost and the mark was reported as a separate + `UnknownCitation`. #299 ## Current diff --git a/eyecite/helpers.py b/eyecite/helpers.py index 84213cb5..3b62f7b2 100644 --- a/eyecite/helpers.py +++ b/eyecite/helpers.py @@ -19,12 +19,14 @@ PlaceholderCitationToken, ReferenceCitation, ResourceCitation, + SectionToken, ShortCaseCitation, StopWordToken, SupraCitation, SupraToken, Token, Tokens, + UnknownCitation, ) from eyecite.regexes import ( POST_FULL_CITATION_REGEX, @@ -1015,7 +1017,9 @@ def match_on_tokens( token = words[index] # check for stop token - if strings_only and not isinstance(token, str): + # A section mark is a marker, not a citation: its text is ordinary + # pin cite vocabulary, so scanning has to see it rather than stop. + if strings_only and not isinstance(token, str | SectionToken): break if isinstance(token, ParagraphToken): break @@ -1109,6 +1113,11 @@ def filter_citations(citations: list[CitationBase]) -> list[CitationBase]: filtered_citations.append(citation) continue + # A section mark the previous citation took as its pin cite + # is that pin cite, not a citation of its own. + if isinstance(citation, UnknownCitation): + continue + # Known overlap case are parallel full citations if not ( isinstance(citation, FullCaseCitation) diff --git a/tests/test_FindTest.py b/tests/test_FindTest.py index 9d03d354..495d6fe9 100644 --- a/tests/test_FindTest.py +++ b/tests/test_FindTest.py @@ -947,6 +947,44 @@ def test_no_duplicate_editions(self): self.assertEqual(len(ambiguous.all_editions), 2) self.assertEqual(len(set(ambiguous.all_editions)), 2) + def test_id_citation_with_section_pin_cite(self): + """Does an Id. cite keep a section pin cite? (#299) + + The section mark is its own token, and the pin cite scan used to stop + at any non-string token, so "Id. § 5" lost the pin cite and left the + mark behind as a citation of its own. + """ + # fmt: off + test_pairs = ( + ('42 U.S.C. § 1983. Id. § 1985.', + [law_citation('42 U.S.C. § 1983', reporter='U.S.C.', + groups={'title': '42', 'section': '1983'}), + id_citation('Id.', metadata={'pin_cite': '§ 1985'})]), + ('Foo v. Bar 1 U.S. 12. Id. §§ 5-7.', + [case_citation(page='12', + metadata={'plaintiff': 'Foo', 'defendant': 'Bar'}), + id_citation('Id.', metadata={'pin_cite': '§§ 5-7'})]), + # The mark is glued to the number, so it is one token. + ('Foo v. Bar 1 U.S. 12. Id. §5.', + [case_citation(page='12', + metadata={'plaintiff': 'Foo', 'defendant': 'Bar'}), + id_citation('Id.', metadata={'pin_cite': '§5'})]), + ('Foo v. Bar 1 U.S. 12. Id. § 5, 7.', + [case_citation(page='12', + metadata={'plaintiff': 'Foo', 'defendant': 'Bar'}), + id_citation('Id.', metadata={'pin_cite': '§ 5, 7'})]), + # A page pin cite still works. + ('Foo v. Bar 1 U.S. 12. Id. at 5.', + [case_citation(page='12', + metadata={'plaintiff': 'Foo', 'defendant': 'Bar'}), + id_citation('Id.', metadata={'pin_cite': 'at 5'})]), + # A mark that no citation absorbs is still reported on its own. + ('lorem ipsum see § 99 of the U.S. code.', + [unknown_citation('§')]), + ) + # fmt: on + self.run_test_pairs(test_pairs, "Id. citation with a section pin cite") + def test_find_law_citations(self): """Can we find citations from laws.json?""" # fmt: off