Skip to content

fix: remove espaço extra ao redor de parênteses e ; nas citações do corpo - #1327

Open
Rossi-Luciano wants to merge 2 commits into
scieloorg:masterfrom
Rossi-Luciano:fix/pdf-citation-spacing
Open

fix: remove espaço extra ao redor de parênteses e ; nas citações do corpo#1327
Rossi-Luciano wants to merge 2 commits into
scieloorg:masterfrom
Rossi-Luciano:fix/pdf-citation-spacing

Conversation

@Rossi-Luciano

@Rossi-Luciano Rossi-Luciano commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

O que esse PR faz?

Corrige o espaçamento das citações no corpo do texto do pdf_generator: citações entre parênteses ganhavam espaço extra logo após (, antes de ) e antes de cada ; que separa referências agrupadas — ex.: ( Lang and Barling, 2012 ; Ripple et al., 2019 ) em vez de (Lang and Barling, 2012; Ripple et al., 2019).

Causa raiz: extract_body_data extraía o texto de cada <p> via para.xpath('.//text()...') seguido de ' '.join(texts).split(), que insere um espaço entre todo par de fragmentos de texto, independente de ter espaço na fonte XML. Um <xref> cercado de parênteses vira dois fragmentos de texto adjacentes sem espaço na fonte, mas ganhava um artificialmente.

A função get_text_from_node (em xml_utils.py) já fazia extração correta preservando adjacência real via .tail, com tratamento próprio para <xref>, mas só era usada como fallback de exceção. Passei a usá-la como caminho principal, adicionando um parâmetro skip_tags para manter a exclusão de <fig>/<table-wrap> do texto do parágrafo (preservando só o .tail que vem depois deles). Também generalizei _remove_double_spaces para colapsar qualquer sequência de espaço em branco (não só espaço duplo literal), porque o .tail de uma <fig>/<table-wrap> pulada pode carregar a indentação de quebra de linha do XML fonte.

Onde a revisão poderia começar?

packtools/sps/formats/pdf/utils/xml_utils.pyget_text_from_node (novo parâmetro skip_tags) e _remove_double_spaces (generalizada). Depois packtools/sps/formats/pdf/pipeline/xml.pyextract_body_data, que passou a chamar get_text_from_node(para, skip_tags={'fig', 'table-wrap'}) em vez do xpath antigo.

Como este poderia ser testado manualmente?

python -m packtools.sps.formats.pdf_generator \
  -i <artigo-com-citacoes-agrupadas>.xml \
  -l layout.docx \
  -o saida.pdf \
  --libreoffice-binary libreoffice

Conferir qualquer citação entre parênteses no corpo do texto.

Algum cenário de contexto que queira dar?

Achado revisando visualmente o corpus de teste de 26 artigos reais (layout_examples_rafael/corpus/). Validado contra o corpus inteiro (1765 parágrafos extraídos): sobraram 15 casos com parênteses/espaço "suspeitos", todos explicáveis — 6 são espaço que existe de verdade na fonte XML (a16.xml: "( <xref>Figure</xref> )", espaço literal no XML original) e 9 são fórmulas matemáticas achatadas em texto sem tratamento de fórmula (fora de escopo, item futuro do backlog de PDF).

Screenshots

itemC_before_after

Quais são os tickets relevantes?

Nenhuma issue aberta associada; achado durante revisão visual do corpus de teste do gerador de PDF.

Referências

N/A


Segurança da informação (NSI.04)

Seção obrigatória. Marque as opções aplicáveis e justifique quando necessário. Referência: NSI.04 - Norma de Desenvolvimento Seguro.

Este PR manipula dados sensíveis ou pessoais (LGPD)?

  • Sim — descreva os controles de proteção aplicados (criptografia, mascaramento, anonimização, etc.):
  • Não

Este PR altera autenticação, autorização, controle de acesso ou gerenciamento de sessão?

  • Sim — descreva o que mudou e por quê:
  • Não

Este PR introduz, atualiza ou remove dependências de terceiros?

  • Sim — as novas dependências foram verificadas no SBOM/Trivy sem vulnerabilidades críticas/altas em aberto?
    • Verificado e aprovado
    • Pendente / vulnerabilidade aceita com justificativa:
  • Não

Este PR foi validado pelo pipeline de segurança (SonarQube / Trivy)?

  • Sim — link do job:
  • Não aplicável a este PR (justifique): mudança isolada de extração/formatação de texto a partir do próprio XML do artigo, sem I/O externo nem entrada não confiável

Este PR concatena, monta ou executa comandos SQL, HTML ou JavaScript a partir de entrada externa?

  • Sim — confirme que há sanitização/parametrização (prepared statements, escaping, etc.):
  • Não

Este PR expõe novos endpoints, telas ou serviços?

  • Sim — HTTPS obrigatório está garantido e o acesso segue o princípio de menor privilégio?
  • Não

Algum segredo, senha, chave ou token está sendo adicionado ao código-fonte?

  • Não, nenhum segredo foi commitado
  • Sim (bloquear merge e corrigir antes de prosseguir)

🤖 Generated with Claude Code

https://claude.ai/code/session_01X8r2LRJ3PGTT9vLaPtb373

…orpo

extract_body_data extraia o texto de cada <p> via
`para.xpath('.//text()...')` seguido de `' '.join(texts).split()`, que
insere um espaco entre TODO par de fragmentos de texto, independente de
ter espaco na fonte. Um paragrafo com citacao entre parenteses, ex.:
"...pressure (<xref>Lang and Barling, 2012</xref>; <xref>Ripple et al.,
2019</xref>) crop...", virava "...pressure ( Lang and Barling, 2012 ;
Ripple et al., 2019 ) crop...": espaco extra logo apos "(", antes de
")" e antes de cada ";".

get_text_from_node (xml_utils.py) ja fazia extracao correta preservando
adjacencia real via .tail, inclusive com tratamento proprio pra xref,
mas so era usado como fallback de excecao nessa funcao. Passa a ser o
caminho principal, com um novo parametro skip_tags pra manter a exclusao
de <fig>/<table-wrap> do texto do paragrafo (preservando so o .tail que
vem depois deles).

_remove_double_spaces (chamada por get_text_from_node e por
get_text_from_mixed_citation_node) passa a colapsar qualquer sequencia
de espaco em branco (nao so espaco duplo literal) num unico espaco: o
.tail de uma <fig>/<table-wrap> pulada pode carregar a indentacao de
quebra de linha do XML fonte (`\n      `), que antes vazava crua pro
paragrafo renderizado.

Validado contra o corpus real de 26 artigos: 1765 paragrafos
extraidos, so 15 ainda tem parenteses/espaco "suspeitos" e todos sao
explicaveis (espaco literal na propria fonte XML, ou formula MathML
achatada em texto, sem tratamento de formula ainda).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8r2LRJ3PGTT9vLaPtb373
@pitangainnovare

Copy link
Copy Markdown
Contributor

Ainda encontrei trechos em que o problema ocorre. Por exemplo, há espaços extras no a2.pdf:

A2 - Foto 1
image

A2 - Foto 2
image

texts_els.append(node.text)

for child in node:
if child.tag == 'xref':

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sugiro simplificar get_text_from_node e aplicar uma normalização de espaçamento de pontuação:

  1. A remoção dos branches elif child.tag == 'xref': e elif child.tag in ('italic', 'bold'): evita a perda de tags como <sup> (estilo Vancouver), <sub>, <sc>, além de evitar a perda de subchild.tail em marcações aninhadas.
  2. A inclusão de _normalize_punctuation_spacing garante que artefatos de espaço presentes no XML de origem (como ( <xref> em a2.xml e a16.xml) sejam corrigidos para o padrão tipográfico esperado no PDF ((Citation) em vez de ( Citation)).
def get_text_from_node(node, skip_tags=None):
    skip_tags = skip_tags or set()
    texts_els = []

    if node.text:
        texts_els.append(node.text)

    for child in node:
        if child.tag in skip_tags:
            pass
        else:
            texts_els.append(get_text_from_node(child, skip_tags=skip_tags))

        if child.tail:
            texts_els.append(child.tail)

    text = ''.join(texts_els)
    text = _remove_double_spaces(text)
    text = _normalize_punctuation_spacing(text)
    return text


def _normalize_punctuation_spacing(text):
    text = re.sub(r'\(\s+', '(', text)
    text = re.sub(r'\s+\)', ')', text)
    text = re.sub(r'\[\s+', '[', text)
    text = re.sub(r'\s+\]', ']', text)
    text = re.sub(r'\s+;', ';', text)
    text = re.sub(r'\s+,', ',', text)
    return text

)
result = xml_utils.get_text_from_node(xmltree)
self.assertEqual('Before Figure 1 after', result)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sugiro adicionar testes cobrindo:

  1. <sup> dentro de <xref>
  2. Marcações aninhadas com tail
  3. Normalização de espaços ao redor de parênteses, colchetes, ponto-e-vírgula e vírgula
    def test_get_text_from_node_with_sup_inside_xref(self):
        xmltree = etree.fromstring(
            '<p>Author <xref ref-type="bibr"><sup>1,2</sup></xref> stated</p>'
        )
        self.assertEqual('Author 1,2 stated', xml_utils.get_text_from_node(xmltree))

    def test_get_text_from_node_nested_formatting_with_tail(self):
        xmltree = etree.fromstring(
            '<p>Start <bold>bold <italic>and italic</italic> still bold</bold> end</p>'
        )
        self.assertEqual(
            'Start bold and italic still bold end',
            xml_utils.get_text_from_node(xmltree),
        )

    def test_get_text_from_node_normalizes_spaces_around_parentheses_and_punctuation(self):
        xmltree = etree.fromstring(
            '<p>Studies ( <xref ref-type="bibr">Author, 2020</xref> ; '
            '<xref ref-type="bibr">Other, 2021</xref> ) and [ <xref ref-type="bibr">1</xref> ] '
            'with comma ( <xref ref-type="bibr">Foo, 2019</xref> , more).</p>'
        )
        self.assertEqual(
            'Studies (Author, 2020; Other, 2021) and [1] with comma (Foo, 2019, more).',
            xml_utils.get_text_from_node(xmltree),
        )

@pitangainnovare pitangainnovare left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testando o PR em artigos reais (ex.: a2.xml, a10.xml, a11.xml, a19.xml), identifiquei dois pontos:

  1. Problema em citações numéricas (<sup>): A função get_text_from_node (em xml_utils.py) tinha um tratamento específico elif child.tag == 'xref': que só considerava subelementos <italic> e <bold>. Qualquer tag diferente - em especial <sup> (estilo Vancouver, como <xref ref-type="bibr"><sup>1</sup></xref>) - é descartada. Isso apagou 531 citações em 118 parágrafos de 3 artigos do corpus (ex.: tratamento fonoterápico(). em a11.xml).
  2. O problema de espaçamento ainda persiste no PDF em artigos como a2.xml: Você mencionou na descrição que sobraram casos como a16.xml: "( <xref>Figure</xref> )" porque o espaço já existia no XML fonte. Em a2.xml, há 47 ocorrências de ( <xref... no próprio XML. No entanto, entendo que esses são defeitos do XML que podem ser tratados pelo sistema.

Recomendo

  • Remover os branches especiais elif child.tag == 'xref': e elif child.tag in ('italic', 'bold'): de get_text_from_node, deixando a recursão geral tratar todos os nós uniformemente
  • Adicionar uma função simples de normalização tipográfica _normalize_punctuation_spacing para remover espaços internos a parênteses/colchetes (( -> (, ) -> )) e antes de pontuação ( ; -> ;, , -> ,).

Rossi-Luciano added a commit to Rossi-Luciano/packtools that referenced this pull request Sep 8, 2026
…m_node

Atende à revisão de @pitangainnovare no PR scieloorg#1327: os branches especiais
para <xref> e <italic>/<bold> em get_text_from_node só reconheciam
subelementos italic/bold, descartando qualquer outra tag (ex.: <sup>,
estilo Vancouver) e perdendo o tail de marcação aninhada (ex.: texto
depois de um <italic> dentro de um <bold>). Confirmado contra o corpus
de 26 artigos: 659 ocorrências de <sup> dentro de <xref> em 17 artigos
tinham a citação apagada (ex. a11.xml: "fonoterápico()." em vez de
"fonoterápico(1).").

Substituídos os branches especiais por recursão genérica uniforme, que
já trata texto+filhos+tail corretamente para qualquer tag. Adicionada
_normalize_punctuation_spacing para remover espaços presos a
parênteses/colchetes e antes de ";"/"," quando existem literalmente no
XML fonte ao redor de um <xref> (segundo ponto da revisão).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvPK3d53nxxxVnMhQJJdzC
…m_node

Atende à revisão de @pitangainnovare no PR scieloorg#1327: os branches especiais
para <xref> e <italic>/<bold> em get_text_from_node só reconheciam
subelementos italic/bold, descartando qualquer outra tag (ex.: <sup>,
estilo Vancouver) e perdendo o tail de marcação aninhada (ex.: texto
depois de um <italic> dentro de um <bold>). Confirmado contra o corpus
de 26 artigos: 659 ocorrências de <sup> dentro de <xref> em 17 artigos
tinham a citação apagada (ex. a11.xml: "fonoterápico()." em vez de
"fonoterápico(1).").

Substituídos os branches especiais por recursão genérica uniforme, que
já trata texto+filhos+tail corretamente para qualquer tag. Adicionada
_normalize_punctuation_spacing para remover espaços presos a
parênteses/colchetes e antes de ";"/"," quando existem literalmente no
XML fonte ao redor de um <xref> (segundo ponto da revisão).
@Rossi-Luciano
Rossi-Luciano force-pushed the fix/pdf-citation-spacing branch from d58fb1b to be8ec53 Compare September 8, 2026 12:38
@Rossi-Luciano

Copy link
Copy Markdown
Contributor Author

Obrigado pela revisão, @pitangainnovare. Os dois pontos procedem, corrigidos em be8ec53:

  1. Perda de <sup>/tail aninhado: removidos os branches especiais de <xref>/<italic>/<bold> em get_text_from_node, unificando para a recursão genérica (que já tratava texto+filhos+tail corretamente para qualquer tag). Isso corrige tanto a perda de <sup> (estilo Vancouver) quanto um segundo bug correlato que encontrei: perda do .tail em marcações aninhadas (<bold>texto <italic>x</italic> resto</bold> perdia "resto"). Validado contra o corpus de 26 artigos: 659 ocorrências de <xref><sup> em 17 artigos, todas com a citação restaurada (ex.: a11.xml, "fonoterápico()." virou "fonoterápico(1).").
  2. Espaços residuais em parênteses/pontuação: adicionada _normalize_punctuation_spacing, exatamente como sugerido.

Também adicionei os 3 testes que você propôs. Suíte completa do pipeline PDF: 191 testes passando.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants