1551 lines
66 KiB
Python
1551 lines
66 KiB
Python
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) |