fix: livros numerados em citações e remoção completa de citação não sustentada

Dois defeitos na validação de citações, ambos visíveis ao usuário:

1. _WORD_RE excluía dígitos, então o número do livro era descartado. Afeta os 18
   livros numerados (1/2/3 Samuel, Reis, Crônicas, Coríntios, Tessalonicenses,
   Timóteo, Pedro, João):
   - extract_reference('1 João 4:8') devolvia ('João',4,8) — perguntar por
     1 João 4:8 devolvia João 4:8 em silêncio pela busca determinística
   - '2 Reis 5:14' e '1 Coríntios 13:4' devolviam None — caiam na busca por
     similaridade em vez da exata
   - validate_citations removia citações CORRETAS: '(1 João 4:8)' sustentada
     pelas fontes era apagada e sobrava '(1 João )' na resposta
   O regex agora casa o número junto ('1 João', e '1Sm' aglutinado via \s*).

2. A remoção de citação não sustentada usava o span só do 'Capítulo:Versículo',
   deixando o nome do livro pendurado: '(Gênesis 1:1)' virava '(Gênesis )'.
   O span agora cobre do início do nome do livro ao fim dos versículos, e
   parênteses que ficam vazios são limpos.
This commit is contained in:
omp committed 2026-11-02 13:14:18 -03:00
1 parent 816e6be159
commit 0e498e2826
2 files changed
+125 -8

No files matched your search

+18 -8
View File
@@ -42,8 +42,11 @@ _CHAPTER_VERSE_RE = re.compile(
r"(?<![\w:])(\d{1,3})\s*:\s*(\d{1,3})(?:\s*[-–]\s*(\d{1,3}))?(?![\w:])"
)
# Palavra que pode compor o nome de um livro.
_WORD_RE = re.compile(r"[^\W\d_]+", re.UNICODE)
# Palavra que pode compor o nome de um livro, com o número que o precede.
# O número é parte do nome: sem ele, "1 João 4:8" seria lido como "João 4:8" e a
# busca determinística devolveria outro livro. `\s*` (e não `\s+`) cobre também a
# forma aglutinada "1Sm".
_WORD_RE = re.compile(r"(?:[123]\s*)?[^\W\d_]+", re.UNICODE)
# Nomes de livros com até 4 palavras ("Cântico dos Cânticos").
_MAX_BOOK_WORDS = 4
@@ -122,8 +125,14 @@ def extract_reference(text: str) -> tuple[str, int, int] | None:
return None
def _iter_references(text: str) -> Iterable[tuple[re.Match, str, str]]:
"""Percorre as referências do texto, devolvendo livro canônico e faixa."""
def _iter_references(text: str) -> Iterable[tuple[tuple[int, int], str, str]]:
"""Percorre as referências do texto.
Devolve `(span, livro canônico, referência)`, onde `span` cobre **do início do
nome do livro ao fim do capítulo:versículo**. Repassar só o trecho numérico
faria a remoção deixar o nome do livro pendurado — "(Gênesis 1:1)" viraria
"(Gênesis )" na resposta exibida ao usuário.
"""
for canonical, start, match in _iter_reference_matches(text):
chapter = int(match.group(1))
verse = int(match.group(2))
@@ -131,7 +140,7 @@ def _iter_references(text: str) -> Iterable[tuple[re.Match, str, str]]:
reference = f"{canonical} {chapter}:{verse}"
if end_verse:
reference = f"{reference}-{end_verse}"
yield match, canonical, reference
yield (start, match.end()), canonical, reference
def validate_citations(
@@ -148,18 +157,19 @@ def validate_citations(
removals: list[tuple[int, int]] = []
citations: list[str] = []
for match, canonical, reference in _iter_references(text):
for span, canonical, reference in _iter_references(text):
base = reference.split("-")[0]
if reference in allowed or base in allowed:
citations.append(reference)
else:
removals.append(match.span())
removals.append(span)
cleaned = text
for start, end in reversed(removals):
cleaned = cleaned[:start] + cleaned[end:]
# Espaços duplos deixados pela remoção são normalizados.
# Resíduos da remoção: parênteses que ficaram vazios e espaços duplos.
cleaned = re.sub(r"\(\s*\)", "", cleaned)
cleaned = re.sub(r"[ \t]{2,}", " ", cleaned)
cleaned = re.sub(r"\s+([.,;:!?])", r"\1", cleaned)