from abc import ABC, abstractmethod from typing import Dict, List, Any, Optional, Union, Callable, Tuple, NamedTuple from docx import Document from docx.shared import Pt, Cm, RGBColor, Inches from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING, WD_BREAK from docx.enum.style import WD_STYLE_TYPE from docx.oxml.ns import qn from docx.oxml import OxmlElement from docx.oxml.ns import nsdecls from docx.oxml.shape import CT_Picture from docx.oxml.xmlchemy import BaseOxmlElement import re import os import logging # Настройка логирования logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class GostDocumentConfig: """Конфигурация параметров документа по ГОСТ""" def __init__(self, **kwargs): # Поля документа (в сантиметрах) self.margins = kwargs.get('margins', { 'top': 2.0, 'bottom': 2.0, 'left': 3.0, 'right': 1.5 }) # Шрифты self.font = kwargs.get('font', { 'name': 'Times New Roman', 'size': 14, 'main_language': 'ru-RU' }) # Межстрочный интервал self.line_spacing = kwargs.get('line_spacing', 1.5) # Отступы для заголовков self.heading_spacing = kwargs.get('heading_spacing', { 1: {'before': 24, 'after': 12}, 2: {'before': 24, 'after': 6}, 3: {'before': 18, 'after': 6}, 4: {'before': 12, 'after': 6} # Для пунктов }) # Стили заголовков self.heading_styles = kwargs.get('heading_styles', { 1: {'size': 16, 'bold': True, 'all_caps': True, 'align': 'center'}, 2: {'size': 14, 'bold': True, 'all_caps': False, 'align': 'left'}, 3: {'size': 14, 'bold': True, 'italic': True, 'all_caps': False, 'align': 'left'}, 4: {'size': 14, 'bold': False, 'italic': False, 'all_caps': False, 'align': 'left'} }) # Стиль абзаца self.paragraph = kwargs.get('paragraph', { 'first_line_indent': 1.25, # в см 'alignment': 'justify' }) # Стиль таблиц self.table = kwargs.get('table', { 'font_size': 12, 'header_bold': True, 'allow_breaking': True # Разрешить перенос таблицы на новую страницу }) # Стиль подписей self.caption = kwargs.get('caption', { 'font_size': 12, 'italic': True, 'align': 'center', 'format': '{type} {number} — {text}' # Формат подписи }) # Стиль для программного кода self.code = kwargs.get('code', { 'font_name': 'Courier New', 'font_size': 12, 'background_color': (240, 240, 240) # Светло-серый фон }) # Стиль для формул self.formula = kwargs.get('formula', { 'font_size': 14, 'number_format': '({number})', # Формат номера формулы 'alignment': 'center' }) # Настройки списка литературы self.references = kwargs.get('references', { 'style': 'ГОСТ Р 7.0.5-2008', 'sort_by': 'author' # 'author' или 'order' }) # Режим нумерации элементов (по умолчанию - сквозная нумерация) self.element_numbering = kwargs.get('element_numbering', { 'table': 'global', # 'global' или 'section' 'figure': 'global', 'listing': 'global', 'formula': 'global' }) @classmethod def for_standard(cls, standard: str, **kwargs) -> 'GostDocumentConfig': """ Создает конфигурацию для конкретного стандарта ГОСТ Args: standard: Название стандарта (например, 'GOST 7.32-2017') **kwargs: Дополнительные параметры для переопределения Returns: Конфигурация для указанного стандарта """ standard = standard.upper() # Базовые настройки для ГОСТ 7.32-2017 if "7.32-2017" in standard: config = cls( margins={ 'top': 2.0, 'bottom': 2.0, 'left': 2.5, # Увеличенное левое поле для подшивки 'right': 1.0 }, font={ 'name': 'Times New Roman', 'size': 14, 'main_language': 'ru-RU' }, line_spacing=1.5, heading_spacing={ 1: {'before': 24, 'after': 12}, 2: {'before': 24, 'after': 6}, 3: {'before': 18, 'after': 6}, 4: {'before': 12, 'after': 6} }, heading_styles={ 1: {'size': 16, 'bold': True, 'all_caps': True, 'align': 'center'}, 2: {'size': 14, 'bold': True, 'all_caps': False, 'align': 'left'}, 3: {'size': 14, 'bold': True, 'italic': True, 'all_caps': False, 'align': 'left'}, 4: {'size': 14, 'bold': False, 'italic': False, 'all_caps': False, 'align': 'left'} }, paragraph={ 'first_line_indent': 1.25, 'alignment': 'justify' }, element_numbering={ 'table': 'global', 'figure': 'global', 'listing': 'global', 'formula': 'global' } ) # Настройки для ГОСТ Р 7.0.5-2008 elif "7.0.5-2008" in standard: config = cls( margins={ 'top': 2.0, 'bottom': 2.0, 'left': 3.0, # Больше для подшивки 'right': 1.5 }, font={ 'name': 'Times New Roman', 'size': 14, 'main_language': 'ru-RU' }, line_spacing=1.5, heading_spacing={ 1: {'before': 24, 'after': 12}, 2: {'before': 24, 'after': 6}, # Другие настройки... }, # Другие специфические настройки... ) # Настройки для учебных работ (ГОСТ для ВУЗов) elif "УЧЕБНЫЙ" in standard: config = cls( margins={ 'top': 2.0, 'bottom': 2.0, 'left': 3.0, 'right': 1.5 }, font={ 'name': 'Times New Roman', 'size': 14, 'main_language': 'ru-RU' }, line_spacing=1.5, heading_styles={ 1: {'size': 16, 'bold': True, 'all_caps': True, 'align': 'center'}, # Учебные работы часто требуют иного оформления... }, paragraph={ 'first_line_indent': 1.25, 'alignment': 'justify' }, references={ 'style': 'ГОСТ 7.1-2003', 'sort_by': 'order' } ) else: # Стандартные настройки config = cls() # Применяем пользовательские переопределения for key, value in kwargs.items(): if hasattr(config, key): setattr(config, key, value) return config class ElementReference(NamedTuple): """Структура для хранения информации о ссылке на элемент""" type: str number: str text: str page: int = 0 # Пока не реализовано получение номера страницы class GostDocumentCore(ABC): """Базовый класс для создания документов по ГОСТ с возможностью расширения""" def __init__(self, config: Optional[GostDocumentConfig] = None, title: str = "", author: str = ""): """ Инициализация документа с настройками ГОСТ Args: config: Конфигурация документа (если None - используются стандартные настройки ГОСТ) title: Заголовок документа author: Автор документа """ self.config = config or GostDocumentConfig() self.title = title self.author = author self.document = Document() self._element_processors = {} self._reference_processors = {} self._setup_document() self._create_gost_styles() self._create_list_styles() self._create_appendix_styles() self.setup_document_metadata() # Трекеры элементов и разделов self._current_section = [0] # Иерархия разделов (1, 1.1, 1.1.1) self._element_counters = { 'table': {}, 'figure': {}, 'listing': {}, 'formula': {} } self._element_references = [] # Список всех элементов для оглавления и ссылок self._references = [] # Список использованных источников # Регистрируем базовые процессоры элементов self._register_default_processors() self._register_reference_processors() @classmethod def create(cls, standard: str = "GOST 7.32-2017", title: str = "", author: str = "", **kwargs) -> 'GostDocumentCore': """ Фабричный метод для создания документа с настройками под конкретный стандарт Args: standard: Название стандарта ГОСТ title: Заголовок документа author: Автор документа **kwargs: Дополнительные параметры конфигурации Returns: Экземпляр GostDocumentCore с настройками для указанного стандарта """ config = GostDocumentConfig.for_standard(standard, **kwargs) return cls(config=config, title=title, author=author) def _setup_document(self): """Настройка базовых параметров документа согласно ГОСТ""" # Настройка полей страницы sections = self.document.sections for section in sections: section.top_margin = Cm(self.config.margins['top']) section.bottom_margin = Cm(self.config.margins['bottom']) section.left_margin = Cm(self.config.margins['left']) section.right_margin = Cm(self.config.margins['right']) # Установка стандартного стиля для всего документа style = self.document.styles['Normal'] font = style.font font.name = self.config.font['name'] font.size = Pt(self.config.font['size']) # Установка кириллического шрифта font.element.rPr.rFonts.set(qn('w:eastAsia'), self.config.font['name']) # Настройка формата абзаца paragraph_format = style.paragraph_format paragraph_format.line_spacing_rule = ( WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5 else WD_LINE_SPACING.DOUBLE if self.config.line_spacing == 2.0 else WD_LINE_SPACING.EXACTLY ) paragraph_format.alignment = ( WD_ALIGN_PARAGRAPH.JUSTIFY if self.config.paragraph['alignment'] == 'justify' else WD_ALIGN_PARAGRAPH.LEFT if self.config.paragraph['alignment'] == 'left' else WD_ALIGN_PARAGRAPH.CENTER ) paragraph_format.first_line_indent = Cm(self.config.paragraph['first_line_indent']) def _create_gost_styles(self): """Создание стилей заголовков и других элементов в соответствии с ГОСТ""" # Создаем стили для всех уровней заголовков (1-4) for level in range(1, 5): self._create_heading_style(level) # Стиль для списка self._create_list_style() # Стиль для подписей таблиц и рисунков self._create_caption_style() # Стиль для программного кода self._create_code_style() # Стиль для формул self._create_formula_style() def _create_heading_style(self, level: int): """Создает стиль заголовка заданного уровня""" style_name = f'Heading {level} GOST' if style_name in self.document.styles: return style = self.document.styles.add_style(style_name, WD_STYLE_TYPE.PARAGRAPH) # Настройка шрифта heading_config = self.config.heading_styles.get(level, {}) style.font.name = self.config.font['name'] style.font.size = Pt(heading_config.get('size', 14)) style.font.bold = heading_config.get('bold', False) style.font.italic = heading_config.get('italic', False) style.font.all_caps = heading_config.get('all_caps', False) # Настройка абзаца spacing = self.config.heading_spacing.get(level, {'before': 12, 'after': 6}) style.paragraph_format.space_before = Pt(spacing['before']) style.paragraph_format.space_after = Pt(spacing['after']) style.paragraph_format.line_spacing_rule = ( WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5 else WD_LINE_SPACING.DOUBLE ) # Выравнивание align = heading_config.get('align', 'left') if align == 'center': style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER elif align == 'justify': style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY else: style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT style.paragraph_format.first_line_indent = Cm(0) # Без красной строки для заголовков def _create_list_style(self): """Создает стиль для списков""" if 'List GOST' not in self.document.styles: list_style = self.document.styles.add_style('List GOST', WD_STYLE_TYPE.PARAGRAPH) list_style.base_style = self.document.styles['List'] list_style.font.name = self.config.font['name'] list_style.font.size = Pt(self.config.font['size']) list_style.paragraph_format.left_indent = Cm(1.25) list_style.paragraph_format.space_before = Pt(0) list_style.paragraph_format.space_after = Pt(6) list_style.paragraph_format.line_spacing_rule = ( WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5 else WD_LINE_SPACING.DOUBLE ) def _create_caption_style(self): """Создает стиль для подписей""" if 'Caption GOST' not in self.document.styles: caption_style = self.document.styles.add_style('Caption GOST', WD_STYLE_TYPE.PARAGRAPH) caption_style.base_style = self.document.styles['Caption'] caption_style.font.name = self.config.font['name'] caption_style.font.size = Pt(self.config.caption['font_size']) caption_style.font.italic = self.config.caption['italic'] caption_style.paragraph_format.alignment = ( WD_ALIGN_PARAGRAPH.CENTER if self.config.caption['align'] == 'center' else WD_ALIGN_PARAGRAPH.LEFT ) caption_style.paragraph_format.space_before = Pt(6) caption_style.paragraph_format.space_after = Pt(12) def _create_code_style(self): """Создает стиль для программного кода""" if 'Code GOST' not in self.document.styles: code_style = self.document.styles.add_style('Code GOST', WD_STYLE_TYPE.PARAGRAPH) code_style.font.name = self.config.code['font_name'] code_style.font.size = Pt(self.config.code['font_size']) code_style.paragraph_format.left_indent = Cm(1.25) code_style.paragraph_format.right_indent = Cm(1.25) code_style.paragraph_format.space_before = Pt(12) code_style.paragraph_format.space_after = Pt(12) code_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE def _create_formula_style(self): """Создает стиль для формул""" if 'Formula GOST' not in self.document.styles: formula_style = self.document.styles.add_style('Formula GOST', WD_STYLE_TYPE.PARAGRAPH) formula_style.font.name = self.config.font['name'] formula_style.font.size = Pt(self.config.formula['font_size']) formula_style.paragraph_format.alignment = ( WD_ALIGN_PARAGRAPH.CENTER if self.config.formula['alignment'] == 'center' else WD_ALIGN_PARAGRAPH.LEFT ) formula_style.paragraph_format.space_before = Pt(6) formula_style.paragraph_format.space_after = Pt(6) def _register_default_processors(self): """Регистрирует базовые обработчики элементов""" self.register_processor('heading', self._process_heading) self.register_processor('section', self._process_section) self.register_processor('subsection', self._process_subsection) self.register_processor('subsubsection', self._process_subsubsection) self.register_processor('paragraph', self._process_paragraph) self.register_processor('table', self._process_table) self.register_processor('figure', self._process_figure) self.register_processor('listing', self._process_listing) self.register_processor('formula', self._process_formula) self.register_processor('reference', self._process_reference) self.register_processor('toc', self._process_toc) self.register_processor('page_break', self._process_page_break) def _register_reference_processors(self): """Регистрирует обработчики для создания ссылок на элементы""" self.register_reference_processor('table', self._get_table_reference) self.register_reference_processor('figure', self._get_figure_reference) self.register_reference_processor('listing', self._get_listing_reference) self.register_reference_processor('formula', self._get_formula_reference) self.register_reference_processor('section', self._get_section_reference) def register_processor(self, element_type: str, processor: Callable): """ Регистрирует обработчик для определенного типа элемента Args: element_type: Тип элемента (например, 'heading', 'table') processor: Функция-обработчик """ self._element_processors[element_type] = processor def get_processor(self, element_type: str) -> Optional[Callable]: """ Возвращает обработчик для указанного типа элемента Args: element_type: Тип элемента Returns: Обработчик или None, если не найден """ return self._element_processors.get(element_type) def register_reference_processor(self, element_type: str, processor: Callable): """ Регистрирует обработчик для создания ссылок на элементы Args: element_type: Тип элемента processor: Функция-обработчик, возвращающая текст ссылки """ self._reference_processors[element_type] = processor def get_reference_processor(self, element_type: str) -> Optional[Callable]: """ Возвращает обработчик ссылок для указанного типа элемента Args: element_type: Тип элемента Returns: Обработчик или None, если не найден """ return self._reference_processors.get(element_type) def _get_current_section_path(self) -> str: """Возвращает текущий путь раздела в формате '1.2.3'""" return '.'.join(map(str, self._current_section)) def _increment_section_counter(self, level: int): """ Увеличивает счетчик раздела заданного уровня Args: level: Уровень раздела (1 - раздел, 2 - подраздел и т.д.) """ # Приводим длину списка к нужному уровню while len(self._current_section) < level: self._current_section.append(0) while len(self._current_section) > level: self._current_section.pop() # Увеличиваем счетчик текущего уровня self._current_section[level - 1] += 1 # Обнуляем счетчики более глубоких уровней for i in range(level, len(self._current_section)): self._current_section[i] = 0 def _get_element_number(self, element_type: str) -> str: """ Возвращает номер элемента в соответствии с выбранным режимом нумерации Args: element_type: Тип элемента (table, figure, listing, formula) Returns: Номер элемента """ numbering_style = self.config.element_numbering.get(element_type, 'global') if numbering_style == 'global': # Глобальная сквозная нумерация (Таблица 1, Таблица 2 и т.д.) if element_type not in self._element_counters: self._element_counters[element_type] = 0 self._element_counters[element_type] += 1 return str(self._element_counters[element_type]) else: # Иерархическая нумерация (1.2.3) section_path = self._get_current_section_path() if section_path not in self._element_counters[element_type]: self._element_counters[element_type][section_path] = 0 self._element_counters[element_type][section_path] += 1 return f"{section_path}.{self._element_counters[element_type][section_path]}" def _create_caption(self, element_type: str, text: str) -> str: """ Создает подпись для элемента в соответствии с ГОСТ Args: element_type: Тип элемента text: Текст подписи Returns: Форматированная подпись """ element_number = self._get_element_number(element_type) # Определяем тип элемента для подписи type_name = { 'table': 'Таблица', 'figure': 'Рисунок', 'listing': 'Листинг', 'formula': 'Формула' }.get(element_type, element_type.capitalize()) # Форматируем подпись return self.config.caption['format'].format( type=type_name, number=element_number, text=text ) def add_table_reference(self, number: str, prefix: str = "") -> str: """ Добавляет ссылку на таблицу с гиперссылкой Args: number: Номер таблицы prefix: Префикс перед ссылкой (например, "См. ") Returns: Текст ссылки на таблицу с гиперссылкой """ processor = self.get_reference_processor('table') if processor: ref_text = processor(number) p = self.document.add_paragraph() p.add_run(prefix) self._add_hyperlink(p, f"table_{number}", ref_text) return prefix + ref_text return f"{prefix}Таблица {number}" def _process_table(self, element: Dict[str, Any]) -> None: """Обрабатывает элемент таблицы""" data = element.get('data', []) headers = element.get('headers') caption = element.get('caption', '') # Генерируем уникальный идентификатор для таблицы table_number = self._get_element_number('table') bookmark_name = f"table_{table_number}" # Добавляем подпись таблицы caption_text = self._create_caption('table', caption) caption_para = self.document.add_paragraph(style='Caption GOST') self._add_bookmark(caption_para, bookmark_name) caption_para.add_run(caption_text) # ... остальной код обработки таблицы ... def _add_bookmark(self, paragraph, bookmark_name: str): """ Добавляет закладку в указанный параграф Args: paragraph: Параграф для добавления закладки bookmark_name: Имя закладки """ p = paragraph._p # Создаем начало закладки bookmark_start = OxmlElement('w:bookmarkStart') bookmark_start.set(qn('w:id'), '0') bookmark_start.set(qn('w:name'), bookmark_name) # Создаем конец закладки bookmark_end = OxmlElement('w:bookmarkEnd') bookmark_end.set(qn('w:id'), '0') # Добавляем закладку в начало параграфа p.insert(0, bookmark_start) p.append(bookmark_end) def _add_hyperlink(self, paragraph, bookmark_name: str, text: str): """ Добавляет гиперссылку на закладку Args: paragraph: Параграф для добавления гиперссылки bookmark_name: Имя закладки text: Текст ссылки Returns: Объект гиперссылки """ hyperlink = OxmlElement('w:hyperlink') hyperlink.set(qn('w:anchor'), bookmark_name) # Создаем стиль для гиперссылки run = OxmlElement('w:r') rPr = OxmlElement('w:rPr') # Синий цвет и подчеркивание (как в Word) color = OxmlElement('w:color') color.set(qn('w:val'), '0000FF') rPr.append(color) underline = OxmlElement('w:u') underline.set(qn('w:val'), 'single') rPr.append(underline) run.append(rPr) # Добавляем текст text_elm = OxmlElement('w:t') text_elm.text = text run.append(text_elm) hyperlink.append(run) paragraph._p.append(hyperlink) return hyperlink def _add_element_reference(self, element_type: str, number: str, text: str): """ Добавляет элемент в список для создания оглавления и ссылок Args: element_type: Тип элемента number: Номер элемента text: Текст элемента """ self._element_references.append(ElementReference( type=element_type, number=number, text=text )) def _process_heading(self, element: Dict[str, Any]) -> None: """Обрабатывает элемент заголовка""" level = element.get('level', 1) text = element.get('text', '') # Для заголовков уровня 1-4 используем специальные стили if 1 <= level <= 4: style_name = f'Heading {level} GOST' heading = self.document.add_heading(level=0) if style_name in self.document.styles: heading.style = style_name else: # Базовая настройка, если стиль не создан heading.alignment = WD_ALIGN_PARAGRAPH.CENTER if level == 1 else WD_ALIGN_PARAGRAPH.LEFT run = heading.add_run(text) run.font.size = Pt(16 if level == 1 else 14) run.font.bold = True # Добавляем текст heading.add_run(text) # Добавляем в список для оглавления self._add_element_reference('section', str(level), text) else: # Для других уровней просто добавляем текст как абзац p = self.document.add_paragraph() p.add_run(text).font.bold = True def _process_section(self, element: Dict[str, Any]) -> None: """Обрабатывает раздел (уровень 1)""" self._increment_section_counter(1) element['level'] = 1 element['text'] = f"{self._get_current_section_path()} {element.get('text', '')}" self._process_heading(element) def _process_subsection(self, element: Dict[str, Any]) -> None: """Обрабатывает подраздел (уровень 2)""" self._increment_section_counter(2) element['level'] = 2 element['text'] = f"{self._get_current_section_path()} {element.get('text', '')}" self._process_heading(element) def _process_subsubsection(self, element: Dict[str, Any]) -> None: """Обрабатывает подподраздел (уровень 3)""" self._increment_section_counter(3) element['level'] = 3 element['text'] = f"{self._get_current_section_path()} {element.get('text', '')}" self._process_heading(element) def add_footnote(self, paragraph, text: str) -> None: """ Добавляет сноску к указанному параграфу Args: paragraph: Параграф, к которому добавляется сноска text: Текст сноски """ footnote = self.document.part.add_footnote(text) footnote_ref = footnote.reference() paragraph._p.append(footnote_ref) def _process_paragraph(self, element: Dict[str, Any]) -> None: """Обрабатывает элемент абзаца с поддержкой сносок""" text = element.get('text', '') style = element.get('style') footnotes = element.get('footnotes', []) # Сноски в формате [{'position': индекс, 'text': текст}] p = self.document.add_paragraph() if style and style in self.document.styles: p.style = style # Основной текст по ГОСТ p.paragraph_format.line_spacing_rule = ( WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5 else WD_LINE_SPACING.DOUBLE ) p.paragraph_format.alignment = ( WD_ALIGN_PARAGRAPH.JUSTIFY if self.config.paragraph['alignment'] == 'justify' else WD_ALIGN_PARAGRAPH.LEFT ) p.paragraph_format.first_line_indent = Cm(self.config.paragraph['first_line_indent']) # Добавляем текст с поддержкой форматирования и сносок self._add_formatted_text_with_footnotes(p, text, footnotes) def _add_formatted_text_with_footnotes(self, paragraph, text: str, footnotes: List[Dict]) -> None: """ Добавляет текст с поддержкой базового форматирования и сносок Args: paragraph: Объект параграфа text: Текст с возможными маркерами форматирования footnotes: Список сносок """ # Сортируем сноски по позиции footnotes = sorted(footnotes, key=lambda x: x['position']) # Добавляем основной текст с разбиением на части для сносок current_pos = 0 footnote_index = 0 for footnote in footnotes: # Добавляем текст до сноски if footnote['position'] > current_pos: part = text[current_pos:footnote['position']] self._add_formatted_text(paragraph, part) # Добавляем маркер сноски run = paragraph.add_run(str(footnote_index + 1)) run.font.superscript = True run.font.size = Pt(10) # Добавляем саму сноску self.add_footnote(paragraph, footnote['text']) current_pos = footnote['position'] + 1 footnote_index += 1 # Добавляем остаток текста if current_pos < len(text): self._add_formatted_text(paragraph, text[current_pos:]) def _add_formatted_text(self, paragraph, text: str): """ Добавляет текст с поддержкой базового форматирования Args: paragraph: Объект параграфа text: Текст с возможными маркерами форматирования """ # Простая поддержка **жирного** и *курсива* parts = re.split(r'(\*\*.*?\*\*|\*.*?\*)', text) for part in parts: if part.startswith('**') and part.endswith('**'): run = paragraph.add_run(part[2:-2]) run.bold = True elif part.startswith('*') and part.endswith('*'): run = paragraph.add_run(part[1:-1]) run.italic = True else: paragraph.add_run(part) def _create_list_styles(self): """Создает стили для списков в соответствии с ГОСТ""" # Нумерованный список if 'Numbered List GOST' not in self.document.styles: num_style = self.document.styles.add_style('Numbered List GOST', WD_STYLE_TYPE.PARAGRAPH) num_style.base_style = self.document.styles['List Number'] num_style.font.name = self.config.font['name'] num_style.font.size = Pt(self.config.font['size']) num_style.paragraph_format.left_indent = Cm(1.25) num_style.paragraph_format.first_line_indent = Cm(-0.63) num_style.paragraph_format.space_before = Pt(0) num_style.paragraph_format.space_after = Pt(6) num_style.paragraph_format.line_spacing_rule = ( WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5 else WD_LINE_SPACING.DOUBLE ) # Маркированный список if 'Bulleted List GOST' not in self.document.styles: bullet_style = self.document.styles.add_style('Bulleted List GOST', WD_STYLE_TYPE.PARAGRAPH) bullet_style.base_style = self.document.styles['List Bullet'] bullet_style.font.name = self.config.font['name'] bullet_style.font.size = Pt(self.config.font['size']) bullet_style.paragraph_format.left_indent = Cm(1.25) bullet_style.paragraph_format.first_line_indent = Cm(-0.63) bullet_style.paragraph_format.space_before = Pt(0) bullet_style.paragraph_format.space_after = Pt(6) bullet_style.paragraph_format.line_spacing_rule = ( WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5 else WD_LINE_SPACING.DOUBLE ) def add_numbered_list(self, items: List[str], level: int = 0) -> None: """ Добавляет нумерованный список с правильной нумерацией по ГОСТ Args: items: Элементы списка level: Уровень вложенности списка """ for i, item in enumerate(items): p = self.document.add_paragraph(style='Numbered List GOST') p.paragraph_format.left_indent = Cm(1.25 + 0.63 * level) p.paragraph_format.first_line_indent = Cm(-0.63) p.add_run(f"{i + 1}. {item}") def add_bulleted_list(self, items: List[str], level: int = 0) -> None: """ Добавляет маркированный список Args: items: Элементы списка level: Уровень вложенности списка """ for item in items: p = self.document.add_paragraph(style='Bulleted List GOST') p.paragraph_format.left_indent = Cm(1.25 + 0.63 * level) p.paragraph_format.first_line_indent = Cm(-0.63) p.add_run(f"• {item}") def setup_headers_footers(self) -> None: """Настраивает колонтитулы по ГОСТ""" for section in self.document.sections: # Верхний колонтитул header = section.header header.is_linked_to_previous = False header_paragraph = header.paragraphs[0] if header.paragraphs else header.add_paragraph() header_paragraph.text = self.title header_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER header_paragraph.style = "Header" header_paragraph.runs[0].font.size = Pt(12) header_paragraph.runs[0].font.name = self.config.font['name'] # Нижний колонтитул с номером страницы footer = section.footer footer.is_linked_to_previous = False footer_paragraph = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph() footer_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER # Добавляем поле номера страницы page_run = footer_paragraph.add_run() fldChar = OxmlElement('w:fldChar') fldChar.set(qn('w:fldCharType'), 'begin') instrText = OxmlElement('w:instrText') instrText.set(qn('xml:space'), 'preserve') instrText.text = "PAGE" fldChar2 = OxmlElement('w:fldChar') fldChar2.set(qn('w:fldCharType'), 'separate') fldChar3 = OxmlElement('w:fldChar') fldChar3.set(qn('w:fldCharType'), 'end') page_run._r.append(fldChar) page_run._r.append(instrText) page_run._r.append(fldChar2) page_run._r.append(fldChar3) # Добавляем текст после номера страницы page_run = footer_paragraph.add_run() page_run.text = " стр. " page_run.font.size = Pt(10) def _create_appendix_styles(self): """Создает стили для приложений""" if 'Appendix Heading' not in self.document.styles: style = self.document.styles.add_style('Appendix Heading', WD_STYLE_TYPE.PARAGRAPH) style.font.name = self.config.font['name'] style.font.size = Pt(16) style.font.bold = True style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER style.paragraph_format.space_before = Pt(24) style.paragraph_format.space_after = Pt(12) def add_appendix(self, title: str, letter: str) -> None: """ Добавляет приложение с буквенной нумерацией Args: title: Название приложения letter: Буква приложения (А, Б, В и т.д.) """ # Добавляем разрыв страницы перед приложением self.document.add_page_break() # Заголовок "ПРИЛОЖЕНИЕ" appendix_heading = self.document.add_paragraph(style='Appendix Heading') appendix_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER appendix_heading.add_run(f"ПРИЛОЖЕНИЕ {letter}").font.all_caps = True # Название приложения appendix_title = self.document.add_paragraph(style='Appendix Heading') appendix_title.alignment = WD_ALIGN_PARAGRAPH.CENTER appendix_title.add_run(title).font.bold = False # Сохраняем информацию о приложении для оглавления self._add_element_reference('appendix', letter, title) def _get_appendix_reference(self, letter: str) -> str: """Возвращает текст ссылки на приложение""" return f"приложение {letter}" def _register_reference_processors(self): """Регистрирует обработчики для создания ссылок на элементы""" # ... существующие процессоры ... self.register_reference_processor('appendix', self._get_appendix_reference) def validate_gost_compliance(self) -> List[str]: """ Проверяет соответствие документа требованиям ГОСТ Returns: Список ошибок, если документ не соответствует ГОСТ """ errors = [] # Проверка полей страницы section = self.document.sections[0] cm_to_twips = 567 # 1 см = 567 twips if section.top_margin < Cm(2.0): errors.append("Верхнее поле должно быть не менее 2.0 см") if section.bottom_margin < Cm(2.0): errors.append("Нижнее поле должно быть не менее 2.0 см") if section.left_margin < Cm(2.5): errors.append("Левое поле должно быть не менее 2.5 см (ГОСТ 7.32-2017)") if section.right_margin < Cm(1.0): errors.append("Правое поле должно быть не менее 1.0 см") # Проверка шрифта style = self.document.styles['Normal'] if style.font.name not in ["Times New Roman", "TimesNewRoman"]: errors.append("Основной шрифт должен быть Times New Roman (ГОСТ 7.32-2017)") if style.font.size != Pt(14): errors.append("Основной размер шрифта должен быть 14 pt (ГОСТ 7.32-2017)") # Проверка межстрочного интервала if style.paragraph_format.line_spacing_rule != WD_LINE_SPACING.ONE_POINT_FIVE: errors.append("Межстрочный интервал должен быть 1.5 (ГОСТ 7.32-2017)") # Проверка выравнивания if style.paragraph_format.alignment != WD_ALIGN_PARAGRAPH.JUSTIFY: errors.append("Выравнивание текста должно быть по ширине (ГОСТ 7.32-2017)") # Проверка отступа первой строки if style.paragraph_format.first_line_indent != Cm(1.25): errors.append("Отступ первой строки должен быть 1.25 см (ГОСТ 7.32-2017)") return errors def setup_document_metadata(self) -> None: """Настраивает метаданные документа""" core_props = self.document.core_properties core_props.author = self.author core_props.title = self.title core_props.subject = "Документ, оформленный по ГОСТ" core_props.keywords = "ГОСТ, техническая документация, стандарт" core_props.comments = f"Создано с использованием GostEditor by Hlebushek {self._get_version()}" core_props.category = "Техническая документация" # Устанавливаем дату создания import datetime core_props.created = datetime.datetime.now() def _get_version(self) -> str: """Возвращает версию библиотеки""" return "1.0.0" # Можно заменить на реальную версию def _process_table(self, element: Dict[str, Any]) -> None: """Обрабатывает элемент таблицы""" data = element.get('data', []) headers = element.get('headers') caption = element.get('caption', '') # Добавляем подпись таблицы caption_text = self._create_caption('table', caption) caption_para = self.document.add_paragraph(style='Caption GOST') caption_para.add_run(caption_text) # Создаем таблицу rows = len(data) + (1 if headers else 0) cols = len(data[0]) if data else 0 if cols == 0: return table = self.document.add_table(rows=rows, cols=cols) table.style = 'Table Grid' # Настройка переноса таблицы на новую страницу if self.config.table['allow_breaking']: for row in table.rows: tr = row._tr trPr = tr.get_or_add_trPr() # Разрешить перенос строк таблицы trHeight = OxmlElement('w:trHeight') trHeight.set(qn('w:val'), "0") trHeight.set(qn('w:rule'), "auto") trPr.append(trHeight) # Запретить разрыв строк внутри ячейки gridBefore = OxmlElement('w:gridBefore') gridBefore.set(qn('w:val'), "0") trPr.append(gridBefore) # Заполняем заголовки, если они есть if headers: hdr_cells = table.rows[0].cells for i, header in enumerate(headers): hdr_cells[i].text = header for paragraph in hdr_cells[i].paragraphs: for run in paragraph.runs: run.font.bold = self.config.table['header_bold'] # Заполняем данные start_row = 1 if headers else 0 for i, row_data in enumerate(data): row_cells = table.rows[i + start_row].cells for j, cell_data in enumerate(row_data): row_cells[j].text = str(cell_data) # Настройка шрифта для всех ячеек for row in table.rows: for cell in row.cells: for paragraph in cell.paragraphs: for run in paragraph.runs: run.font.name = self.config.font['name'] run.font.size = Pt(self.config.table['font_size']) def _process_figure(self, element: Dict[str, Any]) -> None: """Обрабатывает элемент изображения""" path = element.get('path', '') caption = element.get('caption', '') width = element.get('width', None) # Добавляем подпись caption_text = self._create_caption('figure', caption) caption_para = self.document.add_paragraph(style='Caption GOST') caption_para.add_run(caption_text) # Добавляем изображение, если путь существует if path and os.path.exists(path): if width: self.document.add_picture(path, width=Cm(width)) else: self.document.add_picture(path) else: # Добавляем заглушку, если изображение не найдено p = self.document.add_paragraph() p.add_run("[Изображение не найдено]").font.italic = True def _process_listing(self, element: Dict[str, Any]) -> None: """Обрабатывает элемент программного кода""" code = element.get('code', '') caption = element.get('caption', '') language = element.get('language', '') # Добавляем подпись caption_text = self._create_caption('listing', caption) caption_para = self.document.add_paragraph(style='Caption GOST') caption_para.add_run(caption_text) # Добавляем код с фоном p = self.document.add_paragraph(style='Code GOST') # Добавляем фон для абзаца shading_elm = OxmlElement('w:shd') shading_elm.set(qn('w:fill'), f"{self.config.code['background_color'][0]:02X}" f"{self.config.code['background_color'][1]:02X}" f"{self.config.code['background_color'][2]:02X}") p.paragraph_format.element.get_or_add_pPr().append(shading_elm) # Добавляем код p.add_run(code) def _process_formula(self, element: Dict[str, Any]) -> None: """Обрабатывает элемент формулы""" formula = element.get('formula', '') caption = element.get('caption', '') # Добавляем формулу p = self.document.add_paragraph(style='Formula GOST') p.add_run(formula) # Добавляем номер формулы formula_number = self._get_element_number('formula') number_text = self.config.formula['number_format'].format(number=formula_number) p.add_run(f" {number_text}").font.size = Pt(self.config.formula['font_size'] - 2) # Добавляем описание, если есть if caption: desc_p = self.document.add_paragraph() desc_p.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER desc_p.add_run(caption).font.italic = True def _process_reference(self, element: Dict[str, Any]) -> None: """Обрабатывает элемент ссылки на источник""" author = element.get('author', '') title = element.get('title', '') year = element.get('year', '') publisher = element.get('publisher', '') pages = element.get('pages', '') # Сохраняем источник для дальнейшего использования self._references.append({ 'author': author, 'title': title, 'year': year, 'publisher': publisher, 'pages': pages }) def _process_toc(self, element: Dict[str, Any]) -> None: """Обрабатывает элемент оглавления""" # Добавляем заголовок heading = self.document.add_heading(level=0) heading.style = 'Heading 1 GOST' heading.alignment = WD_ALIGN_PARAGRAPH.CENTER heading.add_run("СОДЕРЖАНИЕ").font.all_caps = True # Добавляем отступ self.document.add_paragraph() # Создаем поле оглавления (TOC) paragraph = self.document.add_paragraph() paragraph_format = paragraph.paragraph_format paragraph_format.space_before = Pt(12) paragraph_format.space_after = Pt(24) run = paragraph.add_run() # Создаем XML для поля оглавления fldChar = OxmlElement('w:fldChar') fldChar.set(qn('w:fldCharType'), 'begin') instrText = OxmlElement('w:instrText') instrText.set(qn('xml:space'), 'preserve') # Параметры оглавления: уровень 1-3, гиперссылки, отступы instrText.text = 'TOC \\o "1-3" \\h \\z \\u' fldChar2 = OxmlElement('w:fldChar') fldChar2.set(qn('w:fldCharType'), 'separate') fldChar3 = OxmlElement('w:fldChar') fldChar3.set(qn('w:fldCharType'), 'end') # Добавляем все элементы в run run._r.append(fldChar) run._r.append(instrText) run._r.append(fldChar2) run._r.append(fldChar3) # Добавляем разделительный параграф self.document.add_paragraph() def _process_page_break(self, element: Dict[str, Any]) -> None: """Добавляет разрыв страницы""" self.document.add_page_break() def _get_table_reference(self, number: str) -> str: """Возвращает текст ссылки на таблицу""" return f"Таблица {number}" def _get_figure_reference(self, number: str) -> str: """Возвращает текст ссылки на рисунок""" return f"Рисунок {number}" def _get_listing_reference(self, number: str) -> str: """Возвращает текст ссылки на листинг""" return f"Листинг {number}" def _get_formula_reference(self, number: str) -> str: """Возвращает текст ссылки на формулу""" return f"формула ({number})" def _get_section_reference(self, number: str) -> str: """Возвращает текст ссылки на раздел""" return f"раздел {number}" def add_section(self, title: str) -> None: """ Добавляет раздел (уровень 1) Args: title: Название раздела """ self.add_element('section', text=title) def add_subsection(self, title: str) -> None: """ Добавляет подраздел (уровень 2) Args: title: Название подраздела """ self.add_element('subsection', text=title) def add_subsubsection(self, title: str) -> None: """ Добавляет подподраздел (уровень 3) Args: title: Название подподраздела """ self.add_element('subsubsection', text=title) def add_paragraph(self, text: str, style: Optional[str] = None) -> None: """ Добавляет абзац текста Args: text: Текст абзаца style: Стиль абзаца (опционально) """ self.add_element('paragraph', text=text, style=style) def add_table(self, data: List[List[Any]], headers: Optional[List[str]] = None, caption: str = "") -> None: """ Добавляет таблицу Args: data: Данные таблицы в виде списка списков headers: Заголовки таблицы caption: Подпись к таблице """ self.add_element('table', data=data, headers=headers, caption=caption) def add_figure(self, path: str, caption: str, width: Optional[float] = None) -> None: """ Добавляет изображение Args: path: Путь к файлу изображения caption: Подпись к изображению width: Ширина изображения в см (опционально) """ self.add_element('figure', path=path, caption=caption, width=width) def add_listing(self, code: str, caption: str, language: Optional[str] = None) -> None: """ Добавляет программный код Args: code: Текст программного кода caption: Подпись к коду language: Язык программирования (опционально) """ self.add_element('listing', code=code, caption=caption, language=language) def add_formula(self, formula: str, caption: Optional[str] = "") -> None: """ Добавляет формулу Args: formula: Текст формулы caption: Описание формулы (опционально) """ self.add_element('formula', formula=formula, caption=caption) def add_reference(self, author: str, title: str, year: str, publisher: Optional[str] = "", pages: Optional[str] = "") -> None: """ Добавляет источник в список литературы Args: author: Автор(ы) title: Название работы year: Год издания publisher: Издательство (опционально) pages: Страницы (опционально) """ self.add_element('reference', author=author, title=title, year=year, publisher=publisher, pages=pages) def add_table_reference(self, number: str, prefix: str = "") -> str: """ Добавляет ссылку на таблицу Args: number: Номер таблицы prefix: Префикс перед ссылкой (например, "См. ") Returns: Текст ссылки на таблицу """ processor = self.get_reference_processor('table') if processor: return f"{prefix}{processor(number)}" return f"{prefix}Таблица {number}" def add_figure_reference(self, number: str, prefix: str = "") -> str: """ Добавляет ссылку на рисунок Args: number: Номер рисунка prefix: Префикс перед ссылкой Returns: Текст ссылки на рисунок """ processor = self.get_reference_processor('figure') if processor: return f"{prefix}{processor(number)}" return f"{prefix}Рисунок {number}" def generate_toc(self) -> None: """Генерирует оглавление документа""" self.add_element('toc', type='toc') def add_page_break(self) -> None: """Добавляет разрыв страницы""" self.add_element('page_break') @abstractmethod def add_element(self, element_type: str, **kwargs) -> None: """ Абстрактный метод для добавления элемента в документ. Должен быть реализован в дочерних классах. Args: element_type: Тип элемента (heading, paragraph, table и т.д.) **kwargs: Параметры элемента """ pass def save(self, filename: str) -> None: """ Сохраняет документ в файл с обновлением полей Args: filename: Имя файла для сохранения """ compliance_errors = self.validate_gost_compliance() if compliance_errors: logger.warning(f"Документ не полностью соответствует ГОСТ: {', '.join(compliance_errors)}") # Настраиваем колонтитулы self.setup_headers_footers() # Обновляем поля перед сохранением self._update_fields() # Добавляем настройки совместимости try: self.document.settings.element.xpath('./w:compat')[0].clear() except (IndexError, AttributeError): pass self.document.save(filename) logger.info(f"Документ успешно сохранен как '{filename}'") def _update_fields(self): """Обновляет все поля в документе (оглавление, номера страниц и т.д.)""" # Добавляем команду обновления полей fldChar = OxmlElement('w:fldChar') fldChar.set(qn('w:fldCharType'), 'begin') instrText = OxmlElement('w:instrText') instrText.set(qn('xml:space'), 'preserve') instrText.text = 'TOC \\o "1-3" \\h \\z \\u' fldChar2 = OxmlElement('w:fldChar') fldChar2.set(qn('w:fldCharType'), 'separate') fldChar3 = OxmlElement('w:fldChar') fldChar3.set(qn('w:fldCharType'), 'end') # В реальности для автоматического обновления полей при открытии # нужно установить флаг, но python-docx не поддерживает это напрямую # Поэтому добавляем примечание в документ self.document.settings.set_qn('w:updateFields', 'true') def create_title_page(self) -> None: """Создает титульную страницу документа""" # Добавляем отступ сверху for _ in range(5): self.document.add_paragraph() # Заголовок title_paragraph = self.document.add_heading(level=0) title_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER title_run = title_paragraph.add_run(self.title) title_run.font.size = Pt(16) title_run.font.bold = True title_run.font.name = self.config.font['name'] # Добавляем отступ self.document.add_paragraph() # Автор author_paragraph = self.document.add_paragraph() author_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER author_run = author_paragraph.add_run(self.author) author_run.font.size = Pt(self.config.font['size']) author_run.font.name = self.config.font['name'] # Добавляем отступ снизу for _ in range(10): self.document.add_paragraph() # Дата date_paragraph = self.document.add_paragraph() date_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER date_run = date_paragraph.add_run("2023") date_run.font.size = Pt(self.config.font['size']) date_run.font.name = self.config.font['name'] # Устанавливаем титульную страницу как отдельный раздел self.document.sections[0].start_type = 2 # Новая страница def generate_references(self) -> None: """Генерирует список использованных источников в соответствии с ГОСТ""" if not self._references: return # Добавляем заголовок heading = self.document.add_heading(level=0) heading.style = 'Heading 1 GOST' heading.alignment = WD_ALIGN_PARAGRAPH.CENTER heading.add_run("СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ").font.all_caps = True # Добавляем отступ self.document.add_paragraph() # Сортируем источники в соответствии с выбранным стилем if self.config.references['sort_by'] == 'author': sorted_refs = sorted(self._references, key=lambda x: x['author'].lower()) else: # 'order' sorted_refs = self._references # Добавляем источники for i, ref in enumerate(sorted_refs, 1): p = self.document.add_paragraph(style='List Number') p.paragraph_format.left_indent = Cm(1.25) p.paragraph_format.first_line_indent = Cm(-0.63) # Форматируем источник согласно ГОСТ Р 7.0.5-2008 ref_text = self._format_reference(i, ref) p.add_run(ref_text) def _format_reference(self, index: int, ref: Dict) -> str: """ Форматирует источник в соответствии с ГОСТ Р 7.0.5-2008 Args: index: Порядковый номер источника ref: Данные источника Returns: Форматированный текст источника """ # Основные элементы parts = [ f"[{index}] {ref['author']}.", f"{ref['title']}." ] # Издательство и год if ref.get('publisher') and ref.get('year'): parts.append(f"— {ref['publisher']}, {ref['year']}.") elif ref.get('year'): parts.append(f"— {ref['year']}.") # Страницы if ref.get('pages'): parts.append(f"— С. {ref['pages']}.") return " ".join(parts)