Files
2025-09-15 07:48:37 +03:00

1551 lines
66 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)