fix: nested loops work - preserve inner loop_end markers

This commit is contained in:
2026-07-31 00:29:12 +04:00
parent 745e70a164
commit c5466f2992
2 changed files with 39 additions and 20 deletions
+35 -16
View File
@@ -82,20 +82,25 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
if not loops: if not loops:
return docx_path return docx_path
# Временная копия # Временный файл
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.docx') tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.docx')
tmp.close() tmp.close()
shutil.copy2(docx_path, tmp.name)
max_passes = 30 max_passes = 30
for _ in range(max_passes): for _ in range(max_passes):
# Читаем document.xml # Читаем ВСЕ файлы из исходного ZIP
all_files = {}
try: try:
with zipfile.ZipFile(tmp.name, 'r') as z: with zipfile.ZipFile(docx_path if _ == 0 else tmp.name, 'r') as z:
doc_xml = z.read('word/document.xml').decode('utf-8') for name in z.namelist():
all_files[name] = z.read(name)
doc_xml = all_files.get('word/document.xml', b'').decode('utf-8')
except Exception: except Exception:
break break
if not doc_xml:
break
# Находим ВСЕ маркеры циклов # Находим ВСЕ маркеры циклов
start_markers = list(LOOP_START.finditer(doc_xml)) start_markers = list(LOOP_START.finditer(doc_xml))
end_markers = list(LOOP_END.finditer(doc_xml)) end_markers = list(LOOP_END.finditer(doc_xml))
@@ -107,10 +112,24 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
outer = start_markers[0] outer = start_markers[0]
outer_key = outer.group(1) outer_key = outer.group(1)
# Соответствующий end = последний end после outer # Соответствующий end = первый end после outer (или последний, если вложенные)
# Считаем глубину: каждый start +1, каждый end -1
depth = 1
outer_end = None outer_end = None
for m in reversed(end_markers): all_markers = sorted(
if m.start() > outer.start(): [('start', m) for m in start_markers] +
[('end', m) for m in end_markers],
key=lambda x: x[1].start()
)
found_start = False
for kind, m in all_markers:
if m.start() <= outer.start():
continue
if kind == 'start':
depth += 1
elif kind == 'end':
depth -= 1
if depth == 0:
outer_end = m outer_end = m
break break
@@ -121,19 +140,17 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
if not items: if not items:
# Удаляем блок цикла полностью # Удаляем блок цикла полностью
doc_xml = doc_xml[:outer.start()] + doc_xml[outer_end.end():] doc_xml = doc_xml[:outer.start()] + doc_xml[outer_end.end():]
all_files['word/document.xml'] = doc_xml.encode('utf-8')
with zipfile.ZipFile(tmp.name, 'w', zipfile.ZIP_DEFLATED) as z: with zipfile.ZipFile(tmp.name, 'w', zipfile.ZIP_DEFLATED) as z:
z.writestr('word/document.xml', doc_xml.encode('utf-8')) for name, data in all_files.items():
z.writestr(name, data)
continue continue
# Извлекаем шаблон (всё между start и end маркерами) # Извлекаем шаблон (всё между start и end маркерами)
template = doc_xml[outer.end():outer_end.start()] template = doc_xml[outer.end():outer_end.start()]
# Удаляем только последний loop_end (принадлежит внешнему циклу) # НЕ удаляем loop_end из шаблона — они принадлежат вложенным циклам!
template_clean = template template_clean = template
last_end_markers = list(LOOP_END.finditer(template_clean))
if last_end_markers:
last_end = last_end_markers[-1]
template_clean = template_clean[:last_end.start()] + template_clean[last_end.end():]
# Размножаем шаблон для каждого элемента данных # Размножаем шаблон для каждого элемента данных
expanded_parts = [] expanded_parts = []
@@ -148,9 +165,11 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
# Собираем новый XML # Собираем новый XML
new_xml = doc_xml[:outer.start()] + ''.join(expanded_parts) + doc_xml[outer_end.end():] new_xml = doc_xml[:outer.start()] + ''.join(expanded_parts) + doc_xml[outer_end.end():]
# Сохраняем # Сохраняем ВСЕ файлы ZIP, обновляя только document.xml
all_files['word/document.xml'] = new_xml.encode('utf-8')
with zipfile.ZipFile(tmp.name, 'w', zipfile.ZIP_DEFLATED) as z: with zipfile.ZipFile(tmp.name, 'w', zipfile.ZIP_DEFLATED) as z:
z.writestr('word/document.xml', new_xml.encode('utf-8')) for name, data in all_files.items():
z.writestr(name, data)
return tmp.name return tmp.name
+2 -2
View File
@@ -9,7 +9,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from dokogen.models import Company, CommissionMember, InformationSystem from dokogen.models import Company, CommissionMember, InformationSystem
from dokogen.variables import build_replacements from dokogen.variables import build_replacements
from dokogen.generator import process_template, _expand_loops_in_zip from dokogen.generator import process_template, expand_loops_in_zip
def create_test_template(): def create_test_template():
"""Creates a test .docx with nested loops for verification.""" """Creates a test .docx with nested loops for verification."""
@@ -108,7 +108,7 @@ def test_zip_loop_expansion():
# Process with ZIP-level loop expansion # Process with ZIP-level loop expansion
print('\nProcessing loops via ZIP...') print('\nProcessing loops via ZIP...')
result_path = _expand_loops_in_zip(template_path, loops) result_path = expand_loops_in_zip(template_path, loops)
# Verify result # Verify result
with zipfile.ZipFile(result_path, 'r') as z: with zipfile.ZipFile(result_path, 'r') as z: