From 577bed22d6dce81b1188784e7472ca5d6d31ff75 Mon Sep 17 00:00:00 2001 From: DokoGen Date: Mon, 3 Aug 2026 10:53:59 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BA=D0=BB=D0=B0=D0=B2=D0=B8=D1=88?= =?UTF-8?q?=D0=B8=20(Ctrl/Alt/Del)=20=D0=B2=20=D0=B8=D1=81=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20+=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D1=81=D0=B2=D0=B5=D1=82=D0=BA=D0=B0=20=D0=BC=D0=B0=D1=80=D0=BA?= =?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=BC=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IGNORED_OLD_VARS: , , и др. клавиши больше не считаются незаменёнными переменными (7.6 Инструкция Пользователя) - Подсветка: вместо красного цвета текста — красный маркер (w:highlight) на самой переменной, а не на всём абзаце - OLD_VAR_MAP: ShortCompanyName1-6 (склонения краткого названия), IspdnName → {{is_name_1}} --- dokogen/generator.py | 81 +++++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 19 deletions(-) diff --git a/dokogen/generator.py b/dokogen/generator.py index f37ef42..4e0a314 100644 --- a/dokogen/generator.py +++ b/dokogen/generator.py @@ -30,6 +30,17 @@ VAR_PATTERN = re.compile(r'\{\{[^}]+\}\}') OLD_VAR_PATTERN = re.compile(r'<[A-Za-z0-9_]+>') UNMATCHED_PATTERN = re.compile(r'\{\{[^}]+\}\}|<[A-Za-z0-9_]+>') +# «Переменные», которые на самом деле не переменные, а клавиши в тексте +# инструкций (Ctrl+Alt+Del и т.п.) — их не считаем незаменёнными +IGNORED_OLD_VARS = { + '', '', '', '', '', '', '', + '', '', '', '', '', '', '', + '', '', '', '', '', + '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', + '', '', '', +} + LOOP_START = re.compile(r'(?:<!--|)') LOOP_END = re.compile(r'(?:<!--|)') @@ -61,6 +72,14 @@ OLD_VAR_MAP = { '': '{{document_number}}', '': '{{document_date}}', '': '{{date}}', + # Краткое наименование по падежам (старые шаблоны) + '': '{{company_short_name}}', + '': '{{company_short_name_genitive}}', + '': '{{company_short_name_dative}}', + '': '{{company_short_name_accs}}', + '': '{{company_short_name_ablt}}', + '': '{{company_short_name_loct}}', + '': '{{is_name_1}}', } @@ -342,42 +361,64 @@ def _strip_prooferr(doc): def _highlight_unmatched(doc): - """Подсвечивает незаменённые переменные красным.""" + """Выделяет незаменённые переменные красным маркером (только саму переменную).""" ns = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' - def _highlight_run(run): - rpr = run._element.find(f'{{{ns}}}rPr') + def _apply_highlight(run_el): + rpr = run_el.find(f'{{{ns}}}rPr') if rpr is None: rpr = OxmlElement('w:rPr') - run._element.insert(0, rpr) - color = OxmlElement('w:color') - color.set(f'{{{ns}}}val', 'FF0000') - rpr.append(color) + run_el.insert(0, rpr) + hl = OxmlElement('w:highlight') + hl.set(f'{{{ns}}}val', 'red') + rpr.append(hl) + + def _process_para(para): + full = para.text + matches = [m for m in UNMATCHED_PATTERN.finditer(full) + if m.group(0) not in IGNORED_OLD_VARS] + if not matches: + return + + # Пересобираем параграф: обычный текст и переменные отдельными run'ами, + # переменные — с красным маркером. Сохраняем стиль первого run. + first = para.runs[0] if para.runs else None + para.clear() + pos = 0 + for m in matches: + if m.start() > pos: + run = para.add_run(full[pos:m.start()]) + if first is not None: + _copy_run_style(first, run) + run = para.add_run(m.group(0)) + if first is not None: + _copy_run_style(first, run) + _apply_highlight(run._element) + pos = m.end() + if pos < len(full): + run = para.add_run(full[pos:]) + if first is not None: + _copy_run_style(first, run) for para in doc.paragraphs: - for run in para.runs: - if UNMATCHED_PATTERN.search(run.text or ''): - _highlight_run(run) + _process_para(para) for table in doc.tables: for row in table.rows: for cell in row.cells: for para in cell.paragraphs: - for run in para.runs: - if UNMATCHED_PATTERN.search(run.text or ''): - _highlight_run(run) + _process_para(para) for section in doc.sections: - for hf in [section.header, section.footer]: - if hf: + for hf in [section.header, section.footer, + section.first_page_header, section.first_page_footer]: + if hf is not None: for para in hf.paragraphs: - for run in para.runs: - if UNMATCHED_PATTERN.search(run.text or ''): - _highlight_run(run) + _process_para(para) def _log_unmatched(doc, replacements=None, log_func=None): - """Логирует незаменённые переменные.""" + """Логирует незаменённые переменные (без клавиш-исключений).""" if not log_func: return unmatched = [] @@ -385,6 +426,8 @@ def _log_unmatched(doc, replacements=None, log_func=None): for para in doc.paragraphs: for m in UNMATCHED_PATTERN.finditer(para.text): var = m.group(0) + if var in IGNORED_OLD_VARS: + continue if var not in seen: seen.add(var) unmatched.append(var)