- update документация - промт для ии полу конфигурируемый
This commit is contained in:
@@ -182,3 +182,7 @@ md2gost.include-cache.json
|
||||
include-cache/
|
||||
.md2gost-cache/
|
||||
/build-fast
|
||||
build-fast/md2gost.fast/xref-md2gost.fast.html
|
||||
build-fast/md2gost.fast/PYZ-00.pyz
|
||||
build-fast/md2gost.fast/md2gost.pkg
|
||||
build-fast/md2gost.fast/base_library.zip
|
||||
|
||||
@@ -1,209 +1,34 @@
|
||||
# GhostEditor
|
||||
Программа для конвертации из формата `md` в формат `docx`(x это без макросов, m с макросами) (`Office Open XML`),`odt`(`OpenDocument`)
|
||||
# GhostEditor / md2gost
|
||||
|
||||
#### **1. python-docx**
|
||||
**Назначение**: Создание и модификация Word-документов.
|
||||
**Ключевые возможности**:
|
||||
- Добавление/редактирование текста, таблиц, изображений.
|
||||
- Управление стилями, колонтитулами, сносками.
|
||||
- Работа с XML через абстракции (Paragraph, Run, Table).
|
||||
Конвертер **Markdown → DOCX** по методическим указаниям РТУ МИРЭА (ГОСТ 7.32-2017) и профилям ПИС / АПИД. Есть GUI, CLI и промпты для генерации `.md` через ИИ.
|
||||
|
||||
**Пример**:
|
||||
```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")
|
||||
```bash
|
||||
pip install -e .
|
||||
python -m md2gost # GUI
|
||||
python -m md2gost report.md -o report.docx --type coursework --check
|
||||
```
|
||||
|
||||
**Плюсы**:
|
||||
- Интуитивный API, близкий к логике Word.
|
||||
- Поддержка всех ключевых частей пакета (headers, footers, styles).
|
||||
Windows: `build-exe.bat` → `dist\md2gost.exe` (двойной клик — GUI).
|
||||
|
||||
**Минусы**:
|
||||
- Нет поддержки макросов.
|
||||
- Ограниченная работа с графикой (изображения добавляются, но редактирование сложно).
|
||||
## Документация
|
||||
|
||||
**Ссылка**: [python-docx.readthedocs.io](https://python-docx.readthedocs.io/)
|
||||
Полное описание — в [`docs/`](docs/):
|
||||
|
||||
---
|
||||
| Раздел | Содержание |
|
||||
|--------|------------|
|
||||
| [Быстрый старт](docs/quickstart.md) | Установка, GUI, типы документов |
|
||||
| [Синтаксис Markdown](docs/markdown.md) | Диалект md2gost vs обычный MD |
|
||||
| [Схемы и диаграммы](docs/schemes.md) | PlantUML, Mermaid, `md2gost.schemes.json` |
|
||||
| [Типы работ](docs/types.md) | coursework, practice, vkr, PIS, APID |
|
||||
| [CLI и GUI](docs/cli-gui.md) | Флаги и где они в окне |
|
||||
| [Промпты для ИИ](docs/prompts.md) | Генерация MD, кнопки схем в GUI |
|
||||
|
||||
#### **2. docxtpl**
|
||||
**Назначение**: Генерация документов по шаблонам (Jinja2).
|
||||
**Особенности**:
|
||||
- Шаблоны .docx с переменными (`{{ variable }}`) и логикой (`{% if %}`).
|
||||
- Поддержка таблиц, изображений, HTML-фрагментов.
|
||||
Краткая справка по пакету: [`md2gost/README.md`](md2gost/README.md).
|
||||
Промпты: [`prompts/`](prompts/).
|
||||
|
||||
**Пример**:
|
||||
```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/) (полная спецификация).
|
||||
- FODT (LibreOffice): `python -m md2fodt` — [`md2fodt/`](md2fodt/)
|
||||
- PDF через LaTeX: [`md2latex/`](md2latex/)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Документация md2gost
|
||||
|
||||
Конвертер Markdown → DOCX для учебных работ (ТЗ МИРЭА / ГОСТ 7.32, ПИС, АПИД).
|
||||
|
||||
## Содержание
|
||||
|
||||
1. [Быстрый старт](quickstart.md) — установка, первое окно, exe
|
||||
2. [Синтаксис Markdown](markdown.md) — чем диалект отличается от обычного MD
|
||||
3. [Схемы и диаграммы](schemes.md) — UML, Mermaid, настраиваемые схемы
|
||||
4. [Типы документов](types.md) — coursework, practice, vkr, PIS_custom, APID_coursework
|
||||
5. [CLI и GUI](cli-gui.md) — флаги и соответствие в интерфейсе
|
||||
6. [Кастомные стили (JSON)](styles.md) — оверлей полей и стилей абзацев
|
||||
7. [Промпты для ИИ](prompts.md) — генерация `.md`, расширение промпта схемами
|
||||
|
||||
Исходный код конвертера: [`md2gost/`](../md2gost/). Примеры: [`examples/`](../examples/).
|
||||
|
||||
В GUI: **Справка → Документация** — тот же набор страниц (вшит в exe).
|
||||
@@ -0,0 +1,58 @@
|
||||
# CLI и GUI
|
||||
|
||||
Движок один: GUI собирает те же параметры, что и аргументы `python -m md2gost`.
|
||||
|
||||
## Основные команды
|
||||
|
||||
```bash
|
||||
python -m md2gost # GUI
|
||||
python -m md2gost --gui report.md
|
||||
python -m md2gost report.md -o out.docx --type coursework --check --strict
|
||||
python -m md2gost report.docx --check-pages # только проверка вёрстки готового DOCX
|
||||
```
|
||||
|
||||
Справка: `python -m md2gost -h`.
|
||||
|
||||
## Соответствие флагов и GUI
|
||||
|
||||
| CLI | Где в GUI | Смысл |
|
||||
|-----|-----------|--------|
|
||||
| `--type` | Основные → тип | practice, coursework, vkr, PIS_custom, APID_coursework |
|
||||
| `-o` / `--output` | Выходной DOCX | путь результата |
|
||||
| `--heading-numbering` | Основные | `manual` (цифры в md) / `auto` (Word) |
|
||||
| `--toc` | Основные | `native` (поле Word) / `manual` (собирает md2gost) |
|
||||
| `--emdash-to-hyphen` | Галочка «—» → «-» | по умолчанию выкл. |
|
||||
| `--hr-pagebreak` | Галочка --- → разрыв | по умолчанию `---` игнорируется |
|
||||
| `--title` / `--assignment` / `-t` | Настройки → Файлы | титул, задание, шаблон |
|
||||
| `--check` / `--check-only` / `--strict` | Галочки проверки ТЗ | замечания по структуре и источникам |
|
||||
| `--check-pages` | Проверить вёрстку в Word | эвристика полупустых страниц (Word + pywin32) |
|
||||
| `--table-continuation` | Основные | `word` (по умолчанию), `off`, `legacy`, `caption`, `soft` |
|
||||
| `--listing-continuation` | Основные | то же для листингов |
|
||||
| `--table-repeat-header` | Галочка | шапка на фрагментах после word-split |
|
||||
| `--plantuml-jar` / `--kroki-url` | Настройки → Диаграммы | локальный рендер |
|
||||
| `--diagram-fallback` | Диаграммы | `remote` / `local` / `off` |
|
||||
| `--diagram-format` | Диаграммы | `png` / `svg` |
|
||||
| `--diagram-scale` | Диаграммы | качество PNG PlantUML (по умолчанию 2) |
|
||||
| `--schemes` | файл схем / Шаблоны UML | путь к `md2gost.schemes.json` |
|
||||
| `--styles` | Настройки → Файлы → Стили JSON | оверлей `md2gost.styles.json` (также рядом с `.md`); см. [styles.md](styles.md) |
|
||||
| `--syntax-highlighting` | (CLI) | подсветка в листингах |
|
||||
| `--debug` | меню Дебаг | отладочные данные в документе (на одну сборку в GUI) |
|
||||
|
||||
## Продолжение таблиц и листингов
|
||||
|
||||
DOCX не умеет сам писать «Продолжение Таблицы N» только со 2-й страницы. Режим **`word`** (по умолчанию): после сохранения Word COM режет по реальной пагинации. Нужны Windows + Word + pywin32. Без Word: `--table-continuation off` (и то же для листингов).
|
||||
|
||||
## Меню GUI
|
||||
|
||||
- **Настройки → Файлы** — шаблон, титул, задание, стили JSON, выходной путь
|
||||
- **Настройки → Диаграммы** — PlantUML, Kroki, формат, кэш includes
|
||||
- **Шаблоны UML** — редактор `md2gost.schemes.json`
|
||||
- **Справка → Инструкция / Документация / Схемы / Промпт для ИИ**
|
||||
(Документация — встроенный просмотр `docs/*.md`, вшито в exe)
|
||||
|
||||
## Другие форматы
|
||||
|
||||
```bash
|
||||
python -m md2fodt report.md -o report.fodt # LibreOffice
|
||||
# PDF через LaTeX — см. md2latex/
|
||||
```
|
||||
@@ -0,0 +1,153 @@
|
||||
# Синтаксис Markdown (диалект md2gost)
|
||||
|
||||
Обычный Markdown описывает структуру текста. Для отчёта по ГОСТ нужны ещё: спецразделы без номера, подписи объектов, перекрёстные ссылки, библиография в заданном виде. **md2gost** расширяет MD ровно этими элементами; конвертер сам нумерует рисунки/таблицы/листинги и оформляет DOCX.
|
||||
|
||||
## Что совпадает с обычным MD
|
||||
|
||||
- Заголовки `#` … `######`
|
||||
- Абзацы, **жирный**, *курсив*
|
||||
- Маркированные и нумерованные списки
|
||||
- Таблицы `| … |`
|
||||
- Картинки ``
|
||||
- Блоки кода в ограде ` ```язык `
|
||||
- Формулы `$$ … $$` (и инлайн `$…$` где поддерживается)
|
||||
|
||||
## Зачем расширения
|
||||
|
||||
| Задача | Обычный MD | md2gost |
|
||||
|--------|------------|---------|
|
||||
| Введение без номера «1» | `# Введение` → станет разделом 1 | `# *ВВЕДЕНИЕ` |
|
||||
| Подпись «Рисунок 1.1 — …» | руками / HTML | `%id` или title у картинки |
|
||||
| «см. рис. 2» в тексте | нет семантики | `@Рисунок:id` |
|
||||
| UML → картинка в Word | экспорт PNG вручную | fence `` ```uml `` / схема → Рисунок |
|
||||
|
||||
## Спецразделы
|
||||
|
||||
Звёздочка `*` = без автоматической нумерации раздела. Текст — ПРОПИСНЫМИ:
|
||||
|
||||
```markdown
|
||||
# *СОДЕРЖАНИЕ
|
||||
[TOC]
|
||||
|
||||
# *ВВЕДЕНИЕ
|
||||
|
||||
# 1 Название первого раздела
|
||||
## 1.1 Подраздел
|
||||
|
||||
# *ЗАКЛЮЧЕНИЕ
|
||||
|
||||
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
|
||||
|
||||
# *ПРИЛОЖЕНИЯ
|
||||
```
|
||||
|
||||
- После `# *СОДЕРЖАНИЕ` обязателен `[TOC]`.
|
||||
- В конце названия заголовка точку не ставить.
|
||||
- Для `PIS_custom` структура другая (практические работы) — см. [types.md](types.md).
|
||||
|
||||
## Рисунок (файл)
|
||||
|
||||
```markdown
|
||||
Текст со ссылкой на @Рисунок:arch.
|
||||
|
||||

|
||||
```
|
||||
|
||||
В тексте пишите **«Рисунок»**, не «рис.» / «рис».
|
||||
|
||||
## Таблица
|
||||
|
||||
```markdown
|
||||
См. @Таблица:cmp.
|
||||
|
||||
%cmp Название таблицы
|
||||
|
||||
| A | B |
|
||||
|---|---|
|
||||
| 1 | 2 |
|
||||
```
|
||||
|
||||
Склеивание ячеек:
|
||||
|
||||
- `^` — rowspan (продолжение ячейки сверху)
|
||||
- `>` — colspan (продолжение слева)
|
||||
|
||||
Не ставить `^`/`>` в заголовочной строке; `>` — не в первом столбце. Графу «№ п/п» не добавлять.
|
||||
|
||||
## Листинг
|
||||
|
||||
~~~markdown
|
||||
Фрагмент в @Листинг:code1.
|
||||
|
||||
%code1 Название листинга
|
||||
|
||||
```python
|
||||
def f():
|
||||
return 1
|
||||
```
|
||||
~~~
|
||||
|
||||
## Диаграмма → Рисунок
|
||||
|
||||
Перед блоком — `%id Подпись`. Языки: `uml`, `plantuml`, `mermaid`/`mmd`, или id схемы (`c4`, `bpmn`, …).
|
||||
|
||||
- `+listing` — ещё и Листинг с исходником
|
||||
- `+landscape` — альбомная страница под широкий рисунок/таблицу
|
||||
|
||||
Подробности: [schemes.md](schemes.md).
|
||||
|
||||
## Формула
|
||||
|
||||
```markdown
|
||||
%eq1
|
||||
$$
|
||||
E = mc^2
|
||||
$$
|
||||
|
||||
Зависимость (@Формула:eq1) используется далее.
|
||||
```
|
||||
|
||||
Нумеруются **только** формулы, на которые есть `@Формула:…`.
|
||||
|
||||
## Ссылки на объекты
|
||||
|
||||
`@Рисунок:id`, `@Таблица:id`, `@Листинг:id`, `@Формула:id` — id совпадает с меткой после `%` или в title картинки.
|
||||
|
||||
## Источники
|
||||
|
||||
В тексте: `[1]`, `[2, 3]` (для ВКР — `[1.5]`).
|
||||
|
||||
В списке:
|
||||
|
||||
```markdown
|
||||
[1]: Иванов И. И. Название. — М.: Наука, 2024. — 120 с.
|
||||
```
|
||||
|
||||
Во **ВВЕДЕНИИ** и **ЗАКЛЮЧЕНИИ** ссылок `[n]` быть не должно.
|
||||
|
||||
## Приложения
|
||||
|
||||
```markdown
|
||||
# *ПРИЛОЖЕНИЯ
|
||||
|
||||
## Приложение А Листинг модуля
|
||||
|
||||
…
|
||||
|
||||
## Приложение Б Графический материал
|
||||
```
|
||||
|
||||
Буквы: А, Б, В, Г, Д, Е, Ж, И, К… **Нельзя:** Ё, З, Й, О, Ч, Ь, Ы, Ъ.
|
||||
|
||||
## Тире и разрыв страницы
|
||||
|
||||
- Тире в предложениях: «—» (по умолчанию сохраняется). Замена на «-»: `--emdash-to-hyphen`.
|
||||
- Строка `---` / `***` / `___` по умолчанию **игнорируется**. Разрыв страницы: `--hr-pagebreak` или галочка в GUI.
|
||||
|
||||
## Запрещено в отчёте
|
||||
|
||||
- «рис.», «табл.»
|
||||
- графа «№ п/п»
|
||||
- сноски `[^1]`
|
||||
- формулы обычным текстом вместо `$$…$$`
|
||||
- нумерация спецразделов (`# ВВЕДЕНИЕ` вместо `# *ВВЕДЕНИЕ`)
|
||||
@@ -0,0 +1,33 @@
|
||||
# Промпты для ИИ
|
||||
|
||||
Промпты лежат в [`prompts/`](../prompts/). В GUI: **Справка → Промпт для ИИ** — выбрать, при необходимости **дописать схемы**, скопировать в буфер. Полная документация также доступна в **Справка → Документация** (вшита в exe).
|
||||
|
||||
## Какие файлы
|
||||
|
||||
| Файл | Когда |
|
||||
|------|--------|
|
||||
| [`generate-md.md`](../prompts/generate-md.md) | Базовый: диалект md2gost, чем отличается от обычного MD; UML/Mermaid рисует конвертер |
|
||||
| [`generate-mirea-report.md`](../prompts/generate-mirea-report.md) | Полный отчёт ГОСТ / МИРЭА (`coursework`, `practice`, `vkr`) |
|
||||
| [`generate-pis-custom-report.md`](../prompts/generate-pis-custom-report.md) | Итоговый отчёт ПИС (`PIS_custom`) |
|
||||
|
||||
## Как пользоваться
|
||||
|
||||
1. Откройте промпт (файл или GUI).
|
||||
2. В GUI включите нужные схемы (C4, BPMN, …) — в конец промпта добавятся макросы и `ai-prompt` из `md2gost.schemes.json`.
|
||||
3. Скопируйте собранный текст в ChatGPT / Claude / Cursor / Copilot.
|
||||
4. Следующим сообщением: тип работы, тема, черновик / требования.
|
||||
5. Сохраните ответ как `.md` и конвертируйте:
|
||||
|
||||
```bash
|
||||
python -m md2gost report.md -o report.docx --type coursework --check --strict
|
||||
```
|
||||
|
||||
## Зачем кнопки схем
|
||||
|
||||
Базовый промпт уже говорит: блоки `` ```uml `` / `` ```mermaid `` / `` ```uml-c4 `` конвертер сам превратит в Рисунок. Кнопки нужны, когда модели надо знать **конкретные макросы** выбранной схемы (Person/Rel для C4, Start/XOR/Flow для BPMN и т.д.) — в том числе пользовательских из «Шаблоны UML».
|
||||
|
||||
## Подробнее о синтаксисе
|
||||
|
||||
- [markdown.md](markdown.md)
|
||||
- [schemes.md](schemes.md)
|
||||
- [prompts/README.md](../prompts/README.md)
|
||||
@@ -0,0 +1,61 @@
|
||||
# Быстрый старт
|
||||
|
||||
## Установка
|
||||
|
||||
Нужен Python 3.10+.
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
# или
|
||||
poetry install
|
||||
```
|
||||
|
||||
Для продолжения таблиц/листингов через Word COM (режим `word` по умолчанию) на Windows:
|
||||
|
||||
```bash
|
||||
pip install pywin32
|
||||
```
|
||||
|
||||
Нужны установленный Microsoft Word.
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
python -m md2gost # GUI без файла
|
||||
python -m md2gost --gui
|
||||
python -m md2gost report.md -o report.docx --type coursework --check
|
||||
md2gost-gui # то же GUI (entry point)
|
||||
```
|
||||
|
||||
### Windows exe
|
||||
|
||||
В корне репозитория: `build-exe.bat` → `dist\md2gost.exe`.
|
||||
|
||||
- Двойной клик — GUI.
|
||||
- CLI: `md2gost.exe report.md -o report.docx --type coursework`.
|
||||
|
||||
## GUI за минуту
|
||||
|
||||
1. Перетащите `.md` в верхнюю область (или кликните по ней).
|
||||
2. Выберите тип документа (`practice` по умолчанию).
|
||||
3. При необходимости: Настройки → Файлы (титул, задание), Настройки → Диаграммы, меню «Шаблоны UML».
|
||||
4. Нажмите «Конвертировать».
|
||||
|
||||
Справка в меню: **Инструкция**, **Схемы**, **Промпт для ИИ**.
|
||||
|
||||
## Типы документов (кратко)
|
||||
|
||||
| `--type` | Назначение |
|
||||
|----------|------------|
|
||||
| `practice` / `coursework` | Практика / курсовая по ТЗ МИРЭА |
|
||||
| `vkr` | ВКР (раздельный список, графическое приложение) |
|
||||
| `PIS_custom` | Итоговый отчёт по практическим работам |
|
||||
| `APID_coursework` | Курсовая «Архитектура приложений и данных» |
|
||||
|
||||
Подробнее: [types.md](types.md).
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
- Написать отчёт в диалекте md2gost: [markdown.md](markdown.md)
|
||||
- Вставить диаграммы: [schemes.md](schemes.md)
|
||||
- Сгенерировать `.md` через ИИ: [prompts.md](prompts.md)
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
# Схемы и диаграммы
|
||||
|
||||
В отчёте блок кода с языком диаграммы превращается в **Рисунок** (PNG или SVG). PNG руками экспортировать не нужно: md2gost вызывает PlantUML (jar / Kroki) или Kroki для Mermaid.
|
||||
|
||||
## Синтаксис в markdown
|
||||
|
||||
````markdown
|
||||
%usecase1 Диаграмма прецедентов +listing
|
||||
|
||||
```uml
|
||||
@startuml
|
||||
actor Student
|
||||
Student --> (Login)
|
||||
@enduml
|
||||
```
|
||||
````
|
||||
|
||||
Широкая схема:
|
||||
|
||||
````markdown
|
||||
%arch1 Архитектура +landscape
|
||||
|
||||
```uml-c4
|
||||
Person(user, "Студент")
|
||||
System(app, "Портал")
|
||||
Rel(user, app, "логин")
|
||||
```
|
||||
````
|
||||
|
||||
| Оградка | Что происходит |
|
||||
|---------|----------------|
|
||||
| `` ```uml `` / `` ```plantuml `` | Сырой PlantUML (`@startuml`…`@enduml`) |
|
||||
| `` ```uml-<id> `` / `` ```<id> `` | Схема из `md2gost.schemes.json` (обёртка и `!include` добавляются сами) |
|
||||
| `` ```mermaid `` / `` ```mmd `` | Mermaid через Kroki |
|
||||
| `` ```bpmn `` / `` ```uml-bpmn `` | BPMN 2.0 (макросы библиотеки md2gost) |
|
||||
|
||||
**IDEF0 / DFD** конвертер не рисует — вставляйте готовый PNG как обычный Рисунок.
|
||||
|
||||
## Встроенные схемы
|
||||
|
||||
Файл шаблона: [`md2gost/diagrams/schemes.json`](../md2gost/diagrams/schemes.json). При первом запуске копируется в `md2gost.schemes.json` рядом с приложением (существующий файл не перезаписывается).
|
||||
|
||||
| id | Название | Оградка |
|
||||
|----|----------|---------|
|
||||
| `c4` | C4 Container | `` ```uml-c4 `` / `` ```c4 `` |
|
||||
| `c4context` | C4 Context | `` ```uml-c4context `` |
|
||||
| `c4component` | C4 Component | `` ```uml-c4component `` |
|
||||
| `usecase` | Прецеденты | `` ```uml-usecase `` / `` ```usecase `` |
|
||||
| `bpmn` | BPMN 2.0 | `` ```bpmn `` / `` ```uml-bpmn `` |
|
||||
|
||||
Для схем в теле пишите **только макросы** (Person, Start, XOR, …) — без `@startuml` и без `!include`, если схема сама их добавляет.
|
||||
|
||||
### C4 (пример макросов)
|
||||
|
||||
`Person`, `System`, `System_Ext`, `Container`, `ContainerDb`, `Rel` / `Rel_R`…
|
||||
|
||||
### Use Case
|
||||
|
||||
`actor`, `usecase`, связи `-->`, `<<include>>`, `<<extend>>`.
|
||||
|
||||
### BPMN
|
||||
|
||||
`Pool` / `Lane`, `Start` / `StartMessage` / `End`, `UserTask` / `ServiceTask`, `XOR` / `AND` / `OR`, `Flow` / `CondFlow` / `DefaultFlow` / `MessageFlow`, …
|
||||
|
||||
Sequence Flow только внутри пула; между пулами — `MessageFlow`.
|
||||
|
||||
Полные шпаргалки — поля `docs` и `ai-prompt` в JSON; в GUI: **Шаблоны UML** и кнопки схем в **Справка → Промпт для ИИ**.
|
||||
|
||||
## Файл схем: поля
|
||||
|
||||
```json
|
||||
{
|
||||
"c4": {
|
||||
"title": "C4 Container",
|
||||
"docs": "…макросы…",
|
||||
"ai-prompt": "…инструкция для ИИ…",
|
||||
"includes": ["https://…/C4_Container.puml"],
|
||||
"prefix": "@startuml\n",
|
||||
"postfix": "\nLAYOUT_WITH_LEGEND()\n@enduml"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Назначение |
|
||||
|------|------------|
|
||||
| `title` | Подпись в GUI |
|
||||
| `docs` | Шпаргалка макросов |
|
||||
| `ai-prompt` | Фрагмент для промпта ИИ |
|
||||
| `includes` | Локальные `.puml` или URL |
|
||||
| `prefix` / `postfix` | Обёртка вокруг тела |
|
||||
| `theme` | опционально `!theme …` |
|
||||
|
||||
id схемы: латиница, цифры, `_`; начинается с буквы.
|
||||
|
||||
## Слои загрузки (позже побеждает)
|
||||
|
||||
1. Встроенный шаблон `md2gost/diagrams/schemes.json`
|
||||
2. Пользовательский файл рядом с приложением (`md2gost.schemes.json`)
|
||||
3. `md2gost.schemes.json` рядом с `.md`
|
||||
4. Явный путь `--schemes path.json`
|
||||
|
||||
## Рендер
|
||||
|
||||
**UML / схемы:**
|
||||
|
||||
1. Java + `plantuml.jar` (вшитый / `%LOCALAPPDATA%\md2gost\` / `--plantuml-jar`)
|
||||
2. Локальный Kroki (`KROKI_URL` / `--kroki-url`, по умолчанию `http://localhost:8000`)
|
||||
3. `https://kroki.io` при `--diagram-fallback remote` (по умолчанию)
|
||||
|
||||
**Mermaid** — только Kroki (шаги 2–3).
|
||||
|
||||
Формат: `--diagram-format png` (по умолчанию) или `svg`. Масштаб рендера PlantUML: `--diagram-scale` (по умолчанию 2 — качество, размер на странице как при 1).
|
||||
|
||||
Кэш картинок: `{каталог_md}/.md2gost-cache/`.
|
||||
Кэш includes: `md2gost.include-cache.json` + папка `include-cache/` (сброс в GUI: Настройки → Диаграммы / Шаблоны UML).
|
||||
|
||||
## Свои схемы
|
||||
|
||||
1. Меню **Шаблоны UML** — добавить / править / сохранить.
|
||||
2. Или править `md2gost.schemes.json` вручную («Открыть JSON»).
|
||||
3. CLI: `--schemes путь.json`.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Кастомные стили (JSON)
|
||||
|
||||
Опциональный оверлей оформления поверх пресета `--type` (`mirea` или `pis_custom`).
|
||||
Неуказанные поля остаются у пресета. Стили пишет конвертер в DOCX сам — вручную в шаблон добавлять не нужно.
|
||||
|
||||
## Как подключить
|
||||
|
||||
Позже побеждает (deep-merge только указанных ключей):
|
||||
|
||||
1. Встроенный пресет типа документа
|
||||
2. `md2gost.styles.json` рядом с `.md` (если есть)
|
||||
3. `--styles путь.json` или путь в GUI: **Настройки → Файлы → Стили JSON**
|
||||
|
||||
Файл рядом с exe при первом запуске **не создаётся**.
|
||||
|
||||
```bash
|
||||
python -m md2gost report.md -o report.docx --type practice --styles examples/md2gost.styles.json
|
||||
```
|
||||
|
||||
Пример: [`examples/md2gost.styles.json`](../examples/md2gost.styles.json).
|
||||
|
||||
## Схема
|
||||
|
||||
Два блока. Неизвестный ключ или имя стиля — ошибка.
|
||||
|
||||
```json
|
||||
{
|
||||
"page": {
|
||||
"left_mm": 30,
|
||||
"right_mm": 10,
|
||||
"top_mm": 20,
|
||||
"bottom_mm": 20
|
||||
},
|
||||
"styles": {
|
||||
"Heading 1": {
|
||||
"alignment": "center",
|
||||
"size_pt": 14,
|
||||
"all_caps": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `page`
|
||||
|
||||
| Ключ | Единица | Смысл |
|
||||
|------|---------|--------|
|
||||
| `left_mm` / `right_mm` / `top_mm` / `bottom_mm` | мм | Поля страницы |
|
||||
|
||||
### Имена стилей
|
||||
|
||||
`Normal`, `Heading 1`, `Heading 2`, `Heading 3`, `Caption Figure`, `Caption Table`, `Название таблицы`, `Caption Listing`, `Caption`, `Code`, `Table Text`, `Bibliography`, `Bibliography Heading`, `toc 1`, `toc 2`, `toc 3`, `Footer`, `Hyperlink`, `FollowedHyperlink`, `Space After Table`.
|
||||
|
||||
### Поля стиля
|
||||
|
||||
| Ключ | Тип / значения |
|
||||
|------|----------------|
|
||||
| `font_name` | строка |
|
||||
| `size_pt` | число (pt) |
|
||||
| `bold`, `italic`, `all_caps`, `underline` | bool |
|
||||
| `alignment` | `left` \| `center` \| `justify` |
|
||||
| `first_line_indent_cm`, `left_indent_cm`, `right_indent_cm` | см |
|
||||
| `space_before_mm`, `space_after_mm` | мм |
|
||||
| `line_spacing` | `1.0` \| `1.5` |
|
||||
| `page_break_before`, `keep_with_next`, `widow_control` | bool |
|
||||
|
||||
Не в JSON: табы оглавления (считаются от ширины полосы), отступы списков, формулы из шаблона, цвет (всегда чёрный).
|
||||
@@ -0,0 +1,66 @@
|
||||
# Типы документов
|
||||
|
||||
Задаётся флагом `--type` / выбором в GUI. Профили: [`md2gost/profiles.py`](../md2gost/profiles.py).
|
||||
|
||||
## Сводка
|
||||
|
||||
| Тип | Стили | Нумерация объектов | Источники | Структура |
|
||||
|-----|--------|-------------------|-----------|-----------|
|
||||
| `practice` | МИРЭА | по разделам (1.1, 2.1) | 5–7, ≤5 лет | Введение, разделы, заключение, список, приложения |
|
||||
| `coursework` | МИРЭА | по разделам | 5–7, ≤5 лет | то же |
|
||||
| `vkr` | МИРЭА | по разделам | ≥10 в **каждом** разделе списка, ≤5 лет | + приложение «Графический материал»; ссылки `[1.5]` |
|
||||
| `PIS_custom` | ПИС | сквозная (1, 2, 3) | не требуются | H1 = практические работы; без введения/заключения по умолчанию |
|
||||
| `APID_coursework` | МИРЭА | по разделам | 7–20, ≤5 лет | обязательные главы/пункты АПИД (см. ниже) |
|
||||
|
||||
## coursework / practice
|
||||
|
||||
Спецразделы с `*`, нумерованная основная часть. В DOCX: H1 слева с отступом (кроме СОДЕРЖАНИЕ / СПИСОК — по центру).
|
||||
|
||||
Проверка: `--check` / `--strict`.
|
||||
|
||||
## vkr
|
||||
|
||||
- Список источников **с разделами**; в каждом ≥10 записей.
|
||||
- Ссылки вида `[1.5]`, при цитировании `[2.18, c. 21-25]`.
|
||||
- Обязательное приложение с текстом «Графический материал» в названии.
|
||||
|
||||
## PIS_custom
|
||||
|
||||
Итоговый отчёт по практическим работам.
|
||||
|
||||
```markdown
|
||||
# *СОДЕРЖАНИЕ
|
||||
[TOC]
|
||||
|
||||
# Практическая работа №1. Название работы
|
||||
|
||||
## Цель работы
|
||||
## Ход работы
|
||||
## Выводы
|
||||
|
||||
# Практическая работа №2. …
|
||||
```
|
||||
|
||||
- H1 **без** `*` (кроме СОДЕРЖАНИЕ): в DOCX — ПРОПИСНЫМИ по центру.
|
||||
- H2 с абзацным отступом.
|
||||
- Титул («Отчёт по практическим работам …») — отдельный DOCX: `--title title.docx`.
|
||||
|
||||
Пример: [`examples/pis_custom.md`](../examples/pis_custom.md).
|
||||
Промпт: [`prompts/generate-pis-custom-report.md`](../prompts/generate-pis-custom-report.md).
|
||||
|
||||
## APID_coursework
|
||||
|
||||
Курсовая «Архитектура приложений и данных» (методичка Аншиной/Лагуновой). Оформление как у `coursework`, плюс проверка заголовков:
|
||||
|
||||
1. Теоретические аспекты …
|
||||
2. Прикладные аспекты …
|
||||
- 2.1 Описание проекта …
|
||||
- 2.2 Описание роли …
|
||||
- 2.3 Описание архитектуры …
|
||||
- 2.4 Варианты развития архитектуры …
|
||||
|
||||
Источники: 7–20.
|
||||
|
||||
```bash
|
||||
python -m md2gost report.md -o report.docx --type APID_coursework --check --title title.docx --assignment assignment.docx
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"page": {
|
||||
"left_mm": 20,
|
||||
"right_mm": 20,
|
||||
"top_mm": 20,
|
||||
"bottom_mm": 20
|
||||
},
|
||||
"styles": {
|
||||
"Heading 1": {
|
||||
"alignment": "center",
|
||||
"size_pt": 14,
|
||||
"all_caps": true,
|
||||
"left_indent_cm": 0,
|
||||
"first_line_indent_cm": 0
|
||||
},
|
||||
"Normal": {
|
||||
"first_line_indent_cm": 1.25,
|
||||
"line_spacing": 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ datas = [
|
||||
(str(ROOT / "md2gost" / "mml2omml"), "md2gost"),
|
||||
(str(ROOT / "md2gost" / "diagrams"), "md2gost/diagrams"),
|
||||
(str(ROOT / "prompts"), "prompts"),
|
||||
(str(ROOT / "docs"), "docs"),
|
||||
]
|
||||
_plantuml = ROOT / "md2gost" / "vendor" / "plantuml.jar"
|
||||
if not _plantuml.is_file():
|
||||
|
||||
@@ -14,6 +14,7 @@ datas = [
|
||||
(str(ROOT / "md2gost" / "mml2omml"), "md2gost"),
|
||||
(str(ROOT / "md2gost" / "diagrams"), "md2gost/diagrams"),
|
||||
(str(ROOT / "prompts"), "prompts"),
|
||||
(str(ROOT / "docs"), "docs"),
|
||||
]
|
||||
_plantuml = ROOT / "md2gost" / "vendor" / "plantuml.jar"
|
||||
if _plantuml.is_file():
|
||||
|
||||
+3
-1
@@ -2,6 +2,8 @@
|
||||
|
||||
Конвертер Markdown → DOCX по методическим указаниям РТУ МИРЭА (ГОСТ 7.32-2017) на базе [md2gost](https://github.com/benzlokzik/md2gost).
|
||||
|
||||
**Полная документация:** [`docs/`](../docs/) (быстрый старт, синтаксис, схемы, типы, CLI/GUI, промпты).
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
@@ -23,7 +25,7 @@ md2gost-gui
|
||||
|
||||
На Windows файл можно бросить из Проводника в верхнюю область окна. Клик по области — выбор через диалог. Все флаги CLI есть в форме (тип, нумерация, TOC, тире, `---` → разрыв страницы, титул/задание, диаграммы, проверка ТЗ).
|
||||
|
||||
Вкладки **Инструкция** и **Промпт для ИИ** — справка и копирование системного промпта (МИРЭА / ПИС) в буфер.
|
||||
Вкладки **Инструкция** и **Промпт для ИИ** — справка и копирование системного промпта (с опциональным дописыванием UML-схем) в буфер.
|
||||
|
||||
### Сборка exe (Windows)
|
||||
|
||||
|
||||
@@ -166,6 +166,14 @@ def build_parser() -> ArgumentParser:
|
||||
dest="schemes_path",
|
||||
help="Путь к md2gost.schemes.json (иначе рядом с приложением / с .md)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--styles",
|
||||
dest="styles_path",
|
||||
help=(
|
||||
"Путь к md2gost.styles.json (оверлей оформления поверх пресета --type; "
|
||||
"также подхватывается md2gost.styles.json рядом с .md)"
|
||||
),
|
||||
)
|
||||
parser.add_argument("--debug", help="Добавляет отладочные данные в документ",
|
||||
action="store_true")
|
||||
return parser
|
||||
@@ -197,6 +205,7 @@ def request_from_args(args) -> ConvertRequest:
|
||||
diagram_format=args.diagram_format,
|
||||
diagram_scale=float(args.diagram_scale),
|
||||
schemes_path=args.schemes_path,
|
||||
styles_path=getattr(args, "styles_path", None),
|
||||
debug=args.debug,
|
||||
open_when_done=bool(args.debug),
|
||||
)
|
||||
|
||||
+16
-2
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
import docx
|
||||
from docx.document import Document
|
||||
|
||||
@@ -43,7 +45,8 @@ class Converter:
|
||||
toc_mode: str = DEFAULT_TOC_MODE,
|
||||
table_continuation: str = DEFAULT_TABLE_CONTINUATION,
|
||||
listing_continuation: str = DEFAULT_LISTING_CONTINUATION,
|
||||
hr_pagebreak: bool = False):
|
||||
hr_pagebreak: bool = False,
|
||||
styles_path: str | None = None):
|
||||
if heading_numbering not in HEADING_NUMBERING_MODES:
|
||||
raise ValueError(
|
||||
f"heading_numbering must be one of {HEADING_NUMBERING_MODES}, "
|
||||
@@ -71,10 +74,17 @@ class Converter:
|
||||
self._table_continuation = table_continuation
|
||||
self._listing_continuation = listing_continuation
|
||||
self._hr_pagebreak = hr_pagebreak
|
||||
self._styles_path = styles_path
|
||||
self._profile = get_profile(doc_type)
|
||||
self._document: Document = docx.Document(template_path)
|
||||
self._document._body.clear_content()
|
||||
apply_document_styles(self._document, self._profile.style_preset)
|
||||
md_dir = os.path.dirname(os.path.abspath(input_path)) or "."
|
||||
self._style_config = apply_document_styles(
|
||||
self._document,
|
||||
self._profile.style_preset,
|
||||
md_dir=md_dir,
|
||||
styles_path=styles_path,
|
||||
)
|
||||
self._debugger = Debugger(self._document) if debug else None
|
||||
with open(input_path, encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
@@ -123,6 +133,10 @@ class Converter:
|
||||
def document(self) -> Document:
|
||||
return self._document
|
||||
|
||||
@property
|
||||
def style_config(self):
|
||||
return self._style_config
|
||||
|
||||
@property
|
||||
def raw_markdown(self) -> str:
|
||||
return self._raw_markdown
|
||||
|
||||
+175
-15
@@ -14,13 +14,20 @@ from .diagram_schemes import (
|
||||
DiagramScheme,
|
||||
SCHEME_ID_RE,
|
||||
ensure_user_schemes,
|
||||
get_schemes,
|
||||
load_schemes_from_path,
|
||||
reload_schemes,
|
||||
save_schemes,
|
||||
user_schemes_path,
|
||||
)
|
||||
from .dnd import enable_file_drop, first_markdown
|
||||
from .help_content import SCHEMES_HELP, USAGE_HELP, load_prompt_catalog
|
||||
from .help_content import (
|
||||
SCHEMES_HELP,
|
||||
USAGE_HELP,
|
||||
compose_prompt,
|
||||
load_docs_catalog,
|
||||
load_prompt_catalog,
|
||||
)
|
||||
from .pipeline import ConvertRequest, convert, default_output_path, timestamped_output_path
|
||||
from .profiles import (
|
||||
DEFAULT_HEADING_NUMBERING,
|
||||
@@ -244,6 +251,7 @@ class Md2GostApp:
|
||||
self.template_var = tk.StringVar()
|
||||
self.title_var = tk.StringVar()
|
||||
self.assignment_var = tk.StringVar()
|
||||
self.styles_var = tk.StringVar()
|
||||
self.fallback_var = tk.StringVar()
|
||||
self.format_var = tk.StringVar()
|
||||
self.plantuml_var = tk.StringVar()
|
||||
@@ -266,6 +274,7 @@ class Md2GostApp:
|
||||
|
||||
help_menu = tk.Menu(menubar, tearoff=0)
|
||||
help_menu.add_command(label="Инструкция", command=lambda: self._open_help_window("usage"))
|
||||
help_menu.add_command(label="Документация", command=lambda: self._open_help_window("docs"))
|
||||
help_menu.add_command(label="Схемы", command=lambda: self._open_help_window("schemes"))
|
||||
help_menu.add_command(label="Промпт для ИИ", command=lambda: self._open_help_window("prompts"))
|
||||
menubar.add_cascade(label="Справка", menu=help_menu)
|
||||
@@ -313,8 +322,8 @@ class Md2GostApp:
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("Настройки — Файлы")
|
||||
win.configure(bg=PAPER)
|
||||
win.minsize(520, 200)
|
||||
win.geometry("620x240")
|
||||
win.minsize(520, 240)
|
||||
win.geometry("620x280")
|
||||
win.transient(self.root)
|
||||
frame = ttk.Frame(win, padding=12)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
@@ -325,13 +334,16 @@ class Md2GostApp:
|
||||
tip="DOCX титула, вставляется перед телом отчёта")
|
||||
self._row_path(frame, 2, "Бланк задания", self.assignment_var, self._browse_assignment, clearable=True,
|
||||
tip="DOCX бланка задания, вставляется перед телом отчёта")
|
||||
self._row_path(frame, 3, "Стили JSON", self.styles_var, self._browse_styles, clearable=True,
|
||||
tip="Оверлей md2gost.styles.json поверх пресета типа документа")
|
||||
hint = ttk.Label(
|
||||
frame,
|
||||
text="Пустой шаблон — встроенный Template.docx. Титул и задание вставляются перед телом отчёта.",
|
||||
text="Пустой шаблон — встроенный Template.docx. Титул и задание вставляются перед телом отчёта.\n"
|
||||
"Стили JSON — опционально; также подхватывается md2gost.styles.json рядом с .md.",
|
||||
style="Hint.TLabel",
|
||||
wraplength=560,
|
||||
)
|
||||
hint.grid(row=3, column=0, columnspan=2, sticky="w", pady=(8, 0))
|
||||
hint.grid(row=4, column=0, columnspan=2, sticky="w", pady=(8, 0))
|
||||
win.protocol("WM_DELETE_WINDOW", lambda: self._close_toplevel(win, "_win_files"))
|
||||
return win
|
||||
|
||||
@@ -456,6 +468,7 @@ class Md2GostApp:
|
||||
def _create_help_window(self, kind: str) -> tk.Toplevel:
|
||||
titles = {
|
||||
"usage": "Справка — Инструкция",
|
||||
"docs": "Справка — Документация",
|
||||
"schemes": "Справка — Схемы",
|
||||
"prompts": "Справка — Промпт для ИИ",
|
||||
}
|
||||
@@ -463,12 +476,17 @@ class Md2GostApp:
|
||||
win.title(titles.get(kind, "Справка"))
|
||||
win.configure(bg=PAPER)
|
||||
win.minsize(560, 400)
|
||||
win.geometry("700x560")
|
||||
if kind == "docs":
|
||||
win.geometry("780x560")
|
||||
else:
|
||||
win.geometry("700x560")
|
||||
win.transient(self.root)
|
||||
frame = ttk.Frame(win, padding=12)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
if kind == "usage":
|
||||
self._build_help(frame)
|
||||
elif kind == "docs":
|
||||
self._build_docs_browser(frame)
|
||||
elif kind == "schemes":
|
||||
self._build_schemes_help(frame)
|
||||
else:
|
||||
@@ -540,6 +558,60 @@ class Md2GostApp:
|
||||
box = self._readonly_text(parent, USAGE_HELP.strip() + "\n")
|
||||
box.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
def _build_docs_browser(self, parent) -> None:
|
||||
ttk.Label(
|
||||
parent,
|
||||
text="Страницы слева; ссылки вида (file.md) в тексте открывайте тем же списком. "
|
||||
"Markdown показывается как есть.",
|
||||
style="Hint.TLabel",
|
||||
wraplength=740,
|
||||
).pack(anchor=tk.W, pady=(0, 8))
|
||||
|
||||
catalog = load_docs_catalog()
|
||||
if not catalog:
|
||||
ttk.Label(
|
||||
parent,
|
||||
text="Документация не найдена (папка docs/ рядом с программой или внутри сборки).",
|
||||
style="Hint.TLabel",
|
||||
).pack(anchor=tk.W)
|
||||
return
|
||||
|
||||
self._docs_catalog = catalog
|
||||
body = ttk.Frame(parent)
|
||||
body.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
left = ttk.Frame(body)
|
||||
left.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
|
||||
self._docs_list = tk.Listbox(left, height=22, width=28, exportselection=False)
|
||||
self._docs_list.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
||||
for _name, title, _text in catalog:
|
||||
self._docs_list.insert(tk.END, title)
|
||||
self._docs_list.bind("<<ListboxSelect>>", self._on_docs_select)
|
||||
_tip(self._docs_list, "Разделы документации (docs/*.md)")
|
||||
|
||||
right = ttk.Frame(body)
|
||||
right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
self._docs_view = self._readonly_text(right, catalog[0][2])
|
||||
self._docs_view.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self._docs_list.selection_set(0)
|
||||
self._docs_list.see(0)
|
||||
|
||||
def _on_docs_select(self, _event=None) -> None:
|
||||
if not hasattr(self, "_docs_list") or not hasattr(self, "_docs_catalog"):
|
||||
return
|
||||
sel = self._docs_list.curselection()
|
||||
if not sel:
|
||||
return
|
||||
idx = int(sel[0])
|
||||
if idx < 0 or idx >= len(self._docs_catalog):
|
||||
return
|
||||
text = self._docs_catalog[idx][2]
|
||||
self._docs_view.configure(state=tk.NORMAL)
|
||||
self._docs_view.delete("1.0", tk.END)
|
||||
self._docs_view.insert("1.0", text)
|
||||
self._docs_view.configure(state=tk.DISABLED)
|
||||
|
||||
def _build_schemes_help(self, parent) -> None:
|
||||
ttk.Label(
|
||||
parent,
|
||||
@@ -553,7 +625,9 @@ class Md2GostApp:
|
||||
catalog = load_prompt_catalog()
|
||||
ttk.Label(
|
||||
parent,
|
||||
text="Скопируйте промпт целиком и вставьте в ChatGPT / Cursor / Copilot. Следующим сообщением — тип, тема, черновик.",
|
||||
text="Скопируйте промпт и вставьте в ChatGPT / Cursor / Copilot. "
|
||||
"Кнопки схем дописывают макросы из md2gost.schemes.json — "
|
||||
"когда ИИ должен знать C4/BPMN/свои шаблоны. Следующим сообщением — тип, тема, черновик.",
|
||||
style="Hint.TLabel",
|
||||
wraplength=640,
|
||||
).pack(anchor=tk.W, pady=(0, 8))
|
||||
@@ -575,32 +649,113 @@ class Md2GostApp:
|
||||
ttk.Label(row, text="Промпт").pack(side=tk.LEFT, padx=(0, 8))
|
||||
box = ttk.Combobox(row, textvariable=self._prompt_choice, values=titles, state="readonly")
|
||||
box.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
box.bind("<<ComboboxSelected>>", lambda _e: self._show_selected_prompt())
|
||||
box.bind("<<ComboboxSelected>>", lambda _e: self._refresh_composed_prompt())
|
||||
btn = ttk.Button(row, text="Скопировать", command=self._copy_selected_prompt)
|
||||
btn.pack(side=tk.LEFT, padx=(8, 0))
|
||||
_tip(btn, "Скопировать выбранный промпт в буфер обмена")
|
||||
_tip(btn, "Скопировать собранный промпт (база + выбранные схемы) в буфер")
|
||||
|
||||
self._prompt_view = self._readonly_text(parent, self._prompt_texts[titles[0]])
|
||||
schemes_frame = ttk.LabelFrame(parent, text="Дописать схемы в промпт", padding=6)
|
||||
schemes_frame.pack(fill=tk.X, pady=(0, 6))
|
||||
ttk.Label(
|
||||
schemes_frame,
|
||||
text="UML/Mermaid и так конвертируются в Рисунок; схемы нужны для макросов.",
|
||||
style="Hint.TLabel",
|
||||
wraplength=620,
|
||||
).pack(anchor=tk.W, pady=(0, 4))
|
||||
|
||||
try:
|
||||
ensure_user_schemes()
|
||||
schemes_map = get_schemes()
|
||||
except Exception:
|
||||
schemes_map = {}
|
||||
self._prompt_scheme_ids = sorted(schemes_map.keys())
|
||||
self._prompt_scheme_vars: dict[str, tk.BooleanVar] = {}
|
||||
self._prompt_schemes_by_id = dict(schemes_map)
|
||||
|
||||
toggles = ttk.Frame(schemes_frame)
|
||||
toggles.pack(fill=tk.X)
|
||||
if not self._prompt_scheme_ids:
|
||||
ttk.Label(
|
||||
toggles,
|
||||
text="Схемы не найдены (md2gost.schemes.json).",
|
||||
style="Hint.TLabel",
|
||||
).pack(anchor=tk.W)
|
||||
else:
|
||||
for sid in self._prompt_scheme_ids:
|
||||
var = tk.BooleanVar(value=False)
|
||||
self._prompt_scheme_vars[sid] = var
|
||||
title = schemes_map[sid].title or sid
|
||||
cb = ttk.Checkbutton(
|
||||
toggles,
|
||||
text=f"{title} ({sid})",
|
||||
variable=var,
|
||||
command=self._refresh_composed_prompt,
|
||||
)
|
||||
cb.pack(anchor=tk.W)
|
||||
_tip(cb, f"Добавить в промпт макросы схемы ```uml-{sid}")
|
||||
|
||||
ctrl = ttk.Frame(schemes_frame)
|
||||
ctrl.pack(fill=tk.X, pady=(4, 0))
|
||||
btn_all = ttk.Button(ctrl, text="Все", command=self._prompt_schemes_select_all)
|
||||
btn_all.pack(side=tk.LEFT, padx=(0, 6))
|
||||
_tip(btn_all, "Включить все схемы")
|
||||
btn_none = ttk.Button(ctrl, text="Сбросить", command=self._prompt_schemes_clear)
|
||||
btn_none.pack(side=tk.LEFT)
|
||||
_tip(btn_none, "Выключить все схемы")
|
||||
|
||||
self._prompt_view = self._readonly_text(parent, "")
|
||||
self._prompt_view.pack(fill=tk.BOTH, expand=True)
|
||||
self._copy_status = ttk.Label(parent, text="", style="Hint.TLabel")
|
||||
self._copy_status.pack(anchor=tk.W, pady=(6, 0))
|
||||
self._refresh_composed_prompt()
|
||||
|
||||
def _show_selected_prompt(self) -> None:
|
||||
text = self._prompt_texts.get(self._prompt_choice.get(), "")
|
||||
def _selected_prompt_schemes(self) -> list:
|
||||
out = []
|
||||
for sid in getattr(self, "_prompt_scheme_ids", []):
|
||||
var = self._prompt_scheme_vars.get(sid)
|
||||
if var is not None and var.get():
|
||||
scheme = self._prompt_schemes_by_id.get(sid)
|
||||
if scheme is not None:
|
||||
out.append(scheme)
|
||||
return out
|
||||
|
||||
def _composed_prompt_text(self) -> str:
|
||||
base = self._prompt_texts.get(self._prompt_choice.get(), "")
|
||||
return compose_prompt(base, self._selected_prompt_schemes())
|
||||
|
||||
def _refresh_composed_prompt(self) -> None:
|
||||
if not hasattr(self, "_prompt_view"):
|
||||
return
|
||||
text = self._composed_prompt_text()
|
||||
self._prompt_view.configure(state=tk.NORMAL)
|
||||
self._prompt_view.delete("1.0", tk.END)
|
||||
self._prompt_view.insert("1.0", text)
|
||||
self._prompt_view.configure(state=tk.DISABLED)
|
||||
self._copy_status.configure(text="")
|
||||
if hasattr(self, "_copy_status"):
|
||||
self._copy_status.configure(text="")
|
||||
|
||||
def _prompt_schemes_select_all(self) -> None:
|
||||
for var in self._prompt_scheme_vars.values():
|
||||
var.set(True)
|
||||
self._refresh_composed_prompt()
|
||||
|
||||
def _prompt_schemes_clear(self) -> None:
|
||||
for var in self._prompt_scheme_vars.values():
|
||||
var.set(False)
|
||||
self._refresh_composed_prompt()
|
||||
|
||||
def _copy_selected_prompt(self) -> None:
|
||||
text = self._prompt_texts.get(self._prompt_choice.get(), "")
|
||||
text = self._composed_prompt_text()
|
||||
if not text:
|
||||
return
|
||||
self.root.clipboard_clear()
|
||||
self.root.clipboard_append(text)
|
||||
self.root.update_idletasks()
|
||||
self._copy_status.configure(text="Скопировано в буфер обмена — можно вставлять в ИИ.")
|
||||
n = len(self._selected_prompt_schemes())
|
||||
extra = f" (+{n} схем)" if n else ""
|
||||
self._copy_status.configure(
|
||||
text=f"Скопировано в буфер обмена{extra} — можно вставлять в ИИ."
|
||||
)
|
||||
|
||||
def _readonly_text(self, parent, content: str) -> tk.Text:
|
||||
wrap = ttk.Frame(parent)
|
||||
@@ -1065,6 +1220,9 @@ class Md2GostApp:
|
||||
def _browse_assignment(self) -> None:
|
||||
self._pick_into(self.assignment_var, "Бланк задания", [("Word", "*.docx")])
|
||||
|
||||
def _browse_styles(self) -> None:
|
||||
self._pick_into(self.styles_var, "Стили JSON", [("JSON", "*.json"), ("Все файлы", "*.*")])
|
||||
|
||||
def _browse_jar(self) -> None:
|
||||
self._pick_into(self.plantuml_var, "Свой plantuml.jar", [("JAR", "*.jar"), ("Все файлы", "*.*")])
|
||||
self._refresh_diagram_status()
|
||||
@@ -1149,6 +1307,7 @@ class Md2GostApp:
|
||||
self.template_var.set(req.template or "")
|
||||
self.title_var.set(req.title or "")
|
||||
self.assignment_var.set(req.assignment or "")
|
||||
self.styles_var.set(getattr(req, "styles_path", None) or "")
|
||||
self.plantuml_var.set(req.plantuml_jar or "")
|
||||
self.kroki_var.set(req.kroki_url or "")
|
||||
self._refresh_diagram_status()
|
||||
@@ -1181,6 +1340,7 @@ class Md2GostApp:
|
||||
diagram_fallback=_combo_key(self.fallback_var.get(), ("remote", "local", "off"), "remote"),
|
||||
diagram_format=_combo_key(self.format_var.get(), ("png", "svg"), "png"),
|
||||
diagram_scale=self._parse_diagram_scale(),
|
||||
styles_path=self.styles_var.get().strip() or None,
|
||||
debug=self.debug_var.get(),
|
||||
open_when_done=self.open_var.get(),
|
||||
)
|
||||
|
||||
+111
-1
@@ -33,6 +33,13 @@ CLI (тот же движок)
|
||||
python -m md2gost --gui
|
||||
md2gost.exe report.md --type PIS_custom --title title.docx
|
||||
md2gost.exe report.md --schemes path/to/md2gost.schemes.json
|
||||
md2gost.exe report.md --styles path/to/md2gost.styles.json
|
||||
|
||||
Стили JSON (опционально)
|
||||
Оверлей поверх пресета типа документа (--type). Файл md2gost.styles.json рядом с .md
|
||||
или --styles / Настройки → Файлы → «Стили JSON». Меняет поля страницы и параметры
|
||||
стилей абзацев (Normal, Heading 1–3, подписи…). В шаблон DOCX стили руками добавлять не нужно.
|
||||
Подробнее: docs/styles.md.
|
||||
|
||||
СИНТАКСИС MARKDOWN
|
||||
|
||||
@@ -126,7 +133,12 @@ IDEF0 конвертер не рисует — вставляйте готовы
|
||||
off — не резать, Word сам переносит.
|
||||
legacy / caption — режем по оценке высоты в md2gost (может не совпасть с Word).
|
||||
|
||||
Промпт для ИИ — Справка → Промпт для ИИ: скопируйте и вставьте в ChatGPT / Cursor / Copilot, затем дайте тему и черновик.
|
||||
Промпт для ИИ — Справка → Промпт для ИИ: выберите промпт, при необходимости
|
||||
включите схемы (C4, BPMN, …) кнопками — макросы допишутся в конец — скопируйте
|
||||
в ChatGPT / Cursor / Copilot, затем дайте тему и черновик.
|
||||
|
||||
Документация — Справка → Документация: встроенный просмотр docs/*.md
|
||||
(вшито в exe; внешняя папка docs/ не обязательна).
|
||||
"""
|
||||
|
||||
SCHEMES_HELP = """СХЕМЫ ДИАГРАММ (PlantUML)
|
||||
@@ -219,6 +231,7 @@ Mermaid: только Kroki (свой --kroki-url / localhost / kroki.io).
|
||||
"""
|
||||
|
||||
PROMPT_FILES = (
|
||||
("generate-md.md", "Markdown для md2gost"),
|
||||
("generate-mirea-report.md", "МИРЭА / ГОСТ (курсовая, практика, ВКР)"),
|
||||
("generate-pis-custom-report.md", "ПИС — отчёт по практическим работам"),
|
||||
)
|
||||
@@ -259,3 +272,100 @@ def load_prompt_catalog() -> list[tuple[str, str, str]]:
|
||||
if text:
|
||||
catalog.append((name, title, text))
|
||||
return catalog
|
||||
|
||||
|
||||
def scheme_prompt_block(scheme) -> str:
|
||||
"""Format one DiagramScheme for appending to an AI prompt."""
|
||||
sid = getattr(scheme, "id", "") or ""
|
||||
title = (getattr(scheme, "title", None) or sid).strip()
|
||||
lines = [
|
||||
f"## Схема: {title} (`{sid}`)",
|
||||
"",
|
||||
f"Оградка в markdown: ```uml-{sid} или ```{sid}",
|
||||
"",
|
||||
]
|
||||
ai = (getattr(scheme, "ai_prompt", None) or "").strip()
|
||||
if ai:
|
||||
lines.append(ai)
|
||||
lines.append("")
|
||||
docs = (getattr(scheme, "docs", None) or "").strip()
|
||||
if docs:
|
||||
lines.append("Макросы / шпаргалка:")
|
||||
lines.append(docs)
|
||||
lines.append("")
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def compose_prompt(base: str, schemes: list | None = None) -> str:
|
||||
"""
|
||||
Base prompt text plus optional scheme blocks (order preserved).
|
||||
schemes: iterable of DiagramScheme (or objects with id/title/docs/ai_prompt).
|
||||
"""
|
||||
text = (base or "").rstrip()
|
||||
if not schemes:
|
||||
return text + ("\n" if text else "")
|
||||
parts = [text, "", "---", "", "# Дополнение: выбранные схемы диаграмм", ""]
|
||||
for scheme in schemes:
|
||||
parts.append(scheme_prompt_block(scheme))
|
||||
parts.append("")
|
||||
return "\n".join(parts).rstrip() + "\n"
|
||||
|
||||
|
||||
def docs_search_dirs() -> list[Path]:
|
||||
dirs: list[Path] = []
|
||||
here = Path(package_dir())
|
||||
dirs.append(here / "docs")
|
||||
dirs.append(here.parent / "docs")
|
||||
if getattr(sys, "frozen", False):
|
||||
mei = getattr(sys, "_MEIPASS", None)
|
||||
if mei:
|
||||
dirs.append(Path(mei) / "docs")
|
||||
dirs.append(Path(sys.executable).resolve().parent / "docs")
|
||||
seen: set[str] = set()
|
||||
out: list[Path] = []
|
||||
for path in dirs:
|
||||
key = str(path.resolve()) if path.exists() else str(path)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(path)
|
||||
return out
|
||||
|
||||
|
||||
def _doc_title_from_text(filename: str, text: str) -> str:
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("#"):
|
||||
return stripped.lstrip("#").strip() or Path(filename).stem
|
||||
return Path(filename).stem
|
||||
|
||||
|
||||
def load_docs_catalog() -> list[tuple[str, str, str]]:
|
||||
"""
|
||||
Return list of (filename, title, text) from the first existing docs/ folder.
|
||||
README.md first, then other *.md alphabetically.
|
||||
"""
|
||||
folder: Path | None = None
|
||||
for path in docs_search_dirs():
|
||||
if path.is_dir():
|
||||
folder = path
|
||||
break
|
||||
if folder is None:
|
||||
return []
|
||||
|
||||
files = sorted(p for p in folder.glob("*.md") if p.is_file())
|
||||
if not files:
|
||||
return []
|
||||
|
||||
readme = [p for p in files if p.name.lower() == "readme.md"]
|
||||
rest = [p for p in files if p.name.lower() != "readme.md"]
|
||||
ordered = readme + rest
|
||||
|
||||
catalog: list[tuple[str, str, str]] = []
|
||||
for path in ordered:
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
continue
|
||||
catalog.append((path.name, _doc_title_from_text(path.name, text), text))
|
||||
return catalog
|
||||
|
||||
+6
-4
@@ -22,7 +22,6 @@ from .profiles import (
|
||||
DEFAULT_TABLE_CONTINUATION,
|
||||
DEFAULT_LISTING_CONTINUATION,
|
||||
DEFAULT_TOC_MODE,
|
||||
get_profile,
|
||||
)
|
||||
|
||||
LogFn = Callable[[str], None]
|
||||
@@ -52,6 +51,7 @@ class ConvertRequest:
|
||||
diagram_format: str = "png"
|
||||
diagram_scale: float = 2.0
|
||||
schemes_path: str | None = None
|
||||
styles_path: str | None = None
|
||||
debug: bool = False
|
||||
open_when_done: bool = False
|
||||
check_pages: bool = False
|
||||
@@ -219,6 +219,7 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
table_continuation=req.table_continuation,
|
||||
listing_continuation=req.listing_continuation,
|
||||
hr_pagebreak=req.hr_pagebreak,
|
||||
styles_path=req.styles_path or None,
|
||||
)
|
||||
converter.convert()
|
||||
document = converter.document
|
||||
@@ -228,10 +229,11 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
from docxcompose.composer import Composer
|
||||
except ImportError:
|
||||
return fail(3, "Для титула/задания нужен пакет docxcompose", check_report)
|
||||
from .styles import apply_document_styles
|
||||
from .styles import apply_style_config
|
||||
|
||||
style_cfg = converter.style_config
|
||||
shell = Document(template)
|
||||
apply_document_styles(shell, get_profile(req.doc_type).style_preset)
|
||||
apply_style_config(shell, style_cfg)
|
||||
body = shell.element.body
|
||||
for child in list(body):
|
||||
if not child.tag.endswith("}sectPr"):
|
||||
@@ -246,7 +248,7 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
shell.add_page_break()
|
||||
composer.append(document)
|
||||
document = composer.doc
|
||||
apply_document_styles(document, get_profile(req.doc_type).style_preset)
|
||||
apply_style_config(document, style_cfg)
|
||||
_fix_front_matter_after_compose(
|
||||
document,
|
||||
had_title=bool(req.title),
|
||||
|
||||
@@ -0,0 +1,597 @@
|
||||
"""JSON overlay for document styles (page margins + paragraph styles).
|
||||
|
||||
Layers (later wins, deep-merge of specified keys only):
|
||||
1) built-in preset from DocProfile.style_preset (mirea | pis_custom)
|
||||
2) md2gost.styles.json next to the .md (if present)
|
||||
3) --styles path / GUI path
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, field, fields
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
STYLES_FILENAME = "md2gost.styles.json"
|
||||
|
||||
PAGE_KEYS = frozenset({"left_mm", "right_mm", "top_mm", "bottom_mm"})
|
||||
|
||||
STYLE_NAMES = frozenset({
|
||||
"Normal",
|
||||
"Heading 1",
|
||||
"Heading 2",
|
||||
"Heading 3",
|
||||
"Caption Figure",
|
||||
"Caption Table",
|
||||
"Название таблицы",
|
||||
"Caption Listing",
|
||||
"Caption",
|
||||
"Code",
|
||||
"Table Text",
|
||||
"Bibliography",
|
||||
"Bibliography Heading",
|
||||
"toc 1",
|
||||
"toc 2",
|
||||
"toc 3",
|
||||
"Footer",
|
||||
"Hyperlink",
|
||||
"FollowedHyperlink",
|
||||
"Space After Table",
|
||||
})
|
||||
|
||||
ALIGNMENT_VALUES = frozenset({"left", "center", "justify"})
|
||||
LINE_SPACING_VALUES = frozenset({1.0, 1.5})
|
||||
|
||||
STYLE_FIELD_KEYS = frozenset({
|
||||
"font_name",
|
||||
"size_pt",
|
||||
"bold",
|
||||
"italic",
|
||||
"all_caps",
|
||||
"underline",
|
||||
"alignment",
|
||||
"first_line_indent_cm",
|
||||
"left_indent_cm",
|
||||
"right_indent_cm",
|
||||
"space_before_mm",
|
||||
"space_after_mm",
|
||||
"line_spacing",
|
||||
"page_break_before",
|
||||
"keep_with_next",
|
||||
"widow_control",
|
||||
})
|
||||
|
||||
TOP_LEVEL_KEYS = frozenset({"page", "styles"})
|
||||
|
||||
|
||||
class StyleConfigError(ValueError):
|
||||
"""Invalid styles JSON or unknown key/style name."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class PageSpec:
|
||||
left_mm: float | None = None
|
||||
right_mm: float | None = None
|
||||
top_mm: float | None = None
|
||||
bottom_mm: float | None = None
|
||||
|
||||
def merge(self, overlay: "PageSpec | dict[str, Any] | None") -> "PageSpec":
|
||||
if overlay is None:
|
||||
return copy.copy(self)
|
||||
if isinstance(overlay, PageSpec):
|
||||
data = {
|
||||
f.name: getattr(overlay, f.name)
|
||||
for f in fields(overlay)
|
||||
if getattr(overlay, f.name) is not None
|
||||
}
|
||||
else:
|
||||
data = dict(overlay)
|
||||
out = copy.copy(self)
|
||||
for key, val in data.items():
|
||||
if key not in PAGE_KEYS:
|
||||
raise StyleConfigError(f"Unknown page key: {key!r}")
|
||||
setattr(out, key, float(val))
|
||||
return out
|
||||
|
||||
def to_dict(self) -> dict[str, float]:
|
||||
out: dict[str, float] = {}
|
||||
for f in fields(self):
|
||||
val = getattr(self, f.name)
|
||||
if val is not None:
|
||||
out[f.name] = val
|
||||
return out
|
||||
|
||||
def require_complete(self) -> "PageSpec":
|
||||
"""Ensure all margins are set (for apply). Missing → GOST defaults."""
|
||||
return PageSpec(
|
||||
left_mm=30.0 if self.left_mm is None else self.left_mm,
|
||||
right_mm=10.0 if self.right_mm is None else self.right_mm,
|
||||
top_mm=20.0 if self.top_mm is None else self.top_mm,
|
||||
bottom_mm=20.0 if self.bottom_mm is None else self.bottom_mm,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParagraphStyleSpec:
|
||||
font_name: str | None = None
|
||||
size_pt: float | None = None
|
||||
bold: bool | None = None
|
||||
italic: bool | None = None
|
||||
all_caps: bool | None = None
|
||||
underline: bool | None = None
|
||||
alignment: str | None = None # left | center | justify
|
||||
first_line_indent_cm: float | None = None
|
||||
left_indent_cm: float | None = None
|
||||
right_indent_cm: float | None = None
|
||||
space_before_mm: float | None = None
|
||||
space_after_mm: float | None = None
|
||||
line_spacing: float | None = None # 1.0 | 1.5
|
||||
page_break_before: bool | None = None
|
||||
keep_with_next: bool | None = None
|
||||
widow_control: bool | None = None
|
||||
|
||||
def merge(self, overlay: "ParagraphStyleSpec | dict[str, Any] | None") -> "ParagraphStyleSpec":
|
||||
if overlay is None:
|
||||
return copy.copy(self)
|
||||
if isinstance(overlay, ParagraphStyleSpec):
|
||||
data = {
|
||||
f.name: getattr(overlay, f.name)
|
||||
for f in fields(overlay)
|
||||
if getattr(overlay, f.name) is not None
|
||||
}
|
||||
else:
|
||||
data = dict(overlay)
|
||||
out = copy.copy(self)
|
||||
for key, val in data.items():
|
||||
if key not in STYLE_FIELD_KEYS:
|
||||
raise StyleConfigError(f"Unknown style field: {key!r}")
|
||||
if val is None:
|
||||
continue
|
||||
if key == "alignment":
|
||||
s = str(val).lower()
|
||||
if s not in ALIGNMENT_VALUES:
|
||||
raise StyleConfigError(
|
||||
f"Invalid alignment {val!r}; expected one of {sorted(ALIGNMENT_VALUES)}"
|
||||
)
|
||||
setattr(out, key, s)
|
||||
elif key == "line_spacing":
|
||||
fval = float(val)
|
||||
if fval not in LINE_SPACING_VALUES:
|
||||
raise StyleConfigError(
|
||||
f"Invalid line_spacing {val!r}; expected 1.0 or 1.5"
|
||||
)
|
||||
setattr(out, key, fval)
|
||||
elif key in ("font_name",):
|
||||
setattr(out, key, str(val))
|
||||
elif key in ("size_pt", "first_line_indent_cm", "left_indent_cm",
|
||||
"right_indent_cm", "space_before_mm", "space_after_mm"):
|
||||
setattr(out, key, float(val))
|
||||
elif key in ("bold", "italic", "all_caps", "underline",
|
||||
"page_break_before", "keep_with_next", "widow_control"):
|
||||
setattr(out, key, bool(val))
|
||||
else:
|
||||
setattr(out, key, val)
|
||||
return out
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
out: dict[str, Any] = {}
|
||||
for f in fields(self):
|
||||
val = getattr(self, f.name)
|
||||
if val is not None:
|
||||
out[f.name] = val
|
||||
return out
|
||||
|
||||
|
||||
@dataclass
|
||||
class StyleConfig:
|
||||
page: PageSpec = field(default_factory=PageSpec)
|
||||
styles: dict[str, ParagraphStyleSpec] = field(default_factory=dict)
|
||||
|
||||
def merge(self, overlay: "StyleConfig | None") -> "StyleConfig":
|
||||
if overlay is None:
|
||||
return copy.deepcopy(self)
|
||||
page = self.page.merge(overlay.page)
|
||||
styles = {name: copy.copy(spec) for name, spec in self.styles.items()}
|
||||
for name, spec in overlay.styles.items():
|
||||
if name not in STYLE_NAMES:
|
||||
raise StyleConfigError(f"Unknown style name: {name!r}")
|
||||
if name in styles:
|
||||
styles[name] = styles[name].merge(spec)
|
||||
else:
|
||||
styles[name] = copy.copy(spec)
|
||||
return StyleConfig(page=page, styles=styles)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"page": self.page.to_dict(),
|
||||
"styles": {name: spec.to_dict() for name, spec in self.styles.items()},
|
||||
}
|
||||
|
||||
|
||||
def _ps(**kwargs) -> ParagraphStyleSpec:
|
||||
return ParagraphStyleSpec(**kwargs)
|
||||
|
||||
|
||||
def _common_body_and_misc() -> dict[str, ParagraphStyleSpec]:
|
||||
"""Shared Normal + captions/misc (identical for mirea and pis_custom)."""
|
||||
return {
|
||||
"Normal": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=False,
|
||||
italic=False,
|
||||
alignment="justify",
|
||||
first_line_indent_cm=1.25,
|
||||
left_indent_cm=0,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.5,
|
||||
widow_control=True,
|
||||
),
|
||||
"Caption Figure": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
bold=True,
|
||||
italic=False,
|
||||
alignment="center",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=6,
|
||||
line_spacing=1.0,
|
||||
widow_control=True,
|
||||
),
|
||||
"Caption Table": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
bold=False,
|
||||
italic=True,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
space_before_mm=6,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.0,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
),
|
||||
"Название таблицы": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
bold=False,
|
||||
italic=True,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
space_before_mm=6,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.0,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
),
|
||||
"Caption Listing": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
bold=False,
|
||||
italic=True,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
space_before_mm=6,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.0,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
),
|
||||
"Caption": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
bold=True,
|
||||
italic=False,
|
||||
alignment="center",
|
||||
first_line_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=6,
|
||||
line_spacing=1.0,
|
||||
),
|
||||
"Code": _ps(
|
||||
font_name="Courier New",
|
||||
size_pt=10,
|
||||
bold=False,
|
||||
italic=False,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.0,
|
||||
),
|
||||
"Table Text": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
bold=False,
|
||||
italic=False,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.0,
|
||||
),
|
||||
"Bibliography": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=False,
|
||||
italic=False,
|
||||
alignment="justify",
|
||||
first_line_indent_cm=1.25,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.5,
|
||||
),
|
||||
"Bibliography Heading": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=False,
|
||||
italic=False,
|
||||
all_caps=True,
|
||||
alignment="center",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=1.25,
|
||||
space_before_mm=6,
|
||||
space_after_mm=6,
|
||||
line_spacing=1.5,
|
||||
keep_with_next=True,
|
||||
),
|
||||
"toc 1": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=False,
|
||||
all_caps=True,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.5,
|
||||
),
|
||||
"toc 2": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=False,
|
||||
all_caps=False,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.5,
|
||||
),
|
||||
"toc 3": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=False,
|
||||
all_caps=False,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.5,
|
||||
),
|
||||
"Footer": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
alignment="center",
|
||||
first_line_indent_cm=0,
|
||||
),
|
||||
"Hyperlink": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
underline=False,
|
||||
),
|
||||
"FollowedHyperlink": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
underline=False,
|
||||
),
|
||||
"Space After Table": _ps(
|
||||
first_line_indent_cm=1.25,
|
||||
space_before_mm=6,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.5,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def preset_mirea() -> StyleConfig:
|
||||
styles = _common_body_and_misc()
|
||||
styles["Heading 1"] = _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=18,
|
||||
bold=True,
|
||||
all_caps=True,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=1.25,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=10,
|
||||
line_spacing=1.5,
|
||||
page_break_before=True,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
)
|
||||
styles["Heading 2"] = _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=16,
|
||||
bold=True,
|
||||
all_caps=False,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=1.25,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=15,
|
||||
space_after_mm=10,
|
||||
line_spacing=1.5,
|
||||
page_break_before=False,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
)
|
||||
styles["Heading 3"] = _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=True,
|
||||
all_caps=False,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=1.25,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=15,
|
||||
space_after_mm=10,
|
||||
line_spacing=1.5,
|
||||
page_break_before=False,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
)
|
||||
return StyleConfig(
|
||||
page=PageSpec(left_mm=30, right_mm=10, top_mm=20, bottom_mm=20),
|
||||
styles=styles,
|
||||
)
|
||||
|
||||
|
||||
def preset_pis_custom() -> StyleConfig:
|
||||
styles = _common_body_and_misc()
|
||||
styles["Heading 1"] = _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=True,
|
||||
all_caps=True,
|
||||
alignment="center",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=10,
|
||||
line_spacing=1.5,
|
||||
page_break_before=True,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
)
|
||||
styles["Heading 2"] = _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=True,
|
||||
all_caps=False,
|
||||
alignment="justify",
|
||||
first_line_indent_cm=1.25,
|
||||
left_indent_cm=0,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=15,
|
||||
space_after_mm=10,
|
||||
line_spacing=1.5,
|
||||
page_break_before=False,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
)
|
||||
styles["Heading 3"] = _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=True,
|
||||
all_caps=False,
|
||||
alignment="justify",
|
||||
first_line_indent_cm=1.25,
|
||||
left_indent_cm=0,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=10,
|
||||
space_after_mm=10,
|
||||
line_spacing=1.5,
|
||||
page_break_before=False,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
)
|
||||
return StyleConfig(
|
||||
page=PageSpec(left_mm=30, right_mm=10, top_mm=20, bottom_mm=20),
|
||||
styles=styles,
|
||||
)
|
||||
|
||||
|
||||
PRESETS: dict[str, Callable[[], StyleConfig]] = {
|
||||
"mirea": preset_mirea,
|
||||
"pis_custom": preset_pis_custom,
|
||||
}
|
||||
|
||||
|
||||
def get_preset(name: str) -> StyleConfig:
|
||||
factory = PRESETS.get(name, preset_mirea)
|
||||
return factory()
|
||||
|
||||
|
||||
def style_config_from_dict(data: dict[str, Any]) -> StyleConfig:
|
||||
if not isinstance(data, dict):
|
||||
raise StyleConfigError("Styles JSON root must be an object")
|
||||
unknown = set(data.keys()) - TOP_LEVEL_KEYS
|
||||
if unknown:
|
||||
raise StyleConfigError(f"Unknown top-level key(s): {sorted(unknown)}")
|
||||
|
||||
page = PageSpec()
|
||||
if "page" in data:
|
||||
raw_page = data["page"]
|
||||
if not isinstance(raw_page, dict):
|
||||
raise StyleConfigError("'page' must be an object")
|
||||
page = PageSpec().merge(raw_page)
|
||||
|
||||
styles: dict[str, ParagraphStyleSpec] = {}
|
||||
if "styles" in data:
|
||||
raw_styles = data["styles"]
|
||||
if not isinstance(raw_styles, dict):
|
||||
raise StyleConfigError("'styles' must be an object")
|
||||
for name, raw_spec in raw_styles.items():
|
||||
if name not in STYLE_NAMES:
|
||||
raise StyleConfigError(f"Unknown style name: {name!r}")
|
||||
if not isinstance(raw_spec, dict):
|
||||
raise StyleConfigError(f"Style {name!r} must be an object")
|
||||
styles[name] = ParagraphStyleSpec().merge(raw_spec)
|
||||
|
||||
return StyleConfig(page=page, styles=styles)
|
||||
|
||||
|
||||
def load_styles_file(path: Path) -> StyleConfig:
|
||||
if not path.is_file():
|
||||
raise StyleConfigError(f"Styles file not found: {path}")
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise StyleConfigError(f"Cannot read styles {path}: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise StyleConfigError(f"Styles JSON root must be an object: {path}")
|
||||
return style_config_from_dict(raw)
|
||||
|
||||
|
||||
def resolve_style_config(
|
||||
style_preset: str,
|
||||
*,
|
||||
md_dir: str | Path | None = None,
|
||||
styles_path: str | Path | None = None,
|
||||
) -> StyleConfig:
|
||||
"""Merge preset ← near-md file ← explicit --styles path."""
|
||||
config = get_preset(style_preset)
|
||||
|
||||
if md_dir:
|
||||
near = Path(md_dir) / STYLES_FILENAME
|
||||
if near.is_file():
|
||||
try:
|
||||
config = config.merge(load_styles_file(near))
|
||||
_log.info("Applied styles overlay: %s", near)
|
||||
except StyleConfigError:
|
||||
raise
|
||||
|
||||
if styles_path:
|
||||
path = Path(styles_path)
|
||||
config = config.merge(load_styles_file(path))
|
||||
_log.info("Applied styles overlay: %s", path)
|
||||
|
||||
return config
|
||||
+156
-230
@@ -1,5 +1,7 @@
|
||||
"""Apply MIREA TZ (GOST 7.32 / методичка 2022) paragraph styles to a Document."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from docx.document import Document
|
||||
from docx.enum.style import WD_STYLE_TYPE
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING, WD_TAB_ALIGNMENT, WD_TAB_LEADER
|
||||
@@ -7,6 +9,32 @@ from docx.oxml.ns import qn
|
||||
from docx.shared import Cm, Mm, Pt
|
||||
from docx.styles.style import _ParagraphStyle as ParagraphStyle
|
||||
|
||||
from .style_config import (
|
||||
ParagraphStyleSpec,
|
||||
StyleConfig,
|
||||
get_preset,
|
||||
resolve_style_config,
|
||||
)
|
||||
|
||||
_ALIGN = {
|
||||
"left": WD_ALIGN_PARAGRAPH.LEFT,
|
||||
"center": WD_ALIGN_PARAGRAPH.CENTER,
|
||||
"justify": WD_ALIGN_PARAGRAPH.JUSTIFY,
|
||||
}
|
||||
|
||||
# Styles that may be missing from Template.docx and need ensure + base
|
||||
_ENSURE_BASE = {
|
||||
"Caption Figure": "Caption",
|
||||
"Caption Table": "Caption",
|
||||
"Название таблицы": "Caption Table",
|
||||
"Caption Listing": "Caption",
|
||||
"Code": "Normal",
|
||||
"Table Text": "Normal",
|
||||
"Bibliography": "Normal",
|
||||
"Bibliography Heading": "Normal",
|
||||
"Space After Table": "Normal",
|
||||
}
|
||||
|
||||
|
||||
def _set_run_font(style: ParagraphStyle, name: str, size_pt: float, bold: bool = False, italic: bool = False):
|
||||
"""Lock typeface/size/color so Word theme (Calibri + accent blue) cannot leak."""
|
||||
@@ -55,6 +83,18 @@ def _ensure_style(document: Document, name: str, base: str = "Normal") -> Paragr
|
||||
return style
|
||||
|
||||
|
||||
def _get_or_ensure(document: Document, name: str) -> ParagraphStyle | None:
|
||||
base = _ENSURE_BASE.get(name)
|
||||
if base is not None:
|
||||
return _ensure_style(document, name, base)
|
||||
try:
|
||||
return document.styles[name]
|
||||
except KeyError:
|
||||
if name.startswith("toc "):
|
||||
return _ensure_style(document, name, "Normal")
|
||||
return None
|
||||
|
||||
|
||||
def _clear_tab_stops(style: ParagraphStyle) -> None:
|
||||
pPr = style.element.get_or_add_pPr()
|
||||
tabs = pPr.find(qn("w:tabs"))
|
||||
@@ -88,249 +128,135 @@ def _fix_toc_tab_stops(document: Document) -> None:
|
||||
)
|
||||
|
||||
|
||||
def apply_mirea_styles(document: Document) -> None:
|
||||
"""Mutate section margins and key paragraph styles to match the MIREA method guide."""
|
||||
_apply_common_page_and_body(document)
|
||||
def _apply_paragraph_spec(style, spec: ParagraphStyleSpec) -> None:
|
||||
need_font = (
|
||||
spec.font_name is not None
|
||||
or spec.size_pt is not None
|
||||
or spec.bold is not None
|
||||
or spec.italic is not None
|
||||
)
|
||||
if need_font:
|
||||
name = spec.font_name if spec.font_name is not None else (style.font.name or "Times New Roman")
|
||||
size = spec.size_pt if spec.size_pt is not None else (
|
||||
style.font.size.pt if style.font.size else 14
|
||||
)
|
||||
bold = bool(spec.bold) if spec.bold is not None else bool(style.font.bold)
|
||||
italic = bool(spec.italic) if spec.italic is not None else bool(style.font.italic)
|
||||
_set_run_font(style, name, size, bold=bold, italic=italic)
|
||||
|
||||
# --- Headings (табл. 2.1): слева с отступом 1,25 см ---
|
||||
heading_specs = [
|
||||
(1, 18, Mm(0), Mm(10), True),
|
||||
(2, 16, Mm(15), Mm(10), False),
|
||||
(3, 14, Mm(15), Mm(10), False),
|
||||
]
|
||||
for level, size, before, after, page_break in heading_specs:
|
||||
style: ParagraphStyle = document.styles[f"Heading {level}"]
|
||||
_set_run_font(style, "Times New Roman", size, bold=True)
|
||||
if level == 1:
|
||||
style.font.all_caps = True
|
||||
if spec.all_caps is not None:
|
||||
style.font.all_caps = spec.all_caps
|
||||
if spec.underline is not None:
|
||||
style.font.underline = spec.underline
|
||||
|
||||
# Character styles (Hyperlink, …) have no paragraph_format
|
||||
if style.type != WD_STYLE_TYPE.PARAGRAPH:
|
||||
return
|
||||
|
||||
pf = style.paragraph_format
|
||||
if spec.alignment is not None:
|
||||
pf.alignment = _ALIGN[spec.alignment]
|
||||
if spec.first_line_indent_cm is not None:
|
||||
pf.first_line_indent = Cm(spec.first_line_indent_cm)
|
||||
if spec.left_indent_cm is not None:
|
||||
pf.left_indent = Cm(spec.left_indent_cm)
|
||||
if spec.right_indent_cm is not None:
|
||||
pf.right_indent = Cm(spec.right_indent_cm)
|
||||
if spec.space_before_mm is not None:
|
||||
pf.space_before = Mm(spec.space_before_mm)
|
||||
if spec.space_after_mm is not None:
|
||||
pf.space_after = Mm(spec.space_after_mm)
|
||||
if spec.line_spacing is not None:
|
||||
if spec.line_spacing == 1.5:
|
||||
pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
else:
|
||||
style.font.all_caps = False
|
||||
hpf = style.paragraph_format
|
||||
hpf.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
hpf.first_line_indent = Cm(0)
|
||||
hpf.left_indent = Cm(1.25)
|
||||
hpf.right_indent = Cm(0)
|
||||
hpf.space_before = before
|
||||
hpf.space_after = after
|
||||
hpf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
hpf.page_break_before = page_break
|
||||
hpf.keep_with_next = True
|
||||
hpf.widow_control = True
|
||||
|
||||
_apply_common_captions_and_misc(document)
|
||||
pf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
if spec.page_break_before is not None:
|
||||
pf.page_break_before = spec.page_break_before
|
||||
if spec.keep_with_next is not None:
|
||||
pf.keep_with_next = spec.keep_with_next
|
||||
if spec.widow_control is not None:
|
||||
pf.widow_control = spec.widow_control
|
||||
|
||||
|
||||
def apply_pis_custom_styles(document: Document) -> None:
|
||||
"""Styles for PIS_custom: итоговый отчёт по практическим работам.
|
||||
def _apply_page_margins(config: StyleConfig) -> None:
|
||||
from . import page_geometry as pg
|
||||
|
||||
* H1 (разделы / практические работы): по центру, ПРОПИСНЫЕ, без точки.
|
||||
* H2 (подразделы): с абзацного отступа 1,25 см, с прописной буквы.
|
||||
* Поля / шрифт / интервал / красная строка — как в чек-листе ПИС (= ГОСТ поля).
|
||||
"""
|
||||
_apply_common_page_and_body(document)
|
||||
|
||||
# H1 — раздел: центр, caps, с новой страницы
|
||||
h1: ParagraphStyle = document.styles["Heading 1"]
|
||||
_set_run_font(h1, "Times New Roman", 14, bold=True)
|
||||
h1.font.all_caps = True
|
||||
h1pf = h1.paragraph_format
|
||||
h1pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
h1pf.first_line_indent = Cm(0)
|
||||
h1pf.left_indent = Cm(0)
|
||||
h1pf.right_indent = Cm(0)
|
||||
h1pf.space_before = Mm(0)
|
||||
h1pf.space_after = Mm(10)
|
||||
h1pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
h1pf.page_break_before = True
|
||||
h1pf.keep_with_next = True
|
||||
h1pf.widow_control = True
|
||||
|
||||
# H2 / H3 — подразделы: абзацный отступ, не caps
|
||||
for level, size, before in ((2, 14, Mm(15)), (3, 14, Mm(10))):
|
||||
style = document.styles[f"Heading {level}"]
|
||||
_set_run_font(style, "Times New Roman", size, bold=True)
|
||||
style.font.all_caps = False
|
||||
hpf = style.paragraph_format
|
||||
hpf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||
hpf.first_line_indent = Cm(1.25)
|
||||
hpf.left_indent = Cm(0)
|
||||
hpf.right_indent = Cm(0)
|
||||
hpf.space_before = before
|
||||
hpf.space_after = Mm(10)
|
||||
hpf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
hpf.page_break_before = False
|
||||
hpf.keep_with_next = True
|
||||
hpf.widow_control = True
|
||||
|
||||
_apply_common_captions_and_misc(document)
|
||||
page = config.page.require_complete()
|
||||
pg.MARGIN_LEFT = Mm(page.left_mm)
|
||||
pg.MARGIN_RIGHT = Mm(page.right_mm)
|
||||
pg.MARGIN_TOP = Mm(page.top_mm)
|
||||
pg.MARGIN_BOTTOM = Mm(page.bottom_mm)
|
||||
|
||||
|
||||
def apply_document_styles(document: Document, style_preset: str = "mirea") -> None:
|
||||
if style_preset == "pis_custom":
|
||||
apply_pis_custom_styles(document)
|
||||
else:
|
||||
apply_mirea_styles(document)
|
||||
|
||||
|
||||
def _apply_common_page_and_body(document: Document) -> None:
|
||||
def apply_style_config(document: Document, config: StyleConfig) -> None:
|
||||
"""Apply full StyleConfig (page margins + paragraph styles) to document."""
|
||||
from .page_geometry import apply_section_geometry, is_landscape_section
|
||||
|
||||
_apply_page_margins(config)
|
||||
|
||||
for section in document.sections:
|
||||
# Do not wipe landscape sections created for +landscape figures/tables.
|
||||
apply_section_geometry(section, landscape=is_landscape_section(section))
|
||||
|
||||
_fix_toc_tab_stops(document)
|
||||
|
||||
normal: ParagraphStyle = document.styles["Normal"]
|
||||
_set_run_font(normal, "Times New Roman", 14)
|
||||
pf = normal.paragraph_format
|
||||
pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||
pf.first_line_indent = Cm(1.25)
|
||||
pf.left_indent = Cm(0)
|
||||
pf.right_indent = Cm(0)
|
||||
pf.space_before = Pt(0)
|
||||
pf.space_after = Pt(0)
|
||||
pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
pf.widow_control = True
|
||||
|
||||
|
||||
def _apply_common_captions_and_misc(document: Document) -> None:
|
||||
# --- Caption Figure: 12pt bold, center, under figure ---
|
||||
caption_fig = _ensure_style(document, "Caption Figure", "Caption")
|
||||
_set_run_font(caption_fig, "Times New Roman", 12, bold=True)
|
||||
cpf = caption_fig.paragraph_format
|
||||
cpf.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
cpf.first_line_indent = Cm(0)
|
||||
cpf.left_indent = Cm(0)
|
||||
cpf.space_before = Mm(0)
|
||||
cpf.space_after = Mm(6)
|
||||
cpf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
cpf.widow_control = True
|
||||
|
||||
# --- Caption Table (legacy EN name) + «Название таблицы» (основной) ---
|
||||
caption_tbl = _ensure_style(document, "Caption Table", "Caption")
|
||||
_set_run_font(caption_tbl, "Times New Roman", 12, italic=True)
|
||||
tpf = caption_tbl.paragraph_format
|
||||
tpf.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
tpf.first_line_indent = Cm(0)
|
||||
tpf.left_indent = Cm(0)
|
||||
tpf.space_before = Mm(6)
|
||||
tpf.space_after = Mm(0)
|
||||
tpf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
tpf.keep_with_next = True
|
||||
tpf.widow_control = True
|
||||
|
||||
# ГОСТ-имя стиля подписи таблицы (те же параметры, что Caption Table)
|
||||
caption_tbl_ru = _ensure_style(document, "Название таблицы", "Caption Table")
|
||||
_set_run_font(caption_tbl_ru, "Times New Roman", 12, italic=True)
|
||||
tpf_ru = caption_tbl_ru.paragraph_format
|
||||
tpf_ru.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
tpf_ru.first_line_indent = Cm(0)
|
||||
tpf_ru.left_indent = Cm(0)
|
||||
tpf_ru.space_before = Mm(6)
|
||||
tpf_ru.space_after = Mm(0)
|
||||
tpf_ru.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
tpf_ru.keep_with_next = True
|
||||
tpf_ru.widow_control = True
|
||||
|
||||
# --- Caption Listing (как таблицы) ---
|
||||
caption_lst = _ensure_style(document, "Caption Listing", "Caption")
|
||||
_set_run_font(caption_lst, "Times New Roman", 12, italic=True)
|
||||
lpf = caption_lst.paragraph_format
|
||||
lpf.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
lpf.first_line_indent = Cm(0)
|
||||
lpf.left_indent = Cm(0)
|
||||
lpf.space_before = Mm(6)
|
||||
lpf.space_after = Mm(0)
|
||||
lpf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
lpf.keep_with_next = True
|
||||
lpf.widow_control = True
|
||||
|
||||
caption = document.styles["Caption"]
|
||||
_set_run_font(caption, "Times New Roman", 12, bold=True)
|
||||
capf = caption.paragraph_format
|
||||
capf.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
capf.first_line_indent = Cm(0)
|
||||
capf.space_before = Mm(0)
|
||||
capf.space_after = Mm(6)
|
||||
capf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
|
||||
code = _ensure_style(document, "Code", "Normal")
|
||||
_set_run_font(code, "Courier New", 10)
|
||||
cdpf = code.paragraph_format
|
||||
cdpf.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
cdpf.first_line_indent = Cm(0)
|
||||
cdpf.left_indent = Cm(0)
|
||||
cdpf.space_before = Mm(0)
|
||||
cdpf.space_after = Mm(0)
|
||||
cdpf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
|
||||
table_text = _ensure_style(document, "Table Text", "Normal")
|
||||
_set_run_font(table_text, "Times New Roman", 12)
|
||||
ttf = table_text.paragraph_format
|
||||
ttf.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
ttf.first_line_indent = Cm(0)
|
||||
ttf.left_indent = Cm(0)
|
||||
ttf.space_before = Mm(0)
|
||||
ttf.space_after = Mm(0)
|
||||
ttf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
|
||||
biblio = _ensure_style(document, "Bibliography", "Normal")
|
||||
_set_run_font(biblio, "Times New Roman", 14)
|
||||
bpf = biblio.paragraph_format
|
||||
bpf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||
bpf.first_line_indent = Cm(1.25)
|
||||
bpf.space_before = Pt(0)
|
||||
bpf.space_after = Pt(0)
|
||||
bpf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
|
||||
biblio_h = _ensure_style(document, "Bibliography Heading", "Normal")
|
||||
_set_run_font(biblio_h, "Times New Roman", 14)
|
||||
biblio_h.font.all_caps = True
|
||||
bhpf = biblio_h.paragraph_format
|
||||
bhpf.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
bhpf.first_line_indent = Cm(0)
|
||||
bhpf.left_indent = Cm(1.25) # табл. 5.1
|
||||
bhpf.space_before = Mm(6)
|
||||
bhpf.space_after = Mm(6)
|
||||
bhpf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
bhpf.keep_with_next = True
|
||||
|
||||
# TOC styles: TNR 14, 1.5, no bold, no first-line indent; toc 1 = ALL CAPS
|
||||
for toc_name, all_caps in (("toc 1", True), ("toc 2", False), ("toc 3", False)):
|
||||
try:
|
||||
toc_style = document.styles[toc_name]
|
||||
except KeyError:
|
||||
toc_style = _ensure_style(document, toc_name, "Normal")
|
||||
_set_run_font(toc_style, "Times New Roman", 14, bold=False)
|
||||
toc_style.font.all_caps = all_caps
|
||||
toc_style.font.bold = False
|
||||
tpf_toc = toc_style.paragraph_format
|
||||
tpf_toc.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
tpf_toc.first_line_indent = Cm(0)
|
||||
tpf_toc.space_before = Pt(0)
|
||||
tpf_toc.space_after = Pt(0)
|
||||
tpf_toc.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
|
||||
try:
|
||||
footer_style = document.styles["Footer"]
|
||||
_set_run_font(footer_style, "Times New Roman", 12)
|
||||
footer_style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
footer_style.paragraph_format.first_line_indent = Cm(0)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for hyper_name in ("Hyperlink", "FollowedHyperlink"):
|
||||
try:
|
||||
hyper = document.styles[hyper_name]
|
||||
except KeyError:
|
||||
# Apply in a stable order: Normal first, then headings, then the rest
|
||||
order = [
|
||||
"Normal",
|
||||
"Heading 1", "Heading 2", "Heading 3",
|
||||
"Caption Figure", "Caption Table", "Название таблицы", "Caption Listing", "Caption",
|
||||
"Code", "Table Text",
|
||||
"Bibliography", "Bibliography Heading",
|
||||
"toc 1", "toc 2", "toc 3",
|
||||
"Footer", "Hyperlink", "FollowedHyperlink",
|
||||
"Space After Table",
|
||||
]
|
||||
applied = set()
|
||||
for name in order:
|
||||
spec = config.styles.get(name)
|
||||
if spec is None:
|
||||
continue
|
||||
_set_run_font(hyper, "Times New Roman", 14)
|
||||
hyper.font.underline = False
|
||||
style = _get_or_ensure(document, name)
|
||||
if style is None:
|
||||
continue
|
||||
_apply_paragraph_spec(style, spec)
|
||||
applied.add(name)
|
||||
|
||||
after = _ensure_style(document, "Space After Table", "Normal")
|
||||
apf = after.paragraph_format
|
||||
apf.space_before = Mm(6)
|
||||
apf.space_after = Pt(0)
|
||||
apf.first_line_indent = Cm(1.25)
|
||||
apf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
for name, spec in config.styles.items():
|
||||
if name in applied:
|
||||
continue
|
||||
style = _get_or_ensure(document, name)
|
||||
if style is None:
|
||||
continue
|
||||
_apply_paragraph_spec(style, spec)
|
||||
|
||||
|
||||
def apply_mirea_styles(document: Document) -> None:
|
||||
"""Mutate section margins and key paragraph styles to match the MIREA method guide."""
|
||||
apply_style_config(document, get_preset("mirea"))
|
||||
|
||||
|
||||
def apply_pis_custom_styles(document: Document) -> None:
|
||||
"""Styles for PIS_custom: итоговый отчёт по практическим работам."""
|
||||
apply_style_config(document, get_preset("pis_custom"))
|
||||
|
||||
|
||||
def apply_document_styles(
|
||||
document: Document,
|
||||
style_preset: str = "mirea",
|
||||
*,
|
||||
overlay: StyleConfig | None = None,
|
||||
md_dir: str | None = None,
|
||||
styles_path: str | None = None,
|
||||
) -> StyleConfig:
|
||||
"""Apply preset (+ optional JSON overlays). Returns the resolved StyleConfig."""
|
||||
if overlay is not None:
|
||||
config = get_preset(style_preset).merge(overlay)
|
||||
else:
|
||||
config = resolve_style_config(
|
||||
style_preset,
|
||||
md_dir=md_dir,
|
||||
styles_path=styles_path,
|
||||
)
|
||||
apply_style_config(document, config)
|
||||
return config
|
||||
|
||||
+11
-6
@@ -1,22 +1,27 @@
|
||||
# Промпты для генерации учебных работ
|
||||
|
||||
## Основной файл
|
||||
## Файлы
|
||||
|
||||
[`generate-mirea-report.md`](generate-mirea-report.md) — полный системный промпт (ГОСТ / МИРЭА).
|
||||
[`generate-pis-custom-report.md`](generate-pis-custom-report.md) — итоговый отчёт ПИС.
|
||||
| Файл | Назначение |
|
||||
|------|------------|
|
||||
| [`generate-md.md`](generate-md.md) | Диалект md2gost: отличия от обычного MD, диаграммы конвертируются сами |
|
||||
| [`generate-mirea-report.md`](generate-mirea-report.md) | Полный системный промпт (ГОСТ / МИРЭА) |
|
||||
| [`generate-pis-custom-report.md`](generate-pis-custom-report.md) | Итоговый отчёт ПИС |
|
||||
|
||||
В GUI (**Справка → Промпт для ИИ**) можно дописать к любому промпту выбранные UML-схемы из `md2gost.schemes.json`.
|
||||
|
||||
### Как использовать
|
||||
|
||||
1. Открой файл, скопируй всё начиная с роли («Ты — оформитель…») или весь файл.
|
||||
1. Открой файл или GUI, скопируй промпт (в GUI — после выбора схем).
|
||||
2. Вставь в:
|
||||
- **ChatGPT / Claude** — Custom instructions / первое сообщение;
|
||||
- **Cursor** — Rules for AI / сообщение агенту;
|
||||
- **GitHub Copilot Chat** — контекст чата.
|
||||
3. Следующим сообщением задай: тип (`coursework` | `practice` | `vkr`), тему, черновик.
|
||||
3. Следующим сообщением задай: тип (`coursework` | `practice` | `vkr` | `PIS_custom` | …), тему, черновик.
|
||||
4. Полученный `.md` проверь и сконвертируй:
|
||||
|
||||
```bash
|
||||
python -m md2gost report.md -o report.docx --type coursework --check --strict
|
||||
```
|
||||
|
||||
Каноническая копия промпта — файл в репозитории [`prompts/generate-mirea-report.md`](generate-mirea-report.md).
|
||||
Подробнее: [`docs/prompts.md`](../docs/prompts.md).
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# Промпт: Markdown для md2gost
|
||||
|
||||
Скопируйте **весь** текст ниже в системный промпт / первое сообщение. В GUI можно дописать выбранные UML-схемы кнопками — они добавятся в конец.
|
||||
|
||||
В следующем сообщении укажите тип работы, тему и черновик / факты.
|
||||
|
||||
---
|
||||
|
||||
Ты оформляешь учебный текст в **диалекте Markdown для конвертера md2gost** (Markdown → DOCX по ГОСТ / ТЗ МИРЭА и родственным профилям).
|
||||
|
||||
## Зачем не обычный Markdown
|
||||
|
||||
Обычный MD описывает структуру текста, но не несёт:
|
||||
|
||||
- спецразделов без номера («ВВЕДЕНИЕ», «ЗАКЛЮЧЕНИЕ»);
|
||||
- подписей «Рисунок 1.1 — …» / «Таблица …» / «Листинг …»;
|
||||
- перекрёстных ссылок на объекты;
|
||||
- библиографии в виде `[n]: …` с правилами ГОСТ;
|
||||
- диаграмм, которые конвертер сам превратит в Рисунок.
|
||||
|
||||
md2gost читает расширения ниже, нумерует объекты и собирает DOCX. Твоя задача — выдать **один готовый `.md`**, а не Word и не PNG.
|
||||
|
||||
## Выход
|
||||
|
||||
- Только markdown документа.
|
||||
- Без пояснений до и после, без обёртки в ```markdown```, без HTML/DOCX.
|
||||
|
||||
## Что писать как в обычном MD
|
||||
|
||||
Заголовки `#`…, абзацы, списки, таблицы `|…|`, картинки `![]()`, блоки кода, формулы `$$…$$`.
|
||||
|
||||
## Что писать иначе (обязательно)
|
||||
|
||||
### Спецразделы
|
||||
|
||||
```markdown
|
||||
# *СОДЕРЖАНИЕ
|
||||
[TOC]
|
||||
|
||||
# *ВВЕДЕНИЕ
|
||||
|
||||
# 1 Название раздела
|
||||
## 1.1 Подраздел
|
||||
|
||||
# *ЗАКЛЮЧЕНИЕ
|
||||
|
||||
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
|
||||
|
||||
# *ПРИЛОЖЕНИЯ
|
||||
```
|
||||
|
||||
Звёздочка `*` = раздел без номера. ПРОПИСНЫЕ. Точку в конце заголовка не ставить. После СОДЕРЖАНИЕ — `[TOC]`.
|
||||
|
||||
Для отчёта ПИС (`PIS_custom`) H1 работ без `*`: `# Практическая работа №N. Название`.
|
||||
|
||||
### Подписи и ссылки
|
||||
|
||||
- Рисунок-файл: `` + в тексте `@Рисунок:id`
|
||||
- Таблица / листинг / диаграмма: строка `%id Подпись` перед объектом + `@Таблица:id` / `@Листинг:id` / `@Рисунок:id`
|
||||
- Формула: `%eq1` + `$$…$$` + `@Формула:eq1` (номер только если есть ссылка)
|
||||
|
||||
В тексте: **«Рисунок»**, **«Таблица»**, **«Листинг»** — не «рис.» / «табл.»
|
||||
|
||||
Склеивание ячеек таблицы: `^` (rowspan сверху), `>` (colspan слева). Без графы «№ п/п».
|
||||
|
||||
Флаги в строке `%`: `+listing` (ещё Листинг с исходником), `+landscape` (альбомная страница).
|
||||
|
||||
### Источники
|
||||
|
||||
В тексте: `[1]`. В списке: `[1]: Автор. Название. — …`.
|
||||
Во введении и заключении ссылок `[n]` не должно быть.
|
||||
|
||||
### Запрещено
|
||||
|
||||
Сноски `[^1]`, «рис.», «табл.», `# ВВЕДЕНИЕ` без `*`, пустые подписи.
|
||||
|
||||
## Диаграммы: конвертер рисует сам
|
||||
|
||||
**Не** экспортируй UML/Mermaid в PNG вручную и **не** пиши «вставь картинку сюда».
|
||||
|
||||
В `.md` оставь fenced-блок — md2gost превратит его в **Рисунок** (PlantUML jar / Kroki; Mermaid — через Kroki).
|
||||
|
||||
| Оградка | Содержание тела |
|
||||
|---------|-----------------|
|
||||
| `` ```uml `` / `` ```plantuml `` | Полный PlantUML, обычно с `@startuml`…`@enduml` |
|
||||
| `` ```uml-<id> `` / `` ```<id> `` | Только макросы схемы (C4, BPMN, usecase…); **без** `@startuml` и **без** `!include` — схема добавит сама |
|
||||
| `` ```mermaid `` / `` ```mmd `` | Синтаксис Mermaid |
|
||||
|
||||
Перед блоком — `%id Подпись` (и ссылка `@Рисунок:id` в тексте).
|
||||
|
||||
**IDEF0** конвертер не рисует — только готовый PNG как обычный Рисунок.
|
||||
|
||||
Если в конец этого промпта добавлены блоки «Схема: …» — используй их макросы и оградки. Если блоков нет — для C4/BPMN/usecase всё равно можно писать `` ```uml-c4 ``, `` ```bpmn ``, `` ```usecase `` с типичными макросами; детали пользователь может дописать из GUI.
|
||||
|
||||
Пример схемы:
|
||||
|
||||
~~~markdown
|
||||
См. @Рисунок:arch1.
|
||||
|
||||
%arch1 Контейнеры системы
|
||||
|
||||
```uml-c4
|
||||
Person(user, "Студент")
|
||||
System(app, "Портал")
|
||||
Rel(user, app, "логин")
|
||||
```
|
||||
~~~
|
||||
|
||||
## Контроль перед выдачей
|
||||
|
||||
1. Структура соответствует типу работы (МИРЭА / ПИС / иное — из входа пользователя).
|
||||
2. У каждого `%id` / title картинки есть `@Тип:id` в тексте.
|
||||
3. Диаграммы — fence в md, не внешние PNG (кроме IDEF0 и готовых иллюстраций).
|
||||
4. Нет «рис.» и ссылок `[n]` во введении/заключении.
|
||||
|
||||
После проверки выдай только markdown.
|
||||
@@ -44,8 +44,17 @@ packages = ["md2gost", "md2latex", "md2fodt"]
|
||||
"md2gost/mml2omml" = "md2gost/mml2omml"
|
||||
"md2gost/diagrams" = "md2gost/diagrams"
|
||||
"latex/mirea" = "latex/mirea"
|
||||
"prompts/generate-md.md" = "md2gost/prompts/generate-md.md"
|
||||
"prompts/generate-mirea-report.md" = "md2gost/prompts/generate-mirea-report.md"
|
||||
"prompts/generate-pis-custom-report.md" = "md2gost/prompts/generate-pis-custom-report.md"
|
||||
"docs/README.md" = "md2gost/docs/README.md"
|
||||
"docs/quickstart.md" = "md2gost/docs/quickstart.md"
|
||||
"docs/markdown.md" = "md2gost/docs/markdown.md"
|
||||
"docs/schemes.md" = "md2gost/docs/schemes.md"
|
||||
"docs/types.md" = "md2gost/docs/types.md"
|
||||
"docs/cli-gui.md" = "md2gost/docs/cli-gui.md"
|
||||
"docs/styles.md" = "md2gost/docs/styles.md"
|
||||
"docs/prompts.md" = "md2gost/docs/prompts.md"
|
||||
|
||||
# Legacy Poetry metadata (optional; prefer uv + [project] above)
|
||||
[tool.poetry]
|
||||
|
||||
@@ -153,11 +153,49 @@ def test_prompt_catalog_loads():
|
||||
from md2gost.help_content import load_prompt_catalog
|
||||
catalog = load_prompt_catalog()
|
||||
names = {name for name, _title, text in catalog}
|
||||
assert "generate-md.md" in names
|
||||
assert "generate-mirea-report.md" in names
|
||||
assert "generate-pis-custom-report.md" in names
|
||||
assert all(text.strip() for _n, _t, text in catalog)
|
||||
|
||||
|
||||
def test_compose_prompt_with_schemes():
|
||||
from md2gost.diagram_schemes import DiagramScheme
|
||||
from md2gost.help_content import compose_prompt, scheme_prompt_block
|
||||
|
||||
scheme = DiagramScheme(
|
||||
id="c4",
|
||||
title="C4 Container",
|
||||
docs='Person(alias, "Label")',
|
||||
ai_prompt="Рисуй C4. Блок ```uml-c4 без @startuml.",
|
||||
)
|
||||
block = scheme_prompt_block(scheme)
|
||||
assert "uml-c4" in block
|
||||
assert "```c4" in block or "```uml-c4" in block
|
||||
assert "Рисуй C4" in block
|
||||
assert "Person(alias" in block
|
||||
|
||||
base = "Базовый промпт про markdown.\n"
|
||||
out = compose_prompt(base, [scheme])
|
||||
assert out.startswith("Базовый промпт")
|
||||
assert "Дополнение: выбранные схемы" in out
|
||||
assert "uml-c4" in out
|
||||
assert compose_prompt(base, []).strip() == base.strip()
|
||||
assert compose_prompt(base, None).strip() == base.strip()
|
||||
|
||||
|
||||
def test_docs_catalog_loads():
|
||||
from md2gost.help_content import load_docs_catalog
|
||||
catalog = load_docs_catalog()
|
||||
assert catalog, "docs/ catalog should not be empty"
|
||||
names = {name for name, _title, _text in catalog}
|
||||
assert "README.md" in names
|
||||
assert "markdown.md" in names
|
||||
assert catalog[0][0].lower() == "readme.md"
|
||||
assert "Документация" in catalog[0][1]
|
||||
assert all(text.strip() for _n, _t, text in catalog)
|
||||
|
||||
|
||||
def test_listing_continuation_mode():
|
||||
import os
|
||||
import docx
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Tests for JSON style overlay (md2gost.styles.json / --styles)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import docx
|
||||
import pytest
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
|
||||
from md2gost.style_config import (
|
||||
StyleConfigError,
|
||||
get_preset,
|
||||
resolve_style_config,
|
||||
style_config_from_dict,
|
||||
)
|
||||
from md2gost.styles import apply_document_styles, apply_style_config
|
||||
from md2gost import page_geometry as pg
|
||||
|
||||
|
||||
TEMPLATE = Path("md2gost/Template.docx")
|
||||
|
||||
|
||||
def test_preset_unchanged_without_overlay():
|
||||
cfg = resolve_style_config("mirea")
|
||||
assert cfg.page.left_mm == 30
|
||||
assert cfg.page.right_mm == 10
|
||||
h1 = cfg.styles["Heading 1"]
|
||||
assert h1.alignment == "left"
|
||||
assert h1.size_pt == 18
|
||||
assert h1.all_caps is True
|
||||
|
||||
|
||||
def test_overlay_changes_margins_and_h1(tmp_path: Path):
|
||||
overlay = {
|
||||
"page": {"left_mm": 20, "right_mm": 20},
|
||||
"styles": {
|
||||
"Heading 1": {
|
||||
"alignment": "center",
|
||||
"size_pt": 14,
|
||||
"left_indent_cm": 0,
|
||||
}
|
||||
},
|
||||
}
|
||||
path = tmp_path / "custom.json"
|
||||
path.write_text(json.dumps(overlay), encoding="utf-8")
|
||||
|
||||
cfg = resolve_style_config("mirea", styles_path=path)
|
||||
assert cfg.page.left_mm == 20
|
||||
assert cfg.page.right_mm == 20
|
||||
# Unspecified margins stay from preset
|
||||
assert cfg.page.top_mm == 20
|
||||
assert cfg.page.bottom_mm == 20
|
||||
assert cfg.styles["Heading 1"].alignment == "center"
|
||||
assert cfg.styles["Heading 1"].size_pt == 14
|
||||
# Unspecified heading fields stay from preset
|
||||
assert cfg.styles["Heading 1"].all_caps is True
|
||||
assert cfg.styles["Heading 1"].bold is True
|
||||
|
||||
|
||||
def test_unknown_style_name_raises():
|
||||
with pytest.raises(StyleConfigError, match="Unknown style name"):
|
||||
style_config_from_dict({"styles": {"Heading 9": {"size_pt": 12}}})
|
||||
|
||||
|
||||
def test_unknown_page_key_raises():
|
||||
with pytest.raises(StyleConfigError, match="Unknown page key"):
|
||||
style_config_from_dict({"page": {"gutter_mm": 5}})
|
||||
|
||||
|
||||
def test_unknown_style_field_raises():
|
||||
with pytest.raises(StyleConfigError, match="Unknown style field"):
|
||||
style_config_from_dict({"styles": {"Normal": {"color": "red"}}})
|
||||
|
||||
|
||||
def test_cli_overlay_wins_over_near_md(tmp_path: Path):
|
||||
near = tmp_path / "md2gost.styles.json"
|
||||
near.write_text(
|
||||
json.dumps({"page": {"left_mm": 25}, "styles": {"Heading 1": {"size_pt": 16}}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
cli = tmp_path / "cli.json"
|
||||
cli.write_text(
|
||||
json.dumps({"page": {"left_mm": 15}, "styles": {"Heading 1": {"alignment": "center"}}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
cfg = resolve_style_config("mirea", md_dir=tmp_path, styles_path=cli)
|
||||
assert cfg.page.left_mm == 15
|
||||
assert cfg.styles["Heading 1"].size_pt == 16 # from near-md, not overridden
|
||||
assert cfg.styles["Heading 1"].alignment == "center" # from CLI
|
||||
|
||||
|
||||
def test_apply_overlay_to_document(tmp_path: Path):
|
||||
overlay = style_config_from_dict({
|
||||
"page": {"left_mm": 20, "right_mm": 20, "top_mm": 20, "bottom_mm": 20},
|
||||
"styles": {"Heading 1": {"alignment": "center", "size_pt": 14}},
|
||||
})
|
||||
doc = docx.Document(str(TEMPLATE))
|
||||
apply_document_styles(doc, "mirea", overlay=overlay)
|
||||
h1 = doc.styles["Heading 1"]
|
||||
assert h1.paragraph_format.alignment == WD_ALIGN_PARAGRAPH.CENTER
|
||||
assert abs(h1.font.size.pt - 14) < 0.01
|
||||
section = doc.sections[0]
|
||||
assert abs(section.left_margin.mm - 20) < 0.1
|
||||
assert abs(pg.MARGIN_LEFT.mm - 20) < 0.1
|
||||
|
||||
|
||||
def test_pis_preset_h1_centered():
|
||||
doc = docx.Document(str(TEMPLATE))
|
||||
apply_style_config(doc, get_preset("pis_custom"))
|
||||
h1 = doc.styles["Heading 1"]
|
||||
assert h1.paragraph_format.alignment == WD_ALIGN_PARAGRAPH.CENTER
|
||||
assert h1.font.all_caps is True
|
||||
Reference in New Issue
Block a user