commit d2da20fdb212fb8df7b7d42f1daa8b6282e4f2e6 Author: Igor20264 Date: Mon Sep 15 07:48:37 2025 +0300 First Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1be18a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +.venv +*.docx \ No newline at end of file diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 0000000..900fa43 --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,209 @@ +# GhostEditor +Программа для конвертации из формата `md` в формат `docx`(x это без макросов, m с макросами) (`Office Open XML`),`odt`(`OpenDocument`) + +#### **1. python-docx** +**Назначение**: Создание и модификация Word-документов. +**Ключевые возможности**: +- Добавление/редактирование текста, таблиц, изображений. +- Управление стилями, колонтитулами, сносками. +- Работа с XML через абстракции (Paragraph, Run, Table). + +**Пример**: +```python +from docx import Document +from docx.shared import Pt + +doc = Document() +paragraph = doc.add_paragraph("Hello ") +run = paragraph.add_run("OOXML!") +run.bold = True +run.font.size = Pt(14) + +# Таблица +table = doc.add_table(rows=2, cols=2) +table.cell(0, 0).text = "Cell A1" + +doc.save("report.docx") +``` + +**Плюсы**: +- Интуитивный API, близкий к логике Word. +- Поддержка всех ключевых частей пакета (headers, footers, styles). + +**Минусы**: +- Нет поддержки макросов. +- Ограниченная работа с графикой (изображения добавляются, но редактирование сложно). + +**Ссылка**: [python-docx.readthedocs.io](https://python-docx.readthedocs.io/) + +--- + +#### **2. docxtpl** +**Назначение**: Генерация документов по шаблонам (Jinja2). +**Особенности**: +- Шаблоны .docx с переменными (`{{ variable }}`) и логикой (`{% if %}`). +- Поддержка таблиц, изображений, HTML-фрагментов. + +**Пример**: +```python +from docxtpl import DocxTemplate + +doc = DocxTemplate("template.docx") +context = { + "title": "Отчет за 2023", + "items": [{"name": "Item 1"}, {"name": "Item 2"}] +} +doc.render(context) +doc.save("output.docx") +``` + +**Плюсы**: +- Идеален для шаблонизированных отчетов (договоры, счета). +- Интеграция с Django/Flask. + +**Минусы**: +- Требует предварительной настройки шаблонов в Word. + +**Ссылка**: [github.com/elapouya/python-docx-template](https://github.com/elapouya/python-docx-template) + +--- + +#### **3. mammoth** +**Назначение**: Конвертация .docx → HTML с сохранением структуры. +**Особенности**: +- Преобразует стили Word в CSS-классы. +- Подходит для извлечения контента из сложных документов. + +**Пример**: +```python +import mammoth + +with open("document.docx", "rb") as docx_file: + result = mammoth.convert_to_html(docx_file) + html = result.value # HTML-строка + warnings = result.messages # Предупреждения +``` + +**Плюсы**: +- Сохраняет семантику (заголовки, списки, таблицы). +- Легко интегрируется с парсерами (BeautifulSoup). + +**Минусы**: +- Не подходит для обратной конвертации (HTML → docx). + +**Ссылка**: [github.com/mwilliamson/mammoth.py](https://github.com/mwilliamson/mammoth.py) + +--- + +### **Низкоуровневые инструменты (для кастомных задач)** + +#### **1. zipfile + lxml** +**Назначение**: Прямой доступ к XML-структуре OOXML. +**Когда использовать**: +- При работе с нестандартными частями пакета (напр., кастомные XML-маппинги в Excel). +- Для восстановления поврежденных файлов. + +**Пример для Excel**: +```python +import zipfile +from lxml import etree + +# Распаковываем workbook.xml +with zipfile.ZipFile("report.xlsx") as xlsx: + xml_data = xlsx.read("xl/workbook.xml") + +# Парсим XML +workbook = etree.fromstring(xml_data) +namespaces = {"ns": "http://schemas.../spreadsheetml/2006/main"} +sheets = workbook.findall(".//ns:sheet", namespaces) + +# Меняем имя листа +sheets[0].set("{http://.../officeDocument/2006/relationships}name", "New Sheet") + +# Сохраняем изменения +with zipfile.ZipFile("fixed.xlsx", "w") as new_xlsx: + for file in xlsx.namelist(): + if file != "xl/workbook.xml": + new_xlsx.writestr(file, xlsx.read(file)) + new_xlsx.writestr("xl/workbook.xml", etree.tostring(workbook)) +``` + +**Плюсы**: +- Полный контроль над структурой. +- Работает с любыми частями пакета. + +**Минусы**: +- Требует знания XML-схем OOXML. +- Риск повреждения файла при ошибках. + +--- + +#### **2. officedoc** +**Назначение**: Универсальный доступ к форматам Office (docx, xlsx, pptx). +**Особенности**: +- Объединяет функционал `python-docx`, `openpyxl`, `python-pptx` в едином API. +- Упрощает обработку мультимедийных вложений. + +**Пример**: +```python +from officedoc import Document + +doc = Document("mixed.docx") +for image in doc.images: + image.save("extracted_" + image.filename) + +for table in doc.tables: + print(table.to_dataframe()) # Конвертация в pandas DataFrame +``` + +**Ссылка**: [github.com/mikem1701/officedoc](https://github.com/mikem1701/officedoc) + +--- + +### **Сравнение библиотек** + +| **Библиотека** | **Скорость** | **Сложность** | **Особенности** | +|----------------------|-------------|---------------|----------------------------| +| **python-docx** | Средняя | Средняя | Стандарт для работы с Word | +| **docxtpl** | Средняя | Низкая | Шаблонизация через Jinja2 | +| **zipfile + lxml** | Высокая | Высокая | Полный контроль над XML | + +--- + +#### Для Word (.docx) +- **Простое создание документов**: `python-docx`. +- **Шаблонные документы** (договоры, счета): `docxtpl`. +- **Извлечение контента**: `mammoth` → обработка HTML через BeautifulSoup. +- **Редактирование структуры**: `python-docx` + `zipfile` для кастомных частей. + +#### Для кастомных задач +- **Восстановление файлов**: `zipfile` + `lxml` (ручное исправление XML). +- **Интеграция с внешними системами**: `openpyxl`/`python-docx` + REST API (напр., выгрузка данных из БД в Excel). + +--- + +### Типичные проблемы и решения + +2. Стили не применяются в Word + - Проблема: `python-docx` использует стили из `styles.xml`, но не создает новые автоматически. + - Решение: + ```python + from docx.shared import Pt + style = doc.styles.add_style("Heading3", WD_STYLE_TYPE.PARAGRAPH) + font = style.font + font.name = "Arial" + font.size = Pt(12) + ``` + +--- + +### **Заключение** +Для 90% задач достаточно: +- **Excel**: `openpyxl` или `pandas`. +- **Word**: `python-docx` + `docxtpl`. + +**Используйте низкоуровневые методы (zipfile/lxml) только когда**: + +*Для углубленного изучения:* +- [Open XML SDK 2.5](https://learn.microsoft.com/ru-ru/office/open-xml/open-xml-sdk) (официальная документация Microsoft). +- [ECMA-376 стандарт](https://www.ecma-international.org/publications-and-standards/standards/ecma-376/) (полная спецификация). \ No newline at end of file diff --git a/asd.py b/asd.py new file mode 100644 index 0000000..cd6cd0d --- /dev/null +++ b/asd.py @@ -0,0 +1,317 @@ +from docx import Document +from docx.shared import Pt, Cm, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING +from docx.enum.style import WD_STYLE_TYPE +from docx.oxml.ns import qn +from docx.oxml import OxmlElement + + +def create_gost_document(title="Документ", author="Автор"): + """ + Создает новый документ Word с оформлением по ГОСТ + + Args: + title (str): Заголовок документа + author (str): Автор документа + + Returns: + Document: Объект документа с настроенными стилями ГОСТ + """ + doc = Document() + + # Настройка полей страницы согласно ГОСТ + sections = doc.sections + for section in sections: + section.top_margin = Cm(2) # Верхнее поле 2 см + section.bottom_margin = Cm(2) # Нижнее поле 2 см + section.left_margin = Cm(3) # Левое поле 3 см + section.right_margin = Cm(1.5) # Правое поле 1.5 см + + # Установка стандартного стиля для всего документа + style = doc.styles['Normal'] + font = style.font + font.name = 'Times New Roman' + font.size = Pt(14) + # Установка кириллического шрифта для документов на русском языке + font.element.rPr.rFonts.set(qn('w:eastAsia'), 'Times New Roman') + + # Настройка формата абзаца + paragraph_format = style.paragraph_format + paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE # Полуторный интервал + paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY # Выравнивание по ширине + paragraph_format.first_line_indent = Cm(1.25) # Красная строка 1.25 см + + # Создаем заголовок документа + title_paragraph = doc.add_heading(level=0) + title_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER + title_run = title_paragraph.add_run(title) + title_run.font.size = Pt(16) + title_run.font.bold = True + title_run.font.name = 'Times New Roman' + + # Добавляем имя автора + author_paragraph = doc.add_paragraph() + author_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER + author_run = author_paragraph.add_run(f"\n{author}") + author_run.font.size = Pt(14) + author_run.font.name = 'Times New Roman' + + # Добавляем отступ после заголовка + doc.add_paragraph() + + # Создаем стили заголовков в соответствии с ГОСТ + create_gost_heading_styles(doc) + + return doc + + +def create_gost_heading_styles(doc): + """ + Создает стили заголовков в соответствии с требованиями ГОСТ + + Args: + doc (Document): Объект документа + """ + # Заголовок 1 уровня + h1_style = doc.styles.add_style('Heading 1 GOST', WD_STYLE_TYPE.PARAGRAPH) + h1_style.base_style = doc.styles['Heading 1'] + h1_style.font.name = 'Times New Roman' + h1_style.font.size = Pt(16) + h1_style.font.bold = True + h1_style.font.all_caps = True # В ГОСТ заголовки часто пишутся прописными + h1_style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER + h1_style.paragraph_format.space_before = Pt(24) # Отступ перед заголовком + h1_style.paragraph_format.space_after = Pt(12) # Отступ после заголовка + h1_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE + + # Заголовок 2 уровня + h2_style = doc.styles.add_style('Heading 2 GOST', WD_STYLE_TYPE.PARAGRAPH) + h2_style.base_style = doc.styles['Heading 2'] + h2_style.font.name = 'Times New Roman' + h2_style.font.size = Pt(14) + h2_style.font.bold = True + h2_style.paragraph_format.space_before = Pt(24) + h2_style.paragraph_format.space_after = Pt(6) + h2_style.paragraph_format.first_line_indent = Cm(0) # Без красной строки + h2_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE + + # Заголовок 3 уровня + h3_style = doc.styles.add_style('Heading 3 GOST', WD_STYLE_TYPE.PARAGRAPH) + h3_style.base_style = doc.styles['Heading 3'] + h3_style.font.name = 'Times New Roman' + h3_style.font.size = Pt(14) + h3_style.font.bold = True + h3_style.font.italic = True + h3_style.paragraph_format.space_before = Pt(18) + h3_style.paragraph_format.space_after = Pt(6) + h3_style.paragraph_format.first_line_indent = Cm(0) + h3_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE + + # Стиль для списка + list_style = doc.styles.add_style('List GOST', WD_STYLE_TYPE.PARAGRAPH) + list_style.base_style = doc.styles['List'] + list_style.font.name = 'Times New Roman' + list_style.font.size = Pt(14) + 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 + + # Стиль для подпись таблиц и рисунков + caption_style = doc.styles.add_style('Caption GOST', WD_STYLE_TYPE.PARAGRAPH) + caption_style.base_style = doc.styles['Caption'] + caption_style.font.name = 'Times New Roman' + caption_style.font.size = Pt(12) + caption_style.font.italic = True + caption_style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER + caption_style.paragraph_format.space_before = Pt(6) + caption_style.paragraph_format.space_after = Pt(12) + + +def add_gost_heading(doc, text, level=1): + """ + Добавляет заголовок с оформлением по ГОСТ + + Args: + doc (Document): Объект документа + text (str): Текст заголовка + level (int): Уровень заголовка (1, 2 или 3) + + Returns: + Paragraph: Объект параграфа с заголовком + """ + if level == 1: + style_name = 'Heading 1 GOST' + elif level == 2: + style_name = 'Heading 2 GOST' + elif level == 3: + style_name = 'Heading 3 GOST' + else: + raise ValueError("Уровень заголовка должен быть 1, 2 или 3") + + # Проверяем, существует ли стиль, если нет - создаем стили ГОСТ + if style_name not in doc.styles: + create_gost_heading_styles(doc) + + heading = doc.add_heading(text, level=0) + heading.style = style_name + return heading + + +def add_gost_paragraph(doc, text, style=None): + """ + Добавляет абзац с оформлением по ГОСТ + + Args: + doc (Document): Объект документа + text (str): Текст абзаца + style (str, optional): Название стиля + + Returns: + Paragraph: Объект параграфа + """ + p = doc.add_paragraph() + + if style and style in doc.styles: + p.style = style + + # Основной текст по ГОСТ + p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE + p.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY + p.paragraph_format.first_line_indent = Cm(1.25) + + # Добавляем текст + p.add_run(text) + + return p + + +def add_gost_table(doc, data, headers=None): + """ + Создает таблицу в формате ГОСТ + + Args: + doc (Document): Объект документа + data (list): Данные таблицы в виде списка списков + headers (list, optional): Заголовки таблицы + + Returns: + Table: Объект таблицы + """ + # Добавляем подпись таблицы + caption = doc.add_paragraph() + caption.style = 'Caption GOST' + caption.add_run("Таблица ").bold = True + + # Создаем таблицу + rows = len(data) + (1 if headers else 0) + cols = len(data[0]) if data else 0 + table = doc.add_table(rows=rows, cols=cols) + + # Стиль таблицы + table.style = 'Table Grid' + + # Заполняем заголовки, если они есть + 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 = True + + # Заполняем данные + 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 = 'Times New Roman' + run.font.size = Pt(12) + + return table + + +def save_gost_document(doc, filename): + """ + Сохраняет документ с настройками совместимости для MS Word + + Args: + doc (Document): Объект документа + filename (str): Имя файла для сохранения + """ + # Добавляем настройки совместимости + doc.settings.element.xpath('./w:compat')[0].clear() + + # Сохраняем документ + doc.save(filename) + + +# Пример использования +if __name__ == "__main__": + # Создаем документ по ГОСТ + document = create_gost_document("Отчет о научно-исследовательской работе", "Иванов И.И.") + + # Добавляем раздел с заголовком 1 уровня + add_gost_heading(document, "ВВЕДЕНИЕ", level=1) + + # Добавляем текст введение + intro_text = ("Настоящий отчет посвящен исследованию актуальных проблем в области " + "разработки программного обеспечения. Цель работы - анализ и разработка " + "методов оптимизации процесса создания программных продуктов. В ходе " + "исследования были применены методы системного анализа, математического " + "моделирования и экспериментальные методы.") + add_gost_paragraph(document, intro_text) + + # Добавляем раздел с заголовком 2 уровня + add_gost_heading(document, "1. АНАЛИЗ ПРЕДМЕТНОЙ ОБЛАСТИ", level=2) + + # Добавляем текст раздела + analysis_text = ("В данном разделе представлен анализ современных подходов к разработке " + "программного обеспечения. Проведенный анализ показал, что основными " + "проблемами являются недостаточная модульность архитектуры и слабая " + "документация проектов.") + add_gost_paragraph(document, analysis_text) + + # Добавляем подраздел с заголовком 3 уровня + add_gost_heading(document, "1.1. Методы исследования", level=3) + + # Добавляем нумерованный список + methods = [ + "Анализ существующих решений в предметной области", + "Моделирование архитектуры программной системы", + "Экспериментальная проверка предложенных методов" + ] + + for method in methods: + p = document.add_paragraph(method, style='List GOST') + p.style = 'List GOST' + p.paragraph_format.left_indent = Cm(1.25) + p.paragraph_format.first_line_indent = Cm(-0.63) # Отрицательный отступ для номера + + # Добавляем таблицу + headers = ["Метод", "Достоинства", "Недостатки"] + data = [ + ["Метод 1", "Высокая точность", "Сложность реализации"], + ["Метод 2", "Простота", "Низкая точность"], + ["Метод 3", "Универсальность", "Высокие требования к ресурсам"] + ] + add_gost_table(document, data, headers) + + # Добавляем заключение + add_gost_heading(document, "ЗАКЛЮЧЕНИЕ", level=1) + conclusion_text = ("В результате проведенного исследования были выявлены основные проблемы " + "в области разработки программного обеспечения. Предложенные методы " + "позволяют повысить качество создаваемых программных продуктов и сократить " + "время на их разработку.") + add_gost_paragraph(document, conclusion_text) + + # Сохраняем документ + save_gost_document(document, "otchet_po_GOST.docx") + print("Документ успешно создан и сохранен как 'otchet_po_GOST.docx'") \ No newline at end of file diff --git a/image.png b/image.png new file mode 100644 index 0000000..af88dbe Binary files /dev/null and b/image.png differ diff --git a/image2.jpg b/image2.jpg new file mode 100644 index 0000000..06d0b4e Binary files /dev/null and b/image2.jpg differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..26b73bb --- /dev/null +++ b/main.py @@ -0,0 +1,32 @@ +# открытие файла +# установка нужных шрифтов в документе 1. +# работа с текстом +# работа с таблицами +# -поддержка переноса таблицы +# сохранение +# Настройка оглавлений + + +from docx import Document + +# Создаем новый документ +document = Document() + +# Добавляем заголовок +document.add_heading('Пример документа Word', level=1) + +# Добавляем параграф с текстом +document.add_paragraph('Это первый параграф, который был добавлен с помощью Python.') + +# Добавляем еще один параграф +document.add_paragraph('Это второй параграф. Можно добавлять новый текст, как вам нужно.') + +# Добавляем текст в уже существующий параграф +paragraph = document.add_paragraph('А это текст, добавленный к ') +paragraph.add_run('существующему ').italic = True +paragraph.add_run('параграфу.') + +# Сохраняем документ +document.save('мой_документ.docx') + +print("Документ 'мой_документ.docx' успешно создан и текст добавлен.") \ No newline at end of file diff --git a/main12.py b/main12.py new file mode 100644 index 0000000..72d6713 --- /dev/null +++ b/main12.py @@ -0,0 +1,915 @@ +from abc import ABC, abstractmethod +from typing import Dict, List, Any, Optional, Union, Callable +from docx import Document +from docx.shared import Pt, Cm, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING +from docx.enum.style import WD_STYLE_TYPE +from docx.oxml.ns import qn +from docx.oxml import OxmlElement +import re + + +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} + }) + + # Стили заголовков + 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'} + }) + + # Стиль абзаца + self.paragraph = kwargs.get('paragraph', { + 'first_line_indent': 1.25, # в см + 'alignment': 'justify' + }) + + # Стиль таблиц + self.table = kwargs.get('table', { + 'font_size': 12, + 'header_bold': True + }) + + # Стиль подписей + self.caption = kwargs.get('caption', { + 'font_size': 12, + 'italic': True, + 'align': 'center' + }) + + +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._setup_document() + self._create_gost_styles() + + # Регистрируем базовые процессоры элементов + self.register_processor('heading', self._process_heading) + self.register_processor('paragraph', self._process_paragraph) + self.register_processor('table', self._process_table) + self.register_processor('list', self._process_list) + self.register_processor('image', self._process_image) + + 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 уровня + self._create_heading_style(1) + # Заголовок 2 уровня + self._create_heading_style(2) + # Заголовок 3 уровня + self._create_heading_style(3) + + # Стиль для списка + 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 + ) + + # Стиль для подписей таблиц и рисунков + 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_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[level] + style.font.name = self.config.font['name'] + style.font.size = Pt(heading_config['size']) + style.font.bold = heading_config['bold'] + style.font.italic = heading_config.get('italic', False) + style.font.all_caps = heading_config['all_caps'] + + # Настройка абзаца + spacing = self.config.heading_spacing[level] + 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 + ) + + # Выравнивание + if heading_config['align'] == 'center': + style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER + elif heading_config['align'] == 'left': + style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT + style.paragraph_format.first_line_indent = Cm(0) # Без красной строки для заголовков + + 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 _process_heading(self, element: Dict[str, Any]) -> None: + """Обрабатывает элемент заголовка""" + level = element.get('level', 1) + text = element.get('text', '') + + heading = self.document.add_heading(level=0) + style_name = f'Heading {level} GOST' + + 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) + + def _process_paragraph(self, element: Dict[str, Any]) -> None: + """Обрабатывает элемент абзаца""" + text = element.get('text', '') + style = element.get('style') + + 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']) + print(text) + # Добавляем текст + p.add_run(text) + + def _process_table(self, element: Dict[str, Any]) -> None: + """Обрабатывает элемент таблицы""" + data = element.get('data', []) + headers = element.get('headers') + + # Добавляем подпись таблицы + caption = self.document.add_paragraph() + caption.style = 'Caption GOST' + caption.add_run("Таблица ").bold = True + + # Создаем таблицу + 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 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_list(self, element: Dict[str, Any]) -> None: + """Обрабатывает элемент списка""" + items = element.get('items', []) + list_type = element.get('type', 'unordered') # 'unordered' или 'ordered' + + for item in items: + p = self.document.add_paragraph(style='List GOST') + p.paragraph_format.left_indent = Cm(1.25) + p.paragraph_format.first_line_indent = Cm(-0.63) if list_type == 'ordered' else Cm(0) + + if list_type == 'ordered': + # Добавляем номер вручную для упорядоченного списка + p.add_run(f"{items.index(item) + 1}. ").bold = True + + p.add_run(item) + + def _process_image(self, element: Dict[str, Any]) -> None: + """Обрабатывает элемент изображения (заглушка для будущей реализации)""" + # В реальной реализации здесь будет добавление изображения + path = element.get('path', '') + width = element.get('width', None) + + # Добавляем подпись + caption = self.document.add_paragraph() + caption.style = 'Caption GOST' + caption.add_run("Рисунок ").bold = True + + # В реальной реализации здесь будет добавление изображения + # if path and os.path.exists(path): + # if width: + # self.document.add_picture(path, width=Cm(width)) + # else: + # self.document.add_picture(path) + + @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: Имя файла для сохранения + """ + # Добавляем настройки совместимости + try: + self.document.settings.element.xpath('./w:compat')[0].clear() + except (IndexError, AttributeError): + pass + + self.document.save(filename) + + 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 # Новая страница + + +class MarkdownGostDocument(GostDocumentCore): + """ + Расширенная реализация для обработки Markdown и преобразования в документ по ГОСТ + """ + + def __init__(self, config: Optional[GostDocumentConfig] = None, + title: str = "", author: str = ""): + super().__init__(config, title, author) + self._markdown_processors = {} + self._setup_markdown_processors() + + def _setup_markdown_processors(self): + """Настраивает обработчики для различных элементов Markdown""" + # Заголовки + self.register_markdown_processor(r'#{1,6}\s+(.+)', self._process_md_heading) + # Обычный текст + self.register_markdown_processor(r'^(?!#{1,6}\s+)(?!-{3,})(?!#{1,6}$)(?!```)(?!!\[.*\]\(.*\)).+', + self._process_md_paragraph) + # Горизонтальная линия + self.register_markdown_processor(r'^-{3,}$', self._process_md_horizontal_rule) + # Списки + self.register_markdown_processor(r'^(\*|\-|\d+\.)\s+(.+)$', self._process_md_list) + # Таблицы (упрощенная обработка) + self.register_markdown_processor(r'^\|.*\|$', self._process_md_table) + # Изображения + self.register_markdown_processor(r'^!\[(.*)\]\((.*)\)$', self._process_md_image) + # Код + self.register_markdown_processor(r'^```(.*)$', self._process_md_code) + + def register_markdown_processor(self, pattern: str, processor: Callable): + """ + Регистрирует обработчик для определенного паттерна Markdown + + Args: + pattern: Регулярное выражение для паттерна + processor: Функция-обработчик + """ + self._markdown_processors[pattern] = processor + + def _process_md_heading(self, match: re.Match, line: str) -> Dict[str, Any]: + """Обрабатывает заголовок Markdown""" + heading_text = match.group(1).strip() + level = line.count('#') + # Ограничиваем уровень заголовка от 1 до 3 (согласно ГОСТ) + level = min(max(level, 1), 3) + return {'type': 'heading', 'level': level, 'text': heading_text} + + def _process_md_paragraph(self, match: re.Match, line: str) -> Dict[str, Any]: + """Обрабатывает обычный текст Markdown""" + return {'type': 'paragraph', 'text': line.strip()} + + def _process_md_horizontal_rule(self, match: re.Match, line: str) -> Dict[str, Any]: + """Обрабатывает горизонтальную линию Markdown""" + # В ГОСТ горизонтальные линии не используются, поэтому просто пропускаем + return None + + def _process_md_list(self, match: re.Match, line: str) -> Dict[str, Any]: + """Обрабатывает список Markdown""" + items = [line.strip()] + list_type = 'ordered' if re.match(r'^\d+\.', line) else 'unordered' + return {'type': 'list', 'items': items, 'type': list_type} + + def _process_md_table(self, match: re.Match, line: str) -> Dict[str, Any]: + """Обрабатывает таблицу Markdown (заглушка)""" + # Полная обработка таблиц требует анализа нескольких строк + return None # Будет обработано в полной реализации + + def _process_md_image(self, match: re.Match, line: str) -> Dict[str, Any]: + """Обрабатывает изображение Markdown""" + alt_text = match.group(1) + image_path = match.group(2) + return {'type': 'image', 'path': image_path, 'alt': alt_text} + + def _process_md_code(self, match: re.Match, line: str) -> Dict[str, Any]: + """Обрабатывает блок кода Markdown""" + # Код в ГОСТ обычно оформляется как отдельный раздел или сноска + return {'type': 'paragraph', 'text': f"Код: {line}", 'style': 'Code'} + + def add_element(self, element_type: str, **kwargs) -> None: + """ + Добавляет элемент в документ согласно ГОСТ + + Args: + element_type: Тип элемента (heading, paragraph, table и т.д.) + **kwargs: Параметры элемента + """ + processor = self.get_processor(element_type) + if processor: + element = {'type': element_type, **kwargs} + processor(element) + + def process_markdown(self, markdown_text: str) -> None: + """ + Обрабатывает Markdown текст и добавляет элементы в документ + + Args: + markdown_text: Текст в формате Markdown + """ + lines = markdown_text.split('\n') + i = 0 + while i < len(lines): + line = lines[i].strip() + if not line: + i += 1 + continue + + processed = False + for pattern, processor in self._markdown_processors.items(): + match = re.match(pattern, line) + if match: + element = processor(match, line) + if element: + self.add_element(element['type'], **{k: v for k, v in element.items() if k != 'type'}) + processed = True + break + + if not processed: + # Если строка не соответствует ни одному паттерну, обрабатываем как обычный текст + self.add_element('paragraph', text=line) + + i += 1 + + def process_markdown_file(self, file_path: str) -> None: + """ + Обрабатывает файл Markdown и добавляет элементы в документ + + Args: + file_path: Путь к файлу Markdown + """ + with open(file_path, 'r', encoding='utf-8') as f: + markdown_text = f.read() + self.process_markdown(markdown_text) + + +# Пример использования +if __name__ == "__main__": + # Создаем кастомную конфигурацию (можно оставить стандартную) + custom_config = GostDocumentConfig( + margins={ + 'top': 2.0, + 'bottom': 2.0, + 'left': 3.0, + 'right': 1.5 + }, + font={ + 'name': 'Times New Roman', + 'size': 14 + }, + line_spacing=1.5, + heading_spacing={ + 1: {'before': 24, 'after': 12}, + 2: {'before': 24, 'after': 6}, + 3: {'before': 18, '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'} + } + ) + + # Создаем документ с поддержкой Markdown + doc = MarkdownGostDocument( + config=custom_config, + title="Отчет о научно-исследовательской работе", + author="Иванов И.И., группа 123" + ) + + # Добавляем титульную страницу + doc.create_title_page() + + # Пример обработки Markdown + markdown_example = """ +## Практическая работа 6 (часть 2) +**Тема. Нелинейные структуры данных. Бинарное дерево.** + +--- + +### 1. Условие задания и варианта +**Вариант 27** +**Значение информационной части:** Содержит символьное значение и счетчик (подсчитывает, сколько раз в дерево вставлялся символ). +**Операции варианта:** +1. Модифицировать алгоритм вставки ключа в дерево в соответствии со структурой узла. +2. Определить символ, который встретился большее количество раз. +3. Определить количество цифр в дереве, используя алгоритм обхода в ширину. + +--- + +### 2. Свойства дерева варианта +- **Тип дерева:** Бинарное дерево поиска (БДП) без операции удаления. +- **Структура узла:** + ```cpp + struct Node { + char symbol; // Символ + int count; // Счетчик вхождений + Node* left; // Указатель на левое поддерево + Node* right; // Указатель на правое поддерево + }; + ``` +- **Особенности:** + - При вставке символа: + - Если символ уже существует в дереве, увеличивается его `count`. + - Иначе создается новый узел с `count = 1`. + - Упорядочение узлов: По ASCII-коду символов (меньшие символы — слева, большие — справа). + - Операция удаления не реализована (требования варианта 24–30). + +--- + +### 3. Тестовые примеры +#### Пример 1: Вставка символов `a`, `a`, `b`, `3`, `3`, `3` +**Структура дерева:** +``` + b(1) + a(2) + 3(3) +``` +**Пояснение:** +- `a` вставлен 2 раза → `count = 2`. +- `3` вставлен 3 раза → `count = 3` (самый частый символ). +- Количество цифр: 3 (только символ `3`). + +#### Пример 2: Вставка символов `5`, `5`, `a`, `5` +**Структура дерева:** +``` + a(1) + 5(3) +``` +**Пояснение:** +- `5` вставлен 3 раза → `count = 3` (самый частый символ). +- Количество цифр: 3. + +--- + +### 4. Описание функций + +#### 4.1. Модифицированная вставка символа +**Задача:** Добавить символ в БДП, увеличивая счетчик при совпадении. +**Предусловие:** Дерево инициализировано. +**Постусловие:** Символ добавлен или обновлен в дереве. +**Прототип:** +```cpp +void BinaryTree::insert(char symbol); +``` +**Алгоритм:** +1. Начать с корня. +2. Пока текущий узел не `nullptr`: + - Если символ совпадает с текущим узлом → увеличить `count` и завершить. + - Если символ меньше текущего → перейти в левое поддерево. + - Иначе → перейти в правое поддерево. +3. Если узел не найден → создать новый узел с `count = 1`. + +--- + +#### 4.2. Поиск самого частого символа +**Задача:** Найти символ с максимальным значением `count`. +**Предусловие:** Дерево не пустое. +**Постусловие:** Возвращен символ и его счетчик. +**Прототип:** +```cpp +std::pair BinaryTree::findMostFrequent(); +``` +**Алгоритм:** +1. Инициализировать `maxCount = 0`, `maxSymbol = '\\0'`. +2. Обойти все узлы (инордер-обход): + - Для каждого узла сравнить `count` с `maxCount`. + - Если `count > maxCount` → обновить `maxSymbol` и `maxCount`. +3. Вернуть пару `(maxSymbol, maxCount)`. + +--- + +#### 4.3. Подсчет цифр в дереве (обход в ширину) +**Задача:** Посчитать общее количество вхождений цифровых символов (`0`–`9`). +**Предусловие:** Дерево инициализировано. +**Постусловие:** Возвращено число — сумма `count` для всех цифровых символов. +**Прототип:** +```cpp +int BinaryTree::countDigits(); +``` +**Алгоритм:** +1. Инициализировать очередь и счетчик `total = 0`. +2. Поместить корень в очередь. +3. Пока очередь не пуста: + - Извлечь узел из очереди. + - Если символ узла — цифра (`'0'`–`'9'`) → добавить `count` к `total`. + - Поместить левый и правый дочерние узлы в очередь. +4. Вернуть `total`. + +--- + +#### 4.4. Отображение дерева +**Задача:** Вывести структуру дерева на экран (повернутое на 90°). +**Предусловие:** Дерево инициализировано. +**Постусловие:** Дерево отображено в консоли. +**Прототип:** +```cpp +void BinaryTree::displayTree(); +void BinaryTree::printTree(Node* node, int space); +``` +**Алгоритм:** +1. Рекурсивно обойти дерево в порядке: **правое поддерево → корень → левое поддерево**. +2. Для каждого уровня увеличивать отступ (на 10 пробелов). +3. Вывести символ и `count` с текущим отступом. + +--- + +### 5. Код программы +```cpp +#include +#include +#include +using namespace std; + +struct Node { + char symbol; + int count; + Node* left; + Node* right; + Node(char s) : symbol(s), count(1), left(nullptr), right(nullptr) {} +}; + +class BinaryTree { +private: + Node* root; + + void printTree(Node* node, int space) { + if (node == nullptr) return; + space += 10; + printTree(node->right, space); + cout << endl; + for (int i = 10; i < space; i++) cout << " "; + cout << node->symbol << "(" << node->count << ")"; + printTree(node->left, space); + } + +public: + BinaryTree() : root(nullptr) {} + + void insert(char symbol) { + Node** current = &root; + while (*current != nullptr) { + if (symbol == (*current)->symbol) { + (*current)->count++; + return; + } else if (symbol < (*current)->symbol) { + current = &((*current)->left); + } else { + current = &((*current)->right); + } + } + *current = new Node(symbol); + } + + pair findMostFrequent() { + if (root == nullptr) return make_pair('\\0', 0); + char maxSymbol = '\\0'; + int maxCount = 0; + stack s; + Node* current = root; + while (current != nullptr || !s.empty()) { + while (current != nullptr) { + s.push(current); + current = current->left; + } + current = s.top(); + s.pop(); + if (current->count > maxCount) { + maxCount = current->count; + maxSymbol = current->symbol; + } + current = current->right; + } + return make_pair(maxSymbol, maxCount); + } + + int countDigits() { + if (root == nullptr) return 0; + int total = 0; + queue q; + q.push(root); + while (!q.empty()) { + Node* current = q.front(); + q.pop(); + if (current->symbol >= '0' && current->symbol <= '9') { + total += current->count; + } + if (current->left) q.push(current->left); + if (current->right) q.push(current->right); + } + return total; + } + + void displayTree() { + printTree(root, 0); + cout << "\n\n"; + } +}; + +int main() { + BinaryTree tree; + int choice; + char symbol; + + while (true) { + cout << "\n1. Insert symbol\n"; + cout << "2. Display tree\n"; + cout << "3. Find most frequent symbol\n"; + cout << "4. Count digits\n"; + cout << "5. Exit\n"; + cout << "Enter choice: "; + cin >> choice; + + switch (choice) { + case 1: + cout << "Enter symbol: "; + cin >> symbol; + tree.insert(symbol); + break; + case 2: + cout << "Tree structure:\n"; + tree.displayTree(); + break; + case 3: { + auto result = tree.findMostFrequent(); + if (result.second == 0) { + cout << "Tree is empty.\n"; + } else { + cout << "Most frequent symbol: " << result.first + << " (count: " << result.second << ")\n"; + } + break; + } + case 4: + cout << "Number of digits: " << tree.countDigits() << endl; + break; + case 5: + return 0; + default: + cout << "Invalid choice.\n"; + } + } +} +``` + +--- + +### 6. Скриншоты тестирования +#### Тест 1: Вставка `a`, `a`, `b`, `3`, `3`, `3` +![Тест 1](https://i.imgur.com/1aXJz9l.png) +- **Вывод дерева:** Корректно отображена структура с `3(3)` как самым частым символом. +- **Количество цифр:** 3 (символ `3` вставлен 3 раза). + +#### Тест 2: Вставка `5`, `5`, `a`, `5` +![Тест 2](https://i.imgur.com/5GkR7yP.png) +- **Самый частый символ:** `5` (count=3). +- **Количество цифр:** 3. + +--- + +### 7. Ответы на контрольные вопросы +1. **Степень дерева** определяется максимальным количеством дочерних узлов у любого узла. +2. **Сильноветвящееся дерево** — дерево, где степень каждого узла равна максимальной степени дерева. +3. **Путь в дереве** — последовательность узлов от корня к целевому узлу. +4. **Длина пути** — количество ребер в пути (на 1 меньше количества узлов). +5. **Степень бинарного дерева** равна 2 (максимум 2 дочерних узла на узел). +6. **Пустое дерево** допустимо (корень = `nullptr`). +7. **Бинарное дерево** — дерево, где каждый узел имеет не более 2 дочерних узлов. +8. **Алгоритм обхода** используется для последовательного доступа ко всем узлам. +9. **Высота дерева:** + ``` + height(nullptr) = -1 + height(node) = 1 + max(height(node->left), height(node->right)) + ``` +10. **Дерево по таблице:** + ![Дерево](https://i.imgur.com/mVcRf0D.png) +11. **Порядки обхода:** + - Прямой (КЛП): 18 → 12 → 7 → 4 → 10 → 2 → 5 → 21 → 15 → 14 + - Симметричный (ЛКП): 7 → 12 → 2 → 10 → 5 → 18 → 21 → 4 → 14 → 15 + - Обратный (ЛПК): 7 → 2 → 5 → 10 → 12 → 21 → 4 → 15 → 14 → 18 +12. **Обход в ширину** использует **очередь**. +13. **Пример обхода в ширину:** 18 → 12 → 14 → 7 → 10 → 15 → 4 → 2 → 5 → 21. +14. **Не рекурсивный обход в глубину** использует **стек**. +15. **Обход дерева выражения `(5+3)*((1+2)/3)`:** + - Прямой: `* + 5 3 / + 1 2 3` + - Симметричный: `5 + 3 * 1 + 2 / 3` + - Обратный: `5 3 + 1 2 + 3 / *` +16. **Деревья выражений:** + - `a+b-c*d+e` → `+ - + a b * c d e` + - `/a-b*c d` → Ошибка в выражении (некорректный формат). + - `a b c d / - *` → `* a - b / c d` + - `* - / + a b c d e` → `* - / + a b c d e` +17. **Обход в ширину со стеком** даст порядок, аналогичный **обратному обходу в глубину**. +18. **БДП для симметричного обхода `40 45 46 50 65 70 75`:** + ![БДП](https://i.imgur.com/3kLmF9A.png) +19. **Исходное дерево для прямого обхода `50 45 35 15 40 46 65 75 70`:** + ![Дерево](https://i.imgur.com/8W0jKQv.png) +20. **БДП после вставки `1, 48, 75, 100`:** + ![Дерево](https://i.imgur.com/5GkR7yP.png) + +--- + +**Вывод:** Реализованы все операции для бинарного дерева поиска с модифицированной структурой узла. Программа корректно обрабатывает вставку, поиск самого частого символа и подсчет цифр через обход в ширину. Тесты подтверждают работоспособность алгоритмов.. + """ + # Обрабатываем Markdown + doc.process_markdown(markdown_example) + + # Сохраняем документ + doc.save("./asdotchet_po_GOST.docx") + print("Документ успешно создан и сохранен как 'otchet_po_GOST.docx'") \ No newline at end of file diff --git a/mm2.py b/mm2.py new file mode 100644 index 0000000..98f857a --- /dev/null +++ b/mm2.py @@ -0,0 +1,1551 @@ +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) \ No newline at end of file diff --git a/mm3.py b/mm3.py new file mode 100644 index 0000000..86102b4 --- /dev/null +++ b/mm3.py @@ -0,0 +1,2210 @@ +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) diff --git a/mm4.py b/mm4.py new file mode 100644 index 0000000..44c481c --- /dev/null +++ b/mm4.py @@ -0,0 +1,241 @@ +# md_to_docx_gost_final.py +from docx import Document +from docx.shared import Pt, Mm, RGBColor, Inches +from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING +from docx.oxml.ns import qn +from docx.oxml import OxmlElement +from docx.enum.table import WD_TABLE_ALIGNMENT +import re +import requests +from io import BytesIO + +# Счётчик для листингов +listing_counter = 0 + + +def setup_gost_style(doc): + style = doc.styles['Normal'] + font = style.font + font.name = 'Times New Roman' + font.size = Pt(14) + font.color.rgb = RGBColor(0, 0, 0) # Чёрный цвет + + rFonts = style.element.rPr.get_or_add_rFonts() + rFonts.set(qn('w:eastAsia'), 'Times New Roman') + rFonts.set(qn('w:cs'), 'Times New Roman') + + pf = style.paragraph_format + pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY + pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE + pf.first_line_indent = Mm(1.25) + pf.space_before = Pt(0) + pf.space_after = Pt(0) + + section = doc.sections[0] + section.top_margin = Mm(20) + section.bottom_margin = Mm(20) + section.left_margin = Mm(30) + section.right_margin = Mm(15) + + +def add_heading(doc, text, level=1): + heading = doc.add_heading(text, level=level) + if level <= 3: + heading.alignment = WD_ALIGN_PARAGRAPH.CENTER + for run in heading.runs: + run.font.bold = True + run.font.size = Pt(16) if level == 1 else Pt(14) + run.font.color.rgb = RGBColor(0, 0, 0) + + +def add_listing_caption(doc, language="C++"): + global listing_counter + listing_counter += 1 + p = doc.add_paragraph(f"Листинг {listing_counter} — Код на {language}", style='Caption') + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + p.style.font.color.rgb = RGBColor(0, 0, 0) + for run in p.runs: + run.font.color.rgb = RGBColor(0, 0, 0) + + +def add_code_block_as_table(doc, code_lines, language=""): + global listing_counter + add_listing_caption(doc, language) # Подпись ДО таблицы + + table = doc.add_table(rows=1, cols=1) + table.style = 'Table Grid' + table.alignment = WD_TABLE_ALIGNMENT.CENTER + cell = table.cell(0, 0) + + # Серый фон + shading_elm = parse_xml(f'') + cell._tc.get_or_add_tcPr().append(shading_elm) + + paragraph = cell.paragraphs[0] + run = paragraph.add_run() + run.font.name = 'Courier New' + run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Courier New') + run.font.size = Pt(12) + run.font.color.rgb = RGBColor(0, 0, 0) + run.text = "\n".join(code_lines).strip() + + # Запрещаем разрыв строки внутри первой строки таблицы + tr = table.rows[0]._tr + trPr = tr.get_or_add_trPr() + cantSplit = OxmlElement('w:cantSplit') + cantSplit.set(qn('w:val'), 'true') + trPr.append(cantSplit) + + +def add_image_from_url(doc, url, caption_text): + try: + response = requests.get(url.strip(), timeout=5) + image_stream = BytesIO(response.content) + p = doc.add_picture(image_stream, width=Inches(5)) + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + + # Подпись к рисунку + caption = doc.add_paragraph(f"Рисунок — {caption_text}", style='Caption') + caption.alignment = WD_ALIGN_PARAGRAPH.CENTER + for run in caption.runs: + run.font.color.rgb = RGBColor(0, 0, 0) + except Exception as e: + print(f"⚠️ Ошибка загрузки изображения: {url} — {e}") + doc.add_paragraph(f"[Изображение не загружено: {caption_text}]").alignment = WD_ALIGN_PARAGRAPH.CENTER + + +def parse_xml(xml): + from docx.oxml import parse_xml as ox_parse + return ox_parse(xml) + + +def nsdecls(*namespaces): + return ' '.join(f'xmlns:{ns}="http://schemas.openxmlformats.org/wordprocessingml/2006/main"' for ns in namespaces) + + +def add_formatted_paragraph(doc, text): + p = doc.add_paragraph() + p.style = 'Normal' + + parts = re.split(r'(\*\*[^*]+\*\*)', text) + for part in parts: + if part.startswith('**') and part.endswith('**'): + clean_text = part[2:-2] + run = p.add_run(clean_text) + run.bold = True + run.font.color.rgb = RGBColor(0, 0, 0) + elif part.strip(): + run = p.add_run(part) + run.font.color.rgb = RGBColor(0, 0, 0) + + +def parse_markdown_and_fill(doc, md_text): + global listing_counter + lines = md_text.split('\n') + i = 0 + in_code_block = False + code_buffer = [] + code_language = "" + + while i < len(lines): + line = lines[i].rstrip() + + # === Заголовки 1–4 === + if line.startswith('#### '): + if in_code_block: + add_code_block_as_table(doc, code_buffer, code_language) + in_code_block = False + code_buffer = [] + add_heading(doc, line[5:].strip(), level=4) + + elif line.startswith('### '): + if in_code_block: + add_code_block_as_table(doc, code_buffer, code_language) + in_code_block = False + code_buffer = [] + add_heading(doc, line[4:].strip(), level=3) + + elif line.startswith('## '): + if in_code_block: + add_code_block_as_table(doc, code_buffer, code_language) + in_code_block = False + code_buffer = [] + add_heading(doc, line[3:].strip(), level=2) + + elif line.startswith('# '): + if in_code_block: + add_code_block_as_table(doc, code_buffer, code_language) + in_code_block = False + code_buffer = [] + add_heading(doc, line[2:].strip(), level=1) + + # === Горизонтальная линия === + elif line.strip() == '---': + doc.add_paragraph().add_run().add_break() + + # === Блок кода ``` === + elif line.startswith('```'): + if not in_code_block: + in_code_block = True + code_language = line[3:].strip() or "C++" + code_buffer = [] + else: + in_code_block = False + add_code_block_as_table(doc, code_buffer, code_language) + code_buffer = [] + + elif in_code_block: + code_buffer.append(line) + + # === Изображения: ![Описание](url) === + elif re.match(r'^!\[.*?\]\(.*?\)$', line.strip()): + match = re.match(r'^!\[(.*?)\]\((.*?)\)$', line.strip()) + if match: + caption = match.group(1) + url = match.group(2) + add_image_from_url(doc, url, caption) + + # === Маркированный список === + elif line.startswith('- '): + p = doc.add_paragraph(line[2:], style='List Bullet') + for run in p.runs: + run.font.color.rgb = RGBColor(0, 0, 0) + + # === Обычный абзац с жирным === + else: + if line.strip(): + if '**' in line: + add_formatted_paragraph(doc, line) + else: + p = doc.add_paragraph(line) + p.style = 'Normal' + for run in p.runs: + run.font.color.rgb = RGBColor(0, 0, 0) + + i += 1 + + # Остаток кода + if in_code_block and code_buffer: + add_code_block_as_table(doc, code_buffer, code_language) + + +def main(): + try: + with open(r"C:\Users\hlebushek\Desktop\Пример.txt", "r", encoding="utf-8") as f: + md_content = f.read() + except FileNotFoundError: + print("Файл 'Пример.txt' не найден.") + return + + doc = Document() + setup_gost_style(doc) + parse_markdown_and_fill(doc, md_content) + + output_filename = "Практическая_работа_6.docx" + doc.save(output_filename) + print(f"✅ Готово! Файл сохранён как '{output_filename}'") + print(f"📎 Включены: изображения, чёрный текст, листинги перед кодом, нумерация.") + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/mm5.py b/mm5.py new file mode 100644 index 0000000..742cd39 --- /dev/null +++ b/mm5.py @@ -0,0 +1,284 @@ +# md_to_docx_gost_final.py +from docx import Document +from docx.shared import Pt, Mm, RGBColor, Inches +from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING +from docx.oxml.ns import qn +from docx.oxml import OxmlElement +from docx.enum.table import WD_TABLE_ALIGNMENT +import json +import re +import requests +from io import BytesIO + +import parser + +# Счётчик листингов +listing_counter = 0 + + +def setup_gost_style(doc): + style = doc.styles['Normal'] + font = style.font + font.name = 'Times New Roman' + font.size = Pt(14) + font.color.rgb = RGBColor(0, 0, 0) # Чёрный цвет + + rFonts = style.element.rPr.get_or_add_rFonts() + rFonts.set(qn('w:eastAsia'), 'Times New Roman') + rFonts.set(qn('w:cs'), 'Times New Roman') + + pf = style.paragraph_format + pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY + pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE + pf.first_line_indent = Mm(1.25) + pf.space_before = Pt(0) + pf.space_after = Pt(0) + + section = doc.sections[0] + section.top_margin = Mm(20) + section.bottom_margin = Mm(20) + section.left_margin = Mm(30) + section.right_margin = Mm(15) + + +def add_heading(doc, text, level=1): + heading = doc.add_heading(text, level=level) + if level <= 3: + heading.alignment = WD_ALIGN_PARAGRAPH.CENTER + for run in heading.runs: + run.font.bold = True + run.font.size = Pt(16) if level == 1 else Pt(14) + run.font.color.rgb = RGBColor(0, 0, 0) + + +def add_listing_caption(doc, language="", is_continuation=False): + global listing_counter + p = doc.add_paragraph(style='Caption') + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + run = p.add_run() + run.font.color.rgb = RGBColor(0, 0, 0) + run.font.name = 'Times New Roman' + run._element.rPr.rFonts.set(qn('w:cs'), 'Times New Roman') + + if is_continuation: + run.text = f"Продолжение листинга {listing_counter} — Код на {language}" + else: + listing_counter += 1 + run.text = f"Листинг {listing_counter} — Код на {language}" + + +def add_code_block(doc, code_lines, language=""): + global listing_counter + add_listing_caption(doc, language, is_continuation=False) + + # Создаём таблицу для кода с выравниванием по левому краю + table = doc.add_table(rows=0, cols=1) + table.style = 'Table Grid' + table.alignment = WD_TABLE_ALIGNMENT.LEFT # Важно: выравнивание по левому краю + + # Добавляем каждую строку кода как отдельную строку таблицы + for line in code_lines: + row = table.add_row() + cell = row.cells[0] + cell.text = line.strip() + + # Серый фон для ячейки + shading_elm = parse_xml(f'') + cell._tc.get_or_add_tcPr().append(shading_elm) + + # Настройка параграфа внутри ячейки + for paragraph in cell.paragraphs: + paragraph_format = paragraph.paragraph_format + paragraph_format.left_indent = Mm(0) # Убираем левый отступ + paragraph_format.right_indent = Mm(0) + paragraph_format.first_line_indent = Mm(0) + paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT # Выравнивание по левому краю + + for run in paragraph.runs: + run.font.name = 'Courier New' + run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Courier New') + run.font.size = Pt(12) + run.font.color.rgb = RGBColor(0, 0, 0) + + # Добавляем подпись о продолжении, если код длинный + if len(code_lines) > 25: + p = doc.add_paragraph() + run = p.add_run(f"Продолжение листинга {listing_counter} — Код на {language}") + run.italic = True + run.font.size = Pt(12) + run.font.color.rgb = RGBColor(80, 80, 80) + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + p.space_before = Pt(6) + p.space_after = Pt(6) + + +def add_image_from_url(doc, url, caption_text): + try: + response = requests.get(url.strip(), timeout=5) + image_stream = BytesIO(response.content) + p = doc.add_picture(image_stream, width=Inches(5)) + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + + caption = doc.add_paragraph(f"Рисунок — {caption_text}", style='Caption') + caption.alignment = WD_ALIGN_PARAGRAPH.CENTER + for run in caption.runs: + run.font.color.rgb = RGBColor(0, 0, 0) + except Exception as e: + print(f"⚠️ Ошибка загрузки изображения: {url} — {e}") + doc.add_paragraph(f"[Изображение не загружено: {caption_text}]").alignment = WD_ALIGN_PARAGRAPH.CENTER + + +def parse_xml(xml): + from docx.oxml import parse_xml as ox_parse + return ox_parse(xml) + + +def nsdecls(*namespaces): + return ' '.join(f'xmlns:{ns}="http://schemas.openxmlformats.org/wordprocessingml/2006/main"' for ns in namespaces) + + +def process_element(doc, element, depth=0): + """Рекурсивная обработка элемента с учётом вложенности""" + global listing_counter + + if element['type'] == 'blank_line': + return + + elif element['type'] == 'heading': + level = element['attrs']['level'] + text = ''.join(extract_text(element['children'])) + add_heading(doc, text, level=level) + + elif element['type'] == 'paragraph': + p = doc.add_paragraph() + p.style = 'Normal' + + # Обработка вложенных элементов в параграфе + for child in element['children']: + if child['type'] == 'text': + p.add_run(child['raw']) + elif child['type'] == 'strong': + text = ''.join(extract_text(child['children'])) + run = p.add_run(text) + run.bold = True + elif child['type'] == 'codespan': + run = p.add_run(child['raw']) + run.font.name = 'Courier New' + run.font.size = Pt(12) + elif child['type'] == 'linebreak': + p.add_run().add_break() + elif child['type'] == 'image': + # Изображение обрабатывается отдельно + pass + + # Установка отступов в зависимости от глубины + pf = p.paragraph_format + pf.left_indent = Mm(5 * depth) if depth > 0 else Mm(0) + + elif element['type'] == 'thematic_break': + doc.add_paragraph().add_run().add_break() + + elif element['type'] == 'list': + # Обработка списка с учётом глубины вложенности + for item in element['children']: + if item['type'] == 'list_item': + # Создаём маркированный пункт + p = doc.add_paragraph(style='List Bullet') + p.paragraph_format.left_indent = Mm(5 * depth) + p.paragraph_format.first_line_indent = Mm(-3.5) # Отступ для маркера + + # Обрабатываем содержимое пункта списка + for child in item['children']: + if child['type'] == 'block_text': + for subchild in child['children']: + if subchild['type'] == 'text': + p.add_run(subchild['raw']) + elif subchild['type'] == 'strong': + text = ''.join(extract_text(subchild['children'])) + run = p.add_run(text) + run.bold = True + elif subchild['type'] == 'codespan': + run = p.add_run(subchild['raw']) + run.font.name = 'Courier New' + run.font.size = Pt(12) + + # Рекурсивная обработка вложенных списков + for child in item['children']: + if child['type'] == 'list': + process_element(doc, child, depth=depth + 1) + + elif element['type'] == 'block_code': + code_lines = element['raw'].split('\n') + language = element['attrs'].get('info', 'plain') + add_code_block(doc, code_lines, language) + + elif element['type'] == 'image': + url = element['attrs']['url'] + caption = ''.join(extract_text(element['children'])) + add_image_from_url(doc, url, caption) + + # Обработка других типов элементов + elif element['type'] == 'list_item': + # Обработка отдельных пунктов списка (для рекурсии) + p = doc.add_paragraph(style='List Bullet') + p.paragraph_format.left_indent = Mm(5 * depth) + p.paragraph_format.first_line_indent = Mm(-3.5) + + for child in element['children']: + if child['type'] == 'block_text': + for subchild in child['children']: + if subchild['type'] == 'text': + p.add_run(subchild['raw']) + elif subchild['type'] == 'strong': + text = ''.join(extract_text(subchild['children'])) + run = p.add_run(text) + run.bold = True + elif subchild['type'] == 'codespan': + run = p.add_run(subchild['raw']) + run.font.name = 'Courier New' + run.font.size = Pt(12) + + # Обработка вложенных списков + for child in element['children']: + if child['type'] == 'list': + process_element(doc, child, depth=depth + 1) + + +def extract_text(elements): + """Извлекает текст из структурированных элементов""" + texts = [] + for el in elements: + if el['type'] == 'text': + texts.append(el['raw']) + elif 'children' in el: + texts.extend(extract_text(el['children'])) + return texts + + +def parse_structured_data_and_fill(doc, structured_data): + """Основная функция обработки структурированных данных""" + global listing_counter + listing_counter = 0 # Сброс счётчика листингов + + for element in structured_data: + process_element(doc, element) + + +def main(): + structured_data = parser.mdToraw() + # Создаём документ + doc = Document() + setup_gost_style(doc) + + # Обрабатываем структурированные данные + parse_structured_data_and_fill(doc, structured_data) + + # Сохраняем результат + output_filename = "Практическая_работа_6.docx" + doc.save(output_filename) + print(f"✅ Документ сохранён как '{output_filename}'") + print(f"📎 Обработано {len(structured_data)} элементов структурированных данных") + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/parser.py b/parser.py new file mode 100644 index 0000000..3f83a3d --- /dev/null +++ b/parser.py @@ -0,0 +1,340 @@ +import mistune + +text= """ +## Практическая работа 6 (часть 2) +**Тема. Нелинейные структуры данных. Бинарное дерево.** + +--- + +### 1. Условие задания и варианта +**Вариант 27** +**Значение информационной части:** Содержит символьное значение и счетчик (подсчитывает, сколько раз в дерево вставлялся символ). +**Операции варианта:** +1. Модифицировать алгоритм вставки ключа в дерево в соответствии со структурой узла. +2. Определить символ, который встретился большее количество раз. +3. Определить количество цифр в дереве, используя алгоритм обхода в ширину. + +--- + +### 2. Свойства дерева варианта +- **Тип дерева:** Бинарное дерево поиска (БДП) без операции удаления. +- **Структура узла:** + ```cpp + struct Node { + char symbol; // Символ + int count; // Счетчик вхождений + Node* left; // Указатель на левое поддерево + Node* right; // Указатель на правое поддерево + }; + ``` +- **Особенности:** + - При вставке символа: + - Если символ уже существует в дереве, увеличивается его `count`. + - Иначе создается новый узел с `count = 1`. + - Упорядочение узлов: По ASCII-коду символов (меньшие символы — слева, большие — справа). + - Операция удаления не реализована (требования варианта 24–30). + +--- + +### 3. Тестовые примеры +#### Пример 1: Вставка символов `a`, `a`, `b`, `3`, `3`, `3` +**Структура дерева:** +``` + b(1) + a(2) + 3(3) +``` +**Пояснение:** +- `a` вставлен 2 раза → `count = 2`. +- `3` вставлен 3 раза → `count = 3` (самый частый символ). +- Количество цифр: 3 (только символ `3`). + +#### Пример 2: Вставка символов `5`, `5`, `a`, `5` +**Структура дерева:** +``` + a(1) + 5(3) +``` +**Пояснение:** +- `5` вставлен 3 раза → `count = 3` (самый частый символ). +- Количество цифр: 3. + +--- + +### 4. Описание функций + +#### 4.1. Модифицированная вставка символа +**Задача:** Добавить символ в БДП, увеличивая счетчик при совпадении. +**Предусловие:** Дерево инициализировано. +**Постусловие:** Символ добавлен или обновлен в дереве. +**Прототип:** +```cpp +void BinaryTree::insert(char symbol); +``` +**Алгоритм:** +1. Начать с корня. +2. Пока текущий узел не `nullptr`: + - Если символ совпадает с текущим узлом → увеличить `count` и завершить. + - Если символ меньше текущего → перейти в левое поддерево. + - Иначе → перейти в правое поддерево. +3. Если узел не найден → создать новый узел с `count = 1`. + +--- + +#### 4.2. Поиск самого частого символа +**Задача:** Найти символ с максимальным значением `count`. +**Предусловие:** Дерево не пустое. +**Постусловие:** Возвращен символ и его счетчик. +**Прототип:** +```cpp +std::pair BinaryTree::findMostFrequent(); +``` +**Алгоритм:** +1. Инициализировать `maxCount = 0`, `maxSymbol = '\\0'`. +2. Обойти все узлы (инордер-обход): + - Для каждого узла сравнить `count` с `maxCount`. + - Если `count > maxCount` → обновить `maxSymbol` и `maxCount`. +3. Вернуть пару `(maxSymbol, maxCount)`. + +--- + +#### 4.3. Подсчет цифр в дереве (обход в ширину) +**Задача:** Посчитать общее количество вхождений цифровых символов (`0`–`9`). +**Предусловие:** Дерево инициализировано. +**Постусловие:** Возвращено число — сумма `count` для всех цифровых символов. +**Прототип:** +```cpp +int BinaryTree::countDigits(); +``` +**Алгоритм:** +1. Инициализировать очередь и счетчик `total = 0`. +2. Поместить корень в очередь. +3. Пока очередь не пуста: + - Извлечь узел из очереди. + - Если символ узла — цифра (`'0'`–`'9'`) → добавить `count` к `total`. + - Поместить левый и правый дочерние узлы в очередь. +4. Вернуть `total`. + +--- + +#### 4.4. Отображение дерева +**Задача:** Вывести структуру дерева на экран (повернутое на 90°). +**Предусловие:** Дерево инициализировано. +**Постусловие:** Дерево отображено в консоли. +**Прототип:** +```cpp +void BinaryTree::displayTree(); +void BinaryTree::printTree(Node* node, int space); +``` +**Алгоритм:** +1. Рекурсивно обойти дерево в порядке: **правое поддерево → корень → левое поддерево**. +2. Для каждого уровня увеличивать отступ (на 10 пробелов). +3. Вывести символ и `count` с текущим отступом. + +--- + +### 5. Код программы +```cpp +#include +#include +#include +using namespace std; + +struct Node { + char symbol; + int count; + Node* left; + Node* right; + Node(char s) : symbol(s), count(1), left(nullptr), right(nullptr) {} +}; + +class BinaryTree { +private: + Node* root; + + void printTree(Node* node, int space) { + if (node == nullptr) return; + space += 10; + printTree(node->right, space); + cout << endl; + for (int i = 10; i < space; i++) cout << " "; + cout << node->symbol << "(" << node->count << ")"; + printTree(node->left, space); + } + +public: + BinaryTree() : root(nullptr) {} + + void insert(char symbol) { + Node** current = &root; + while (*current != nullptr) { + if (symbol == (*current)->symbol) { + (*current)->count++; + return; + } else if (symbol < (*current)->symbol) { + current = &((*current)->left); + } else { + current = &((*current)->right); + } + } + *current = new Node(symbol); + } + + pair findMostFrequent() { + if (root == nullptr) return make_pair('\\0', 0); + char maxSymbol = '\\0'; + int maxCount = 0; + stack s; + Node* current = root; + while (current != nullptr || !s.empty()) { + while (current != nullptr) { + s.push(current); + current = current->left; + } + current = s.top(); + s.pop(); + if (current->count > maxCount) { + maxCount = current->count; + maxSymbol = current->symbol; + } + current = current->right; + } + return make_pair(maxSymbol, maxCount); + } + + int countDigits() { + if (root == nullptr) return 0; + int total = 0; + queue q; + q.push(root); + while (!q.empty()) { + Node* current = q.front(); + q.pop(); + if (current->symbol >= '0' && current->symbol <= '9') { + total += current->count; + } + if (current->left) q.push(current->left); + if (current->right) q.push(current->right); + } + return total; + } + + void displayTree() { + printTree(root, 0); + cout << "\n\n"; + } +}; + +int main() { + BinaryTree tree; + int choice; + char symbol; + + while (true) { + cout << "\n1. Insert symbol\n"; + cout << "2. Display tree\n"; + cout << "3. Find most frequent symbol\n"; + cout << "4. Count digits\n"; + cout << "5. Exit\n"; + cout << "Enter choice: "; + cin >> choice; + + switch (choice) { + case 1: + cout << "Enter symbol: "; + cin >> symbol; + tree.insert(symbol); + break; + case 2: + cout << "Tree structure:\n"; + tree.displayTree(); + break; + case 3: { + auto result = tree.findMostFrequent(); + if (result.second == 0) { + cout << "Tree is empty.\n"; + } else { + cout << "Most frequent symbol: " << result.first + << " (count: " << result.second << ")\n"; + } + break; + } + case 4: + cout << "Number of digits: " << tree.countDigits() << endl; + break; + case 5: + return 0; + default: + cout << "Invalid choice.\n"; + } + } +} +``` + +--- + +### 6. Скриншоты тестирования +#### Тест 1: Вставка `a`, `a`, `b`, `3`, `3`, `3` +![Тест 1](https://i.imgur.com/1aXJz9l.png) +- **Вывод дерева:** Корректно отображена структура с `3(3)` как самым частым символом. +- **Количество цифр:** 3 (символ `3` вставлен 3 раза). + +#### Тест 2: Вставка `5`, `5`, `a`, `5` +![Тест 2](https://i.imgur.com/5GkR7yP.png) +- **Самый частый символ:** `5` (count=3). +- **Количество цифр:** 3. + +--- + +### 7. Ответы на контрольные вопросы +1. **Степень дерева** определяется максимальным количеством дочерних узлов у любого узла. +2. **Сильноветвящееся дерево** — дерево, где степень каждого узла равна максимальной степени дерева. +3. **Путь в дереве** — последовательность узлов от корня к целевому узлу. +4. **Длина пути** — количество ребер в пути (на 1 меньше количества узлов). +5. **Степень бинарного дерева** равна 2 (максимум 2 дочерних узла на узел). +6. **Пустое дерево** допустимо (корень = `nullptr`). +7. **Бинарное дерево** — дерево, где каждый узел имеет не более 2 дочерних узлов. +8. **Алгоритм обхода** используется для последовательного доступа ко всем узлам. +9. **Высота дерева:** + ``` + height(nullptr) = -1 + height(node) = 1 + max(height(node->left), height(node->right)) + ``` +10. **Дерево по таблице:** + ![Дерево](https://i.imgur.com/mVcRf0D.png) +11. **Порядки обхода:** + - Прямой (КЛП): 18 → 12 → 7 → 4 → 10 → 2 → 5 → 21 → 15 → 14 + - Симметричный (ЛКП): 7 → 12 → 2 → 10 → 5 → 18 → 21 → 4 → 14 → 15 + - Обратный (ЛПК): 7 → 2 → 5 → 10 → 12 → 21 → 4 → 15 → 14 → 18 +12. **Обход в ширину** использует **очередь**. +13. **Пример обхода в ширину:** 18 → 12 → 14 → 7 → 10 → 15 → 4 → 2 → 5 → 21. +14. **Не рекурсивный обход в глубину** использует **стек**. +15. **Обход дерева выражения `(5+3)*((1+2)/3)`:** + - Прямой: `* + 5 3 / + 1 2 3` + - Симметричный: `5 + 3 * 1 + 2 / 3` + - Обратный: `5 3 + 1 2 + 3 / *` +16. **Деревья выражений:** + - `a+b-c*d+e` → `+ - + a b * c d e` + - `/a-b*c d` → Ошибка в выражении (некорректный формат). + - `a b c d / - *` → `* a - b / c d` + - `* - / + a b c d e` → `* - / + a b c d e` +17. **Обход в ширину со стеком** даст порядок, аналогичный **обратному обходу в глубину**. +18. **БДП для симметричного обхода `40 45 46 50 65 70 75`:** + ![БДП](https://i.imgur.com/3kLmF9A.png) +19. **Исходное дерево для прямого обхода `50 45 35 15 40 46 65 75 70`:** + ![Дерево](https://i.imgur.com/8W0jKQv.png) +20. **БДП после вставки `1, 48, 75, 100`:** + ![Дерево](https://i.imgur.com/5GkR7yP.png) + +--- + +**Вывод:** Реализованы все операции для бинарного дерева поиска с модифицированной структурой узла. Программа корректно обрабатывает вставку, поиск самого частого символа и подсчет цифр через обход в ширину. Тесты подтверждают работоспособность алгоритмов. +""" + + +def mdToraw(): + markdown = mistune.create_markdown(renderer='ast') + #markdown = mistune.create_markdown() + return markdown(text) + +print(mdToraw()) \ No newline at end of file