Files
md_to_gost/mm3.py
2025-09-15 07:48:37 +03:00

2211 lines
94 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.
import math
from abc import ABC, abstractmethod
from turtle import pd
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.chart = kwargs.get('chart', {
'default_width': 15, # в см
'default_height': 10,
'style': 'seaborn', # Стиль matplotlib
'dpi': 300,
'color_scheme': 'default', # 'default', 'business', 'academic'
'font_size': 10 # Размер шрифта на графике
})
# Поддержка различных типов графиков
self.chart_types = kwargs.get('chart_types', {
'bar': {'orientation': 'vertical', 'stacked': False},
'line': {'markers': True, 'linestyle': '-'},
'pie': {'explode': 0.05, 'autopct': '%1.1f%%'},
'scatter': {'size': 50, 'alpha': 0.7},
'heatmap': {'cmap': 'viridis', 'annot': True}
})
# Стиль абзаца
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 SnippetType:
"""Типы сниппетов"""
STATIC = "static" # Статический текст
DYNAMIC = "dynamic" # Динамический контент (дата, время и т.д.)
SCRIPT_PYTHON = "script_python" # Python-скрипт
SCRIPT_LUA = "script_lua" # Lua-скрипт
SCRIPT_SIMPLE = "script_simple" # Простой язык (калькулятор выражений)
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()
self._plugins = {}
self._plugin_tags = {}
self._register_default_plugins()
# Регистрируем обработчик для замены тегов в тексте
self.register_text_processor(self._process_plugin_tags)
def register_plugin(self, name: str, plugin: Callable,
tag_pattern: Optional[str] = None) -> None:
"""
Регистрирует плагин для обработки тегов
Args:
name: Имя плагина
plugin: Функция-обработчик
tag_pattern: Шаблон тега (опционально)
"""
self._plugins[name] = plugin
# Автоматический шаблон, если не указан
if tag_pattern is None:
tag_pattern = f"\\{{function\\({name}\\),args\\(([^{{}}]*)\\)\\}}"
self._plugin_tags[name] = tag_pattern
def get_plugin(self, name: str) -> Optional[Callable]:
"""
Возвращает зарегистрированный плагин
Args:
name: Имя плагина
Returns:
Функция-обработчик или None
"""
return self._plugins.get(name)
def register_text_processor(self, processor: Callable) -> None:
"""
Регистрирует обработчик текста
Args:
processor: Функция для обработки текста
"""
if not hasattr(self, '_text_processors'):
self._text_processors = []
self._text_processors.append(processor)
def process_text(self, text: str) -> str:
"""
Обрабатывает текст через все зарегистрированные обработчики
Args:
text: Исходный текст
Returns:
Обработанный текст
"""
if not hasattr(self, '_text_processors'):
return text
for processor in self._text_processors:
text = processor(text)
return text
def _process_plugin_tags(self, text: str) -> str:
"""
Обрабатывает теги плагинов в тексте
Args:
text: Исходный текст
Returns:
Текст с обработанными тегами
"""
for plugin_name, pattern in self._plugin_tags.items():
def replace_match(match):
args_str = match.group(1)
args = self._parse_plugin_args(args_str)
plugin = self.get_plugin(plugin_name)
if plugin:
try:
# Указываем, что результат НЕ должен быть отдельным параграфом
args['as_paragraph'] = False
return plugin(**args)
except Exception as e:
logger.error(f"Ошибка при выполнении плагина {plugin_name}: {str(e)}")
return f"[Ошибка плагина: {str(e)}]"
return match.group(0)
text = re.sub(pattern, replace_match, text)
return text
def _plugin_current_date(self, format: str = "%d.%m.%Y", **kwargs) -> str:
"""Плагин для вставки текущей даты"""
from datetime import datetime
return datetime.now().strftime(format)
def _plugin_page_counter(self, **kwargs) -> str:
"""Плагин для отображения номера страницы (заглушка)"""
return "X" # Номер страницы будет обновлен при открытии документа
def _plugin_calc(self, expression: str = "", **kwargs) -> str:
"""Плагин для вычисления математических выражений"""
try:
# Используем простой скриптовый обработчик
return self._handle_simple_script(expression)
except Exception as e:
return f"[Ошибка вычисления: {str(e)}]"
def _plugin_gpt(self, prompt: str = "",
use_text_for_generate: bool = False,
model: str = "gpt-3.5-turbo",
**kwargs) -> str:
"""
Плагин для генерации текста через GPT
Args:
prompt: Текстовый запрос
use_text_for_generate: Использовать текст вокруг тега как часть запроса
model: Модель GPT
Returns:
Сгенерированный текст
"""
try:
# Имитация вызова API (в реальности здесь будет обращение к API)
if 'api_key' not in kwargs:
return "[Ошибка: отсутствует API-ключ для GPT]"
# Здесь должен быть вызов к API OpenAI
# Для примера вернем заглушку
return f"Сгенерированный текст на основе запроса: '{prompt}'"
except Exception as e:
return f"[Ошибка GPT: {str(e)}]"
def _plugin_table_of_contents(self, levels: int = 3, **kwargs) -> str:
"""Плагин для вставки оглавления"""
# Это заглушка, реальное оглавление генерируется отдельно
return "{Оглавление будет сгенерировано при сохранении документа}"
def _parse_plugin_args(self, args_str: str) -> Dict[str, str]:
"""
Парсит аргументы плагина из строки
Args:
args_str: Строка с аргументами
Returns:
Словарь с аргументами
"""
args = {}
if not args_str:
return args
# Парсим аргументы в формате key=value
for arg in args_str.split(','):
arg = arg.strip()
if '=' in arg:
key, value = arg.split('=', 1)
args[key.strip()] = value.strip()
else:
# Позиционные аргументы (не рекомендуется)
args[f"arg_{len(args) + 1}"] = arg
return args
def _register_default_plugins(self):
"""Регистрирует стандартные плагины"""
self.register_plugin('current_date', self._plugin_current_date)
self.register_plugin('page_counter', self._plugin_page_counter)
self.register_plugin('gpt', self._plugin_gpt)
self.register_plugin('calc', self._plugin_calc)
self.register_plugin('table_of_contents', self._plugin_table_of_contents)
@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)
self.register_processor('chart', self._process_chart)
self.register_processor('snippet', self._process_snippet)
self.register_processor('plugin', self._process_plugin)
# Регистрируем обработчики сниппетов
self.register_snippet_handler(SnippetType.STATIC, self._handle_static_snippet)
self.register_snippet_handler(SnippetType.DYNAMIC, self._handle_dynamic_snippet)
self.register_snippet_handler(SnippetType.SCRIPT_PYTHON, self._handle_python_script)
self.register_snippet_handler(SnippetType.SCRIPT_LUA, self._handle_lua_script)
self.register_snippet_handler(SnippetType.SCRIPT_SIMPLE, self._handle_simple_script)
def register_snippet_handler(self, snippet_type: str, handler: Callable) -> None:
"""
Регистрирует обработчик для определенного типа сниппета
Args:
snippet_type: Тип сниппета
handler: Функция-обработчик
"""
if not hasattr(self, '_snippet_handlers'):
self._snippet_handlers = {}
self._snippet_handlers[snippet_type] = handler
def add_snippet(self, snippet_type: str, content: str,
language: Optional[str] = None,
style: Optional[str] = None,
caption: Optional[str] = None) -> None:
"""
Добавляет сниппет в документ
Args:
snippet_type: Тип сниппета
content: Содержимое сниппета
language: Язык программирования (для скриптов)
style: Стиль оформления
caption: Подпись к сниппету
"""
self.add_element('snippet', snippet_type=snippet_type, content=content,
language=language, style=style, caption=caption)
def _process_snippet(self, element: Dict[str, Any]) -> None:
"""Обрабатывает элемент сниппета"""
snippet_type = element['snippet_type']
content = element['content']
language = element.get('language')
style = element.get('style')
caption = element.get('caption')
# Обрабатываем содержимое через соответствующий обработчик
handler = self.get_snippet_handler(snippet_type)
if handler:
try:
processed_content = handler(content, language=language)
except Exception as e:
logger.error(f"Ошибка обработки сниппета {snippet_type}: {str(e)}")
processed_content = f"[Ошибка обработки сниппета: {str(e)}]"
else:
processed_content = content
# Добавляем подпись, если есть
if caption:
caption_text = self._create_caption('listing', caption)
caption_para = self.document.add_paragraph(style='Caption GOST')
caption_para.add_run(caption_text)
# Добавляем содержимое сниппета
if snippet_type in [SnippetType.SCRIPT_PYTHON, SnippetType.SCRIPT_LUA, SnippetType.SCRIPT_SIMPLE]:
# Для скриптов используем стиль кода
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(processed_content)
else:
# Для других типов сниппетов просто добавляем текст
p = self.document.add_paragraph()
p.add_run(processed_content)
def add_plugin(self, name: str, as_paragraph: bool = True, **kwargs) -> None:
"""
Добавляет плагин в документ
Args:
name: Имя плагина
as_paragraph: Добавлять результат как отдельный параграф
**kwargs: Аргументы плагина
"""
self.add_element('plugin', name=name, args=kwargs, as_paragraph=as_paragraph)
def _process_plugin(self, element: Dict[str, Any]) -> None:
"""
Обрабатывает элемент плагина
Args:
element: Словарь с информацией о плагине
"""
plugin_name = element.get('name', '')
args = element.get('args', {})
# Получаем обработчик плагина
plugin = self.get_plugin(plugin_name)
if plugin:
try:
# Вызываем плагин с аргументами
result = plugin(**args)
# Добавляем результат в документ
if element.get('as_paragraph', True):
p = self.document.add_paragraph()
p.add_run(result)
else:
# Если нужно встроить в текущий параграф (например, через текстовый процессор)
# этот случай будет обработан текстовым процессором
pass
except Exception as e:
logger.error(f"Ошибка при выполнении плагина {plugin_name}: {str(e)}")
p = self.document.add_paragraph()
error_run = p.add_run(f"[Ошибка плагина {plugin_name}: {str(e)}]")
error_run.font.color.rgb = RGBColor(255, 0, 0)
error_run.font.italic = True
else:
logger.warning(f"Плагин '{plugin_name}' не зарегистрирован")
p = self.document.add_paragraph()
p.add_run(f"[Плагин '{plugin_name}' не найден]").font.italic = True
def _handle_static_snippet(self, content: str, **kwargs) -> str:
"""Обрабатывает статический сниппет"""
return content
def _handle_dynamic_snippet(self, content: str, **kwargs) -> str:
"""Обрабатывает динамический сниппет"""
from datetime import datetime
# Замена динамических тегов
replacements = {
'{current_date}': datetime.now().strftime("%d.%m.%Y"),
'{current_time}': datetime.now().strftime("%H:%M:%S"),
'{document_title}': self.title,
'{author}': self.author,
'{page_number}': "X", # Нельзя определить на этапе генерации
'{total_pages}': "Y" # Нельзя определить на этапе генерации
}
for tag, value in replacements.items():
content = content.replace(tag, value)
return content
def _handle_python_script(self, code: str, **kwargs) -> str:
"""Обрабатывает Python-скрипт в безопасной песочнице"""
try:
# Создаем безопасное окружение
safe_globals = {
'__builtins__': {
'True': True,
'False': False,
'None': None,
'abs': abs,
'chr': chr,
'dict': dict,
'float': float,
'int': int,
'len': len,
'list': list,
'map': map,
'max': max,
'min': min,
'pow': pow,
'range': range,
'round': round,
'str': str,
'sum': sum,
'tuple': tuple,
# Добавляем математические функции
'math': __import__('math', fromlist=['*'])
}
}
safe_locals = {}
# Выполняем код
exec(code, safe_globals, safe_locals)
# Возвращаем результат (последнее выражение, если есть)
if 'result' in safe_locals:
return str(safe_locals['result'])
elif len(safe_locals) > 0:
return str(list(safe_locals.values())[-1])
return "[Выполнено]"
except Exception as e:
return f"[Ошибка выполнения Python-скрипта: {str(e)}]"
def _handle_lua_script(self, code: str, **kwargs) -> str:
"""Обрабатывает Lua-скрипт (требует установленного lupa)"""
try:
from lupa import LuaRuntime
lua = LuaRuntime(unpack_returned_tuples=True)
# Создаем безопасное окружение Lua
lua.eval('''
-- Очищаем глобальное окружение
local safe_env = {
_G = {},
print = print,
tostring = tostring,
tonumber = tonumber,
string = string,
table = table,
math = math,
ipairs = ipairs,
pairs = pairs,
next = next,
select = select,
error = error,
assert = assert,
type = type,
pcall = pcall,
xpcall = xpcall
}
setmetatable(safe_env, {__index = _G})
setfenv(1, safe_env)
''')
# Выполняем код
result = lua.execute(code)
# Преобразуем результат в строку
if result is None:
return "[Выполнено]"
return str(result)
except ImportError:
return "[Ошибка: установите lupa для выполнения Lua-скриптов]"
except Exception as e:
return f"[Ошибка выполнения Lua-скрипта: {str(e)}]"
def _handle_simple_script(self, expression: str, **kwargs) -> str:
"""Обрабатывает простой язык (калькулятор выражений)"""
try:
# Поддерживаем базовые математические операции
expression = expression.replace('^', '**') # Заменяем ^ на **
# Безопасное вычисление выражения
result = eval(expression, {"__builtins__": None}, {
'math': __import__('math'),
'sin': math.sin,
'cos': math.cos,
'tan': math.tan,
'sqrt': math.sqrt,
'pow': math.pow,
'pi': math.pi,
'e': math.e
})
return str(result)
except Exception as e:
return f"[Ошибка вычисления: {str(e)}]"
def get_snippet_handler(self, snippet_type: str) -> Optional[Callable]:
"""
Возвращает обработчик для указанного типа сниппета
Args:
snippet_type: Тип сниппета
Returns:
Обработчик или None, если не найден
"""
return self._snippet_handlers.get(snippet_type, None)
def _process_chart(self, element: Dict[str, Any]) -> None:
"""Обрабатывает элемент графика с автоматической генерацией"""
try:
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import tempfile
import numpy as np
# Настройка стиля matplotlib
plt.style.use(self.config.chart['style'])
matplotlib.rcParams.update({
'font.size': self.config.chart['font_size'],
'figure.dpi': self.config.chart['dpi']
})
# Подготовка данных
data = element['data']
chart_type = element['chart_type']
title = element.get('title', '')
x_label = element.get('x_label', '')
y_label = element.get('y_label', '')
width = element['width'] # в см
height = element['height'] # в см
# Преобразуем размеры в дюймы (1 дюйм = 2.54 см)
width_in = width / 2.54
height_in = height / 2.54
# Создаем график
plt.figure(figsize=(width_in, height_in))
# Настройка цветовой схемы
if self.config.chart['color_scheme'] == 'business':
palette = sns.color_palette("pastel")
elif self.config.chart['color_scheme'] == 'academic':
palette = sns.color_palette("muted")
else:
palette = None
# Построение графика в зависимости от типа
if chart_type == 'bar':
if isinstance(data, dict):
if 'orientation' in element and element['orientation'] == 'horizontal':
plt.barh(list(data.keys()), list(data.values()), color=palette)
else:
plt.bar(data.keys(), data.values(), color=palette)
elif isinstance(data, list):
plt.bar(range(len(data)), data, color=palette)
elif isinstance(data, pd.DataFrame):
if 'x' in element and 'y' in element:
sns.barplot(x=element['x'], y=element['y'], data=data, palette=palette)
else:
data.plot(kind='bar', palette=palette)
elif chart_type == 'line':
if isinstance(data, dict):
plt.plot(data.keys(), data.values(),
marker='o' if element.get('markers', True) else '',
linestyle=element.get('linestyle', '-'))
elif isinstance(data, list):
plt.plot(data,
marker='o' if element.get('markers', True) else '',
linestyle=element.get('linestyle', '-'))
elif isinstance(data, pd.DataFrame):
if 'x' in element and 'y' in element:
plt.plot(data[element['x']], data[element['y']],
marker='o' if element.get('markers', True) else '',
linestyle=element.get('linestyle', '-'))
else:
data.plot(kind='line')
elif chart_type == 'pie':
if isinstance(data, dict):
plt.pie(data.values(), labels=data.keys(),
explode=[element.get('explode', 0.05)] * len(data),
autopct=element.get('autopct', '%1.1f%%'),
startangle=90)
elif isinstance(data, pd.DataFrame) and 'labels' in element:
plt.pie(data[element['values']], labels=data[element['labels']],
explode=[element.get('explode', 0.05)] * len(data),
autopct=element.get('autopct', '%1.1f%%'),
startangle=90)
elif chart_type == 'scatter':
if isinstance(data, pd.DataFrame) and 'x' in element and 'y' in element:
sns.scatterplot(x=element['x'], y=element['y'], data=data,
size=element.get('size', 50),
alpha=element.get('alpha', 0.7),
palette=palette)
else:
raise ValueError("Для scatter-графика требуется DataFrame с указанием x и y")
elif chart_type == 'heatmap':
if isinstance(data, pd.DataFrame):
sns.heatmap(data,
cmap=element.get('cmap', 'viridis'),
annot=element.get('annot', True),
fmt=element.get('fmt', '.2f'))
else:
raise ValueError("Для heatmap требуется DataFrame")
else:
raise ValueError(f"Неподдерживаемый тип графика: {chart_type}")
# Добавляем заголовок и метки осей
plt.title(title, fontsize=self.config.chart['font_size'] + 2)
if x_label:
plt.xlabel(x_label)
if y_label:
plt.ylabel(y_label)
# Делаем график более читаемым
plt.tight_layout()
# Сохраняем во временный файл
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
plt.savefig(tmp.name, bbox_inches='tight', dpi=self.config.chart['dpi'])
tmp_path = tmp.name
plt.close()
# Сохраняем путь к временному файлу для последующей очистки
if not hasattr(self, '_temp_files'):
self._temp_files = []
self._temp_files.append(tmp_path)
# Добавляем график как изображение с подписью
self.add_figure(tmp_path, caption, width=width)
except ImportError:
# Обработка отсутствия необходимых библиотек
p = self.document.add_paragraph()
p.add_run("[График не может быть сгенерирован: установите matplotlib и seaborn]").font.italic = True
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
except Exception as e:
logger.error(f"Ошибка при генерации графика: {str(e)}")
p = self.document.add_paragraph()
p.add_run(f"[Ошибка при генерации графика: {str(e)}]").font.color.rgb = RGBColor(255, 0, 0)
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
def add_chart(self, data: Union[List, Dict, pd.DataFrame],
chart_type: str = 'bar', title: str = "",
x_label: str = "", y_label: str = "",
width: Optional[float] = None,
height: Optional[float] = None,
caption: str = "", **kwargs) -> None:
"""
Добавляет график в документ с автоматической генерацией
Args:
data: Данные для построения графика (список, словарь или DataFrame)
chart_type: Тип графика (bar, line, pie, scatter, heatmap)
title: Заголовок графика
x_label: Название оси X
y_label: Название оси Y
width: Ширина графика в см (опционально)
height: Высота графика в см (опционально)
caption: Подпись к графику
**kwargs: Дополнительные параметры для конкретного типа графика
"""
# Используем настройки по умолчанию из конфигурации
width = width or self.config.chart['default_width']
height = height or self.config.chart['default_height']
self.add_element('chart', data=data, chart_type=chart_type, title=title,
x_label=x_label, y_label=y_label, width=width,
height=height, caption=caption, **kwargs)
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')
# Обрабатываем текст через все зарегистрированные обработчики
processed_text = self.process_text(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(p, processed_text)
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)