From ab96991a7298db934659b701a9e2c4e2a40fda06 Mon Sep 17 00:00:00 2001 From: DokoGen Date: Mon, 3 Aug 2026 10:08:45 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20Word=20=D1=80=D0=B0=D0=B7=D0=B1=D0=B8?= =?UTF-8?q?=D0=B2=D0=B0=D0=B5=D1=82=20=D0=BC=D0=B0=D1=80=D0=BA=D0=B5=D1=80?= =?UTF-8?q?=D1=8B=20=D1=86=D0=B8=D0=BA=D0=BB=D0=BE=D0=B2/=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BD=D0=B0?= =?UTF-8?q?=20run'=D1=8B=20+=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=20=D0=98?= =?UTF-8?q?=D0=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _normalize_loop_markers: склейка текста параграфа в один run перед поиском и {{item.*}}, иначе циклы не разворачивались (шаблон '6. Акт определения уровня защищенности ПДн.docx') - variables.py: поле address в цикле information_systems --- dokogen/generator.py | 56 ++++++++++++++++++++++++++++++++++++++++++++ dokogen/variables.py | 1 + 2 files changed, 57 insertions(+) diff --git a/dokogen/generator.py b/dokogen/generator.py index 8f1afa3..f35be88 100644 --- a/dokogen/generator.py +++ b/dokogen/generator.py @@ -68,6 +68,59 @@ OLD_VAR_MAP = { # 2. ОБРАБОТКА ЦИКЛОВ НА УРОВНЕ ZIP/XML # ============================================================ +W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' + + +def _normalize_loop_markers(doc_xml: str) -> str: + """Word разбивает маркеры и переменные {{item.x}} + на несколько run'ов (например '' в третьем). Склеиваем текст каждого параграфа в один run, + чтобы маркеры и переменные циклов снова стали едиными.""" + try: + root = etree.fromstring(doc_xml.encode('utf-8')) + except Exception: + return doc_xml + + changed = False + for p in root.iter(f'{{{W_NS}}}p'): + # Собираем весь текст параграфа (по всем run'ам) + texts = [] + for t in p.iter(f'{{{W_NS}}}t'): + texts.append(t.text or '') + full = ''.join(texts) + + # Интересуют только параграфы с маркерами циклов ИЛИ переменными цикла + if 'loop:' not in full and 'loop_end' not in full and '{{item.' not in full: + continue + + # Сохраняем форматирование первого run + first_r = p.find(f'{{{W_NS}}}r') + rpr = None + if first_r is not None: + rpr_el = first_r.find(f'{{{W_NS}}}rPr') + if rpr_el is not None: + rpr = etree.fromstring(etree.tostring(rpr_el)) + + # Оставляем только pPr, удаляем все run'ы + for child in list(p): + if child.tag != f'{{{W_NS}}}pPr': + p.remove(child) + + # Создаём один run с полным текстом параграфа + new_r = etree.SubElement(p, f'{{{W_NS}}}r') + if rpr is not None: + new_r.append(rpr) + new_t = etree.SubElement(new_r, f'{{{W_NS}}}t') + new_t.text = full + new_t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve') + changed = True + + if not changed: + return doc_xml + + return etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True).decode('utf-8') + + def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str: """Обрабатывает циклы напрямую в ZIP/DOCX на уровне XML. Работает до загрузки python-docx — гарантирует корректную вложенность. @@ -102,6 +155,9 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str: if not doc_xml: break + # Word мог разбить маркеры циклов на несколько run'ов — склеиваем + doc_xml = _normalize_loop_markers(doc_xml) + # Находим ВСЕ маркеры циклов start_markers = list(LOOP_START.finditer(doc_xml)) end_markers = list(LOOP_END.finditer(doc_xml)) diff --git a/dokogen/variables.py b/dokogen/variables.py index 44e8c05..4b3aaa8 100644 --- a/dokogen/variables.py +++ b/dokogen/variables.py @@ -219,6 +219,7 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]: items.append({ 'name': isys.name or '—', 'description': isys.description or '—', + 'address': getattr(isys, 'address', '') or '—', 'is_has_lan': 'Да' if isys.is_local_network else 'Нет', 'has_internet': 'да' if isys.is_internet else 'нет', 'defence_level': str(isys.defence_level or '—'),