From d2da20fdb212fb8df7b7d42f1daa8b6282e4f2e6 Mon Sep 17 00:00:00 2001 From: Igor20264 Date: Mon, 15 Sep 2025 07:48:37 +0300 Subject: [PATCH] First Commit --- .gitignore | 3 + ReadMe.md | 209 +++++ asd.py | 317 ++++++++ image.png | Bin 0 -> 48544 bytes image2.jpg | Bin 0 -> 128068 bytes main.py | 32 + main12.py | 915 ++++++++++++++++++++++ mm2.py | 1551 ++++++++++++++++++++++++++++++++++++ mm3.py | 2210 ++++++++++++++++++++++++++++++++++++++++++++++++++++ mm4.py | 241 ++++++ mm5.py | 284 +++++++ parser.py | 340 ++++++++ 12 files changed, 6102 insertions(+) create mode 100644 .gitignore create mode 100644 ReadMe.md create mode 100644 asd.py create mode 100644 image.png create mode 100644 image2.jpg create mode 100644 main.py create mode 100644 main12.py create mode 100644 mm2.py create mode 100644 mm3.py create mode 100644 mm4.py create mode 100644 mm5.py create mode 100644 parser.py 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 0000000000000000000000000000000000000000..af88dbe6542c76d68bc66d70e818785da4f830d5 GIT binary patch literal 48544 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�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 literal 0 HcmV?d00001 diff --git a/image2.jpg b/image2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..06d0b4eee01696f478aa9bf86d30de978bf3e5d9 GIT binary patch literal 128068 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 T=06NCf@-cEPu>dsd+NUc;>!h` literal 0 HcmV?d00001 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