update 0.4.4
Python application / build (push) Has been cancelled

- update документация
- промт для ии полу конфигурируемый
This commit is contained in:
Igor20264
2026-09-06 11:04:01 +03:00
parent 638fd38d7f
commit 818a044aa1
26 changed files with 1987 additions and 457 deletions
+4
View File
@@ -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
+23 -198
View File
@@ -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/)
+17
View File
@@ -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).
+58
View File
@@ -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/
```
+153
View File
@@ -0,0 +1,153 @@
# Синтаксис Markdown (диалект md2gost)
Обычный Markdown описывает структуру текста. Для отчёта по ГОСТ нужны ещё: спецразделы без номера, подписи объектов, перекрёстные ссылки, библиография в заданном виде. **md2gost** расширяет MD ровно этими элементами; конвертер сам нумерует рисунки/таблицы/листинги и оформляет DOCX.
## Что совпадает с обычным MD
- Заголовки `#``######`
- Абзацы, **жирный**, *курсив*
- Маркированные и нумерованные списки
- Таблицы `| … |`
- Картинки `![alt](path.png)`
- Блоки кода в ограде ` ```язык `
- Формулы `$$ … $$` (и инлайн `$…$` где поддерживается)
## Зачем расширения
| Задача | Обычный 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.
![описание](images/arch.png "%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]`
- формулы обычным текстом вместо `$$…$$`
- нумерация спецразделов (`# ВВЕДЕНИЕ` вместо `# *ВВЕДЕНИЕ`)
+33
View File
@@ -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)
+61
View File
@@ -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
View File
@@ -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`.
+67
View File
@@ -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: табы оглавления (считаются от ширины полосы), отступы списков, формулы из шаблона, цвет (всегда чёрный).
+66
View File
@@ -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 Варианты развития архитектуры …
Источники: 720.
```bash
python -m md2gost report.md -o report.docx --type APID_coursework --check --title title.docx --assignment assignment.docx
```
+21
View File
@@ -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
}
}
}
+1
View File
@@ -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():
+1
View 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
View File
@@ -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)
+9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 13, подписи). В шаблон 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
View File
@@ -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),
+597
View File
@@ -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
View File
@@ -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
View File
@@ -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).
+116
View File
@@ -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. Название`.
### Подписи и ссылки
- Рисунок-файл: `![…](file.png "%id Подпись")` + в тексте `@Рисунок: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.
+9
View File
@@ -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]
+38
View File
@@ -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
+114
View File
@@ -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