From 2402899c1ed7dc9c27b3b13ab9a1086bf1fb8a0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:15:53 +0000 Subject: [PATCH 1/3] Initial plan From 730a8eacae9a917280e9062552b48c0886ef9025 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:20:41 +0000 Subject: [PATCH 2/3] Add xml_fixer module with fix_inline_graphic_in_caption_and_label function and comprehensive tests Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> --- packtools/sps/utils/xml_fixer.py | 159 ++++++++++++++++ tests/sps/utils/test_xml_fixer.py | 298 ++++++++++++++++++++++++++++++ 2 files changed, 457 insertions(+) create mode 100644 packtools/sps/utils/xml_fixer.py create mode 100644 tests/sps/utils/test_xml_fixer.py diff --git a/packtools/sps/utils/xml_fixer.py b/packtools/sps/utils/xml_fixer.py new file mode 100644 index 000000000..5aa02adcb --- /dev/null +++ b/packtools/sps/utils/xml_fixer.py @@ -0,0 +1,159 @@ +""" +Módulo para funções de correção de XML. + +Este módulo contém funções utilitárias para corrigir problemas +comuns em documentos XML, especialmente para conformidade com SPS. +""" +import logging +from lxml import etree +from typing import List, Dict + +logger = logging.getLogger(__name__) + + +def fix_inline_graphic_in_caption_and_label(xmltree): + """ + Corrige elementos inline-graphic incorretamente posicionados dentro de caption ou label. + + Esta função identifica todos os elementos `inline-graphic` que estejam dentro de + `label` ou `caption` e, quando não existir um elemento `graphic` no pai (fig, + table-wrap, boxed-text, etc), move o `inline-graphic` para fora do `label`/`caption` + e o renomeia para `graphic`. + + Args: + xmltree: Objeto lxml.etree representando o documento XML a ser corrigido. + + Returns: + list: Lista de dicionários documentando cada modificação realizada. Cada + dicionário contém os campos: + - xpath: caminho XPath do elemento modificado + - action: tipo de ação realizada + - old_parent: tag do elemento pai original + - new_parent: tag do novo elemento pai + + Examples: + >>> from lxml import etree + >>> xml = ''' + ... + ... Título<inline-graphic xlink:href="img.jpg"/> + ... ''' + >>> tree = etree.fromstring(xml) + >>> changes = fix_inline_graphic_in_caption_and_label(tree) + >>> len(changes) + 1 + >>> changes[0]['action'] + 'moved_and_renamed' + """ + changes = [] + + # Encontra todos os inline-graphic dentro de caption ou label + # XPath para encontrar inline-graphic em qualquer descendente de caption ou label + inline_graphics_in_caption_or_label = xmltree.xpath( + ".//caption//inline-graphic | .//label//inline-graphic" + ) + + for inline_graphic in inline_graphics_in_caption_or_label: + # Obtém o elemento pai do inline-graphic para documentação + old_parent = inline_graphic.getparent() + old_parent_tag = old_parent.tag if old_parent is not None else None + + # Navega até encontrar o elemento container (fig, table-wrap, boxed-text, etc) + # que é o pai do caption/label + container = old_parent + while container is not None: + parent = container.getparent() + if parent is not None: + # Verifica se o pai contém caption ou label + has_caption_or_label = ( + parent.find(".//caption") is not None or + parent.find(".//label") is not None + ) + if has_caption_or_label: + # Este é provavelmente o container (fig, table-wrap, etc) + container = parent + break + container = parent + + # Se não encontrou um container adequado, tenta encontrar o pai direto de caption/label + if container is None or container.tag in ('caption', 'label', 'title', 'p'): + # Procura pelo ancestral que contém caption ou label + current = inline_graphic + while current is not None: + parent = current.getparent() + if parent is None: + break + # Verifica se este elemento é um possível container + if parent.tag in ('fig', 'table-wrap', 'boxed-text', 'disp-formula', + 'supplementary-material', 'fig-group', 'article', + 'sub-article', 'body', 'sec', 'app'): + container = parent + break + current = parent + + # Se ainda não encontrou container, usa o elemento raiz + if container is None: + container = xmltree + + # Verifica se já existe um elemento graphic no container + existing_graphic = container.find(".//graphic") + + if existing_graphic is None: + # Não existe graphic, então vamos mover e renomear o inline-graphic + + # Captura o XPath antes de modificar (para documentação) + try: + xpath = xmltree.getpath(inline_graphic) + except AttributeError: + # Fallback se getpath não estiver disponível + xpath = f".//{inline_graphic.tag}" + + # Remove o inline-graphic de sua posição atual + old_parent.remove(inline_graphic) + + # Cria um novo elemento graphic com os mesmos atributos + graphic = etree.Element("graphic", attrib=inline_graphic.attrib) + + # Copia o texto e tail se existirem + graphic.text = inline_graphic.text + graphic.tail = inline_graphic.tail + + # Copia todos os sub-elementos, se houver + for child in inline_graphic: + graphic.append(child) + + # Adiciona o novo elemento graphic ao container + # Tenta adicionar após caption/label, se existirem + caption = container.find(".//caption") + label = container.find(".//label") + + inserted = False + if caption is not None: + # Adiciona após o caption + caption_index = list(container).index(caption) + container.insert(caption_index + 1, graphic) + inserted = True + elif label is not None: + # Adiciona após o label + label_index = list(container).index(label) + container.insert(label_index + 1, graphic) + inserted = True + + if not inserted: + # Adiciona como último filho + container.append(graphic) + + # Registra a mudança + change = { + "xpath": xpath, + "action": "moved_and_renamed", + "old_parent": old_parent_tag, + "new_parent": container.tag, + } + changes.append(change) + + logger.info( + f"Moved and renamed inline-graphic to graphic: {xpath} " + f"from {old_parent_tag} to {container.tag}" + ) + + return changes diff --git a/tests/sps/utils/test_xml_fixer.py b/tests/sps/utils/test_xml_fixer.py new file mode 100644 index 000000000..d264cbf5d --- /dev/null +++ b/tests/sps/utils/test_xml_fixer.py @@ -0,0 +1,298 @@ +# coding: utf-8 +import unittest +from lxml import etree + +from packtools.sps.utils import xml_fixer + + +class TestFixInlineGraphicInCaptionAndLabel(unittest.TestCase): + """Testes para a função fix_inline_graphic_in_caption_and_label.""" + + def test_simple_inline_graphic_in_caption_title(self): + """Testa correção de inline-graphic simples dentro de caption/title.""" + xml = etree.fromstring( + b""" + + + T\xc3\xadtulo da figura<inline-graphic xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="img1.jpg"/> + + """ + ) + + changes = xml_fixer.fix_inline_graphic_in_caption_and_label(xml) + + # Verifica que houve uma mudança + self.assertEqual(len(changes), 1) + self.assertEqual(changes[0]['action'], 'moved_and_renamed') + self.assertEqual(changes[0]['old_parent'], 'title') + self.assertEqual(changes[0]['new_parent'], 'fig') + + # Verifica que inline-graphic foi removido de caption + inline_graphics_in_caption = xml.xpath(".//caption//inline-graphic") + self.assertEqual(len(inline_graphics_in_caption), 0) + + # Verifica que graphic foi adicionado ao fig + graphics = xml.findall(".//graphic") + self.assertEqual(len(graphics), 1) + + # Verifica que o atributo foi preservado + graphic = graphics[0] + self.assertEqual( + graphic.get("{http://www.w3.org/1999/xlink}href"), + "img1.jpg" + ) + + def test_inline_graphic_in_label(self): + """Testa correção de inline-graphic dentro de label.""" + xml = etree.fromstring( + b""" + + T\xc3\xadtulo + """ + ) + + changes = xml_fixer.fix_inline_graphic_in_caption_and_label(xml) + + # Verifica que houve uma mudança + self.assertEqual(len(changes), 1) + self.assertEqual(changes[0]['action'], 'moved_and_renamed') + self.assertEqual(changes[0]['old_parent'], 'label') + self.assertEqual(changes[0]['new_parent'], 'fig') + + # Verifica que inline-graphic foi removido de label + inline_graphics_in_label = xml.xpath(".//label//inline-graphic") + self.assertEqual(len(inline_graphics_in_label), 0) + + # Verifica que graphic foi adicionado + graphics = xml.findall(".//graphic") + self.assertEqual(len(graphics), 1) + + def test_multiple_inline_graphics(self): + """Testa correção de múltiplos inline-graphics.""" + xml = etree.fromstring( + b"""
+ + + + + T\xc3\xadtulo 1<inline-graphic xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="img1.jpg"/> + + + + + + T\xc3\xadtulo 2<inline-graphic xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="img2.jpg"/> + + + +
""" + ) + + changes = xml_fixer.fix_inline_graphic_in_caption_and_label(xml) + + # Verifica que houve duas mudanças + self.assertEqual(len(changes), 2) + + # Verifica que não há mais inline-graphics em caption + inline_graphics_in_caption = xml.xpath(".//caption//inline-graphic") + self.assertEqual(len(inline_graphics_in_caption), 0) + + # Verifica que foram criados dois graphics + graphics = xml.findall(".//graphic") + self.assertEqual(len(graphics), 2) + + # Verifica que estão nos lugares corretos + fig_graphic = xml.find(".//fig[@id='f1']/graphic") + self.assertIsNotNone(fig_graphic) + + table_graphic = xml.find(".//table-wrap[@id='t1']/graphic") + self.assertIsNotNone(table_graphic) + + def test_graphic_already_exists(self): + """Testa que não modifica quando graphic já existe no pai.""" + xml = etree.fromstring( + b""" + + + T\xc3\xadtulo<inline-graphic xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="img_wrong.jpg"/> + + + """ + ) + + changes = xml_fixer.fix_inline_graphic_in_caption_and_label(xml) + + # Verifica que não houve mudanças + self.assertEqual(len(changes), 0) + + # Verifica que inline-graphic ainda está em caption + inline_graphics_in_caption = xml.xpath(".//caption//inline-graphic") + self.assertEqual(len(inline_graphics_in_caption), 1) + + # Verifica que graphic original permanece + graphics = xml.findall(".//graphic") + self.assertEqual(len(graphics), 1) + self.assertEqual( + graphics[0].get("{http://www.w3.org/1999/xlink}href"), + "img_correct.jpg" + ) + + def test_table_wrap_context(self): + """Testa correção em contexto de table-wrap.""" + xml = etree.fromstring( + b""" + + + T\xc3\xadtulo da tabela<inline-graphic xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="table_img.jpg"/> + + + + + +
Dados
+
""" + ) + + changes = xml_fixer.fix_inline_graphic_in_caption_and_label(xml) + + # Verifica que houve uma mudança + self.assertEqual(len(changes), 1) + self.assertEqual(changes[0]['new_parent'], 'table-wrap') + + # Verifica que graphic foi adicionado a table-wrap + graphics = xml.findall(".//graphic") + self.assertEqual(len(graphics), 1) + + def test_boxed_text_context(self): + """Testa correção em contexto de boxed-text.""" + xml = etree.fromstring( + b""" + + + T\xc3\xadtulo do box<inline-graphic xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="box_img.jpg"/> + +

Conte\xc3\xbado do box

+
""" + ) + + changes = xml_fixer.fix_inline_graphic_in_caption_and_label(xml) + + # Verifica que houve uma mudança + self.assertEqual(len(changes), 1) + self.assertEqual(changes[0]['new_parent'], 'boxed-text') + + # Verifica que graphic foi adicionado + graphics = xml.findall(".//graphic") + self.assertEqual(len(graphics), 1) + + def test_preserves_all_attributes(self): + """Testa que todos os atributos são preservados.""" + xml = etree.fromstring( + b""" + + + T\xc3\xadtulo<inline-graphic id="ig1" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="img.jpg" content-type="image/jpeg"/> + + """ + ) + + changes = xml_fixer.fix_inline_graphic_in_caption_and_label(xml) + + # Verifica que houve uma mudança + self.assertEqual(len(changes), 1) + + # Verifica que todos os atributos foram preservados + graphic = xml.find(".//graphic") + self.assertIsNotNone(graphic) + self.assertEqual(graphic.get("id"), "ig1") + self.assertEqual( + graphic.get("{http://www.w3.org/1999/xlink}href"), + "img.jpg" + ) + self.assertEqual(graphic.get("content-type"), "image/jpeg") + + def test_preserves_namespaces(self): + """Testa que namespaces são preservados.""" + xml = etree.fromstring( + b""" + + + T\xc3\xadtulo<inline-graphic xlink:href="img.jpg"/> + + """ + ) + + changes = xml_fixer.fix_inline_graphic_in_caption_and_label(xml) + + # Verifica que houve uma mudança + self.assertEqual(len(changes), 1) + + # Verifica que namespace foi preservado + graphic = xml.find(".//graphic") + self.assertIsNotNone(graphic) + self.assertEqual( + graphic.get("{http://www.w3.org/1999/xlink}href"), + "img.jpg" + ) + + def test_caption_without_title(self): + """Testa correção quando inline-graphic está diretamente em caption sem title.""" + xml = etree.fromstring( + b""" + + +

Par\xc3\xa1grafo

+ +
""" + ) + + changes = xml_fixer.fix_inline_graphic_in_caption_and_label(xml) + + # Verifica que houve uma mudança + self.assertEqual(len(changes), 1) + + # Verifica que graphic foi adicionado + graphics = xml.findall(".//graphic") + self.assertEqual(len(graphics), 1) + + def test_no_inline_graphics(self): + """Testa que retorna lista vazia quando não há inline-graphics para corrigir.""" + xml = etree.fromstring( + b""" + + T\xc3\xadtulo + + """ + ) + + changes = xml_fixer.fix_inline_graphic_in_caption_and_label(xml) + + # Verifica que não houve mudanças + self.assertEqual(len(changes), 0) + + def test_inline_graphic_outside_caption_label_not_affected(self): + """Testa que inline-graphics fora de caption/label não são afetados.""" + xml = etree.fromstring( + b"""
+ + + + T\xc3\xadtulo + +

Par\xc3\xa1grafo com inline

+ +
""" + ) + + changes = xml_fixer.fix_inline_graphic_in_caption_and_label(xml) + + # Verifica que não houve mudanças + self.assertEqual(len(changes), 0) + + # Verifica que inline-graphic ainda está no parágrafo + inline_graphics = xml.xpath(".//p//inline-graphic") + self.assertEqual(len(inline_graphics), 1) + + +if __name__ == "__main__": + unittest.main() From 99d8b8f0acdeead885028675dff8db4a7fff51b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:22:34 +0000 Subject: [PATCH 3/3] Add type hints and improve imports in xml_fixer module Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> --- packtools/sps/utils/xml_fixer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packtools/sps/utils/xml_fixer.py b/packtools/sps/utils/xml_fixer.py index 5aa02adcb..7850ccd8e 100644 --- a/packtools/sps/utils/xml_fixer.py +++ b/packtools/sps/utils/xml_fixer.py @@ -5,13 +5,14 @@ comuns em documentos XML, especialmente para conformidade com SPS. """ import logging -from lxml import etree from typing import List, Dict +from lxml import etree + logger = logging.getLogger(__name__) -def fix_inline_graphic_in_caption_and_label(xmltree): +def fix_inline_graphic_in_caption_and_label(xmltree: etree._Element) -> List[Dict[str, str]]: """ Corrige elementos inline-graphic incorretamente posicionados dentro de caption ou label.